feat(nspd): get_feature_info FEATURE_COUNT=10 — multi-feature per point (#1067)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / changes (push) Successful in 6s
CI / backend-tests (push) Successful in 5m46s
CI / backend-tests (pull_request) Successful in 5m45s

NSPD WMS GetFeatureInfo по умолчанию отдаёт 1 фичу/точку → перекрывающиеся зоны
одного слоя теряются (на ЕКБ 37581 «иные ЗОУИТ» в точке возвращает 2). Портал
шлёт FEATURE_COUNT=10. Добавлен параметр feature_count (default 10) → полный
overlay-ингест. Tile-геометрия (256/128) не тронута — low-regression.

Refs #1067 (ИРД foundation A2).
This commit is contained in:
bot-backend 2026-06-06 21:10:09 +03:00
parent 38d948f078
commit f900096359
2 changed files with 49 additions and 1 deletions

View file

@ -370,13 +370,21 @@ class NSPDClient:
lon: float, lon: float,
lat: float, lat: float,
buffer_m: int = 100, buffer_m: int = 100,
feature_count: int = 10,
) -> list[NSPDFeature]: ) -> list[NSPDFeature]:
"""WMS GetFeatureInfo на точке — какие feature'ы layer'а покрывают (lon, lat). """WMS GetFeatureInfo на точке — какие feature'ы layer'а покрывают (lon, lat).
Использует микро-bbox вокруг точки в EPSG:3857 (WMS требует bbox + I/J Использует микро-bbox вокруг точки в EPSG:3857 (WMS требует bbox + I/J
пиксель). Tile 256x256, I=128, J=128 центр. пиксель). Tile 256x256, I=128, J=128 центр.
Returns features (может быть 0+). Args:
feature_count: макс. число фич в ответе (WMS FEATURE_COUNT). Default 10
(паритет с порталом nspd.gov.ru). Без него NSPD по умолчанию отдаёт
ТОЛЬКО 1 фичу на точку а на ЕКБ в одной точке часто пересекаются
несколько зон одного слоя (напр. 37581 «иные ЗОУИТ» вернул 2). Для
overlay-ингеста (#1067) нужно >1.
Returns features (может быть 0+, до feature_count).
Use cases: Use cases:
- ВРИ конкретного участка (layer_id=875838 territorial_zones) - ВРИ конкретного участка (layer_id=875838 territorial_zones)
@ -393,6 +401,9 @@ class NSPDClient:
"QUERY_LAYERS": str(layer_id), "QUERY_LAYERS": str(layer_id),
"LAYERS": str(layer_id), "LAYERS": str(layer_id),
"INFO_FORMAT": "application/json", "INFO_FORMAT": "application/json",
# FEATURE_COUNT: NSPD по умолчанию отдаёт 1 фичу/точку → теряем
# перекрывающиеся зоны. Портал шлёт 10. Без этого overlay-ингест неполон.
"FEATURE_COUNT": str(feature_count),
# STYLES обязателен (пустой), иначе INFO_FORMAT игнорируется per NSPD WAF # STYLES обязателен (пустой), иначе INFO_FORMAT игнорируется per NSPD WAF
"STYLES": "", "STYLES": "",
"I": "128", "I": "128",

View file

@ -232,10 +232,47 @@ def test_get_feature_info_parses_response(monkeypatch: pytest.MonkeyPatch) -> No
assert "INFO_FORMAT=application%2Fjson" in url assert "INFO_FORMAT=application%2Fjson" in url
assert "STYLES=" in url # required per NSPD WAF assert "STYLES=" in url # required per NSPD WAF
assert "CRS=EPSG%3A3857" in url assert "CRS=EPSG%3A3857" in url
assert "FEATURE_COUNT=10" in url # default >1 — иначе NSPD отдаёт 1 фичу/точку
# bbox should reflect 100m buffer (200m side) # bbox should reflect 100m buffer (200m side)
assert "BBOX=" in url assert "BBOX=" in url
def test_get_feature_info_returns_multiple_overlapping_features(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""FEATURE_COUNT>1 → несколько перекрывающихся зон в одной точке (как 37581 на ЕКБ)."""
captured_urls: list[str] = []
def fake_http(url: str, **kwargs: Any) -> dict[str, Any]:
captured_urls.append(url)
return {
"type": "FeatureCollection",
"features": [
{"id": "z1", "properties": {"reg_numb_border": "66:41-6.4125"}},
{"id": "z2", "properties": {"reg_numb_border": "66:41-6.914"}},
],
}
monkeypatch.setattr("app.services.scrapers.nspd_client._http_get_json", fake_http)
feats = NSPDClient().get_feature_info(37581, lon=60.576, lat=56.839, feature_count=10)
assert len(feats) == 2
assert {f.properties["reg_numb_border"] for f in feats} == {"66:41-6.4125", "66:41-6.914"}
assert "FEATURE_COUNT=10" in captured_urls[0]
def test_get_feature_info_custom_feature_count(monkeypatch: pytest.MonkeyPatch) -> None:
"""feature_count прокидывается в URL (override default)."""
captured_urls: list[str] = []
def fake_http(url: str, **kwargs: Any) -> dict[str, Any]:
captured_urls.append(url)
return {"features": []}
monkeypatch.setattr("app.services.scrapers.nspd_client._http_get_json", fake_http)
NSPDClient().get_feature_info(875838, lon=60.605, lat=56.838, feature_count=25)
assert "FEATURE_COUNT=25" in captured_urls[0]
def test_get_features_in_bbox_url_builds_correctly(monkeypatch: pytest.MonkeyPatch) -> None: def test_get_features_in_bbox_url_builds_correctly(monkeypatch: pytest.MonkeyPatch) -> None:
"""get_features_in_bbox использует переданный bbox без преобразования.""" """get_features_in_bbox использует переданный bbox без преобразования."""
captured_urls: list[str] = [] captured_urls: list[str] = []