feat: map view with GPS extraction fix

Adds a new Map sidebar entry that plots photos by their EXIF GPS
coordinates on a clustered Leaflet map. While wiring this up, the
metadata extractor was reading unprefixed GPS keys that never exist
in `exiftool -G -j` output AND assumed coordinates were already
floats — every photo silently lost its GPS. The new extract_gps
helper handles Composite/EXIF group prefixes and parses DMS strings,
and lat/lon are stored as first-class indexed columns so the map
can query them without parsing exif_json on every request.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 23:44:29 +02:00
parent 9c9f5bd899
commit 7cf546af7a
17 changed files with 529 additions and 46 deletions

View File

@@ -1,8 +1,10 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useHotkeys } from 'react-hotkeys-hook'
import { X, Info } from 'lucide-react'
import { usePhotoStore } from '../../store/photoStore'
import { usePhotosQuery } from '../../hooks/usePhotosQuery'
import { photos as photosApi } from '../../services/api'
import type { Photo } from '../../types/photo'
import { PreviewImage } from './PreviewImage'
import { PreviewFilmstrip } from './PreviewFilmstrip'
@@ -43,7 +45,24 @@ export function PreviewView() {
? photos.findIndex((p) => p.id === activePhotoId)
: 0
const safeIndex = currentIndex < 0 ? 0 : currentIndex
const currentPhoto: Photo | undefined = photos[safeIndex]
// Fallback fetch: when the preview is opened for a photo that isn't in
// the timeline query result (e.g. clicked from the Map view, where the
// active section's filter excludes it), look it up by id directly.
// PhotoInfoPanel runs the same query under the same key, so they share
// one cache entry — no extra request.
const photoInListById = activePhotoId
? photos.find((p) => p.id === activePhotoId)
: undefined
const { data: standalonePhoto } = useQuery<Photo>({
queryKey: ['photo', activePhotoId],
queryFn: () => photosApi.get(activePhotoId as string),
enabled: !!activePhotoId && !photoInListById,
staleTime: 60_000,
})
const currentPhoto: Photo | undefined =
photoInListById ?? photos[safeIndex] ?? standalonePhoto
// Keep the latest photos array + active id in a ref so the keyboard
// handlers ALWAYS read the freshest state. Without this, react-hotkeys-