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:
@@ -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)}
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren