import { Fragment, useEffect, useRef } from 'react'; import L from 'leaflet'; import { Circle, GeoJSON, Marker, Tooltip, useMap, useMapEvents } from 'react-leaflet'; import '@geoman-io/leaflet-geoman-free'; import '@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css'; import type { GeoJsonLineString, GeoJsonMultiPolygon, GeoJsonPoint } from '@patrol/shared'; import { PERIMETER_STYLE, ROUTE_STYLE, numberIcon } from './base'; export type EditorCheckpoint = { key: string; name: string; location: GeoJsonPoint; radiusM: number }; export type EditorMode = 'path' | 'points' | 'view'; // Линия маршрута — через geoman: нарисовать, поправить вершины, удалить. Всегда одна линия. function PathLayer({ value, onChange, mode }: { value: GeoJsonLineString | null; onChange: (v: GeoJsonLineString | null) => void; mode: EditorMode }) { const map = useMap(); const line = useRef(null); const changeRef = useRef(onChange); changeRef.current = onChange; useEffect(() => { const emit = () => { const l = line.current; if (!l) return changeRef.current(null); const geo = l.toGeoJSON().geometry as GeoJSON.LineString; changeRef.current(geo.coordinates.length >= 2 ? { type: 'LineString', coordinates: geo.coordinates as [number, number][] } : null); }; const attach = (l: L.Polyline) => { line.current = l; l.setStyle(ROUTE_STYLE); l.on('pm:edit pm:dragend', emit); }; if (value) attach(L.polyline(value.coordinates.map(([lng, lat]) => [lat, lng] as [number, number])).addTo(map)); map.pm.setLang('ru'); map.pm.setGlobalOptions({ pathOptions: ROUTE_STYLE, snappable: true }); const onCreate = (e: { shape: string; layer: L.Layer }) => { if (e.shape !== 'Line') return; line.current?.remove(); attach(e.layer as L.Polyline); emit(); }; const onRemove = (e: { layer: L.Layer }) => { if (e.layer === line.current) { line.current = null; emit(); } }; map.on('pm:create', onCreate); map.on('pm:remove', onRemove); return () => { map.off('pm:create', onCreate); map.off('pm:remove', onRemove); map.pm.removeControls(); map.pm.disableDraw(); map.pm.disableGlobalEditMode(); line.current?.remove(); line.current = null; }; // Инициализация один раз на маршрут (key у редактора). // eslint-disable-next-line react-hooks/exhaustive-deps }, [map]); // Панель geoman видна только в режиме «линия», чтобы клики в режиме «точки» не путались с рисованием. useEffect(() => { map.pm.removeControls(); map.pm.disableDraw(); map.pm.disableGlobalEditMode(); if (mode === 'path') { map.pm.addControls({ position: 'topleft', drawMarker: false, drawCircleMarker: false, drawPolygon: false, drawRectangle: false, drawCircle: false, drawText: false, drawPolyline: true, cutPolygon: false, rotateMode: false, dragMode: false, editMode: true, removalMode: true, }); } if (line.current) line.current.options.pmIgnore = mode !== 'path'; }, [map, mode]); return null; } function ClickToAdd({ enabled, onAdd }: { enabled: boolean; onAdd: (p: GeoJsonPoint) => void }) { const map = useMapEvents({ click(e) { if (!enabled || map.pm.globalDrawModeEnabled()) return; onAdd({ type: 'Point', coordinates: [Number(e.latlng.lng.toFixed(7)), Number(e.latlng.lat.toFixed(7))] }); }, }); useEffect(() => { const el = map.getContainer(); el.style.cursor = enabled ? 'crosshair' : ''; return () => { el.style.cursor = ''; }; }, [map, enabled]); return null; } export function RouteEditor({ perimeter, path, onPathChange, checkpoints, onCheckpointsChange, mode, selected, onSelect, }: { perimeter: GeoJsonMultiPolygon | null; path: GeoJsonLineString | null; onPathChange: (v: GeoJsonLineString | null) => void; checkpoints: EditorCheckpoint[]; onCheckpointsChange: (v: EditorCheckpoint[]) => void; mode: EditorMode; selected: number | null; onSelect: (i: number | null) => void; }) { const add = (location: GeoJsonPoint) => { const next = [...checkpoints, { key: crypto.randomUUID(), name: `Точка ${checkpoints.length + 1}`, location, radiusM: 30 }]; onCheckpointsChange(next); onSelect(next.length - 1); }; const move = (i: number, latlng: L.LatLng) => { onCheckpointsChange( checkpoints.map((c, j) => (j === i ? { ...c, location: { type: 'Point', coordinates: [Number(latlng.lng.toFixed(7)), Number(latlng.lat.toFixed(7))] } } : c)), ); }; return ( <> {perimeter && } {checkpoints.map((c, i) => { const pos: [number, number] = [c.location.coordinates[1], c.location.coordinates[0]]; return ( { L.DomEvent.stopPropagation(e); onSelect(i); }, dragend: (e) => move(i, (e.target as L.Marker).getLatLng()), }} > {c.name} ); })} ); }