| 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: |
| 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 |
|
|