From 41a987f848f1ddbb3cc90294a6058e68c414587b Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Thu, 26 Mar 2026 09:23:56 +0100 Subject: [PATCH] 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 --- src/data_vlm.py | 5 +- static/js/ui/vlm.js | 179 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 147 insertions(+), 37 deletions(-) diff --git a/src/data_vlm.py b/src/data_vlm.py index 7411ed4..286aa02 100644 --- a/src/data_vlm.py +++ b/src/data_vlm.py @@ -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 diff --git a/static/js/ui/vlm.js b/static/js/ui/vlm.js index 6d0e610..d0e9d5e 100644 --- a/static/js/ui/vlm.js +++ b/static/js/ui/vlm.js @@ -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: '
' + + '
EXIF GPS-Position
' + + '' + + '' + + '' + + (exif.altitude ? '' : '') + + (exif.camera_model ? '' : '') + + (exif.timestamp ? '' : '') + + (exif.compass_heading ? '' : '') + + '
Latitude' + exif.latitude + '
Longitude' + exif.longitude + '
Hoehe' + exif.altitude + 'm
Kamera' + (exif.camera_make || '') + ' ' + exif.camera_model + '
Zeitstempel' + exif.timestamp + '
Heading' + exif.compass_heading + '\u00B0
', + }); // 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';