MapLibre GL ist ein Open-Source Fork von MapBox GL. =====Minimalbeispiel===== ====Library einbinden==== ====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("Hallo Wien") ) .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(` ${feature.properties.name || "Ohne Name"}
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(` ${feature.properties.name || "Ohne Name"}
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===== Siehe auch [[GTFS]] ====V1==== ===Data Export=== 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 ===Python script=== #!/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)}") ===Datenaufbereitung=== 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 ===Anzeigen=== Wien Öffis
Öffi-Stationen
Bus
Tram
U-Bahn
Zug
====V2==== Öffi Router ===Data Export=== osmium tags-filter wien.osm.pbf r/route=bus -o bus-routes-only.osm.pbf -O osmium cat bus-routes-only.osm.pbf -f opl | awk -F' ' '$1 ~ /^r/ {print $1}' > bus-route-ids.txt osmium getid -r wien.osm.pbf $(cat bus-route-ids.txt) -o bus-complete.osm.pbf -O osmium tags-filter wien.osm.pbf r/route=tram -o tram-routes-only.osm.pbf -O osmium cat tram-routes-only.osm.pbf -f opl | awk -F' ' '$1 ~ /^r/ {print $1}' > tram-route-ids.txt osmium getid -r wien.osm.pbf $(cat tram-route-ids.txt) -o tram-complete.osm.pbf -O osmium tags-filter wien.osm.pbf r/route=subway -o subway-routes-only.osm.pbf -O osmium cat subway-routes-only.osm.pbf -f opl | awk -F' ' '$1 ~ /^r/ {print $1}' > subway-route-ids.txt osmium getid -r wien.osm.pbf $(cat subway-route-ids.txt) -o subway-complete.osm.pbf -O osmium tags-filter wien.osm.pbf r/route=train -o train-routes-only.osm.pbf -O osmium cat train-routes-only.osm.pbf -f opl | awk -F' ' '$1 ~ /^r/ {print $1}' > train-route-ids.txt osmium getid -r wien.osm.pbf $(cat train-route-ids.txt) -o train-complete.osm.pbf -O ===Python script=== #!/usr/bin/env python3 import json import math import re 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 haversine_m(lat1, lon1, lat2, lon2): r = 6371000.0 p1 = math.radians(lat1) p2 = math.radians(lat2) dp = math.radians(lat2 - lat1) dl = math.radians(lon2 - lon1) a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 return 2 * r * math.asin(math.sqrt(a)) def normalize_name(name): name = (name or "").strip().lower() name = name.replace("wien ", "") name = name.replace("/", " ") name = name.replace("-", " ") name = re.sub(r"[()\,\.]", " ", name) name = re.sub(r"\s+", " ", name).strip() return name def display_name(tags, fallback_id): return ( tags.get("name") or tags.get("local_ref") or tags.get("ref") or f"stop-{fallback_id}" ) def is_station_only(tags): public_transport = tags.get("public_transport") railway = tags.get("railway") station = tags.get("station") if public_transport == "station": return True if railway == "station": return True if station == "subway": return True return False def is_real_stop_position(tags): return tags.get("public_transport") == "stop_position" def is_real_platform(tags, route_type): public_transport = tags.get("public_transport") highway = tags.get("highway") railway = tags.get("railway") if public_transport == "platform": return True if route_type == "bus" and highway == "bus_stop": return True if route_type == "tram" and railway == "tram_stop": return True return False def classify_member(member, node, route_type): role = (member.get("role") or "").strip().lower() tags = node["tags"] if is_station_only(tags): return None if role in {"stop", "stop_entry_only", "stop_exit_only"}: if is_real_stop_position(tags): return "stop" # wenn Rolle stop ist, aber kein station-only Objekt, trotzdem akzeptieren return "stop" if is_real_stop_position(tags): return "stop" if role in {"platform", "platform_entry_only", "platform_exit_only"}: if is_real_platform(tags, route_type): return "platform" if is_real_platform(tags, route_type): return "platform" return None def extract_relation_stop_sequence(rel, nodes, route_type): raw = [] for idx, member in enumerate(rel["members"]): if member.get("type") != "node": continue ref = member.get("ref") node = nodes.get(ref) if not node: continue kind = classify_member(member, node, route_type) if kind is None: continue tags = node["tags"] name = display_name(tags, ref) norm = normalize_name(name) if not norm: continue raw.append({ "node_id": ref, "name": name, "normalized_name": norm, "lat": node["lat"], "lon": node["lon"], "kind": kind, "member_index": idx, }) if not raw: return [] grouped = [] current_group = [raw[0]] for item in raw[1:]: if item["normalized_name"] == current_group[-1]["normalized_name"]: current_group.append(item) else: grouped.append(current_group) current_group = [item] grouped.append(current_group) cleaned = [] for group in grouped: stop_candidates = [g for g in group if g["kind"] == "stop"] chosen = stop_candidates[0] if stop_candidates else group[0] cleaned.append(chosen) final = [] for item in cleaned: if final and final[-1]["normalized_name"] == item["normalized_name"]: continue final.append(item) return final def build_spatial_index(stations, cell_size_deg=0.003): grid = defaultdict(list) for sid, st in stations.items(): key = (int(st["lon"] / cell_size_deg), int(st["lat"] / cell_size_deg)) grid[key].append(sid) return grid def nearby_station_pairs(stations, max_transfer_m=120): grid = build_spatial_index(stations) cell_size_deg = 0.003 seen = set() for sid, st in stations.items(): gx = int(st["lon"] / cell_size_deg) gy = int(st["lat"] / cell_size_deg) for dx in (-1, 0, 1): for dy in (-1, 0, 1): for other_sid in grid.get((gx + dx, gy + dy), []): if other_sid == sid: continue a, b = sorted((sid, other_sid)) if (a, b) in seen: continue seen.add((a, b)) sa = stations[a] sb = stations[b] dist = haversine_m(sa["lat"], sa["lon"], sb["lat"], sb["lon"]) if dist <= max_transfer_m: yield a, b, dist args = sys.argv[1:] if len(args) < 3: fail( "Aufruf: python3 osm_route_relations_to_station_graph.py " "input.osm.pbf graph.json route_type [--transfer=120]" ) input_pbf = args[0] graph_json_path = args[1] route_type = args[2].strip().lower() max_transfer_m = 120.0 for arg in args[3:]: if arg.startswith("--transfer="): max_transfer_m = float(arg.split("=", 1)[1]) else: fail(f"Unbekanntes Argument: {arg}") if route_type not in {"bus", "tram", "subway", "train"}: fail("route_type muss einer von: bus, tram, subway, train sein.") 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 = {} 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")} nodes[node_id] = { "id": node_id, "lat": float(lat), "lon": float(lon), "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}") stations = {} station_members = defaultdict(list) edges = [] route_variants = [] skipped_night_buses = 0 for rel in route_relations: rel_id = rel["id"] rel_tags = rel["tags"] ref = (rel_tags.get("ref") or "").strip().upper() if route_type == "bus" and ref.startswith("N"): skipped_night_buses += 1 continue stop_seq = extract_relation_stop_sequence(rel, nodes, route_type) if len(stop_seq) < 2: continue ordered_station_ids = [] for item in stop_seq: sid = f"station:{item['normalized_name']}" ordered_station_ids.append(sid) if sid not in stations: stations[sid] = { "id": sid, "type": "station", "name": item["name"], "normalized_name": item["normalized_name"], "lat": item["lat"], "lon": item["lon"], } station_members[sid].append(item) route_meta = { "relation_id": rel_id, "ref": rel_tags.get("ref"), "route_name": rel_tags.get("name"), "route_from": rel_tags.get("from"), "route_to": rel_tags.get("to"), "operator": rel_tags.get("operator"), "network": rel_tags.get("network"), "route_type": route_type, } for i in range(len(ordered_station_ids) - 1): a = ordered_station_ids[i] b = ordered_station_ids[i + 1] if a == b: continue sa = stations[a] sb = stations[b] dist = haversine_m(sa["lat"], sa["lon"], sb["lat"], sb["lon"]) edges.append({ "id": f"ride:{rel_id}:{i}", "type": "ride", "from": a, "to": b, "distance_m": round(dist, 1), "weight": round(dist, 1), "from_stop_name": sa["name"], "to_stop_name": sb["name"], **route_meta, }) edges.append({ "id": f"ride:{rel_id}:{i}:rev", "type": "ride", "from": b, "to": a, "distance_m": round(dist, 1), "weight": round(dist, 1), "from_stop_name": sb["name"], "to_stop_name": sa["name"], **route_meta, "is_reverse_edge": True, }) route_variants.append({ "relation_id": rel_id, "ref": rel_tags.get("ref"), "route_name": rel_tags.get("name"), "route_from": rel_tags.get("from"), "route_to": rel_tags.get("to"), "route_type": route_type, "ordered_station_ids": ordered_station_ids, "ordered_station_names": [s["name"] for s in stop_seq], "ordered_stop_kinds": [s["kind"] for s in stop_seq], }) for sid, members in station_members.items(): if members: stations[sid]["lat"] = sum(m["lat"] for m in members) / len(members) stations[sid]["lon"] = sum(m["lon"] for m in members) / len(members) for e in edges: if e["type"] != "ride": continue a = stations[e["from"]] b = stations[e["to"]] dist = haversine_m(a["lat"], a["lon"], b["lat"], b["lon"]) e["distance_m"] = round(dist, 1) e["weight"] = round(dist, 1) for a, b, dist in nearby_station_pairs(stations, max_transfer_m=max_transfer_m): sa = stations[a] sb = stations[b] edges.append({ "id": f"transfer:{a}:{b}", "type": "transfer", "from": a, "to": b, "distance_m": round(dist, 1), "weight": round(dist, 1), "from_stop_name": sa["name"], "to_stop_name": sb["name"], "route_type": "transfer", "transfer_kind": "nearby_local", }) edges.append({ "id": f"transfer:{b}:{a}", "type": "transfer", "from": b, "to": a, "distance_m": round(dist, 1), "weight": round(dist, 1), "from_stop_name": sb["name"], "to_stop_name": sa["name"], "route_type": "transfer", "transfer_kind": "nearby_local", }) # Plausibilitätscheck, damit kaputte Läufe sofort abbrechen if route_type == "bus" and len(stations) < 500: fail(f"Bus-Export unplausibel: nur {len(stations)} Stationen. Datei/Script ist kaputt.") if route_type == "tram" and len(stations) < 100: fail(f"Tram-Export unplausibel: nur {len(stations)} Stationen. Datei/Script ist kaputt.") if route_type == "subway" and len(stations) < 20: fail(f"Subway-Export unplausibel: nur {len(stations)} Stationen. Datei/Script ist kaputt.") if route_type == "train" and len(stations) < 20: fail(f"Train-Export unplausibel: nur {len(stations)} Stationen. Datei/Script ist kaputt.") graph = { "type": "TransitGraph", "route_type": route_type, "nodes": list(stations.values()), "edges": edges, "route_variants": route_variants, "transfer_radius_m": max_transfer_m, } with open(graph_json_path, "w", encoding="utf-8") as f: json.dump(graph, f, ensure_ascii=False, indent=2) print(f"Graph geschrieben: {graph_json_path}") print(f"Typ: {route_type}") print(f"Route-Relationen: {len(route_relations)}") print(f"Übersprungene Nachtbusse: {skipped_night_buses}") print(f"Stationen: {len(stations)}") print(f"Kanten: {len(edges)}") print(f"Linienvarianten: {len(route_variants)}") ===Datenaufbereitung=== python3 osm_route_relations_to_station_graph.py bus-complete.osm.pbf bus-station-graph.json bus --transfer=120 python3 osm_route_relations_to_station_graph.py tram-complete.osm.pbf tram-station-graph.json tram --transfer=120 python3 osm_route_relations_to_station_graph.py subway-complete.osm.pbf subway-station-graph.json subway --transfer=120 python3 osm_route_relations_to_station_graph.py train-complete.osm.pbf train-station-graph.json train --transfer=120 python3 merge_station_graphs.py merged-station-graph.json \ bus-station-graph.json tram-station-graph.json subway-station-graph.json train-station-graph.json \ --global-transfer=150 ===Test=== python3 route_station_transit.py \ merged-station-graph.json \ 16.328566,48.169252 \ 16.387568,48.203825 \ route-result.json =====Links===== * [[https://dev.to/geoapify-maps-api/how-to-visualize-and-style-routes-on-a-maplibre-gl-map-416g|Visual and style routes on a map]]