Benutzer-Werkzeuge

Webseiten-Werkzeuge


openstreetmap

Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen angezeigt.

Link zu dieser Vergleichsansicht

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:
 <code> <code>
 pmtiles convert austria.mbtiles austria.pmtiles pmtiles convert austria.mbtiles austria.pmtiles
 +</code>
 +
 +Website (mit verschiedenen Features)
 +<code html>
 +<!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>
 </code> </code>
openstreetmap.txt · Zuletzt geändert: 2026/03/21 14:05 von jango