feat(nspd): get_feature_info FEATURE_COUNT=10 — multi-feature per point (#1080)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m14s
Deploy / build-worker (push) Successful in 2m24s
Deploy / deploy (push) Successful in 1m21s

get_feature_info() шлёт WMS FEATURE_COUNT=10 (новый параметр feature_count, default 10). NSPD по умолчанию отдаёт 1 фичу/точку → перекрывающиеся зоны одного слоя теряются (37581 «иные ЗОУИТ» на ЕКБ = 2 фичи). Tile-геометрия не тронута. +2 теста (multi-feature parse, override), существующий обновлён.

Refs #1067.
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
This commit is contained in:
lekss361 2026-06-06 18:18:07 +00:00 committed by bot-reviewer
parent 22fffb9878
commit c2e0428803
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] = []