Orts-Scanner Mini
Kleine, stabilere Webador-Version mit groben Auffälligkeitszonen auf Ortsniveau.
Diese Version zeigt nur grobe Auffälligkeitszonen und Ortsnamen, keine exakten Adressen und keine einzelnen Häuser.
Satellitenkarte
Bereit.
Gefundene Orte
Noch keine Daten geladen.
'; return; } resultsEl.innerHTML = list.map((item, idx) => '
' + '
' + (idx + 1) + '. ' + escapeHtml(item.name) + '
' + '
Region: ' + escapeHtml(item.region) + '
' + '
Auffälligkeitswert: ' + Number(item.score).toFixed(1) + '
' + '
' ).join(''); } async function geocode(query) { const url = 'https://nominatim.openstreetmap.org/search?format=jsonv2&limit=1&polygon_geojson=1&q=' + encodeURIComponent(query); const res = await fetch(url, { headers: { 'Accept': 'application/json' } }); if (!res.ok) throw new Error('Ort konnte nicht geladen werden'); const data = await res.json(); if (!data || !data.length) throw new Error('Ort nicht gefunden'); return data[0]; } async function reversePlace(lat, lon) { const url = 'https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=' + encodeURIComponent(lat) + '&lon=' + encodeURIComponent(lon); const res = await fetch(url, { headers: { 'Accept': 'application/json' } }); if (!res.ok) return null; return await res.json(); } async function loadVegetation(bounds) { const center = bounds.getCenter(); const radius = 0.14; const s = center.lat - radius; const w = center.lng - radius; const n = center.lat + radius; const e = center.lng + radius; const query = ` [out:json][timeout:18]; ( way["natural"="wood"](${s},${w},${n},${e}); relation["natural"="wood"](${s},${w},${n},${e}); way["natural"="scrub"](${s},${w},${n},${e}); relation["natural"="scrub"](${s},${w},${n},${e}); way["landuse"="forest"](${s},${w},${n},${e}); relation["landuse"="forest"](${s},${w},${n},${e}); way["landuse"="grass"](${s},${w},${n},${e}); relation["landuse"="grass"](${s},${w},${n},${e}); way["landuse"="meadow"](${s},${w},${n},${e}); relation["landuse"="meadow"](${s},${w},${n},${e}); ); out geom; `; const servers = [ 'https://overpass-api.de/api/interpreter', 'https://lz4.overpass-api.de/api/interpreter', 'https://overpass.kumi.systems/api/interpreter' ]; for (let i = 0; i < servers.length; i++) { try { setStatus('Vegetationsdaten werden geladen ... Server ' + (i + 1) + '/' + servers.length); const res = await fetch(servers[i], { method: 'POST', headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, body: query }); if (res.ok) return await res.json(); } catch (e) {} } throw new Error('Vegetationsdaten konnten nicht geladen werden'); } function squareGrid(bounds, stepDeg) { const cells = []; for (let lat = bounds.getSouth(); lat < bounds.getNorth(); lat += stepDeg) { for (let lon = bounds.getWest(); lon < bounds.getEast(); lon += stepDeg) { cells.push({ south: lat, west: lon, north: Math.min(lat + stepDeg, bounds.getNorth()), east: Math.min(lon + stepDeg, bounds.getEast()) }); } } return cells; } function polygonBBox(geom) { let minLat = Infinity, minLon = Infinity, maxLat = -Infinity, maxLon = -Infinity; for (const p of geom) { if (p.lat < minLat) minLat = p.lat; if (p.lat > maxLat) maxLat = p.lat; if (p.lon < minLon) minLon = p.lon; if (p.lon > maxLon) maxLon = p.lon; } return { minLat, minLon, maxLat, maxLon }; } function intersects(a, b) { return !(a.maxLon < b.west || a.minLon > b.east || a.maxLat < b.south || a.minLat > b.north); } function scoreCell(cell, elements) { let score = 0; for (const el of elements || []) { if (!el.geometry || el.geometry.length < 3) continue; const box = polygonBBox(el.geometry); if (!intersects(box, cell)) continue; const tags = el.tags || {}; if (tags.natural === 'wood' || tags.landuse === 'forest') score += 3; else if (tags.natural === 'scrub') score += 2.5; else if (tags.landuse === 'meadow') score += 1.5; else if (tags.landuse === 'grass') score += 1; } return score; } function colorForScore(score) { if (score >= 10) return '#b91c1c'; if (score >= 6) return '#ea580c'; if (score >= 3) return '#ca8a04'; if (score >= 1.5) return '#65a30d'; return null; } async function scanView() { if (isLoading) return; isLoading = true; loadBtn.disabled = true; scanBtn.disabled = true; try { clearLayers(); renderResults([]); setStatus('Aktuelle Ansicht wird analysiert ...'); const currentBounds = map.getBounds(); const vegetation = await loadVegetation(currentBounds); const cells = squareGrid(currentBounds, 0.015); const suspiciousCells = []; for (let i = 0; i < cells.length; i++) { const cell = cells[i]; const score = scoreCell(cell, vegetation.elements || []); if (score >= 3) { const color = colorForScore(score); if (color) { const rect = L.rectangle( [[cell.south, cell.west], [cell.north, cell.east]], { color: color, weight: 1, fillColor: color, fillOpacity: 0.28 } ).addTo(map); rect.bindPopup( 'Grobe Auffälligkeitszone
Wert: ' + score.toFixed(1) + '
Nur Ortsniveau, keine Hausadresse.' ); layers.push(rect); suspiciousCells.push({ lat: (cell.south + cell.north) / 2, lon: (cell.west + cell.east) / 2, score }); } } } suspiciousCells.sort((a, b) => b.score - a.score); const top = suspiciousCells.slice(0, 10); if (!top.length) { renderResults([]); setStatus('Keine auffälligen Zonen gefunden.'); return; } setStatus('Ortsnamen werden ermittelt ...'); const results = []; const seen = new Set(); for (let i = 0; i < top.length; i++) { const rev = await reversePlace(top[i].lat, top[i].lon); if (!rev || !rev.address) continue; const a = rev.address; const place = a.city || a.town || a.village || a.municipality || a.county || a.state; if (!place) continue; const key = (place + '|' + (a.state || a.county || '')).toLowerCase(); if (seen.has(key)) continue; seen.add(key); results.push({ name: place, region: a.state || a.county || countryEl.value || '', score: top[i].score }); } renderResults(results); setStatus('Fertig. Orte ohne genaue Adressen geladen.'); } catch (err) { console.error(err); setStatus('Fehler: ' + (err.message || 'Unbekannter Fehler'), true); resultsEl.innerHTML = '
Fehler beim Laden.
'; } finally { isLoading = false; loadBtn.disabled = false; scanBtn.disabled = false; } } async function loadPlace() { if (isLoading) return; isLoading = true; loadBtn.disabled = true; scanBtn.disabled = true; try { const country = (countryEl.value || '').trim(); const state = (stateEl.value || '').trim(); const city = (cityEl.value || '').trim(); if (!city) throw new Error('Bitte einen Ort eingeben'); let query = city; if (state) query += ', ' + state; if (country) query += ', ' + country; setStatus('Ort wird geladen ...'); const place = await geocode(query); const lat = Number(place.lat); const lon = Number(place.lon); clearLayers(); const bounds = L.latLngBounds( [lat - 0.14, lon - 0.14], [lat + 0.14, lon + 0.14] ); map.fitBounds(bounds); if (place.geojson) { const regionLayer = L.geoJSON(place.geojson, { style: { color: '#3b82f6', weight: 2, fillOpacity: 0.03 } }).addTo(map); layers.push(regionLayer); } isLoading = false; loadBtn.disabled = false; scanBtn.disabled = false; await scanView(); } catch (err) { console.error(err); setStatus('Fehler: ' + (err.message || 'Unbekannter Fehler'), true); isLoading = false; loadBtn.disabled = false; scanBtn.disabled = false; } } loadBtn.addEventListener('click', loadPlace); scanBtn.addEventListener('click', scanView); })();