/**
* 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 = '
SCHIFFSTYPEN
';
cats.forEach(function(c) {
html += '';
});
html += '';
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' ? 7 : level === 'medium' ? 5 : 4;
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 = '' +
'' + 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||'?') + '°
';
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(){});
}
}
},
_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); });
},
};