Skip to content

Commit

Permalink
feat: added google heatmap
Browse files Browse the repository at this point in the history
Signed-off-by: Varun Raj <varun@skcript.com>
  • Loading branch information
varun-raj committed Dec 30, 2024
1 parent 31e5879 commit 842e323
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 2 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion src/components/shared/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"), {
Expand Down
7 changes: 6 additions & 1 deletion src/config/constants/sidebarNavs.tsx
Original file line number Diff line number Diff line change
@@ -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 = [
{
Expand Down Expand Up @@ -31,5 +31,10 @@ export const sidebarNavs = [
link: "/albums",
icon: <GalleryHorizontal className="h-4 w-4" />,
},
{
title: "Geo Heatmap",
link: "/assets/geo-heatmap",
icon: <MapPin className="h-4 w-4" />,
}

];
1 change: 1 addition & 0 deletions src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};


1 change: 1 addition & 0 deletions src/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/contexts/ConfigContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface ConfigContextType {
exImmichUrl: string;
version?: string;
geminiEnabled: boolean;
googleMapsApiKey: string;
}

const ConfigContext = createContext<ConfigContextType>({
Expand Down
5 changes: 5 additions & 0 deletions src/handlers/api/asset.handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ADD_ASSETS_ALBUMS_PATH,
ASSET_GEO_HEATMAP_PATH,
FIND_ASSETS,
LIST_ALBUMS_PATH,
LIST_MISSING_LOCATION_ASSETS_PATH,
Expand Down Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
};
Expand Down
33 changes: 33 additions & 0 deletions src/pages/api/assets/geo-heatmap.ts
Original file line number Diff line number Diff line change
@@ -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);
}
96 changes: 96 additions & 0 deletions src/pages/assets/geo-heatmap.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<PageLayout className="!p-0 !mb-0">
<Header leftComponent="Geo Heatmap" />
<div className='h-full w-full'>
{googleMapsApiKey ? (
<APIProvider apiKey={googleMapsApiKey}>
<Map
defaultCenter={{ lat: 0, lng: 0 }}
mapId={mapId}
defaultZoom={2}
gestureHandling={'greedy'}
disableDefaultUI={true}
className='h-full w-full'
/>

<HeatMap />
</APIProvider>
) : (
<div className='h-full w-full flex items-center justify-center flex-col gap-2'>
<MapPinX className='w-10 h-10 text-muted-foreground' />
<p className='text-muted-foreground'>No Google Maps API key found</p>
<p className='text-muted-foreground'>
Please create and add your Google Maps API key in the env file under <kbd className='bg-zinc-200 text-black dark:text-white px-1 py-0.5 rounded-md dark:bg-zinc-500'>GOOGLE_MAPS_API_KEY</kbd>.
</p>
</div>
)}
</div>
</PageLayout>
)
}

0 comments on commit 842e323

Please sign in to comment.