diff --git a/src/static/js/geoint.js b/src/static/js/geoint.js index 0f53200..d537d3c 100644 --- a/src/static/js/geoint.js +++ b/src/static/js/geoint.js @@ -1,47 +1,34 @@ /** * GEOINT-Modus: Taktische Kartenansicht mit Echtzeit-Datenlayern. - * Eigenstaendiges Modul — alle GEOINT-Logik gekapselt. */ const GEOINT = { _active: false, _map: null, _sublayers: {}, - _flightLayer: null, - _quakeLayer: null, - _gdeltLayer: null, - _heatLayer: null, - _flightInterval: null, - _quakeInterval: null, - _gdeltInterval: null, - _shipsLayer: null, - _flightsData: null, - _shipsData: null, - _shipsInterval: null, + _canvasRenderer: null, + // Layer references + _flightLayer: null, _quakeLayer: null, _gdeltLayer: null, + _heatLayer: null, _shipsLayer: null, + // Data caches + _flightsData: null, _shipsData: null, + // Intervals + _flightInterval: null, _quakeInterval: null, _gdeltInterval: null, _shipsInterval: null, _flightFetching: false, - _moveHandler: null, - _coordControl: null, - _coordHandler: null, - _distanceActive: false, - _distancePoints: [], - _distanceLayers: null, - _distanceHandler: null, - _subControl: null, - _osmTileLayer: null, - _satTileLayer: null, - _satLabelLayer: null, + // UI controls + _moveHandler: null, _subControl: null, + _coordControl: null, _coordHandler: null, + _distanceLayers: null, _distancePoints: [], _distanceHandler: null, + _satTileLayer: null, _satLabelLayer: null, _timelineData: null, - // ----------------------------------------------------------------------- - // Hauptschalter - // ----------------------------------------------------------------------- + // === HAUPTSCHALTER ===================================================== toggle(enabled, map) { if (!map) map = this._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._map = map; - // CSS-Klassen var container = document.getElementById('map-container'); if (container) container.classList.toggle('geoint-active', enabled); var fsContainer = document.getElementById('map-fullscreen-container'); @@ -49,158 +36,120 @@ const GEOINT = { var card = container ? container.closest('.map-card') : null; if (card) card.classList.toggle('geoint-card-active', enabled); - // Sync beider Checkboxen - 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; + document.querySelectorAll('#geoint-mode-cb, #geoint-mode-cb-fs').forEach(function(cb) { cb.checked = enabled; }); if (enabled) { if (!this._canvasRenderer) this._canvasRenderer = L.canvas({ padding: 0.5 }); this._applySatelliteTiles(map); this._createSubControl(map); this._restoreSublayers(map); - // Timeline anzeigen wenn Daten vorhanden - var tl = document.getElementById('geoint-timeline'); - if (tl) tl.style.display = ''; } else { this.cleanup(); this._restoreOsmTiles(map); - var tl = document.getElementById('geoint-timeline'); - if (tl) tl.style.display = 'none'; } - this._saveState(); }, - // ----------------------------------------------------------------------- - // Tile-Management - // ----------------------------------------------------------------------- + // === TILES ============================================================== _applySatelliteTiles(map) { if (!map) return; - // Bestehende Tile-Layer in separatem Array sammeln, dann entfernen var toRemove = []; - map.eachLayer(function(layer) { - if (layer instanceof L.TileLayer) toRemove.push(layer); - }); - toRemove.forEach(function(layer) { map.removeLayer(layer); }); - // Esri World Imagery + map.eachLayer(function(l) { if (l instanceof L.TileLayer) toRemove.push(l); }); + toRemove.forEach(function(l) { map.removeLayer(l); }); this._satTileLayer = L.tileLayer( 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles © Esri', maxZoom: 19, noWrap: true } ).addTo(map); - // Ortsnamen-Overlay this._satLabelLayer = L.tileLayer( 'https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19, noWrap: true } ).addTo(map); - // Satellite nach hinten, damit Marker darueber liegen if (this._satTileLayer.bringToBack) this._satTileLayer.bringToBack(); }, _restoreOsmTiles(map) { if (!map) return; - map.eachLayer(function(layer) { - if (layer instanceof L.TileLayer) map.removeLayer(layer); - }); - // UI._applyMapTiles() wiederherstellen + var toRemove = []; + map.eachLayer(function(l) { if (l instanceof L.TileLayer) toRemove.push(l); }); + toRemove.forEach(function(l) { map.removeLayer(l); }); if (typeof UI !== 'undefined' && UI._applyMapTiles) { UI._applyMapTiles(); } else { L.tileLayer('https://tile.openstreetmap.de/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap', - maxZoom: 18, noWrap: true + attribution: '© OpenStreetMap', maxZoom: 18, noWrap: true }).addTo(map); } }, - // ----------------------------------------------------------------------- - // Sub-Layer Control Panel - // ----------------------------------------------------------------------- + // === SUB-LAYER CONTROL ================================================== _createSubControl(map) { if (this._subControl) return; 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' }, onAdd: function() { var div = L.DomUtil.create('div', 'geoint-sub-control'); L.DomEvent.disableClickPropagation(div); L.DomEvent.disableScrollPropagation(div); - div.innerHTML = - '

GEOINT Layer

' + - self._subItemHtml('flights', 'Flugverkehr', 'flights') + - self._subItemHtml('ships', 'Schiffsverkehr', 'ships') + - self._subItemHtml('quakes', 'Erdbeben', 'quakes') + - self._subItemHtml('gdelt', 'Nachrichten', 'gdelt') + - '
' + - self._subItemHtml('heatmap', 'Heatmap', 'heatmap') + - self._subItemHtml('coords', 'Koordinaten', 'coords') + - self._subItemHtml('distance', 'Distanz', 'distance'); + var html = '

GEOINT Layer

'; + items.forEach(function(it) { + if (it[0] === '_sep') { html += '
'; return; } + var checked = self._sublayers[it[0]] ? ' checked' : ''; + html += '
' + + '' + + '
'; + }); + div.innerHTML = html; return div; } }); - this._subControl = new SubControl(); + this._subControl = new SubCtrl(); map.addControl(this._subControl); - - // Event-Listener fuer Sub-Checkboxen - ['flights', 'ships', 'quakes', 'gdelt', 'heatmap', 'coords', 'distance'].forEach(function(id) { - var cb = document.getElementById('geoint-sub-' + id); - if (cb) { - cb.addEventListener('change', function() { - self._toggleSublayer(id, this.checked, map); - }); - } + items.forEach(function(it) { + if (it[0] === '_sep') return; + var cb = document.getElementById('geoint-sub-' + it[0]); + if (cb) cb.addEventListener('change', function() { self._toggleSub(it[0], this.checked, map); }); }); }, - _subItemHtml(id, label, dotClass) { - var checked = this._sublayers[id] ? ' checked' : ''; - return '
' + - '' + - '' + - '
'; - }, - _removeSubControl() { - if (this._subControl && this._map) { - this._map.removeControl(this._subControl); - this._subControl = null; - } + if (this._subControl && this._map) { this._map.removeControl(this._subControl); this._subControl = null; } }, - _toggleSublayer(id, enabled, map) { - this._sublayers[id] = enabled; + _toggleSub(id, on, map) { + this._sublayers[id] = on; this._saveState(); - switch (id) { - case 'flights': enabled ? this._startFlights(map) : this._stopFlights(); break; - case 'ships': enabled ? this._startShips(map) : this._stopShips(); break; - case 'quakes': enabled ? this._startQuakes(map) : this._stopQuakes(); break; - case 'gdelt': enabled ? this._startGdelt(map) : this._stopGdelt(); break; - 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; - } + var m = { flights: ['_startFlights','_stopFlights'], ships: ['_startShips','_stopShips'], + quakes: ['_startQuakes','_stopQuakes'], gdelt: ['_startGdelt','_stopGdelt'], + heatmap: ['_startHeatmap','_stopHeatmap'], coords: ['_startCoords','_stopCoords'], + distance: ['_startDistance','_stopDistance'] }; + if (m[id]) this[m[id][on ? 0 : 1]](map); }, _restoreSublayers(map) { var self = this; Object.keys(this._sublayers).forEach(function(id) { - if (self._sublayers[id]) { - self._toggleSublayer(id, true, map); - } + if (self._sublayers[id]) self._toggleSub(id, true, map); }); }, - // ----------------------------------------------------------------------- - // Layer: Flugverkehr - // ----------------------------------------------------------------------- + // === FLUGVERKEHR ======================================================== _startFlights(map) { if (this._flightLayer) return; this._flightLayer = L.layerGroup().addTo(map); var self = this; this._fetchFlights(map); - this._flightInterval = setInterval(function() { self._fetchFlights(map); }, 30000); // 30s global refresh - // Bei Kartenbewegung neu rendern (client-seitig aus Cache) + this._flightInterval = setInterval(function() { self._fetchFlights(map); }, 30000); this._moveHandler = function() { clearTimeout(self._moveDebounce); self._moveDebounce = setTimeout(function() { @@ -222,37 +171,13 @@ const GEOINT = { this._flightFetching = true; var self = this; var token = localStorage.getItem('osint_token') || ''; - var headers = token ? { 'Authorization': 'Bearer ' + token } : {}; - - fetch('/api/geoint/flights', { headers: headers }) + fetch('/api/geoint/flights', { headers: token ? { 'Authorization': 'Bearer ' + token } : {} }) .then(function(r) { return r.ok ? r.json() : { ac: [] }; }) .then(function(data) { - if (!self._flightLayer) return; self._flightsData = data.ac || data.aircraft || []; self._renderFlights(map); }) - var heading = a.track || a.true_heading || 0; - var icon = L.divIcon({ - className: '', - html: '
' + - '' + - '' + - '
', - 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 = '
' + - '' + callsign + '' + - (typ ? ' (' + typ + ')' : '') + - '
ALT ' + (typeof alt === 'number' ? alt.toLocaleString() + ' ft' : alt) + - '
SPD ' + (typeof spd === 'number' ? Math.round(spd) + ' kts' : spd) + - '
HDG ' + Math.round(heading) + '°' + - '
'; - .catch(function(e) { if (typeof DEV_MODE !== 'undefined' && DEV_MODE) console.warn('GEOINT flights:', e); }) + .catch(function() {}) .finally(function() { self._flightFetching = false; }); }, @@ -261,45 +186,35 @@ const GEOINT = { var newLayer = L.layerGroup(); var bounds = map.getBounds(); var zoom = map.getZoom(); - // Zoom-adaptiv: weniger bei Uebersicht, mehr bei Detail - var maxMarkers = zoom >= 10 ? 600 : zoom >= 7 ? 400 : zoom >= 5 ? 200 : 80; - var markerSize = zoom >= 10 ? 4 : zoom >= 7 ? 3 : 2; + var max = zoom >= 10 ? 600 : zoom >= 7 ? 400 : zoom >= 5 ? 200 : 80; + var r = zoom >= 10 ? 4 : zoom >= 7 ? 3 : 2; 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]; if (!a.lat || !a.lon || !bounds.contains([a.lat, a.lon])) continue; 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 spd = a.gs || a.ground_speed || '?'; - var typ = a.t || a.type || ''; - var popup = '
' + - '' + callsign + '' + - (typ ? ' (' + typ + ')' : '') + + var popup = '
' + cs + '' + '
ALT ' + (typeof alt === 'number' ? alt.toLocaleString() + ' ft' : alt) + - '
SPD ' + (typeof spd === 'number' ? Math.round(spd) + ' kts' : spd) + - '
'; + '
SPD ' + (typeof spd === 'number' ? Math.round(spd) + ' kts' : spd) + '
'; 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 }).bindPopup(popup, { className: 'geoint-leaflet-popup' }).addTo(newLayer); } - if (this._map) { - this._map.removeLayer(this._flightLayer); - this._flightLayer = newLayer.addTo(this._map); - } + this._map.removeLayer(this._flightLayer); + this._flightLayer = newLayer.addTo(this._map); }, - - // ----------------------------------------------------------------------- - // Layer: Schiffsverkehr (Digitraffic AIS) - // ----------------------------------------------------------------------- + // === SCHIFFSVERKEHR ===================================================== _startShips(map) { if (this._shipsLayer) return; this._shipsLayer = L.layerGroup().addTo(map); var self = this; this._fetchShips(map); - this._shipsInterval = setInterval(function() { self._fetchShips(map); }, 60000); // 60s + this._shipsInterval = setInterval(function() { self._fetchShips(map); }, 60000); }, _stopShips() { @@ -308,40 +223,15 @@ const GEOINT = { }, _fetchShips(map) { - if (!map) return; var self = this; var token = localStorage.getItem('osint_token') || ''; - var headers = token ? { 'Authorization': 'Bearer ' + token } : {}; - - fetch('/api/geoint/ships', { headers: headers }) + fetch('/api/geoint/ships', { headers: token ? { 'Authorization': 'Bearer ' + token } : {} }) .then(function(r) { return r.ok ? r.json() : { ships: [] }; }) .then(function(data) { - if (!self._shipsLayer) return; self._shipsData = data.ships || []; self._renderShips(map); }) - var heading = s.heading || s.cog || 0; - var sog = s.sog || 0; - // Nur Schiffe mit Bewegung oder in Hafennaehe anzeigen - var icon = L.divIcon({ - className: '', - html: '
' + - '' + - '' + - '
', - 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 = '
' + - 'MMSI ' + mmsi + '' + - '
SOG ' + sog.toFixed(1) + ' kn' + - '
COG ' + Math.round(s.cog || 0) + '\u00b0' + - '
NAV ' + navText + - '
'; - .catch(function(e) { if (typeof DEV_MODE !== 'undefined' && DEV_MODE) console.warn('GEOINT ships:', e); }); + .catch(function() {}); }, _renderShips(map) { @@ -349,46 +239,36 @@ const GEOINT = { var newLayer = L.layerGroup(); var bounds = map.getBounds(); var zoom = map.getZoom(); - // Zoom-adaptiv: bei weitem Zoom nur fahrende Schiffe, naeher alle - var maxMarkers = zoom >= 10 ? 800 : zoom >= 7 ? 400 : zoom >= 5 ? 150 : 50; - var minSog = zoom >= 8 ? 0 : zoom >= 5 ? 0.3 : 1.0; // Nur bewegte Schiffe bei Uebersicht - var markerSize = zoom >= 10 ? 3.5 : zoom >= 7 ? 2.5 : 2; + var max = zoom >= 10 ? 800 : zoom >= 7 ? 400 : zoom >= 5 ? 150 : 50; + var minSog = zoom >= 8 ? 0 : zoom >= 5 ? 0.3 : 1.0; + var r = zoom >= 10 ? 3.5 : zoom >= 7 ? 2.5 : 2; 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]; 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++; - var sog = s.sog || 0; - var color = sog > 0.5 ? '#4499ff' : '#556688'; - 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 = '
' + - 'MMSI ' + mmsi + '' + - '
SOG ' + sog.toFixed(1) + ' kn' + - '
NAV ' + navText + - '
'; + var color = (s.sog || 0) > 0.5 ? '#4499ff' : '#556688'; + var navLabels = {0:'Motor',1:'Anker',2:'N.steuerb.',3:'Eingeschr.',5:'Festgemacht',7:'Fischfang',8:'Segel'}; + var popup = '
MMSI ' + (s.mmsi||'?') + '' + + '
SOG ' + (s.sog||0).toFixed(1) + ' kn' + + '
NAV ' + (navLabels[s.navStat] || s.navStat) + '
'; 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 }).bindPopup(popup, { className: 'geoint-leaflet-popup' }).addTo(newLayer); } - if (this._map) { - this._map.removeLayer(this._shipsLayer); - this._shipsLayer = newLayer.addTo(this._map); - } + this._map.removeLayer(this._shipsLayer); + this._shipsLayer = newLayer.addTo(this._map); }, - // ----------------------------------------------------------------------- - // Layer: Erdbeben - // ----------------------------------------------------------------------- + // === ERDBEBEN ============================================================ _startQuakes(map) { if (this._quakeLayer) return; this._quakeLayer = L.layerGroup().addTo(map); this._fetchQuakes(map); var self = this; - this._quakeInterval = setInterval(function() { self._fetchQuakes(map); }, 300000); // 5 min + this._quakeInterval = setInterval(function() { self._fetchQuakes(map); }, 300000); }, _stopQuakes() { @@ -396,7 +276,7 @@ const GEOINT = { if (this._quakeLayer && this._map) { this._map.removeLayer(this._quakeLayer); this._quakeLayer = null; } }, - _fetchQuakes(map) { + _fetchQuakes() { if (!this._quakeLayer) return; var self = this; fetch('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson') @@ -406,54 +286,36 @@ const GEOINT = { self._quakeLayer.clearLayers(); var now = Date.now(); (data.features || []).forEach(function(f) { - var coords = f.geometry.coordinates; - var p = f.properties; + var c = f.geometry.coordinates, p = f.properties; var mag = p.mag || 1; var ageH = (now - p.time) / 3600000; 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 marker = L.circleMarker([coords[1], coords[0]], { - radius: radius, - fillColor: color, - color: color, - weight: 1.5, - fillOpacity: 0.6, - className: cls - }); - var timeStr = new Date(p.time).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }); - marker.bindPopup( - '
' + - 'M' + mag.toFixed(1) + ' ' + (p.place || '') + - '
TIEFE ' + (coords[2] || '?') + ' km' + - '
ZEIT ' + timeStr + - '
', { className: 'geoint-leaflet-popup' } - ); - marker.addTo(self._quakeLayer); + L.circleMarker([c[1], c[0]], { + radius: Math.max(mag * 3.5, 5), fillColor: color, color: color, + weight: 1.5, fillOpacity: 0.6, className: cls + }).bindPopup('
M' + mag.toFixed(1) + ' ' + (p.place||'') + + '
TIEFE ' + (c[2]||'?') + ' km
', + { className: 'geoint-leaflet-popup' } + ).addTo(self._quakeLayer); }); }) - .catch(function(e) { if (typeof DEV_MODE !== 'undefined' && DEV_MODE) console.warn('GEOINT quakes:', e); }); + .catch(function() {}); }, - // ----------------------------------------------------------------------- - // Layer: GDELT Nachrichten - // ----------------------------------------------------------------------- + // === GDELT NACHRICHTEN =================================================== _startGdelt(map) { if (this._gdeltLayer) return; this._gdeltLayer = L.markerClusterGroup({ maxClusterRadius: 30, iconCreateFunction: function(cluster) { - var count = cluster.getChildCount(); - return L.divIcon({ - html: '
' + (count > 99 ? '99+' : count) + '
', - className: '', - iconSize: [22, 22], - }); + var n = cluster.getChildCount(); + return L.divIcon({ html: '
' + (n > 99 ? '99+' : n) + '
', className: '', iconSize: [22, 22] }); } }).addTo(map); this._fetchGdelt(map); var self = this; - this._gdeltInterval = setInterval(function() { self._fetchGdelt(map); }, 600000); // 10 min + this._gdeltInterval = setInterval(function() { self._fetchGdelt(map); }, 600000); }, _stopGdelt() { @@ -461,67 +323,40 @@ const GEOINT = { if (this._gdeltLayer && this._map) { this._map.removeLayer(this._gdeltLayer); this._gdeltLayer = null; } }, - _fetchGdelt(map) { + _fetchGdelt() { if (!this._gdeltLayer) return; var self = this; - // Holt Lage-Kontext fuer GDELT-Suche - var query = ''; + var query = 'conflict OR crisis OR disaster'; if (typeof App !== 'undefined' && 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') || ''; - - fetch('/api/geoint/gdelt?query=' + query, { - headers: token ? { 'Authorization': 'Bearer ' + token } : {} - }) - .then(function(r) { return r.ok ? r.json() : Promise.reject(r.status); }) + fetch('/api/geoint/gdelt?query=' + query, { headers: token ? { 'Authorization': 'Bearer ' + token } : {} }) + .then(function(r) { return r.ok ? r.json() : { features: [] }; }) .then(function(data) { if (!self._gdeltLayer) return; self._gdeltLayer.clearLayers(); - var features = data.features || []; - features.slice(0, 200).forEach(function(f) { - var coords = f.geometry.coordinates; - var p = f.properties || {}; - var name = p.name || p.title || 'Nachricht'; - var url = p.url || p.shareimage || ''; - var icon = L.divIcon({ - className: '', - html: '
N
', - iconSize: [18, 18], - iconAnchor: [9, 9], - }); - var popup = '
' + - '' + name.substring(0, 100) + '' + - (url ? '
Quelle' : '') + - '
'; - L.marker([coords[1], coords[0]], { icon: icon }).bindPopup(popup, { className: 'geoint-leaflet-popup' }).addTo(self._gdeltLayer); + (data.features || []).slice(0, 200).forEach(function(f) { + var c = f.geometry.coordinates, p = f.properties || {}; + var icon = L.divIcon({ className: '', html: '
N
', iconSize: [18, 18], iconAnchor: [9, 9] }); + var popup = '
' + (p.name || p.title || 'Nachricht').substring(0, 100) + '' + + (p.url ? '
Quelle' : '') + '
'; + L.marker([c[1], c[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() {}); }, - // ----------------------------------------------------------------------- - // Layer: Heatmap - // ----------------------------------------------------------------------- + // === HEATMAP ============================================================= _startHeatmap(map) { - if (this._heatLayer) return; - if (typeof L.heatLayer === 'undefined') { - if (typeof DEV_MODE !== 'undefined' && DEV_MODE) console.warn('GEOINT: leaflet-heat nicht geladen'); - return; - } - // Locations aus UI-State holen - var locations = (typeof UI !== 'undefined' && UI._lastLocations) ? UI._lastLocations : []; - 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, + if (this._heatLayer || typeof L.heatLayer === 'undefined') return; + var locs = (typeof UI !== 'undefined' && UI._lastLocations) ? UI._lastLocations : []; + if (!locs.length) 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]; }); + this._heatLayer = L.heatLayer(pts, { + radius: 30, blur: 20, maxZoom: 12, gradient: { 0.2: '#004400', 0.4: '#00ff88', 0.6: '#ffaa00', 0.8: '#ff4400', 1.0: '#ff0000' } }).addTo(map); }, @@ -530,12 +365,10 @@ const GEOINT = { if (this._heatLayer && this._map) { this._map.removeLayer(this._heatLayer); this._heatLayer = null; } }, - // ----------------------------------------------------------------------- - // Koordinatenanzeige - // ----------------------------------------------------------------------- + // === KOORDINATENANZEIGE ================================================== _startCoords(map) { if (this._coordControl) return; - var CoordControl = L.Control.extend({ + var Ctrl = L.Control.extend({ options: { position: 'bottomleft' }, onAdd: function() { var div = L.DomUtil.create('div', 'geoint-coord-display'); @@ -544,7 +377,7 @@ const GEOINT = { return div; } }); - this._coordControl = new CoordControl(); + this._coordControl = new Ctrl(); map.addControl(this._coordControl); var el = document.getElementById('geoint-coord-text'); this._coordHandler = function(e) { @@ -558,193 +391,100 @@ const GEOINT = { if (this._coordControl && this._map) { this._map.removeControl(this._coordControl); this._coordControl = null; } }, - // ----------------------------------------------------------------------- - // Distanzmessung - // ----------------------------------------------------------------------- + // === DISTANZMESSUNG ====================================================== _startDistance(map) { if (this._distanceLayers) return; this._distanceLayers = L.layerGroup().addTo(map); this._distancePoints = []; - this._distanceActive = true; map.getContainer().style.cursor = 'crosshair'; var self = this; this._distanceHandler = function(e) { 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) { var p1 = self._distancePoints[self._distancePoints.length - 2]; 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 totalDist = 0; - for (var i = 1; i < self._distancePoints.length; i++) { - totalDist += self._distancePoints[i - 1].distanceTo(self._distancePoints[i]); - } - var label = dist >= 1000 - ? (dist / 1000).toFixed(1) + ' km' - : Math.round(dist) + ' m'; - 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: '
' + text + '
', - iconSize: [0, 0], - iconAnchor: [0, 12], - }) + var total = 0; + for (var i = 1; i < self._distancePoints.length; i++) total += 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 text = self._distancePoints.length > 2 ? label + ' (\u03a3 ' + tLabel + ')' : label; + L.marker([(p1.lat+p2.lat)/2, (p1.lng+p2.lng)/2], { + icon: L.divIcon({ className: '', html: '
' + text + '
', iconSize: [0,0], iconAnchor: [0,12] }) }).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); }, _stopDistance() { - this._distanceActive = false; this._distancePoints = []; - if (this._distanceHandler && this._map) { - this._map.off('click', this._distanceHandler); - this._distanceHandler = null; - this._map.getContainer().style.cursor = ''; - } + if (this._distanceHandler && this._map) { 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; } }, - // ----------------------------------------------------------------------- - // Timeline-Slider - // ----------------------------------------------------------------------- + // === TIMELINE ============================================================ initTimeline(articles) { if (!articles || !articles.length) return; - var dates = articles - .map(function(a) { return a.collected_at || a.published_at; }) - .filter(Boolean) - .map(function(d) { return new Date(d).getTime(); }) - .filter(function(t) { return !isNaN(t); }) - .sort(function(a, b) { return a - b; }); + var dates = articles.map(function(a) { return a.collected_at || a.published_at; }) + .filter(Boolean).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; - - 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'); + 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'); - if (!slider) return; - 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]); + if (label) label.textContent = this._fmtDate(dates[dates.length-1]); }, - _onTimelineChange(value) { + _onTimelineChange(val) { var label = document.getElementById('geoint-timeline-label'); - if (label) label.textContent = this._formatTimelineDate(parseInt(value)); - this._filterMarkersByTime(parseInt(value)); + if (label) label.textContent = this._fmtDate(parseInt(val)); + 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); return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) + ' ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); }, - _filterMarkersByTime(maxTime) { - // 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._stopQuakes(); - this._stopGdelt(); - this._stopHeatmap(); - this._stopCoords(); - this._stopDistance(); + this._stopFlights(); this._stopShips(); this._stopQuakes(); + this._stopGdelt(); this._stopHeatmap(); this._stopCoords(); this._stopDistance(); this._removeSubControl(); - // CSS-Klassen entfernen - 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'); + document.querySelectorAll('.geoint-active').forEach(function(el) { el.classList.remove('geoint-active'); }); }, - // ----------------------------------------------------------------------- - // State Persistenz - // ----------------------------------------------------------------------- + // === STATE =============================================================== _saveState() { try { localStorage.setItem('geoint_mode', this._active ? 'true' : 'false'); localStorage.setItem('geoint_sublayers', JSON.stringify(this._sublayers)); - } catch (e) { /* quota exceeded */ } + } catch(e) {} }, restoreState(map) { if (!map) return; this._map = map; - try { - var savedSublayers = localStorage.getItem('geoint_sublayers'); - if (savedSublayers) this._sublayers = JSON.parse(savedSublayers); - } catch (e) { this._sublayers = {}; } - - var wasActive = localStorage.getItem('geoint_mode') === 'true'; - if (wasActive) { - this.toggle(true, map); - } + try { var s = localStorage.getItem('geoint_sublayers'); if (s) this._sublayers = JSON.parse(s); } catch(e) { this._sublayers = {}; } + if (localStorage.getItem('geoint_mode') === 'true') this.toggle(true, map); }, };