Dateien
AegisSight-Globe/src/data_ships.py
Claude Dev bfa74ec992 Militaerschiff-Datenbank mit Bildern + Klassifizierung
Neue Datei milship_db.py:
- MMSI-Laenderzuordnung (200+ Laendercodes)
- Schiffsklassen-Datenbank (Nimitz, Arleigh-Burke, Gorshkov,
  Type 052D, Queen Elizabeth, F125, Charles de Gaulle)
- Bilder aus Wikimedia Commons (frei lizenziert)
- Klassifizierung nach MMSI-Prefix + Schiffsname

Klick auf Militaerschiff zeigt:
- Foto der Schiffsklasse (wenn verfuegbar)
- Klassenname und Schiffstyp
- Herkunftsland (aus MMSI)
- MMSI, SOG, COG, Heading

API: GET /api/ships/military liefert alle Militaerschiffe
mit Klassifizierung und Bild-URLs.
2026-03-24 23:27:32 +01:00

162 Zeilen
6.5 KiB
Python

"""Schiffsverkehr-Collector: AISStream.io WebSocket (global, Echtzeit)."""
import asyncio
import json
import logging
import os
import time
import websockets
from fastapi import APIRouter
from milship_db import get_country_from_mmsi, classify_military_ship
logger = logging.getLogger("globe.ships")
router = APIRouter()
_AISSTREAM_KEY = os.getenv("AISSTREAM_KEY", "1a56b078db829727abd4d617937bae51c6f9973e")
_AISSTREAM_URL = "wss://stream.aisstream.io/v0/stream"
# {mmsi: {mmsi, lat, lon, sog, cog, heading, name, ts}}
_store: dict[int, dict] = {}
_connected = False
_task = None
async def _listener():
"""Dauerhafter WebSocket-Client fuer AISStream."""
global _connected
while True:
try:
logger.info("AISStream: Verbinde...")
async with websockets.connect(
_AISSTREAM_URL, ping_interval=30, ping_timeout=10, close_timeout=5
) as ws:
sub = {
"APIKey": _AISSTREAM_KEY,
"BoundingBoxes": [[[-90, -180], [90, 180]]],
"FilterMessageTypes": ["PositionReport"],
}
await ws.send(json.dumps(sub))
_connected = True
logger.info("AISStream: Verbunden")
async for raw in ws:
try:
text = raw.decode("utf-8") if isinstance(raw, bytes) else raw
msg = json.loads(text)
meta = msg.get("MetaData", {})
mmsi = meta.get("MMSI")
if not mmsi:
continue
pos = msg.get("Message", {}).get("PositionReport", {})
lat = meta.get("latitude") or pos.get("Latitude")
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),
"lon": round(lon, 5),
"sog": round(pos.get("Sog", 0), 1),
"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:
cutoff = time.time() - 900
stale = [k for k, v in _store.items() if v["ts"] < cutoff]
for k in stale:
del _store[k]
if len(_store) % 5000 == 0:
logger.info(f"AISStream: {len(_store)} Schiffe")
except Exception:
continue
except Exception as e:
_connected = False
logger.warning(f"AISStream Fehler: {e}. Reconnect in 10s...")
await asyncio.sleep(10)
def start_ais_collector():
global _task
if _task is None or _task.done():
_task = asyncio.create_task(_listener())
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():
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/military")
async def get_military_ships():
"""Alle Militaerschiffe mit Klassifizierung und Bildern."""
mil_ships = []
for s in _store.values():
if s.get("ship_type") == 35 or (isinstance(s.get("ship_type"), int) and 30 <= s["ship_type"] <= 39):
info = classify_military_ship(s.get("mmsi"), s.get("name",""))
mil_ships.append({
"mmsi": s["mmsi"],
"name": s.get("name", ""),
"lat": s["lat"], "lon": s["lon"],
"sog": s["sog"], "cog": s["cog"],
"country": info["country"],
"ship_class": info["class"],
"ship_type_detail": info["type"],
"image": info["image"],
})
return {"ships": mil_ships, "total": len(mil_ships)}
@router.get("/ships/dark")
async def get_dark_ships():
dark = _detect_dark_ships()
return {"dark_ships": dark, "total": len(dark)}