feat: GEOINT-Toolkit mit 6 neuen Features

- EXIF-Extraktion: Automatische GPS/Kamera/Zeitstempel-Analyse bei Bildupload
- Sonnenstand-Rechner: Azimut, Elevation, Schattenverhaeltnis fuer beliebige Position/Zeit
- Reverse Geolocation: Erweiterte VLM-Analyse mit Landschaftsmerkmalen (Vegetation, Architektur, Strassen, Schilder)
- Nachtlichter: NASA VIIRS Black Marble Layer
- Hoehenprofil: Interaktives 2-Punkte-Tool mit SVG-Chart und Sichtlinienanalyse
- Funkmasten: Mobilfunkinfrastruktur via Overpass (zoomabhaengig)

Backend: data_geoint.py (EXIF, Sun, Elevation, Celltowers)
Frontend: GEOINT Tools Section im Layer Panel
Dieser Commit ist enthalten in:
Claude Dev
2026-03-26 08:58:05 +01:00
Ursprung 1b74c95bac
Commit c7cb19d584
10 geänderte Dateien mit 965 neuen und 4 gelöschten Zeilen

Datei anzeigen

@@ -786,3 +786,100 @@ html, body { height: 100%; overflow: hidden; background: var(--bg-primary); colo
.vlm-panel-right::-webkit-scrollbar { width: 4px; }
.vlm-panel-right::-webkit-scrollbar-track { background: transparent; }
.vlm-panel-right::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* === GEOINT Tools === */
.dot-nightlights { background: #ffd740; box-shadow: 0 0 4px rgba(255,215,64,0.4); }
.dot-celltowers { background: #e040fb; }
.geoint-btn-row {
display: flex;
gap: 4px;
padding: 4px 8px;
}
.geoint-tool-btn {
flex: 1;
padding: 5px 8px;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 700;
letter-spacing: 1px;
background: rgba(224, 64, 251, 0.08);
border: 1px solid rgba(224, 64, 251, 0.3);
border-radius: 3px;
color: #e040fb;
cursor: pointer;
transition: all 0.15s;
text-transform: uppercase;
}
.geoint-tool-btn:hover {
background: rgba(224, 64, 251, 0.18);
border-color: #e040fb;
}
.geoint-panel {
position: fixed;
top: 56px;
right: 12px;
width: 320px;
max-height: calc(100vh - 100px);
overflow-y: auto;
background: var(--bg-panel);
border: 1px solid rgba(224, 64, 251, 0.3);
border-radius: 8px;
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
z-index: 100;
padding: 14px;
}
.geoint-panel::-webkit-scrollbar { width: 4px; }
.geoint-panel::-webkit-scrollbar-thumb { background: rgba(224,64,251,0.3); border-radius: 2px; }
.geoint-input {
width: 100%;
padding: 6px 8px;
background: rgba(255,255,255,0.05);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-family: var(--font-mono);
font-size: 12px;
outline: none;
margin-top: 2px;
}
.geoint-input:focus { border-color: #e040fb; }
.geoint-result-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-top: 8px;
}
.geoint-result-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 8px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 4px;
}
.geoint-label {
font-size: 9px;
color: var(--text-dim);
letter-spacing: 0.5px;
text-transform: uppercase;
}
.geoint-value {
font-size: 13px;
color: var(--text);
font-weight: 600;
}
/* === VLM EXIF Card === */
.vlm-exif-card {
padding: 8px 10px;
background: rgba(224, 64, 251, 0.06);
border: 1px solid rgba(224, 64, 251, 0.2);
border-radius: 4px;
margin-bottom: 8px;
}

Datei anzeigen

@@ -114,6 +114,25 @@
<span class="layer-name" title="Satellitenbild-Analyse mit Claude VLM">Bildanalyse</span>
<span class="layer-count" id="count-vlm">-</span>
</label>
<div class="panel-divider"></div>
<div style="font-size:9px;letter-spacing:1.5px;color:#e040fb;margin-bottom:4px">GEOINT TOOLS</div>
<label class="layer-toggle">
<input type="checkbox" id="layer-nightlights" title="NASA VIIRS Nachtlicht-Karte (Black Marble)">
<span class="layer-dot dot-nightlights"></span>
<span class="layer-name" title="Globale Nachtlicht-Aktivitaet (Besiedlung, Stromausfaelle)">Nachtlichter</span>
</label>
<label class="layer-toggle">
<input type="checkbox" id="layer-celltowers" title="Mobilfunkmasten aus OpenStreetMap (zoomabhaengig)">
<span class="layer-dot dot-celltowers"></span>
<span class="layer-name" title="Sendemasten und Mobilfunkinfrastruktur">Funkmasten</span>
<span class="layer-count" id="count-celltowers">-</span>
</label>
<div class="layer-status" id="status-celltowers"></div>
<div class="geoint-btn-row">
<button class="geoint-tool-btn" onclick="GeointTools.showSunCalc()" title="Sonnenstand und Schattenanalyse berechnen">Sonne</button>
<button class="geoint-tool-btn" onclick="GeointTools.startElevation()" title="Hoehenprofil und Sichtlinienanalyse zwischen zwei Punkten">Hoehe</button>
</div>
<div class="panel-divider"></div>
<label class="layer-toggle">
<input type="checkbox" id="layer-iss" title="ISS Echtzeit-Position (5s Refresh)">
<span class="layer-dot dot-iss"></span>
@@ -204,6 +223,8 @@
<!-- VLM Bildanalyse Panel -->
<div id="vlm-panel" class="vlm-panel-right" style="display:none"></div>
<!-- GEOINT Tools Panel -->
<div id="geoint-panel" class="geoint-panel" style="display:none"></div>
<aside id="sidebar-right" class="sidebar-right">
<button id="sidebar-toggle" class="sidebar-toggle" title="Seitenleiste ein-/ausblenden">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
@@ -251,6 +272,9 @@
<script src="/static/js/layers/infra.js"></script>
<script src="/static/js/layers/overpass.js"></script>
<script src="/static/js/ui/vlm.js"></script>
<script src="/static/js/layers/nightlights.js"></script>
<script src="/static/js/layers/celltowers.js"></script>
<script src="/static/js/ui/geoint.js"></script>
<script src="/static/js/layers/iss.js"></script>
<script src="/static/js/layers/terminator.js"></script>
<script src="/static/js/layers/timezones.js"></script>

Datei anzeigen

@@ -171,6 +171,8 @@ const Globe = {
if (sidebar) sidebar.style.display = '';
}
},
'layer-nightlights': function(on) { on ? NightlightsLayer.start(Globe.viewer) : NightlightsLayer.stop(); },
'layer-celltowers': function(on) { on ? CelltowersLayer.start(Globe.viewer) : CelltowersLayer.stop(); },
'layer-iss': function(on) { on ? ISSLayer.start(Globe.viewer) : ISSLayer.stop(); },
'layer-disasters': function(on) { on ? DisastersLayer.start(Globe.viewer) : DisastersLayer.stop(); },
'layer-weather': function(on) { on ? WeatherLayer.start(Globe.viewer) : WeatherLayer.stop(); },
@@ -211,6 +213,10 @@ const Globe = {
if (typeof GdeltLayer !== 'undefined' && GdeltLayer._count > 0) {
document.getElementById('count-gdelt').textContent = GdeltLayer._count.toLocaleString('de-DE');
}
if (typeof CelltowersLayer !== 'undefined' && CelltowersLayer._count > 0) {
var ctEl = document.getElementById('count-celltowers');
if (ctEl) ctEl.textContent = CelltowersLayer._count.toLocaleString('de-DE');
}
if (parts.length) {
document.getElementById('bottom-stats').textContent = parts.join(' | ');
}

120
static/js/layers/celltowers.js Normale Datei
Datei anzeigen

@@ -0,0 +1,120 @@
/**
* Funkmasten-Layer: Mobilfunkmasten aus OpenStreetMap via Overpass.
*/
const CelltowersLayer = {
_viewer: null,
_points: null,
_labels: null,
_count: 0,
_interval: null,
_lastBbox: null,
start: function(viewer) {
if (this._points) return;
this._viewer = viewer;
this._points = viewer.scene.primitives.add(new Cesium.PointPrimitiveCollection());
this._labels = viewer.scene.primitives.add(new Cesium.LabelCollection());
this._fetch();
var self = this;
this._interval = setInterval(function() { self._fetchIfMoved(); }, 8000);
},
stop: function() {
if (this._interval) { clearInterval(this._interval); this._interval = null; }
if (this._points && this._viewer) { this._viewer.scene.primitives.remove(this._points); this._points = null; }
if (this._labels && this._viewer) { this._viewer.scene.primitives.remove(this._labels); this._labels = null; }
this._count = 0;
this._lastBbox = null;
var c = document.getElementById('count-celltowers');
if (c) c.textContent = '-';
},
_getBbox: function() {
var rect = this._viewer.camera.computeViewRectangle();
if (!rect) return null;
return {
south: Cesium.Math.toDegrees(rect.south),
west: Cesium.Math.toDegrees(rect.west),
north: Cesium.Math.toDegrees(rect.north),
east: Cesium.Math.toDegrees(rect.east),
};
},
_fetchIfMoved: function() {
var bbox = this._getBbox();
if (!bbox) return;
// Nur bei deutlicher Bewegung neu laden
if (this._lastBbox) {
var dLat = Math.abs(bbox.south - this._lastBbox.south) + Math.abs(bbox.north - this._lastBbox.north);
var dLon = Math.abs(bbox.west - this._lastBbox.west) + Math.abs(bbox.east - this._lastBbox.east);
if (dLat + dLon < 0.5) return;
}
// Nur bei Zoom nah genug (< ~500 km Sichtfeld)
var cam = this._viewer.camera.positionCartographic;
if (cam && cam.height > 600000) return;
this._fetch();
},
_fetch: function() {
var bbox = this._getBbox();
if (!bbox) return;
// Viewport zu gross -> nicht laden
if (Math.abs(bbox.north - bbox.south) > 5 || Math.abs(bbox.east - bbox.west) > 5) {
var s = document.getElementById('status-celltowers');
if (s) { s.textContent = 'Naeher heranzoomen'; s.classList.add('active'); }
return;
}
this._lastBbox = bbox;
var self = this;
fetch('/api/geoint/celltowers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(bbox),
})
.then(function(r) { return r.json(); })
.then(function(data) {
self._render(data.towers || []);
})
.catch(function() {});
},
_render: function(towers) {
if (!this._points) return;
this._points.removeAll();
this._labels.removeAll();
var color = Cesium.Color.fromCssColorString('#e040fb');
var colorDim = color.withAlpha(0.6);
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
this._points.add({
position: Cesium.Cartesian3.fromDegrees(t.lon, t.lat, 0),
pixelSize: 6,
color: color,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 1,
});
var label = t.operator || t.name || '';
if (label && towers.length < 200) {
this._labels.add({
position: Cesium.Cartesian3.fromDegrees(t.lon, t.lat, 0),
text: label,
font: '9px monospace',
fillColor: colorDim,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(6, -4),
scale: 0.6,
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 100000),
});
}
}
this._count = towers.length;
var c = document.getElementById('count-celltowers');
if (c) c.textContent = this._count.toLocaleString('de-DE');
var s = document.getElementById('status-celltowers');
if (s) { s.textContent = ''; s.classList.remove('active'); }
},
};

Datei anzeigen

@@ -0,0 +1,36 @@
/**
* Nachtlicht-Layer: NASA VIIRS Black Marble Nighttime Lights.
*/
const NightlightsLayer = {
_viewer: null,
_layer: null,
start: function(viewer) {
if (this._layer) return;
this._viewer = viewer;
this._layer = viewer.imageryLayers.addImageryProvider(
new Cesium.WebMapTileServiceImageryProvider({
url: 'https://gibs.earthdata.nasa.gov/wmts/epsg4326/best/VIIRS_Black_Marble/default/2024-01-01/500m/{TileMatrix}/{TileRow}/{TileCol}.png',
layer: 'VIIRS_Black_Marble',
style: 'default',
tileMatrixSetID: '500m',
tileMatrixLabels: [
'0', '1', '2', '3', '4', '5', '6', '7', '8',
],
format: 'image/png',
tilingScheme: new Cesium.GeographicTilingScheme(),
tileWidth: 512,
tileHeight: 512,
credit: 'NASA VIIRS Black Marble',
})
);
this._layer.alpha = 0.85;
},
stop: function() {
if (this._layer && this._viewer) {
this._viewer.imageryLayers.remove(this._layer);
this._layer = null;
}
},
};

221
static/js/ui/geoint.js Normale Datei
Datei anzeigen

