Dies ist eine alte Version des Dokuments!


Minimalbeispiel

Library einbinden

<link rel="stylesheet" href="maplibre-gl.css" />
<script src="maplibre-gl.js"></script>
<script src="pmtiles.js"></script>

Map initialisieren

Man kann den Style auch in eine Datei auslagern, dann statt dem JSON Array den Dateinamen schreiben
// PMTiles-Protokoll bei MapLibre registrieren
const protocol = new pmtiles.Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile);
// Karte erzeugen
const map = new maplibregl.Map({
    container: "map", // Die div-id in welches Div die Map geladen wird
    center: [10.0, 51.0], // Startposition
    zoom: 6,
    style: {
        "version": 8,
        "sources": {
            "omt": {
                "type": "vector",
                "url": "pmtiles://http://localhost/wien-edited.pmtiles",
                "attribution": "© OpenStreetMap and 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
            }
        }, ]
    }
});
map.addControl(new maplibregl.NavigationControl(), "top-right");
map.on("load", () => {
    console.log("Karte geladen");
});

Marker

// Marker setzen
const marker = new maplibregl.Marker().setLngLat([16.3738, 48.2082]).addTo(map);
 
// Marker entfernen
marker.remove();
 
// Marker mit Popup
const marker = new maplibregl.Marker()
  .setLngLat([16.3738, 48.2082])
  .setPopup(
    new maplibregl.Popup().setHTML("<b>Hallo Wien</b>")
  )
  .addTo(map);
// Neuen Marker setzen und alten löschen
let currentMarker = null;
 
function setMarker(lng, lat) {
  if (currentMarker) {
    currentMarker.remove();
  }
 
  currentMarker = new maplibregl.Marker()
    .setLngLat([lng, lat])
    .addTo(map);
}
 
function removeMarker() {
  if (currentMarker) {
    currentMarker.remove();
    currentMarker = null;
  }
}
 
// Test
setMarker(16.3738, 48.2082);
removeMarker();
// Marker per Klick setzen
let currentMarker = null;
 
map.on("click", (e) => {
  if (currentMarker) {
    currentMarker.remove();
  }
 
  currentMarker = new maplibregl.Marker()
    .setLngLat(e.lngLat)
    .addTo(map);
});
// Marker per Klick setzen und mit Funktion löschen
let currentMarker = null;
 
function addMarkerAtClick(e) {
  if (currentMarker) {
    currentMarker.remove();
  }
 
  currentMarker = new maplibregl.Marker()
    .setLngLat([e.lngLat.lng, e.lngLat.lat])
    .addTo(map);
}
 
function clearMarker() {
  if (currentMarker) {
    currentMarker.remove();
    currentMarker = null;
  }
}
 
map.on("click", addMarkerAtClick);

GeoJSON einbinden

im map load Event

map.on("load", () => {
        console.log("Karte geladen");
	map.addSource("punkte", {
		type: "geojson",
		data: "pt.geojson"
	});
	map.addLayer({
		id: "punkte-layer",
		type: "circle",
		source: "punkte",
		paint: {
		  "circle-radius": 5,
		  "circle-color": "#e60000"
		}
	});
});

Layer später dynamisch hinzufügen/entfernen

// GeoJSON-Layer hinzufügen oder aktualisieren
function addGeoJsonLayerToMap(map, options) {
	const {
		sourceId,
		layerId,
		data,
		type = "circle",
		paint = {},
		layout = {},
		filter = null,
		beforeId = null,
		onClick = null
	} = options;
 
	if (map.getSource(sourceId)) {
		map.getSource(sourceId).setData(data);
	} else {
		map.addSource(sourceId, {
			type: "geojson",
			data: data
		});
	}
 
	if (!map.getLayer(layerId)) {
		const layerConfig = {
			id: layerId,
			type: type,
			source: sourceId,
			paint: paint,
			layout: layout
		};
 
		if (filter) {
			layerConfig.filter = filter;
		}
 
		if (beforeId && map.getLayer(beforeId)) {
			map.addLayer(layerConfig, beforeId);
		} else {
			map.addLayer(layerConfig);
		}
 
		if (onClick) {
			map.on("click", layerId, (e) => {
				if (!e.features || !e.features.length) return;
				onClick(e.features[0], e);
			});
 
			map.on("mouseenter", layerId, () => {
				map.getCanvas().style.cursor = "pointer";
			});
 
			map.on("mouseleave", layerId, () => {
				map.getCanvas().style.cursor = "";
			});
		}
	}
}
 
