diff --git a/backend/app/services/scrapers/nspd_client.py b/backend/app/services/scrapers/nspd_client.py index 0475ce6e..5be0efc7 100644 --- a/backend/app/services/scrapers/nspd_client.py +++ b/backend/app/services/scrapers/nspd_client.py @@ -370,13 +370,21 @@ class NSPDClient: lon: float, lat: float, buffer_m: int = 100, + feature_count: int = 10, ) -> list[NSPDFeature]: """WMS GetFeatureInfo на точке — какие feature'ы layer'а покрывают (lon, lat). Использует микро-bbox вокруг точки в EPSG:3857 (WMS требует bbox + I/J пиксель). 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: - ВРИ конкретного участка (layer_id=875838 territorial_zones) @@ -393,6 +401,9 @@ class NSPDClient: "QUERY_LAYERS": str(layer_id), "LAYERS": str(layer_id), "INFO_FORMAT": "application/json", + # FEATURE_COUNT: NSPD по умолчанию отдаёт 1 фичу/точку → теряем + # перекрывающиеся зоны. Портал шлёт 10. Без этого overlay-ингест неполон. + "FEATURE_COUNT": str(feature_count), # STYLES обязателен (пустой), иначе INFO_FORMAT игнорируется per NSPD WAF "STYLES": "", "I": "128", diff --git a/backend/tests/test_nspd_client.py b/backend/tests/test_nspd_client.py index d3606aab..052d98ba 100644 --- a/backend/tests/test_nspd_client.py +++ b/backend/tests/test_nspd_client.py @@ -232,10 +232,47 @@ def test_get_feature_info_parses_response(monkeypatch: pytest.MonkeyPatch) -> No assert "INFO_FORMAT=application%2Fjson" in url assert "STYLES=" in url # required per NSPD WAF assert "CRS=EPSG%3A3857" in url + assert "FEATURE_COUNT=10" in url # default >1 — иначе NSPD отдаёт 1 фичу/точку # bbox should reflect 100m buffer (200m side) 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: """get_features_in_bbox использует переданный bbox без преобразования.""" captured_urls: list[str] = []