diff --git a/src/data_ships.py b/src/data_ships.py index af855c9..d9a2a91 100644 --- a/src/data_ships.py +++ b/src/data_ships.py @@ -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)} diff --git a/static/css/globe.css b/static/css/globe.css index c46370b..f961e1d 100644 --- a/static/css/globe.css +++ b/static/css/globe.css @@ -577,3 +577,9 @@ html, body { height: 100%; overflow: hidden; background: var(--bg-primary); colo .dot-terminator { background: #ff8800; } .dot-timezones { background: #8888ff; } + +/* === Ship Filters === */ +.ship-filters { + padding: 4px 12px 6px; + border-top: 1px solid rgba(255,255,255,0.04); +} diff --git a/static/index.html b/static/index.html index 12d822a..f0988c1 100644 --- a/static/index.html +++ b/static/index.html @@ -74,6 +74,7 @@
+