feat: added google heatmap
Signed-off-by: Varun Raj <varun@skcript.com>
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/qs": "^6.9.15",
|
"@types/qs": "^6.9.15",
|
||||||
"@types/react-calendar-heatmap": "^1.6.7",
|
"@types/react-calendar-heatmap": "^1.6.7",
|
||||||
|
"@vis.gl/react-google-maps": "^1.4.2",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.4",
|
||||||
"chrono-node": "^1.4.9",
|
"chrono-node": "^1.4.9",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { cn } from "@/lib/utils";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Image from "next/image";
|
|
||||||
import ProfileInfo from "./ProfileInfo";
|
import ProfileInfo from "./ProfileInfo";
|
||||||
|
|
||||||
const ThemeSwitcher = dynamic(() => import("@/components/shared/ThemeSwitcher"), {
|
const ThemeSwitcher = dynamic(() => import("@/components/shared/ThemeSwitcher"), {
|
||||||
|
|||||||
@@ -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 = [
|
export const sidebarNavs = [
|
||||||
{
|
{
|
||||||
@@ -31,5 +31,10 @@ export const sidebarNavs = [
|
|||||||
link: "/albums",
|
link: "/albums",
|
||||||
icon: <GalleryHorizontal className="h-4 w-4" />,
|
icon: <GalleryHorizontal className="h-4 w-4" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Geo Heatmap",
|
||||||
|
link: "/assets/geo-heatmap",
|
||||||
|
icon: <MapPin className="h-4 w-4" />,
|
||||||
|
}
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const ENV = {
|
|||||||
SECURE_COOKIE: process.env.SECURE_COOKIE === 'true',
|
SECURE_COOKIE: process.env.SECURE_COOKIE === 'true',
|
||||||
VERSION: process.env.VERSION,
|
VERSION: process.env.VERSION,
|
||||||
GEMINI_API_KEY: process.env.GEMINI_API_KEY as string,
|
GEMINI_API_KEY: process.env.GEMINI_API_KEY as string,
|
||||||
|
GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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_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_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_VIDEO_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/video/" + id;
|
||||||
|
export const ASSET_GEO_HEATMAP_PATH = BASE_API_ENDPOINT + "/assets/geo-heatmap";
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface ConfigContextType {
|
|||||||
exImmichUrl: string;
|
exImmichUrl: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
geminiEnabled: boolean;
|
geminiEnabled: boolean;
|
||||||
|
googleMapsApiKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigContext = createContext<ConfigContextType>({
|
const ConfigContext = createContext<ConfigContextType>({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ADD_ASSETS_ALBUMS_PATH,
|
ADD_ASSETS_ALBUMS_PATH,
|
||||||
|
ASSET_GEO_HEATMAP_PATH,
|
||||||
FIND_ASSETS,
|
FIND_ASSETS,
|
||||||
LIST_ALBUMS_PATH,
|
LIST_ALBUMS_PATH,
|
||||||
LIST_MISSING_LOCATION_ASSETS_PATH,
|
LIST_MISSING_LOCATION_ASSETS_PATH,
|
||||||
@@ -50,4 +51,8 @@ export const updateAssets = async (params: IUpdateAssetsParams) => {
|
|||||||
|
|
||||||
export const findAssets = async (query: string) => {
|
export const findAssets = async (query: string) => {
|
||||||
return API.post(FIND_ASSETS, { query });
|
return API.post(FIND_ASSETS, { query });
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAssetGeoHeatmap = async () => {
|
||||||
|
return API.get(ASSET_GEO_HEATMAP_PATH);
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,7 @@ App.getInitialProps = async () => {
|
|||||||
immichURL: ENV.IMMICH_URL,
|
immichURL: ENV.IMMICH_URL,
|
||||||
version: ENV.VERSION,
|
version: ENV.VERSION,
|
||||||
geminiEnabled: !!ENV.GEMINI_API_KEY?.length,
|
geminiEnabled: !!ENV.GEMINI_API_KEY?.length,
|
||||||
|
googleMapsApiKey: ENV.GOOGLE_MAPS_API_KEY,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user