// GeoJSON-Layer entfernen
function removeGeoJsonLayerFromMap(map, layerId, sourceId) {
	if (map.getLayer(layerId)) {
		map.removeLayer(layerId);
	}
 
	if (map.getSource(sourceId)) {
		map.removeSource(sourceId);
	}
}

Beispiele

addGeoJsonLayerToMap(map, {
    sourceId: "pt-source",
    layerId: "pt-layer",
    data: "pt.geojson",
    type: "circle",
    paint: {
        "circle-radius": 6,
        "circle-color": "#ff0000"
    },
    onClick: (feature, e) => {
        new maplibregl.Popup()
            .setLngLat(e.lngLat)
            .setHTML(`
        <b>${feature.properties.name || "Ohne Name"}</b><br>
        ID: ${feature.properties.id || "-"}
      `)
            .addTo(map);
    }
});
 
addGeoJsonLayerToMap(map, {
    sourceId: "pt-source",
    layerId: "pt-layer",
    data: "pt.geojson",
    type: "circle",
    paint: {
        "circle-radius": 6,
        "circle-color": "#ff0000"
    },
    onClick: (feature, e) => {
        new maplibregl.Popup()
            .setLngLat(e.lngLat)
            .setHTML(`
        <b>${feature.properties.name || "Ohne Name"}</b><br>
        ID: ${feature.properties.id || "-"}
      `)
            .addTo(map);
    }
});
 
removeGeoJsonLayer(map, "pt-layer", "pt-source");

GeoJSON von URL nachladen

function loadExternalGeoJson(url, idBase, type) {
    addGeoJsonLayer(map, {
        sourceId: `${idBase}-source`,
        layerId: `${idBase}-layer`,
        data: url,
        type: type,
        paint: type === "circle" ?
            {
                "circle-radius": 5,
                "circle-color": "#ff0000"
            } :
            type === "line" ?
            {
                "line-color": "#0000ff",
                "line-width": 3
            } :
            {
                "fill-color": "#00aa00",
                "fill-opacity": 0.4
            }
    });
}
 
loadExternalGeoJson("radwege.geojson", "radwege", "line");
loadExternalGeoJson("orte.geojson", "orte", "circle");

OEPNV

osmium tags-filter wien.osm.pbf \
  n/highway=bus_stop \
  r/route=bus \
  w/highway \
  -o bus-relevant.osm.pbf -O
 
osmium tags-filter wien.osm.pbf \
  n/railway=tram_stop \
  r/route=tram \
  w/railway=tram \
  -o tram-relevant.osm.pbf -O
 
osmium tags-filter wien.osm.pbf \
  n/station=subway \
  n/railway=station \
  r/route=subway \
  w/railway=subway \
  -o subway-relevant.osm.pbf -O
 
osmium tags-filter wien.osm.pbf \
  n/railway=station \
  n/railway=halt \
  r/route=train \
  w/railway \
  -o train-relevant.osm.pbf -O
#!/usr/bin/env python3
import json
import subprocess
import sys
import xml.etree.ElementTree as ET
from collections import defaultdict
 
 
def fail(msg, code=1):
    print(msg, file=sys.stderr)
    sys.exit(code)
 
 
def dedupe_dicts(items, key_fn):
    seen = set()
    out = []
    for item in items:
        key = key_fn(item)
        if key in seen:
            continue
        seen.add(key)
        out.append(item)
    return out
 
 
def sort_key(value):
    return (value or "").strip().lower()
 
 
if len(sys.argv) != 5:
    fail(
        "Aufruf: python3 osm_pt_data_to_geojson.py "
        "input.osm.pbf stops.geojson routes.geojson route_type"
    )
 
input_pbf = sys.argv[1]
stops_geojson_path = sys.argv[2]
routes_geojson_path = sys.argv[3]
route_type = sys.argv[4].strip().lower()
 
allowed_route_types = {"bus", "tram", "subway", "train"}
if route_type not in allowed_route_types:
    fail("route_type muss einer von: bus, tram, subway, train sein.")
 
 
