fix(cadastre): skip grid-walk for broken quarter geometries (#168) #179
2 changed files with 67 additions and 2 deletions
|
|
@ -46,12 +46,28 @@ def quarter_bbox_3857(db: Session, quarter: str) -> tuple[float, float, float, f
|
||||||
if not row or row["xmin"] is None:
|
if not row or row["xmin"] is None:
|
||||||
logger.warning("quarter_bbox_3857: квартал %s не найден в cad_quarters_geom", quarter)
|
logger.warning("quarter_bbox_3857: квартал %s не найден в cad_quarters_geom", quarter)
|
||||||
return None
|
return None
|
||||||
return (
|
bbox = (
|
||||||
float(row["xmin"]),
|
float(row["xmin"]),
|
||||||
float(row["ymin"]),
|
float(row["ymin"]),
|
||||||
float(row["xmax"]),
|
float(row["xmax"]),
|
||||||
float(row["ymax"]),
|
float(row["ymax"]),
|
||||||
)
|
)
|
||||||
|
# Sanity check: cad_quarters_geom имеет ~72% rows с broken micro-precision
|
||||||
|
# geometry (width <0.01m). Это вызывает useless 1px tiles в WMS и 500 errors
|
||||||
|
# от NSPD. Реальный квартал ЕКБ имеет width 200-4000м. Skip grid-walk для
|
||||||
|
# broken — snapshot phase даст 20 per cat, что норм для MVP.
|
||||||
|
width = bbox[2] - bbox[0]
|
||||||
|
height = bbox[3] - bbox[1]
|
||||||
|
if width < 100 or height < 100 or width > 10000 or height > 10000:
|
||||||
|
logger.warning(
|
||||||
|
"quarter_bbox_3857: квартал %s broken geom — width=%.2fm height=%.2fm "
|
||||||
|
"(expected 100-10000m). Grid-walk skipped, only snapshot phase.",
|
||||||
|
quarter,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return bbox
|
||||||
|
|
||||||
|
|
||||||
def generate_grid_click_points(
|
def generate_grid_click_points(
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,10 @@ from app.services.cadastre.bulk_harvest import (
|
||||||
_safe_numeric,
|
_safe_numeric,
|
||||||
upsert_features,
|
upsert_features,
|
||||||
)
|
)
|
||||||
from app.services.cadastre.grid_geometry import generate_grid_click_points
|
from app.services.cadastre.grid_geometry import (
|
||||||
|
generate_grid_click_points,
|
||||||
|
quarter_bbox_3857,
|
||||||
|
)
|
||||||
|
|
||||||
# ── Фикстуры ─────────────────────────────────────────────────────────────────
|
# ── Фикстуры ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -195,6 +198,52 @@ def test_generate_grid_click_points_custom_grid() -> None:
|
||||||
assert cell_bbox[3] - cell_bbox[1] == pytest.approx(200.0)
|
assert cell_bbox[3] - cell_bbox[1] == pytest.approx(200.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ── quarter_bbox_3857 sanity check ───────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_db_with_bbox(
|
||||||
|
xmin: float | None, ymin: float | None, xmax: float | None, ymax: float | None
|
||||||
|
) -> MagicMock:
|
||||||
|
"""Mock SQLAlchemy session.execute().mappings().first() возвращает заданный bbox."""
|
||||||
|
mock_db = MagicMock()
|
||||||
|
row = None if xmin is None else {"xmin": xmin, "ymin": ymin, "xmax": xmax, "ymax": ymax}
|
||||||
|
mock_db.execute.return_value.mappings.return_value.first.return_value = row
|
||||||
|
return mock_db
|
||||||
|
|
||||||
|
|
||||||
|
def test_quarter_bbox_3857_healthy_geom() -> None:
|
||||||
|
"""Real ЕКБ-квартал ~750м — должен пройти sanity check."""
|
||||||
|
db = _mock_db_with_bbox(6735845.0, 8329000.0, 6736595.0, 8329750.0)
|
||||||
|
bbox = quarter_bbox_3857(db, "66:41:0303161")
|
||||||
|
assert bbox is not None
|
||||||
|
assert bbox == (6735845.0, 8329000.0, 6736595.0, 8329750.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_quarter_bbox_3857_skips_broken_micro_geom() -> None:
|
||||||
|
"""width < 100m — broken micro-precision geom (72% rows в cad_quarters_geom)."""
|
||||||
|
# width=60.7m — реальный broken quarter 66:41:0101005 после ST_Transform
|
||||||
|
db = _mock_db_with_bbox(60.0, 60.0, 120.7, 120.7)
|
||||||
|
assert quarter_bbox_3857(db, "66:41:0101005") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_quarter_bbox_3857_skips_oversized_geom() -> None:
|
||||||
|
"""width > 10000m — broken/regional geom (не городской квартал)."""
|
||||||
|
db = _mock_db_with_bbox(0.0, 0.0, 15000.0, 15000.0)
|
||||||
|
assert quarter_bbox_3857(db, "66:41:9999999") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_quarter_bbox_3857_skips_zero_height() -> None:
|
||||||
|
"""height < 100m — симметрия по другой оси."""
|
||||||
|
db = _mock_db_with_bbox(0.0, 0.0, 500.0, 50.0)
|
||||||
|
assert quarter_bbox_3857(db, "66:41:0000001") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_quarter_bbox_3857_returns_none_when_quarter_missing() -> None:
|
||||||
|
"""Квартал не найден в cad_quarters_geom → None (xmin IS NULL)."""
|
||||||
|
db = _mock_db_with_bbox(None, None, None, None)
|
||||||
|
assert quarter_bbox_3857(db, "66:99:0000000") is None
|
||||||
|
|
||||||
|
|
||||||
# ── upsert_features routing ──────────────────────────────────────────────────
|
# ── upsert_features routing ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue