gendesign/frontend/src/components/concept/ConceptResultMap.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

150 lines
4.3 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";
/**
* 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='&copy; <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>
);
}