Многоагентный аудит + имплементация: один воркер на файл, точечные правки. Верификация: 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>
150 lines
4.3 KiB
TypeScript
150 lines
4.3 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* ConceptResultMap — renders one variant's building placement: the parcel
|
||
* polygon (outline) + the generated buildings FeatureCollection (filled), over
|
||
* an OSM base. Re-uses the react-leaflet + GeoJSON pattern from SiteMap.
|
||
* Lazy-mounted (ssr:false) by the parent — Leaflet touches `window`.
|
||
*/
|
||
|
||
import { useEffect, useMemo } from "react";
|
||
import { MapContainer, TileLayer, GeoJSON, useMap } from "react-leaflet";
|
||
import type { FeatureCollection, Polygon, Position } from "geojson";
|
||
import L from "leaflet";
|
||
|
||
import "leaflet/dist/leaflet.css";
|
||
|
||
// ── Bounds helper ─────────────────────────────────────────────────────────────
|
||
|
||
function ringBounds(rings: Position[][]): L.LatLngBoundsExpression | null {
|
||
let minLat = Infinity;
|
||
let maxLat = -Infinity;
|
||
let minLon = Infinity;
|
||
let maxLon = -Infinity;
|
||
for (const ring of rings) {
|
||
for (const [lon, lat] of ring) {
|
||
if (lat < minLat) minLat = lat;
|
||
if (lat > maxLat) maxLat = lat;
|
||
if (lon < minLon) minLon = lon;
|
||
if (lon > maxLon) maxLon = lon;
|
||
}
|
||
}
|
||
if (!Number.isFinite(minLat)) return null;
|
||
return [
|
||
[minLat, minLon],
|
||
[maxLat, maxLon],
|
||
];
|
||
}
|
||
|
||
interface FitBoundsProps {
|
||
bounds: L.LatLngBoundsExpression | null;
|
||
}
|
||
|
||
// react-leaflet does not auto-fit to data; do it imperatively once mounted.
|
||
function FitBounds({ bounds }: FitBoundsProps) {
|
||
const map = useMap();
|
||
useEffect(() => {
|
||
if (bounds) {
|
||
map.fitBounds(bounds, { padding: [24, 24] });
|
||
}
|
||
}, [map, bounds]);
|
||
return null;
|
||
}
|
||
|
||
// ── Component ─────────────────────────────────────────────────────────────────
|
||
|
||
interface Props {
|
||
parcel: Polygon;
|
||
buildings: FeatureCollection;
|
||
/** Keying makes react-leaflet remount the buildings layer on variant switch. */
|
||
variantKey: string;
|
||
height?: number;
|
||
}
|
||
|
||
export function ConceptResultMap({
|
||
parcel,
|
||
buildings,
|
||
variantKey,
|
||
height = 360,
|
||
}: Props) {
|
||
// Memoize so FitBounds only re-fits when the parcel geometry actually
|
||
// changes — recomputing inline returns a new array each render, which would
|
||
// make the fitBounds effect fire on every re-render and discard manual zoom/pan.
|
||
const bounds = useMemo(
|
||
() => ringBounds(parcel.coordinates as Position[][]),
|
||
[parcel.coordinates],
|
||
);
|
||
const hasBuildings = buildings.features.length > 0;
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
border: "1px solid var(--border-card)",
|
||
borderRadius: 12,
|
||
overflow: "hidden",
|
||
height,
|
||
position: "relative",
|
||
}}
|
||
>
|
||
<MapContainer
|
||
center={[56.8389, 60.6057]}
|
||
zoom={15}
|
||
style={{ width: "100%", height: "100%" }}
|
||
scrollWheelZoom
|
||
>
|
||
<TileLayer
|
||
attribution='© <a href="https://www.openstreetmap.org/copyright">OSM</a>'
|
||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||
/>
|
||
|
||
{/* Parcel boundary — outline only so buildings read on top. */}
|
||
<GeoJSON
|
||
key={`parcel-${variantKey}`}
|
||
data={parcel}
|
||
style={{
|
||
color: "#D1D5DB",
|
||
weight: 2,
|
||
dashArray: "6 4",
|
||
fillOpacity: 0,
|
||
}}
|
||
/>
|
||
|
||
{/* Generated buildings — filled blocks. */}
|
||
{hasBuildings && (
|
||
<GeoJSON
|
||
key={`buildings-${variantKey}`}
|
||
data={buildings}
|
||
style={{
|
||
color: "#1d4ed8",
|
||
weight: 1.5,
|
||
fillColor: "#1d4ed8",
|
||
fillOpacity: 0.55,
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
<FitBounds bounds={bounds} />
|
||
</MapContainer>
|
||
|
||
{!hasBuildings && (
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
bottom: 12,
|
||
left: 12,
|
||
right: 12,
|
||
zIndex: 1000,
|
||
background: "var(--bg-card)",
|
||
border: "1px solid var(--border-card)",
|
||
borderRadius: 8,
|
||
padding: "8px 12px",
|
||
fontSize: 12,
|
||
color: "var(--fg-secondary)",
|
||
}}
|
||
>
|
||
Движок ещё не вернул геометрию зданий для этого варианта.
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|