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:
@@ -439,7 +439,8 @@ async def generate_queries(req: QueryGenRequest):
|
|||||||
}
|
}
|
||||||
if region_used:
|
if region_used:
|
||||||
result["region"] = 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")
|
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
|
return result
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ const VlmUI = {
|
|||||||
_dropZone: null,
|
_dropZone: null,
|
||||||
_currentAnalysis: null,
|
_currentAnalysis: null,
|
||||||
_isAnalyzing: false,
|
_isAnalyzing: false,
|
||||||
|
_searchAreaEntity: null,
|
||||||
|
_exifMarkerEntity: null,
|
||||||
|
|
||||||
init: function() {
|
init: function() {
|
||||||
this._createPanel();
|
this._createPanel();
|
||||||
@@ -300,27 +302,47 @@ const VlmUI = {
|
|||||||
var exif = analysis.exif;
|
var exif = analysis.exif;
|
||||||
var hasExifGps = exif && exif.has_gps && exif.latitude && exif.longitude;
|
var hasExifGps = exif && exif.has_gps && exif.latitude && exif.longitude;
|
||||||
if (hasExifGps) {
|
if (hasExifGps) {
|
||||||
// EXIF-Position als Marker auf dem Globus
|
// EXIF-Position als hervorgehobener Marker auf dem Globus
|
||||||
OverpassLayer.start(Globe.viewer);
|
OverpassLayer.start(Globe.viewer);
|
||||||
OverpassLayer.setColor('#e040fb');
|
OverpassLayer.setColor('#e040fb');
|
||||||
var exifNode = {
|
this._exifMarkerEntity = Globe.viewer.entities.add({
|
||||||
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',
|
name: 'EXIF GPS-Position',
|
||||||
}],
|
position: Cesium.Cartesian3.fromDegrees(exif.longitude, exif.latitude, 0),
|
||||||
ways: [],
|
point: {
|
||||||
relations: [],
|
pixelSize: 12,
|
||||||
total: 1,
|
color: Cesium.Color.fromCssColorString('#e040fb'),
|
||||||
};
|
outlineColor: Cesium.Color.WHITE,
|
||||||
OverpassLayer.render(exifNode);
|
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
|
// Zur EXIF-Position fliegen
|
||||||
Globe.viewer.camera.flyTo({
|
Globe.viewer.camera.flyTo({
|
||||||
destination: Cesium.Cartesian3.fromDegrees(exif.longitude, exif.latitude, 50000),
|
destination: Cesium.Cartesian3.fromDegrees(exif.longitude, exif.latitude, 50000),
|
||||||
@@ -363,6 +385,7 @@ const VlmUI = {
|
|||||||
|
|
||||||
// === Overpass-Suche mit allen verfuegbaren Daten ===
|
// === Overpass-Suche mit allen verfuegbaren Daten ===
|
||||||
var startTime = Date.now();
|
var startTime = Date.now();
|
||||||
|
var self = this;
|
||||||
fetch('/api/vlm/generate-queries', {
|
fetch('/api/vlm/generate-queries', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -380,6 +403,12 @@ const VlmUI = {
|
|||||||
var src = hasExifGps ? 'EXIF BBox' : (data.region ? 'Region: ' + data.region : 'weltweit');
|
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...';
|
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');
|
OverpassLayer.setColor('#e040fb');
|
||||||
return fetch('/api/overpass/query', {
|
return fetch('/api/overpass/query', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -401,24 +430,14 @@ const VlmUI = {
|
|||||||
})
|
})
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||||
// Bei EXIF-GPS: Overpass-Ergebnisse zu bestehendem Marker hinzufuegen
|
// Overpass-Ergebnisse rendern (EXIF-Marker ist separate Entity)
|
||||||
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);
|
OverpassLayer.render(data);
|
||||||
}
|
|
||||||
|
|
||||||
if (searchResult) {
|
if (searchResult) {
|
||||||
var parts = [];
|
var parts = [];
|
||||||
if (hasExifGps) parts.push('EXIF GPS-Marker');
|
if (hasExifGps) parts.push('EXIF-Marker');
|
||||||
parts.push(data.total + ' OSM-Orte');
|
if (self._searchAreaEntity) parts.push('Suchbereich');
|
||||||
|
parts.push(data.total + ' OSM-Treffer');
|
||||||
var text = parts.join(' + ') + ' (' + elapsed + 's)';
|
var text = parts.join(' + ') + ' (' + elapsed + 's)';
|
||||||
if (data.cached) text += ' [Cache]';
|
if (data.cached) text += ' [Cache]';
|
||||||
if (data.truncated) text += ' [Limit]';
|
if (data.truncated) text += ' [Limit]';
|
||||||
@@ -426,7 +445,7 @@ const VlmUI = {
|
|||||||
searchResult.style.color = 'var(--accent)';
|
searchResult.style.color = 'var(--accent)';
|
||||||
}
|
}
|
||||||
var countEl = document.getElementById('count-vlm');
|
var countEl = document.getElementById('count-vlm');
|
||||||
if (countEl) countEl.textContent = (hasExifGps ? 1 : 0) + data.total;
|
if (countEl) countEl.textContent = data.total;
|
||||||
})
|
})
|
||||||
.catch(function(e) {
|
.catch(function(e) {
|
||||||
if (searchResult) {
|
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() {
|
_reset: function() {
|
||||||
this._currentAnalysis = null;
|
this._currentAnalysis = null;
|
||||||
this._isAnalyzing = false;
|
this._isAnalyzing = false;
|
||||||
|
this._clearSearchArea();
|
||||||
|
|
||||||
if (this._dropZone) this._dropZone.style.display = 'block';
|
if (this._dropZone) this._dropZone.style.display = 'block';
|
||||||
|
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren