feat: Suchbereich visuell auf dem Globus umreissen

- Halbtransparentes Rechteck zeigt die Suchregion
- Eckpunkt-Marker + SUCHBEREICH-Label in der Mitte
- EXIF-GPS: Hervorgehobener Marker mit Punkt + Ring + Detailbox
- Kleine BBox (EXIF): staerkere Fuellung, nah sichtbar
- Grosse BBox (Region): schwache Fuellung, weit sichtbar + Kamera fliegt hin
- Backend: generate-queries gibt effective_bbox zurueck
- Reset raeumt alle Visualisierungen auf
Dieser Commit ist enthalten in:
Claude Dev
2026-03-26 09:23:56 +01:00
Ursprung 446b8b9228
Commit 41a987f848
2 geänderte Dateien mit 147 neuen und 37 gelöschten Zeilen

Datei anzeigen

@@ -439,7 +439,8 @@ async def generate_queries(req: QueryGenRequest):
}
if region_used:
result["region"] = region_used
logger.info(f"VLM Query generiert: {query}")
if region_used:
logger.info(f"VLM Query: Region '{region_used}' als BBox verwendet")
# Effektive BBox zurueckgeben fuer Visualisierung
if bbox and len(bbox) == 4:
result["effective_bbox"] = {"south": bbox[0], "west": bbox[1], "north": bbox[2], "east": bbox[3]}
return result

Datei anzeigen

