diff --git a/admin/inertia/components/maps/MapComponent.tsx b/admin/inertia/components/maps/MapComponent.tsx index 922713d..b346e4b 100644 --- a/admin/inertia/components/maps/MapComponent.tsx +++ b/admin/inertia/components/maps/MapComponent.tsx @@ -42,13 +42,22 @@ type MapLocationParams = { zoom: number } -const getMapLocationParams = (): MapLocationParams | null => { +const SAVED_MAP_VIEW_KEY = 'nomad:map-view' + +export const getMapLocationParams = (): MapLocationParams | null => { const params = new URLSearchParams(window.location.search) - const lat = Number(params.get('lat')) + const latParam = params.get('lat') const lngParam = params.get('lng') const longParam = params.get('long') - const lng = Number(lngParam ?? longParam) + const effectiveLng = lngParam ?? longParam + + // Both lat and lng must be present and non-empty. Number(null)/Number('') + // both coerce to 0, which would silently fly to (0,0) — null island. + if (!latParam || !effectiveLng) return null + + const lat = Number(latParam) + const lng = Number(effectiveLng) const zoom = Number(params.get('zoom') ?? 12) if ( @@ -81,6 +90,35 @@ const getMapLocationParams = (): MapLocationParams | null => { } } +const getSavedMapView = (): { longitude: number; latitude: number; zoom: number } | null => { + try { + const raw = localStorage.getItem(SAVED_MAP_VIEW_KEY) + if (!raw) return null + + const parsed = JSON.parse(raw) + if ( + typeof parsed === 'object' && + parsed !== null && + Number.isFinite(parsed.longitude) && + Number.isFinite(parsed.latitude) && + Number.isFinite(parsed.zoom) && + parsed.latitude >= -90 && + parsed.latitude <= 90 && + parsed.longitude >= -180 && + parsed.longitude <= 180 + ) { + return { + longitude: parsed.longitude, + latitude: parsed.latitude, + zoom: parsed.zoom, + } + } + } catch { + // ignore — fall through to default + } + return null +} + export default function MapComponent({ mapCommand, isHoveringUI = false, @@ -111,6 +149,24 @@ export default function MapComponent({ y: number } | null>(null) + // Resolve the initial map view once at mount: URL params → saved view → default. + // Lazy useState so we don't recompute on every render. + const [initialViewState] = useState(() => { + const urlParams = getMapLocationParams() + if (urlParams) { + return { + longitude: urlParams.lng, + latitude: urlParams.lat, + zoom: urlParams.zoom, + } + } + + const saved = getSavedMapView() + if (saved) return saved + + return { longitude: -101, latitude: 40, zoom: 3.5 } + }) + const confirmDiscardMarkerChanges = useCallback(() => { if (!hasUnsavedMarkerChanges) return true return window.confirm('Discard unsaved marker changes?') @@ -121,17 +177,6 @@ export default function MapComponent({ setCursorLngLat(null) }, []) - const flyToLocationParams = useCallback(() => { - const location = getMapLocationParams() - if (!location) return - - mapRef.current?.flyTo({ - center: [location.lng, location.lat], - zoom: location.zoom, - duration: 1500, - }) - }, []) - useEffect(() => { const protocol = new Protocol() maplibregl.addProtocol('pmtiles', protocol.tile) @@ -203,10 +248,6 @@ export default function MapComponent({ localStorage.setItem('nomad:map-scale-unit', unit) }, []) - const handleMapLoad = useCallback(() => { - flyToLocationParams() - }, [flyToLocationParams]) - const handleMapClick = useCallback( (e: MapLayerMouseEvent) => { if (!confirmDiscardMarkerChanges()) return @@ -297,12 +338,21 @@ export default function MapComponent({ cursor={isDraggingMap ? 'grabbing' : 'crosshair'} mapStyle={`${window.location.protocol}//${window.location.hostname}:${window.location.port}/api/maps/styles`} mapLib={maplibregl} - initialViewState={{ - longitude: -101, - latitude: 40, - zoom: 3.5, + initialViewState={initialViewState} + onMoveEnd={(e) => { + try { + localStorage.setItem( + SAVED_MAP_VIEW_KEY, + JSON.stringify({ + longitude: e.viewState.longitude, + latitude: e.viewState.latitude, + zoom: e.viewState.zoom, + }) + ) + } catch { + // ignore — quota / privacy mode + } }} - onLoad={handleMapLoad} onMouseDown={() => { setIsDraggingMap(true) hideCoordinates() diff --git a/admin/inertia/pages/maps.tsx b/admin/inertia/pages/maps.tsx index 1dcfffb..e043463 100644 --- a/admin/inertia/pages/maps.tsx +++ b/admin/inertia/pages/maps.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import MapsLayout from '~/layouts/MapsLayout' import { Head, Link, router } from '@inertiajs/react' -import MapComponent from '~/components/maps/MapComponent' +import MapComponent, { getMapLocationParams } from '~/components/maps/MapComponent' import StyledButton from '~/components/StyledButton' import { IconArrowLeft, IconMapPin, IconPlaneTilt } from '@tabler/icons-react' import { FileEntry } from '../../types/files' @@ -18,7 +18,12 @@ type MapCommand = { export default function Maps(props: { maps: { baseAssetsExist: boolean; regionFiles: FileEntry[] } }) { - const [coordinateSearch, setCoordinateSearch] = useState('') + // Pre-fill search box from URL params so users landing at /maps?lat=X&lng=Y + // can immediately click the marker button to drop a pin without re-typing. + const [coordinateSearch, setCoordinateSearch] = useState(() => { + const params = getMapLocationParams() + return params ? `${params.lat},${params.lng}` : '' + }) const [mapCommand, setMapCommand] = useState(null) const [showCoordinatesEnabled, setShowCoordinatesEnabled] = useState(true)