@@ -0,0 +1,221 @@
/**
* GEOINT Tools: Sonnenstand-Rechner, Hoehenprofil, EXIF-Anzeige im VLM-Workflow.
*/
const GeointTools = {
_panel: null,
_elevMode: false,
_elevStart: null,
_elevHandler: null,
_elevLine: null,
_elevLabels: null,
// ============================================================
// Sonnenstand-Rechner
// ============================================================
showSunCalc: function() {
var panel = document.getElementById('geoint-panel');
if (!panel) return;
// Default: aktuelle Cursor-Position oder Kartenmitte
var cam = Globe.viewer.camera.positionCartographic;
var lat = cam ? Cesium.Math.toDegrees(cam.latitude).toFixed(4) : '50.0';
var lon = cam ? Cesium.Math.toDegrees(cam.longitude).toFixed(4) : '10.0';
var now = new Date().toISOString().slice(0, 16);
panel.innerHTML =
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">' +
'<div style="font-size:11px;font-weight:700;letter-spacing:2px;color:var(--accent)">SONNENSTAND</div>' +
'<button onclick="GeointTools.closePanel()" style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:18px">&times;</button>' +
'</div>' +
'<div style="display:flex;flex-direction:column;gap:6px;font-size:11px">' +
'<div style="display:flex;gap:6px">' +
'<div style="flex:1"><label style="color:var(--text-dim)">Breitengrad</label><input type="number" id="sun-lat" value="' + lat + '" step="0.01" class="geoint-input"></div>' +
'<div style="flex:1"><label style="color:var(--text-dim)">Laengengrad</label><input type="number" id="sun-lon" value="' + lon + '" step="0.01" class="geoint-input"></div>' +
'</div>' +
'<div><label style="color:var(--text-dim)">Datum / Uhrzeit (UTC)</label><input type="datetime-local" id="sun-datetime" value="' + now + '" class="geoint-input"></div>' +
'<button class="overpass-exec-btn" onclick="GeointTools._calcSun()" style="margin-top:4px">BERECHNEN</button>' +
'</div>' +
'<div id="sun-result" style="margin-top:10px"></div>';
panel.style.display = 'block';
},
_calcSun: function() {
var lat = parseFloat(document.getElementById('sun-lat').value);
var lon = parseFloat(document.getElementById('sun-lon').value);
var dt = document.getElementById('sun-datetime').value;
if (isNaN(lat) || isNaN(lon) || !dt) return;
var resultEl = document.getElementById('sun-result');
resultEl.innerHTML = '<div style="color:var(--text-dim);font-size:11px">Berechne...</div>';
fetch('/api/geoint/sun-position', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ latitude: lat, longitude: lon, datetime_iso: dt + ':00Z' }),
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.detail) { resultEl.innerHTML = '<div style="color:#ff5252">' + data.detail + '</div>'; return; }
var dayIcon = data.is_daylight ? '(Tag)' : '(Nacht)';
var golden = data.golden_hour ? ' | Golden Hour' : '';
resultEl.innerHTML =
'<div class="geoint-result-grid">' +
'<div class="geoint-result-item"><span class="geoint-label">Azimut</span><span class="geoint-value">' + data.azimuth + '&deg;</span></div>' +
'<div class="geoint-result-item"><span class="geoint-label">Elevation</span><span class="geoint-value">' + data.elevation + '&deg; ' + dayIcon + '</span></div>' +
'<div class="geoint-result-item"><span class="geoint-label">Schattenrichtung</span><span class="geoint-value">' + data.shadow_direction + '&deg;</span></div>' +
'<div class="geoint-result-item"><span class="geoint-label">Schattenverhaeltnis</span><span class="geoint-value">' + (data.shadow_ratio !== null ? data.shadow_ratio + 'x Objekthoehe' : 'Kein Schatten') + '</span></div>' +
(golden ? '<div class="geoint-result-item" style="grid-column:1/-1"><span class="geoint-label" style="color:#ff9800">Golden Hour aktiv</span></div>' : '') +
'</div>';
})
.catch(function(e) { resultEl.innerHTML = '<div style="color:#ff5252">Fehler: ' + e.message + '</div>'; });
},
// ============================================================
// Hoehenprofil (2-Punkte-Tool)
// ============================================================
startElevation: function() {
if (this._elevMode) { this.stopElevation(); return; }
this._elevMode = true;
this._elevStart = null;
var panel = document.getElementById('geoint-panel');
if (panel) {
panel.innerHTML =
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">' +
'<div style="font-size:11px;font-weight:700;letter-spacing:2px;color:var(--accent)">HOEHENPROFIL</div>' +
'<button onclick="GeointTools.stopElevation()" style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:18px">&times;</button>' +
'</div>' +
'<div id="elev-status" style="font-size:12px;color:var(--accent)">Klicke Startpunkt auf den Globus</div>' +
'<div id="elev-result" style="margin-top:10px"></div>';
panel.style.display = 'block';
}
// Polyline + Labels fuer Visualisierung
this._elevLine = Globe.viewer.scene.primitives.add(new Cesium.PolylineCollection());
this._elevLabels = Globe.viewer.scene.primitives.add(new Cesium.LabelCollection());
var self = this;
this._elevHandler = new Cesium.ScreenSpaceEventHandler(Globe.viewer.scene.canvas);
this._elevHandler.setInputAction(function(click) {
var cart = Globe.viewer.scene.pickPosition(click.position);
if (!cart) {
var ray = Globe.viewer.scene.camera.getPickRay(click.position);
cart = Globe.viewer.scene.globe.pick(ray, Globe.viewer.scene);
}
if (!cart) return;
var c = Cesium.Cartographic.fromCartesian(cart);
var lat = Cesium.Math.toDegrees(c.latitude);
var lon = Cesium.Math.toDegrees(c.longitude);
if (!self._elevStart) {
self._elevStart = { lat: lat, lon: lon };
// Marker setzen
self._elevLabels.add({
position: cart, text: 'A',
font: '14px monospace', fillColor: Cesium.Color.fromCssColorString('#00ff88'),
outlineColor: Cesium.Color.BLACK, outlineWidth: 3,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(0, -14),
});
var statusEl = document.getElementById('elev-status');
if (statusEl) statusEl.textContent = 'Klicke Endpunkt auf den Globus';
} else {
// Endpunkt
self._elevLabels.add({
position: cart, text: 'B',
font: '14px monospace', fillColor: Cesium.Color.fromCssColorString('#ff5252'),
outlineColor: Cesium.Color.BLACK, outlineWidth: 3,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(0, -14),
});
// Linie
var startCart = Cesium.Cartesian3.fromDegrees(self._elevStart.lon, self._elevStart.lat, 100);
var endCart = Cesium.Cartesian3.fromDegrees(lon, lat, 100);
self._elevLine.add({
positions: [startCart, endCart], width: 2,
material: Cesium.Material.fromType('Color', { color: Cesium.Color.fromCssColorString('#00ff88').withAlpha(0.6) }),
});
self._fetchElevation(self._elevStart.lat, self._elevStart.lon, lat, lon);
if (self._elevHandler) { self._elevHandler.destroy(); self._elevHandler = null; }
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
},
_fetchElevation: function(sLat, sLon, eLat, eLon) {
var statusEl = document.getElementById('elev-status');
if (statusEl) statusEl.textContent = 'Berechne Hoehenprofil...';
fetch('/api/geoint/elevation-profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start_lat: sLat, start_lon: sLon, end_lat: eLat, end_lon: eLon, samples: 80 }),
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.detail) { if (statusEl) statusEl.textContent = 'Fehler: ' + data.detail; return; }
if (statusEl) statusEl.textContent = '';
GeointTools._renderElevation(data);
})
.catch(function(e) { if (statusEl) statusEl.textContent = 'Fehler: ' + e.message; });
},
_renderElevation: function(data) {
var resultEl = document.getElementById('elev-result');
if (!resultEl) return;
var s = data.stats;
var los = data.line_of_sight;
var losColor = los.clear ? '#00ff88' : '#ff5252';
var losText = los.clear ? 'Frei' : 'Blockiert';
// SVG Chart
var points = data.points;
var maxElev = s.elevation_max + 10;
var minElev = Math.max(0, s.elevation_min - 10);
var range = maxElev - minElev || 1;
var w = 280, h = 100;
var pathD = '';
for (var i = 0; i < points.length; i++) {
var x = (i / (points.length - 1)) * w;
var y = h - ((points[i].elevation - minElev) / range) * h;
pathD += (i === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1);
}
// LOS line
var losY1 = h - ((points[0].elevation - minElev) / range) * h;
var losY2 = h - ((points[points.length - 1].elevation - minElev) / range) * h;
resultEl.innerHTML =
'<svg width="' + w + '" height="' + (h + 20) + '" style="display:block;margin-bottom:8px">' +
'<defs><linearGradient id="elev-grad" x1="0" y1="0" x2="0" y2="1">' +
'<stop offset="0" stop-color="#00ff88" stop-opacity="0.3"/>' +
'<stop offset="1" stop-color="#00ff88" stop-opacity="0.02"/>' +
'</linearGradient></defs>' +
'<path d="' + pathD + 'L' + w + ',' + (h + 5) + 'L0,' + (h + 5) + 'Z" fill="url(#elev-grad)" stroke="none"/>' +
'<path d="' + pathD + '" fill="none" stroke="#00ff88" stroke-width="1.5"/>' +
'<line x1="0" y1="' + losY1.toFixed(1) + '" x2="' + w + '" y2="' + losY2.toFixed(1) + '" stroke="' + losColor + '" stroke-width="1" stroke-dasharray="4,3" opacity="0.6"/>' +
'<text x="2" y="' + (h + 14) + '" fill="var(--text-dim)" font-size="9" font-family="monospace">0 km</text>' +
'<text x="' + (w - 2) + '" y="' + (h + 14) + '" fill="var(--text-dim)" font-size="9" font-family="monospace" text-anchor="end">' + s.distance_km + ' km</text>' +
'</svg>' +
'<div class="geoint-result-grid">' +
'<div class="geoint-result-item"><span class="geoint-label">Distanz</span><span class="geoint-value">' + s.distance_km + ' km</span></div>' +
'<div class="geoint-result-item"><span class="geoint-label">Sichtlinie</span><span class="geoint-value" style="color:' + losColor + '">' + losText + '</span></div>' +
'<div class="geoint-result-item"><span class="geoint-label">Min / Max</span><span class="geoint-value">' + s.elevation_min + ' / ' + s.elevation_max + ' m</span></div>' +
'<div class="geoint-result-item"><span class="geoint-label">Hoehendiff.</span><span class="geoint-value">' + s.elevation_diff + ' m</span></div>' +
'<div class="geoint-result-item"><span class="geoint-label">Anstieg</span><span class="geoint-value">+' + s.total_ascent + ' m</span></div>' +
'<div class="geoint-result-item"><span class="geoint-label">Abstieg</span><span class="geoint-value">-' + s.total_descent + ' m</span></div>' +
'</div>' +
'<button class="overpass-exec-btn" onclick="GeointTools.startElevation()" style="margin-top:8px;font-size:10px;padding:6px">NEUE MESSUNG</button>';
},
stopElevation: function() {
this._elevMode = false;
this._elevStart = null;
if (this._elevHandler) { this._elevHandler.destroy(); this._elevHandler = null; }
if (this._elevLine && Globe.viewer) { Globe.viewer.scene.primitives.remove(this._elevLine); this._elevLine = null; }
if (this._elevLabels && Globe.viewer) { Globe.viewer.scene.primitives.remove(this._elevLabels); this._elevLabels = null; }
this.closePanel();
},
closePanel: function() {
var panel = document.getElementById('geoint-panel');
if (panel) { panel.style.display = 'none'; panel.innerHTML = ''; }
},
};

