Spaces:
Running on Zero
Running on Zero
| """ | |
| Downloads BDCavités (BRGM's national underground cavity inventory -- | |
| karst sinkholes, quarries, marl pits, natural cavities) for the Eure/ | |
| Risle area, via Géorisques' WFS. | |
| Directly relevant to the bétoire investigation: this is an independent, | |
| purpose-built dataset for exactly this phenomenon, unlike inferring it | |
| from Hub'Eau station naming or BD TOPO's provisional karst attribute. | |
| CONFIRMED (fetched GetCapabilities directly): endpoint | |
| https://georisques.gouv.fr/services, WFS 1.1.0, typeName | |
| CAVITE_LOCALISEE ("Cavités souterraines abandonnées d'origine non | |
| minière"), GeoJSON output supported directly. Axis order for this | |
| specific server is NOT separately confirmed -- reusing the same | |
| lon,lat-then-lat,lon retry that scripts/download_bdtopo_hydro.py needed | |
| for a different WFS server, since different servers have behaved | |
| differently on this before and there's no reason to assume this one | |
| won't too. | |
| Usage: | |
| python -m scripts.download_bdcavites --check | |
| python -m scripts.download_bdcavites | |
| """ | |
| import argparse | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import requests | |
| WFS_URL = "https://georisques.gouv.fr/services" | |
| TYPE_NAME = "CAVITE_LOCALISEE" | |
| # Same widened bbox as scripts/download_bdtopo_hydro.py's BBOX (updated | |
| # after confirmed real evidence of truncation on the original, tighter | |
| # box) -- keeping both scripts scoped to the same area. | |
| BBOX = (-0.1, 47.7, 2.1, 49.9) # (min_lon, min_lat, max_lon, max_lat) | |
| def check_typename() -> bool: | |
| params = { | |
| "SERVICE": "WFS", "VERSION": "1.1.0", "REQUEST": "GetFeature", | |
| "TYPENAME": TYPE_NAME, "MAXFEATURES": 1, "OUTPUTFORMAT": "application/json; subtype=geojson; charset=utf-8", | |
| } | |
| resp = requests.get(WFS_URL, params=params, timeout=30) | |
| ok, detail = _parse_response(resp) | |
| print(f" {TYPE_NAME}: {'OK' if ok else 'FAILED'}") | |
| if not ok: | |
| print(f" {detail}") | |
| return ok | |
| def _parse_response(resp: "requests.Response"): | |
| if resp.status_code != 200: | |
| return False, f"HTTP {resp.status_code}: {resp.text[:400]}" | |
| try: | |
| data = resp.json() | |
| except ValueError: | |
| return False, f"Response was not JSON (likely a WFS ExceptionReport): {resp.text[:400]}" | |
| if isinstance(data, dict) and data.get("type") == "FeatureCollection": | |
| return True, "" | |
| return False, f"Response was JSON but not a FeatureCollection: {str(data)[:400]}" | |
| def fetch_all_pages(bbox_param: str, page_size: int = 1000) -> list: | |
| all_features = [] | |
| start_index = 0 | |
| while True: | |
| params = { | |
| "SERVICE": "WFS", "VERSION": "1.1.0", "REQUEST": "GetFeature", | |
| "TYPENAME": TYPE_NAME, "BBOX": bbox_param, | |
| "OUTPUTFORMAT": "application/json; subtype=geojson; charset=utf-8", | |
| "MAXFEATURES": page_size, "STARTINDEX": start_index, | |
| } | |
| resp = requests.get(WFS_URL, params=params, timeout=60) | |
| ok, detail = _parse_response(resp) | |
| if not ok: | |
| print(f" ERROR at startIndex={start_index}: {detail}") | |
| break | |
| data = resp.json() | |
| features = data.get("features", []) | |
| if not features: | |
| break | |
| all_features.extend(features) | |
| if len(features) < page_size: | |
| break | |
| start_index += page_size | |
| return all_features | |
| def fetch_with_axis_retry(bbox: tuple, out_path: Path) -> None: | |
| min_lon, min_lat, max_lon, max_lat = bbox | |
| orderings = [ | |
| ("lon,lat", f"{min_lon},{min_lat},{max_lon},{max_lat},urn:ogc:def:crs:EPSG::4326"), | |
| ("lat,lon", f"{min_lat},{min_lon},{max_lat},{max_lon},urn:ogc:def:crs:EPSG::4326"), | |
| ] | |
| features = [] | |
| for label, bbox_param in orderings: | |
| print(f" trying axis order {label}...") | |
| features = fetch_all_pages(bbox_param) | |
| if features: | |
| print(f" -> {label} worked ({len(features)} feature(s))") | |
| break | |
| print(f" -> {label} returned 0 features") | |
| geojson = {"type": "FeatureCollection", "features": features} | |
| out_path.write_text(json.dumps(geojson)) | |
| if features: | |
| print(f"Saved {len(features)} feature(s) to {out_path}") | |
| else: | |
| print(f"Saved an EMPTY file to {out_path} -- both axis orders returned nothing. " | |
| f"Run --check first, or this area may genuinely have zero recorded cavities " | |
| f"(plausible -- BDCavités coverage is built department-by-department and " | |
| f"isn't uniformly complete everywhere).") | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Download BDCavités for the Eure/Risle area") | |
| parser.add_argument("--check", action="store_true") | |
| parser.add_argument("--output-dir", type=Path, default=Path("datasets/bdcavites")) | |
| args = parser.parse_args() | |
| if args.check: | |
| print("Testing typeName against the live service...") | |
| ok = check_typename() | |
| if not ok: | |
| print("\nFailed -- the typeName or service details may have changed since this " | |
| "script was written. Fetch GetCapabilities directly to check:") | |
| print(f" {WFS_URL}?SERVICE=WFS&VERSION=1.1.0&REQUEST=GetCapabilities") | |
| else: | |
| print("\nOK. Re-run without --check to download.") | |
| return | |
| args.output_dir.mkdir(parents=True, exist_ok=True) | |
| print("Fetching CAVITE_LOCALISEE (BDCavités)...") | |
| fetch_with_axis_retry(BBOX, args.output_dir / "cavite_localisee.geojson") | |
| print() | |
| print("Once downloaded, cross-reference against the amont/aval bétoire stations " | |
| "(H605641101 at 48.98492,0.78902 and H605641201 at 49.04707,0.79984) the same " | |
| "way scripts/analyze_bdtopo_hydro.py's check_karst_near_betoire did for the " | |
| "BD TOPO karst attribute -- this is a genuinely independent second check, " | |
| "not a re-run of the same one.") | |
| if __name__ == "__main__": | |
| main() |