from __future__ import annotations import urllib.parse from collections import Counter from typing import Any from .evidence import make_evidence from .geometry import EARTH_RADIUS_M from .http_client import post_text from .models import EvidenceItem, SiteSelection OVERPASS_URL = "https://overpass-api.de/api/interpreter" def fetch_osm_context(selection: SiteSelection) -> tuple[dict[str, Any], list[EvidenceItem]]: if selection.anchor_lat is None or selection.anchor_lon is None: return {"counts": {}, "features": []}, [ make_evidence( category="Geographic context", finding="OSM context was skipped because no real-world anchor coordinate is available.", source_name="OpenStreetMap", source_url="https://www.openstreetmap.org/copyright", source_type="open map data", resolution_or_scope="not available", confidence="low", limitation="Public map data requires a real-world coordinate.", design_implication="Use CAD/user observations until the site is anchored.", verification_needed="Provide lat/lon or draw the boundary on the map.", output_label="site_visit_required", ) ] radius = int(selection.radius_m or 500) radius = min(max(radius, 100), 1500) query = f""" [out:json][timeout:20]; ( way(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["highway"]; way(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["building"]; way(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["natural"="water"]; way(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["waterway"]; way(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["landuse"]; way(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["leisure"="park"]; node(around:{radius},{selection.anchor_lat},{selection.anchor_lon})["amenity"]; ); out tags center geom 120; """ try: payload = post_text(OVERPASS_URL, urllib.parse.urlencode({"data": query})) elements = payload.get("elements") or [] counts = _count_context(elements) evidence = [ make_evidence( category="Geographic context", finding=f"OSM context found {sum(counts.values())} nearby mapped features in the selected search radius.", source_name="OpenStreetMap / Overpass API", source_url="https://www.openstreetmap.org/copyright", source_type="open map data", resolution_or_scope=f"{radius} m around anchor coordinate", confidence="medium" if counts else "low", limitation="OSM tags may be incomplete or outdated; public tile/API use requires attribution.", design_implication="Use mapped roads, water, green, land-use, and amenities as first-pass context only.", verification_needed="Verify access, road widths, vegetation, water bodies, and local activity on site.", output_label="public_data", ) ] return {"counts": dict(counts), "features": elements[:80], "radius_m": radius}, evidence except Exception as exc: # noqa: BLE001 return {"counts": {}, "features": [], "radius_m": radius}, [ make_evidence( category="Geographic context", finding="OSM context could not be retrieved.", source_name="OpenStreetMap / Overpass API", source_url="https://overpass-api.de/", source_type="open map data", resolution_or_scope=f"{radius} m around anchor coordinate", confidence="low", limitation=f"Overpass request failed: {type(exc).__name__}.", design_implication="Do not infer mapped context from this missing layer.", verification_needed="Manually verify roads, water, vegetation, access, and surrounding buildings.", output_label="site_visit_required", ) ] def _count_context(elements: list[dict[str, Any]]) -> Counter[str]: counts: Counter[str] = Counter() for element in elements: tags = element.get("tags") or {} if "highway" in tags: counts["roads/access"] += 1 if "building" in tags: counts["buildings"] += 1 if tags.get("natural") == "water" or "waterway" in tags: counts["water"] += 1 if tags.get("leisure") == "park" or tags.get("landuse") in {"forest", "grass", "recreation_ground"}: counts["green/open space"] += 1 if "landuse" in tags: counts[f"landuse:{tags['landuse']}"] += 1 if "amenity" in tags: counts[f"amenity:{tags['amenity']}"] += 1 return counts