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.
Dieser Commit ist enthalten in:
Claude Dev
2026-03-24 23:27:32 +01:00
Ursprung 4b731823e6
Commit bfa74ec992
3 geänderte Dateien mit 243 neuen und 5 gelöschten Zeilen

Datei anzeigen

@@ -7,6 +7,7 @@ import time
import websockets import websockets
from fastapi import APIRouter from fastapi import APIRouter
from milship_db import get_country_from_mmsi, classify_military_ship
logger = logging.getLogger("globe.ships") logger = logging.getLogger("globe.ships")
router = APIRouter() router = APIRouter()
@@ -135,6 +136,25 @@ async def get_ships():
ships_out.append(ship) ships_out.append(ship)
return {"ships": ships_out, "total": len(ships_out), "connected": _connected} 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") @router.get("/ships/dark")
async def get_dark_ships(): async def get_dark_ships():
dark = _detect_dark_ships() dark = _detect_dark_ships()

202
src/milship_db.py Normale Datei
Datei anzeigen

@@ -0,0 +1,202 @@
"""Militaerschiff-Datenbank: MMSI-Zuordnung, Klassifizierung, Bilder."""
import json
import logging
import os
logger = logging.getLogger("globe.milships")
# MMSI-Laendercodes (MID = Maritime Identification Digits, erste 3 Ziffern)
MID_COUNTRIES = {
"201": "Albanien", "202": "Andorra", "203": "Oesterreich", "204": "Azoren",
"205": "Belgien", "206": "Belarus", "207": "Bulgarien", "208": "Vatikan",
"209": "Zypern", "210": "Zypern", "211": "Deutschland", "212": "Zypern",
"213": "Georgien", "214": "Moldawien", "215": "Malta", "216": "Armenien",
"218": "Deutschland", "219": "Daenemark", "220": "Daenemark",
"224": "Spanien", "225": "Spanien", "226": "Frankreich", "227": "Frankreich",
"228": "Frankreich", "229": "Malta", "230": "Finnland", "231": "Faeroeer",
"232": "UK", "233": "UK", "234": "UK", "235": "UK",
"236": "Gibraltar", "237": "Griechenland", "238": "Kroatien", "239": "Griechenland",
"240": "Griechenland", "241": "Griechenland", "242": "Marokko", "243": "Ungarn",
"244": "Niederlande", "245": "Niederlande", "246": "Niederlande",
"247": "Italien", "248": "Malta", "249": "Malta", "250": "Irland",
"251": "Island", "252": "Liechtenstein", "253": "Luxemburg", "254": "Madeira",
"255": "Portugal", "256": "Malta", "257": "Norwegen", "258": "Norwegen",
"259": "Norwegen", "261": "Polen", "263": "Portugal", "264": "Rumaenien",
"265": "Schweden", "266": "Schweden", "267": "Schweden", "268": "Schweiz",
"269": "Tschechien", "270": "Tschechien", "271": "Tuerkei", "272": "Ukraine",
"273": "Russland", "274": "Nordmazedonien", "275": "Lettland", "276": "Estland",
"277": "Litauen", "278": "Slowenien", "279": "Serbien",
"301": "Anguilla", "303": "Alaska", "304": "Antigua", "305": "Antigua",
"306": "Curacao", "307": "Aruba", "308": "Bahamas", "309": "Bahamas",
"310": "Bermuda", "311": "Bahamas", "312": "Belize",
"316": "Kanada", "319": "Cayman Islands",
"330": "Grenada", "331": "Groenland", "332": "Guatemala",
"338": "USA", "339": "Jamaica", "341": "St. Kitts",
"345": "Mexiko", "347": "Martinique",
"351": "Peru", "352": "Panama", "353": "Panama", "354": "Panama",
"355": "Panama", "356": "Panama", "357": "Panama",
"366": "USA", "367": "USA", "368": "USA", "369": "USA",
"370": "Panama", "371": "Panama", "372": "Panama", "373": "Panama",
"374": "Panama", "375": "Dominikanische Republik",
"376": "Bolivien", "377": "Trinidad",
"378": "Haiti", "379": "Honduras",
"401": "Afghanistan", "403": "Saudi-Arabien",
"405": "Bangladesch", "408": "Bahrain",
"410": "Bhutan", "412": "China", "413": "China", "414": "China",
"416": "Taiwan", "417": "Sri Lanka",
"419": "Indien", "422": "Iran", "423": "Aserbaidschan",
"425": "Irak", "428": "Israel",
"431": "Japan", "432": "Japan",
"434": "Turkmenistan", "436": "Kasachstan",
"437": "Usbekistan", "438": "Jordanien",
"440": "Suedkorea", "441": "Suedkorea",
"443": "Palaestina", "445": "Nordkorea",
"447": "Kuwait", "450": "Libanon",
"451": "Kirgistan", "453": "Macau",
"455": "Malediven", "457": "Mongolei",
"459": "Nepal", "461": "Oman",
"463": "Pakistan", "466": "Katar",
"468": "Syrien", "470": "VAE", "471": "VAE",
"472": "Tadschikistan", "473": "Jemen",
"477": "Hongkong",
"501": "Antarktis (FR)", "503": "Australien",
"506": "Myanmar", "508": "Brunei",
"510": "Mikronesien", "511": "Palau",
"512": "Neuseeland", "514": "Kambodscha",
"515": "Kambodscha", "516": "Weihnachtsinsel",
"518": "Cookinseln",
"520": "Fidschi", "523": "Cocos",
"525": "Indonesien", "529": "Kiribati",
"533": "Malaysia", "536": "Nordliche Marianen",
"538": "Marshallinseln",
"540": "Neukaledonien", "542": "Niue",
"544": "Nauru", "546": "Franzoesisch-Polynesien",
"548": "Philippinen",
"553": "Papua-Neuguinea", "555": "Pitcairn",
"557": "Salomonen",
"559": "Amerikanisch-Samoa", "561": "Samoa",
"563": "Singapur", "564": "Singapur", "565": "Singapur", "566": "Singapur",
"567": "Thailand", "570": "Tonga",
"572": "Tuvalu", "574": "Vietnam",
"576": "Vanuatu", "577": "Vanuatu",
"578": "Wallis",
"601": "Suedafrika", "603": "Angola",
"605": "Algerien", "607": "Mauritius",
"609": "Burundi", "610": "Benin",
"611": "Botsuana", "612": "Zentralafrikanische Republik",
"613": "Kamerun", "615": "Kongo",
"616": "Komoren", "617": "Kap Verde",
"618": "Antarktis",
"619": "Elfenbeinkueste", "620": "Komoren",
"621": "Dschibuti", "622": "Aegypten",
"624": "Aethiopien", "625": "Eritrea",
"626": "Gabun", "627": "Ghana",
"629": "Gambia", "630": "Guinea-Bissau",
"631": "Aequatorialguinea", "632": "Guinea",
"633": "Burkina Faso", "634": "Kenia",
"635": "Kerguelen",
"636": "Liberia", "637": "Liberia",
"638": "Suedsudan", "642": "Libyen",
"644": "Lesotho", "645": "Mauritius",
"647": "Madagaskar", "649": "Mali",
"650": "Mosambik", "654": "Mauretanien",
"655": "Malawi", "656": "Niger",
"657": "Nigeria", "659": "Namibia",
"660": "Reunion", "661": "Ruanda",
"662": "Sudan", "663": "Senegal",
"664": "Seychellen", "665": "St. Helena",
"666": "Somalia", "667": "Sierra Leone",
"668": "Sao Tome", "669": "Eswatini",
"670": "Tschad", "671": "Togo",
"672": "Tunesien", "674": "Tansania",
"675": "Uganda", "676": "Kongo",
"677": "Tansania", "678": "Sambia",
"679": "Simbabwe",
}
# Bekannte Militaerschiff-Klassen mit Bildern (Wikimedia Commons)
MILITARY_SHIP_DB = {
# Format: MMSI-Prefix oder exakte MMSI -> Schiffsdaten
# USA (MMSI 338*, 366-369*)
"_class_usa_carrier": {
"class": "Nimitz/Gerald R. Ford-Klasse",
"type": "Flugzeugtraeger",
"country": "USA",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e4/USS_Abraham_Lincoln_%28CVN-72%29_closeup.jpg/640px-USS_Abraham_Lincoln_%28CVN-72%29_closeup.jpg",
},
"_class_usa_destroyer": {
"class": "Arleigh-Burke-Klasse",
"type": "Zerstoerer",
"country": "USA",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/US_Navy_050715-N-8163B-003.jpg/640px-US_Navy_050715-N-8163B-003.jpg",
},
"_class_russia_frigate": {
"class": "Admiral Gorshkov-Klasse",
"type": "Fregatte",
"country": "Russland",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/52/Admiral_Gorshkov_frigate.jpg/640px-Admiral_Gorshkov_frigate.jpg",
},
"_class_china_destroyer": {
"class": "Type 052D",
"type": "Zerstoerer",
"country": "China",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/90/Type_052D_Kunming.jpg/640px-Type_052D_Kunming.jpg",
},
"_class_uk_carrier": {
"class": "Queen Elizabeth-Klasse",
"type": "Flugzeugtraeger",
"country": "UK",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e0/HMS_Queen_Elizabeth_in_Gibraltar_-_2018_%2828386226189%29.jpg/640px-HMS_Queen_Elizabeth_in_Gibraltar_-_2018_%2828386226189%29.jpg",
},
"_class_germany_frigate": {
"class": "Baden-Wuerttemberg-Klasse (F125)",
"type": "Fregatte",
"country": "Deutschland",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Fregatte_Baden-W%C3%BCrttemberg_F222_%28cropped%29.jpg/640px-Fregatte_Baden-W%C3%BCrttemberg_F222_%28cropped%29.jpg",
},
"_class_france_carrier": {
"class": "Charles de Gaulle (R91)",
"type": "Flugzeugtraeger",
"country": "Frankreich",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Charles_De_Gaulle_PEO.jpg/640px-Charles_De_Gaulle_PEO.jpg",
},
"_class_default_military": {
"class": "Militaerschiff",
"type": "Unbekannt",
"country": "",
"image": "",
},
}
def get_country_from_mmsi(mmsi):
"""Bestimmt das Land anhand der MMSI (erste 3 Ziffern)."""
if not mmsi:
return "Unbekannt"
mid = str(mmsi)[:3]
return MID_COUNTRIES.get(mid, "Unbekannt")
def classify_military_ship(mmsi, name=""):
"""Versucht ein Militaerschiff zu klassifizieren."""
country = get_country_from_mmsi(mmsi)
mid = str(mmsi)[:3] if mmsi else ""
name_upper = (name or "").upper()
# Laender-basierte Klassenzuordnung
if mid in ("338","366","367","368","369"): # USA
if any(w in name_upper for w in ("CVN","CARRIER","ENTERPRISE","FORD","NIMITZ","LINCOLN","WASHINGTON","REAGAN","BUSH","EISENHOWER","TRUMAN","STENNIS","VINSON")):
return {**MILITARY_SHIP_DB["_class_usa_carrier"], "country": country}
return {**MILITARY_SHIP_DB["_class_usa_destroyer"], "country": country}
if mid in ("273",): # Russland
return {**MILITARY_SHIP_DB["_class_russia_frigate"], "country": country}
if mid in ("412","413","414"): # China
return {**MILITARY_SHIP_DB["_class_china_destroyer"], "country": country}
if mid in ("232","233","234","235"): # UK
if any(w in name_upper for w in ("QUEEN","ELIZABETH","PRINCE","WALES")):
return {**MILITARY_SHIP_DB["_class_uk_carrier"], "country": country}
if mid in ("211","218"): # Deutschland
return {**MILITARY_SHIP_DB["_class_germany_frigate"], "country": country}
if mid in ("226","227","228"): # Frankreich
if any(w in name_upper for w in ("CHARLES","GAULLE")):
return {**MILITARY_SHIP_DB["_class_france_carrier"], "country": country}
return {"class": "Militaerschiff", "type": "Unbekannt", "country": country, "image": ""}

