import { useCallback, useEffect, useMemo, useRef, type RefObject } from 'react'; import { CircleMarker, MapContainer, TileLayer, useMap, useMapEvents } from 'react-leaflet'; import { Box, Text } from '@mantine/core'; import { OSM_ATTRIBUTION, OSM_TILE_URL } from './osm'; import { DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM, locationFromLatLng, SELECTED_MAP_ZOOM, toLocationLatLng, } from './location-point'; import 'leaflet/dist/leaflet.css'; import './leaflet-stacking.css'; export interface LocationMapProps { latitude?: unknown; longitude?: unknown; onChange?: (point: { latitude: number; longitude: number }) => void; height?: number; emptyLabel?: string; helperLabel?: string; defaultCenter?: [number, number]; defaultZoom?: number; tileUrl?: string; attribution?: string; } function SyncMapView({ latitude, longitude, lastPickedRef, }: { latitude: unknown; longitude: unknown; lastPickedRef: RefObject<{ latitude: number; longitude: number } | null>; }) { const map = useMap(); const point = toLocationLatLng(latitude, longitude); const lat = point?.[0] ?? null; const lng = point?.[1] ?? null; const hasCentered = useRef(false); useEffect(() => { const id = window.setTimeout(() => map.invalidateSize(), 0); return () => window.clearTimeout(id); }, [map]); useEffect(() => { if (lat === null || lng === null) return; const picked = lastPickedRef.current; if (picked && picked.latitude === lat && picked.longitude === lng) { return; } if (!hasCentered.current) { hasCentered.current = true; map.setView([lat, lng], SELECTED_MAP_ZOOM); return; } map.panTo([lat, lng]); }, [map, lat, lng, lastPickedRef]); return null; } function MapClickPicker({ onPick }: { onPick: (lat: number, lng: number) => void }) { const onPickRef = useRef(onPick); onPickRef.current = onPick; const handlers = useMemo( () => ({ click(event: { latlng: { lat: number; lng: number } }) { onPickRef.current(event.latlng.lat, event.latlng.lng); }, }), [], ); useMapEvents(handlers); return null; } export function LocationMap({ latitude, longitude, onChange, height = 280, emptyLabel = 'No location', helperLabel, defaultCenter = DEFAULT_MAP_CENTER, defaultZoom = DEFAULT_MAP_ZOOM, tileUrl = OSM_TILE_URL, attribution = OSM_ATTRIBUTION, }: LocationMapProps) { const interactive = Boolean(onChange); const position = toLocationLatLng(latitude, longitude); const lastPickedRef = useRef<{ latitude: number; longitude: number } | null>(null); const center = position ?? defaultCenter; const zoom = position ? SELECTED_MAP_ZOOM : defaultZoom; const handlePick = useCallback( (lat: number, lng: number) => { const point = locationFromLatLng(lat, lng); lastPickedRef.current = point; onChange?.(point); }, [onChange], ); if (!interactive && !position) { return ( {emptyLabel} ); } return ( {position ? ( ) : null} {onChange ? : null} {interactive && helperLabel ? ( {helperLabel} ) : null} ); }