gendesign/backend/app/services/cadastre/grid_geometry.py
bot-backend 14f3ef2019
All checks were successful
Deploy / build-worker (push) Successful in 2m47s
Deploy / deploy (push) Successful in 1m20s
Deploy / changes (push) Successful in 9s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m52s
fix(week-review): backend-аудит v2 — 82 фиксов (#1660)
Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
2026-06-17 17:13:38 +00:00

129 lines
5.4 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
import math
from sqlalchemy import text
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
# Радиус сферы Web Mercator (EPSG:3857), м. Нужен для инверсии Y → широта.
_WEB_MERCATOR_R = 6378137.0
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.
#
# Bug #1629: EPSG:3857 (Web Mercator) — конформная, НЕ равнопротяжённая
# проекция. Линейный масштаб = sec(lat); на широте ЕКБ (~56-60°N) фактор
# ≈1.8-2.0, т.е. 1 наземный метр ≈ 1.8-2.0 единиц 3857. Пороги ниже заданы
# в НАЗЕМНЫХ метрах, поэтому переводим протяжённость bbox из единиц 3857 в
# наземные метры умножением на cos(lat) (lat берём из центра bbox через
# инверсию Y). Иначе верхняя граница 10000 отсекала бы реальные крупные/
# вытянутые кварталы (10000 единиц 3857 ≈ всего ~5050 м земли на широте ЕКБ).
y_mid = (bbox[1] + bbox[3]) / 2.0
lat_mid = 2.0 * math.atan(math.exp(y_mid / _WEB_MERCATOR_R)) - math.pi / 2.0
ground_scale = math.cos(lat_mid) # ground_m = mercator_units * cos(lat)
width = (bbox[2] - bbox[0]) * ground_scale
height = (bbox[3] - bbox[1]) * ground_scale
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