diff --git a/admin/inertia/components/maps/MapComponent.tsx b/admin/inertia/components/maps/MapComponent.tsx index 18812b3..468470f 100644 --- a/admin/inertia/components/maps/MapComponent.tsx +++ b/admin/inertia/components/maps/MapComponent.tsx @@ -10,7 +10,7 @@ import maplibregl from 'maplibre-gl' import 'maplibre-gl/dist/maplibre-gl.css' import { Protocol } from 'pmtiles' -import { useEffect, useRef, useState, useCallback } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useMapMarkers, PIN_COLORS } from '~/hooks/useMapMarkers' @@ -23,25 +23,82 @@ import MapMarkerFormPopup from './MapMarkerFormPopup' type ScaleUnit = 'imperial' | 'metric' +type MapCommand = { + id: number + lat: number + lng: number + action: 'fly' | 'marker' +} + type MapComponentProps = { - isHoveringUI: boolean - showCoordinatesEnabled: boolean + mapCommand?: MapCommand | null + isHoveringUI?: boolean + showCoordinatesEnabled?: boolean +} + +type MapLocationParams = { + lat: number + lng: number + zoom: number +} + +const getMapLocationParams = (): MapLocationParams | null => { + const params = new URLSearchParams(window.location.search) + + const lat = Number(params.get('lat')) + const lngParam = params.get('lng') + const longParam = params.get('long') + const lng = Number(lngParam ?? longParam) + const zoom = Number(params.get('zoom') ?? 12) + + if ( + !Number.isFinite(lat) || + !Number.isFinite(lng) || + lat < -90 || + lat > 90 || + lng < -180 || + lng > 180 + ) { + return null + } + + if (!lngParam && longParam) { + params.set('lng', longParam) + params.delete('long') + + const query = params.toString() + window.history.replaceState( + null, + '', + `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash}` + ) + } + + return { + lat, + lng, + zoom: Number.isFinite(zoom) ? zoom : 12, + } } export default function MapComponent({ - isHoveringUI, - showCoordinatesEnabled, + mapCommand, + isHoveringUI = false, + showCoordinatesEnabled = true, }: MapComponentProps) { const mapRef = useRef(null) const animationFrameRef = useRef(null) + const handledMapCommandIdRef = useRef(null) const { markers, addMarker, updateMarker, deleteMarker } = useMapMarkers() + const [targetIndicator, setTargetIndicator] = useState<{ lng: number; lat: number } | null>(null) const [isDraggingMap, setIsDraggingMap] = useState(false) const [placingMarker, setPlacingMarker] = useState<{ lng: number; lat: number } | null>(null) const [selectedMarkerId, setSelectedMarkerId] = useState(null) const [editingMarkerId, setEditingMarkerId] = useState(null) const [hasUnsavedMarkerChanges, setHasUnsavedMarkerChanges] = useState(false) + const [showCoordinates, setShowCoordinates] = useState(false) const [scaleUnit, setScaleUnit] = useState( () => (localStorage.getItem('nomad:map-scale-unit') as ScaleUnit) || 'metric' @@ -54,14 +111,27 @@ export default function MapComponent({ y: number } | null>(null) - const [showCoordinates, setShowCoordinates] = useState(false) - const confirmDiscardMarkerChanges = useCallback(() => { if (!hasUnsavedMarkerChanges) return true - return window.confirm('Discard unsaved marker changes?') }, [hasUnsavedMarkerChanges]) + const hideCoordinates = useCallback(() => { + setShowCoordinates(false) + 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) @@ -79,16 +149,77 @@ export default function MapComponent({ } }, []) - const hideCoordinates = useCallback(() => { - setShowCoordinates(false) - setCursorLngLat(null) - }, []) + useEffect(() => { + if (!mapCommand) return + if (handledMapCommandIdRef.current === mapCommand.id) return + + handledMapCommandIdRef.current = mapCommand.id + + if (mapCommand.action === 'fly') { + const currentZoom = mapRef.current?.getZoom() ?? 12 + + setTargetIndicator({ + lng: mapCommand.lng, + lat: mapCommand.lat, + }) + + mapRef.current?.flyTo({ + center: [mapCommand.lng, mapCommand.lat], + zoom: currentZoom, + duration: 1500, + }) + + return + } + + if (mapCommand.action === 'marker') { + if (!confirmDiscardMarkerChanges()) return + + setTargetIndicator(null) + + const currentZoom = mapRef.current?.getZoom() ?? 12 + + mapRef.current?.flyTo({ + center: [mapCommand.lng, mapCommand.lat], + zoom: currentZoom, + duration: 750, + }) + + window.setTimeout(() => { + setPlacingMarker({ + lng: mapCommand.lng, + lat: mapCommand.lat, + }) + + setSelectedMarkerId(null) + setEditingMarkerId(null) + setHasUnsavedMarkerChanges(false) + }, 750) + } + }, [mapCommand, confirmDiscardMarkerChanges]) const handleScaleUnitChange = useCallback((unit: ScaleUnit) => { setScaleUnit(unit) localStorage.setItem('nomad:map-scale-unit', unit) }, []) + const handleMapLoad = useCallback(() => { + flyToLocationParams() + }, [flyToLocationParams]) + + const handleMapClick = useCallback( + (e: MapLayerMouseEvent) => { + if (!confirmDiscardMarkerChanges()) return + + setPlacingMarker({ lng: e.lngLat.lng, lat: e.lngLat.lat }) + setSelectedMarkerId(null) + setEditingMarkerId(null) + setHasUnsavedMarkerChanges(false) + setTargetIndicator(null) + }, + [confirmDiscardMarkerChanges] + ) + const handleMouseMove = useCallback( (e: MapLayerMouseEvent) => { const target = e.originalEvent.target as HTMLElement | null @@ -120,31 +251,15 @@ export default function MapComponent({ [hideCoordinates, isHoveringUI, isDraggingMap, showCoordinatesEnabled] ) - const handleMapClick = useCallback( - (e: MapLayerMouseEvent) => { - if (!confirmDiscardMarkerChanges()) return - - setPlacingMarker({ lng: e.lngLat.lng, lat: e.lngLat.lat }) - setSelectedMarkerId(null) - setEditingMarkerId(null) - setHasUnsavedMarkerChanges(false) - }, - [confirmDiscardMarkerChanges] - ) - const handleFlyTo = useCallback((longitude: number, latitude: number) => { + setTargetIndicator(null) mapRef.current?.flyTo({ center: [longitude, latitude], zoom: 12, duration: 1500 }) }, []) const handleDeleteMarker = useCallback( (id: number) => { - if (selectedMarkerId === id) { - setSelectedMarkerId(null) - } - - if (editingMarkerId === id) { - setEditingMarkerId(null) - } + if (selectedMarkerId === id) setSelectedMarkerId(null) + if (editingMarkerId === id) setEditingMarkerId(null) deleteMarker(id) }, @@ -187,6 +302,7 @@ export default function MapComponent({ latitude: 40, zoom: 3.5, }} + onLoad={handleMapLoad} onMouseDown={() => { setIsDraggingMap(true) hideCoordinates() @@ -210,12 +326,6 @@ export default function MapComponent({ - - {showCoordinates && cursorLngLat && ( )} + {targetIndicator && ( + +