Многоагентный аудит + имплементация: один воркер на файл, точечные правки. Верификация: py_compile (47/47 .py) + tsc --noEmit (0 ошибок). Unit-тесты не прогонялись (окружение не поднято: rollup native dep / нет pytest-venv). Полностью исправлено (169): #1336, #1337, #1339, #1340, #1341, #1342, #1343, #1345, #1346, #1348, #1349, #1350, #1351, #1354, #1356, #1358, #1359, #1360, #1362, #1364, #1365, #1366, #1367, #1368, #1369, #1370, #1371, #1372, #1373, #1374, #1375, #1376, #1377, #1378, #1379, #1380, #1381, #1382, #1384, #1385, #1386, #1387, #1388, #1389, #1390, #1391, #1392, #1394, #1395, #1396, #1397, #1399, #1400, #1401, #1402, #1403, #1404, #1408, #1409, #1410, #1411, #1412, #1413, #1414, #1415, #1416, #1417, #1418, #1420, #1423, #1425, #1426, #1427, #1428, #1429, #1430, #1431, #1432, #1433, #1434, #1435, #1437, #1438, #1439, #1440, #1441, #1442, #1443, #1444, #1445, #1446, #1447, #1448, #1449, #1450, #1451, #1452, #1453, #1454, #1455, #1456, #1457, #1458, #1459, #1460, #1461, #1462, #1463, #1464, #1465, #1466, #1467, #1468, #1469, #1471, #1472, #1473, #1474, #1476, #1478, #1479, #1481, #1482, #1483, #1484, #1485, #1487, #1488, #1489, #1490, #1491, #1492, #1493, #1494, #1495, #1496, #1497, #1499, #1500, #1501, #1502, #1504, #1505, #1506, #1507, #1510, #1514, #1515, #1516, #1517, #1518, #1519, #1521, #1522, #1523, #1524, #1525, #1526, #1527, #1528, #1529, #1531, #1532, #1533, #1534, #1535, #1536, #1537, #1538 Частично (9, in-file часть, остаток cross-file): #1361, #1419, #1422, #1424, #1470, #1475, #1477, #1480, #1498 Требуют cross-file (3, не тронуты): #1338, #1363, #1421 Пропущено (1): #1539 Не входило в партию: 22 needs-Leha issue (нужны решения владельца). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
227 lines
7 KiB
TypeScript
227 lines
7 KiB
TypeScript
"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<L.FeatureGroup | null>(null);
|
||
const handlerRef = useRef<L.Draw.Polygon | null>(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 <GeoJSON>.
|
||
// 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 (
|
||
<div style={{ position: "relative", width: "100%", height }}>
|
||
<MapContainer
|
||
center={EKB_CENTER}
|
||
zoom={DEFAULT_ZOOM}
|
||
style={{ width: "100%", height: "100%", borderRadius: 12 }}
|
||
scrollWheelZoom
|
||
>
|
||
<TileLayer
|
||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||
/>
|
||
{/* Externally supplied polygon (cadastre → geom). Keyed on the ring so
|
||
react-leaflet remounts the layer when the geometry changes. */}
|
||
{polygon && (
|
||
<GeoJSON
|
||
key={JSON.stringify(polygon.coordinates[0]?.[0] ?? [])}
|
||
data={polygon}
|
||
style={{
|
||
color: "#1d4ed8",
|
||
weight: 2.5,
|
||
fillColor: "#3b82f6",
|
||
fillOpacity: 0.2,
|
||
}}
|
||
/>
|
||
)}
|
||
<DrawLayer
|
||
drawTick={drawTick}
|
||
clearTick={clearTick}
|
||
onPolygon={onChange}
|
||
onCleared={() => {}}
|
||
/>
|
||
</MapContainer>
|
||
|
||
{/* Toolbar — primary draw + clear; overlays the map top-left. */}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
top: 12,
|
||
left: 12,
|
||
zIndex: 1000,
|
||
display: "flex",
|
||
gap: 8,
|
||
}}
|
||
>
|
||
<button
|
||
type="button"
|
||
onClick={handleDraw}
|
||
aria-label="Нарисовать полигон участка"
|
||
style={{
|
||
display: "inline-flex",
|
||
alignItems: "center",
|
||
gap: 6,
|
||
padding: "6px 12px",
|
||
height: 32,
|
||
background: "var(--accent)",
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: 6,
|
||
fontSize: 13,
|
||
fontWeight: 500,
|
||
cursor: "pointer",
|
||
}}
|
||
>
|
||
<Pencil size={14} strokeWidth={1.5} />
|
||
Нарисовать участок
|
||
</button>
|
||
{polygon && (
|
||
<button
|
||
type="button"
|
||
onClick={handleClear}
|
||
aria-label="Очистить полигон"
|
||
style={{
|
||
display: "inline-flex",
|
||
alignItems: "center",
|
||
gap: 6,
|
||
padding: "6px 12px",
|
||
height: 32,
|
||
background: "var(--bg-card)",
|
||
color: "var(--fg-secondary)",
|
||
border: "1px solid var(--border-card)",
|
||
borderRadius: 6,
|
||
fontSize: 13,
|
||
fontWeight: 500,
|
||
cursor: "pointer",
|
||
}}
|
||
>
|
||
<Trash2 size={14} strokeWidth={1.5} />
|
||
Очистить
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|