Schiffsverkehr: Typ-Filter, Kurslinien, Dark Ships

SCHIFFSTYP-FILTER:
- 6 Kategorien: Tanker (rot), Cargo (orange), Passagier (gruen),
  Fischerei (blau), Militaer (magenta), Sonstige (grau)
- Einzeln ein-/ausblendbar via Checkboxen im Panel
- Farbkodierte Punkte nach Schiffstyp

KURSLINIEN-PROJEKTION:
- Zuschaltbar per Checkbox
- Zeigt 30min-Vorausprojektion basierend auf COG + SOG
- Farblich passend zum Schiffstyp, halbtransparent

DARK SHIPS ERKENNUNG:
- Backend erkennt Schiffe die >10min kein AIS-Update senden
- Positionshistorie (letzte 10 Positionen) gespeichert
- API-Endpoint: GET /api/ships/dark
- Verdaechtig wenn vorher aktiv (SOG>0.5) und jetzt still

Backend: ship_type aus AISStream MetaData gespeichert,
AIS-Typcode zu Kategorie klassifiziert (IMO Standard).
Dieser Commit ist enthalten in:
Claude Dev
2026-03-24 23:21:06 +01:00
Ursprung b8d6ed9442
Commit 4b731823e6
4 geänderte Dateien mit 186 neuen und 84 gelöschten Zeilen

Datei anzeigen

@@ -51,6 +51,12 @@ async def _listener():
lon = meta.get("longitude") or pos.get("Longitude")
if not lat or not lon or not (-90 <= lat <= 90 and -180 <= lon <= 180):
continue
ship_type = meta.get("ShipType", 0)
# Vorherige Position fuer Dark-Ship-Erkennung merken
prev = _store.get(mmsi)
prev_positions = (prev.get("track", []) if prev else [])[-10:]
prev_positions.append({"lat": round(lat, 5), "lon": round(lon, 5), "ts": time.time()})
_store[mmsi] = {
"mmsi": mmsi,
"lat": round(lat, 5),
@@ -59,7 +65,9 @@ async def _listener():
"cog": round(pos.get("Cog", 0), 1),
"heading": pos.get("TrueHeading", 0),
"name": (meta.get("ShipName") or "").strip(),
"ship_type": ship_type,
"ts": time.time(),
"track": prev_positions,
}
# Stale-Cleanup alle 1000 Updates
if len(_store) % 1000 == 0:
@@ -84,6 +92,50 @@ def start_ais_collector():
logger.info("AIS collector gestartet")
def _classify_ship(ship_type):
"""AIS Ship Type zu Kategorie."""
if not ship_type:
return "unknown"
t = int(ship_type)
if 20 <= t <= 29: return "wing_in_ground"
if 30 <= t <= 39: return "fishing" if t == 30 else "towing" if t in (31,32) else "military" if t == 35 else "sailing" if t == 36 else "other"
if 40 <= t <= 49: return "hsc"
if 50 <= t <= 59: return "pilot" if t == 50 else "sar" if t == 51 else "tug" if t == 52 else "port" if t == 53 else "medical" if t == 58 else "other"
if 60 <= t <= 69: return "passenger"
if 70 <= t <= 79: return "cargo"
if 80 <= t <= 89: return "tanker"
return "other"
def _detect_dark_ships():
"""Schiffe die laenger als 10 Minuten kein Update hatten aber vorher aktiv waren."""
now = time.time()
dark = []
for mmsi, s in _store.items():
age = now - s["ts"]
track = s.get("track", [])
# Aktiv gewesen (>3 Positionen) aber seit >10min kein Update und SOG war > 1
if age > 600 and len(track) >= 3 and s.get("sog", 0) > 0.5:
dark.append({
"mmsi": mmsi, "name": s.get("name", ""),
"last_lat": s["lat"], "last_lon": s["lon"],
"last_sog": s["sog"], "last_cog": s["cog"],
"silent_minutes": round(age / 60, 1),
"ship_type": _classify_ship(s.get("ship_type")),
})
return dark
@router.get("/ships")
async def get_ships():
return {"ships": list(_store.values()), "total": len(_store), "connected": _connected}
ships_out = []
for s in _store.values():
ship = dict(s)
ship["category"] = _classify_ship(s.get("ship_type"))
# Track auf letzte 5 Positionen kuerzen fuer API
ship["track"] = ship.get("track", [])[-5:]
ships_out.append(ship)
return {"ships": ships_out, "total": len(ships_out), "connected": _connected}
@router.get("/ships/dark")
async def get_dark_ships():
dark = _detect_dark_ships()
return {"dark_ships": dark, "total": len(dark)}