Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
| Beide Seiten der vorigen Revision Vorhergehende Überarbeitung | |||
|
openstreetmap [2026/03/21 14:04] jango [MapLibre] |
openstreetmap [2026/03/21 14:05] (aktuell) jango [MapLibre] |
||
|---|---|---|---|
| Zeile 331: | Zeile 331: | ||
| < | < | ||
| pmtiles convert austria.mbtiles austria.pmtiles | pmtiles convert austria.mbtiles austria.pmtiles | ||
| + | </ | ||
| + | |||
| + | Website (mit verschiedenen Features) | ||
| + | <code html> | ||
| + | < | ||
| + | <html lang=" | ||
| + | < | ||
| + | <meta charset=" | ||
| + | < | ||
| + | <meta name=" | ||
| + | |||
| + | <link rel=" | ||
| + | <script src=" | ||
| + | <script src=" | ||
| + | |||
| + | < | ||
| + | html, body { | ||
| + | height: 100%; | ||
| + | margin: 0; | ||
| + | font-family: | ||
| + | } | ||
| + | |||
| + | #app { | ||
| + | display: grid; | ||
| + | grid-template-columns: | ||
| + | height: 100%; | ||
| + | } | ||
| + | |||
| + | #sidebar { | ||
| + | background: #111827; | ||
| + | color: #f9fafb; | ||
| + | padding: 18px; | ||
| + | overflow: auto; | ||
| + | box-sizing: border-box; | ||
| + | border-right: | ||
| + | } | ||
| + | |||
| + | #map { | ||
| + | height: 100%; | ||
| + | width: 100%; | ||
| + | } | ||
| + | |||
| + | h1 { | ||
| + | font-size: 22px; | ||
| + | margin: 0 0 6px 0; | ||
| + | } | ||
| + | |||
| + | .sub { | ||
| + | color: #9ca3af; | ||
| + | font-size: 13px; | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | .group { | ||
| + | background: #1f2937; | ||
| + | border: 1px solid #374151; | ||
| + | border-radius: | ||
| + | padding: 14px; | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | .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: | ||
| + | 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: | ||
| + | margin-top: 10px; | ||
| + | } | ||
| + | |||
| + | button: | ||
| + | background: #1d4ed8; | ||
| + | } | ||
| + | |||
| + | .btn-secondary { | ||
| + | background: #374151; | ||
| + | } | ||
| + | |||
| + | .btn-secondary: | ||
| + | background: #4b5563; | ||
| + | } | ||
| + | |||
| + | .row { | ||
| + | display: grid; | ||
| + | grid-template-columns: | ||
| + | gap: 8px; | ||
| + | } | ||
| + | |||
| + | .mini { | ||
| + | font-size: 12px; | ||
| + | color: #cbd5e1; | ||
| + | line-height: | ||
| + | } | ||
| + | |||
| + | .status { | ||
| + | margin-top: 10px; | ||
| + | padding: 10px 12px; | ||
| + | border-radius: | ||
| + | background: #0f172a; | ||
| + | color: #e5e7eb; | ||
| + | font-size: 13px; | ||
| + | min-height: 18px; | ||
| + | } | ||
| + | |||
| + | .result-box { | ||
| + | margin-top: 10px; | ||
| + | padding: 10px 12px; | ||
| + | border-radius: | ||
| + | background: #0b1220; | ||
| + | color: #f8fafc; | ||
| + | font-size: 13px; | ||
| + | line-height: | ||
| + | } | ||
| + | |||
| + | .chip { | ||
| + | display: inline-block; | ||
| + | background: #0b1220; | ||
| + | color: #bfdbfe; | ||
| + | border: 1px solid #1d4ed8; | ||
| + | padding: 6px 8px; | ||
| + | border-radius: | ||
| + | font-size: 12px; | ||
| + | margin: 4px 6px 0 0; | ||
| + | } | ||
| + | |||
| + | .marker-pin { | ||
| + | width: 18px; | ||
| + | height: 18px; | ||
| + | border-radius: | ||
| + | border: 3px solid white; | ||
| + | box-shadow: 0 0 0 2px rgba(0, | ||
| + | } | ||
| + | |||
| + | .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: | ||
| + | } | ||
| + | |||
| + | @media (max-width: 900px) { | ||
| + | #app { | ||
| + | grid-template-columns: | ||
| + | grid-template-rows: | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | </ | ||
| + | < | ||
| + | <div id=" | ||
| + | <aside id=" | ||
| + | < | ||
| + | <div class=" | ||
| + | |||
| + | <div class=" | ||
| + | < | ||
| + | |||
| + | <label for=" | ||
| + | <input id=" | ||
| + | |||
| + | <label for=" | ||
| + | <input id=" | ||
| + | |||
| + | <div class=" | ||
| + | <button id=" | ||
| + | <button id=" | ||
| + | </ | ||
| + | |||
| + | <button id=" | ||
| + | <button id=" | ||
| + | <button id=" | ||
| + | <button id=" | ||
| + | |||
| + | <div id=" | ||
| + | </ | ||
| + | |||
| + | <div class=" | ||
| + | < | ||
| + | <div id=" | ||
| + | </ | ||
| + | |||
| + | <div class=" | ||
| + | < | ||
| + | <button id=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | |||
| + | <div class=" | ||
| + | < | ||
| + | <button id=" | ||
| + | <button id=" | ||
| + | <div class=" | ||
| + | <div id=" | ||
| + | </ | ||
| + | |||
| + | <div class=" | ||
| + | < | ||
| + | <div id=" | ||
| + | </ | ||
| + | |||
| + | <div class=" | ||
| + | < | ||
| + | <div class=" | ||
| + | Dieses Beispiel nutzt öffentliche Geocoding- und Routing-Endpoints. | ||
| + | Für Produktion solltest du Geocoding und Routing selbst hosten oder einen passenden Anbieter verwenden. | ||
| + | </ | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | |||
| + | <div id=" | ||
| + | </ | ||
| + | |||
| + | <div id=" | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | const PMTILES_URL = " | ||
| + | |||
| + | const NOMINATIM_SEARCH_URL = " | ||
| + | const OSRM_ROUTE_BASE = " | ||
| + | |||
| + | const AUSTRIA_BBOX = [9.4, 46.3, 17.3, 49.1]; | ||
| + | |||
| + | const protocol = new pmtiles.Protocol(); | ||
| + | maplibregl.addProtocol(" | ||
| + | |||
| + | 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(" | ||
| + | const searchInfoEl = document.getElementById(" | ||
| + | const routeInfoEl = document.getElementById(" | ||
| + | const directionsEl = document.getElementById(" | ||
| + | const markerModeStateEl = document.getElementById(" | ||
| + | |||
| + | function setStatus(msg) { | ||
| + | statusEl.textContent = msg; | ||
| + | } | ||
| + | |||
| + | function makeMarkerElement(cls) { | ||
| + | const el = document.createElement(" | ||
| + | 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, | ||
| + | const name = item.display_name || "Kein Name"; | ||
| + | const lat = Number(item.lat).toFixed(6); | ||
| + | const lon = Number(item.lon).toFixed(6); | ||
| + | searchInfoEl.innerHTML = ` | ||
| + | < | ||
| + | ${name}< | ||
| + | <span style=" | ||
| + | <span style=" | ||
| + | `; | ||
| + | } | ||
| + | |||
| + | function getInitialView(defaultLng, | ||
| + | const params = new URLSearchParams(window.location.search); | ||
| + | |||
| + | const lng = parseFloat(params.get(" | ||
| + | const lat = parseFloat(params.get(" | ||
| + | const z = parseFloat(params.get(" | ||
| + | |||
| + | 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(" | ||
| + | url.searchParams.set(" | ||
| + | url.searchParams.set(" | ||
| + | |||
| + | history.replaceState(null, | ||
| + | } | ||
| + | |||
| + | function updateUrlRouteParams() { | ||
| + | const url = new URL(window.location.href); | ||
| + | |||
| + | if (startCoord) { | ||
| + | url.searchParams.set(" | ||
| + | } else { | ||
| + | url.searchParams.delete(" | ||
| + | } | ||
| + | |||
| + | if (endCoord) { | ||
| + | url.searchParams.set(" | ||
| + | } else { | ||
| + | url.searchParams.delete(" | ||
| + | } | ||
| + | |||
| + | history.replaceState(null, | ||
| + | } | ||
| + | |||
| + | 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, | ||
| + | if (!query || !query.trim()) { | ||
| + | throw new Error(`${roleLabel}: | ||
| + | } | ||
| + | |||
| + | setStatus(`${roleLabel}: | ||
| + | |||
| + | const url = new URL(NOMINATIM_SEARCH_URL); | ||
| + | url.searchParams.set(" | ||
| + | url.searchParams.set(" | ||
| + | url.searchParams.set(" | ||
| + | url.searchParams.set(" | ||
| + | url.searchParams.set(" | ||
| + | url.searchParams.set(" | ||
| + | url.searchParams.set(" | ||
| + | |||
| + | const res = await fetch(url.toString(), | ||
| + | headers: { | ||
| + | " | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | 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 " | ||
| + | } | ||
| + | |||
| + | const best = results[0]; | ||
| + | setStatus(`${roleLabel}: | ||
| + | setSearchInfo(roleLabel, | ||
| + | return best; | ||
| + | } | ||
| + | |||
| + | function setStartPoint(lng, | ||
| + | startCoord = [lng, lat]; | ||
| + | |||
| + | if (startMarker) startMarker.remove(); | ||
| + | startMarker = new maplibregl.Marker({ | ||
| + | element: makeMarkerElement(" | ||
| + | draggable: true | ||
| + | }) | ||
| + | .setLngLat(startCoord) | ||
| + | .setPopup(new maplibregl.Popup().setHTML(labelHtml)) | ||
| + | .addTo(map); | ||
| + | |||
| + | startMarker.on(" | ||
| + | const ll = startMarker.getLngLat(); | ||
| + | startCoord = [ll.lng, ll.lat]; | ||
| + | updateUrlRouteParams(); | ||
| + | updateUrlFromMap(); | ||
| + | setStatus(" | ||
| + | }); | ||
| + | |||
| + | updateUrlRouteParams(); | ||
| + | updateUrlFromMap(); | ||
| + | } | ||
| + | |||
| + | function setEndPoint(lng, | ||
| + | endCoord = [lng, lat]; | ||
| + | |||
| + | if (endMarker) endMarker.remove(); | ||
| + | endMarker = new maplibregl.Marker({ | ||
| + | element: makeMarkerElement(" | ||
| + | draggable: true | ||
| + | }) | ||
| + | .setLngLat(endCoord) | ||
| + | .setPopup(new maplibregl.Popup().setHTML(labelHtml)) | ||
| + | .addTo(map); | ||
| + | |||
| + | endMarker.on(" | ||
| + | const ll = endMarker.getLngLat(); | ||
| + | endCoord = [ll.lng, ll.lat]; | ||
| + | updateUrlRouteParams(); | ||
| + | updateUrlFromMap(); | ||
| + | setStatus(" | ||
| + | }); | ||
| + | |||
| + | updateUrlRouteParams(); | ||
| + | updateUrlFromMap(); | ||
| + | } | ||
| + | |||
| + | function ensureAccuracyLayers() { | ||
| + | if (map.getSource(" | ||
| + | |||
| + | map.addSource(" | ||
| + | type: " | ||
| + | data: { | ||
| + | type: " | ||
| + | features: [] | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | map.addLayer({ | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | map.addLayer({ | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }); | ||
| + | } | ||
| + | |||
| + | function circleGeoJSON(center, | ||
| + | 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: " | ||
| + | geometry: { | ||
| + | type: " | ||
| + | coordinates: | ||
| + | } | ||
| + | }; | ||
| + | } | ||
| + | |||
| + | async function locateMe() { | ||
| + | if (!navigator.geolocation) { | ||
| + | alert(" | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | setStatus(" | ||
| + | |||
| + | 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(" | ||
| + | }) | ||
| + | .setLngLat([lng, | ||
| + | .setPopup( | ||
| + | new maplibregl.Popup().setHTML( | ||
| + | `< | ||
| + | ) | ||
| + | ) | ||
| + | .addTo(map); | ||
| + | |||
| + | ensureAccuracyLayers(); | ||
| + | map.getSource(" | ||
| + | |||
| + | updateUrlFromMap(); | ||
| + | setStatus(`Standort gefunden. Genauigkeit ca. ${Math.round(accuracy)} m.`); | ||
| + | }, | ||
| + | (err) => { | ||
| + | console.error(err); | ||
| + | alert(" | ||
| + | setStatus(" | ||
| + | }, | ||
| + | { | ||
| + | enableHighAccuracy: | ||
| + | timeout: 10000, | ||
| + | maximumAge: 0 | ||
| + | } | ||
| + | ); | ||
| + | } | ||
| + | |||
| + | function modifierToGerman(mod) { | ||
| + | const map = { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | "sharp left": " | ||
| + | "sharp right": | ||
| + | " | ||
| + | }; | ||
| + | return map[mod] || mod || ""; | ||
| + | } | ||
| + | |||
| + | function stepToGerman(step, | ||
| + | const type = step.maneuver? | ||
| + | const modifier = modifierToGerman(step.maneuver? | ||
| + | const roadName = step.name ? ` auf ${step.name}` : ""; | ||
| + | const dist = formatMeters(step.distance || 0); | ||
| + | |||
| + | if (type === " | ||
| + | return `${index + 1}. Starten Sie${roadName}.`; | ||
| + | } | ||
| + | |||
| + | if (type === " | ||
| + | return `${index + 1}. Sie haben Ihr Ziel erreicht.`; | ||
| + | } | ||
| + | |||
| + | if (type === " | ||
| + | return `${index + 1}. In ${dist} ${modifier} abbiegen${roadName}.`; | ||
| + | } | ||
| + | |||
| + | if (type === " | ||
| + | return `${index + 1}. ${dist}${roadName} folgen, dann ${modifier}.`; | ||
| + | } | ||
| + | |||
| + | if (type === "new name") { | ||
| + | return `${index + 1}. Dem Straßenverlauf ${dist}${roadName} folgen.`; | ||
| + | } | ||
| + | |||
| + | if (type === " | ||
| + | return `${index + 1}. In ${dist} ${modifier} einfädeln${roadName}.`; | ||
| + | } | ||
| + | |||
| + | if (type === " | ||
| + | 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 === " | ||
| + | const exit = step.maneuver? | ||
| + | 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? | ||
| + | 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 = "< | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | const html = allSteps | ||
| + | .map((step, i) => `< | ||
| + | .join("" | ||
| + | |||
| + | directionsEl.innerHTML = `< | ||
| + | } | ||
| + | |||
| + | async function drawRoute() { | ||
| + | if (!startCoord || !endCoord) { | ||
| + | alert(" | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | setStatus(" | ||
| + | |||
| + | const url = | ||
| + | `${OSRM_ROUTE_BASE}/ | ||
| + | `${startCoord[0]}, | ||
| + | `? | ||
| + | |||
| + | 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(" | ||
| + | } | ||
| + | |||
| + | const route = data.routes[0]; | ||
| + | const routeFeature = { | ||
| + | type: " | ||
| + | geometry: route.geometry, | ||
| + | properties: {} | ||
| + | }; | ||
| + | |||
| + | if (map.getSource(" | ||
| + | map.getSource(" | ||
| + | } else { | ||
| + | map.addSource(" | ||
| + | type: " | ||
| + | data: routeFeature | ||
| + | }); | ||
| + | |||
| + | map.addLayer({ | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | map.addLayer({ | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }); | ||
| + | } | ||
| + | |||
| + | const bounds = new maplibregl.LngLatBounds(); | ||
| + | route.geometry.coordinates.forEach(c => bounds.extend(c)); | ||
| + | map.fitBounds(bounds, | ||
| + | |||
| + | routeInfoEl.innerHTML = ` | ||
| + | < | ||
| + | Distanz: ${formatKm(route.distance)}< | ||
| + | Dauer: ${formatDuration(route.duration)} | ||
| + | `; | ||
| + | |||
| + | renderTextDirections(route); | ||
| + | updateUrlRouteParams(); | ||
| + | updateUrlFromMap(); | ||
| + | setStatus(" | ||
| + | } | ||
| + | |||
| + | function clearRoute() { | ||
| + | if (map.getLayer(" | ||
| + | if (map.getLayer(" | ||
| + | if (map.getSource(" | ||
| + | |||
| + | startCoord = null; | ||
| + | endCoord = null; | ||
| + | |||
| + | if (startMarker) { | ||
| + | startMarker.remove(); | ||
| + | startMarker = null; | ||
| + | } | ||
| + | |||
| + | if (endMarker) { | ||
| + | endMarker.remove(); | ||
| + | endMarker = null; | ||
| + | } | ||
| + | |||
| + | document.getElementById(" | ||
| + | document.getElementById(" | ||
| + | |||
| + | routeInfoEl.textContent = "Noch keine Route."; | ||
| + | directionsEl.textContent = "Noch keine Wegbeschreibung."; | ||
| + | |||
| + | updateUrlRouteParams(); | ||
| + | updateUrlFromMap(); | ||
| + | setStatus(" | ||
| + | } | ||
| + | |||
| + | function clearFreeMarkers() { | ||
| + | freeMarkers.forEach(m => m.remove()); | ||
| + | freeMarkers = []; | ||
| + | setStatus(" | ||
| + | } | ||
| + | |||
| + | function swapStartEnd() { | ||
| + | const startText = document.getElementById(" | ||
| + | const endText = document.getElementById(" | ||
| + | |||
| + | document.getElementById(" | ||
| + | document.getElementById(" | ||
| + | |||
| + | 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("< | ||
| + | } | ||
| + | if (endMarker) { | ||
| + | endMarker.setPopup(new maplibregl.Popup().setHTML("< | ||
| + | } | ||
| + | |||
| + | updateUrlRouteParams(); | ||
| + | updateUrlFromMap(); | ||
| + | setStatus(" | ||
| + | } | ||
| + | |||
| + | archive.getHeader().then((h) => { | ||
| + | const initialView = getInitialView(h.centerLon, | ||
| + | |||
| + | map = new maplibregl.Map({ | ||
| + | container: " | ||
| + | center: [initialView.lng, | ||
| + | zoom: initialView.zoom, | ||
| + | pitch: 0, //60, | ||
| + | bearing: 0, //-20, | ||
| + | style: { | ||
| + | version: 8, | ||
| + | sources: { | ||
| + | omt: { | ||
| + | type: " | ||
| + | url: `pmtiles:// | ||
| + | attribution: | ||
| + | } | ||
| + | }, | ||
| + | layers: [ | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | paint: { " | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | [" | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | "# | ||
| + | ] | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | [" | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | "# | ||
| + | ], | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | paint: { " | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | paint: { " | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 6, 0.5, | ||
| + | 10, 1.2, | ||
| + | 14, 2.2 | ||
| + | ] | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 5, 0.4, | ||
| + | 10, 1.0, | ||
| + | 14, 1.5 | ||
| + | ] | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | [" | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | "# | ||
| + | ], | ||
| + | " | ||
| + | " | ||
| + | 5, 0.4, | ||
| + | 8, 1.0, | ||
| + | 10, 1.8, | ||
| + | 12, 3.0, | ||
| + | 14, 5.0 | ||
| + | ] | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | minzoom: 15, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | minzoom: 10, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | [" | ||
| + | 8 | ||
| + | ], | ||
| + | " | ||
| + | " | ||
| + | [" | ||
| + | 0 | ||
| + | ], | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | minzoom: 8, | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | minzoom: 7, | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | |||
| + | / | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | minzoom: 10, | ||
| + | filter: [ | ||
| + | " | ||
| + | [" | ||
| + | [" | ||
| + | true, | ||
| + | false | ||
| + | ], | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 9, 10, | ||
| + | 11, 11, | ||
| + | 13, 13, | ||
| + | 15, 14 | ||
| + | ], | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | minzoom: 11, | ||
| + | filter: [ | ||
| + | " | ||
| + | [" | ||
| + | [" | ||
| + | true, | ||
| + | false | ||
| + | ], | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 11, 10, | ||
| + | 13, 11, | ||
| + | 15, 12 | ||
| + | ] | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | minzoom: 12, | ||
| + | filter: [ | ||
| + | " | ||
| + | [" | ||
| + | [" | ||
| + | true, | ||
| + | false | ||
| + | ], | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | minzoom: 17, | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 5, 10, | ||
| + | 8, 12, | ||
| + | 10, 14, | ||
| + | 14, 16 | ||
| + | ] | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | |||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | minzoom: 10, | ||
| + | /*filter: [ | ||
| + | " | ||
| + | [" | ||
| + | [" | ||
| + | true, | ||
| + | false | ||
| + | ],*/ | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | |||
| + | /* | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | minzoom: 16, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | */ | ||
| + | |||
| + | ] | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | map.addControl(new maplibregl.NavigationControl(), | ||
| + | |||
| + | map.on(" | ||
| + | updateUrlFromMap(); | ||
| + | }); | ||
| + | |||
| + | map.on(" | ||
| + | if (!markerMode) return; | ||
| + | |||
| + | const marker = new maplibregl.Marker({ | ||
| + | element: makeMarkerElement(" | ||
| + | draggable: true | ||
| + | }) | ||
| + | .setLngLat([e.lngLat.lng, | ||
| + | .setPopup( | ||
| + | new maplibregl.Popup().setHTML( | ||
| + | `< | ||
| + | ) | ||
| + | ) | ||
| + | .addTo(map); | ||
| + | |||
| + | freeMarkers.push(marker); | ||
| + | setStatus(`Freier Marker gesetzt. Gesamt: ${freeMarkers.length}`); | ||
| + | }); | ||
| + | |||
| + | |||
| + | map.on(" | ||
| + | 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[" | ||
| + | const cls = props[" | ||
| + | const sub = props[" | ||
| + | |||
| + | new maplibregl.Popup() | ||
| + | .setLngLat(e.lngLat) | ||
| + | .setHTML(` | ||
| + | < | ||
| + | Klasse: ${cls}/ | ||
| + | Koordinaten: | ||
| + | `) | ||
| + | .addTo(map); | ||
| + | }); | ||
| + | |||
| + | map.on(" | ||
| + | map.getCanvas().style.cursor = " | ||
| + | }); | ||
| + | |||
| + | map.on(" | ||
| + | map.getCanvas().style.cursor = ""; | ||
| + | }); | ||
| + | |||
| + | |||
| + | const startFromUrl = readCoordParam(" | ||
| + | const endFromUrl = readCoordParam(" | ||
| + | |||
| + | if (startFromUrl) { | ||
| + | setStartPoint(startFromUrl[0], | ||
| + | } | ||
| + | |||
| + | if (endFromUrl) { | ||
| + | setEndPoint(endFromUrl[0], | ||
| + | } | ||
| + | |||
| + | if (startFromUrl && endFromUrl) { | ||
| + | drawRoute().catch(console.error); | ||
| + | } | ||
| + | |||
| + | document.getElementById(" | ||
| + | try { | ||
| + | const q = document.getElementById(" | ||
| + | const hit = await geocodeAddress(q, | ||
| + | const lng = Number(hit.lon); | ||
| + | const lat = Number(hit.lat); | ||
| + | setStartPoint(lng, | ||
| + | map.flyTo({ center: [lng, lat], zoom: 16 }); | ||
| + | } catch (err) { | ||
| + | console.error(err); | ||
| + | alert(err.message); | ||
| + | setStatus(err.message); | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | document.getElementById(" | ||
| + | try { | ||
| + | const q = document.getElementById(" | ||
| + | const hit = await geocodeAddress(q, | ||
| + | const lng = Number(hit.lon); | ||
| + | const lat = Number(hit.lat); | ||
| + | setEndPoint(lng, | ||
| + | map.flyTo({ center: [lng, lat], zoom: 16 }); | ||
| + | } catch (err) { | ||
| + | console.error(err); | ||
| + | alert(err.message); | ||
| + | setStatus(err.message); | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | document.getElementById(" | ||
| + | try { | ||
| + | await drawRoute(); | ||
| + | } catch (err) { | ||
| + | console.error(err); | ||
| + | alert(err.message); | ||
| + | setStatus(err.message); | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | document.getElementById(" | ||
| + | clearRoute(); | ||
| + | }); | ||
| + | |||
| + | document.getElementById(" | ||
| + | swapStartEnd(); | ||
| + | }); | ||
| + | |||
| + | document.getElementById(" | ||
| + | |||
| + | document.getElementById(" | ||
| + | markerMode = !markerMode; | ||
| + | markerModeStateEl.textContent = `Klick-Marker: | ||
| + | setStatus(`Klick-Marker ${markerMode ? " | ||
| + | }); | ||
| + | |||
| + | document.getElementById(" | ||
| + | |||
| + | document.getElementById(" | ||
| + | try { | ||
| + | updateUrlFromMap(); | ||
| + | updateUrlRouteParams(); | ||
| + | await navigator.clipboard.writeText(window.location.href); | ||
| + | setStatus(" | ||
| + | } catch (err) { | ||
| + | console.error(err); | ||
| + | setStatus(" | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | document.getElementById(" | ||
| + | if (e.key === " | ||
| + | }); | ||
| + | |||
| + | document.getElementById(" | ||
| + | if (e.key === " | ||
| + | }); | ||
| + | |||
| + | updateUrlFromMap(); | ||
| + | setStatus(" | ||
| + | }).catch((err) => { | ||
| + | console.error(err); | ||
| + | setStatus(" | ||
| + | }); | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| </ | </ | ||