gendesign/frontend/src/components/concept/ConceptDrawMap.tsx
Light1YT 86e9ea2937 fix(week-review): автофиксы код-ревью — 169 issue (label «week ревью 1»)
Многоагентный аудит + имплементация: один воркер на файл, точечные правки.
Верификация: 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>
2026-06-15 20:21:11 +05:00

227 lines
7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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='&copy; <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>
);
}