Siehe auch https://openrouteservice.org/, https://github.com/Overv/openstreetmap-tile-server, https://switch2osm.org/serving-tiles/manually-building-a-tile-server-ubuntu-22-04-lts/#Fonts
curl https://download.geofabrik.de/europe/austria-latest.osm.pbf -o austria-latest.osm.pbf curl https://download.geofabrik.de/europe/austria.poly -o austria.poly
import folium # Koordinaten für den Kartenmittelpunkt latitude = 48.2082 longitude = 16.3738 # Karte erstellen map_osm = folium.Map(location=[latitude, longitude], zoom_start=12) # Marker hinzufügen folium.Marker([latitude, longitude], popup='Wien').add_to(map_osm) # Karte anzeigen map_osm.save('map.html')
import folium import overpy # Koordinaten für den Kartenmittelpunkt latitude = 48.2082 longitude = 16.3738 # Overpass API-Abfrage erstellen api = overpy.Overpass() # Overpass API-Abfrage ausführen result = api.query(f'node(around:2000, {latitude}, {longitude})["highway"];out;') # Karte erstellen map_osm = folium.Map(location=[latitude, longitude], zoom_start=14) # Straßennamen hinzufügen for node in result.nodes: if 'name' in node.tags: folium.Marker([node.lat, node.lon], popup=node.tags['name']).add_to(map_osm) # Karte anzeigen map_osm.save('map.html')
import requests # Overpass API-Abfrage für Straßen in Wien overpass_url = "http://overpass-api.de/api/interpreter" query = ''' [out:xml]; area["name"="Wien"]->.a; way(area.a)["highway"]; out; ''' response = requests.get(overpass_url, params={'data': query}) # Daten lokal speichern with open('wien_streets.osm', 'wb') as file: file.write(response.content)
import folium import xml.etree.ElementTree as ET # XML-Daten laden tree = ET.parse('wien_streets.osm') root = tree.getroot() # Koordinaten für den Kartenmittelpunkt latitude = 48.2082 longitude = 16.3738 # Karte erstellen map_osm = folium.Map(location=[latitude, longitude], zoom_start=14) # Straßennamen hinzufügen for way in root.findall(".//way"): street_name = None coords = [] for tag in way.findall(".//tag"): if tag.get('k') == 'name': street_name = tag.get('v') break if street_name: for nd in way.findall(".//nd"): ref = nd.get('ref') node = root.find(f".//node[@id='{ref}']") if node is not None: lat = float(node.get('lat')) lon = float(node.get('lon')) coords.append([lat, lon]) if coords: folium.PolyLine(coords, color="blue", weight=2, popup=street_name).add_to(map_osm) # Karte anzeigen map_osm.save('map.html')
Marker setzen
<!DOCTYPE html> <html> <head> <title>Wien Karte mit Marker</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" /> <style> #map { height: 100vh; } </style> </head> <body> <div id="map"></div> <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script> <script> const map = L.map('map').setView([48.2082, 16.3738], 13); // Wien-Zentrum // OSM-Tiles L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap-Mitwirkende' }).addTo(map); // Marker hinzufügen const marker = L.marker([48.2082, 16.3738]).addTo(map); // Wien-Zentrum marker.bindPopup("<b>Wien!</b><br>Hauptstadt von Österreich").openPopup(); // Weitere Marker hinzufügen (z. B. Stephansdom) const stephansdom = L.marker([48.2064, 16.3705]).addTo(map); // Stephansdom stephansdom.bindPopup("<b>Stephansdom</b><br>Berühmte Kirche in Wien").openPopup(); </script> </body> </html>
Marker mit Adresse setzen (Geocoder)
<!DOCTYPE html> <html> <head> <title>Wien Karte mit Adresse und Marker</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css" /> <style> #map { height: 100vh; } </style> </head> <body> <div id="map"></div> <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script> <script> const map = L.map('map').setView([48.2082, 16.3738], 13); // Wien-Zentrum // OSM-Tiles L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap-Mitwirkende' }).addTo(map); // Geocoding: Adresse eingeben und Marker setzen const geocoder = L.Control.Geocoder.nominatim(); // Beispiel: Adresse "Stephansdom, Wien" in Koordinaten umwandeln und Marker setzen geocoder.geocode("Stephansdom, Wien", function(results) { const latlng = results[0].center; const marker = L.marker(latlng).addTo(map); marker.bindPopup("<b>Stephansdom</b><br>Berühmte Kirche in Wien").openPopup(); map.setView(latlng, 16); // Karte auf Marker zentrieren }); </script> </body> </html>
<!DOCTYPE html> <html> <head> <title>Wien Karte mit Adresse und Marker</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" /> <style> #map { height: 100vh; } </style> </head> <body> <div id="map"></div> <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script> <script> // Initialisiere die Karte mit Wien als Mittelpunkt const map = L.map('map').setView([48.2082, 16.3738], 13); // OSM-Tiles L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap-Mitwirkende' }).addTo(map); // Geocoding API (Nominatim) für eine Adresse function geocodeAddress(address) { fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`) .then(response => response.json()) .then(data => { if (data.length > 0) { const latlng = [data[0].lat, data[0].lon]; // Koordinaten der ersten Antwort const marker = L.marker(latlng).addTo(map); marker.bindPopup(`<b>${address}</b><br>Gefunden in OSM`).openPopup(); map.setView(latlng, 16); // Karte auf den Marker zentrieren } else { alert("Adresse nicht gefunden!"); } }) .catch(error => console.error('Geocoding-Fehler:', error)); } // Beispiel: Geocode für "Stephansdom, Wien" geocodeAddress("Stephansdom, Wien"); </script> </body> </html>
<!DOCTYPE html> <html> <head> <title>Wien Karte mit Adresse, Marker und Weg</title> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet-routing-machine/dist/leaflet-routing-machine.css" /> <style> #map { height: 100vh; } </style> </head> <body> <div id="map"></div> <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet-routing-machine/dist/leaflet-routing-machine.js"></script> <script> // Initialisiere die Karte mit Wien als Mittelpunkt const map = L.map('map').setView([48.2082, 16.3738], 13); // OSM-Tiles L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap-Mitwirkende' }).addTo(map); // Geocoding API (Nominatim) für eine Adresse function geocodeAddress(address) { return fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`) .then(response => response.json()) .then(data => { if (data.length > 0) { const latlng = [data[0].lat, data[0].lon]; return latlng; } else { alert("Adresse nicht gefunden!"); return null; } }) .catch(error => { console.error('Geocoding-Fehler:', error); return null; }); } // Beispiel: Geocode für "Stephansdom, Wien" und "Prater, Wien" async function drawRoute() { const startAddress = "Stephansdom, Wien"; const endAddress = "Prater, Wien"; const startCoords = await geocodeAddress(startAddress); const endCoords = await geocodeAddress(endAddress); if (startCoords && endCoords) { // Route mit Leaflet Routing Machine zeichnen L.Routing.control({ waypoints: [ L.latLng(startCoords), L.latLng(endCoords) ], routeWhileDragging: true }).addTo(map); } } // Route zeichnen drawRoute(); </script> </body> </html>
MBTiles Download und mit dem Tool pmtiles zu pmtiles konvertieren. Hier findet man verschiedene Builds, auch Windows binaries. Precompiled binaries
pmtiles convert austria.mbtiles austria.pmtiles
Website (mit verschiedenen Features)
<!DOCTYPE html> <html lang="de"> <head> <meta charset="utf-8" /> <title>Österreich-Karte Pro</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5.21.0/dist/maplibre-gl.css" /> <script src="https://unpkg.com/maplibre-gl@5.21.0/dist/maplibre-gl.js"></script> <script src="https://unpkg.com/pmtiles@3.2.0/dist/pmtiles.js"></script> <style> html, body { height: 100%; margin: 0; font-family: Inter, Arial, sans-serif; } #app { display: grid; grid-template-columns: 380px 1fr; height: 100%; } #sidebar { background: #111827; color: #f9fafb; padding: 18px; overflow: auto; box-sizing: border-box; border-right: 1px solid #1f2937; } #map { height: 100%; width: 100%; } h1 { font-size: 22px; margin: 0 0 6px 0; } .sub { color: #9ca3af; font-size: 13px; margin-bottom: 18px; } .group { background: #1f2937; border: 1px solid #374151; border-radius: 12px; padding: 14px; margin-bottom: 14px; } .group h2 { font-size: 15px; margin: 0 0 12px 0; color: #fff; } label { display: block; font-size: 12px; color: #cbd5e1; margin: 8px 0 6px; } input, button { width: 100%; box-sizing: border-box; border-radius: 10px; border: 1px solid #4b5563; padding: 10px 12px; font-size: 14px; } input { background: #111827; color: #fff; } button { background: #2563eb; color: white; border: none; cursor: pointer; font-weight: 600; margin-top: 10px; } button:hover { background: #1d4ed8; } .btn-secondary { background: #374151; } .btn-secondary:hover { background: #4b5563; } .row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .mini { font-size: 12px; color: #cbd5e1; line-height: 1.4; } .status { margin-top: 10px; padding: 10px 12px; border-radius: 10px; background: #0f172a; color: #e5e7eb; font-size: 13px; min-height: 18px; } .result-box { margin-top: 10px; padding: 10px 12px; border-radius: 10px; background: #0b1220; color: #f8fafc; font-size: 13px; line-height: 1.5; } .chip { display: inline-block; background: #0b1220; color: #bfdbfe; border: 1px solid #1d4ed8; padding: 6px 8px; border-radius: 999px; font-size: 12px; margin: 4px 6px 0 0; } .marker-pin { width: 18px; height: 18px; border-radius: 50%; border: 3px solid white; box-shadow: 0 0 0 2px rgba(0,0,0,0.25); } .marker-start { background: #16a34a; } .marker-end { background: #dc2626; } .marker-free { background: #7c3aed; } .marker-user { background: #2563eb; } #directions ol { margin: 8px 0 0 18px; padding: 0; } #directions li { margin-bottom: 8px; } @media (max-width: 900px) { #app { grid-template-columns: 1fr; grid-template-rows: 420px 1fr; } } </style> </head> <body> <div id="app"> <aside id="sidebar"> <h1>Österreich-Karte Pro</h1> <div class="sub">PMTiles + MapLibre + Geocoding + Routing + Textanweisungen</div> <div class="group"> <h2>Route planen</h2> <label for="startInput">Start</label> <input id="startInput" type="text" placeholder="z. B. Liebenstraße 36, Graz" /> <label for="endInput">Ziel</label> <input id="endInput" type="text" placeholder="z. B. Hauptplatz 1, Linz" /> <div class="row"> <button id="searchStartBtn">Start suchen</button> <button id="searchEndBtn">Ziel suchen</button> </div> <button id="routeBtn">Route berechnen</button> <button id="swapBtn" class="btn-secondary">Start/Ziel tauschen</button> <button id="clearRouteBtn" class="btn-secondary">Route löschen</button> <button id="copyLinkBtn" class="btn-secondary">Link kopieren</button> <div id="routeInfo" class="result-box">Noch keine Route.</div> </div> <div class="group"> <h2>Wegbeschreibung</h2> <div id="directions" class="result-box">Noch keine Wegbeschreibung.</div> </div> <div class="group"> <h2>Meine Position</h2> <button id="locateBtn">Zu meiner GPS-Position</button> <div class="mini">Auf localhost funktioniert Geolocation in Browsern meist direkt. Auf echter Domain meist nur mit HTTPS.</div> </div> <div class="group"> <h2>Freie Marker</h2> <button id="enableMarkerModeBtn" class="btn-secondary">Klick-Marker einschalten</button> <button id="clearMarkersBtn" class="btn-secondary">Freie Marker löschen</button> <div class="mini">Wenn aktiv, setzt ein Klick auf die Karte einen eigenen Marker.</div> <div id="markerModeState" class="status">Klick-Marker: aus</div> </div> <div class="group"> <h2>Suche & Treffer</h2> <div id="searchInfo" class="result-box">Noch keine Suche.</div> </div> <div class="group"> <h2>Hinweise</h2> <div class="mini"> Dieses Beispiel nutzt öffentliche Geocoding- und Routing-Endpoints. Für Produktion solltest du Geocoding und Routing selbst hosten oder einen passenden Anbieter verwenden. </div> <div class="chip">PMTiles lokal</div> <div class="chip">Geocoding via Nominatim</div> <div class="chip">Routing via OSRM</div> <div class="chip">Link via GET</div> </div> <div id="status" class="status">Bereit.</div> </aside> <div id="map"></div> </div> <script> const PMTILES_URL = "http://localhost/at.pmtiles"; const NOMINATIM_SEARCH_URL = "https://nominatim.openstreetmap.org/search"; const OSRM_ROUTE_BASE = "https://router.project-osrm.org/route/v1/driving"; const AUSTRIA_BBOX = [9.4, 46.3, 17.3, 49.1]; const protocol = new pmtiles.Protocol(); maplibregl.addProtocol("pmtiles", protocol.tile); const archive = new pmtiles.PMTiles(PMTILES_URL); protocol.add(archive); let map; let startMarker = null; let endMarker = null; let userMarker = null; let freeMarkers = []; let markerMode = false; let startCoord = null; let endCoord = null; const statusEl = document.getElementById("status"); const searchInfoEl = document.getElementById("searchInfo"); const routeInfoEl = document.getElementById("routeInfo"); const directionsEl = document.getElementById("directions"); const markerModeStateEl = document.getElementById("markerModeState"); function setStatus(msg) { statusEl.textContent = msg; } function makeMarkerElement(cls) { const el = document.createElement("div"); el.className = `marker-pin ${cls}`; return el; } function formatKm(meters) { if (meters < 1000) return `${Math.round(meters)} m`; return `${(meters / 1000).toFixed(1)} km`; } function formatDuration(seconds) { const h = Math.floor(seconds / 3600); const m = Math.round((seconds % 3600) / 60); if (h > 0) return `${h} h ${m} min`; return `${m} min`; } function formatMeters(m) { if (m < 1000) return `${Math.round(m)} m`; return `${(m / 1000).toFixed(1)} km`; } function setSearchInfo(title, item) { const name = item.display_name || "Kein Name"; const lat = Number(item.lat).toFixed(6); const lon = Number(item.lon).toFixed(6); searchInfoEl.innerHTML = ` <b>${title}</b><br> ${name}<br> <span style="color:#93c5fd">Lat:</span> ${lat}, <span style="color:#93c5fd">Lon:</span> ${lon} `; } function getInitialView(defaultLng, defaultLat, defaultZoom) { const params = new URLSearchParams(window.location.search); const lng = parseFloat(params.get("lng")); const lat = parseFloat(params.get("lat")); const z = parseFloat(params.get("z")); return { lng: Number.isFinite(lng) ? lng : defaultLng, lat: Number.isFinite(lat) ? lat : defaultLat, zoom: Number.isFinite(z) ? z : defaultZoom }; } function updateUrlFromMap() { if (!map) return; const center = map.getCenter(); const zoom = map.getZoom(); const url = new URL(window.location.href); url.searchParams.set("lng", center.lng.toFixed(6)); url.searchParams.set("lat", center.lat.toFixed(6)); url.searchParams.set("z", zoom.toFixed(2)); history.replaceState(null, "", url.toString()); } function updateUrlRouteParams() { const url = new URL(window.location.href); if (startCoord) { url.searchParams.set("start", `${startCoord[0].toFixed(6)},${startCoord[1].toFixed(6)}`); } else { url.searchParams.delete("start"); } if (endCoord) { url.searchParams.set("end", `${endCoord[0].toFixed(6)},${endCoord[1].toFixed(6)}`); } else { url.searchParams.delete("end"); } history.replaceState(null, "", url.toString()); } function readCoordParam(name) { const params = new URLSearchParams(window.location.search); const raw = params.get(name); if (!raw) return null; const parts = raw.split(","); if (parts.length !== 2) return null; const lng = parseFloat(parts[0]); const lat = parseFloat(parts[1]); if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null; return [lng, lat]; } async function geocodeAddress(query, roleLabel) { if (!query || !query.trim()) { throw new Error(`${roleLabel}: Bitte Adresse eingeben.`); } setStatus(`${roleLabel}: Suche läuft...`); const url = new URL(NOMINATIM_SEARCH_URL); url.searchParams.set("q", query); url.searchParams.set("format", "jsonv2"); url.searchParams.set("limit", "5"); url.searchParams.set("addressdetails", "1"); url.searchParams.set("countrycodes", "at"); url.searchParams.set("bounded", "1"); url.searchParams.set("viewbox", AUSTRIA_BBOX.join(",")); const res = await fetch(url.toString(), { headers: { "Accept": "application/json" } }); if (!res.ok) { throw new Error(`Geocoding fehlgeschlagen (${res.status}).`); } const results = await res.json(); if (!Array.isArray(results) || results.length === 0) { throw new Error(`Keine Treffer für "${query}" gefunden.`); } const best = results[0]; setStatus(`${roleLabel}: Treffer gefunden.`); setSearchInfo(roleLabel, best); return best; } function setStartPoint(lng, lat, labelHtml = "<b>Start</b>") { startCoord = [lng, lat]; if (startMarker) startMarker.remove(); startMarker = new maplibregl.Marker({ element: makeMarkerElement("marker-start"), draggable: true }) .setLngLat(startCoord) .setPopup(new maplibregl.Popup().setHTML(labelHtml)) .addTo(map); startMarker.on("dragend", () => { const ll = startMarker.getLngLat(); startCoord = [ll.lng, ll.lat]; updateUrlRouteParams(); updateUrlFromMap(); setStatus("Startpunkt verschoben."); }); updateUrlRouteParams(); updateUrlFromMap(); } function setEndPoint(lng, lat, labelHtml = "<b>Ziel</b>") { endCoord = [lng, lat]; if (endMarker) endMarker.remove(); endMarker = new maplibregl.Marker({ element: makeMarkerElement("marker-end"), draggable: true }) .setLngLat(endCoord) .setPopup(new maplibregl.Popup().setHTML(labelHtml)) .addTo(map); endMarker.on("dragend", () => { const ll = endMarker.getLngLat(); endCoord = [ll.lng, ll.lat]; updateUrlRouteParams(); updateUrlFromMap(); setStatus("Zielpunkt verschoben."); }); updateUrlRouteParams(); updateUrlFromMap(); } function ensureAccuracyLayers() { if (map.getSource("user-accuracy")) return; map.addSource("user-accuracy", { type: "geojson", data: { type: "FeatureCollection", features: [] } }); map.addLayer({ id: "user-accuracy-fill", type: "fill", source: "user-accuracy", paint: { "fill-color": "#3b82f6", "fill-opacity": 0.12 } }); map.addLayer({ id: "user-accuracy-line", type: "line", source: "user-accuracy", paint: { "line-color": "#2563eb", "line-width": 2 } }); } function circleGeoJSON(center, radiusMeters, points = 64) { const [lng, lat] = center; const coords = []; const earthRadius = 6378137; for (let i = 0; i <= points; i++) { const angle = (i * 360 / points) * Math.PI / 180; const dx = radiusMeters * Math.cos(angle); const dy = radiusMeters * Math.sin(angle); const dLat = (dy / earthRadius) * (180 / Math.PI); const dLng = (dx / (earthRadius * Math.cos(lat * Math.PI / 180))) * (180 / Math.PI); coords.push([lng + dLng, lat + dLat]); } return { type: "Feature", geometry: { type: "Polygon", coordinates: [coords] } }; } async function locateMe() { if (!navigator.geolocation) { alert("Geolocation wird von deinem Browser nicht unterstützt."); return; } setStatus("Standort wird ermittelt..."); navigator.geolocation.getCurrentPosition( (position) => { const lng = position.coords.longitude; const lat = position.coords.latitude; const accuracy = position.coords.accuracy; map.flyTo({ center: [lng, lat], zoom: 16, essential: true }); if (userMarker) userMarker.remove(); userMarker = new maplibregl.Marker({ element: makeMarkerElement("marker-user") }) .setLngLat([lng, lat]) .setPopup( new maplibregl.Popup().setHTML( `<b>Deine Position</b><br>Genauigkeit: ${Math.round(accuracy)} m` ) ) .addTo(map); ensureAccuracyLayers(); map.getSource("user-accuracy").setData(circleGeoJSON([lng, lat], accuracy)); updateUrlFromMap(); setStatus(`Standort gefunden. Genauigkeit ca. ${Math.round(accuracy)} m.`); }, (err) => { console.error(err); alert("Standort konnte nicht ermittelt werden."); setStatus("Standort konnte nicht ermittelt werden."); }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } ); } function modifierToGerman(mod) { const map = { "left": "links", "right": "rechts", "straight": "geradeaus", "slight left": "leicht links", "slight right": "leicht rechts", "sharp left": "scharf links", "sharp right": "scharf rechts", "uturn": "wenden" }; return map[mod] || mod || ""; } function stepToGerman(step, index) { const type = step.maneuver?.type || ""; const modifier = modifierToGerman(step.maneuver?.modifier); const roadName = step.name ? ` auf ${step.name}` : ""; const dist = formatMeters(step.distance || 0); if (type === "depart") { return `${index + 1}. Starten Sie${roadName}.`; } if (type === "arrive") { return `${index + 1}. Sie haben Ihr Ziel erreicht.`; } if (type === "turn") { return `${index + 1}. In ${dist} ${modifier} abbiegen${roadName}.`; } if (type === "continue") { return `${index + 1}. ${dist}${roadName} folgen, dann ${modifier}.`; } if (type === "new name") { return `${index + 1}. Dem Straßenverlauf ${dist}${roadName} folgen.`; } if (type === "merge") { return `${index + 1}. In ${dist} ${modifier} einfädeln${roadName}.`; } if (type === "fork") { return `${index + 1}. Bei der Gabelung ${modifier} halten${roadName}.`; } if (type === "on ramp") { return `${index + 1}. In ${dist} die Auffahrt nehmen${roadName}.`; } if (type === "off ramp") { return `${index + 1}. In ${dist} die Abfahrt nehmen${roadName}.`; } if (type === "end of road") { return `${index + 1}. Am Ende der Straße ${modifier} abbiegen${roadName}.`; } if (type === "roundabout") { const exit = step.maneuver?.exit; if (exit) { return `${index + 1}. In ${dist} in den Kreisverkehr einfahren und die ${exit}. Ausfahrt nehmen${roadName}.`; } return `${index + 1}. In ${dist} in den Kreisverkehr einfahren${roadName}.`; } if (type === "exit roundabout") { const exit = step.maneuver?.exit; if (exit) { return `${index + 1}. Kreisverkehr an der ${exit}. Ausfahrt verlassen${roadName}.`; } return `${index + 1}. Kreisverkehr verlassen${roadName}.`; } return `${index + 1}. ${dist}${roadName}.`; } function renderTextDirections(route) { const allSteps = []; for (const leg of route.legs || []) { for (const step of leg.steps || []) { allSteps.push(step); } } if (!allSteps.length) { directionsEl.innerHTML = "<p>Keine Textanweisungen vorhanden.</p>"; return; } const html = allSteps .map((step, i) => `<li>${stepToGerman(step, i)}</li>`) .join(""); directionsEl.innerHTML = `<ul>${html}</ul>`; } async function drawRoute() { if (!startCoord || !endCoord) { alert("Bitte zuerst Start und Ziel setzen."); return; } setStatus("Route wird berechnet..."); const url = `${OSRM_ROUTE_BASE}/` + `${startCoord[0]},${startCoord[1]};${endCoord[0]},${endCoord[1]}` + `?overview=full&geometries=geojson&steps=true`; const res = await fetch(url); if (!res.ok) { throw new Error(`Routing fehlgeschlagen (${res.status}).`); } const data = await res.json(); if (!data.routes || !data.routes.length) { throw new Error("Keine Route gefunden."); } const route = data.routes[0]; const routeFeature = { type: "Feature", geometry: route.geometry, properties: {} }; if (map.getSource("route")) { map.getSource("route").setData(routeFeature); } else { map.addSource("route", { type: "geojson", data: routeFeature }); map.addLayer({ id: "route-line-outline", type: "line", source: "route", layout: { "line-cap": "round", "line-join": "round" }, paint: { "line-color": "#ffffff", "line-width": 8 } }); map.addLayer({ id: "route-line", type: "line", source: "route", layout: { "line-cap": "round", "line-join": "round" }, paint: { "line-color": "#2563eb", "line-width": 5 } }); } const bounds = new maplibregl.LngLatBounds(); route.geometry.coordinates.forEach(c => bounds.extend(c)); map.fitBounds(bounds, { padding: 60, maxZoom: 16 }); routeInfoEl.innerHTML = ` <b>Route gefunden</b><br> Distanz: ${formatKm(route.distance)}<br> Dauer: ${formatDuration(route.duration)} `; renderTextDirections(route); updateUrlRouteParams(); updateUrlFromMap(); setStatus("Route berechnet."); } function clearRoute() { if (map.getLayer("route-line")) map.removeLayer("route-line"); if (map.getLayer("route-line-outline")) map.removeLayer("route-line-outline"); if (map.getSource("route")) map.removeSource("route"); startCoord = null; endCoord = null; if (startMarker) { startMarker.remove(); startMarker = null; } if (endMarker) { endMarker.remove(); endMarker = null; } document.getElementById("startInput").value = ""; document.getElementById("endInput").value = ""; routeInfoEl.textContent = "Noch keine Route."; directionsEl.textContent = "Noch keine Wegbeschreibung."; updateUrlRouteParams(); updateUrlFromMap(); setStatus("Route gelöscht."); } function clearFreeMarkers() { freeMarkers.forEach(m => m.remove()); freeMarkers = []; setStatus("Freie Marker gelöscht."); } function swapStartEnd() { const startText = document.getElementById("startInput").value; const endText = document.getElementById("endInput").value; document.getElementById("startInput").value = endText; document.getElementById("endInput").value = startText; const oldStartCoord = startCoord; const oldEndCoord = endCoord; startCoord = oldEndCoord; endCoord = oldStartCoord; const oldStartMarker = startMarker; const oldEndMarker = endMarker; startMarker = oldEndMarker; endMarker = oldStartMarker; if (startMarker && startCoord) startMarker.setLngLat(startCoord); if (endMarker && endCoord) endMarker.setLngLat(endCoord); if (startMarker) { startMarker.setPopup(new maplibregl.Popup().setHTML("<b>Start</b>")); } if (endMarker) { endMarker.setPopup(new maplibregl.Popup().setHTML("<b>Ziel</b>")); } updateUrlRouteParams(); updateUrlFromMap(); setStatus("Start und Ziel getauscht."); } archive.getHeader().then((h) => { const initialView = getInitialView(h.centerLon, h.centerLat, 7); map = new maplibregl.Map({ container: "map", center: [initialView.lng, initialView.lat], zoom: initialView.zoom, pitch: 0, //60, bearing: 0, //-20, style: { version: 8, sources: { omt: { type: "vector", url: `pmtiles://${PMTILES_URL}`, attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' } }, layers: [ { id: "background", type: "background", paint: { "background-color": "#f2efe9" } }, { id: "landcover", type: "fill", source: "omt", "source-layer": "landcover", paint: { "fill-color": [ "match", ["get", "class"], "forest", "#d8e8c8", "wood", "#d8e8c8", "grass", "#e6efd8", "#e9efe3" ] } }, { id: "landuse", type: "fill", source: "omt", "source-layer": "landuse", paint: { "fill-color": [ "match", ["get", "class"], "residential", "#ece7e1", "industrial", "#e3ddd5", "commercial", "#e9e1d6", "farmland", "#eef3d6", "#ebe7df" ], "fill-opacity": 0.7 } }, { id: "parks", type: "fill", source: "omt", "source-layer": "park", paint: { "fill-color": "#d7edc2" } }, { id: "water", type: "fill", source: "omt", "source-layer": "water", paint: { "fill-color": "#bcdff5" } }, { id: "waterway", type: "line", source: "omt", "source-layer": "waterway", paint: { "line-color": "#8cc7ee", "line-width": [ "interpolate", ["linear"], ["zoom"], 6, 0.5, 10, 1.2, 14, 2.2 ] } }, { id: "boundaries", type: "line", source: "omt", "source-layer": "boundary", paint: { "line-color": "#9a8f80", "line-width": [ "interpolate", ["linear"], ["zoom"], 5, 0.4, 10, 1.0, 14, 1.5 ] } }, { id: "roads", type: "line", source: "omt", "source-layer": "transportation", paint: { "line-color": [ "match", ["get", "class"], "motorway", "#d28c8c", "trunk", "#d9a07b", "primary", "#e0b26f", "secondary", "#ead39c", "tertiary", "#ffffff", "minor", "#ffffff", "service", "#f7f7f7", "#ffffff" ], "line-width": [ "interpolate", ["linear"], ["zoom"], 5, 0.4, 8, 1.0, 10, 1.8, 12, 3.0, 14, 5.0 ] } }, { id: "buildings", type: "fill", source: "omt", "source-layer": "building", minzoom: 15, paint: { "fill-color": "#d9d0c7", "fill-outline-color": "#c8beb5" } }, { id: "buildings-3d", type: "fill-extrusion", source: "omt", "source-layer": "building", minzoom: 10, paint: { "fill-extrusion-color": "#d9d0c7", "fill-extrusion-height": [ "coalesce", ["get", "render_height"], 8 ], "fill-extrusion-base": [ "coalesce", ["get", "render_min_height"], 0 ], "fill-extrusion-opacity": 0.85 } }, { id: "water-names", type: "symbol", source: "omt", "source-layer": "water_name", minzoom: 8, layout: { "text-field": ["coalesce", ["get", "name:de"], ["get", "name_de"], ["get", "name"]], "text-size": 11 }, paint: { "text-color": "#2b6e99", "text-halo-color": "#ffffff", "text-halo-width": 1 } }, { id: "road-names", type: "symbol", source: "omt", "source-layer": "transportation_name", minzoom: 7, layout: { "symbol-placement": "line", "text-field": ["coalesce", ["get", "name:de"], ["get", "name_de"], ["get", "name"]], "text-size": 14, "text-allow-overlap": true }, paint: { "text-color": "#555", "text-halo-color": "#ffffff", "text-halo-width": 1.0 } }, /* { id: "road-names-major", type: "symbol", source: "omt", "source-layer": "transportation_name", minzoom: 10, filter: [ "match", ["get", "class"], ["motorway", "trunk", "primary", "secondary"], true, false ], layout: { "symbol-placement": "line", "text-field": ["coalesce", ["get", "name:de"], ["get", "name_de"], ["get", "name"]], "text-size": [ "interpolate", ["linear"], ["zoom"], 9, 10, 11, 11, 13, 13, 15, 14 ], "text-letter-spacing": 0.02 }, paint: { "text-color": "#4b5563", "text-halo-color": "#ffffff", "text-halo-width": 1.5 } }, { id: "road-names-medium", type: "symbol", source: "omt", "source-layer": "transportation_name", minzoom: 11, filter: [ "match", ["get", "class"], ["tertiary", "minor"], true, false ], layout: { "symbol-placement": "line", "text-field": ["coalesce", ["get", "name:de"], ["get", "name_de"], ["get", "name"]], "text-size": [ "interpolate", ["linear"], ["zoom"], 11, 10, 13, 11, 15, 12 ] }, paint: { "text-color": "#555", "text-halo-color": "#ffffff", "text-halo-width": 1.3 } }, { id: "road-names-small", type: "symbol", source: "omt", "source-layer": "transportation_name", minzoom: 12, filter: [ "match", ["get", "class"], ["service", "track", "path"], true, false ], layout: { "symbol-placement": "line", "text-field": ["coalesce", ["get", "name:de"], ["get", "name_de"], ["get", "name"]], "text-size": 14, "text-allow-overlap": true }, paint: { "text-color": "#666", "text-halo-color": "#ffffff", "text-halo-width": 1.1 } },*/ { id: "house-numbers", type: "symbol", source: "omt", "source-layer": "housenumber", minzoom: 17, layout: { "text-field": ["get", "housenumber"], "text-size": 12, "text-allow-overlap": false }, paint: { "text-color": "#6b3f1f", "text-halo-color": "#ffffff", "text-halo-width": 1 } }, { id: "place-names", type: "symbol", source: "omt", "source-layer": "place", layout: { "text-field": ["coalesce", ["get", "name:de"], ["get", "name_de"], ["get", "name"]], "text-size": [ "interpolate", ["linear"], ["zoom"], 5, 10, 8, 12, 10, 14, 14, 16 ] }, paint: { "text-color": "#222", "text-halo-color": "#ffffff", "text-halo-width": 1.5 } }, { id: "poi-labels", type: "symbol", source: "omt", "source-layer": "poi", minzoom: 10, /*filter: [ "match", ["get", "class"], ["hospital", "school", "railway", "attraction", "museum"], true, false ],*/ layout: { "text-field": ["coalesce", ["get", "name:de"], ["get", "name_de"], ["get", "name"]], "text-size": 16, "icon-image": "circle", "text-offset": [0, 0.8], "text-anchor": "top" }, paint: { "text-color": "#7c2d12", "text-halo-color": "#ffffff", "text-halo-width": 1.2 } } /* { id: "poi-dots", type: "circle", source: "omt", "source-layer": "poi", minzoom: 16, paint: { "circle-radius": 4, "circle-color": "#c2410c", "circle-stroke-color": "#ffffff", "circle-stroke-width": 1 } } */ ] } }); map.addControl(new maplibregl.NavigationControl(), "top-right"); map.on("moveend", () => { updateUrlFromMap(); }); map.on("click", (e) => { if (!markerMode) return; const marker = new maplibregl.Marker({ element: makeMarkerElement("marker-free"), draggable: true }) .setLngLat([e.lngLat.lng, e.lngLat.lat]) .setPopup( new maplibregl.Popup().setHTML( `<b>Freier Marker</b><br>Lng: ${e.lngLat.lng.toFixed(6)}<br>Lat: ${e.lngLat.lat.toFixed(6)}` ) ) .addTo(map); freeMarkers.push(marker); setStatus(`Freier Marker gesetzt. Gesamt: ${freeMarkers.length}`); }); map.on("click", "poi-dots", (e) => { const f = e.features[0]; const props = f.properties; const lng = e.lngLat.lng.toFixed(6); const lat = e.lngLat.lat.toFixed(6); const name = props["name:de"] || props["name_de"] || props["name"] || "POI"; const cls = props["class"] || "-"; const sub = props["subclass"] || "-"; new maplibregl.Popup() .setLngLat(e.lngLat) .setHTML(` <b>${name}</b><br> Klasse: ${cls}/${sub}<br> Koordinaten: ${lat}, ${lng} `) .addTo(map); }); map.on("mouseenter", "poi-dots", () => { map.getCanvas().style.cursor = "pointer"; }); map.on("mouseleave", "poi-dots", () => { map.getCanvas().style.cursor = ""; }); const startFromUrl = readCoordParam("start"); const endFromUrl = readCoordParam("end"); if (startFromUrl) { setStartPoint(startFromUrl[0], startFromUrl[1], "<b>Start</b><br>Aus Link geladen"); } if (endFromUrl) { setEndPoint(endFromUrl[0], endFromUrl[1], "<b>Ziel</b><br>Aus Link geladen"); } if (startFromUrl && endFromUrl) { drawRoute().catch(console.error); } document.getElementById("searchStartBtn").addEventListener("click", async () => { try { const q = document.getElementById("startInput").value; const hit = await geocodeAddress(q, "Start"); const lng = Number(hit.lon); const lat = Number(hit.lat); setStartPoint(lng, lat, `<b>Start</b><br>${hit.display_name}`); map.flyTo({ center: [lng, lat], zoom: 16 }); } catch (err) { console.error(err); alert(err.message); setStatus(err.message); } }); document.getElementById("searchEndBtn").addEventListener("click", async () => { try { const q = document.getElementById("endInput").value; const hit = await geocodeAddress(q, "Ziel"); const lng = Number(hit.lon); const lat = Number(hit.lat); setEndPoint(lng, lat, `<b>Ziel</b><br>${hit.display_name}`); map.flyTo({ center: [lng, lat], zoom: 16 }); } catch (err) { console.error(err); alert(err.message); setStatus(err.message); } }); document.getElementById("routeBtn").addEventListener("click", async () => { try { await drawRoute(); } catch (err) { console.error(err); alert(err.message); setStatus(err.message); } }); document.getElementById("clearRouteBtn").addEventListener("click", () => { clearRoute(); }); document.getElementById("swapBtn").addEventListener("click", () => { swapStartEnd(); }); document.getElementById("locateBtn").addEventListener("click", locateMe); document.getElementById("enableMarkerModeBtn").addEventListener("click", () => { markerMode = !markerMode; markerModeStateEl.textContent = `Klick-Marker: ${markerMode ? "an" : "aus"}`; setStatus(`Klick-Marker ${markerMode ? "aktiviert" : "deaktiviert"}.`); }); document.getElementById("clearMarkersBtn").addEventListener("click", clearFreeMarkers); document.getElementById("copyLinkBtn").addEventListener("click", async () => { try { updateUrlFromMap(); updateUrlRouteParams(); await navigator.clipboard.writeText(window.location.href); setStatus("Link in die Zwischenablage kopiert."); } catch (err) { console.error(err); setStatus("Link konnte nicht kopiert werden."); } }); document.getElementById("startInput").addEventListener("keydown", (e) => { if (e.key === "Enter") document.getElementById("searchStartBtn").click(); }); document.getElementById("endInput").addEventListener("keydown", (e) => { if (e.key === "Enter") document.getElementById("searchEndBtn").click(); }); updateUrlFromMap(); setStatus("Karte geladen."); }).catch((err) => { console.error(err); setStatus("Fehler beim Laden der PMTiles-Datei."); }); </script> </body> </html>