Dateien
AegisSight-Globe/static/js/layers/ships.js
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

223 Zeilen
11 KiB
JavaScript

/**
* Schiffsverkehr: Typ-Filter, Routen-Projektion, Dark Ships.
*/
const ShipsLayer = {
_viewer: null,
_points: null,
_projLines: null,
_labels: null,
_interval: null,
_count: 0,
_data: [],
_filters: { tanker: true, cargo: true, passenger: true, fishing: true, military: true, other: true },
_showProjection: false,
_cameraListener: null,
_lastZoomLevel: null,
_handler: null,
start(viewer) {
if (this._points) return;
this._viewer = viewer;
this._points = viewer.scene.primitives.add(new Cesium.PointPrimitiveCollection());
this._projLines = viewer.scene.primitives.add(new Cesium.PolylineCollection());
this._labels = viewer.scene.primitives.add(new Cesium.LabelCollection());
this._fetch();
var self = this;
this._interval = setInterval(function() { self._fetch(); }, 60000);
this._cameraListener = function() { self._renderForZoom(); };
viewer.camera.changed.addEventListener(this._cameraListener);
this._handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
this._handler.setInputAction(function(c) { self._onClick(c.position); }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
this._createFilterUI();
},
stop() {
if (this._interval) { clearInterval(this._interval); this._interval = null; }
if (this._cameraListener && this._viewer) { this._viewer.camera.changed.removeEventListener(this._cameraListener); }
if (this._handler) { this._handler.destroy(); this._handler = null; }
if (this._points && this._viewer) { this._viewer.scene.primitives.remove(this._points); this._points = null; }
if (this._projLines && this._viewer) { this._viewer.scene.primitives.remove(this._projLines); this._projLines = null; }
if (this._labels && this._viewer) { this._viewer.scene.primitives.remove(this._labels); this._labels = null; }
var filterEl = document.getElementById('ship-filters');
if (filterEl) filterEl.style.display = 'none';
this._count = 0; this._data = []; this._lastZoomLevel = null;
},
_createFilterUI() {
var el = document.getElementById('ship-filters');
if (!el) return;
var cats = [
{ id: 'tanker', label: 'Tanker', color: '#ff4444' },
{ id: 'cargo', label: 'Cargo', color: '#ffaa00' },
{ id: 'passenger', label: 'Passagier', color: '#44ff44' },
{ id: 'fishing', label: 'Fischerei', color: '#44aaff' },
{ id: 'military', label: 'Militaer', color: '#ff44ff' },
{ id: 'other', label: 'Sonstige', color: '#888888' },
];
var self = this;
var html = '<div style="font-size:9px;color:var(--accent);letter-spacing:1px;margin-bottom:4px">SCHIFFSTYPEN</div>';
cats.forEach(function(c) {
html += '<label style="display:flex;align-items:center;gap:4px;font-size:10px;color:var(--text);cursor:pointer;padding:1px 0">' +
'<input type="checkbox" checked onchange="ShipsLayer.toggleFilter(\'' + c.id + '\',this.checked)" style="accent-color:' + c.color + ';width:11px;height:11px">' +
'<span style="width:6px;height:6px;border-radius:50%;background:' + c.color + '"></span>' + c.label + '</label>';
});
html += '<label style="display:flex;align-items:center;gap:4px;font-size:10px;color:var(--text);cursor:pointer;padding:3px 0 0;border-top:1px solid rgba(255,255,255,0.06);margin-top:3px">' +
'<input type="checkbox" onchange="ShipsLayer.toggleProjection(this.checked)" style="accent-color:var(--accent);width:11px;height:11px">Kurslinien</label>';
el.innerHTML = html;
el.style.display = 'block';
},
toggleFilter(cat, on) {
this._filters[cat] = on;
this._lastZoomLevel = null;
this._render();
},
toggleProjection(on) {
this._showProjection = on;
this._render();
},
_getColor(cat) {
var colors = {
tanker: '#ff4444', cargo: '#ffaa00', passenger: '#44ff44',
fishing: '#44aaff', military: '#ff44ff', other: '#888888', unknown: '#666666',
};
return colors[cat] || colors.other;
},
_getZoomLevel() {
var alt = this._viewer.camera.positionCartographic.height;
if (alt > 5000000) return 'far';
if (alt > 1000000) return 'medium';
return 'close';
},
_renderForZoom() {
var level = this._getZoomLevel();
if (level === this._lastZoomLevel) return;
this._lastZoomLevel = level;
this._render();
},
_render() {
if (!this._points || !this._data.length) return;
this._points.removeAll();
this._projLines.removeAll();
this._labels.removeAll();
var bounds = this._viewer.camera.computeViewRectangle();
var level = this._getZoomLevel();
var max = level === 'far' ? 300 : level === 'medium' ? 800 : 2000;
var self = this;
var count = 0;
for (var i = 0; i < this._data.length && count < max; i++) {
var s = this._data[i];
if (!s.lat || !s.lon) continue;
var cat = s.category || 'other';
if (!this._filters[cat] && cat !== 'unknown') continue;
count++;
var colorStr = this._getColor(cat);
var color = Cesium.Color.fromCssColorString(colorStr);
var r = level === 'close' ? 3 : level === 'medium' ? 2.5 : 2;
this._points.add({
position: Cesium.Cartesian3.fromDegrees(s.lon, s.lat, 0),
pixelSize: r, color: color,
});
// Kurslinie (Projektion 30min voraus)
if (this._showProjection && s.sog > 0.5 && s.cog !== undefined && level !== 'far') {
var distNm = s.sog * 0.5; // 30 Minuten
var distDeg = distNm / 60;
var cogRad = (s.cog || 0) * Math.PI / 180;
var endLat = s.lat + distDeg * Math.cos(cogRad);
var endLon = s.lon + distDeg * Math.sin(cogRad);
this._projLines.add({
positions: [
Cesium.Cartesian3.fromDegrees(s.lon, s.lat, 0),
Cesium.Cartesian3.fromDegrees(endLon, endLat, 0),
],
width: 1,
material: Cesium.Material.fromType('Color', { color: color.withAlpha(0.3) }),
});
}
// Labels bei Zoom
if (level === 'close' && s.name) {
this._labels.add({
position: Cesium.Cartesian3.fromDegrees(s.lon, s.lat, 0),
text: s.name, font: '9px monospace',
fillColor: color.withAlpha(0.7),
outlineColor: Cesium.Color.BLACK, outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(5, -3), scale: 0.6,
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 300000),
});
}
}
},
_onClick(pos) {
if (!this._viewer || !this._data.length) return;
var cart = this._viewer.scene.pickPosition(pos);
if (!cart) { var ray = this._viewer.scene.camera.getPickRay(pos); cart = this._viewer.scene.globe.pick(ray, this._viewer.scene); }
if (!cart) return;
var c = Cesium.Cartographic.fromCartesian(cart);
var lat = Cesium.Math.toDegrees(c.latitude), lon = Cesium.Math.toDegrees(c.longitude);
var best = null, bd = 999;
for (var i = 0; i < this._data.length; i++) {
var s = this._data[i], d = Math.abs(s.lat-lat)+Math.abs(s.lon-lon);
if (d < bd) { bd = d; best = s; }
}
if (best && bd < 0.5) {
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;
var baseHtml = '<div style="font-family:monospace;font-size:12px;padding:8px;color:' + this._getColor(best.category) + '">' +
'<strong>' + name + '</strong><br>' +
'MMSI: ' + (best.mmsi||'?') + '<br>' +
'Typ: ' + (catLabels[best.category] || best.category || '?') + '<br>' +
'SOG: ' + (best.sog||0).toFixed(1) + ' kn | COG: ' + Math.round(best.cog||0) + '&deg;<br>' +
'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(){});
}
}
},
_fetch() {
var self = this;
var statusEl = document.getElementById('status-ships');
if (statusEl) { statusEl.textContent = 'Lade...'; statusEl.classList.add('active'); }
fetch('/api/ships')
.then(function(r) { return r.json(); })
.then(function(data) {
self._data = data.ships || [];
self._count = self._data.length;
self._lastZoomLevel = null;
self._render();
if (statusEl) statusEl.textContent = self._count.toLocaleString('de-DE') + ' Schiffe';
})
.catch(function(e) { console.warn('Ships:', e); if (statusEl) statusEl.textContent = 'Fehler'; })
.finally(function() { setTimeout(function() { if (statusEl) statusEl.classList.remove('active'); }, 5000); });
},
};