@@ -6,6 +6,8 @@ const VlmUI = {
_dropZone: null,
_currentAnalysis: null,
_isAnalyzing: false,
_searchAreaEntity: null,
_exifMarkerEntity: null,
init: function() {
this._createPanel();
@@ -300,27 +302,47 @@ const VlmUI = {
var exif = analysis.exif;
var hasExifGps = exif && exif.has_gps && exif.latitude && exif.longitude;
if (hasExifGps) {
// EXIF-Position als Marker auf dem Globus
// EXIF-Position als hervorgehobener Marker auf dem Globus
OverpassLayer.start(Globe.viewer);
OverpassLayer.setColor('#e040fb');
var exifNode = {
nodes: [{
id: 'exif-gps',
lat: exif.latitude,
lon: exif.longitude,
tags: {
'source': 'EXIF GPS',
'camera': (exif.camera_make || '') + ' ' + (exif.camera_model || ''),
'timestamp': exif.timestamp || '',
'altitude': exif.altitude ? exif.altitude + 'm' : '',
},
name: 'EXIF GPS-Position',
}],
ways: [],
relations: [],
total: 1,
};
OverpassLayer.render(exifNode);
this._exifMarkerEntity = Globe.viewer.entities.add({
name: 'EXIF GPS-Position',
position: Cesium.Cartesian3.fromDegrees(exif.longitude, exif.latitude, 0),
point: {
pixelSize: 12,
color: Cesium.Color.fromCssColorString('#e040fb'),
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
ellipse: {
semiMinorAxis: 500,
semiMajorAxis: 500,
material: Cesium.Color.fromCssColorString('#e040fb').withAlpha(0.15),
outline: true,
outlineColor: Cesium.Color.fromCssColorString('#e040fb').withAlpha(0.5),
},
label: {
text: 'EXIF GPS',
font: '12px monospace',
fillColor: Cesium.Color.fromCssColorString('#e040fb'),
outlineColor: Cesium.Color.BLACK,
outlineWidth: 3,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
pixelOffset: new Cesium.Cartesian2(0, -20),
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
description: '<div style="font-family:monospace;font-size:12px;padding:8px;color:#e8eaf0">' +
'<div style="color:#e040fb;font-weight:700">EXIF GPS-Position</div>' +
'<table style="margin-top:6px">' +
'<tr><td style="color:#00ff88;padding:2px 8px 2px 0">Latitude</td><td>' + exif.latitude + '</td></tr>' +
'<tr><td style="color:#00ff88;padding:2px 8px 2px 0">Longitude</td><td>' + exif.longitude + '</td></tr>' +
(exif.altitude ? '<tr><td style="color:#00ff88;padding:2px 8px 2px 0">Hoehe</td><td>' + exif.altitude + 'm</td></tr>' : '') +
(exif.camera_model ? '<tr><td style="color:#00ff88;padding:2px 8px 2px 0">Kamera</td><td>' + (exif.camera_make || '') + ' ' + exif.camera_model + '</td></tr>' : '') +
(exif.timestamp ? '<tr><td style="color:#00ff88;padding:2px 8px 2px 0">Zeitstempel</td><td>' + exif.timestamp + '</td></tr>' : '') +
(exif.compass_heading ? '<tr><td style="color:#00ff88;padding:2px 8px 2px 0">Heading</td><td>' + exif.compass_heading + '\u00B0</td></tr>' : '') +
'</table></div>',
});
// Zur EXIF-Position fliegen
Globe.viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(exif.longitude, exif.latitude, 50000),
@@ -363,6 +385,7 @@ const VlmUI = {
// === Overpass-Suche mit allen verfuegbaren Daten ===
var startTime = Date.now();
var self = this;
fetch('/api/vlm/generate-queries', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -380,6 +403,12 @@ const VlmUI = {
var src = hasExifGps ? 'EXIF BBox' : (data.region ? 'Region: ' + data.region : 'weltweit');
if (searchResult) searchResult.textContent = statusParts.join(' | ') + (statusParts.length ? ' | ' : '') + 'Query generiert (' + data.tag_count + ' Tags, ' + src + '). Suche OSM-Daten...';
// Suchbereich auf dem Globus zeichnen
var drawBbox = data.effective_bbox || (bbox ? {south: bbox[0], west: bbox[1], north: bbox[2], east: bbox[3]} : null);
if (drawBbox) {
self._drawSearchArea(drawBbox, hasExifGps ? 'EXIF' : (data.region || 'Analyse'));
}
OverpassLayer.setColor('#e040fb');
return fetch('/api/overpass/query', {
method: 'POST',
@@ -401,24 +430,14 @@ const VlmUI = {
})
.then(function(data) {
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
// Bei EXIF-GPS: Overpass-Ergebnisse zu bestehendem Marker hinzufuegen
if (hasExifGps && data.total > 0) {
// Bestehende EXIF-Daten beibehalten, Overpass hinzufuegen
var combined = {
nodes: [{ id: 'exif-gps', lat: exif.latitude, lon: exif.longitude, tags: { 'source': 'EXIF GPS' }, name: 'EXIF GPS-Position' }].concat(data.nodes || []),
ways: data.ways || [],
relations: data.relations || [],
total: 1 + data.total,
};
OverpassLayer.render(combined);
} else if (!hasExifGps) {
OverpassLayer.render(data);
}
// Overpass-Ergebnisse rendern (EXIF-Marker ist separate Entity)
OverpassLayer.render(data);
if (searchResult) {
var parts = [];
if (hasExifGps) parts.push('EXIF GPS-Marker');
parts.push(data.total + ' OSM-Orte');
if (hasExifGps) parts.push('EXIF-Marker');
if (self._searchAreaEntity) parts.push('Suchbereich');
parts.push(data.total + ' OSM-Treffer');
var text = parts.join(' + ') + ' (' + elapsed + 's)';
if (data.cached) text += ' [Cache]';
if (data.truncated) text += ' [Limit]';
@@ -426,7 +445,7 @@ const VlmUI = {
searchResult.style.color = 'var(--accent)';
}
var countEl = document.getElementById('count-vlm');
if (countEl) countEl.textContent = (hasExifGps ? 1 : 0) + data.total;
if (countEl) countEl.textContent = data.total;
})
.catch(function(e) {
if (searchResult) {
@@ -440,9 +459,99 @@ const VlmUI = {
});
},
// === Suchbereich auf dem Globus visualisieren ===
_drawSearchArea: function(bbox, label) {
this._clearSearchArea();
if (!Globe.viewer || !bbox) return;
var isSmall = (Math.abs(bbox.north - bbox.south) < 2 && Math.abs(bbox.east - bbox.west) < 2);
var fillAlpha = isSmall ? 0.12 : 0.06;
var outlineAlpha = isSmall ? 0.7 : 0.4;
this._searchAreaEntity = Globe.viewer.entities.add({
name: 'Suchbereich: ' + label,
rectangle: {
coordinates: Cesium.Rectangle.fromDegrees(bbox.west, bbox.south, bbox.east, bbox.north),
material: Cesium.Color.fromCssColorString('#e040fb').withAlpha(fillAlpha),
outline: true,
outlineColor: Cesium.Color.fromCssColorString('#e040fb').withAlpha(outlineAlpha),
outlineWidth: 2,
height: 0,
},
});
// Label in der Mitte des Bereichs
var centerLat = (bbox.south + bbox.north) / 2;
var centerLon = (bbox.west + bbox.east) / 2;
this._searchAreaLabel = Globe.viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 0),
label: {
text: 'SUCHBEREICH',
font: '11px monospace',
fillColor: Cesium.Color.fromCssColorString('#e040fb').withAlpha(0.7),
outlineColor: Cesium.Color.BLACK,
outlineWidth: 3,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
verticalOrigin: Cesium.VerticalOrigin.CENTER,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, isSmall ? 500000 : 8000000),
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
});
// Eckpunkte-Markierungen
this._searchAreaCorners = [];
var corners = [
[bbox.west, bbox.south], [bbox.east, bbox.south],
[bbox.east, bbox.north], [bbox.west, bbox.north],
];
for (var i = 0; i < corners.length; i++) {
this._searchAreaCorners.push(Globe.viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(corners[i][0], corners[i][1], 0),
point: {
pixelSize: 4,
color: Cesium.Color.fromCssColorString('#e040fb').withAlpha(0.6),
outlineColor: Cesium.Color.BLACK,
outlineWidth: 1,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
},
}));
}
// Kamera auf den Bereich ausrichten wenn kein EXIF-GPS (EXIF fliegt bereits hin)
if (!isSmall) {
Globe.viewer.camera.flyTo({
destination: Cesium.Rectangle.fromDegrees(bbox.west - 2, bbox.south - 2, bbox.east + 2, bbox.north + 2),
duration: 2,
});
}
},
_clearSearchArea: function() {
if (this._searchAreaEntity && Globe.viewer) {
Globe.viewer.entities.remove(this._searchAreaEntity);
this._searchAreaEntity = null;
}
if (this._searchAreaLabel && Globe.viewer) {
Globe.viewer.entities.remove(this._searchAreaLabel);
this._searchAreaLabel = null;
}
if (this._searchAreaCorners) {
for (var i = 0; i < this._searchAreaCorners.length; i++) {
if (Globe.viewer) Globe.viewer.entities.remove(this._searchAreaCorners[i]);
}
this._searchAreaCorners = null;
}
if (this._exifMarkerEntity && Globe.viewer) {
Globe.viewer.entities.remove(this._exifMarkerEntity);
this._exifMarkerEntity = null;
}
},
_reset: function() {
this._currentAnalysis = null;
this._isAnalyzing = false;
this._clearSearchArea();
if (this._dropZone) this._dropZone.style.display = 'block';