Spaces:
Running on Zero
Running on Zero
| """ | |
| Fetches fresh IDPR (Indice de Développement et de Persistance des | |
| Réseaux) values directly from BRGM's live geoservice, replacing/ | |
| supplementing the manually-uploaded idpr.csv. | |
| CONFIRMED (fetched GetCapabilities directly): endpoint | |
| http://geoservices.brgm.fr/geologie, WMS 1.3.0, live and responding. | |
| IDPR is raster/grid data at five resolutions (IDPR_50M down to | |
| IDPR_5000M) -- not a vector feature type, so WFS GetFeature (the | |
| pattern used everywhere else in this project) doesn't apply. The | |
| correct WMS operation for extracting a value at a specific point from a | |
| raster layer is GetFeatureInfo: request a small map image centered on | |
| the point, then ask "what's the value at this pixel." | |
| This queries ONE point at a time (GetFeatureInfo has no bulk/vectorized | |
| equivalent the way BD TOPO's WFS BBOX queries did) -- scoped to the 27 | |
| real gauge stations, matching what "redownload IDPR" means (fresh | |
| values at known station points), not the full ~4,500-node reach graph | |
| (which would mean thousands of individual HTTP requests -- a different, | |
| much larger undertaking than this). | |
| Usage: | |
| python -m scripts.fetch_idpr_brgm --check | |
| python -m scripts.fetch_idpr_brgm --stations datasets/station_elevations.csv | |
| """ | |
| import argparse | |
| import re | |
| from pathlib import Path | |
| import pandas as pd | |
| import requests | |
| WMS_URL = "http://geoservices.brgm.fr/geologie" | |
| LAYER = "IDPR_50M" # highest resolution; use --layer to try a coarser one if this fails | |
| def get_feature_info_at_point(lat: float, lon: float, layer: str = LAYER, | |
| half_extent_deg: float = 0.001) -> "float | None": | |
| """ | |
| Query IDPR value at a single point via WMS GetFeatureInfo: request a | |
| tiny 3x3 pixel map centered on (lat, lon), then ask for the value at | |
| the center pixel. | |
| """ | |
| minx, miny = lon - half_extent_deg, lat - half_extent_deg | |
| maxx, maxy = lon + half_extent_deg, lat + half_extent_deg | |
| params = { | |
| "SERVICE": "WMS", "VERSION": "1.1.1", "REQUEST": "GetFeatureInfo", | |
| "LAYERS": layer, "QUERY_LAYERS": layer, "STYLES": "", | |
| "BBOX": f"{minx},{miny},{maxx},{maxy}", "SRS": "EPSG:4326", | |
| "WIDTH": 3, "HEIGHT": 3, "X": 1, "Y": 1, | |
| "INFO_FORMAT": "text/plain", | |
| } | |
| try: | |
| resp = requests.get(WMS_URL, params=params, timeout=30) | |
| except requests.RequestException as e: | |
| print(f" request failed: {e}") | |
| return None | |
| if resp.status_code != 200: | |
| print(f" HTTP {resp.status_code}") | |
| return None | |
| return parse_getfeatureinfo_value(resp.text) | |
| def parse_getfeatureinfo_value(text: str) -> "float | None": | |
| """ | |
| MapServer's text/plain GetFeatureInfo output is typically a small | |
| block like: | |
| Layer 'IDPR_50M' | |
| Feature 0 | |
| value = '885' | |
| Parsing defensively: find any line with 'value' (case-insensitive) | |
| and extract the first number on it, rather than assuming an exact | |
| format -- MapServer's plain-text output format has enough real | |
| variation across deployments that a strict parser is more likely to | |
| silently return nothing than a permissive one is to return a wrong | |
| number. | |
| """ | |
| for line in text.splitlines(): | |
| if "value" in line.lower(): | |
| match = re.search(r"[-+]?\d*\.?\d+", line) | |
| if match: | |
| return float(match.group()) | |
| return None | |
| def check_service() -> bool: | |
| # A real point known to be in mainland France, well within IDPR coverage | |
| test_lat, test_lon = 49.03, 0.79 | |
| print(f"Testing {LAYER} at ({test_lat}, {test_lon})...") | |
| value = get_feature_info_at_point(test_lat, test_lon) | |
| if value is not None: | |
| print(f" OK: value = {value}") | |
| return True | |
| print(" FAILED: no value returned. Try --layer IDPR_100M or IDPR_500M " | |
| "(coarser resolutions), or check GetCapabilities directly:") | |
| print(f" {WMS_URL}?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities") | |
| return False | |
| def fetch_for_stations(stations_path: Path, layer: str, output_path: Path) -> None: | |
| stations = pd.read_csv(stations_path) | |
| lat_col = "latitude" if "latitude" in stations.columns else "lat" | |
| lon_col = "longitude" if "longitude" in stations.columns else "lon" | |
| rows = [] | |
| for _, row in stations.iterrows(): | |
| code = row["station_code"] | |
| value = get_feature_info_at_point(row[lat_col], row[lon_col], layer=layer) | |
| print(f" {code}: {value}") | |
| rows.append({"station_code": code, "idpr_value_brgm_live": value, | |
| "latitude": row[lat_col], "longitude": row[lon_col]}) | |
| out = pd.DataFrame(rows) | |
| n_ok = out["idpr_value_brgm_live"].notna().sum() | |
| print() | |
| print(f"{n_ok}/{len(out)} stations got a real value") | |
| out.to_csv(output_path, index=False) | |
| print(f"Saved to {output_path}") | |
| if n_ok < len(out): | |
| print("Some stations got no value -- this could mean they're just outside " | |
| "IDPR's coverage, or the layer/resolution needs adjusting. Compare " | |
| "against the existing idpr.csv for those specific stations before " | |
| "assuming the live fetch is wrong.") | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Fetch fresh IDPR values from BRGM's live geoservice") | |
| parser.add_argument("--check", action="store_true") | |
| parser.add_argument("--stations", type=Path, default=Path("datasets/station_elevations.csv")) | |
| parser.add_argument("--layer", type=str, default=LAYER) | |
| parser.add_argument("--output", type=Path, default=Path("datasets/idpr_brgm_live.csv")) | |
| args = parser.parse_args() | |
| if args.check: | |
| check_service() | |
| return | |
| fetch_for_stations(args.stations, args.layer, args.output) | |
| if __name__ == "__main__": | |
| main() |