Zuletzt angesehen:

Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen angezeigt.

Link zu dieser Vergleichsansicht

Beide Seiten der vorigen Revision Vorhergehende Überarbeitung
Nächste Überarbeitung
Vorhergehende Überarbeitung
maplibre [2026/04/02 22:50]
admin [OEPNV]
maplibre [2026/04/05 05:11] (aktuell)
jango
Zeile 1: Zeile 1:
 +MapLibre GL ist ein Open-Source Fork von MapBox GL.
 +
 =====Minimalbeispiel===== =====Minimalbeispiel=====
  
Zeile 315: Zeile 317:
 =====OEPNV===== =====OEPNV=====
  
 +Siehe auch [[GTFS]]
 +
 +====V1====
 +===Data Export===
 <code bash> <code bash>
 osmium tags-filter wien.osm.pbf \ osmium tags-filter wien.osm.pbf \
Zeile 343: Zeile 349:
 </code> </code>
  
 +===Python script===
 <code python> <code python>
 #!/usr/bin/env python3 #!/usr/bin/env python3
Zeile 678: Zeile 685:
 </code> </code>
  
 +===Datenaufbereitung===
 <code bash> <code bash>
 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 bus-relevant.osm.pbf bus-stops.geojson bus-routes.geojson bus
Zeile 685: Zeile 693:
 </code> </code>
  
 +===Anzeigen===
 <code html> <code html>
 <!doctype html> <!doctype html>
Zeile 1401: Zeile 1410:
 </html> </html>
 </code> </code>
 +
 +====V2====
 +
 +Öffi Router
 +
 +===Data Export===
 +<code bash>
 +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
 +</code>
 +
 +===Python script===
 +<code python>
 +#!/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)}")
 +</code>
 +
 +===Datenaufbereitung===
 +<code>
 +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
 +</code>
 +
 +<code>
 +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
 +</code>
 +
 +===Test===
 +<code>
 +python3 route_station_transit.py \
 +  merged-station-graph.json \
 +  16.328566,48.169252 \
 +  16.387568,48.203825 \
 +  route-result.json
 +</code>
 +
 +=====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]]