def is_stop(tags, current_route_type):
    """
    Konservative Stop-Erkennung, damit Modi sich weniger vermischen.
    """
    highway = tags.get("highway")
    railway = tags.get("railway")
    station = tags.get("station")
    public_transport = tags.get("public_transport")
    bus = tags.get("bus")
    tram = tags.get("tram")
    subway = tags.get("subway")
    train = tags.get("train")
 
    if current_route_type == "bus":
        # Nur echte Bus-Haltestellen.
        # Kein generisches public_transport=platform mehr.
        return highway == "bus_stop" or bus == "yes"
 
    if current_route_type == "tram":
        # Nur echte Tram-Haltestellen.
        return railway == "tram_stop" or tram == "yes"
 
    if current_route_type == "subway":
        # U-Bahn-Stationen enger fassen.
        return (
            station == "subway"
            or subway == "yes"
            or (railway in {"station", "halt"} and public_transport == "station" and station == "subway")
        )
 
    if current_route_type == "train":
        # Zugstationen, aber keine U-Bahn.
        return (
            (railway in {"station", "halt"} and station != "subway" and subway != "yes")
            or train == "yes"
        )
 
    return False
 
 
try:
    proc = subprocess.Popen(
        ["osmium", "cat", input_pbf, "-f", "osm"],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        encoding="utf-8",
    )
except FileNotFoundError:
    fail("Fehler: 'osmium' wurde nicht gefunden.")
 
if proc.stdout is None or proc.stderr is None:
    fail("Fehler: Konnte osmium-Streams nicht öffnen.")
 
nodes = {}
ways = {}
stops = {}
route_relations = []
 
try:
    for _, elem in ET.iterparse(proc.stdout, events=("end",)):
        if elem.tag == "node":
            node_id = elem.get("id")
            lat = elem.get("lat")
            lon = elem.get("lon")
 
            if node_id and lat and lon:
                tags = {tag.get("k"): tag.get("v") for tag in elem.findall("tag")}
                node_data = {
                    "id": node_id,
                    "lat": float(lat),
                    "lon": float(lon),
                    "tags": tags,
                }
                nodes[node_id] = node_data
 
                if is_stop(tags, route_type):
                    stops[node_id] = node_data
 
            elem.clear()
 
        elif elem.tag == "way":
            way_id = elem.get("id")
            if way_id:
                nd_refs = [nd.get("ref") for nd in elem.findall("nd") if nd.get("ref")]
                tags = {tag.get("k"): tag.get("v") for tag in elem.findall("tag")}
                ways[way_id] = {
                    "id": way_id,
                    "nd_refs": nd_refs,
                    "tags": tags,
                }
 
            elem.clear()
 
        elif elem.tag == "relation":
            rel_tags = {tag.get("k"): tag.get("v") for tag in elem.findall("tag")}
            if rel_tags.get("route") == route_type:
                members = []
                for member in elem.findall("member"):
                    members.append({
                        "type": member.get("type"),
                        "ref": member.get("ref"),
                        "role": member.get("role", ""),
                    })
 
                route_relations.append({
                    "id": elem.get("id"),
                    "tags": rel_tags,
                    "members": members,
                })
 
            elem.clear()
 
except ET.ParseError as e:
    fail(f"XML Parse Error: {e}")
 
stderr_text = proc.stderr.read()
return_code = proc.wait()
 
if return_code != 0:
    fail(f"Fehler bei 'osmium cat':\n{stderr_text}")
 
if not nodes and not ways and not route_relations:
    fail("Keine OSM-Objekte gefunden. Ist die Eingabedatei korrekt?")
 
routes_by_stop = defaultdict(list)
route_features = []
 
