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:
211
static/js/layers/overpass.js
Normale Datei
211
static/js/layers/overpass.js
Normale Datei
@@ -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; },
|
||||
};
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren