geoint.js komplett neu geschrieben (Syntax-Fehler durch Patch-Chaos)
Vorherige inkrementelle Patches hatten die Dateistruktur korrumpiert (orphaned code-Fragmente, fehlende Klammern). Kompletter Neuschrieb mit allen Features: Flug, Schiff, Erdbeben, GDELT, Heatmap, Koordinaten, Distanz, Timeline. Saubere Syntax verifiziert.
Dieser Commit ist enthalten in:
@@ -1,47 +1,34 @@
|
|||||||
/**
|
/**
|
||||||
* GEOINT-Modus: Taktische Kartenansicht mit Echtzeit-Datenlayern.
|
* GEOINT-Modus: Taktische Kartenansicht mit Echtzeit-Datenlayern.
|
||||||
* Eigenstaendiges Modul — alle GEOINT-Logik gekapselt.
|
|
||||||
*/
|
*/
|
||||||
const GEOINT = {
|
const GEOINT = {
|
||||||
_active: false,
|
_active: false,
|
||||||
_map: null,
|
_map: null,
|
||||||
_sublayers: {},
|
_sublayers: {},
|
||||||
_flightLayer: null,
|
_canvasRenderer: null,
|
||||||
_quakeLayer: null,
|
// Layer references
|
||||||
_gdeltLayer: null,
|
_flightLayer: null, _quakeLayer: null, _gdeltLayer: null,
|
||||||
_heatLayer: null,
|
_heatLayer: null, _shipsLayer: null,
|
||||||
_flightInterval: null,
|
// Data caches
|
||||||
_quakeInterval: null,
|
_flightsData: null, _shipsData: null,
|
||||||
_gdeltInterval: null,
|
// Intervals
|
||||||
_shipsLayer: null,
|
_flightInterval: null, _quakeInterval: null, _gdeltInterval: null, _shipsInterval: null,
|
||||||
_flightsData: null,
|
|
||||||
_shipsData: null,
|
|
||||||
_shipsInterval: null,
|
|
||||||
_flightFetching: false,
|
_flightFetching: false,
|
||||||
_moveHandler: null,
|
// UI controls
|
||||||
_coordControl: null,
|
_moveHandler: null, _subControl: null,
|
||||||
_coordHandler: null,
|
_coordControl: null, _coordHandler: null,
|
||||||
_distanceActive: false,
|
_distanceLayers: null, _distancePoints: [], _distanceHandler: null,
|
||||||
_distancePoints: [],
|
_satTileLayer: null, _satLabelLayer: null,
|
||||||
_distanceLayers: null,
|
|
||||||
_distanceHandler: null,
|
|
||||||
_subControl: null,
|
|
||||||
_osmTileLayer: null,
|
|
||||||
_satTileLayer: null,
|
|
||||||
_satLabelLayer: null,
|
|
||||||
_timelineData: null,
|
_timelineData: null,
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// === HAUPTSCHALTER =====================================================
|
||||||
// Hauptschalter
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
toggle(enabled, map) {
|
toggle(enabled, map) {
|
||||||
if (!map) map = this._map;
|
if (!map) map = this._map;
|
||||||
if (!map && typeof UI !== 'undefined') map = UI._map;
|
if (!map && typeof UI !== 'undefined') map = UI._map;
|
||||||
if (!map) { console.warn('GEOINT: keine Karte verfuegbar'); return; }
|
if (!map) return;
|
||||||
this._active = enabled;
|
this._active = enabled;
|
||||||
this._map = map;
|
this._map = map;
|
||||||
|
|
||||||
// CSS-Klassen
|
|
||||||
var container = document.getElementById('map-container');
|
var container = document.getElementById('map-container');
|
||||||
if (container) container.classList.toggle('geoint-active', enabled);
|
if (container) container.classList.toggle('geoint-active', enabled);
|
||||||
var fsContainer = document.getElementById('map-fullscreen-container');
|
var fsContainer = document.getElementById('map-fullscreen-container');
|
||||||
@@ -49,158 +36,120 @@ const GEOINT = {
|
|||||||
var card = container ? container.closest('.map-card') : null;
|
var card = container ? container.closest('.map-card') : null;
|
||||||
if (card) card.classList.toggle('geoint-card-active', enabled);
|
if (card) card.classList.toggle('geoint-card-active', enabled);
|
||||||
|
|
||||||
// Sync beider Checkboxen
|
document.querySelectorAll('#geoint-mode-cb, #geoint-mode-cb-fs').forEach(function(cb) { cb.checked = enabled; });
|
||||||
var cb1 = document.getElementById('geoint-mode-cb');
|
|
||||||
var cb2 = document.getElementById('geoint-mode-cb-fs');
|
|
||||||
if (cb1) cb1.checked = enabled;
|
|
||||||
if (cb2) cb2.checked = enabled;
|
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
if (!this._canvasRenderer) this._canvasRenderer = L.canvas({ padding: 0.5 });
|
if (!this._canvasRenderer) this._canvasRenderer = L.canvas({ padding: 0.5 });
|
||||||
this._applySatelliteTiles(map);
|
this._applySatelliteTiles(map);
|
||||||
this._createSubControl(map);
|
this._createSubControl(map);
|
||||||
this._restoreSublayers(map);
|
this._restoreSublayers(map);
|
||||||
// Timeline anzeigen wenn Daten vorhanden
|
|
||||||
var tl = document.getElementById('geoint-timeline');
|
|
||||||
if (tl) tl.style.display = '';
|
|
||||||
} else {
|
} else {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
this._restoreOsmTiles(map);
|
this._restoreOsmTiles(map);
|
||||||
var tl = document.getElementById('geoint-timeline');
|
|
||||||
if (tl) tl.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._saveState();
|
this._saveState();
|
||||||
},
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// === TILES ==============================================================
|
||||||
// Tile-Management
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
_applySatelliteTiles(map) {
|
_applySatelliteTiles(map) {
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
// Bestehende Tile-Layer in separatem Array sammeln, dann entfernen
|
|
||||||
var toRemove = [];
|
var toRemove = [];
|
||||||
map.eachLayer(function(layer) {
|
map.eachLayer(function(l) { if (l instanceof L.TileLayer) toRemove.push(l); });
|
||||||
if (layer instanceof L.TileLayer) toRemove.push(layer);
|
toRemove.forEach(function(l) { map.removeLayer(l); });
|
||||||
});
|
|
||||||
toRemove.forEach(function(layer) { map.removeLayer(layer); });
|
|
||||||
// Esri World Imagery
|
|
||||||
this._satTileLayer = L.tileLayer(
|
this._satTileLayer = L.tileLayer(
|
||||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||||
{ attribution: 'Tiles © Esri', maxZoom: 19, noWrap: true }
|
{ attribution: 'Tiles © Esri', maxZoom: 19, noWrap: true }
|
||||||
).addTo(map);
|
).addTo(map);
|
||||||
// Ortsnamen-Overlay
|
|
||||||
this._satLabelLayer = L.tileLayer(
|
this._satLabelLayer = L.tileLayer(
|
||||||
'https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}',
|
'https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}',
|
||||||
{ maxZoom: 19, noWrap: true }
|
{ maxZoom: 19, noWrap: true }
|
||||||
).addTo(map);
|
).addTo(map);
|
||||||
// Satellite nach hinten, damit Marker darueber liegen
|
|
||||||
if (this._satTileLayer.bringToBack) this._satTileLayer.bringToBack();
|
if (this._satTileLayer.bringToBack) this._satTileLayer.bringToBack();
|
||||||
},
|
},
|
||||||
|
|
||||||
_restoreOsmTiles(map) {
|
_restoreOsmTiles(map) {
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
map.eachLayer(function(layer) {
|
var toRemove = [];
|
||||||
if (layer instanceof L.TileLayer) map.removeLayer(layer);
|
map.eachLayer(function(l) { if (l instanceof L.TileLayer) toRemove.push(l); });
|
||||||
});
|
toRemove.forEach(function(l) { map.removeLayer(l); });
|
||||||
// UI._applyMapTiles() wiederherstellen
|
|
||||||
if (typeof UI !== 'undefined' && UI._applyMapTiles) {
|
if (typeof UI !== 'undefined' && UI._applyMapTiles) {
|
||||||
UI._applyMapTiles();
|
UI._applyMapTiles();
|
||||||
} else {
|
} else {
|
||||||
L.tileLayer('https://tile.openstreetmap.de/{z}/{x}/{y}.png', {
|
L.tileLayer('https://tile.openstreetmap.de/{z}/{x}/{y}.png', {
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
attribution: '© OpenStreetMap', maxZoom: 18, noWrap: true
|
||||||
maxZoom: 18, noWrap: true
|
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// === SUB-LAYER CONTROL ==================================================
|
||||||
// Sub-Layer Control Panel
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
_createSubControl(map) {
|
_createSubControl(map) {
|
||||||
if (this._subControl) return;
|
if (this._subControl) return;
|
||||||
var self = this;
|
var self = this;
|
||||||
var SubControl = L.Control.extend({
|
var items = [
|
||||||
|
['flights', 'Flugverkehr', 'flights'],
|
||||||
|
['ships', 'Schiffsverkehr', 'ships'],
|
||||||
|
['quakes', 'Erdbeben', 'quakes'],
|
||||||
|
['gdelt', 'Nachrichten', 'gdelt'],
|
||||||
|
['_sep'],
|
||||||
|
['heatmap', 'Heatmap', 'heatmap'],
|
||||||
|
['coords', 'Koordinaten', 'coords'],
|
||||||
|
['distance', 'Distanz', 'distance'],
|
||||||
|
];
|
||||||
|
var SubCtrl = L.Control.extend({
|
||||||
options: { position: 'topright' },
|
options: { position: 'topright' },
|
||||||
onAdd: function() {
|
onAdd: function() {
|
||||||
var div = L.DomUtil.create('div', 'geoint-sub-control');
|
var div = L.DomUtil.create('div', 'geoint-sub-control');
|
||||||
L.DomEvent.disableClickPropagation(div);
|
L.DomEvent.disableClickPropagation(div);
|
||||||
L.DomEvent.disableScrollPropagation(div);
|
L.DomEvent.disableScrollPropagation(div);
|
||||||
div.innerHTML =
|
var html = '<h4>GEOINT Layer</h4>';
|
||||||
'<h4>GEOINT Layer</h4>' +
|
items.forEach(function(it) {
|
||||||
self._subItemHtml('flights', 'Flugverkehr', 'flights') +
|
if (it[0] === '_sep') { html += '<div class="geoint-sub-separator"></div>'; return; }
|
||||||
self._subItemHtml('ships', 'Schiffsverkehr', 'ships') +
|
var checked = self._sublayers[it[0]] ? ' checked' : '';
|
||||||
self._subItemHtml('quakes', 'Erdbeben', 'quakes') +
|
html += '<div class="geoint-sub-item">' +
|
||||||
self._subItemHtml('gdelt', 'Nachrichten', 'gdelt') +
|
'<input type="checkbox" id="geoint-sub-' + it[0] + '"' + checked + '>' +
|
||||||
'<div class="geoint-sub-separator"></div>' +
|
'<label for="geoint-sub-' + it[0] + '"><span class="geoint-dot geoint-dot-' + it[2] + '"></span>' + it[1] + '</label></div>';
|
||||||
self._subItemHtml('heatmap', 'Heatmap', 'heatmap') +
|
});
|
||||||
self._subItemHtml('coords', 'Koordinaten', 'coords') +
|
div.innerHTML = html;
|
||||||
self._subItemHtml('distance', 'Distanz', 'distance');
|
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this._subControl = new SubControl();
|
this._subControl = new SubCtrl();
|
||||||
map.addControl(this._subControl);
|
map.addControl(this._subControl);
|
||||||
|
items.forEach(function(it) {
|
||||||
// Event-Listener fuer Sub-Checkboxen
|
if (it[0] === '_sep') return;
|
||||||
['flights', 'ships', 'quakes', 'gdelt', 'heatmap', 'coords', 'distance'].forEach(function(id) {
|
var cb = document.getElementById('geoint-sub-' + it[0]);
|
||||||
var cb = document.getElementById('geoint-sub-' + id);
|
if (cb) cb.addEventListener('change', function() { self._toggleSub(it[0], this.checked, map); });
|
||||||
if (cb) {
|
|
||||||
cb.addEventListener('change', function() {
|
|
||||||
self._toggleSublayer(id, this.checked, map);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_subItemHtml(id, label, dotClass) {
|
|
||||||
var checked = this._sublayers[id] ? ' checked' : '';
|
|
||||||
return '<div class="geoint-sub-item">' +
|
|
||||||
'<input type="checkbox" id="geoint-sub-' + id + '"' + checked + '>' +
|
|
||||||
'<label for="geoint-sub-' + id + '"><span class="geoint-dot geoint-dot-' + dotClass + '"></span>' + label + '</label>' +
|
|
||||||
'</div>';
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_removeSubControl() {
|
_removeSubControl() {
|
||||||
if (this._subControl && this._map) {
|
if (this._subControl && this._map) { this._map.removeControl(this._subControl); this._subControl = null; }
|
||||||
this._map.removeControl(this._subControl);
|
|
||||||
this._subControl = null;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_toggleSublayer(id, enabled, map) {
|
_toggleSub(id, on, map) {
|
||||||
this._sublayers[id] = enabled;
|
this._sublayers[id] = on;
|
||||||
this._saveState();
|
this._saveState();
|
||||||
switch (id) {
|
var m = { flights: ['_startFlights','_stopFlights'], ships: ['_startShips','_stopShips'],
|
||||||
case 'flights': enabled ? this._startFlights(map) : this._stopFlights(); break;
|
quakes: ['_startQuakes','_stopQuakes'], gdelt: ['_startGdelt','_stopGdelt'],
|
||||||
case 'ships': enabled ? this._startShips(map) : this._stopShips(); break;
|
heatmap: ['_startHeatmap','_stopHeatmap'], coords: ['_startCoords','_stopCoords'],
|
||||||
case 'quakes': enabled ? this._startQuakes(map) : this._stopQuakes(); break;
|
distance: ['_startDistance','_stopDistance'] };
|
||||||
case 'gdelt': enabled ? this._startGdelt(map) : this._stopGdelt(); break;
|
if (m[id]) this[m[id][on ? 0 : 1]](map);
|
||||||
case 'heatmap': enabled ? this._startHeatmap(map) : this._stopHeatmap(); break;
|
|
||||||
case 'coords': enabled ? this._startCoords(map) : this._stopCoords(); break;
|
|
||||||
case 'distance': enabled ? this._startDistance(map) : this._stopDistance(); break;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_restoreSublayers(map) {
|
_restoreSublayers(map) {
|
||||||
var self = this;
|
var self = this;
|
||||||
Object.keys(this._sublayers).forEach(function(id) {
|
Object.keys(this._sublayers).forEach(function(id) {
|
||||||
if (self._sublayers[id]) {
|
if (self._sublayers[id]) self._toggleSub(id, true, map);
|
||||||
self._toggleSublayer(id, true, map);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// === FLUGVERKEHR ========================================================
|
||||||
// Layer: Flugverkehr
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
_startFlights(map) {
|
_startFlights(map) {
|
||||||
if (this._flightLayer) return;
|
if (this._flightLayer) return;
|
||||||
this._flightLayer = L.layerGroup().addTo(map);
|
this._flightLayer = L.layerGroup().addTo(map);
|
||||||
var self = this;
|
var self = this;
|
||||||
this._fetchFlights(map);
|
this._fetchFlights(map);
|
||||||
this._flightInterval = setInterval(function() { self._fetchFlights(map); }, 30000); // 30s global refresh
|
this._flightInterval = setInterval(function() { self._fetchFlights(map); }, 30000);
|
||||||
// Bei Kartenbewegung neu rendern (client-seitig aus Cache)
|
|
||||||
this._moveHandler = function() {
|
this._moveHandler = function() {
|
||||||
clearTimeout(self._moveDebounce);
|
clearTimeout(self._moveDebounce);
|
||||||
self._moveDebounce = setTimeout(function() {
|
self._moveDebounce = setTimeout(function() {
|
||||||
@@ -222,37 +171,13 @@ const GEOINT = {
|
|||||||
this._flightFetching = true;
|
this._flightFetching = true;
|
||||||
var self = this;
|
var self = this;
|
||||||
var token = localStorage.getItem('osint_token') || '';
|
var token = localStorage.getItem('osint_token') || '';
|
||||||
var headers = token ? { 'Authorization': 'Bearer ' + token } : {};
|
fetch('/api/geoint/flights', { headers: token ? { 'Authorization': 'Bearer ' + token } : {} })
|
||||||
|
|
||||||
fetch('/api/geoint/flights', { headers: headers })
|
|
||||||
.then(function(r) { return r.ok ? r.json() : { ac: [] }; })
|
.then(function(r) { return r.ok ? r.json() : { ac: [] }; })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (!self._flightLayer) return;
|
|
||||||
self._flightsData = data.ac || data.aircraft || [];
|
self._flightsData = data.ac || data.aircraft || [];
|
||||||
self._renderFlights(map);
|
self._renderFlights(map);
|
||||||
})
|
})
|
||||||
var heading = a.track || a.true_heading || 0;
|
.catch(function() {})
|
||||||
var icon = L.divIcon({
|
|
||||||
className: '',
|
|
||||||
html: '<div class="geoint-aircraft" style="transform:rotate(' + heading + 'deg)">' +
|
|
||||||
'<svg viewBox="0 0 24 24" fill="#00ff88" stroke="#004422" stroke-width="1">' +
|
|
||||||
'<path d="M12 2L8 10h-4l2 4-2 4h4l4 4 4-4h4l-2-4 2-4h-4z"/>' +
|
|
||||||
'</svg></div>',
|
|
||||||
iconSize: [14, 14],
|
|
||||||
iconAnchor: [7, 7],
|
|
||||||
});
|
|
||||||
var callsign = (a.flight || a.callsign || a.hex || '???').trim();
|
|
||||||
var alt = a.alt_baro || a.altitude || '?';
|
|
||||||
var spd = a.gs || a.ground_speed || '?';
|
|
||||||
var typ = a.t || a.type || '';
|
|
||||||
var popup = '<div class="geoint-popup">' +
|
|
||||||
'<strong>' + callsign + '</strong>' +
|
|
||||||
(typ ? ' <span style="opacity:0.5">(' + typ + ')</span>' : '') +
|
|
||||||
'<br><span class="geoint-popup-key">ALT</span> ' + (typeof alt === 'number' ? alt.toLocaleString() + ' ft' : alt) +
|
|
||||||
'<br><span class="geoint-popup-key">SPD</span> ' + (typeof spd === 'number' ? Math.round(spd) + ' kts' : spd) +
|
|
||||||
'<br><span class="geoint-popup-key">HDG</span> ' + Math.round(heading) + '°' +
|
|
||||||
'</div>';
|
|
||||||
.catch(function(e) { if (typeof DEV_MODE !== 'undefined' && DEV_MODE) console.warn('GEOINT flights:', e); })
|
|
||||||
.finally(function() { self._flightFetching = false; });
|
.finally(function() { self._flightFetching = false; });
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -261,45 +186,35 @@ const GEOINT = {
|
|||||||
var newLayer = L.layerGroup();
|
var newLayer = L.layerGroup();
|
||||||
var bounds = map.getBounds();
|
var bounds = map.getBounds();
|
||||||
var zoom = map.getZoom();
|
var zoom = map.getZoom();
|
||||||
// Zoom-adaptiv: weniger bei Uebersicht, mehr bei Detail
|
var max = zoom >= 10 ? 600 : zoom >= 7 ? 400 : zoom >= 5 ? 200 : 80;
|
||||||
var maxMarkers = zoom >= 10 ? 600 : zoom >= 7 ? 400 : zoom >= 5 ? 200 : 80;
|
var r = zoom >= 10 ? 4 : zoom >= 7 ? 3 : 2;
|
||||||
var markerSize = zoom >= 10 ? 4 : zoom >= 7 ? 3 : 2;
|
|
||||||
var count = 0;
|
var count = 0;
|
||||||
for (var i = 0; i < this._flightsData.length && count < maxMarkers; i++) {
|
for (var i = 0; i < this._flightsData.length && count < max; i++) {
|
||||||
var a = this._flightsData[i];
|
var a = this._flightsData[i];
|
||||||
if (!a.lat || !a.lon || !bounds.contains([a.lat, a.lon])) continue;
|
if (!a.lat || !a.lon || !bounds.contains([a.lat, a.lon])) continue;
|
||||||
count++;
|
count++;
|
||||||
var callsign = (a.flight || a.callsign || a.hex || '???').trim();
|
var cs = (a.flight || a.callsign || a.hex || '???').trim();
|
||||||
var alt = a.alt_baro || a.altitude || '?';
|
var alt = a.alt_baro || a.altitude || '?';
|
||||||
var spd = a.gs || a.ground_speed || '?';
|
var spd = a.gs || a.ground_speed || '?';
|
||||||
var typ = a.t || a.type || '';
|
var popup = '<div class="geoint-popup"><strong>' + cs + '</strong>' +
|
||||||
var popup = '<div class="geoint-popup">' +
|
|
||||||
'<strong>' + callsign + '</strong>' +
|
|
||||||
(typ ? ' <span style="opacity:0.6">(' + typ + ')</span>' : '') +
|
|
||||||
'<br><span class="geoint-popup-key">ALT</span> ' + (typeof alt === 'number' ? alt.toLocaleString() + ' ft' : alt) +
|
'<br><span class="geoint-popup-key">ALT</span> ' + (typeof alt === 'number' ? alt.toLocaleString() + ' ft' : alt) +
|
||||||
'<br><span class="geoint-popup-key">SPD</span> ' + (typeof spd === 'number' ? Math.round(spd) + ' kts' : spd) +
|
'<br><span class="geoint-popup-key">SPD</span> ' + (typeof spd === 'number' ? Math.round(spd) + ' kts' : spd) + '</div>';
|
||||||
'</div>';
|
|
||||||
L.circleMarker([a.lat, a.lon], {
|
L.circleMarker([a.lat, a.lon], {
|
||||||
radius: markerSize, fillColor: '#00ff88', color: '#004422',
|
radius: r, fillColor: '#00ff88', color: '#004422',
|
||||||
fillOpacity: 0.9, weight: 1, renderer: this._canvasRenderer
|
fillOpacity: 0.9, weight: 1, renderer: this._canvasRenderer
|
||||||
}).bindPopup(popup, { className: 'geoint-leaflet-popup' }).addTo(newLayer);
|
}).bindPopup(popup, { className: 'geoint-leaflet-popup' }).addTo(newLayer);
|
||||||
}
|
}
|
||||||
if (this._map) {
|
|
||||||
this._map.removeLayer(this._flightLayer);
|
this._map.removeLayer(this._flightLayer);
|
||||||
this._flightLayer = newLayer.addTo(this._map);
|
this._flightLayer = newLayer.addTo(this._map);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// === SCHIFFSVERKEHR =====================================================
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Layer: Schiffsverkehr (Digitraffic AIS)
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
_startShips(map) {
|
_startShips(map) {
|
||||||
if (this._shipsLayer) return;
|
if (this._shipsLayer) return;
|
||||||
this._shipsLayer = L.layerGroup().addTo(map);
|
this._shipsLayer = L.layerGroup().addTo(map);
|
||||||
var self = this;
|
var self = this;
|
||||||
this._fetchShips(map);
|
this._fetchShips(map);
|
||||||
this._shipsInterval = setInterval(function() { self._fetchShips(map); }, 60000); // 60s
|
this._shipsInterval = setInterval(function() { self._fetchShips(map); }, 60000);
|
||||||
},
|
},
|
||||||
|
|
||||||
_stopShips() {
|
_stopShips() {
|
||||||
@@ -308,40 +223,15 @@ const GEOINT = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_fetchShips(map) {
|
_fetchShips(map) {
|
||||||
if (!map) return;
|
|
||||||
var self = this;
|
var self = this;
|
||||||
var token = localStorage.getItem('osint_token') || '';
|
var token = localStorage.getItem('osint_token') || '';
|
||||||
var headers = token ? { 'Authorization': 'Bearer ' + token } : {};
|
fetch('/api/geoint/ships', { headers: token ? { 'Authorization': 'Bearer ' + token } : {} })
|
||||||
|
|
||||||
fetch('/api/geoint/ships', { headers: headers })
|
|
||||||
.then(function(r) { return r.ok ? r.json() : { ships: [] }; })
|
.then(function(r) { return r.ok ? r.json() : { ships: [] }; })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (!self._shipsLayer) return;
|
|
||||||
self._shipsData = data.ships || [];
|
self._shipsData = data.ships || [];
|
||||||
self._renderShips(map);
|
self._renderShips(map);
|
||||||
})
|
})
|
||||||
var heading = s.heading || s.cog || 0;
|
.catch(function() {});
|
||||||
var sog = s.sog || 0;
|
|
||||||
// Nur Schiffe mit Bewegung oder in Hafennaehe anzeigen
|
|
||||||
var icon = L.divIcon({
|
|
||||||
className: '',
|
|
||||||
html: '<div class="geoint-ship" style="transform:rotate(' + heading + 'deg)">' +
|
|
||||||
'<svg viewBox="0 0 24 24" width="10" height="10">' +
|
|
||||||
'<path d="M12 2l-4 8h-3l3 12h8l3-12h-3z" fill="' + (sog > 0.5 ? '#4499ff' : '#666688') + '" stroke="#223355" stroke-width="1"/>' +
|
|
||||||
'</svg></div>',
|
|
||||||
iconSize: [10, 10],
|
|
||||||
iconAnchor: [5, 5],
|
|
||||||
});
|
|
||||||
var mmsi = s.mmsi || '?';
|
|
||||||
var navLabels = {0:'Motorbetrieb', 1:'Vor Anker', 2:'Nicht steuerbar', 3:'Eingeschraenkt', 5:'Festgemacht', 7:'Fischfang', 8:'Unter Segel'};
|
|
||||||
var navText = navLabels[s.navStat] || 'Status ' + s.navStat;
|
|
||||||
var popup = '<div class="geoint-popup">' +
|
|
||||||
'<strong>MMSI ' + mmsi + '</strong>' +
|
|
||||||
'<br><span class="geoint-popup-key">SOG</span> ' + sog.toFixed(1) + ' kn' +
|
|
||||||
'<br><span class="geoint-popup-key">COG</span> ' + Math.round(s.cog || 0) + '\u00b0' +
|
|
||||||
'<br><span class="geoint-popup-key">NAV</span> ' + navText +
|
|
||||||
'</div>';
|
|
||||||
.catch(function(e) { if (typeof DEV_MODE !== 'undefined' && DEV_MODE) console.warn('GEOINT ships:', e); });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderShips(map) {
|
_renderShips(map) {
|
||||||
@@ -349,46 +239,36 @@ const GEOINT = {
|
|||||||
var newLayer = L.layerGroup();
|
var newLayer = L.layerGroup();
|
||||||
var bounds = map.getBounds();
|
var bounds = map.getBounds();
|
||||||
var zoom = map.getZoom();
|
var zoom = map.getZoom();
|
||||||
// Zoom-adaptiv: bei weitem Zoom nur fahrende Schiffe, naeher alle
|
var max = zoom >= 10 ? 800 : zoom >= 7 ? 400 : zoom >= 5 ? 150 : 50;
|
||||||
var maxMarkers = zoom >= 10 ? 800 : zoom >= 7 ? 400 : zoom >= 5 ? 150 : 50;
|
var minSog = zoom >= 8 ? 0 : zoom >= 5 ? 0.3 : 1.0;
|
||||||
var minSog = zoom >= 8 ? 0 : zoom >= 5 ? 0.3 : 1.0; // Nur bewegte Schiffe bei Uebersicht
|
var r = zoom >= 10 ? 3.5 : zoom >= 7 ? 2.5 : 2;
|
||||||
var markerSize = zoom >= 10 ? 3.5 : zoom >= 7 ? 2.5 : 2;
|
|
||||||
var count = 0;
|
var count = 0;
|
||||||
for (var i = 0; i < this._shipsData.length && count < maxMarkers; i++) {
|
for (var i = 0; i < this._shipsData.length && count < max; i++) {
|
||||||
var s = this._shipsData[i];
|
var s = this._shipsData[i];
|
||||||
if (!s.lat || !s.lon || !bounds.contains([s.lat, s.lon])) continue;
|
if (!s.lat || !s.lon || !bounds.contains([s.lat, s.lon])) continue;
|
||||||
if ((s.sog || 0) < minSog) continue; // Bei Uebersicht nur fahrende
|
if ((s.sog || 0) < minSog) continue;
|
||||||
count++;
|
count++;
|
||||||
var sog = s.sog || 0;
|
var color = (s.sog || 0) > 0.5 ? '#4499ff' : '#556688';
|
||||||
var color = sog > 0.5 ? '#4499ff' : '#556688';
|
var navLabels = {0:'Motor',1:'Anker',2:'N.steuerb.',3:'Eingeschr.',5:'Festgemacht',7:'Fischfang',8:'Segel'};
|
||||||
var mmsi = s.mmsi || '?';
|
var popup = '<div class="geoint-popup"><strong>MMSI ' + (s.mmsi||'?') + '</strong>' +
|
||||||
var navLabels = {0:'Motorbetrieb', 1:'Vor Anker', 2:'Nicht steuerbar', 3:'Eingeschraenkt', 5:'Festgemacht', 7:'Fischfang', 8:'Unter Segel'};
|
'<br><span class="geoint-popup-key">SOG</span> ' + (s.sog||0).toFixed(1) + ' kn' +
|
||||||
var navText = navLabels[s.navStat] || 'Status ' + s.navStat;
|
'<br><span class="geoint-popup-key">NAV</span> ' + (navLabels[s.navStat] || s.navStat) + '</div>';
|
||||||
var popup = '<div class="geoint-popup">' +
|
|
||||||
'<strong>MMSI ' + mmsi + '</strong>' +
|
|
||||||
'<br><span class="geoint-popup-key">SOG</span> ' + sog.toFixed(1) + ' kn' +
|
|
||||||
'<br><span class="geoint-popup-key">NAV</span> ' + navText +
|
|
||||||
'</div>';
|
|
||||||
L.circleMarker([s.lat, s.lon], {
|
L.circleMarker([s.lat, s.lon], {
|
||||||
radius: markerSize, fillColor: color, color: '#223355',
|
radius: r, fillColor: color, color: '#223355',
|
||||||
fillOpacity: 0.85, weight: 0.5, renderer: this._canvasRenderer
|
fillOpacity: 0.85, weight: 0.5, renderer: this._canvasRenderer
|
||||||
}).bindPopup(popup, { className: 'geoint-leaflet-popup' }).addTo(newLayer);
|
}).bindPopup(popup, { className: 'geoint-leaflet-popup' }).addTo(newLayer);
|
||||||
}
|
}
|
||||||
if (this._map) {
|
|
||||||
this._map.removeLayer(this._shipsLayer);
|
this._map.removeLayer(this._shipsLayer);
|
||||||
this._shipsLayer = newLayer.addTo(this._map);
|
this._shipsLayer = newLayer.addTo(this._map);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// === ERDBEBEN ============================================================
|
||||||
// Layer: Erdbeben
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
_startQuakes(map) {
|
_startQuakes(map) {
|
||||||
if (this._quakeLayer) return;
|
if (this._quakeLayer) return;
|
||||||
this._quakeLayer = L.layerGroup().addTo(map);
|
this._quakeLayer = L.layerGroup().addTo(map);
|
||||||
this._fetchQuakes(map);
|
this._fetchQuakes(map);
|
||||||
var self = this;
|
var self = this;
|
||||||
this._quakeInterval = setInterval(function() { self._fetchQuakes(map); }, 300000); // 5 min
|
this._quakeInterval = setInterval(function() { self._fetchQuakes(map); }, 300000);
|
||||||
},
|
},
|
||||||
|
|
||||||
_stopQuakes() {
|
_stopQuakes() {
|
||||||
@@ -396,7 +276,7 @@ const GEOINT = {
|
|||||||
if (this._quakeLayer && this._map) { this._map.removeLayer(this._quakeLayer); this._quakeLayer = null; }
|
if (this._quakeLayer && this._map) { this._map.removeLayer(this._quakeLayer); this._quakeLayer = null; }
|
||||||
},
|
},
|
||||||
|
|
||||||
_fetchQuakes(map) {
|
_fetchQuakes() {
|
||||||
if (!this._quakeLayer) return;
|
if (!this._quakeLayer) return;
|
||||||
var self = this;
|
var self = this;
|
||||||
fetch('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson')
|
fetch('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson')
|
||||||
@@ -406,54 +286,36 @@ const GEOINT = {
|
|||||||
self._quakeLayer.clearLayers();
|
self._quakeLayer.clearLayers();
|
||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
(data.features || []).forEach(function(f) {
|
(data.features || []).forEach(function(f) {
|
||||||
var coords = f.geometry.coordinates;
|
var c = f.geometry.coordinates, p = f.properties;
|
||||||
var p = f.properties;
|
|
||||||
var mag = p.mag || 1;
|
var mag = p.mag || 1;
|
||||||
var ageH = (now - p.time) / 3600000;
|
var ageH = (now - p.time) / 3600000;
|
||||||
var color = ageH < 1 ? '#ff0000' : ageH < 6 ? '#ff6600' : ageH < 12 ? '#ffaa00' : '#ffdd00';
|
var color = ageH < 1 ? '#ff0000' : ageH < 6 ? '#ff6600' : ageH < 12 ? '#ffaa00' : '#ffdd00';
|
||||||
var radius = Math.max(mag * 3.5, 5);
|
|
||||||
var cls = ageH < 2 ? 'geoint-quake-marker' : '';
|
var cls = ageH < 2 ? 'geoint-quake-marker' : '';
|
||||||
var marker = L.circleMarker([coords[1], coords[0]], {
|
L.circleMarker([c[1], c[0]], {
|
||||||
radius: radius,
|
radius: Math.max(mag * 3.5, 5), fillColor: color, color: color,
|
||||||
fillColor: color,
|
weight: 1.5, fillOpacity: 0.6, className: cls
|
||||||
color: color,
|
}).bindPopup('<div class="geoint-popup"><strong>M' + mag.toFixed(1) + '</strong> ' + (p.place||'') +
|
||||||
weight: 1.5,
|
'<br><span class="geoint-popup-key">TIEFE</span> ' + (c[2]||'?') + ' km</div>',
|
||||||
fillOpacity: 0.6,
|
{ className: 'geoint-leaflet-popup' }
|
||||||
className: cls
|
).addTo(self._quakeLayer);
|
||||||
});
|
|
||||||
var timeStr = new Date(p.time).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
||||||
marker.bindPopup(
|
|
||||||
'<div class="geoint-popup">' +
|
|
||||||
'<strong>M' + mag.toFixed(1) + '</strong> ' + (p.place || '') +
|
|
||||||
'<br><span class="geoint-popup-key">TIEFE</span> ' + (coords[2] || '?') + ' km' +
|
|
||||||
'<br><span class="geoint-popup-key">ZEIT</span> ' + timeStr +
|
|
||||||
'</div>', { className: 'geoint-leaflet-popup' }
|
|
||||||
);
|
|
||||||
marker.addTo(self._quakeLayer);
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function(e) { if (typeof DEV_MODE !== 'undefined' && DEV_MODE) console.warn('GEOINT quakes:', e); });
|
.catch(function() {});
|
||||||
},
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// === GDELT NACHRICHTEN ===================================================
|
||||||
// Layer: GDELT Nachrichten
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
_startGdelt(map) {
|
_startGdelt(map) {
|
||||||
if (this._gdeltLayer) return;
|
if (this._gdeltLayer) return;
|
||||||
this._gdeltLayer = L.markerClusterGroup({
|
this._gdeltLayer = L.markerClusterGroup({
|
||||||
maxClusterRadius: 30,
|
maxClusterRadius: 30,
|
||||||
iconCreateFunction: function(cluster) {
|
iconCreateFunction: function(cluster) {
|
||||||
var count = cluster.getChildCount();
|
var n = cluster.getChildCount();
|
||||||
return L.divIcon({
|
return L.divIcon({ html: '<div class="geoint-gdelt-icon">' + (n > 99 ? '99+' : n) + '</div>', className: '', iconSize: [22, 22] });
|
||||||
html: '<div class="geoint-gdelt-icon">' + (count > 99 ? '99+' : count) + '</div>',
|
|
||||||
className: '',
|
|
||||||
iconSize: [22, 22],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
this._fetchGdelt(map);
|
this._fetchGdelt(map);
|
||||||
var self = this;
|
var self = this;
|
||||||
this._gdeltInterval = setInterval(function() { self._fetchGdelt(map); }, 600000); // 10 min
|
this._gdeltInterval = setInterval(function() { self._fetchGdelt(map); }, 600000);
|
||||||
},
|
},
|
||||||
|
|
||||||
_stopGdelt() {
|
_stopGdelt() {
|
||||||
@@ -461,67 +323,40 @@ const GEOINT = {
|
|||||||
if (this._gdeltLayer && this._map) { this._map.removeLayer(this._gdeltLayer); this._gdeltLayer = null; }
|
if (this._gdeltLayer && this._map) { this._map.removeLayer(this._gdeltLayer); this._gdeltLayer = null; }
|
||||||
},
|
},
|
||||||
|
|
||||||
_fetchGdelt(map) {
|
_fetchGdelt() {
|
||||||
if (!this._gdeltLayer) return;
|
if (!this._gdeltLayer) return;
|
||||||
var self = this;
|
var self = this;
|
||||||
// Holt Lage-Kontext fuer GDELT-Suche
|
var query = 'conflict OR crisis OR disaster';
|
||||||
var query = '';
|
|
||||||
if (typeof App !== 'undefined' && App.currentIncidentId) {
|
if (typeof App !== 'undefined' && App.currentIncidentId) {
|
||||||
var inc = (App.incidents || []).find(function(i) { return i.id === App.currentIncidentId; });
|
var inc = (App.incidents || []).find(function(i) { return i.id === App.currentIncidentId; });
|
||||||
if (inc) query = encodeURIComponent((inc.title || '').substring(0, 80));
|
if (inc && inc.title) query = encodeURIComponent(inc.title.substring(0, 80));
|
||||||
}
|
}
|
||||||
if (!query) query = 'conflict OR crisis OR disaster';
|
|
||||||
var token = localStorage.getItem('osint_token') || '';
|
var token = localStorage.getItem('osint_token') || '';
|
||||||
|
fetch('/api/geoint/gdelt?query=' + query, { headers: token ? { 'Authorization': 'Bearer ' + token } : {} })
|
||||||
fetch('/api/geoint/gdelt?query=' + query, {
|
.then(function(r) { return r.ok ? r.json() : { features: [] }; })
|
||||||
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
|
|
||||||
})
|
|
||||||
.then(function(r) { return r.ok ? r.json() : Promise.reject(r.status); })
|
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (!self._gdeltLayer) return;
|
if (!self._gdeltLayer) return;
|
||||||
self._gdeltLayer.clearLayers();
|
self._gdeltLayer.clearLayers();
|
||||||
var features = data.features || [];
|
(data.features || []).slice(0, 200).forEach(function(f) {
|
||||||
features.slice(0, 200).forEach(function(f) {
|
var c = f.geometry.coordinates, p = f.properties || {};
|
||||||
var coords = f.geometry.coordinates;
|
var icon = L.divIcon({ className: '', html: '<div class="geoint-gdelt-icon">N</div>', iconSize: [18, 18], iconAnchor: [9, 9] });
|
||||||
var p = f.properties || {};
|
var popup = '<div class="geoint-popup" style="max-width:240px"><strong>' + (p.name || p.title || 'Nachricht').substring(0, 100) + '</strong>' +
|
||||||
var name = p.name || p.title || 'Nachricht';
|
(p.url ? '<br><a href="' + p.url + '" target="_blank" rel="noopener" style="color:#44aaff;font-size:10px">Quelle</a>' : '') + '</div>';
|
||||||
var url = p.url || p.shareimage || '';
|
L.marker([c[1], c[0]], { icon: icon }).bindPopup(popup, { className: 'geoint-leaflet-popup' }).addTo(self._gdeltLayer);
|
||||||
var icon = L.divIcon({
|
|
||||||
className: '',
|
|
||||||
html: '<div class="geoint-gdelt-icon">N</div>',
|
|
||||||
iconSize: [18, 18],
|
|
||||||
iconAnchor: [9, 9],
|
|
||||||
});
|
|
||||||
var popup = '<div class="geoint-popup" style="max-width:240px">' +
|
|
||||||
'<strong>' + name.substring(0, 100) + '</strong>' +
|
|
||||||
(url ? '<br><a href="' + url + '" target="_blank" rel="noopener" style="color:#44aaff;font-size:10px">Quelle</a>' : '') +
|
|
||||||
'</div>';
|
|
||||||
L.marker([coords[1], coords[0]], { icon: icon }).bindPopup(popup, { className: 'geoint-leaflet-popup' }).addTo(self._gdeltLayer);
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function(e) { if (typeof DEV_MODE !== 'undefined' && DEV_MODE) console.warn('GEOINT gdelt:', e); });
|
.catch(function() {});
|
||||||
},
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// === HEATMAP =============================================================
|
||||||
// Layer: Heatmap
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
_startHeatmap(map) {
|
_startHeatmap(map) {
|
||||||
if (this._heatLayer) return;
|
if (this._heatLayer || typeof L.heatLayer === 'undefined') return;
|
||||||
if (typeof L.heatLayer === 'undefined') {
|
var locs = (typeof UI !== 'undefined' && UI._lastLocations) ? UI._lastLocations : [];
|
||||||
if (typeof DEV_MODE !== 'undefined' && DEV_MODE) console.warn('GEOINT: leaflet-heat nicht geladen');
|
if (!locs.length) return;
|
||||||
return;
|
var maxC = Math.max.apply(null, locs.map(function(l) { return l.article_count || 1; }));
|
||||||
}
|
var pts = locs.map(function(l) { return [l.lat, l.lon, (l.article_count || 1) / maxC]; });
|
||||||
// Locations aus UI-State holen
|
this._heatLayer = L.heatLayer(pts, {
|
||||||
var locations = (typeof UI !== 'undefined' && UI._lastLocations) ? UI._lastLocations : [];
|
radius: 30, blur: 20, maxZoom: 12,
|
||||||
if (!locations.length) return;
|
|
||||||
var maxCount = Math.max.apply(null, locations.map(function(l) { return l.article_count || 1; }));
|
|
||||||
var points = locations.map(function(l) {
|
|
||||||
return [l.lat, l.lon, (l.article_count || 1) / maxCount];
|
|
||||||
});
|
|
||||||
this._heatLayer = L.heatLayer(points, {
|
|
||||||
radius: 30,
|
|
||||||
blur: 20,
|
|
||||||
maxZoom: 12,
|
|
||||||
gradient: { 0.2: '#004400', 0.4: '#00ff88', 0.6: '#ffaa00', 0.8: '#ff4400', 1.0: '#ff0000' }
|
gradient: { 0.2: '#004400', 0.4: '#00ff88', 0.6: '#ffaa00', 0.8: '#ff4400', 1.0: '#ff0000' }
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
},
|
},
|
||||||
@@ -530,12 +365,10 @@ const GEOINT = {
|
|||||||
if (this._heatLayer && this._map) { this._map.removeLayer(this._heatLayer); this._heatLayer = null; }
|
if (this._heatLayer && this._map) { this._map.removeLayer(this._heatLayer); this._heatLayer = null; }
|
||||||
},
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// === KOORDINATENANZEIGE ==================================================
|
||||||
// Koordinatenanzeige
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
_startCoords(map) {
|
_startCoords(map) {
|
||||||
if (this._coordControl) return;
|
if (this._coordControl) return;
|
||||||
var CoordControl = L.Control.extend({
|
var Ctrl = L.Control.extend({
|
||||||
options: { position: 'bottomleft' },
|
options: { position: 'bottomleft' },
|
||||||
onAdd: function() {
|
onAdd: function() {
|
||||||
var div = L.DomUtil.create('div', 'geoint-coord-display');
|
var div = L.DomUtil.create('div', 'geoint-coord-display');
|
||||||
@@ -544,7 +377,7 @@ const GEOINT = {
|
|||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this._coordControl = new CoordControl();
|
this._coordControl = new Ctrl();
|
||||||
map.addControl(this._coordControl);
|
map.addControl(this._coordControl);
|
||||||
var el = document.getElementById('geoint-coord-text');
|
var el = document.getElementById('geoint-coord-text');
|
||||||
this._coordHandler = function(e) {
|
this._coordHandler = function(e) {
|
||||||
@@ -558,193 +391,100 @@ const GEOINT = {
|
|||||||
if (this._coordControl && this._map) { this._map.removeControl(this._coordControl); this._coordControl = null; }
|
if (this._coordControl && this._map) { this._map.removeControl(this._coordControl); this._coordControl = null; }
|
||||||
},
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// === DISTANZMESSUNG ======================================================
|
||||||
// Distanzmessung
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
_startDistance(map) {
|
_startDistance(map) {
|
||||||
if (this._distanceLayers) return;
|
if (this._distanceLayers) return;
|
||||||
this._distanceLayers = L.layerGroup().addTo(map);
|
this._distanceLayers = L.layerGroup().addTo(map);
|
||||||
this._distancePoints = [];
|
this._distancePoints = [];
|
||||||
this._distanceActive = true;
|
|
||||||
map.getContainer().style.cursor = 'crosshair';
|
map.getContainer().style.cursor = 'crosshair';
|
||||||
var self = this;
|
var self = this;
|
||||||
this._distanceHandler = function(e) {
|
this._distanceHandler = function(e) {
|
||||||
self._distancePoints.push(e.latlng);
|
self._distancePoints.push(e.latlng);
|
||||||
|
L.circleMarker(e.latlng, { radius: 6, fillColor: '#ff2222', color: '#ffffff', fillOpacity: 1, weight: 2 }).addTo(self._distanceLayers);
|
||||||
if (self._distancePoints.length >= 2) {
|
if (self._distancePoints.length >= 2) {
|
||||||
var p1 = self._distancePoints[self._distancePoints.length - 2];
|
var p1 = self._distancePoints[self._distancePoints.length - 2];
|
||||||
var p2 = self._distancePoints[self._distancePoints.length - 1];
|
var p2 = self._distancePoints[self._distancePoints.length - 1];
|
||||||
L.polyline([p1, p2], { color: '#000000', weight: 5, opacity: 0.5 }).addTo(self._distanceLayers); L.polyline([p1, p2], { color: '#ff2222', weight: 3, dashArray: '8 5' }).addTo(self._distanceLayers);
|
L.polyline([p1, p2], { color: '#000000', weight: 5, opacity: 0.5 }).addTo(self._distanceLayers);
|
||||||
|
L.polyline([p1, p2], { color: '#ff2222', weight: 3, dashArray: '8 5' }).addTo(self._distanceLayers);
|
||||||
var dist = p1.distanceTo(p2);
|
var dist = p1.distanceTo(p2);
|
||||||
var totalDist = 0;
|
var total = 0;
|
||||||
for (var i = 1; i < self._distancePoints.length; i++) {
|
for (var i = 1; i < self._distancePoints.length; i++) total += self._distancePoints[i-1].distanceTo(self._distancePoints[i]);
|
||||||
totalDist += self._distancePoints[i - 1].distanceTo(self._distancePoints[i]);
|
var label = dist >= 1000 ? (dist/1000).toFixed(1) + ' km' : Math.round(dist) + ' m';
|
||||||
}
|
var tLabel = total >= 1000 ? (total/1000).toFixed(1) + ' km' : Math.round(total) + ' m';
|
||||||
var label = dist >= 1000
|
var text = self._distancePoints.length > 2 ? label + ' (\u03a3 ' + tLabel + ')' : label;
|
||||||
? (dist / 1000).toFixed(1) + ' km'
|
L.marker([(p1.lat+p2.lat)/2, (p1.lng+p2.lng)/2], {
|
||||||
: Math.round(dist) + ' m';
|
icon: L.divIcon({ className: '', html: '<div class="geoint-distance-label">' + text + '</div>', iconSize: [0,0], iconAnchor: [0,12] })
|
||||||
var totalLabel = totalDist >= 1000
|
|
||||||
? (totalDist / 1000).toFixed(1) + ' km'
|
|
||||||
: Math.round(totalDist) + ' m';
|
|
||||||
var midLat = (p1.lat + p2.lat) / 2;
|
|
||||||
var midLng = (p1.lng + p2.lng) / 2;
|
|
||||||
var text = self._distancePoints.length > 2
|
|
||||||
? label + ' (Σ ' + totalLabel + ')'
|
|
||||||
: label;
|
|
||||||
L.marker([midLat, midLng], {
|
|
||||||
icon: L.divIcon({
|
|
||||||
className: '',
|
|
||||||
html: '<div class="geoint-distance-label">' + text + '</div>',
|
|
||||||
iconSize: [0, 0],
|
|
||||||
iconAnchor: [0, 12],
|
|
||||||
})
|
|
||||||
}).addTo(self._distanceLayers);
|
}).addTo(self._distanceLayers);
|
||||||
}
|
}
|
||||||
// Punkt-Marker
|
|
||||||
L.circleMarker(e.latlng, { radius: 6, fillColor: '#ff2222', color: '#ffffff', fillOpacity: 1, weight: 2 }).addTo(self._distanceLayers);
|
|
||||||
};
|
};
|
||||||
map.on('click', this._distanceHandler);
|
map.on('click', this._distanceHandler);
|
||||||
},
|
},
|
||||||
|
|
||||||
_stopDistance() {
|
_stopDistance() {
|
||||||
this._distanceActive = false;
|
|
||||||
this._distancePoints = [];
|
this._distancePoints = [];
|
||||||
if (this._distanceHandler && this._map) {
|
if (this._distanceHandler && this._map) { this._map.off('click', this._distanceHandler); this._distanceHandler = null; this._map.getContainer().style.cursor = ''; }
|
||||||
this._map.off('click', this._distanceHandler);
|
|
||||||
this._distanceHandler = null;
|
|
||||||
this._map.getContainer().style.cursor = '';
|
|
||||||
}
|
|
||||||
if (this._distanceLayers && this._map) { this._map.removeLayer(this._distanceLayers); this._distanceLayers = null; }
|
if (this._distanceLayers && this._map) { this._map.removeLayer(this._distanceLayers); this._distanceLayers = null; }
|
||||||
},
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// === TIMELINE ============================================================
|
||||||
// Timeline-Slider
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
initTimeline(articles) {
|
initTimeline(articles) {
|
||||||
if (!articles || !articles.length) return;
|
if (!articles || !articles.length) return;
|
||||||
var dates = articles
|
var dates = articles.map(function(a) { return a.collected_at || a.published_at; })
|
||||||
.map(function(a) { return a.collected_at || a.published_at; })
|
.filter(Boolean).map(function(d) { return new Date(d).getTime(); })
|
||||||
.filter(Boolean)
|
.filter(function(t) { return !isNaN(t); }).sort(function(a,b) { return a-b; });
|
||||||
.map(function(d) { return new Date(d).getTime(); })
|
|
||||||
.filter(function(t) { return !isNaN(t); })
|
|
||||||
.sort(function(a, b) { return a - b; });
|
|
||||||
if (dates.length < 2) return;
|
if (dates.length < 2) return;
|
||||||
|
this._timelineData = { min: dates[0], max: dates[dates.length-1], articles: articles };
|
||||||
this._timelineData = { min: dates[0], max: dates[dates.length - 1], articles: articles };
|
|
||||||
var slider = document.getElementById('geoint-timeline-slider');
|
var slider = document.getElementById('geoint-timeline-slider');
|
||||||
|
if (slider) { slider.min = dates[0]; slider.max = dates[dates.length-1]; slider.value = dates[dates.length-1]; }
|
||||||
var label = document.getElementById('geoint-timeline-label');
|
var label = document.getElementById('geoint-timeline-label');
|
||||||
if (!slider) return;
|
if (label) label.textContent = this._fmtDate(dates[dates.length-1]);
|
||||||
slider.min = dates[0];
|
|
||||||
slider.max = dates[dates.length - 1];
|
|
||||||
slider.value = dates[dates.length - 1];
|
|
||||||
if (label) label.textContent = this._formatTimelineDate(dates[dates.length - 1]);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_onTimelineChange(value) {
|
_onTimelineChange(val) {
|
||||||
var label = document.getElementById('geoint-timeline-label');
|
var label = document.getElementById('geoint-timeline-label');
|
||||||
if (label) label.textContent = this._formatTimelineDate(parseInt(value));
|
if (label) label.textContent = this._fmtDate(parseInt(val));
|
||||||
this._filterMarkersByTime(parseInt(value));
|
if (!this._map || !UI._mapCluster || !this._timelineData) return;
|
||||||
|
var maxT = parseInt(val), arts = this._timelineData.articles;
|
||||||
|
var vis = new Set();
|
||||||
|
arts.forEach(function(a) { if (new Date(a.collected_at || a.published_at || 0).getTime() <= maxT) vis.add(a.id); });
|
||||||
|
UI._mapCluster.eachLayer(function(m) {
|
||||||
|
if (m._articleIds) m.setOpacity(m._articleIds.some(function(id) { return vis.has(id); }) ? 1 : 0.08);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_formatTimelineDate(ts) {
|
_resetTimeline() {
|
||||||
|
if (this._timelineData) {
|
||||||
|
var slider = document.getElementById('geoint-timeline-slider');
|
||||||
|
if (slider) { slider.value = this._timelineData.max; this._onTimelineChange(this._timelineData.max); }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_fmtDate(ts) {
|
||||||
var d = new Date(ts);
|
var d = new Date(ts);
|
||||||
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) + ' ' +
|
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) + ' ' +
|
||||||
d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||||
},
|
},
|
||||||
|
|
||||||
_filterMarkersByTime(maxTime) {
|
// === CLEANUP =============================================================
|
||||||
// Filtert die bestehenden Marker im Cluster anhand des Zeitstempels
|
|
||||||
if (!this._map || !UI._mapCluster || !this._timelineData) return;
|
|
||||||
var articles = this._timelineData.articles;
|
|
||||||
var visibleIds = new Set();
|
|
||||||
articles.forEach(function(a) {
|
|
||||||
var t = new Date(a.collected_at || a.published_at || 0).getTime();
|
|
||||||
if (t <= maxTime) visibleIds.add(a.id);
|
|
||||||
});
|
|
||||||
// Marker sichtbar/unsichtbar machen ueber Opacity
|
|
||||||
UI._mapCluster.eachLayer(function(marker) {
|
|
||||||
if (marker._articleIds) {
|
|
||||||
var visible = marker._articleIds.some(function(id) { return visibleIds.has(id); });
|
|
||||||
marker.setOpacity(visible ? 1 : 0.08);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_resetTimeline() {
|
|
||||||
var slider = document.getElementById('geoint-timeline-slider');
|
|
||||||
if (slider && this._timelineData) {
|
|
||||||
slider.value = this._timelineData.max;
|
|
||||||
this._onTimelineChange(this._timelineData.max);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
// Berechnet Grid-Punkte fuer vollstaendige Kartenabdeckung
|
|
||||||
_calcGridPoints(bounds) {
|
|
||||||
var n = bounds.getNorth(), s = bounds.getSouth();
|
|
||||||
var e = bounds.getEast(), w = bounds.getWest();
|
|
||||||
var latSpan = Math.abs(n - s);
|
|
||||||
var lonSpan = Math.abs(e - w);
|
|
||||||
// Ein Punkt deckt ~250nm (~4.2 Grad) ab
|
|
||||||
var step = 3.5; // Grad pro Grid-Zelle (etwas Overlap)
|
|
||||||
var latSteps = Math.max(1, Math.ceil(latSpan / step));
|
|
||||||
var lonSteps = Math.max(1, Math.ceil(lonSpan / step));
|
|
||||||
// Maximal 4 Punkte (2x2 Grid) um API-Last zu begrenzen
|
|
||||||
if (latSteps > 2) latSteps = 2;
|
|
||||||
if (lonSteps > 2) lonSteps = 2;
|
|
||||||
var points = [];
|
|
||||||
var latStep = latSpan / latSteps;
|
|
||||||
var lonStep = lonSpan / lonSteps;
|
|
||||||
for (var i = 0; i < latSteps; i++) {
|
|
||||||
for (var j = 0; j < lonSteps; j++) {
|
|
||||||
points.push({
|
|
||||||
lat: s + latStep * (i + 0.5),
|
|
||||||
lon: w + lonStep * (j + 0.5),
|
|
||||||
radius: Math.min(Math.round(Math.max(latStep, lonStep) / 2 * 60), 250)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return points;
|
|
||||||
},
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Cleanup
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
this._stopFlights();
|
this._stopFlights(); this._stopShips(); this._stopQuakes();
|
||||||
this._stopQuakes();
|
this._stopGdelt(); this._stopHeatmap(); this._stopCoords(); this._stopDistance();
|
||||||
this._stopGdelt();
|
|
||||||
this._stopHeatmap();
|
|
||||||
this._stopCoords();
|
|
||||||
this._stopDistance();
|
|
||||||
this._removeSubControl();
|
this._removeSubControl();
|
||||||
// CSS-Klassen entfernen
|
document.querySelectorAll('.geoint-active').forEach(function(el) { el.classList.remove('geoint-active'); });
|
||||||
var container = document.getElementById('map-container');
|
|
||||||
if (container) container.classList.remove('geoint-active');
|
|
||||||
var fsContainer = document.getElementById('map-fullscreen-container');
|
|
||||||
if (fsContainer) fsContainer.classList.remove('geoint-active');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// === STATE ===============================================================
|
||||||
// State Persistenz
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
_saveState() {
|
_saveState() {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('geoint_mode', this._active ? 'true' : 'false');
|
localStorage.setItem('geoint_mode', this._active ? 'true' : 'false');
|
||||||
localStorage.setItem('geoint_sublayers', JSON.stringify(this._sublayers));
|
localStorage.setItem('geoint_sublayers', JSON.stringify(this._sublayers));
|
||||||
} catch (e) { /* quota exceeded */ }
|
} catch(e) {}
|
||||||
},
|
},
|
||||||
|
|
||||||
restoreState(map) {
|
restoreState(map) {
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
this._map = map;
|
this._map = map;
|
||||||
try {
|
try { var s = localStorage.getItem('geoint_sublayers'); if (s) this._sublayers = JSON.parse(s); } catch(e) { this._sublayers = {}; }
|
||||||
var savedSublayers = localStorage.getItem('geoint_sublayers');
|
if (localStorage.getItem('geoint_mode') === 'true') this.toggle(true, map);
|
||||||
if (savedSublayers) this._sublayers = JSON.parse(savedSublayers);
|
|
||||||
} catch (e) { this._sublayers = {}; }
|
|
||||||
|
|
||||||
var wasActive = localStorage.getItem('geoint_mode') === 'true';
|
|
||||||
if (wasActive) {
|
|
||||||
this.toggle(true, map);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren