import Map, { FullscreenControl, NavigationControl, ScaleControl, Marker, Popup, MapProvider, } from 'react-map-gl/maplibre' import type { MapRef, MapLayerMouseEvent } from 'react-map-gl/maplibre' import maplibregl from 'maplibre-gl' import 'maplibre-gl/dist/maplibre-gl.css' import { Protocol } from 'pmtiles' import { useEffect, useRef, useState, useCallback } from 'react' import { useMapMarkers, PIN_COLORS } from '~/hooks/useMapMarkers' import type { PinColorId } from '~/hooks/useMapMarkers' import MarkerPin from './MarkerPin' import MarkerPanel from './MarkerPanel' import CoordinateOverlay from './CoordinateOverlay' import ScaleUnitToggle from './ScaleUnitToggle' type ScaleUnit = 'imperial' | 'metric' type MapComponentProps = { isHoveringUI: boolean showCoordinatesEnabled: boolean } const SAVED_MAP_VIEW_KEY = 'nomad:map-view' const DEFAULT_MAP_VIEW = { longitude: -101, latitude: 40, zoom: 3.5 } type SavedMapView = { longitude: number; latitude: number; zoom: number } // Restore the last map position/zoom from localStorage so a refresh of /maps doesn't snap back // to the default US-wide view. Bounds-checked so a corrupt or out-of-range value falls through // to the default instead of throwing. const getSavedMapView = (): SavedMapView | null => { try { const raw = localStorage.getItem(SAVED_MAP_VIEW_KEY) if (!raw) return null const parsed = JSON.parse(raw) if ( parsed && typeof parsed === 'object' && 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({ isHoveringUI, showCoordinatesEnabled, }: MapComponentProps) { const mapRef = useRef(null) const animationFrameRef = useRef(null) const { markers, addMarker, deleteMarker } = useMapMarkers() const [isDraggingMap, setIsDraggingMap] = useState(false) const [placingMarker, setPlacingMarker] = useState<{ lng: number; lat: number } | null>(null) const [markerName, setMarkerName] = useState('') const [markerColor, setMarkerColor] = useState('orange') const [selectedMarkerId, setSelectedMarkerId] = useState(null) const [scaleUnit, setScaleUnit] = useState( () => (localStorage.getItem('nomad:map-scale-unit') as ScaleUnit) || 'metric' ) // Resolve the initial view once at mount: saved view → default. Lazy so it isn't recomputed // on every render. const [initialViewState] = useState(() => getSavedMapView() ?? DEFAULT_MAP_VIEW) const [cursorLngLat, setCursorLngLat] = useState<{ lng: number lat: number x: number y: number } | null>(null) const [showCoordinates, setShowCoordinates] = useState(false) useEffect(() => { const protocol = new Protocol() maplibregl.addProtocol('pmtiles', protocol.tile) return () => { maplibregl.removeProtocol('pmtiles') } }, []) useEffect(() => { return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current) } } }, []) const hideCoordinates = useCallback(() => { setShowCoordinates(false) setCursorLngLat(null) }, []) const handleScaleUnitChange = useCallback((unit: ScaleUnit) => { setScaleUnit(unit) localStorage.setItem('nomad:map-scale-unit', unit) }, []) const handleMouseMove = useCallback( (e: MapLayerMouseEvent) => { const target = e.originalEvent.target as HTMLElement | null if ( !showCoordinatesEnabled || isHoveringUI || isDraggingMap || target?.closest('.maplibregl-control-container, .maplibregl-ctrl') ) { hideCoordinates() return } if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current) } animationFrameRef.current = requestAnimationFrame(() => { setShowCoordinates(true) setCursorLngLat({ lng: e.lngLat.lng, lat: e.lngLat.lat, x: e.point.x, y: e.point.y, }) }) }, [hideCoordinates, isHoveringUI, isDraggingMap, showCoordinatesEnabled] ) const handleMapClick = useCallback((e: MapLayerMouseEvent) => { setPlacingMarker({ lng: e.lngLat.lng, lat: e.lngLat.lat }) setMarkerName('') setMarkerColor('orange') setSelectedMarkerId(null) }, []) const handleSaveMarker = useCallback(() => { if (placingMarker && markerName.trim()) { addMarker(markerName.trim(), placingMarker.lng, placingMarker.lat, markerColor) setPlacingMarker(null) setMarkerName('') setMarkerColor('orange') } }, [placingMarker, markerName, markerColor, addMarker]) const handleFlyTo = useCallback((longitude: number, latitude: number) => { mapRef.current?.flyTo({ center: [longitude, latitude], zoom: 12, duration: 1500 }) }, []) const handleDeleteMarker = useCallback( (id: number) => { if (selectedMarkerId === id) setSelectedMarkerId(null) deleteMarker(id) }, [selectedMarkerId, deleteMarker] ) const selectedMarker = selectedMarkerId ? markers.find((m) => m.id === selectedMarkerId) : null return (
{ setIsDraggingMap(false) hideCoordinates() }} onMouseMoveCapture={(e) => { const target = e.target as HTMLElement | null if ( target?.closest( '.maplibregl-control-container, .maplibregl-ctrl, .maplibregl-ctrl-group, .maplibregl-ctrl-scale' ) ) { hideCoordinates() } }} > { // Persist the view so a refresh restores where the user was, not the default. const { longitude, latitude, zoom } = e.viewState try { localStorage.setItem( SAVED_MAP_VIEW_KEY, JSON.stringify({ longitude, latitude, zoom }) ) } catch { // ignore persistence failures (private mode, quota) } }} onMouseDown={() => { setIsDraggingMap(true) hideCoordinates() }} onMouseUp={() => { setIsDraggingMap(false) }} onDragStart={() => { setIsDraggingMap(true) hideCoordinates() }} onDragEnd={() => { setIsDraggingMap(false) hideCoordinates() }} onClick={handleMapClick} onMouseMove={handleMouseMove} onMouseLeave={hideCoordinates} > {showCoordinates && cursorLngLat && ( )} {markers.map((marker) => ( { e.originalEvent.stopPropagation() setSelectedMarkerId(marker.id === selectedMarkerId ? null : marker.id) setPlacingMarker(null) }} > c.id === marker.color)?.hex} active={marker.id === selectedMarkerId} /> ))} {selectedMarker && ( setSelectedMarkerId(null)} closeOnClick={false} >
{selectedMarker.name}
{selectedMarker.notes && selectedMarker.notes.trim() && (
{selectedMarker.notes}
)}
)} {placingMarker && ( setPlacingMarker(null)} closeOnClick={false} >
setMarkerName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleSaveMarker() if (e.key === 'Escape') setPlacingMarker(null) }} className="block w-full rounded border border-gray-300 px-2 py-1 text-sm placeholder:text-gray-400 focus:outline-none focus:border-gray-500" />
{PIN_COLORS.map((c) => ( ))}
)}
) }