Inhaltsverzeichnis

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

GeoAPIfy Example + Key

PBF Download Europe/Austria

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

Python

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')

Leaflet

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: '&copy; 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: '&copy; 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>

Marker

<!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: '&copy; 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>

Routing

<!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: '&copy; 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>

MapLibre

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>