"""Overpass Turbo: OverpassQL-Queries gegen OpenStreetMap ausfuehren.""" import logging import time import asyncio import hashlib import httpx from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field logger = logging.getLogger("globe.overpass") router = APIRouter() # --- Rate Limiting --- _last_request_time = 0.0 _RATE_LIMIT_SECONDS = 10 # Overpass Fair-Use Policy # --- Cache --- _cache = {} _CACHE_TTL = 300 # 5 Minuten _MAX_CACHE_ENTRIES = 50 # --- Overpass API --- _OVERPASS_URLS = [ "https://overpass-api.de/api/interpreter", "https://z.overpass-api.de/api/interpreter", ] _MAX_ELEMENTS = 5000 _TIMEOUT = 60 _RETRY_CODES = {502, 504, 429} # Transiente Fehler class OverpassRequest(BaseModel): query: str = Field(..., max_length=4000) bbox: list[float] | None = Field(None, min_length=4, max_length=4) # --- Template-Bibliothek --- _TEMPLATES = [ # === Militaerisch === { "id": "mil_bases", "name": "Militaerbasen", "category": "military", "description": "Militaerische Einrichtungen und Stuetzpunkte", "color": "#ff4444", "query": "[out:json][timeout:25];\n(\n node[\"military\"]({{bbox}});\n way[\"military\"]({{bbox}});\n relation[\"military\"]({{bbox}});\n);\nout center body;", }, { "id": "mil_airfields", "name": "Militaerflughaefen", "category": "military", "description": "Militaerische Flugplaetze und Luftwaffenstuetzpunkte", "color": "#ff4444", "query": "[out:json][timeout:25];\n(\n node[\"aeroway\"=\"aerodrome\"][\"military\"]({{bbox}});\n way[\"aeroway\"=\"aerodrome\"][\"military\"]({{bbox}});\n);\nout center body;", }, { "id": "mil_naval", "name": "Marinestuetzpunkte", "category": "military", "description": "Marinehaefen und Flottenbasen", "color": "#ff4444", "query": "[out:json][timeout:25];\n(\n node[\"military\"=\"naval_base\"]({{bbox}});\n way[\"military\"=\"naval_base\"]({{bbox}});\n node[\"landuse\"=\"military\"][\"name\"~\"[Nn]av|[Ff]leet|[Mm]arine|[Ff]lott\"]({{bbox}});\n);\nout center body;", }, { "id": "mil_radar", "name": "Radarstationen", "category": "military", "description": "Militaerische und zivile Radaranlagen", "color": "#ff4444", "query": "[out:json][timeout:25];\n(\n node[\"man_made\"=\"surveillance\"][\"surveillance:type\"=\"radar\"]({{bbox}});\n node[\"military\"=\"radar\"]({{bbox}});\n node[\"radar\"]({{bbox}});\n);\nout center body;", }, { "id": "mil_range", "name": "Uebungsplaetze", "category": "military", "description": "Militaerische Uebungsgelaende und Schiessplaetze", "color": "#ff4444", "query": "[out:json][timeout:25];\n(\n node[\"military\"=\"range\"]({{bbox}});\n way[\"military\"=\"range\"]({{bbox}});\n relation[\"military\"=\"range\"]({{bbox}});\n node[\"military\"=\"training_area\"]({{bbox}});\n way[\"military\"=\"training_area\"]({{bbox}});\n);\nout center body;", }, { "id": "mil_bunker", "name": "Bunker / Munitionslager", "category": "military", "description": "Bunkeranlagen und Munitionsdepots", "color": "#ff4444", "query": "[out:json][timeout:25];\n(\n node[\"military\"=\"bunker\"]({{bbox}});\n way[\"military\"=\"bunker\"]({{bbox}});\n node[\"military\"=\"ammunition\"]({{bbox}});\n way[\"military\"=\"ammunition\"]({{bbox}});\n);\nout center body;", }, # === Transport === { "id": "tr_airports", "name": "Flughaefen", "category": "transport", "description": "Zivile und gemischte Flughaefen", "color": "#4499ff", "query": "[out:json][timeout:25];\n(\n node[\"aeroway\"=\"aerodrome\"]({{bbox}});\n way[\"aeroway\"=\"aerodrome\"]({{bbox}});\n);\nout center body;", }, { "id": "tr_ports", "name": "Haefen", "category": "transport", "description": "See- und Binnenhaefen", "color": "#4499ff", "query": "[out:json][timeout:25];\n(\n node[\"harbour\"]({{bbox}});\n way[\"harbour\"]({{bbox}});\n node[\"landuse\"=\"port\"]({{bbox}});\n way[\"landuse\"=\"port\"]({{bbox}});\n);\nout center body;", }, { "id": "tr_railways", "name": "Bahnhoefe", "category": "transport", "description": "Bahnhoefe und Haltestellen", "color": "#4499ff", "query": "[out:json][timeout:25];\n(\n node[\"railway\"=\"station\"]({{bbox}});\n node[\"railway\"=\"halt\"]({{bbox}});\n);\nout center body;", }, { "id": "tr_bridges", "name": "Bruecken", "category": "transport", "description": "Grosse Bruecken und Uebergaenge", "color": "#4499ff", "query": "[out:json][timeout:25];\n(\n way[\"bridge\"=\"yes\"][\"name\"]({{bbox}});\n);\nout center body;", }, { "id": "tr_tunnels", "name": "Tunnel", "category": "transport", "description": "Strassen- und Eisenbahntunnel", "color": "#4499ff", "query": "[out:json][timeout:25];\n(\n way[\"tunnel\"=\"yes\"][\"name\"]({{bbox}});\n);\nout center body;", }, # === Energie === { "id": "en_nuclear", "name": "Kernkraftwerke", "category": "energy", "description": "Atomkraftwerke und Reaktoren", "color": "#ffdd00", "query": "[out:json][timeout:25];\n(\n node[\"generator:source\"=\"nuclear\"]({{bbox}});\n way[\"generator:source\"=\"nuclear\"]({{bbox}});\n node[\"plant:source\"=\"nuclear\"]({{bbox}});\n way[\"plant:source\"=\"nuclear\"]({{bbox}});\n);\nout center body;", }, { "id": "en_power", "name": "Kraftwerke", "category": "energy", "description": "Alle Arten von Kraftwerken", "color": "#ffdd00", "query": "[out:json][timeout:25];\n(\n node[\"power\"=\"plant\"]({{bbox}});\n way[\"power\"=\"plant\"]({{bbox}});\n relation[\"power\"=\"plant\"]({{bbox}});\n);\nout center body;", }, { "id": "en_substations", "name": "Umspannwerke", "category": "energy", "description": "Elektrische Umspannwerke", "color": "#ffdd00", "query": "[out:json][timeout:25];\n(\n node[\"power\"=\"substation\"]({{bbox}});\n way[\"power\"=\"substation\"]({{bbox}});\n);\nout center body;", }, { "id": "en_pipelines", "name": "Pipelines", "category": "energy", "description": "Oel- und Gas-Pipelines", "color": "#ffdd00", "query": "[out:json][timeout:25];\n(\n way[\"man_made\"=\"pipeline\"]({{bbox}});\n);\nout center body;", }, { "id": "en_refineries", "name": "Raffinerien", "category": "energy", "description": "Oelraffinerien und Verarbeitungsanlagen", "color": "#ffdd00", "query": "[out:json][timeout:25];\n(\n node[\"man_made\"=\"petroleum_well\"]({{bbox}});\n way[\"industrial\"=\"refinery\"]({{bbox}});\n node[\"industrial\"=\"refinery\"]({{bbox}});\n);\nout center body;", }, { "id": "en_dams", "name": "Staudaemme", "category": "energy", "description": "Staudaemme und Talsperren", "color": "#ffdd00", "query": "[out:json][timeout:25];\n(\n way[\"waterway\"=\"dam\"]({{bbox}});\n node[\"waterway\"=\"dam\"]({{bbox}});\n);\nout center body;", }, { "id": "en_solar", "name": "Solarparks", "category": "energy", "description": "Photovoltaik-Grossanlagen", "color": "#ffdd00", "query": "[out:json][timeout:25];\n(\n way[\"plant:source\"=\"solar\"]({{bbox}});\n way[\"generator:source\"=\"solar\"][\"generator:output:electricity\"]({{bbox}});\n);\nout center body;", }, { "id": "en_wind", "name": "Windparks", "category": "energy", "description": "Windkraftanlagen", "color": "#ffdd00", "query": "[out:json][timeout:25];\n(\n node[\"generator:source\"=\"wind\"]({{bbox}});\n way[\"plant:source\"=\"wind\"]({{bbox}});\n);\nout center body;", }, { "id": "en_storage", "name": "Tanklager", "category": "energy", "description": "Oel- und Gastanklager, Speichertanks", "color": "#ffdd00", "query": "[out:json][timeout:25];\n(\n node[\"man_made\"=\"storage_tank\"]({{bbox}});\n way[\"man_made\"=\"storage_tank\"]({{bbox}});\n);\nout center body;", }, # === Kommunikation === { "id": "com_towers", "name": "Sendemasten", "category": "communication", "description": "Funk- und Fernmeldetuerme", "color": "#e040fb", "query": "[out:json][timeout:25];\n(\n node[\"man_made\"=\"mast\"][\"tower:type\"=\"communication\"]({{bbox}});\n node[\"man_made\"=\"tower\"][\"tower:type\"=\"communication\"]({{bbox}});\n);\nout center body;", }, { "id": "com_datacenters", "name": "Rechenzentren", "category": "communication", "description": "Datenzentren und Serverfarmen", "color": "#e040fb", "query": "[out:json][timeout:25];\n(\n node[\"telecom\"=\"data_center\"]({{bbox}});\n way[\"telecom\"=\"data_center\"]({{bbox}});\n node[\"building\"=\"data_centre\"]({{bbox}});\n way[\"building\"=\"data_centre\"]({{bbox}});\n);\nout center body;", }, { "id": "com_satellite", "name": "Satellitenbodenstationen", "category": "communication", "description": "Satellitenkommunikations-Anlagen", "color": "#e040fb", "query": "[out:json][timeout:25];\n(\n node[\"man_made\"=\"antenna\"][\"antenna:type\"=\"dish\"]({{bbox}});\n node[\"landuse\"=\"observatory\"]({{bbox}});\n way[\"landuse\"=\"observatory\"]({{bbox}});\n);\nout center body;", }, { "id": "com_cables_landing", "name": "Seekabel-Landepunkte", "category": "communication", "description": "Unterseekabel-Anlandestationen", "color": "#e040fb", "query": "[out:json][timeout:25];\n(\n node[\"telecom\"=\"landing_point\"]({{bbox}});\n node[\"communication\"=\"line\"][\"location\"=\"underwater\"]({{bbox}});\n);\nout center body;", }, # === Regierung === { "id": "gov_embassies", "name": "Botschaften / Konsulate", "category": "government", "description": "Diplomatische Vertretungen", "color": "#ff9800", "query": "[out:json][timeout:25];\n(\n node[\"amenity\"=\"embassy\"]({{bbox}});\n way[\"amenity\"=\"embassy\"]({{bbox}});\n node[\"office\"=\"diplomatic\"]({{bbox}});\n);\nout center body;", }, { "id": "gov_buildings", "name": "Regierungsgebaeude", "category": "government", "description": "Parlamente, Ministerien, Regierungssitze", "color": "#ff9800", "query": "[out:json][timeout:25];\n(\n node[\"office\"=\"government\"]({{bbox}});\n way[\"office\"=\"government\"]({{bbox}});\n way[\"building\"=\"government\"]({{bbox}});\n);\nout center body;", }, { "id": "gov_prisons", "name": "Gefaengnisse", "category": "government", "description": "Justizvollzugsanstalten und Haftzentren", "color": "#ff9800", "query": "[out:json][timeout:25];\n(\n node[\"amenity\"=\"prison\"]({{bbox}});\n way[\"amenity\"=\"prison\"]({{bbox}});\n);\nout center body;", }, { "id": "gov_police", "name": "Polizeistationen", "category": "government", "description": "Polizeiwachen und -dienststellen", "color": "#ff9800", "query": "[out:json][timeout:25];\n(\n node[\"amenity\"=\"police\"]({{bbox}});\n way[\"amenity\"=\"police\"]({{bbox}});\n);\nout center body;", }, # === Kritische Infrastruktur === { "id": "crit_hospitals", "name": "Krankenhaeuser", "category": "critical", "description": "Krankenhaeuser und Kliniken", "color": "#00bcd4", "query": "[out:json][timeout:25];\n(\n node[\"amenity\"=\"hospital\"]({{bbox}});\n way[\"amenity\"=\"hospital\"]({{bbox}});\n);\nout center body;", }, { "id": "crit_water", "name": "Wasserwerke", "category": "critical", "description": "Wasseraufbereitung und -versorgung", "color": "#00bcd4", "query": "[out:json][timeout:25];\n(\n node[\"man_made\"=\"water_works\"]({{bbox}});\n way[\"man_made\"=\"water_works\"]({{bbox}});\n node[\"man_made\"=\"wastewater_plant\"]({{bbox}});\n way[\"man_made\"=\"wastewater_plant\"]({{bbox}});\n);\nout center body;", }, { "id": "crit_fire", "name": "Feuerwachen", "category": "critical", "description": "Feuerwehrstationen", "color": "#00bcd4", "query": "[out:json][timeout:25];\n(\n node[\"amenity\"=\"fire_station\"]({{bbox}});\n way[\"amenity\"=\"fire_station\"]({{bbox}});\n);\nout center body;", }, ] def _cache_cleanup(): """Entfernt aelteste Eintraege wenn Cache zu gross.""" if len(_cache) > _MAX_CACHE_ENTRIES: sorted_keys = sorted(_cache, key=lambda k: _cache[k][0]) for k in sorted_keys[: len(_cache) - _MAX_CACHE_ENTRIES]: del _cache[k] @router.get("/overpass/templates") async def get_templates(): """Liefert die Template-Bibliothek.""" categories = {} for t in _TEMPLATES: cat = t["category"] if cat not in categories: categories[cat] = [] categories[cat].append( { "id": t["id"], "name": t["name"], "description": t["description"], "color": t["color"], "query": t["query"], } ) cat_labels = { "military": "Militaerisch", "transport": "Transport", "energy": "Energie", "communication": "Kommunikation", "government": "Regierung", "critical": "Kritische Infrastruktur", } return { "categories": [ {"id": cid, "label": cat_labels.get(cid, cid), "templates": tpls} for cid, tpls in categories.items() ] } @router.post("/overpass/query") async def execute_query(req: OverpassRequest): """Fuehrt eine OverpassQL-Query aus (gecacht, rate-limited).""" global _last_request_time query = req.query.strip() if not query: raise HTTPException(400, "Query darf nicht leer sein") # BBox-Platzhalter ersetzen if req.bbox and "{{bbox}}" in query: bbox_str = f"{req.bbox[0]},{req.bbox[1]},{req.bbox[2]},{req.bbox[3]}" query = query.replace("{{bbox}}", bbox_str) elif "{{bbox}}" in query and not req.bbox: raise HTTPException( 400, "Query enthaelt {{bbox}} aber keine BBox wurde mitgesendet. " "Aktiviere 'Viewport als BBox'.", ) # Cache pruefen cache_key = hashlib.md5(query.encode()).hexdigest() if cache_key in _cache and time.time() - _cache[cache_key][0] < _CACHE_TTL: cached = _cache[cache_key][1] logger.info(f"Cache-Hit: {cache_key} ({cached.get('total', '?')} Elemente)") return cached # Rate-Limiting now = time.time() elapsed = now - _last_request_time if elapsed < _RATE_LIMIT_SECONDS: wait = _RATE_LIMIT_SECONDS - elapsed raise HTTPException( 429, f"Rate-Limit: Bitte {wait:.0f}s warten (Overpass Fair-Use)", headers={"Retry-After": str(int(wait) + 1)}, ) _last_request_time = time.time() # Query ausfuehren (mit Retry bei transienten Fehlern) data = None last_error = None for url in _OVERPASS_URLS: for attempt in range(2): try: async with httpx.AsyncClient(timeout=_TIMEOUT) as client: r = await client.post(url, data={"data": query}) if r.status_code in _RETRY_CODES and attempt == 0: logger.info(f"Overpass {url}: {r.status_code}, retry in 2s...") await asyncio.sleep(2) continue r.raise_for_status() data = r.json() break except Exception as e: last_error = f"{url}: {e}" logger.warning(f"Overpass {url} (attempt {attempt+1}): {e}") break if data is not None: break if data is None: logger.error(f"Alle Overpass-Endpunkte fehlgeschlagen: {last_error}") raise HTTPException(502, "Overpass API nicht erreichbar") elements = data.get("elements", []) # Elemente aufteilen nodes = [] ways = [] relations = [] total = 0 for el in elements: if total >= _MAX_ELEMENTS: break total += 1 el_type = el.get("type") tags = el.get("tags", {}) name = tags.get("name", "") if el_type == "node": nodes.append({ "id": el.get("id"), "lat": el.get("lat"), "lon": el.get("lon"), "tags": tags, "name": name, }) elif el_type == "way": center = el.get("center", {}) geom = el.get("geometry", []) ways.append({ "id": el.get("id"), "lat": center.get("lat"), "lon": center.get("lon"), "geometry": geom, "tags": tags, "name": name, }) elif el_type == "relation": center = el.get("center", {}) relations.append({ "id": el.get("id"), "lat": center.get("lat"), "lon": center.get("lon"), "tags": tags, "name": name, }) result = { "nodes": nodes, "ways": ways, "relations": relations, "total": total, "truncated": len(elements) > _MAX_ELEMENTS, "cached": False, } # Cachen _cache[cache_key] = (time.time(), {**result, "cached": True}) _cache_cleanup() logger.info( f"Overpass: {total} Elemente " f"({len(nodes)} Nodes, {len(ways)} Ways, {len(relations)} Relations)" ) return result