Zuletzt angesehen:
Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
| 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: | ||
| </ | </ | ||
| + | ===Python script=== | ||
| <code python> | <code python> | ||
| # | # | ||
| Zeile 678: | Zeile 685: | ||
| </ | </ | ||
| + | ===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: | ||
| </ | </ | ||
| + | ===Anzeigen=== | ||
| <code html> | <code html> | ||
| < | < | ||
| Zeile 1401: | Zeile 1410: | ||
| </ | </ | ||
| </ | </ | ||
| + | |||
| + | ====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/ | ||
| + | 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/ | ||
| + | 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/ | ||
| + | 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=== | ||
| + | <code python> | ||
| + | # | ||
| + | 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, | ||
| + | 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 "" | ||
| + | name = name.replace(" | ||
| + | name = name.replace("/", | ||
| + | name = name.replace(" | ||
| + | name = re.sub(r" | ||
| + | name = re.sub(r" | ||
| + | return name | ||
| + | |||
| + | |||
| + | def display_name(tags, | ||
| + | return ( | ||
| + | tags.get(" | ||
| + | or tags.get(" | ||
| + | or tags.get(" | ||
| + | or f" | ||
| + | ) | ||
| + | |||
| + | |||
| + | def is_station_only(tags): | ||
| + | public_transport = tags.get(" | ||
| + | railway = tags.get(" | ||
| + | station = tags.get(" | ||
| + | |||
| + | if public_transport == " | ||
| + | return True | ||
| + | if railway == " | ||
| + | return True | ||
| + | if station == " | ||
| + | return True | ||
| + | return False | ||
| + | |||
| + | |||
| + | def is_real_stop_position(tags): | ||
| + | return tags.get(" | ||
| + | |||
| + | |||
| + | def is_real_platform(tags, | ||
| + | public_transport = tags.get(" | ||
| + | highway = tags.get(" | ||
| + | railway = tags.get(" | ||
| + | |||
| + | if public_transport == " | ||
| + | return True | ||
| + | if route_type == " | ||
| + | return True | ||
| + | if route_type == " | ||
| + | return True | ||
| + | return False | ||
| + | |||
| + | |||
| + | def classify_member(member, | ||
| + | role = (member.get(" | ||
| + | tags = node[" | ||
| + | |||
| + | if is_station_only(tags): | ||
| + | return None | ||
| + | |||
| + | if role in {" | ||
| + | if is_real_stop_position(tags): | ||
| + | return " | ||
| + | # wenn Rolle stop ist, aber kein station-only Objekt, trotzdem akzeptieren | ||
| + | return " | ||
| + | |||
| + | if is_real_stop_position(tags): | ||
| + | return " | ||
| + | |||
| + | if role in {" | ||
| + | if is_real_platform(tags, | ||
| + | return " | ||
| + | |||
| + | if is_real_platform(tags, | ||
| + | return " | ||
| + | |||
| + | return None | ||
| + | |||
| + | |||
| + | def extract_relation_stop_sequence(rel, | ||
| + | raw = [] | ||
| + | |||
| + | for idx, member in enumerate(rel[" | ||
| + | if member.get(" | ||
| + | continue | ||
| + | |||
| + | ref = member.get(" | ||
| + | node = nodes.get(ref) | ||
| + | if not node: | ||
| + | continue | ||
| + | |||
| + | kind = classify_member(member, | ||
| + | if kind is None: | ||
| + | continue | ||
| + | |||
| + | tags = node[" | ||
| + | name = display_name(tags, | ||
| + | norm = normalize_name(name) | ||
| + | if not norm: | ||
| + | continue | ||
| + | |||
| + | raw.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }) | ||
| + | |||
| + | if not raw: | ||
| + | return [] | ||
| + | |||
| + | grouped = [] | ||
| + | current_group = [raw[0]] | ||
| + | |||
| + | for item in raw[1:]: | ||
| + | if item[" | ||
| + | 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[" | ||
| + | chosen = stop_candidates[0] if stop_candidates else group[0] | ||
| + | cleaned.append(chosen) | ||
| + | |||
| + | final = [] | ||
| + | for item in cleaned: | ||
| + | if final and final[-1][" | ||
| + | continue | ||
| + | final.append(item) | ||
| + | |||
| + | return final | ||
| + | |||
| + | |||
| + | def build_spatial_index(stations, | ||
| + | grid = defaultdict(list) | ||
| + | for sid, st in stations.items(): | ||
| + | key = (int(st[" | ||
| + | grid[key].append(sid) | ||
| + | return grid | ||
| + | |||
| + | |||
| + | def nearby_station_pairs(stations, | ||
| + | grid = build_spatial_index(stations) | ||
| + | cell_size_deg = 0.003 | ||
| + | seen = set() | ||
| + | |||
| + | for sid, st in stations.items(): | ||
| + | gx = int(st[" | ||
| + | gy = int(st[" | ||
| + | |||
| + | 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, | ||
| + | if (a, b) in seen: | ||
| + | continue | ||
| + | seen.add((a, | ||
| + | |||
| + | sa = stations[a] | ||
| + | sb = stations[b] | ||
| + | dist = haversine_m(sa[" | ||
| + | if dist <= max_transfer_m: | ||
| + | yield a, b, dist | ||
| + | |||
| + | |||
| + | args = sys.argv[1: | ||
| + | if len(args) < 3: | ||
| + | fail( | ||
| + | " | ||
| + | " | ||
| + | ) | ||
| + | |||
| + | 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(" | ||
| + | max_transfer_m = float(arg.split(" | ||
| + | else: | ||
| + | fail(f" | ||
| + | |||
| + | if route_type not in {" | ||
| + | fail(" | ||
| + | |||
| + | try: | ||
| + | proc = subprocess.Popen( | ||
| + | [" | ||
| + | stdout=subprocess.PIPE, | ||
| + | stderr=subprocess.PIPE, | ||
| + | text=True, | ||
| + | encoding=" | ||
| + | ) | ||
| + | except FileNotFoundError: | ||
| + | fail(" | ||
| + | |||
| + | if proc.stdout is None or proc.stderr is None: | ||
| + | fail(" | ||
| + | |||
| + | nodes = {} | ||
| + | route_relations = [] | ||
| + | |||
| + | try: | ||
| + | for _, elem in ET.iterparse(proc.stdout, | ||
| + | if elem.tag == " | ||
| + | node_id = elem.get(" | ||
| + | lat = elem.get(" | ||
| + | lon = elem.get(" | ||
| + | if node_id and lat and lon: | ||
| + | tags = {tag.get(" | ||
| + | nodes[node_id] = { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | elem.clear() | ||
| + | |||
| + | elif elem.tag == " | ||
| + | rel_tags = {tag.get(" | ||
| + | if rel_tags.get(" | ||
| + | members = [] | ||
| + | for member in elem.findall(" | ||
| + | members.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }) | ||
| + | route_relations.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }) | ||
| + | elem.clear() | ||
| + | |||
| + | except ET.ParseError as e: | ||
| + | fail(f" | ||
| + | |||
| + | stderr_text = proc.stderr.read() | ||
| + | return_code = proc.wait() | ||
| + | if return_code != 0: | ||
| + | fail(f" | ||
| + | |||
| + | stations = {} | ||
| + | station_members = defaultdict(list) | ||
| + | edges = [] | ||
| + | route_variants = [] | ||
| + | |||
| + | skipped_night_buses = 0 | ||
| + | |||
| + | for rel in route_relations: | ||
| + | rel_id = rel[" | ||
| + | rel_tags = rel[" | ||
| + | |||
| + | ref = (rel_tags.get(" | ||
| + | if route_type == " | ||
| + | skipped_night_buses += 1 | ||
| + | continue | ||
| + | |||
| + | stop_seq = extract_relation_stop_sequence(rel, | ||
| + | if len(stop_seq) < 2: | ||
| + | continue | ||
| + | |||
| + | ordered_station_ids = [] | ||
| + | |||
| + | for item in stop_seq: | ||
| + | sid = f" | ||
| + | ordered_station_ids.append(sid) | ||
| + | |||
| + | if sid not in stations: | ||
| + | stations[sid] = { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | station_members[sid].append(item) | ||
| + | |||
| + | route_meta = { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | 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[" | ||
| + | |||
| + | edges.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | **route_meta, | ||
| + | }) | ||
| + | |||
| + | edges.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | **route_meta, | ||
| + | " | ||
| + | }) | ||
| + | |||
| + | route_variants.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }) | ||
| + | |||
| + | for sid, members in station_members.items(): | ||
| + | if members: | ||
| + | stations[sid][" | ||
| + | stations[sid][" | ||
| + | |||
| + | for e in edges: | ||
| + | if e[" | ||
| + | continue | ||
| + | a = stations[e[" | ||
| + | b = stations[e[" | ||
| + | dist = haversine_m(a[" | ||
| + | e[" | ||
| + | e[" | ||
| + | |||
| + | for a, b, dist in nearby_station_pairs(stations, | ||
| + | sa = stations[a] | ||
| + | sb = stations[b] | ||
| + | |||
| + | edges.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }) | ||
| + | edges.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }) | ||
| + | |||
| + | # Plausibilitätscheck, | ||
| + | if route_type == " | ||
| + | fail(f" | ||
| + | if route_type == " | ||
| + | fail(f" | ||
| + | if route_type == " | ||
| + | fail(f" | ||
| + | if route_type == " | ||
| + | fail(f" | ||
| + | |||
| + | graph = { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | with open(graph_json_path, | ||
| + | json.dump(graph, | ||
| + | |||
| + | print(f" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | </ | ||
| + | |||
| + | ===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, | ||
| + | 16.387568, | ||
| + | route-result.json | ||
| + | </ | ||
| + | |||
| + | =====Links===== | ||
| + | |||
| + | * [[https:// | ||