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/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",
|
||||
|
||||
@@ -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"), {
|
||||
|
||||
@@ -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: <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',
|
||||
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,
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface ConfigContextType {
|
||||
exImmichUrl: string;
|
||||
version?: string;
|
||||
geminiEnabled: boolean;
|
||||
googleMapsApiKey: string;
|
||||
}
|
||||
|
||||
const ConfigContext = createContext<ConfigContextType>({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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