Datei anzeigen

@@ -175,15 +175,31 @@ const ShipsLayer = {
var name = best.name || ('MMSI ' + best.mmsi); var name = best.name || ('MMSI ' + best.mmsi);
var catLabels = { tanker:'Tanker', cargo:'Frachter', passenger:'Passagier', fishing:'Fischerei', military:'Militaer', other:'Sonstige' }; var catLabels = { tanker:'Tanker', cargo:'Frachter', passenger:'Passagier', fishing:'Fischerei', military:'Militaer', other:'Sonstige' };
this._viewer.trackedEntity = undefined; this._viewer.trackedEntity = undefined;
this._viewer.selectedEntity = new Cesium.Entity({ var baseHtml = '<div style="font-family:monospace;font-size:12px;padding:8px;color:' + this._getColor(best.category) + '">' +
name: name,
description: '<div style="font-family:monospace;font-size:12px;padding:8px;color:' + this._getColor(best.category) + '">' +
'<strong>' + name + '</strong><br>' + '<strong>' + name + '</strong><br>' +
'MMSI: ' + (best.mmsi||'?') + '<br>' + 'MMSI: ' + (best.mmsi||'?') + '<br>' +
'Typ: ' + (catLabels[best.category] || best.category || '?') + '<br>' + 'Typ: ' + (catLabels[best.category] || best.category || '?') + '<br>' +
'SOG: ' + (best.sog||0).toFixed(1) + ' kn | COG: ' + Math.round(best.cog||0) + '&deg;<br>' + 'SOG: ' + (best.sog||0).toFixed(1) + ' kn | COG: ' + Math.round(best.cog||0) + '&deg;<br>' +
'HDG: ' + (best.heading||'?') + '&deg;</div>', 'HDG: ' + (best.heading||'?') + '&deg;</div>';
}); this._viewer.selectedEntity = new Cesium.Entity({ name: name, description: baseHtml });
// Militaerschiff: Bild + Klassifizierung nachladen
if (best.category === 'military') {
var viewer = this._viewer;
fetch('/api/ships/military').then(function(r){return r.json()}).then(function(data){
var mil = (data.ships||[]).find(function(m){return m.mmsi===best.mmsi});
if (mil && viewer.selectedEntity) {
var imgHtml = mil.image ? '<img src="' + mil.image + '" style="width:100%;max-height:180px;object-fit:cover;border-radius:6px;margin-bottom:8px">' : '';
viewer.selectedEntity.description = '<div style="font-family:monospace;font-size:12px;padding:8px">' +
imgHtml +
'<strong style="color:#ff44ff;font-size:14px">' + (mil.name||name) + '</strong><br>' +
'<span style="color:#ccc">' + mil.ship_class + '</span><br>' +
'<span style="color:#aaa">' + mil.ship_type_detail + '</span><br>' +
'<span style="color:#888">Land: ' + mil.country + '</span><br>' +
'<span style="color:#888">MMSI: ' + best.mmsi + '</span><br>' +
'SOG: ' + (best.sog||0).toFixed(1) + ' kn | COG: ' + Math.round(best.cog||0) + '&deg;</div>';
}
}).catch(function(){});
}
} }
}, },