"use client"; /** * ConceptDrawMap — Leaflet draw-polygon input for the parcel boundary. * * Uses the imperative `L.Draw.Polygon` handler (leaflet-draw) inside a * `useMap()` child rather than the react-leaflet-draw wrapper (not a repo dep). * The drawn ring is emitted upward as a GeoJSON Polygon (WGS84) for * ConceptInput.parcel_geojson. Re-using the react-leaflet + OSM TileLayer * pattern from EntryMap / SiteMap. Lazy-mounted (ssr:false) by the page — * Leaflet touches `window`. */ import { useEffect, useRef, useState } from "react"; import { MapContainer, TileLayer, GeoJSON, useMap } from "react-leaflet"; import L from "leaflet"; import "leaflet-draw"; import type { Polygon } from "geojson"; import { Pencil, Trash2 } from "lucide-react"; import "leaflet/dist/leaflet.css"; import "leaflet-draw/dist/leaflet.draw.css"; // Yekaterinburg center — matches EntryMap default view. const EKB_CENTER: [number, number] = [56.8389, 60.6057]; const DEFAULT_ZOOM = 13; // ── Draw handler child (must live inside MapContainer for useMap) ───────────── interface DrawLayerProps { /** Drawing requested by the parent toolbar (toggled on each click). */ drawTick: number; /** Clear requested by the parent toolbar. */ clearTick: number; onPolygon: (polygon: Polygon) => void; onCleared: () => void; } function DrawLayer({ drawTick, clearTick, onPolygon, onCleared, }: DrawLayerProps) { const map = useMap(); const groupRef = useRef(null); const handlerRef = useRef(null); // One FeatureGroup holds the single drawn polygon; wire the CREATED event. useEffect(() => { const group = new L.FeatureGroup(); group.addTo(map); groupRef.current = group; const onCreated = (e: L.LeafletEvent) => { const { layer } = e as L.DrawEvents.Created; // Don't add the layer to the FeatureGroup: the polygon is controlled by // the parent via the `polygon` prop, which renders it as a . // Adding it here would draw a second overlapping copy of the same ring. const gj = (layer as L.Polygon).toGeoJSON(); if (gj.type === "Feature" && gj.geometry.type === "Polygon") { onPolygon(gj.geometry); } }; map.on(L.Draw.Event.CREATED, onCreated); return () => { map.off(L.Draw.Event.CREATED, onCreated); group.remove(); }; // map identity is stable for the MapContainer lifetime; onPolygon is stable. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Start the polygon draw handler when the parent toolbar bumps drawTick. useEffect(() => { if (drawTick === 0) return; handlerRef.current?.disable(); const handler = new L.Draw.Polygon(map as L.DrawMap, { allowIntersection: false, showArea: false, shapeOptions: { color: "#1d4ed8", weight: 2.5, fillColor: "#3b82f6", fillOpacity: 0.2, }, }); handlerRef.current = handler; handler.enable(); return () => { handler.disable(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [drawTick]); // Clear the drawn polygon when the parent toolbar bumps clearTick. useEffect(() => { if (clearTick === 0) return; handlerRef.current?.disable(); groupRef.current?.clearLayers(); onCleared(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [clearTick]); return null; } // ── Component ───────────────────────────────────────────────────────────────── interface Props { /** Externally provided polygon (e.g. resolved from a cadastre number). */ polygon: Polygon | null; onChange: (polygon: Polygon | null) => void; height?: number; } export function ConceptDrawMap({ polygon, onChange, height = 420 }: Props) { // Tick counters bumped by the toolbar buttons drive the DrawLayer effects // (start-draw / clear) without exposing a Leaflet ref to the parent. const [drawTick, setDrawTick] = useState(0); const [clearTick, setClearTick] = useState(0); function handleDraw() { setDrawTick((t) => t + 1); } function handleClear() { setClearTick((t) => t + 1); onChange(null); } return (
{/* Externally supplied polygon (cadastre → geom). Keyed on the ring so react-leaflet remounts the layer when the geometry changes. */} {polygon && ( )} {}} /> {/* Toolbar — primary draw + clear; overlays the map top-left. */}
{polygon && ( )}
); }