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}
);
}