diff --git a/bun.lockb b/bun.lockb index 9e16cf7..c8e45ab 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 53e14c1..65ebe5d 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@types/cookie": "^0.6.0", "@types/qs": "^6.9.15", "@types/react-calendar-heatmap": "^1.6.7", + "@vis.gl/react-google-maps": "^1.4.2", "axios": "^1.7.4", "chrono-node": "^1.4.9", "class-variance-authority": "^0.7.0", diff --git a/src/components/shared/Sidebar.tsx b/src/components/shared/Sidebar.tsx index f37830e..61b3401 100644 --- a/src/components/shared/Sidebar.tsx +++ b/src/components/shared/Sidebar.tsx @@ -5,7 +5,6 @@ import { cn } from "@/lib/utils"; import { useRouter } from "next/router"; import dynamic from "next/dynamic"; -import Image from "next/image"; import ProfileInfo from "./ProfileInfo"; const ThemeSwitcher = dynamic(() => import("@/components/shared/ThemeSwitcher"), { diff --git a/src/config/constants/sidebarNavs.tsx b/src/config/constants/sidebarNavs.tsx index 93ba057..f5c58e9 100644 --- a/src/config/constants/sidebarNavs.tsx +++ b/src/config/constants/sidebarNavs.tsx @@ -1,4 +1,4 @@ -import { Album, GalleryHorizontal, GalleryVerticalEnd, Home, Image, LocateOff, MapPinX, Search, User } from "lucide-react"; +import { Album, GalleryHorizontal, GalleryVerticalEnd, Home, Image, LocateOff, MapPin, MapPinX, Search, User } from "lucide-react"; export const sidebarNavs = [ { @@ -31,5 +31,10 @@ export const sidebarNavs = [ link: "/albums", icon: , }, + { + title: "Geo Heatmap", + link: "/assets/geo-heatmap", + icon: , + } ]; diff --git a/src/config/environment.ts b/src/config/environment.ts index a6ac9d1..692c635 100644 --- a/src/config/environment.ts +++ b/src/config/environment.ts @@ -7,6 +7,7 @@ export const ENV = { SECURE_COOKIE: process.env.SECURE_COOKIE === 'true', VERSION: process.env.VERSION, GEMINI_API_KEY: process.env.GEMINI_API_KEY as string, + GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY as string, }; diff --git a/src/config/routes.ts b/src/config/routes.ts index 8d9bffb..3e25fc0 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -34,6 +34,7 @@ export const UPDATE_ASSETS_PATH = BASE_PROXY_ENDPOINT + "/assets"; export const ASSET_THUMBNAIL_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id; export const ASSET_PREVIEW_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id + "?size=preview"; export const ASSET_VIDEO_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/video/" + id; +export const ASSET_GEO_HEATMAP_PATH = BASE_API_ENDPOINT + "/assets/geo-heatmap"; // Location diff --git a/src/contexts/ConfigContext.tsx b/src/contexts/ConfigContext.tsx index a633fb0..7558664 100644 --- a/src/contexts/ConfigContext.tsx +++ b/src/contexts/ConfigContext.tsx @@ -5,6 +5,7 @@ export interface ConfigContextType { exImmichUrl: string; version?: string; geminiEnabled: boolean; + googleMapsApiKey: string; } const ConfigContext = createContext({ diff --git a/src/handlers/api/asset.handler.ts b/src/handlers/api/asset.handler.ts index b04cced..279f0f7 100644 --- a/src/handlers/api/asset.handler.ts +++ b/src/handlers/api/asset.handler.ts @@ -1,5 +1,6 @@ import { ADD_ASSETS_ALBUMS_PATH, + ASSET_GEO_HEATMAP_PATH, FIND_ASSETS, LIST_ALBUMS_PATH, LIST_MISSING_LOCATION_ASSETS_PATH, @@ -50,4 +51,8 @@ export const updateAssets = async (params: IUpdateAssetsParams) => { export const findAssets = async (query: string) => { return API.post(FIND_ASSETS, { query }); +} + +export const getAssetGeoHeatmap = async () => { + return API.get(ASSET_GEO_HEATMAP_PATH); } \ No newline at end of file diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index d9569e6..649a4b7 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -30,6 +30,7 @@ App.getInitialProps = async () => { immichURL: ENV.IMMICH_URL, version: ENV.VERSION, geminiEnabled: !!ENV.GEMINI_API_KEY?.length, + googleMapsApiKey: ENV.GOOGLE_MAPS_API_KEY, }, }; }; diff --git a/src/pages/api/assets/geo-heatmap.ts b/src/pages/api/assets/geo-heatmap.ts new file mode 100644 index 0000000..ba5870a --- /dev/null +++ b/src/pages/api/assets/geo-heatmap.ts @@ -0,0 +1,33 @@ +import { NextApiResponse } from "next"; + +import { db } from "@/config/db"; +import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; +import { NextApiRequest } from "next"; +import { assets, exif } from "@/schema"; +import { and, eq, isNotNull } from "drizzle-orm"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const currentUser = await getCurrentUser(req); + if (!currentUser) { + return res.status(401).json({ message: 'Unauthorized' }); + } + + const dbAssets = await db.select({ + assetId: assets.id, + latitude: exif.latitude, + longitude: exif.longitude, + }).from(assets) + .innerJoin(exif, eq(assets.id, exif.assetId)) + .where( + and( + eq(assets.ownerId, currentUser.id), + isNotNull(exif.latitude), + isNotNull(exif.longitude) + ) + ); + const heatmapData = dbAssets.map((asset) => [ + asset.longitude, + asset.latitude, + ]); + res.status(200).json(heatmapData); +} diff --git a/src/pages/assets/geo-heatmap.tsx b/src/pages/assets/geo-heatmap.tsx new file mode 100644 index 0000000..44a9136 --- /dev/null +++ b/src/pages/assets/geo-heatmap.tsx @@ -0,0 +1,96 @@ +import Header from '@/components/shared/Header' +import { useConfig } from '@/contexts/ConfigContext'; +import React, { useEffect, useMemo, useState } from 'react' +import { useMap, useMapsLibrary } from '@vis.gl/react-google-maps'; +import { APIProvider, Map } from '@vis.gl/react-google-maps'; +import PageLayout from '@/components/layouts/PageLayout'; +import { getAssetGeoHeatmap } from '@/handlers/api/asset.handler'; +import { useTheme } from 'next-themes'; +import { MapPinX } from 'lucide-react'; + + +const HeatMap = () => { + + const map = useMap(); + const visualization = useMapsLibrary('visualization'); + + const heatmap = useMemo(() => { + if (!visualization) return null; + + return new google.maps.visualization.HeatmapLayer({ + radius: 20, + opacity: 0.5 + }); + + }, [visualization]); + + useEffect(() => { + if (!heatmap) return; + + getAssetGeoHeatmap().then((data) => { + console.log(data); + heatmap.setData(data.map(([lng, lat]: [number, number]) => ({ + location: new google.maps.LatLng(lat, lng), + weight: 10 + }))); + }); + + }, [heatmap]); + + useEffect(() => { + if (!heatmap) return; + + heatmap.setMap(map); + + return () => { + heatmap.setMap(null); + }; + }, [heatmap, map]); + + return null; +} + +export default function GeoHeatmap() { + const { googleMapsApiKey } = useConfig(); + const { theme } = useTheme(); + const [mapId, setMapId] = useState('7a9e2ebecd32a903'); + + useEffect(() => { + if (theme === 'light') { + setMapId(""); + } else { + setMapId('7a9e2ebecd32a903'); + } + }, [theme]); + + + return ( + + + + {googleMapsApiKey ? ( + + + + + + ) : ( + + + No Google Maps API key found + + Please create and add your Google Maps API key in the env file under GOOGLE_MAPS_API_KEY. + + + )} + + + ) +}
No Google Maps API key found
+ Please create and add your Google Maps API key in the env file under GOOGLE_MAPS_API_KEY. +