Dies ist eine alte Version des Dokuments!
Inhaltsverzeichnis
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