/** * VLM UI: Bildanalyse-Panel mit Upload, Zwei-Stufen-Workflow und Overpass-Kopplung. */ const VlmUI = { _panel: null, _dropZone: null, _currentAnalysis: null, _isAnalyzing: false, 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 = '
' + '
BILDANALYSE (VLM)
' + '' + '
' + // Drop-Zone '
' + '' + '' + '' + '
Bild hierher ziehen
' + '
oder klicken zum Waehlen (PNG/JPG/WEBP, max 10MB)
' + '
' + '' + // Preview '' + // Loading '' + // Analyse-Ergebnis '' + // Reset ''; 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 = 'Vorschau' + '
' + file.name + ' (' + (file.size / 1024).toFixed(0) + ' KB)
'; 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('GPS: ' + e.latitude + ', ' + e.longitude + ''); 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 = '
EXIF METADATEN
' + '
' + ep.join(' · ') + '
'; if (e.has_gps) { exifHtml += ''; } } else { exifHtml = '
EXIF
Keine Metadaten gefunden
'; } } // Landscape Clues (Reverse Geolocation) var cluesHtml = ''; if (data.landscape_clues) { var lc = data.landscape_clues; var cl = []; if (lc.vegetation) cl.push('Vegetation: ' + lc.vegetation); if (lc.soil_color) cl.push('Boden: ' + lc.soil_color); if (lc.road_markings) cl.push('Strassen: ' + lc.road_markings); if (lc.architecture_style) cl.push('Architektur: ' + lc.architecture_style); if (lc.signage_language) cl.push('Schilder: ' + lc.signage_language); if (lc.vehicle_types) cl.push('Fahrzeuge: ' + lc.vehicle_types); if (lc.climate_indicators) cl.push('Klima: ' + lc.climate_indicators); if (lc.sun_shadow_direction) cl.push('Schatten: ' + lc.sun_shadow_direction); if (cl.length > 0) { cluesHtml = '
LANDSCHAFTSMERKMALE
' + '
' + cl.join('
') + '
'; } } // Szene var sceneEl = document.getElementById('vlm-scene'); if (sceneEl) { sceneEl.innerHTML = exifHtml + '
' + (data.scene_description || 'Keine Beschreibung') + (data.terrain ? ' | Gelaende: ' + data.terrain : '') + (data.estimated_location_type ? ' | Region: ' + data.estimated_location_type : '') + '
' + 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 ? 'MIL' : ''; html += '
' + '' + '
' + '
' + obj.type.replace(/_/g, ' ') + '
' + '
' + obj.description + '
' + '
' + '' + confLabel + '' + milBadge + '
'; }); if (objects.length === 0) { html = '
Keine Objekte erkannt
'; } 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 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); // Zur EXIF-Position fliegen Globe.viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(exif.longitude, exif.latitude, 50000), duration: 2, }); } // === BBox bestimmen: EXIF-GPS > Viewport-Checkbox > Region === var bbox = null; if (hasExifGps) { // 0.5 Grad um EXIF-Position bbox = [ exif.latitude - 0.5, exif.longitude - 0.5, exif.latitude + 0.5, exif.longitude + 0.5, ]; } 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), ]; } } // Kein explizites BBox? Region wird vom Backend aus estimated_location_type abgeleitet } 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(); 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, }), }) .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...'; 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); // 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); } if (searchResult) { var parts = []; if (hasExifGps) parts.push('EXIF GPS-Marker'); parts.push(data.total + ' OSM-Orte'); 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 = (hasExifGps ? 1 : 0) + 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'; } }); }, _reset: function() { this._currentAnalysis = null; this._isAnalyzing = false; 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(); }, };