From baac5385fac6bc5192a9a87dffeb50b6b54cd3f9 Mon Sep 17 00:00:00 2001 From: Olivier Monod Date: Mon, 1 Dec 2025 12:25:25 +0000 Subject: [PATCH 1/2] remove unused wms parsing functions, remove need for harmony map dict, take max instead of summand --- settings_values/cantons.py | 122 ++++--------------------------- src/routes/drill_category.py | 4 +- src/services/services.py | 135 ++++++++++------------------------- tests/test_settings.py | 10 +-- 4 files changed, 52 insertions(+), 219 deletions(-) diff --git a/settings_values/cantons.py b/settings_values/cantons.py index 1fe24f9..5769b9d 100644 --- a/settings_values/cantons.py +++ b/settings_values/cantons.py @@ -34,11 +34,6 @@ ], }, ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "ZH": { "active": False, @@ -144,11 +139,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "VS": { "active": True, @@ -201,12 +191,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - {"sum": 4, "value": 4}, - ], }, "VD": { "active": True, @@ -241,11 +225,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "UR": { "style": "", @@ -288,11 +267,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "TI": { "active": True, @@ -321,11 +295,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "TG": { "active": True, @@ -374,11 +343,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "SZ": { "active": True, @@ -408,11 +372,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "SH": { "active": True, @@ -458,11 +417,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "SG": { "active": True, @@ -500,11 +454,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "OW": { "active": True, @@ -562,11 +511,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "NW": { "active": True, @@ -632,11 +576,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "LU": { "active": True, @@ -645,7 +584,12 @@ [2665393, 1217714, 1, "ews_zulaessig"], [2652462, 1196901, 2, "ews_zulaessig & ews_zulaessig_auflagen"], [2651972, 1200000, 3, "ews_nicht_zulaessig & ews_zulaessig_auflagen"], - [2645371, 1200735, 2, "ews_vorabklaeren"], + [ + 2645371, + 1200735, + 2, + "ews_vorabklaeren", + ], # TODO: check with LU how to map lanslide areas [2645598, 1202446, 3, "ews_nicht_zulaessig"], ], "wmsUrl": "https://public.geo.lu.ch/ogd/services/managed/EWNUTZXX_COL_V3_MP/MapServer/WMSServer", @@ -654,29 +598,30 @@ "infoFormat": "application/geo+json", "style": "", "loopLayers": True, + # no property values for this canton, only layers are matched if request hits a polygon "layers": [ { "name": "3", "desc": "ews_zulaessig", - "propertyName": "Bewertungskriterium", + "propertyName": "Erdwärmenutzung zulässig", "summand": 1, }, { "name": "1", "desc": "ews_vorabklaeren", - "propertyName": "Bewertungskriterium", + "propertyName": "Erdwärmenutzung zulässig mit Auflagen", "summand": 2, }, { "name": "2", "desc": "ews_zulaessig_auflagen", - "propertyName": "Bewertungskriterium", - "summand": 10, + "propertyName": "Erdwärmenutzung zulässig mit Auflagen", + "summand": 2, }, { "name": "0", "desc": "ews_nicht_zulaessig", - "propertyName": "Bewertungskriterium", + "propertyName": "Erdwärmenutzung nicht zulässig", "summand": 3, }, ], @@ -713,11 +658,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "GL": { "active": True, @@ -741,7 +681,6 @@ ], } ], - "harmonyMap": [{"sum": 2, "value": 2}, {"sum": 3, "value": 3}], }, "GE": { "active": True, @@ -781,11 +720,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "FR": { "active": True, @@ -821,11 +755,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "BL": { "active": True, @@ -876,13 +805,6 @@ ], } ], - "active": True, - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - {"sum": 4, "value": 4}, - ], }, "BE": { "active": True, @@ -923,11 +845,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "AR": { "active": True, @@ -966,11 +883,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "AI": { "active": True, @@ -999,11 +911,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, "AG": { "active": True, @@ -1049,11 +956,6 @@ ], } ], - "harmonyMap": [ - {"sum": 1, "value": 1}, - {"sum": 2, "value": 2}, - {"sum": 3, "value": 3}, - ], }, } } diff --git a/src/routes/drill_category.py b/src/routes/drill_category.py index 280c36c..16fde09 100644 --- a/src/routes/drill_category.py +++ b/src/routes/drill_category.py @@ -42,9 +42,7 @@ async def get_drill_category( "detail": result["error"], } # --- Process features into ground category --- - features = services.process_ground_category( - features, canton_config["layers"], canton_config["harmonyMap"] - ) + features = services.process_ground_category(features, canton_config["layers"]) return { "coord_x": coord_x, diff --git a/src/services/services.py b/src/services/services.py index b16984f..97aa963 100644 --- a/src/services/services.py +++ b/src/services/services.py @@ -1,5 +1,6 @@ import httpx import json +import re import xml.etree.ElementTree as ET from fastapi import HTTPException import logging @@ -181,12 +182,7 @@ async def fetch_features_for_point(coord_x: float, coord_y: float, config: dict) async def parse_wms_getfeatureinfo(content: bytes, info_format: str): """ - Fully unified parser for: - • ESRI REST (arcgis/json, application/json, json) - • GeoJSON (FeatureCollection, Feature) - • WMS GML (gml:featureMember) - • MapServer (msGMLOutput via <*_feature>) - • Fallbacks (single object, list objects) + Parser for differents geoservices outputs """ text = content.decode("utf-8", errors="ignore") @@ -202,67 +198,17 @@ async def parse_wms_getfeatureinfo(content: bytes, info_format: str): raise HTTPException(500, f"Invalid JSON: {e}") # ------------------------------- - # 1) GeoJSON FeatureCollection + # GeoJSON FeatureCollection # ------------------------------- if isinstance(data, dict) and data.get("type") == "FeatureCollection": - feats = [] - print(data) - for feat in data.get("features", []): - props = feat.get("properties", {}) - if isinstance(props, dict): - feats.append(props) - return feats - - # ------------------------------- - # 2) GeoJSON single Feature - # ------------------------------- - if isinstance(data, dict) and data.get("type") == "Feature": - props = data.get("properties", {}) - return [props] if isinstance(props, dict) else [{}] - - # ------------------------------- - # 3) ArcGIS FeatureServer array - # ------------------------------- - if isinstance(data, dict) and "features" in data: - feats = [] - for feat in data.get("features") or []: - if isinstance(feat, dict): - if isinstance(feat.get("attributes"), dict): - feats.append(feat["attributes"]) - else: - # flatten fallback - feats.append( - {k: v for k, v in feat.items() if not isinstance(v, dict)} - ) - return feats - - # ------------------------------- - # 4) ArcGIS single element - # ------------------------------- - if isinstance(data, dict) and isinstance(data.get("attributes"), dict): - return [data["attributes"]] - - # ------------------------------- - # 5) Plain JSON array - # ------------------------------- - if isinstance(data, list): - out = [] - for item in data: - if isinstance(item, dict): - if "attributes" in item and isinstance(item["attributes"], dict): - out.append(item["attributes"]) - elif "properties" in item and isinstance(item["properties"], dict): - out.append(item["properties"]) - else: - out.append(item) - else: - out.append(item) - return out + features = [] - # ------------------------------- - # 6) Fallback single object - # ------------------------------- - return [data] + for feature in data.get("features", []): + props = feature.get("properties", {}) + props["layerName"] = feature.get("layerName") + if isinstance(props, dict): + features.append(props) + return features # ---------------------------------------------------------------------- # GML / XML PARSING (OWSLib-compatible) @@ -295,7 +241,6 @@ async def parse_wms_getfeatureinfo(content: bytes, info_format: str): # ---------------------------------------------------------------------- # 2) MapServer msGMLOutput <*_feature> # ---------------------------------------------------------------------- - import re for elem in root.iter(): if re.search(r"_feature$", elem.tag): @@ -314,7 +259,8 @@ async def parse_wms_getfeatureinfo(content: bytes, info_format: str): def process_ground_category( - ground_features: list, config_layers: list, harmony_map: list + ground_features: list, + config_layers: list, ): """ Reclass canton response into normalized values. @@ -325,22 +271,25 @@ def process_ground_category( if not ground_features: return { "layer_results": [], - "mapping_sum": 0, "harmonized_value": 4, - "note": "No features found; fallback to harmonized category 4.", } layer_results = [] - mapping_sum = 0 + mapped_values = [] + harmonized_value = None + + # For each canton, multiple layers are requested (single request to WMS / ESRI) + # The returned feature(s) are then compared with mapping values defined for: + # - Each layer + # - Each possible value in each layer + # - Some geoservices associate one layer to one category. In this case, layer names are compared, not attribute values for layer_cfg in config_layers: layer_name = layer_cfg.get("name") property_name = layer_cfg.get("propertyName") property_values = layer_cfg.get("propertyValues") - layer_summand = 0 description = None - last_value = None for feature in ground_features: # ESRI REST support @@ -353,49 +302,39 @@ def process_ground_category( value = feature value = normalize_string(value) - last_value = value - # ------------------------- - # TODO: check summand logic! - # ------------------------- if property_values: for item in property_values: + # Match with values for layers that have a defined mapping + if item.get("name") == value: - layer_summand = item.get("summand", 0) + mapped_values.append(item.get("summand")) description = item.get("desc") - break - - mapping_sum = layer_summand + # break + # For some cantons, only the presence or absence of feature is used to define suitability + else: + if layer_cfg.get("propertyName") == feature.get("layerName"): + mapped_values.append(layer_cfg.get("summand")) + # Helping function to identify issues. Only "harmonized_value is useful for frontend application" layer_results.append( { "layer": layer_name, "propertyName": property_name, - "value": last_value, - "summand": layer_summand, + "value": value, "description": description, } ) - harmonized_value = None - if harmony_map: - for h in harmony_map: - if h.get("sum") == mapping_sum: - harmonized_value = h.get("value") - break - - if harmonized_value is None: - # fallback sum=0 → 4 - if mapping_sum == 0: - harmonized_value = 4 - else: - harmonized_value = None - else: - # fallback when no harmony map - harmonized_value = 4 if mapping_sum == 0 else None + # property_values variable contains the mapping between attribute value and drillapi categories (1,2,3) + if mapped_values: + harmonized_value = max(mapped_values) + + # If no value is found, fallback value is = 4 + if not harmonized_value: + harmonized_value = 4 return { "layer_results": layer_results, - "mapping_sum": mapping_sum, "harmonized_value": harmonized_value, } diff --git a/tests/test_settings.py b/tests/test_settings.py index fd11a75..fa267f5 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -15,12 +15,7 @@ class Layer(BaseModel): summand: Optional[int] = None -class HarmonyMapItem(BaseModel): - sum: int - value: int - - -class Region(BaseModel): +class Cantonconfig(BaseModel): active: bool name: str exampleLocation: List[List[Union[int, float, str]]] @@ -30,7 +25,6 @@ class Region(BaseModel): infoFormat: str style: Optional[str] layers: List[Layer] - harmonyMap: List[HarmonyMapItem] @field_validator("layers") @classmethod @@ -54,7 +48,7 @@ def test_cantons_configuration_integrity(): """ for canton_name, canton_data in CANTONS["cantons_configurations"].items(): # Pydantic validation - region = Region(**canton_data) + region = Cantonconfig(**canton_data) if __name__ == "__main__": From f1a731dd791fe68f92bb3ceea1b024fa7c2510f8 Mon Sep 17 00:00:00 2001 From: Olivier Monod Date: Mon, 1 Dec 2025 12:52:00 +0000 Subject: [PATCH 2/2] fix for VS --- settings_values/cantons.py | 19 ++++++++++++------- src/services/services.py | 4 ++-- tests/test_settings.py | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/settings_values/cantons.py b/settings_values/cantons.py index 5769b9d..8e62335 100644 --- a/settings_values/cantons.py +++ b/settings_values/cantons.py @@ -146,7 +146,12 @@ "exampleLocation": [ [2596336, 1122359, 1, "CLASSGV 1"], [2598146, 1124454, 1, "CLASSGV 2"], - [2598043, 1123873, 2, "CLASSGV 3"], + [ + 2598043, + 1123873, + 3, + "CLASSGV 4", + ], # REST response is CLASSGV 4. Clarify classification with VS [2597117, 1120835, 3, "CLASSGV 4"], [2598068.4, 1130927.1, 4, "CLASSGV 5"], ], @@ -164,29 +169,29 @@ "propertyName": "CLASSGV", "propertyValues": [ { - "name": 1, + "name": "1", "desc": "Sondes géothermiques admises (max. 200 m sans étude)", "summand": 1, }, { - "name": 2, + "name": "2", "desc": "Sondes géothermiques limitées à 100 m de profondeur", "summand": 1, }, { - "name": 3, + "name": "3", "desc": "Au cas par cas - Etude préalable (contacter le SEN)", "summand": 2, }, { - "name": 4, + "name": "4", "desc": "Sondes géothermiques interdites", "summand": 3, }, { - "name": 5, + "name": "5", "desc": "Hors zone (contacter le SEN)", - "summand": 4, + "summand": 3, }, ], } diff --git a/src/services/services.py b/src/services/services.py index 97aa963..eb557a2 100644 --- a/src/services/services.py +++ b/src/services/services.py @@ -302,7 +302,6 @@ def process_ground_category( value = feature value = normalize_string(value) - if property_values: for item in property_values: # Match with values for layers that have a defined mapping @@ -310,7 +309,7 @@ def process_ground_category( if item.get("name") == value: mapped_values.append(item.get("summand")) description = item.get("desc") - # break + # For some cantons, only the presence or absence of feature is used to define suitability else: if layer_cfg.get("propertyName") == feature.get("layerName"): @@ -327,6 +326,7 @@ def process_ground_category( ) # property_values variable contains the mapping between attribute value and drillapi categories (1,2,3) + if mapped_values: harmonized_value = max(mapped_values) diff --git a/tests/test_settings.py b/tests/test_settings.py index fa267f5..a48cf13 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -4,7 +4,7 @@ class PropertyValue(BaseModel): - name: Union[int, str] + name: str summand: int