VLM-Schema: - estimated_coordinates: Claude schaetzt Lat/Lon + Radius direkt - identified_features: Konkreter Gewaessername, engste Region, Landmarken - landscape_clues um water_characteristics erweitert VLM-Prompt: Komplett ueberarbeitet - Primaerziel ist jetzt Reverse Geolocation statt nur Objekterkennung - Spezifische Anleitungen: Fluss-ID (Rhein vs Donau), Kiesfarbe, Uferform - Explizite Aufforderung fuer Koordinatenschaetzung BBox-Kaskade (Prioritaet): 1. EXIF-GPS (exakt) 2. estimated_coordinates + Radius (VLM-Schaetzung) 3. identified_features (Gewaesser/Region aus Matching) 4. estimated_location_type (grobe Region) 5. Weltweit (Fallback) Regions-Mapping: 30+ neue Eintraege - Deutsche Bundeslaender (NRW, Bayern, Hessen, etc.) - Flussgebiete (Niederrhein, Oberrhein, Mosel, Elbe, etc.) - Staedte (Duesseldorf, Koeln, Bonn, Neuss) Frontend: - Gruen hervorgehobene Koordinaten-Box mit Radius - Identifizierte Features (Gewaesser, Region, Landmarken) - Fly-To zur geschaetzten Position
667 Zeilen
31 KiB
JavaScript
667 Zeilen
31 KiB
JavaScript
/**
|
|
* VLM UI: Bildanalyse-Panel mit Upload, Zwei-Stufen-Workflow und Overpass-Kopplung.
|
|
*/
|
|
const VlmUI = {
|
|
_panel: null,
|
|
_dropZone: null,
|
|
_currentAnalysis: null,
|
|
_isAnalyzing: false,
|
|
_searchAreaEntity: null,
|
|
_exifMarkerEntity: null,
|
|
|
|
init: function() {
|
|
this._createPanel();
|
|
},
|
|
|
|
show: function() {
|
|
if (!this._panel) this.init();
|
|
this._panel.style.display = 'block';
|
|
},
|
|
|
|
hide: function() {
|
|
if (this._panel) this._panel.style.display = 'none';
|
|
},
|
|
|
|
_createPanel: function() {
|
|
var panel = document.getElementById('vlm-panel');
|
|
if (!panel) return;
|
|
this._panel = panel;
|
|
|
|
panel.innerHTML =
|
|
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">' +
|
|
'<div style="font-size:11px;font-weight:700;letter-spacing:2px;color:var(--accent)">BILDANALYSE (VLM)</div>' +
|
|
'<button onclick="VlmUI.hide();var cb=document.getElementById(\'layer-vlm\');if(cb){cb.checked=false;}" ' +
|
|
'style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:18px;line-height:1" title="Schliessen">×</button>' +
|
|
'</div>' +
|
|
// Drop-Zone
|
|
'<div id="vlm-drop-zone" class="vlm-drop-zone">' +
|
|
'<svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="1.5" style="margin-bottom:8px">' +
|
|
'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>' +
|
|
'</svg>' +
|
|
'<div style="font-size:12px">Bild hierher ziehen</div>' +
|
|
'<div style="font-size:10px;color:var(--text-dim);margin-top:4px">oder klicken zum Waehlen (PNG/JPG/WEBP, max 10MB)</div>' +
|
|
'</div>' +
|
|
'<input type="file" id="vlm-file-input" accept="image/png,image/jpeg,image/webp" style="display:none" onchange="VlmUI._onFileSelect(this)">' +
|
|
// Preview
|
|
'<div id="vlm-preview" class="vlm-preview" style="display:none"></div>' +
|
|
// Loading
|
|
'<div id="vlm-loading" class="vlm-loading" style="display:none">' +
|
|
'<div class="vlm-spinner"></div>' +
|
|
'<div id="vlm-loading-text">Analysiere Bild...</div>' +
|
|
'</div>' +
|
|
// Analyse-Ergebnis
|
|
'<div id="vlm-results" style="display:none">' +
|
|
'<div class="panel-divider"></div>' +
|
|
'<div style="font-size:9px;letter-spacing:1.5px;color:var(--accent);margin-bottom:6px">SZENE</div>' +
|
|
'<div id="vlm-scene" style="font-size:12px;color:var(--text);margin-bottom:10px;line-height:1.5"></div>' +
|
|
'<div style="font-size:9px;letter-spacing:1.5px;color:var(--accent);margin-bottom:6px">ERKANNTE OBJEKTE</div>' +
|
|
'<div id="vlm-objects"></div>' +
|
|
'<label style="display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text-dim);margin-top:8px;cursor:pointer">' +
|
|
'<input type="checkbox" id="vlm-use-bbox" style="accent-color:var(--accent);width:14px;height:14px">' +
|
|
'Nur im sichtbaren Bereich suchen' +
|
|
'</label>' +
|
|
'<button class="overpass-exec-btn" id="vlm-search-btn" onclick="VlmUI._confirmAndSearch()" style="margin-top:8px">AUF GLOBE SUCHEN</button>' +
|
|
'<div id="vlm-search-result" style="margin-top:8px;font-size:11px;color:var(--text-dim);display:none"></div>' +
|
|
'</div>' +
|
|
// Reset
|
|
'<button id="vlm-reset-btn" onclick="VlmUI._reset()" ' +
|
|
'style="display:none;width:100%;padding:6px;margin-top:8px;background:none;border:1px solid var(--border);color:var(--text-dim);' +
|
|
'font-family:var(--font-mono);font-size:11px;border-radius:4px;cursor:pointer;letter-spacing:1px">NEUES BILD</button>';
|
|
|
|
this._dropZone = document.getElementById('vlm-drop-zone');
|
|
this._setupDragDrop();
|
|
},
|
|
|
|
_setupDragDrop: function() {
|
|
var zone = this._dropZone;
|
|
if (!zone) return;
|
|
var self = this;
|
|
|
|
['dragenter', 'dragover'].forEach(function(evt) {
|
|
zone.addEventListener(evt, function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
zone.classList.add('vlm-drop-active');
|
|
});
|
|
});
|
|
['dragleave', 'drop'].forEach(function(evt) {
|
|
zone.addEventListener(evt, function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
zone.classList.remove('vlm-drop-active');
|
|
});
|
|
});
|
|
zone.addEventListener('drop', function(e) {
|
|
var file = e.dataTransfer.files[0];
|
|
if (file) self._handleFile(file);
|
|
});
|
|
zone.addEventListener('click', function() {
|
|
document.getElementById('vlm-file-input').click();
|
|
});
|
|
},
|
|
|
|
_onFileSelect: function(input) {
|
|
if (input.files && input.files[0]) {
|
|
this._handleFile(input.files[0]);
|
|
}
|
|
},
|
|
|
|
_handleFile: function(file) {
|
|
// Validierung
|
|
var allowed = ['image/png', 'image/jpeg', 'image/webp'];
|
|
if (allowed.indexOf(file.type) === -1) {
|
|
alert('Ungültiger Dateityp. Erlaubt: PNG, JPG, WEBP');
|
|
return;
|
|
}
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
alert('Bild zu gross (max 10MB)');
|
|
return;
|
|
}
|
|
|
|
// Preview
|
|
this._showPreview(file);
|
|
// Upload + Analyse
|
|
this._uploadAndAnalyze(file);
|
|
},
|
|
|
|
_showPreview: function(file) {
|
|
var previewEl = document.getElementById('vlm-preview');
|
|
if (!previewEl) return;
|
|
var reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
previewEl.innerHTML = '<img src="' + e.target.result + '" alt="Vorschau">' +
|
|
'<div style="font-size:10px;color:var(--text-dim);margin-top:4px">' + file.name +
|
|
' (' + (file.size / 1024).toFixed(0) + ' KB)</div>';
|
|
previewEl.style.display = 'block';
|
|
};
|
|
reader.readAsDataURL(file);
|
|
},
|
|
|
|
_getViewportInfo: function() {
|
|
if (!Globe.viewer) return '';
|
|
var cam = Globe.viewer.camera;
|
|
var carto = cam.positionCartographic;
|
|
if (!carto) return '';
|
|
var lat = Cesium.Math.toDegrees(carto.latitude).toFixed(2);
|
|
var lon = Cesium.Math.toDegrees(carto.longitude).toFixed(2);
|
|
var alt = (carto.height / 1000).toFixed(0);
|
|
return 'Lat ' + lat + ', Lon ' + lon + ', Hoehe ' + alt + ' km';
|
|
},
|
|
|
|
_uploadAndAnalyze: function(file) {
|
|
this._isAnalyzing = true;
|
|
this._dropZone.style.display = 'none';
|
|
var loadingEl = document.getElementById('vlm-loading');
|
|
if (loadingEl) loadingEl.style.display = 'block';
|
|
|
|
var formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('viewport_info', this._getViewportInfo());
|
|
|
|
var self = this;
|
|
fetch('/api/vlm/analyze', {
|
|
method: 'POST',
|
|
body: formData,
|
|
})
|
|
.then(function(r) {
|
|
if (r.status === 413) {
|
|
throw new Error('Bild zu gross fuer Upload. Bitte kleineres Bild verwenden.');
|
|
}
|
|
if (r.status === 429) {
|
|
throw new Error('Eine Analyse laeuft bereits. Bitte warten.');
|
|
}
|
|
if (!r.ok) {
|
|
return r.text().then(function(txt) {
|
|
try { var d = JSON.parse(txt); throw new Error(d.detail || 'Fehler ' + r.status); }
|
|
catch(e) { if (e.message && !e.message.startsWith('Unexpected')) throw e; throw new Error('Server-Fehler ' + r.status); }
|
|
});
|
|
}
|
|
return r.json();
|
|
})
|
|
.then(function(data) {
|
|
self._currentAnalysis = data;
|
|
self._showResults(data);
|
|
})
|
|
.catch(function(e) {
|
|
alert('VLM-Fehler: ' + e.message);
|
|
self._reset();
|
|
})
|
|
.finally(function() {
|
|
self._isAnalyzing = false;
|
|
if (loadingEl) loadingEl.style.display = 'none';
|
|
});
|
|
},
|
|
|
|
_showResults: function(data) {
|
|
var resultsEl = document.getElementById('vlm-results');
|
|
var resetBtn = document.getElementById('vlm-reset-btn');
|
|
if (!resultsEl) return;
|
|
|
|
// EXIF-Metadaten
|
|
var exifHtml = '';
|
|
if (data.exif) {
|
|
var e = data.exif;
|
|
var ep = [];
|
|
if (e.has_gps) ep.push('<span style="color:var(--accent)">GPS: ' + e.latitude + ', ' + e.longitude + '</span>');
|
|
if (e.altitude) ep.push('Hoehe: ' + e.altitude + 'm');
|
|
if (e.camera_model) ep.push(e.camera_make ? e.camera_make + ' ' + e.camera_model : e.camera_model);
|
|
if (e.focal_length) ep.push(e.focal_length + 'mm');
|
|
if (e.timestamp) ep.push(e.timestamp.replace('T', ' '));
|
|
if (e.compass_heading) ep.push('Heading: ' + e.compass_heading + '\u00B0');
|
|
if (ep.length > 0) {
|
|
exifHtml = '<div class="vlm-exif-card"><div style="font-size:9px;letter-spacing:1.5px;color:#e040fb;margin-bottom:4px">EXIF METADATEN</div>' +
|
|
'<div style="font-size:11px;color:var(--text);line-height:1.6">' + ep.join(' · ') + '</div></div>';
|
|
if (e.has_gps) {
|
|
exifHtml += '<button class="geoint-tool-btn" style="width:100%;margin-top:4px" onclick="Globe.viewer.camera.flyTo({destination:Cesium.Cartesian3.fromDegrees(' + e.longitude + ',' + e.latitude + ',50000),duration:2})">GPS-Position anfliegen</button>';
|
|
}
|
|
} else {
|
|
exifHtml = '<div class="vlm-exif-card"><div style="font-size:9px;letter-spacing:1.5px;color:var(--text-dim);margin-bottom:2px">EXIF</div><div style="font-size:11px;color:var(--text-dim)">Keine Metadaten gefunden</div></div>';
|
|
}
|
|
}
|
|
|
|
// Landscape Clues (Reverse Geolocation)
|
|
var cluesHtml = '';
|
|
if (data.landscape_clues) {
|
|
var lc = data.landscape_clues;
|
|
var cl = [];
|
|
if (lc.vegetation) cl.push('<b>Vegetation:</b> ' + lc.vegetation);
|
|
if (lc.soil_color) cl.push('<b>Boden:</b> ' + lc.soil_color);
|
|
if (lc.road_markings) cl.push('<b>Strassen:</b> ' + lc.road_markings);
|
|
if (lc.architecture_style) cl.push('<b>Architektur:</b> ' + lc.architecture_style);
|
|
if (lc.signage_language) cl.push('<b>Schilder:</b> ' + lc.signage_language);
|
|
if (lc.vehicle_types) cl.push('<b>Fahrzeuge:</b> ' + lc.vehicle_types);
|
|
if (lc.climate_indicators) cl.push('<b>Klima:</b> ' + lc.climate_indicators);
|
|
if (lc.sun_shadow_direction) cl.push('<b>Schatten:</b> ' + lc.sun_shadow_direction);
|
|
if (cl.length > 0) {
|
|
cluesHtml = '<div style="font-size:9px;letter-spacing:1.5px;color:#ff9800;margin:8px 0 4px">LANDSCHAFTSMERKMALE</div>' +
|
|
'<div style="font-size:11px;color:var(--text);line-height:1.8">' + cl.join('<br>') + '</div>';
|
|
}
|
|
}
|
|
|
|
// Geschaetzte Koordinaten
|
|
var coordsHtml = '';
|
|
if (data.estimated_coordinates && data.estimated_coordinates.latitude) {
|
|
var ec = data.estimated_coordinates;
|
|
var rKm = ec.confidence_radius_km || '?';
|
|
coordsHtml = '<div class="vlm-exif-card" style="border-color:rgba(0,255,136,0.3);background:rgba(0,255,136,0.06);margin-top:8px">' +
|
|
'<div style="font-size:9px;letter-spacing:1.5px;color:var(--accent);margin-bottom:4px">GESCHAETZTE POSITION</div>' +
|
|
'<div style="font-size:13px;color:var(--accent);font-weight:700">' + ec.latitude.toFixed(4) + ', ' + ec.longitude.toFixed(4) +
|
|
' <span style="font-size:11px;font-weight:400;color:var(--text-dim)">(±' + rKm + ' km)</span></div>' +
|
|
(ec.reasoning ? '<div style="font-size:10px;color:var(--text-dim);margin-top:4px">' + ec.reasoning + '</div>' : '') +
|
|
'</div>';
|
|
}
|
|
|
|
// Identifizierte Features
|
|
var featHtml = '';
|
|
if (data.identified_features) {
|
|
var if_ = data.identified_features;
|
|
var fp = [];
|
|
if (if_.water_body) fp.push('<b>Gewaesser:</b> ' + if_.water_body);
|
|
if (if_.specific_region) fp.push('<b>Region:</b> ' + if_.specific_region);
|
|
if (if_.nearby_landmarks) fp.push('<b>Landmarken:</b> ' + if_.nearby_landmarks);
|
|
if (fp.length > 0) {
|
|
featHtml = '<div style="font-size:9px;letter-spacing:1.5px;color:#00bcd4;margin:8px 0 4px">IDENTIFIZIERTE MERKMALE</div>' +
|
|
'<div style="font-size:11px;color:var(--text);line-height:1.8">' + fp.join('<br>') + '</div>';
|
|
}
|
|
}
|
|
|
|
// Szene
|
|
var sceneEl = document.getElementById('vlm-scene');
|
|
if (sceneEl) {
|
|
sceneEl.innerHTML = exifHtml + coordsHtml +
|
|
'<div style="margin-top:8px">' + (data.scene_description || 'Keine Beschreibung') +
|
|
(data.terrain ? ' | Gelaende: ' + data.terrain : '') +
|
|
(data.estimated_location_type ? ' | Region: ' + data.estimated_location_type : '') +
|
|
'</div>' + featHtml + cluesHtml;
|
|
}
|
|
|
|
// Objekte
|
|
var objectsEl = document.getElementById('vlm-objects');
|
|
if (objectsEl) {
|
|
var html = '';
|
|
var objects = data.objects || [];
|
|
objects.forEach(function(obj, idx) {
|
|
var confClass = 'vlm-confidence-' + obj.confidence;
|
|
var confLabel = { high: 'HIGH', medium: 'MED', low: 'LOW' }[obj.confidence] || '?';
|
|
var checked = obj.confidence !== 'low' ? ' checked' : '';
|
|
var milBadge = obj.military_relevant ? '<span class="vlm-badge-mil">MIL</span>' : '';
|
|
|
|
html += '<div class="vlm-object-card">' +
|
|
'<input type="checkbox" id="vlm-obj-' + idx + '"' + checked + ' style="accent-color:var(--accent);width:16px;height:16px;flex-shrink:0">' +
|
|
'<div style="flex:1;min-width:0">' +
|
|
'<div style="font-weight:700;color:var(--text)">' + obj.type.replace(/_/g, ' ') + '</div>' +
|
|
'<div style="font-size:10px;color:var(--text-dim);margin-top:2px">' + obj.description + '</div>' +
|
|
'</div>' +
|
|
'<span class="' + confClass + '" style="font-size:10px;font-weight:700;flex-shrink:0">' + confLabel + '</span>' +
|
|
milBadge +
|
|
'</div>';
|
|
});
|
|
if (objects.length === 0) {
|
|
html = '<div style="color:var(--text-dim);font-size:12px">Keine Objekte erkannt</div>';
|
|
}
|
|
objectsEl.innerHTML = html;
|
|
}
|
|
|
|
resultsEl.style.display = 'block';
|
|
if (resetBtn) resetBtn.style.display = 'block';
|
|
},
|
|
|
|
_confirmAndSearch: function() {
|
|
if (!this._currentAnalysis) return;
|
|
var analysis = this._currentAnalysis;
|
|
|
|
// Ausgewaehlte Objekte sammeln
|
|
var objects = analysis.objects || [];
|
|
var selected = [];
|
|
objects.forEach(function(obj, idx) {
|
|
var cb = document.getElementById('vlm-obj-' + idx);
|
|
if (cb && cb.checked) {
|
|
selected.push(obj);
|
|
}
|
|
});
|
|
|
|
if (selected.length === 0) {
|
|
alert('Bitte mindestens ein Objekt auswaehlen');
|
|
return;
|
|
}
|
|
|
|
// === EXIF-GPS: Marker setzen + dorthin fliegen ===
|
|
var exif = analysis.exif;
|
|
var hasExifGps = exif && exif.has_gps && exif.latitude && exif.longitude;
|
|
if (hasExifGps) {
|
|
// EXIF-Position als hervorgehobener Marker auf dem Globus
|
|
OverpassLayer.start(Globe.viewer);
|
|
OverpassLayer.setColor('#e040fb');
|
|
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),
|
|
duration: 2,
|
|
});
|
|
}
|
|
|
|
// === BBox bestimmen: EXIF-GPS > estimated_coordinates > Viewport > Region (Backend) ===
|
|
var bbox = null;
|
|
var ec = analysis.estimated_coordinates;
|
|
var hasEstCoords = ec && ec.latitude && ec.longitude;
|
|
|
|
if (hasExifGps) {
|
|
bbox = [
|
|
exif.latitude - 0.5, exif.longitude - 0.5,
|
|
exif.latitude + 0.5, exif.longitude + 0.5,
|
|
];
|
|
} else if (hasEstCoords) {
|
|
// VLM-geschaetzte Koordinaten: Radius in Grad
|
|
var rKm = ec.confidence_radius_km || 50;
|
|
var rDeg = Math.max(0.1, Math.min(rKm / 111, 10));
|
|
bbox = [
|
|
ec.latitude - rDeg, ec.longitude - rDeg,
|
|
ec.latitude + rDeg, ec.longitude + rDeg,
|
|
];
|
|
// Zur geschaetzten Position fliegen
|
|
Globe.viewer.camera.flyTo({
|
|
destination: Cesium.Cartesian3.fromDegrees(ec.longitude, ec.latitude, Math.max(50000, rKm * 2000)),
|
|
duration: 2,
|
|
});
|
|
} else {
|
|
var bboxCb = document.getElementById('vlm-use-bbox');
|
|
if (bboxCb && bboxCb.checked && Globe.viewer) {
|
|
var rect = Globe.viewer.camera.computeViewRectangle();
|
|
if (rect) {
|
|
bbox = [
|
|
Cesium.Math.toDegrees(rect.south),
|
|
Cesium.Math.toDegrees(rect.west),
|
|
Cesium.Math.toDegrees(rect.north),
|
|
Cesium.Math.toDegrees(rect.east),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
var btn = document.getElementById('vlm-search-btn');
|
|
if (btn) { btn.disabled = true; btn.textContent = 'SUCHE LAEUFT...'; }
|
|
var searchResult = document.getElementById('vlm-search-result');
|
|
var statusParts = [];
|
|
if (hasExifGps) statusParts.push('EXIF GPS-Marker gesetzt');
|
|
if (searchResult) { searchResult.style.display = 'block'; searchResult.textContent = statusParts.join(' | ') + (statusParts.length ? ' | ' : '') + 'Generiere Overpass-Queries...'; searchResult.style.color = 'var(--text-dim)'; }
|
|
|
|
// === 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' },
|
|
body: JSON.stringify({
|
|
objects: selected,
|
|
bbox: bbox,
|
|
estimated_location_type: analysis.estimated_location_type || null,
|
|
estimated_coordinates: analysis.estimated_coordinates || null,
|
|
identified_features: analysis.identified_features || null,
|
|
}),
|
|
})
|
|
.then(function(r) {
|
|
if (!r.ok) return r.json().then(function(d) { throw new Error(d.detail || 'Fehler'); });
|
|
return r.json();
|
|
})
|
|
.then(function(data) {
|
|
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',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query: data.query, bbox: bbox }),
|
|
});
|
|
})
|
|
.then(function(r) {
|
|
if (r.status === 429) {
|
|
return r.json().then(function(d) { throw new Error(d.detail || 'Rate-Limit'); });
|
|
}
|
|
if (!r.ok) {
|
|
return r.text().then(function(txt) {
|
|
try { var d = JSON.parse(txt); throw new Error(d.detail || 'Overpass-Fehler'); }
|
|
catch(e) { if (e.message && !e.message.startsWith('Unexpected')) throw e; throw new Error('Overpass-Fehler ' + r.status); }
|
|
});
|
|
}
|
|
return r.json();
|
|
})
|
|
.then(function(data) {
|
|
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
// Overpass-Ergebnisse rendern (EXIF-Marker ist separate Entity)
|
|
OverpassLayer.render(data);
|
|
|
|
if (searchResult) {
|
|
var parts = [];
|
|
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]';
|
|
searchResult.textContent = text;
|
|
searchResult.style.color = 'var(--accent)';
|
|
}
|
|
var countEl = document.getElementById('count-vlm');
|
|
if (countEl) countEl.textContent = data.total;
|
|
})
|
|
.catch(function(e) {
|
|
if (searchResult) {
|
|
var prefix = hasExifGps ? 'EXIF GPS-Marker gesetzt | ' : '';
|
|
searchResult.textContent = prefix + 'Overpass-Fehler: ' + e.message;
|
|
searchResult.style.color = hasExifGps ? '#ff9800' : '#ff5252';
|
|
}
|
|
})
|
|
.finally(function() {
|
|
if (btn) { btn.disabled = false; btn.textContent = 'AUF GLOBE SUCHEN'; }
|
|
});
|
|
},
|
|
|
|
// === 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);
|
|
|
|
// Helle, klar sichtbare Fuellung
|
|
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(isSmall ? 0.25 : 0.15),
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString('#e040fb'),
|
|
outlineWidth: 3,
|
|
height: 0,
|
|
},
|
|
});
|
|
|
|
// Zweites Rechteck leicht versetzt als Glow-Effekt
|
|
this._searchAreaGlow = Globe.viewer.entities.add({
|
|
rectangle: {
|
|
coordinates: Cesium.Rectangle.fromDegrees(
|
|
bbox.west - 0.05, bbox.south - 0.05,
|
|
bbox.east + 0.05, bbox.north + 0.05
|
|
),
|
|
material: Cesium.Color.TRANSPARENT,
|
|
outline: true,
|
|
outlineColor: Cesium.Color.fromCssColorString('#e040fb').withAlpha(0.35),
|
|
outlineWidth: 1,
|
|
height: 0,
|
|
},
|
|
});
|
|
|
|
// Diagonale Linien fuer taktisches Schraffur-Gefuehl
|
|
this._searchAreaDiags = [];
|
|
var steps = isSmall ? 6 : 12;
|
|
for (var s = 1; s < steps; s++) {
|
|
var t = s / steps;
|
|
// Horizontale Linien
|
|
var lineLat = bbox.south + t * (bbox.north - bbox.south);
|
|
this._searchAreaDiags.push(Globe.viewer.entities.add({
|
|
polyline: {
|
|
positions: Cesium.Cartesian3.fromDegreesArray([
|
|
bbox.west, lineLat, bbox.east, lineLat
|
|
]),
|
|
width: 1,
|
|
material: Cesium.Color.fromCssColorString('#e040fb').withAlpha(0.08),
|
|
clampToGround: true,
|
|
},
|
|
}));
|
|
}
|
|
|
|
// Eckpunkte - groesser und heller
|
|
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: 8,
|
|
color: Cesium.Color.fromCssColorString('#e040fb'),
|
|
outlineColor: Cesium.Color.WHITE,
|
|
outlineWidth: 2,
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
},
|
|
}));
|
|
}
|
|
|
|
// Kantenlinien explizit als Polylines (deutlich sichtbarer als rectangle outline)
|
|
this._searchAreaEdges = [];
|
|
var edgePositions = [
|
|
[bbox.west, bbox.south, bbox.east, bbox.south],
|
|
[bbox.east, bbox.south, bbox.east, bbox.north],
|
|
[bbox.east, bbox.north, bbox.west, bbox.north],
|
|
[bbox.west, bbox.north, bbox.west, bbox.south],
|
|
];
|
|
for (var e = 0; e < edgePositions.length; e++) {
|
|
this._searchAreaEdges.push(Globe.viewer.entities.add({
|
|
polyline: {
|
|
positions: Cesium.Cartesian3.fromDegreesArray(edgePositions[e]),
|
|
width: 3,
|
|
material: Cesium.Color.fromCssColorString('#e040fb').withAlpha(0.9),
|
|
clampToGround: true,
|
|
},
|
|
}));
|
|
}
|
|
|
|
// Label - groesser und deutlicher
|
|
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: '\u25A0 SUCHBEREICH',
|
|
font: 'bold 13px monospace',
|
|
fillColor: Cesium.Color.fromCssColorString('#e040fb'),
|
|
outlineColor: Cesium.Color.BLACK,
|
|
outlineWidth: 4,
|
|
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
|
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
|
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, isSmall ? 800000 : 12000000),
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
backgroundColor: Cesium.Color.fromCssColorString('#e040fb').withAlpha(0.15),
|
|
showBackground: true,
|
|
backgroundPadding: new Cesium.Cartesian2(8, 4),
|
|
},
|
|
});
|
|
|
|
// Kamera auf den Bereich ausrichten wenn kein EXIF-GPS
|
|
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() {
|
|
var v = Globe.viewer;
|
|
if (!v) return;
|
|
var self = this;
|
|
['_searchAreaEntity', '_searchAreaGlow', '_searchAreaLabel', '_exifMarkerEntity'].forEach(function(k) {
|
|
if (self[k]) { v.entities.remove(self[k]); self[k] = null; }
|
|
});
|
|
['_searchAreaCorners', '_searchAreaEdges', '_searchAreaDiags'].forEach(function(k) {
|
|
if (self[k]) {
|
|
for (var i = 0; i < self[k].length; i++) { v.entities.remove(self[k][i]); }
|
|
self[k] = null;
|
|
}
|
|
});
|
|
},
|
|
|
|
_reset: function() {
|
|
this._currentAnalysis = null;
|
|
this._isAnalyzing = false;
|
|
this._clearSearchArea();
|
|
|
|
if (this._dropZone) this._dropZone.style.display = 'block';
|
|
|
|
var els = ['vlm-preview', 'vlm-loading', 'vlm-results', 'vlm-search-result'];
|
|
els.forEach(function(id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.style.display = 'none';
|
|
});
|
|
|
|
var resetBtn = document.getElementById('vlm-reset-btn');
|
|
if (resetBtn) resetBtn.style.display = 'none';
|
|
|
|
var fileInput = document.getElementById('vlm-file-input');
|
|
if (fileInput) fileInput.value = '';
|
|
|
|
var countEl = document.getElementById('count-vlm');
|
|
if (countEl) countEl.textContent = '-';
|
|
|
|
// Overpass-Ergebnisse loeschen
|
|
if (typeof OverpassLayer !== 'undefined') OverpassLayer.clear();
|
|
},
|
|
};
|