"""Геометрические утилиты для 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 bbox = ( float(row["xmin"]), float(row["ymin"]), float(row["xmax"]), 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( bbox: tuple[float, float, float, float], grid_size: int = 15, tile_size: 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_size: размер виртуального 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_size // 2 click_y = tile_size // 2 result.append( ( (cell_xmin, cell_ymin, cell_xmax, cell_ymax), (click_x, click_y), ) ) return result