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.
' +
'
'
).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' + (idx + 1) + '. ' + escapeHtml(item.name) + '
' +
'' +
'' +
'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);
})();