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:
@@ -49,9 +49,20 @@ interface PhotoDetails {
|
||||
user_notes: string | null
|
||||
color_label: string | null
|
||||
exif_json: string | null
|
||||
latitude?: number | null
|
||||
longitude?: number | null
|
||||
tags?: PhotoTagSummary[]
|
||||
}
|
||||
|
||||
/** Format a signed decimal degree value with the hemisphere letter, e.g.
|
||||
* ``48.12777° N``. Keeps the panel readable without dragging in a heavy
|
||||
* formatting lib. */
|
||||
function formatLatLon(value: number, axis: 'lat' | 'lon'): string {
|
||||
const abs = Math.abs(value).toFixed(5)
|
||||
const ref = axis === 'lat' ? (value >= 0 ? 'N' : 'S') : (value >= 0 ? 'E' : 'W')
|
||||
return `${abs}° ${ref}`
|
||||
}
|
||||
|
||||
interface ExifData {
|
||||
Make?: string
|
||||
Model?: string
|
||||
@@ -547,13 +558,20 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro
|
||||
expanded={expandedSections.has('location')}
|
||||
onToggle={() => toggleSection('location')}
|
||||
>
|
||||
{exif.GPSLatitude && exif.GPSLongitude ? (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{photo.latitude != null && photo.longitude != null ? (
|
||||
<a
|
||||
href={`https://www.openstreetmap.org/?mlat=${photo.latitude}&mlon=${photo.longitude}#map=15/${photo.latitude}/${photo.longitude}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-2 text-xs hover:underline"
|
||||
title="Open in OpenStreetMap"
|
||||
>
|
||||
<MapPin className="h-3 w-3 text-text-muted" />
|
||||
<span className="font-mono text-text">
|
||||
{String(exif.GPSLatitude)}, {String(exif.GPSLongitude)}
|
||||
{formatLatLon(photo.latitude, 'lat')},{' '}
|
||||
{formatLatLon(photo.longitude, 'lon')}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<div className="text-xs text-text-muted">No GPS data</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user