Datei anzeigen

@@ -189,13 +189,55 @@ const VlmUI = {
var resetBtn = document.getElementById('vlm-reset-btn');
if (!resultsEl) return;
// EXIF-Metadaten
var exifHtml = '';
if (data.exif) {
var e = data.exif;
var ep = [];
if (e.has_gps) ep.push('<span style="color:var(--accent)">GPS: ' + e.latitude + ', ' + e.longitude + '</span>');
if (e.altitude) ep.push('Hoehe: ' + e.altitude + 'm');
if (e.camera_model) ep.push(e.camera_make ? e.camera_make + ' ' + e.camera_model : e.camera_model);
if (e.focal_length) ep.push(e.focal_length + 'mm');
if (e.timestamp) ep.push(e.timestamp.replace('T', ' '));
if (e.compass_heading) ep.push('Heading: ' + e.compass_heading + '\u00B0');
if (ep.length > 0) {
exifHtml = '<div class="vlm-exif-card"><div style="font-size:9px;letter-spacing:1.5px;color:#e040fb;margin-bottom:4px">EXIF METADATEN</div>' +
'<div style="font-size:11px;color:var(--text);line-height:1.6">' + ep.join(' &middot; ') + '</div></div>';
if (e.has_gps) {
exifHtml += '<button class="geoint-tool-btn" style="width:100%;margin-top:4px" onclick="Globe.viewer.camera.flyTo({destination:Cesium.Cartesian3.fromDegrees(' + e.longitude + ',' + e.latitude + ',50000),duration:2})">GPS-Position anfliegen</button>';
}
} else {
exifHtml = '<div class="vlm-exif-card"><div style="font-size:9px;letter-spacing:1.5px;color:var(--text-dim);margin-bottom:2px">EXIF</div><div style="font-size:11px;color:var(--text-dim)">Keine Metadaten gefunden</div></div>';
}
}
// Landscape Clues (Reverse Geolocation)
var cluesHtml = '';
if (data.landscape_clues) {
var lc = data.landscape_clues;
var cl = [];
if (lc.vegetation) cl.push('<b>Vegetation:</b> ' + lc.vegetation);
if (lc.soil_color) cl.push('<b>Boden:</b> ' + lc.soil_color);
if (lc.road_markings) cl.push('<b>Strassen:</b> ' + lc.road_markings);
if (lc.architecture_style) cl.push('<b>Architektur:</b> ' + lc.architecture_style);
if (lc.signage_language) cl.push('<b>Schilder:</b> ' + lc.signage_language);
if (lc.vehicle_types) cl.push('<b>Fahrzeuge:</b> ' + lc.vehicle_types);
if (lc.climate_indicators) cl.push('<b>Klima:</b> ' + lc.climate_indicators);
if (lc.sun_shadow_direction) cl.push('<b>Schatten:</b> ' + lc.sun_shadow_direction);
if (cl.length > 0) {
cluesHtml = '<div style="font-size:9px;letter-spacing:1.5px;color:#ff9800;margin:8px 0 4px">LANDSCHAFTSMERKMALE</div>' +
'<div style="font-size:11px;color:var(--text);line-height:1.8">' + cl.join('<br>') + '</div>';
}
}
// Szene
var sceneEl = document.getElementById('vlm-scene');
if (sceneEl) {
var text = data.scene_description || 'Keine Beschreibung';
if (data.terrain) text += ' | Gelände: ' + data.terrain;
if (data.estimated_location_type) text += ' | Region: ' + data.estimated_location_type;
sceneEl.textContent = text;
sceneEl.innerHTML = exifHtml +
'<div style="margin-top:8px">' + (data.scene_description || 'Keine Beschreibung') +
(data.terrain ? ' | Gelaende: ' + data.terrain : '') +
(data.estimated_location_type ? ' | Region: ' + data.estimated_location_type : '') +
'</div>' + cluesHtml;
}
// Objekte