GEOINT-Modus: Experimentelle taktische Kartenansicht mit Echtzeit-Datenlayern

Neuer experimenteller GEOINT-Modus per Checkbox auf der Karten-Kachel:
- Satellitenbilder (Esri World Imagery) statt OSM-Strassenkarte
- Echtzeit-Flugverkehr (airplanes.live via Backend-Proxy, 15s Refresh)
- Erdbeben-Layer (USGS M2.5+, pulsierende Kreise nach Magnitude)
- GDELT Nachrichten (geokodierte Echtzeit-News, Cluster-Darstellung)
- Heatmap-Visualisierung der Artikel-Standorte (Leaflet.heat)
- Timeline-Slider fuer zeitliche Filterung der Artikel-Marker
- Koordinatenanzeige (Lat/Lon unter Mauszeiger)
- Distanzmessung (Klick-zu-Klick mit km-Anzeige)
- Taktisches Styling (dunkle Tonung, gruene Akzente, Scanlines)

Neue Dateien: geoint.js, geoint.css, routers/geoint.py
Inspiriert von WorldView/Gods Eye Konzept, komplett eigenentwickelt.
Dieser Commit ist enthalten in:
Claude Dev
2026-03-24 09:29:19 +01:00
Ursprung fdbffa7e00
Commit b2be1358ab
6 geänderte Dateien mit 1030 neuen und 1 gelöschten Zeilen

100
src/routers/geoint.py Normale Datei
Datei anzeigen

@@ -0,0 +1,100 @@
"""GEOINT-Router: Proxy fuer externe Echtzeit-Datenquellen (Flugverkehr, GDELT)."""
import logging
import time
from typing import Optional
import httpx
from fastapi import APIRouter, Depends, Query
from auth import get_current_user
logger = logging.getLogger("osint.geoint")
router = APIRouter(tags=["geoint"])
# ---------------------------------------------------------------------------
# Einfacher In-Memory-Cache
# ---------------------------------------------------------------------------
_cache: dict[str, tuple[float, dict]] = {}
def _get_cached(key: str, ttl: float) -> Optional[dict]:
if key in _cache:
ts, data = _cache[key]
if time.time() - ts < ttl:
return data
return None
def _set_cache(key: str, data: dict):
_cache[key] = (time.time(), data)
# Cache-Groesse begrenzen (max 50 Eintraege)
if len(_cache) > 50:
oldest = min(_cache, key=lambda k: _cache[k][0])
del _cache[oldest]
# ---------------------------------------------------------------------------
# Flugverkehr (airplanes.live)
# ---------------------------------------------------------------------------
@router.get("/flights")
async def get_flights(
lat: float = Query(..., ge=-90, le=90),
lon: float = Query(..., ge=-180, le=180),
radius: int = Query(100, ge=10, le=250),
_user: dict = Depends(get_current_user),
):
"""Proxy fuer airplanes.live API. 10s Cache, max 300 Aircraft."""
cache_key = f"flights:{round(lat, 1)}:{round(lon, 1)}:{radius}"
cached = _get_cached(cache_key, ttl=10)
if cached:
return cached
url = f"https://api.airplanes.live/v2/point/{lat:.4f}/{lon:.4f}/{radius}"
try:
async with httpx.AsyncClient(timeout=8) as client:
resp = await client.get(url)
resp.raise_for_status()
data = resp.json()
except Exception as e:
logger.warning(f"airplanes.live Fehler: {e}")
return {"ac": []}
# Auf 300 Aircraft begrenzen
if "ac" in data and len(data["ac"]) > 300:
data["ac"] = data["ac"][:300]
_set_cache(cache_key, data)
return data
# ---------------------------------------------------------------------------
# GDELT Nachrichten
# ---------------------------------------------------------------------------
@router.get("/gdelt")
async def get_gdelt(
query: str = Query("conflict", max_length=200),
_user: dict = Depends(get_current_user),
):
"""Proxy fuer GDELT GEO 2.0 API. 60s Cache."""
cache_key = f"gdelt:{query[:50]}"
cached = _get_cached(cache_key, ttl=60)
if cached:
return cached
url = (
"https://api.gdeltproject.org/api/v2/geo/geo"
f"?query={query}&mode=PointData&format=GeoJSON"
"&timespan=24h&maxrows=200"
)
try:
async with httpx.AsyncClient(timeout=12) as client:
resp = await client.get(url)
resp.raise_for_status()
data = resp.json()
except Exception as e:
logger.warning(f"GDELT Fehler: {e}")
return {"type": "FeatureCollection", "features": []}
_set_cache(cache_key, data)
return data