Feature: Overpass Turbo + VLM-Bildanalyse

Overpass Turbo Integration:
- POST /api/overpass/query: OverpassQL-Proxy mit Caching, Rate-Limiting, Fallback
- GET /api/overpass/templates: 30 OSINT-Templates in 6 Kategorien
- Frontend: Query-Editor Panel mit Template-Browser, Viewport-BBox
- Layer: Nodes/Ways/Relations Rendering auf CesiumJS mit OSM-Tag InfoBox

VLM-Bildanalyse:
- POST /api/vlm/analyze: Bildupload -> Claude Code headless (Sonnet) -> GEOINT-Analyse
- POST /api/vlm/generate-queries: VLM-Erkennungen -> OverpassQL
- Frontend: Drag&Drop Upload, Zwei-Stufen-Workflow (Analyse -> Overpass-Suche)
- Bild-Resize (Pillow), asyncio Subprocess, Semaphore (max 1 parallel)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Dev
2026-03-26 00:03:01 +01:00
Ursprung ed7db697f1
Commit 7be5edd983
9 geänderte Dateien mit 1727 neuen und 0 gelöschten Zeilen

Datei anzeigen

@@ -589,3 +589,200 @@ html, body { height: 100%; overflow: hidden; background: var(--bg-primary); colo
background: rgba(255,255,255,0.02);
border-radius: 0 4px 4px 0;
}
/* === Overpass Turbo === */
.dot-overpass { background: #ff9800; }
.overpass-panel {
position: fixed;
top: 56px;
right: 12px;
width: 360px;
max-height: calc(100vh - 100px);
overflow-y: auto;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 8px;
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
z-index: 100;
padding: 14px;
}
#overpass-editor {
width: 100%;
min-height: 100px;
background: rgba(0,0,0,0.3);
color: var(--accent);
font-family: var(--font-mono);
font-size: 12px;
border: 1px solid var(--border);
border-radius: 4px;
padding: 8px;
resize: vertical;
line-height: 1.5;
}
#overpass-editor:focus {
border-color: var(--accent);
outline: none;
box-shadow: 0 0 8px rgba(0,255,136,0.15);
}
#overpass-editor::placeholder { color: var(--text-dim); opacity: 0.5; }
.overpass-cat-tabs { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px; }
.overpass-cat-tab {
padding: 4px 8px;
font-size: 10px;
font-family: var(--font-mono);
letter-spacing: 0.5px;
background: rgba(255,255,255,0.05);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text-dim);
cursor: pointer;
transition: all 0.15s;
}
.overpass-cat-tab:hover { border-color: rgba(0,255,136,0.3); color: var(--text); }
.overpass-cat-tab.active {
color: var(--accent);
border-color: var(--accent);
background: rgba(0,255,136,0.1);
}
.overpass-template-btn {
display: block;
width: 100%;
text-align: left;
padding: 5px 8px;
margin: 1px 0;
background: transparent;
border: 1px solid transparent;
color: var(--text);
font-family: var(--font-mono);
font-size: 12px;
cursor: pointer;
border-radius: 3px;
transition: all 0.1s;
}
.overpass-template-btn:hover {
background: rgba(0,255,136,0.06);
border-color: var(--border);
}
.overpass-exec-btn {
width: 100%;
padding: 10px;
background: rgba(0,255,136,0.12);
border: 1px solid var(--accent);
color: var(--accent);
font-family: var(--font-mono);
font-size: 12px;
font-weight: 700;
border-radius: 4px;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 2px;
transition: all 0.15s;
}
.overpass-exec-btn:hover { background: rgba(0,255,136,0.22); }
.overpass-exec-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.overpass-panel::-webkit-scrollbar { width: 4px; }
.overpass-panel::-webkit-scrollbar-track { background: transparent; }
.overpass-panel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* === VLM Bildanalyse === */
.dot-vlm { background: #e040fb; }
.vlm-panel {
position: fixed;
top: 56px;
left: 268px;
width: 360px;
max-height: calc(100vh - 100px);
overflow-y: auto;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 8px;
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
z-index: 100;
padding: 14px;
}
.vlm-drop-zone {
border: 2px dashed var(--border);
border-radius: 8px;
padding: 28px 16px;
text-align: center;
color: var(--text-dim);
cursor: pointer;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
}
.vlm-drop-zone:hover,
.vlm-drop-active {
border-color: var(--accent);
background: rgba(0,255,136,0.04);
color: var(--accent);
}
.vlm-preview img {
max-width: 100%;
max-height: 180px;
object-fit: contain;
border-radius: 4px;
border: 1px solid var(--border);
margin: 8px 0 4px;
}
.vlm-loading {
text-align: center;
padding: 20px;
color: var(--accent);
font-size: 12px;
}
.vlm-spinner {
width: 28px;
height: 28px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: vlm-spin 0.8s linear infinite;
margin: 0 auto 10px;
}
@keyframes vlm-spin { to { transform: rotate(360deg); } }
.vlm-object-card {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border: 1px solid var(--border);
border-radius: 4px;
margin: 4px 0;
font-size: 12px;
transition: border-color 0.15s;
}
.vlm-object-card:hover { border-color: rgba(0,255,136,0.3); }
.vlm-confidence-high { color: #00ff88; }
.vlm-confidence-medium { color: #ff9800; }
.vlm-confidence-low { color: #ff5252; }
.vlm-badge-mil {
background: rgba(255,82,82,0.2);
color: #ff5252;
font-size: 9px;
font-weight: 700;
padding: 2px 5px;
border-radius: 2px;
letter-spacing: 0.5px;
flex-shrink: 0;
}
.vlm-panel::-webkit-scrollbar { width: 4px; }
.vlm-panel::-webkit-scrollbar-track { background: transparent; }
.vlm-panel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }

Datei anzeigen

@@ -108,6 +108,19 @@
<span class="layer-count" id="count-infra">-</span>
</label>
<div class="layer-status" id="status-infra"></div>
<label class="layer-toggle">
<input type="checkbox" id="layer-overpass" title="Overpass Turbo: Beliebige OSM-Daten abfragen">
<span class="layer-dot dot-overpass"></span>
<span class="layer-name" title="OverpassQL-Abfragen gegen OpenStreetMap">Overpass</span>
<span class="layer-count" id="count-overpass">-</span>
</label>
<div class="layer-status" id="status-overpass"></div>
<label class="layer-toggle">
<input type="checkbox" id="layer-vlm" title="VLM-Bildanalyse: Bild hochladen, Claude analysiert, Overpass sucht passende Orte">
<span class="layer-dot dot-vlm"></span>
<span class="layer-name" title="Satellitenbild-Analyse mit Claude VLM">Bildanalyse</span>
<span class="layer-count" id="count-vlm">-</span>
</label>
<label class="layer-toggle">
<input type="checkbox" id="layer-iss" title="ISS Echtzeit-Position (5s Refresh)">
<span class="layer-dot dot-iss"></span>
@@ -195,6 +208,11 @@
<div id="city-links" class="city-links"></div>
<!-- Rechte Sidebar: Datenpunkt-Uebersicht -->
<!-- Overpass Panel -->
<div id="overpass-panel" class="overpass-panel" style="display:none"></div>
<!-- VLM Bildanalyse Panel -->
<div id="vlm-panel" class="vlm-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>
@@ -240,6 +258,9 @@
<script src="/static/js/layers/military.js"></script>
<script src="/static/js/layers/cables.js"></script>
<script src="/static/js/layers/infra.js"></script>
<script src="/static/js/layers/overpass.js"></script>
<script src="/static/js/ui/overpass.js"></script>
<script src="/static/js/ui/vlm.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

@@ -159,6 +159,14 @@ const Globe = {
'layer-satellites': function(on) { on ? SatellitesLayer.start(Globe.viewer) : SatellitesLayer.stop(); },
'layer-cables': function(on) { on ? CablesLayer.start(Globe.viewer) : CablesLayer.stop(); },
'layer-infra': function(on) { on ? InfraLayer.start(Globe.viewer) : InfraLayer.stop(); },
'layer-overpass': function(on) {
if (on) { OverpassLayer.start(Globe.viewer); OverpassUI.show(); }
else { OverpassLayer.stop(); OverpassUI.hide(); }
},
'layer-vlm': function(on) {
if (on) { VlmUI.show(); }
else { VlmUI.hide(); }
},
'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
static/js/layers/overpass.js Normale Datei
Datei anzeigen

@@ -0,0 +1,211 @@
/**
* Overpass Layer: Rendert Overpass-API-Ergebnisse auf dem CesiumJS-Globus.
* Nodes als Punkte, Ways als Linien, Relations als Punkte (Center).
*/
const OverpassLayer = {
_viewer: null,
_points: null,
_labels: null,
_polylines: null,
_count: 0,
_data: null,
_handler: null,
_nodeIndex: [],
_currentColor: '#ff9800',
start(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._polylines = viewer.scene.primitives.add(new Cesium.PolylineCollection());
this._setupClickHandler();
},
stop() {
if (this._handler) { this._handler.destroy(); this._handler = null; }
if (this._points && this._viewer) { this._viewer.scene.primitives.remove(this._points); this._points = null; }
if (this._labels && this._viewer) { this._viewer.scene.primitives.remove(this._labels); this._labels = null; }
if (this._polylines && this._viewer) { this._viewer.scene.primitives.remove(this._polylines); this._polylines = null; }
this._count = 0;
this._data = null;
this._nodeIndex = [];
var countEl = document.getElementById('count-overpass');
if (countEl) countEl.textContent = '-';
},
clear() {
if (this._points) this._points.removeAll();
if (this._labels) this._labels.removeAll();
if (this._polylines) this._polylines.removeAll();
this._count = 0;
this._data = null;
this._nodeIndex = [];
var countEl = document.getElementById('count-overpass');
if (countEl) countEl.textContent = '-';
},
setColor(hexColor) {
this._currentColor = hexColor || '#ff9800';
},
render(data) {
this.clear();
if (!this._points) return;
this._data = data;
var color = Cesium.Color.fromCssColorString(this._currentColor);
var colorDim = color.withAlpha(0.6);
// Nodes
var nodes = data.nodes || [];
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i];
if (!n.lat || !n.lon) continue;
this._points.add({
position: Cesium.Cartesian3.fromDegrees(n.lon, n.lat, 0),
pixelSize: 7,
color: color,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 1,
});
this._nodeIndex.push({ lat: n.lat, lon: n.lon, tags: n.tags, name: n.name, type: 'node' });
if (n.name) {
this._labels.add({
position: Cesium.Cartesian3.fromDegrees(n.lon, n.lat, 0),
text: n.name,
font: '10px monospace',
fillColor: color.withAlpha(0.8),
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(6, -4),
scale: 0.7,
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 500000),
});
}
}
// Ways
var ways = data.ways || [];
for (var j = 0; j < ways.length; j++) {
var w = ways[j];
if (w.geometry && w.geometry.length > 1) {
var positions = [];
for (var k = 0; k < w.geometry.length; k++) {
var g = w.geometry[k];
if (g.lat && g.lon) {
positions.push(Cesium.Cartesian3.fromDegrees(g.lon, g.lat, 0));
}
}
if (positions.length > 1) {
this._polylines.add({
positions: positions,
width: 2.5,
material: Cesium.Material.fromType('Color', { color: colorDim }),
});
}
}
if (w.lat && w.lon) {
this._points.add({
position: Cesium.Cartesian3.fromDegrees(w.lon, w.lat, 0),
pixelSize: 5,
color: colorDim,
});
this._nodeIndex.push({ lat: w.lat, lon: w.lon, tags: w.tags, name: w.name, type: 'way' });
if (w.name) {
this._labels.add({
position: Cesium.Cartesian3.fromDegrees(w.lon, w.lat, 0),
text: w.name,
font: '10px monospace',
fillColor: color.withAlpha(0.7),
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(6, -4),
scale: 0.7,
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 500000),
});
}
}
}
// Relations (Center-Punkt)
var rels = data.relations || [];
for (var m = 0; m < rels.length; m++) {
var r = rels[m];
if (!r.lat || !r.lon) continue;
this._points.add({
position: Cesium.Cartesian3.fromDegrees(r.lon, r.lat, 0),
pixelSize: 9,
color: color,
outlineColor: color.withAlpha(0.3),
outlineWidth: 3,
});
this._nodeIndex.push({ lat: r.lat, lon: r.lon, tags: r.tags, name: r.name, type: 'relation' });
if (r.name) {
this._labels.add({
position: Cesium.Cartesian3.fromDegrees(r.lon, r.lat, 0),
text: r.name,
font: '11px monospace',
fillColor: color,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(8, -6),
scale: 0.8,
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 800000),
});
}
}
this._count = nodes.length + ways.length + rels.length;
var countEl = document.getElementById('count-overpass');
if (countEl) countEl.textContent = this._count.toLocaleString('de-DE');
},
_setupClickHandler() {
var self = this;
this._handler = new Cesium.ScreenSpaceEventHandler(this._viewer.scene.canvas);
this._handler.setInputAction(function(click) {
if (!self._nodeIndex.length) return;
var cart = self._viewer.scene.pickPosition(click.position);
if (!cart) {
var ray = self._viewer.scene.camera.getPickRay(click.position);
cart = self._viewer.scene.globe.pick(ray, self._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);
var best = null, bd = 999;
for (var i = 0; i < self._nodeIndex.length; i++) {
var el = self._nodeIndex[i];
var d = Math.abs(el.lat - lat) + Math.abs(el.lon - lon);
if (d < bd) { bd = d; best = el; }
}
if (best && bd < 0.3) {
var name = best.name || 'Unbenannt';
var typeLabels = { node: 'Node', way: 'Way', relation: 'Relation' };
var tagsHtml = '';
var tags = best.tags || {};
var tagKeys = Object.keys(tags);
for (var t = 0; t < tagKeys.length && t < 20; t++) {
var key = tagKeys[t];
tagsHtml += '<tr><td style="color:#00ff88;padding:2px 8px 2px 0">' + key + '</td>' +
'<td style="color:#ccc">' + tags[key] + '</td></tr>';
}
self._viewer.trackedEntity = undefined;
self._viewer.selectedEntity = new Cesium.Entity({
name: name,
description: '<div style="font-family:monospace;font-size:12px;padding:8px;color:#e8eaf0">' +
'<div style="font-size:10px;color:#888;margin-bottom:4px">' + typeLabels[best.type] + ' | OpenStreetMap</div>' +
'<strong style="color:' + self._currentColor + ';font-size:14px">' + name + '</strong>' +
'<table style="margin-top:8px;border-collapse:collapse;width:100%">' + tagsHtml + '</table></div>',
});
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
},
getCount: function() { return this._count; },
};

207
static/js/ui/overpass.js Normale Datei
Datei anzeigen

@@ -0,0 +1,207 @@
/**
* Overpass UI: Query-Editor-Panel mit Template-Browser und BBox-Integration.
*/
const OverpassUI = {
_panel: null,
_editor: null,
_templates: [],
_categories: [],
_activeCategory: null,
_isLoading: false,
_useBbox: true,
init: function() {
this._createPanel();
this._loadTemplates();
},
show: function() {
if (!this._panel) this.init();
this._panel.style.display = 'block';
if (!OverpassLayer._points) OverpassLayer.start(Globe.viewer);
},
hide: function() {
if (this._panel) this._panel.style.display = 'none';
},
_createPanel: function() {
var panel = document.getElementById('overpass-panel');
if (!panel) return;
this._panel = panel;
panel.innerHTML =
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">' +
'<div style="font-size:11px;font-weight:700;letter-spacing:2px;color:var(--accent)">OVERPASS QUERY</div>' +
'<button onclick="OverpassUI.hide();var cb=document.getElementById(\'layer-overpass\');if(cb){cb.checked=false;}OverpassLayer.stop();" ' +
'style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:18px;line-height:1" title="Schliessen">&times;</button>' +
'</div>' +
'<div id="overpass-cat-tabs" class="overpass-cat-tabs"></div>' +
'<div id="overpass-template-list" style="max-height:160px;overflow-y:auto;margin-bottom:10px"></div>' +
'<div class="panel-divider"></div>' +
'<div style="font-size:9px;letter-spacing:1.5px;color:var(--text-dim);margin-bottom:4px">OVERPASSQL EDITOR</div>' +
'<textarea id="overpass-editor" rows="6" spellcheck="false" placeholder="[out:json][timeout:25];\n(\n node[&quot;amenity&quot;]({{bbox}});\n);\nout center body;"></textarea>' +
'<div style="display:flex;align-items:center;gap:8px;margin:10px 0">' +
'<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text);cursor:pointer;flex:1">' +
'<input type="checkbox" id="overpass-bbox" checked onchange="OverpassUI._useBbox=this.checked" style="accent-color:var(--accent);width:14px;height:14px">' +
'Viewport als BBox' +
'</label>' +
'</div>' +
'<button class="overpass-exec-btn" id="overpass-exec-btn" onclick="OverpassUI._executeQuery()">AUSFUEHREN</button>' +
'<div id="overpass-result" style="margin-top:10px;font-size:11px;color:var(--text-dim);display:none"></div>' +
'<button id="overpass-clear-btn" onclick="OverpassUI._clearResults()" ' +
'style="display:none;width:100%;padding:6px;margin-top:6px;background:none;border:1px solid var(--border);color:var(--text-dim);' +
'font-family:var(--font-mono);font-size:11px;border-radius:4px;cursor:pointer;letter-spacing:1px">ERGEBNISSE LOESCHEN</button>';
this._editor = document.getElementById('overpass-editor');
},
_loadTemplates: function() {
var self = this;
fetch('/api/overpass/templates')
.then(function(r) { return r.json(); })
.then(function(data) {
self._categories = data.categories || [];
if (self._categories.length > 0) {
self._activeCategory = self._categories[0].id;
self._renderCategoryTabs();
self._renderTemplateList();
}
})
.catch(function(e) { console.warn('Overpass Templates:', e); });
},
_renderCategoryTabs: function() {
var el = document.getElementById('overpass-cat-tabs');
if (!el) return;
var self = this;
var html = '';
this._categories.forEach(function(cat) {
var active = cat.id === self._activeCategory ? ' active' : '';
html += '<button class="overpass-cat-tab' + active + '" data-cat="' + cat.id + '" ' +
'onclick="OverpassUI._selectCategory(\'' + cat.id + '\')">' + cat.label + '</button>';
});
el.innerHTML = html;
},
_selectCategory: function(catId) {
this._activeCategory = catId;
this._renderCategoryTabs();
this._renderTemplateList();
},
_renderTemplateList: function() {
var el = document.getElementById('overpass-template-list');
if (!el) return;
var self = this;
var cat = this._categories.find(function(c) { return c.id === self._activeCategory; });
if (!cat) { el.innerHTML = ''; return; }
var html = '';
cat.templates.forEach(function(t) {
html += '<button class="overpass-template-btn" onclick="OverpassUI._applyTemplate(\'' + t.id + '\')" title="' + t.description + '">' +
'<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + t.color + ';margin-right:8px"></span>' +
t.name + '</button>';
});
el.innerHTML = html;
},
_applyTemplate: function(templateId) {
var tpl = null;
for (var i = 0; i < this._categories.length; i++) {
var found = this._categories[i].templates.find(function(t) { return t.id === templateId; });
if (found) { tpl = found; break; }
}
if (tpl && this._editor) {
this._editor.value = tpl.query;
OverpassLayer.setColor(tpl.color);
}
},
_getBboxFromViewport: function() {
if (!Globe.viewer) return null;
var rect = Globe.viewer.camera.computeViewRectangle();
if (!rect) return null;
return [
Cesium.Math.toDegrees(rect.south),
Cesium.Math.toDegrees(rect.west),
Cesium.Math.toDegrees(rect.north),
Cesium.Math.toDegrees(rect.east),
];
},
_executeQuery: function() {
if (this._isLoading || !this._editor) return;
var query = this._editor.value.trim();
if (!query) return;
var bbox = null;
if (this._useBbox) {
bbox = this._getBboxFromViewport();
}
this._isLoading = true;
var btn = document.getElementById('overpass-exec-btn');
if (btn) { btn.disabled = true; btn.textContent = 'LADE...'; }
var resultEl = document.getElementById('overpass-result');
if (resultEl) { resultEl.style.display = 'block'; resultEl.textContent = 'Anfrage wird gesendet...'; resultEl.style.color = 'var(--text-dim)'; }
var startTime = Date.now();
var self = this;
fetch('/api/overpass/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: query, bbox: bbox }),
})
.then(function(r) {
if (r.status === 429) {
return r.json().then(function(d) { throw new Error(d.detail || 'Rate-Limit erreicht'); });
}
if (!r.ok) {
return r.json().then(function(d) { throw new Error(d.detail || 'Fehler ' + r.status); });
}
return r.json();
})
.then(function(data) {
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
OverpassLayer.render(data);
if (resultEl) {
var text = data.total + ' Objekte (' + elapsed + 's)';
if (data.cached) text += ' [Cache]';
if (data.truncated) text += ' [max. ' + data.total + ' angezeigt]';
resultEl.textContent = text;
resultEl.style.color = 'var(--accent)';
}
var clearBtn = document.getElementById('overpass-clear-btn');
if (clearBtn) clearBtn.style.display = 'block';
})
.catch(function(e) {
if (resultEl) {
resultEl.textContent = 'Fehler: ' + e.message;
resultEl.style.color = '#ff5252';
}
})
.finally(function() {
self._isLoading = false;
if (btn) { btn.disabled = false; btn.textContent = 'AUSFUEHREN'; }
});
},
_clearResults: function() {
OverpassLayer.clear();
var resultEl = document.getElementById('overpass-result');
if (resultEl) { resultEl.style.display = 'none'; resultEl.style.color = 'var(--text-dim)'; }
var clearBtn = document.getElementById('overpass-clear-btn');
if (clearBtn) clearBtn.style.display = 'none';
},
executeQueryDirect: function(query, bbox, color) {
if (this._editor) this._editor.value = query;
if (color) OverpassLayer.setColor(color);
this._useBbox = !!bbox;
var bboxCb = document.getElementById('overpass-bbox');
if (bboxCb) bboxCb.checked = this._useBbox;
this.show();
this._executeQuery();
},
};

328
static/js/ui/vlm.js Normale Datei
Datei anzeigen

@@ -0,0 +1,328 @@
/**
* VLM UI: Bildanalyse-Panel mit Upload, Zwei-Stufen-Workflow und Overpass-Kopplung.
*/
const VlmUI = {
_panel: null,
_dropZone: null,
_currentAnalysis: null,
_isAnalyzing: false,
init: function() {
this._createPanel();
},
show: function() {
if (!this._panel) this.init();
this._panel.style.display = 'block';
},
hide: function() {
if (this._panel) this._panel.style.display = 'none';
},
_createPanel: function() {
var panel = document.getElementById('vlm-panel');
if (!panel) return;
this._panel = panel;
panel.innerHTML =
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">' +
'<div style="font-size:11px;font-weight:700;letter-spacing:2px;color:var(--accent)">BILDANALYSE (VLM)</div>' +
'<button onclick="VlmUI.hide();var cb=document.getElementById(\'layer-vlm\');if(cb){cb.checked=false;}" ' +
'style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:18px;line-height:1" title="Schliessen">&times;</button>' +
'</div>' +
// Drop-Zone
'<div id="vlm-drop-zone" class="vlm-drop-zone">' +
'<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="1.5" style="margin-bottom:8px">' +
'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>' +
'</svg>' +
'<div style="font-size:12px">Bild hierher ziehen</div>' +
'<div style="font-size:10px;color:var(--text-dim);margin-top:4px">oder klicken zum Waehlen (PNG/JPG/WEBP, max 10MB)</div>' +
'</div>' +
'<input type="file" id="vlm-file-input" accept="image/png,image/jpeg,image/webp" style="display:none" onchange="VlmUI._onFileSelect(this)">' +
// Preview
'<div id="vlm-preview" class="vlm-preview" style="display:none"></div>' +
// Loading
'<div id="vlm-loading" class="vlm-loading" style="display:none">' +
'<div class="vlm-spinner"></div>' +
'<div id="vlm-loading-text">Analysiere Bild...</div>' +
'</div>' +
// Analyse-Ergebnis
'<div id="vlm-results" style="display:none">' +
'<div class="panel-divider"></div>' +
'<div style="font-size:9px;letter-spacing:1.5px;color:var(--accent);margin-bottom:6px">SZENE</div>' +
'<div id="vlm-scene" style="font-size:12px;color:var(--text);margin-bottom:10px;line-height:1.5"></div>' +
'<div style="font-size:9px;letter-spacing:1.5px;color:var(--accent);margin-bottom:6px">ERKANNTE OBJEKTE</div>' +
'<div id="vlm-objects"></div>' +
'<button class="overpass-exec-btn" id="vlm-search-btn" onclick="VlmUI._confirmAndSearch()" style="margin-top:10px">AUF GLOBE SUCHEN</button>' +
'<div id="vlm-search-result" style="margin-top:8px;font-size:11px;color:var(--text-dim);display:none"></div>' +
'</div>' +
// Reset
'<button id="vlm-reset-btn" onclick="VlmUI._reset()" ' +
'style="display:none;width:100%;padding:6px;margin-top:8px;background:none;border:1px solid var(--border);color:var(--text-dim);' +
'font-family:var(--font-mono);font-size:11px;border-radius:4px;cursor:pointer;letter-spacing:1px">NEUES BILD</button>';
this._dropZone = document.getElementById('vlm-drop-zone');
this._setupDragDrop();
},
_setupDragDrop: function() {
var zone = this._dropZone;
if (!zone) return;
var self = this;
['dragenter', 'dragover'].forEach(function(evt) {
zone.addEventListener(evt, function(e) {
e.preventDefault();
e.stopPropagation();
zone.classList.add('vlm-drop-active');
});
});
['dragleave', 'drop'].forEach(function(evt) {
zone.addEventListener(evt, function(e) {
e.preventDefault();
e.stopPropagation();
zone.classList.remove('vlm-drop-active');
});
});
zone.addEventListener('drop', function(e) {
var file = e.dataTransfer.files[0];
if (file) self._handleFile(file);
});
zone.addEventListener('click', function() {
document.getElementById('vlm-file-input').click();
});
},
_onFileSelect: function(input) {
if (input.files && input.files[0]) {
this._handleFile(input.files[0]);
}
},
_handleFile: function(file) {
// Validierung
var allowed = ['image/png', 'image/jpeg', 'image/webp'];
if (allowed.indexOf(file.type) === -1) {
alert('Ungültiger Dateityp. Erlaubt: PNG, JPG, WEBP');
return;
}
if (file.size > 10 * 1024 * 1024) {
alert('Bild zu gross (max 10MB)');
return;
}
// Preview
this._showPreview(file);
// Upload + Analyse
this._uploadAndAnalyze(file);
},
_showPreview: function(file) {
var previewEl = document.getElementById('vlm-preview');
if (!previewEl) return;
var reader = new FileReader();
reader.onload = function(e) {
previewEl.innerHTML = '<img src="' + e.target.result + '" alt="Vorschau">' +
'<div style="font-size:10px;color:var(--text-dim);margin-top:4px">' + file.name +
' (' + (file.size / 1024).toFixed(0) + ' KB)</div>';
previewEl.style.display = 'block';
};
reader.readAsDataURL(file);
},
_getViewportInfo: function() {
if (!Globe.viewer) return '';
var cam = Globe.viewer.camera;
var carto = cam.positionCartographic;
if (!carto) return '';
var lat = Cesium.Math.toDegrees(carto.latitude).toFixed(2);
var lon = Cesium.Math.toDegrees(carto.longitude).toFixed(2);
var alt = (carto.height / 1000).toFixed(0);
return 'Lat ' + lat + ', Lon ' + lon + ', Hoehe ' + alt + ' km';
},
_uploadAndAnalyze: function(file) {
this._isAnalyzing = true;
this._dropZone.style.display = 'none';
var loadingEl = document.getElementById('vlm-loading');
if (loadingEl) loadingEl.style.display = 'block';
var formData = new FormData();
formData.append('file', file);
formData.append('viewport_info', this._getViewportInfo());
var self = this;
fetch('/api/vlm/analyze', {
method: 'POST',
body: formData,
})
.then(function(r) {
if (r.status === 429) {
throw new Error('Eine Analyse laeuft bereits. Bitte warten.');
}
if (!r.ok) {
return r.json().then(function(d) { throw new Error(d.detail || 'Fehler ' + r.status); });
}
return r.json();
})
.then(function(data) {
self._currentAnalysis = data;
self._showResults(data);
})
.catch(function(e) {
alert('VLM-Fehler: ' + e.message);
self._reset();
})
.finally(function() {
self._isAnalyzing = false;
if (loadingEl) loadingEl.style.display = 'none';
});
},
_showResults: function(data) {
var resultsEl = document.getElementById('vlm-results');
var resetBtn = document.getElementById('vlm-reset-btn');
if (!resultsEl) return;
// 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;
}
// Objekte
var objectsEl = document.getElementById('vlm-objects');
if (objectsEl) {
var html = '';
var objects = data.objects || [];
objects.forEach(function(obj, idx) {
var confClass = 'vlm-confidence-' + obj.confidence;
var confLabel = { high: 'HIGH', medium: 'MED', low: 'LOW' }[obj.confidence] || '?';
var checked = obj.confidence !== 'low' ? ' checked' : '';
var milBadge = obj.military_relevant ? '<span class="vlm-badge-mil">MIL</span>' : '';
html += '<div class="vlm-object-card">' +
'<input type="checkbox" id="vlm-obj-' + idx + '"' + checked + ' style="accent-color:var(--accent);width:16px;height:16px;flex-shrink:0">' +
'<div style="flex:1;min-width:0">' +
'<div style="font-weight:700;color:var(--text)">' + obj.type.replace(/_/g, ' ') + '</div>' +
'<div style="font-size:10px;color:var(--text-dim);margin-top:2px">' + obj.description + '</div>' +
'</div>' +
'<span class="' + confClass + '" style="font-size:10px;font-weight:700;flex-shrink:0">' + confLabel + '</span>' +
milBadge +
'</div>';
});
if (objects.length === 0) {
html = '<div style="color:var(--text-dim);font-size:12px">Keine Objekte erkannt</div>';
}
objectsEl.innerHTML = html;
}
resultsEl.style.display = 'block';
if (resetBtn) resetBtn.style.display = 'block';
},
_confirmAndSearch: function() {
if (!this._currentAnalysis) return;
// Ausgewaehlte Objekte sammeln
var objects = this._currentAnalysis.objects || [];
var selected = [];
objects.forEach(function(obj, idx) {
var cb = document.getElementById('vlm-obj-' + idx);
if (cb && cb.checked) {
selected.push(obj);
}
});
if (selected.length === 0) {
alert('Bitte mindestens ein Objekt auswaehlen');
return;
}
// BBox vom Viewport
var bbox = null;
if (Globe.viewer) {
var rect = Globe.viewer.camera.computeViewRectangle();
if (rect) {
bbox = [
Cesium.Math.toDegrees(rect.south),
Cesium.Math.toDegrees(rect.west),
Cesium.Math.toDegrees(rect.north),
Cesium.Math.toDegrees(rect.east),
];
}
}
var btn = document.getElementById('vlm-search-btn');
if (btn) { btn.disabled = true; btn.textContent = 'SUCHE LAEUFT...'; }
var searchResult = document.getElementById('vlm-search-result');
if (searchResult) { searchResult.style.display = 'block'; searchResult.textContent = 'Generiere Overpass-Queries...'; searchResult.style.color = 'var(--text-dim)'; }
// Queries generieren
fetch('/api/vlm/generate-queries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ objects: selected, bbox: bbox }),
})
.then(function(r) {
if (!r.ok) return r.json().then(function(d) { throw new Error(d.detail || 'Fehler'); });
return r.json();
})
.then(function(data) {
if (searchResult) searchResult.textContent = 'Query generiert (' + data.tag_count + ' Tags). Suche OSM-Daten...';
// Overpass-Layer aktivieren und Query ausfuehren
var overpassCb = document.getElementById('layer-overpass');
if (overpassCb && !overpassCb.checked) {
overpassCb.checked = true;
overpassCb.dispatchEvent(new Event('change'));
}
// Query an Overpass senden
OverpassLayer.setColor('#e040fb');
if (typeof OverpassUI !== 'undefined') {
OverpassUI.executeQueryDirect(data.query, bbox, '#e040fb');
}
if (searchResult) {
searchResult.textContent = 'Overpass-Suche gestartet';
searchResult.style.color = 'var(--accent)';
}
})
.catch(function(e) {
if (searchResult) {
searchResult.textContent = 'Fehler: ' + e.message;
searchResult.style.color = '#ff5252';
}
})
.finally(function() {
if (btn) { btn.disabled = false; btn.textContent = 'AUF GLOBE SUCHEN'; }
});
},
_reset: function() {
this._currentAnalysis = null;
this._isAnalyzing = false;
if (this._dropZone) this._dropZone.style.display = 'block';
var els = ['vlm-preview', 'vlm-loading', 'vlm-results', 'vlm-search-result'];
els.forEach(function(id) {
var el = document.getElementById(id);
if (el) el.style.display = 'none';
});
var resetBtn = document.getElementById('vlm-reset-btn');
if (resetBtn) resetBtn.style.display = 'none';
var fileInput = document.getElementById('vlm-file-input');
if (fileInput) fileInput.value = '';
var countEl = document.getElementById('count-vlm');
if (countEl) countEl.textContent = '-';
},
};