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:
100
src/routers/geoint.py
Normale Datei
100
src/routers/geoint.py
Normale Datei
@@ -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"
|
||||
"×pan=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
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren