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:
|
||||
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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren