fix(generative): MultiPolygon-участки — крупнейший контур в _parse_polygon (#2295) (#2315)
All checks were successful
Deploy / changes (push) Successful in 7s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m58s
Deploy / build-worker (push) Successful in 2m58s
Deploy / deploy (push) Successful in 1m32s

This commit is contained in:
bot-backend 2026-07-03 21:13:00 +00:00
parent bc9549782b
commit 27e21f37d3
2 changed files with 78 additions and 2 deletions

View file

@ -30,7 +30,7 @@ from dataclasses import dataclass
from typing import Any
from pyproj import CRS, Transformer
from shapely.geometry import Polygon, mapping, shape
from shapely.geometry import MultiPolygon, Polygon, mapping, shape
from shapely.geometry.base import BaseGeometry
from shapely.ops import transform as shapely_transform
@ -142,6 +142,19 @@ def _parse_polygon(parcel_geojson: dict[str, Any]) -> Polygon:
except (KeyError, TypeError, ValueError, AttributeError) as exc:
raise ParcelGeometryError(f"cannot parse GeoJSON geometry: {exc}") from exc
# Многоконтурный участок (MultiPolygon): берём крупнейший контур. У некоторых
# КН-участков в Росреестре несколько разрозненных полигонов — концепцию строим
# по основному (наибольшему по площади) контуру; остальные обычно вкрапления.
if isinstance(geom, MultiPolygon):
if geom.is_empty or not geom.geoms:
raise ParcelGeometryError("parcel polygon is empty")
n_contours = len(geom.geoms)
geom = max(geom.geoms, key=lambda g: g.area)
logger.info(
"parse_parcel: MultiPolygon → крупнейший контур из %d",
n_contours,
)
if geom.geom_type != "Polygon":
raise ParcelGeometryError(f"expected Polygon, got {geom.geom_type}")
if geom.is_empty:

View file

@ -100,13 +100,76 @@ def test_tiny_parcel_rejected_after_setback() -> None:
def test_non_polygon_rejected() -> None:
payload = _payload(parcel_geojson={"type": "Point", "coordinates": [60.60, 56.83]})
with pytest.raises(ParcelGeometryError):
geometry.parse_parcel(payload)
def test_linestring_rejected() -> None:
# LineString не имеет площади — прежняя ошибка сохраняется.
payload = _payload(
parcel_geojson={"type": "Point", "coordinates": [60.60, 56.83]}
parcel_geojson={
"type": "LineString",
"coordinates": [[60.60, 56.830], [60.6045, 56.8328]],
}
)
with pytest.raises(ParcelGeometryError):
geometry.parse_parcel(payload)
# Второй, заведомо меньший контур (~90 m x 45 m) поодаль от основного участка.
_SMALL_CONTOUR = [
[60.610, 56.840],
[60.6112, 56.840],
[60.6112, 56.8404],
[60.610, 56.8404],
[60.610, 56.840],
]
def test_multipolygon_picks_largest_contour() -> None:
# Многоконтурный участок: основной прямоугольник + мелкое вкрапление поодаль.
# Берётся крупнейший контур → площадь == площади одиночного основного полигона.
multi = _payload(
parcel_geojson={
"type": "MultiPolygon",
"coordinates": [[_PARCEL_COORDS], [_SMALL_CONTOUR]],
}
)
single = _payload(parcel_geojson={"type": "Polygon", "coordinates": [_PARCEL_COORDS]})
parcel_multi = geometry.parse_parcel(multi)
parcel_single = geometry.parse_parcel(single)
# Результат многоконтурного == основному контуру (крупнейший выбран).
assert math.isclose(parcel_multi.site_area_sqm, parcel_single.site_area_sqm, rel_tol=1e-6)
# Крупнейший контур на масштабе квартала, мелкое вкрапление отброшено.
assert parcel_multi.site_area_sqm > 50_000
def test_multipolygon_single_contour_equals_polygon() -> None:
# MultiPolygon с одним контуром эквивалентен голому Polygon.
multi = _payload(
parcel_geojson={
"type": "MultiPolygon",
"coordinates": [[_PARCEL_COORDS]],
}
)
single = _payload(parcel_geojson={"type": "Polygon", "coordinates": [_PARCEL_COORDS]})
parcel_multi = geometry.parse_parcel(multi)
parcel_single = geometry.parse_parcel(single)
assert math.isclose(parcel_multi.site_area_sqm, parcel_single.site_area_sqm, rel_tol=1e-9)
assert math.isclose(
parcel_multi.buildable_area_sqm,
parcel_single.buildable_area_sqm,
rel_tol=1e-9,
)
def test_empty_multipolygon_rejected() -> None:
payload = _payload(parcel_geojson={"type": "MultiPolygon", "coordinates": []})
with pytest.raises(ParcelGeometryError):
geometry.parse_parcel(payload)
def test_build_placement_grid_anchors_are_step_aligned() -> None:
# Простой метрический квадрат 30x30 м, шаг 10 -> 3x3 = 9 ячеек.
square = Polygon([(0, 0), (30, 0), (30, 30), (0, 30)])