diff --git a/src/data_ships.py b/src/data_ships.py index d9a2a91..7b7ec6a 100644 --- a/src/data_ships.py +++ b/src/data_ships.py @@ -7,6 +7,7 @@ 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() @@ -135,6 +136,25 @@ async def get_ships(): 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() diff --git a/src/milship_db.py b/src/milship_db.py new file mode 100644 index 0000000..e123a2b --- /dev/null +++ b/src/milship_db.py @@ -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": ""} diff --git a/static/js/layers/ships.js b/static/js/layers/ships.js index 84a9250..c99683b 100644 --- a/static/js/layers/ships.js +++ b/static/js/layers/ships.js @@ -175,15 +175,31 @@ const ShipsLayer = { var name = best.name || ('MMSI ' + best.mmsi); var catLabels = { tanker:'Tanker', cargo:'Frachter', passenger:'Passagier', fishing:'Fischerei', military:'Militaer', other:'Sonstige' }; this._viewer.trackedEntity = undefined; - this._viewer.selectedEntity = new Cesium.Entity({ - name: name, - description: '
' + + var baseHtml = '
' + '' + name + '
' + 'MMSI: ' + (best.mmsi||'?') + '
' + 'Typ: ' + (catLabels[best.category] || best.category || '?') + '
' + 'SOG: ' + (best.sog||0).toFixed(1) + ' kn | COG: ' + Math.round(best.cog||0) + '°
' + - 'HDG: ' + (best.heading||'?') + '°
', - }); + 'HDG: ' + (best.heading||'?') + '°
'; + 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 ? '' : ''; + viewer.selectedEntity.description = '
' + + imgHtml + + '' + (mil.name||name) + '
' + + '' + mil.ship_class + '
' + + '' + mil.ship_type_detail + '
' + + 'Land: ' + mil.country + '
' + + 'MMSI: ' + best.mmsi + '
' + + 'SOG: ' + (best.sog||0).toFixed(1) + ' kn | COG: ' + Math.round(best.cog||0) + '°
'; + } + }).catch(function(){}); + } } },