- kumi.systems Fallback (tot) durch z.overpass-api.de ersetzt - Retry-Logik bei transienten HTTP-Fehlern (502/504/429) - 2s Pause vor Retry, dann naechster Endpunkt
466 Zeilen
18 KiB
Python
466 Zeilen
18 KiB
Python
"""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
|