gendesign/backend/app/services/cadastre/grid_geometry.py
lekss361 13025c81ee feat(cadastre): bulk_harvest_quarter Celery task + grid-walker + saga state (#168 PR3/5)
Add bulk cadastre harvest pipeline:
- services/cadastre/bulk_harvest.py: async harvest_quarter() orchestrator (4 phases)
  + 7 upsert helpers (parcels/buildings/constructions/oncs/enks/zouit/quarter_stats)
  using CAST(:x AS jsonb) pattern, begin_nested() SAVEPOINT per grid-walk upsert
- services/cadastre/grid_geometry.py: quarter_bbox_3857 (PostGIS ST_Extent)
  + generate_grid_click_points (15x15 = 225 sub-bbox grid)
- workers/tasks/scrape_cadastre.py: bulk_harvest_quarter_task (acks_late=True,
  dont_autoretry_for NspdBulkWafError), enqueue_cadastre_harvest, cleanup_zombies
- api/v1/admin_cadastre.py: 5 endpoints behind AdminTokenAuth — create/list/get/cancel/resume
- Tests: 13 service unit + 8 API tests
2026-05-15 13:03:35 +03:00

100 lines
3.5 KiB
Python
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.

"""Геометрические утилиты для grid-walk по кадастровому кварталу.
Используется в bulk_harvest.py для вычисления bbox квартала в EPSG:3857
и генерации сетки точек для WMS GetFeatureInfo.
"""
from __future__ import annotations
import logging
from sqlalchemy import text
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
def quarter_bbox_3857(db: Session, quarter: str) -> tuple[float, float, float, float] | None:
"""Получить bbox квартала из cad_quarters_geom в EPSG:3857.
Args:
db: SQLAlchemy session.
quarter: кадастровый номер квартала, например '66:41:0303161'.
Returns:
(xmin, ymin, xmax, ymax) в метрах EPSG:3857, или None если квартал не найден.
"""
row = (
db.execute(
text("""
SELECT
ST_XMin(b) AS xmin,
ST_YMin(b) AS ymin,
ST_XMax(b) AS xmax,
ST_YMax(b) AS ymax
FROM (
SELECT ST_Extent(ST_Transform(geom, 3857)) AS b
FROM cad_quarters_geom
WHERE cad_number = :q
) sub
"""),
{"q": quarter},
)
.mappings()
.first()
)
if not row or row["xmin"] is None:
logger.warning("quarter_bbox_3857: квартал %s не найден в cad_quarters_geom", quarter)
return None
return (
float(row["xmin"]),
float(row["ymin"]),
float(row["xmax"]),
float(row["ymax"]),
)
def generate_grid_click_points(
bbox: tuple[float, float, float, float],
grid_size: int = 15,
tile_width: int = 512,
tile_height: int = 512,
) -> list[tuple[tuple[float, float, float, float], tuple[int, int]]]:
"""Генерировать grid_size × grid_size ячеек для bbox квартала.
Каждая ячейка — свой sub-bbox + центральный пиксель (click_xy) для
WMS GetFeatureInfo. 15×15 = 225 запросов обеспечивают хорошее покрытие
при шаге ~50м для типичного квартала ЕКБ (~750м × ~750м).
Args:
bbox: (xmin, ymin, xmax, ymax) в EPSG:3857.
grid_size: количество ячеек по каждой оси (15 → 225 запросов).
tile_width: ширина виртуального WMS тайла в пикселях.
tile_height: высота виртуального WMS тайла в пикселях.
Returns:
Список (sub_bbox, click_xy) — sub_bbox в EPSG:3857, click_xy в пикселях.
"""
xmin, ymin, xmax, ymax = bbox
x_step = (xmax - xmin) / grid_size
y_step = (ymax - ymin) / grid_size
result: list[tuple[tuple[float, float, float, float], tuple[int, int]]] = []
for i in range(grid_size):
for j in range(grid_size):
cell_xmin = xmin + i * x_step
cell_ymin = ymin + j * y_step
cell_xmax = cell_xmin + x_step
cell_ymax = cell_ymin + y_step
# Клик в центр ячейки
click_x = tile_width // 2
click_y = tile_height // 2
result.append(
(
(cell_xmin, cell_ymin, cell_xmax, cell_ymax),
(click_x, click_y),
)
)
return result