for rel in route_relations:
    rel_id = rel["id"]
    rel_tags = rel["tags"]
 
    route_info = {
        "relation_id": rel_id,
        "ref": rel_tags.get("ref"),
        "name": rel_tags.get("name"),
        "from": rel_tags.get("from"),
        "to": rel_tags.get("to"),
        "operator": rel_tags.get("operator"),
        "network": rel_tags.get("network"),
        "route_type": route_type,
    }
 
    multiline_coords = []
 
    for member in rel["members"]:
        member_type = member.get("type")
        member_ref = member.get("ref")
        member_role = member.get("role", "")
 
        if member_type == "node":
            if member_ref in stops:
                entry = dict(route_info)
                entry["member_role"] = member_role
                routes_by_stop[member_ref].append(entry)
 
        elif member_type == "way":
            way = ways.get(member_ref)
            if not way:
                continue
 
            coords = []
            for nd_ref in way["nd_refs"]:
                node = nodes.get(nd_ref)
                if not node:
                    continue
                coords.append([node["lon"], node["lat"]])
 
            if len(coords) >= 2:
                multiline_coords.append(coords)
 
    if multiline_coords:
        route_features.append({
            "type": "Feature",
            "geometry": {
                "type": "MultiLineString",
                "coordinates": multiline_coords,
            },
            "properties": {
                "osm_type": "relation",
                "osm_id": rel_id,
                **route_info,
            },
        })
 
stop_features = []
 
for stop_id, stop in stops.items():
    routes = routes_by_stop.get(stop_id, [])
 
    routes = dedupe_dicts(
        routes,
        lambda r: (
            r.get("relation_id"),
            r.get("ref"),
            r.get("name"),
            r.get("from"),
            r.get("to"),
            r.get("member_role"),
        ),
    )
 
    grouped = defaultdict(list)
    for route in routes:
        ref = (route.get("ref") or "").strip()
        if not ref:
            # Falls keine ref da ist, über relation_id gruppieren
            ref = f"rel-{route.get('relation_id')}"
        grouped[ref].append(route)
 
    routes_unique = []
    for ref, variants in grouped.items():
        variants = dedupe_dicts(
            variants,
            lambda r: (
                r.get("relation_id"),
                r.get("from"),
                r.get("to"),
                r.get("name"),
            ),
        )
 
        routes_unique.append({
            "ref": ref,
            "name": next((v.get("name") for v in variants if v.get("name")), ""),
            "operator": next((v.get("operator") for v in variants if v.get("operator")), ""),
            "network": next((v.get("network") for v in variants if v.get("network")), ""),
            "variants": [
                {
                    "relation_id": v.get("relation_id"),
                    "from": v.get("from"),
                    "to": v.get("to"),
                    "name": v.get("name"),
                }
                for v in variants
            ],
        })
 
    routes_unique.sort(key=lambda r: sort_key(r.get("ref")))
 
    properties = dict(stop["tags"])
    properties["osm_id"] = stop["id"]
    properties["osm_type"] = "node"
    properties["route_type"] = route_type
    properties["route_refs"] = [r["ref"] for r in routes_unique]
    properties["routes"] = routes_unique
    properties["route_count"] = len(routes_unique)
 
    stop_features.append({
        "type": "Feature",
        "geometry": {
            "type": "Point",
            "coordinates": [stop["lon"], stop["lat"]],
        },
        "properties": properties,
    })
 
stops_geojson = {
    "type": "FeatureCollection",
    "features": stop_features,
}
 
routes_geojson = {
    "type": "FeatureCollection",
    "features": route_features,
}
 
try:
    with open(stops_geojson_path, "w", encoding="utf-8") as f:
        json.dump(stops_geojson, f, ensure_ascii=False, indent=2)
 
    with open(routes_geojson_path, "w", encoding="utf-8") as f:
        json.dump(routes_geojson, f, ensure_ascii=False, indent=2)
 
except OSError as e:
    fail(f"Fehler beim Schreiben der Ausgabedateien: {e}")
 
stops_with_routes = sum(
    1 for feature in stop_features if feature["properties"].get("route_count", 0) > 0
)
 
print(f"Stops geschrieben: {stops_geojson_path}")
print(f"Routen geschrieben: {routes_geojson_path}")
print(f"Typ: {route_type}")
print(f"Haltestellen: {len(stop_features)}")
print(f"Haltestellen mit Routeninfo: {stops_with_routes}")
print(f"Routen-Geometrien: {len(route_features)}")
python3 osm_pt_data_to_geojson.py bus-relevant.osm.pbf bus-stops.geojson bus-routes.geojson bus
python3 osm_pt_data_to_geojson.py tram-relevant.osm.pbf tram-stops.geojson tram-routes.geojson tram
python3 osm_pt_data_to_geojson.py subway-relevant.osm.pbf subway-stops.geojson subway-routes.geojson subway
python3 osm_pt_data_to_geojson.py train-relevant.osm.pbf train-stops.geojson train-routes.geojson train