Checkboxen 16x16px, Dots 10px, Labels 13px, mehr Padding. Identisch zum uebergeordneten Layer-Panel.
223 Zeilen
11 KiB
JavaScript
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:11px;color:var(--accent);letter-spacing:2px;margin-bottom:6px">SCHIFFSTYPEN</div>';
|
|
cats.forEach(function(c) {
|
|
html += '<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text);cursor:pointer;padding:5px 0">' +
|
|
'<input type="checkbox" checked onchange="ShipsLayer.toggleFilter(\'' + c.id + '\',this.checked)" style="accent-color:' + c.color + ';width:16px;height:16px;cursor:pointer">' +
|
|
'<span style="width:10px;height:10px;border-radius:50%;background:' + c.color + '"></span>' + c.label + '</label>';
|
|
});
|
|
html += '<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text);cursor:pointer;padding:6px 0 0;border-top:1px solid rgba(255,255,255,0.06);margin-top:4px">' +
|
|
'<input type="checkbox" onchange="ShipsLayer.toggleProjection(this.checked)" style="accent-color:var(--accent);width:16px;height:16px;cursor:pointer">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' ? 5 : level === 'medium' ? 4 : 3;
|
|
|
|
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) + '°<br>' +
|
|
'HDG: ' + (best.heading||'?') + '°</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) + '°</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); });
|
|
},
|
|
};
|