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 14:42] admin [Marker] |
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 241: | Zeile 243: | ||
| Beispiele | Beispiele | ||
| <code javascript> | <code javascript> | ||
| + | addGeoJsonLayerToMap(map, | ||
| + | sourceId: " | ||
| + | layerId: " | ||
| + | data: " | ||
| + | type: " | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | onClick: (feature, e) => { | ||
| + | new maplibregl.Popup() | ||
| + | .setLngLat(e.lngLat) | ||
| + | .setHTML(` | ||
| + | < | ||
| + | ID: ${feature.properties.id || " | ||
| + | `) | ||
| + | .addTo(map); | ||
| + | } | ||
| + | }); | ||
| + | |||
| addGeoJsonLayerToMap(map, | addGeoJsonLayerToMap(map, | ||
| sourceId: " | sourceId: " | ||
| Zeile 292: | Zeile 314: | ||
| loadExternalGeoJson(" | loadExternalGeoJson(" | ||
| </ | </ | ||
| + | |||
| + | =====OEPNV===== | ||
| + | |||
| + | Siehe auch [[GTFS]] | ||
| + | |||
| + | ====V1==== | ||
| + | ===Data Export=== | ||
| + | <code bash> | ||
| + | osmium tags-filter wien.osm.pbf \ | ||
| + | n/ | ||
| + | r/route=bus \ | ||
| + | w/highway \ | ||
| + | -o bus-relevant.osm.pbf -O | ||
| + | |||
| + | osmium tags-filter wien.osm.pbf \ | ||
| + | n/ | ||
| + | r/ | ||
| + | w/ | ||
| + | -o tram-relevant.osm.pbf -O | ||
| + | |||
| + | osmium tags-filter wien.osm.pbf \ | ||
| + | n/ | ||
| + | n/ | ||
| + | r/ | ||
| + | w/ | ||
| + | -o subway-relevant.osm.pbf -O | ||
| + | |||
| + | osmium tags-filter wien.osm.pbf \ | ||
| + | n/ | ||
| + | n/ | ||
| + | r/ | ||
| + | w/railway \ | ||
| + | -o train-relevant.osm.pbf -O | ||
| + | </ | ||
| + | |||
| + | ===Python script=== | ||
| + | <code python> | ||
| + | # | ||
| + | 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, | ||
| + | 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 "" | ||
| + | |||
| + | |||
| + | if len(sys.argv) != 5: | ||
| + | fail( | ||
| + | " | ||
| + | " | ||
| + | ) | ||
| + | |||
| + | 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 = {" | ||
| + | if route_type not in allowed_route_types: | ||
| + | fail(" | ||
| + | |||
| + | |||
| + | def is_stop(tags, | ||
| + | """ | ||
| + | Konservative Stop-Erkennung, | ||
| + | """ | ||
| + | highway = tags.get(" | ||
| + | railway = tags.get(" | ||
| + | station = tags.get(" | ||
| + | public_transport = tags.get(" | ||
| + | bus = tags.get(" | ||
| + | tram = tags.get(" | ||
| + | subway = tags.get(" | ||
| + | train = tags.get(" | ||
| + | |||
| + | if current_route_type == " | ||
| + | # Nur echte Bus-Haltestellen. | ||
| + | # Kein generisches public_transport=platform mehr. | ||
| + | return highway == " | ||
| + | |||
| + | if current_route_type == " | ||
| + | # Nur echte Tram-Haltestellen. | ||
| + | return railway == " | ||
| + | |||
| + | if current_route_type == " | ||
| + | # U-Bahn-Stationen enger fassen. | ||
| + | return ( | ||
| + | station == " | ||
| + | or subway == " | ||
| + | or (railway in {" | ||
| + | ) | ||
| + | |||
| + | if current_route_type == " | ||
| + | # Zugstationen, | ||
| + | return ( | ||
| + | (railway in {" | ||
| + | or train == " | ||
| + | ) | ||
| + | |||
| + | return False | ||
| + | |||
| + | |||
| + | 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 = {} | ||
| + | ways = {} | ||
| + | stops = {} | ||
| + | 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(" | ||
| + | node_data = { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | nodes[node_id] = node_data | ||
| + | |||
| + | if is_stop(tags, | ||
| + | stops[node_id] = node_data | ||
| + | |||
| + | elem.clear() | ||
| + | |||
| + | elif elem.tag == " | ||
| + | way_id = elem.get(" | ||
| + | if way_id: | ||
| + | nd_refs = [nd.get(" | ||
| + | tags = {tag.get(" | ||
| + | ways[way_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" | ||
| + | |||
| + | if not nodes and not ways and not route_relations: | ||
| + | fail(" | ||
| + | |||
| + | routes_by_stop = defaultdict(list) | ||
| + | route_features = [] | ||
| + | |||
| + | for rel in route_relations: | ||
| + | rel_id = rel[" | ||
| + | rel_tags = rel[" | ||
| + | |||
| + | route_info = { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | multiline_coords = [] | ||
| + | |||
| + | for member in rel[" | ||
| + | member_type = member.get(" | ||
| + | member_ref = member.get(" | ||
| + | member_role = member.get(" | ||
| + | |||
| + | if member_type == " | ||
| + | if member_ref in stops: | ||
| + | entry = dict(route_info) | ||
| + | entry[" | ||
| + | routes_by_stop[member_ref].append(entry) | ||
| + | |||
| + | elif member_type == " | ||
| + | way = ways.get(member_ref) | ||
| + | if not way: | ||
| + | continue | ||
| + | |||
| + | coords = [] | ||
| + | for nd_ref in way[" | ||
| + | node = nodes.get(nd_ref) | ||
| + | if not node: | ||
| + | continue | ||
| + | coords.append([node[" | ||
| + | |||
| + | if len(coords) >= 2: | ||
| + | multiline_coords.append(coords) | ||
| + | |||
| + | if multiline_coords: | ||
| + | route_features.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | **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(" | ||
| + | r.get(" | ||
| + | r.get(" | ||
| + | r.get(" | ||
| + | r.get(" | ||
| + | r.get(" | ||
| + | ), | ||
| + | ) | ||
| + | |||
| + | grouped = defaultdict(list) | ||
| + | for route in routes: | ||
| + | ref = (route.get(" | ||
| + | if not ref: | ||
| + | # Falls keine ref da ist, über relation_id gruppieren | ||
| + | ref = f" | ||
| + | grouped[ref].append(route) | ||
| + | |||
| + | routes_unique = [] | ||
| + | for ref, variants in grouped.items(): | ||
| + | variants = dedupe_dicts( | ||
| + | variants, | ||
| + | lambda r: ( | ||
| + | r.get(" | ||
| + | r.get(" | ||
| + | r.get(" | ||
| + | r.get(" | ||
| + | ), | ||
| + | ) | ||
| + | |||
| + | routes_unique.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | for v in variants | ||
| + | ], | ||
| + | }) | ||
| + | |||
| + | routes_unique.sort(key=lambda r: sort_key(r.get(" | ||
| + | |||
| + | properties = dict(stop[" | ||
| + | properties[" | ||
| + | properties[" | ||
| + | properties[" | ||
| + | properties[" | ||
| + | properties[" | ||
| + | properties[" | ||
| + | |||
| + | stop_features.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | " | ||
| + | }) | ||
| + | |||
| + | stops_geojson = { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | routes_geojson = { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | |||
| + | try: | ||
| + | with open(stops_geojson_path, | ||
| + | json.dump(stops_geojson, | ||
| + | |||
| + | with open(routes_geojson_path, | ||
| + | json.dump(routes_geojson, | ||
| + | |||
| + | except OSError as e: | ||
| + | fail(f" | ||
| + | |||
| + | stops_with_routes = sum( | ||
| + | 1 for feature in stop_features if feature[" | ||
| + | ) | ||
| + | |||
| + | print(f" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | print(f" | ||
| + | </ | ||
| + | |||
| + | ===Datenaufbereitung=== | ||
| + | <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 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=== | ||
| + | <code html> | ||
| + | < | ||
| + | <html lang=" | ||
| + | < | ||
| + | <meta charset=" | ||
| + | <meta name=" | ||
| + | < | ||
| + | |||
| + | <link rel=" | ||
| + | <script src=" | ||
| + | <script src=" | ||
| + | |||
| + | < | ||
| + | html, body { | ||
| + | margin: 0; | ||
| + | padding: 0; | ||
| + | height: 100%; | ||
| + | font-family: | ||
| + | } | ||
| + | |||
| + | #map { | ||
| + | width: 100%; | ||
| + | height: 100vh; | ||
| + | } | ||
| + | |||
| + | .maplibregl-popup-content { | ||
| + | min-width: 280px; | ||
| + | max-width: 420px; | ||
| + | } | ||
| + | |||
| + | .popup-stop-name { | ||
| + | font-weight: | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | .popup-subtitle { | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | .route-list { | ||
| + | margin: 0; | ||
| + | padding-left: | ||
| + | } | ||
| + | |||
| + | .route-list li { | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | .route-link { | ||
| + | display: inline; | ||
| + | border: 0; | ||
| + | background: none; | ||
| + | padding: 0; | ||
| + | margin: 0; | ||
| + | font: inherit; | ||
| + | text-align: left; | ||
| + | cursor: pointer; | ||
| + | color: #0a58ca; | ||
| + | text-decoration: | ||
| + | } | ||
| + | |||
| + | .legend { | ||
| + | position: absolute; | ||
| + | top: 12px; | ||
| + | left: 12px; | ||
| + | z-index: 10; | ||
| + | background: rgba(255, | ||
| + | border-radius: | ||
| + | padding: 10px 12px; | ||
| + | box-shadow: 0 2px 10px rgba(0, | ||
| + | font-size: 14px; | ||
| + | line-height: | ||
| + | } | ||
| + | |||
| + | .legend-row { | ||
| + | display: flex; | ||
| + | align-items: | ||
| + | gap: 8px; | ||
| + | margin-top: 6px; | ||
| + | } | ||
| + | |||
| + | .legend-dot { | ||
| + | width: 10px; | ||
| + | height: 10px; | ||
| + | border-radius: | ||
| + | display: inline-block; | ||
| + | } | ||
| + | |||
| + | .dot-bus { background: #e30000; } | ||
| + | .dot-tram { background: #0066dd; } | ||
| + | .dot-subway { background: #009966; } | ||
| + | .dot-train { background: #7a3db8; } | ||
| + | </ | ||
| + | |||
| + | </ | ||
| + | < | ||
| + | |||
| + | <div id=" | ||
| + | |||
| + | <div class=" | ||
| + | < | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | </ | ||
| + | |||
| + | < | ||
| + | const protocol = new pmtiles.Protocol(); | ||
| + | maplibregl.addProtocol(" | ||
| + | |||
| + | const MODES = { | ||
| + | bus: { | ||
| + | label: " | ||
| + | color: "# | ||
| + | stopsUrl: " | ||
| + | routesUrl: " | ||
| + | stopSourceId: | ||
| + | stopLayerId: | ||
| + | routeHighlightSourceId: | ||
| + | routeHighlightLayerId: | ||
| + | }, | ||
| + | tram: { | ||
| + | label: " | ||
| + | color: "# | ||
| + | stopsUrl: " | ||
| + | routesUrl: " | ||
| + | stopSourceId: | ||
| + | stopLayerId: | ||
| + | routeHighlightSourceId: | ||
| + | routeHighlightLayerId: | ||
| + | }, | ||
| + | subway: { | ||
| + | label: " | ||
| + | color: "# | ||
| + | stopsUrl: " | ||
| + | routesUrl: " | ||
| + | stopSourceId: | ||
| + | stopLayerId: | ||
| + | routeHighlightSourceId: | ||
| + | routeHighlightLayerId: | ||
| + | }, | ||
| + | train: { | ||
| + | label: " | ||
| + | color: "# | ||
| + | stopsUrl: " | ||
| + | routesUrl: " | ||
| + | stopSourceId: | ||
| + | stopLayerId: | ||
| + | routeHighlightSourceId: | ||
| + | routeHighlightLayerId: | ||
| + | } | ||
| + | }; | ||
| + | |||
| + | let activePopup = null; | ||
| + | const routeDataByMode = {}; | ||
| + | |||
| + | const map = new maplibregl.Map({ | ||
| + | container: " | ||
| + | center: [16.3738, 48.2082], | ||
| + | zoom: 12, | ||
| + | style: { | ||
| + | version: 8, | ||
| + | sources: { | ||
| + | omt: { | ||
| + | type: " | ||
| + | url: " | ||
| + | attribution: | ||
| + | } | ||
| + | }, | ||
| + | layers: [ | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | paint: { " | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | [" | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | "# | ||
| + | ] | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | [" | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | "# | ||
| + | ], | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | filter: [" | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 5, 0.8, | ||
| + | 8, 1.2, | ||
| + | 10, 2.0, | ||
| + | 12, 3.5, | ||
| + | 14, 6.0, | ||
| + | 16, 10.0, | ||
| + | 18, 16.0 | ||
| + | ] | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | filter: [" | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 5, 0.5, | ||
| + | 8, 0.9, | ||
| + | 10, 1.5, | ||
| + | 12, 2.6, | ||
| + | 14, 4.2, | ||
| + | 16, 7.0, | ||
| + | 18, 11.0 | ||
| + | ] | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | filter: [" | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 5, 0.4, | ||
| + | 8, 0.7, | ||
| + | 10, 1.2, | ||
| + | 12, 2.0, | ||
| + | 14, 3.2, | ||
| + | 16, 5.5, | ||
| + | 18, 8.5 | ||
| + | ] | ||
| + | } | ||
| + | }, | ||
| + | { | ||
| + | id: " | ||
| + | type: " | ||
| + | source: " | ||
| + | " | ||
| + | filter: [ | ||
| + | " | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | [" | ||
| + | ], | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 5, 0.2, | ||
| + | 8, 0.4, | ||
| + | 10, 0.8, | ||
| + | 12, 1.3, | ||
| + | 14, 2.0, | ||
| + | 16, 3.2, | ||
| + | 18, 5.0 | ||
| + | ] | ||
| + | } | ||
| + | } | ||
| + | ] | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | map.addControl(new maplibregl.NavigationControl(), | ||
| + | |||
| + | function closeActivePopup() { | ||
| + | if (activePopup) { | ||
| + | activePopup.remove(); | ||
| + | activePopup = null; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function safeJsonParse(value, | ||
| + | if (value == null) return fallback; | ||
| + | if (typeof value !== " | ||
| + | try { | ||
| + | return JSON.parse(value); | ||
| + | } catch { | ||
| + | return fallback; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function normalizeRoutesFromStopProperties(properties) { | ||
| + | const rawRoutes = safeJsonParse(properties.routes, | ||
| + | if (!Array.isArray(rawRoutes)) return []; | ||
| + | |||
| + | const routes = rawRoutes.map(route => ({ | ||
| + | ref: String(route? | ||
| + | name: String(route? | ||
| + | variants: Array.isArray(route? | ||
| + | ? route.variants.map(v => ({ | ||
| + | relation_id: | ||
| + | from: String(v? | ||
| + | to: String(v? | ||
| + | name: String(v? | ||
| + | })) | ||
| + | : [] | ||
| + | })).filter(route => route.ref || route.variants.length); | ||
| + | |||
| + | routes.sort((a, | ||
| + | const ra = a.ref || ""; | ||
| + | const rb = b.ref || ""; | ||
| + | return ra.localeCompare(rb, | ||
| + | }); | ||
| + | |||
| + | return routes; | ||
| + | } | ||
| + | |||
| + | function clearAllHighlights() { | ||
| + | for (const mode of Object.values(MODES)) { | ||
| + | const source = map.getSource(mode.routeHighlightSourceId); | ||
| + | if (!source) continue; | ||
| + | |||
| + | source.setData({ | ||
| + | type: " | ||
| + | features: [] | ||
| + | }); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function fitToFeatures(features) { | ||
| + | if (!features.length) return; | ||
| + | |||
| + | const bounds = new maplibregl.LngLatBounds(); | ||
| + | let hasCoords = false; | ||
| + | |||
| + | function extendCoords(coords) { | ||
| + | for (const c of coords) { | ||
| + | if (Array.isArray(c[0])) { | ||
| + | extendCoords(c); | ||
| + | } else { | ||
| + | bounds.extend(c); | ||
| + | hasCoords = true; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | for (const feature of features) { | ||
| + | if (feature.geometry? | ||
| + | extendCoords(feature.geometry.coordinates); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | if (hasCoords) { | ||
| + | map.fitBounds(bounds, | ||
| + | padding: 40, | ||
| + | maxZoom: 15, | ||
| + | duration: 500 | ||
| + | }); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function showRouteHighlightByRelation(routeType, | ||
| + | const mode = MODES[routeType]; | ||
| + | if (!mode) { | ||
| + | console.warn(" | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | const allRoutes = routeDataByMode[routeType]; | ||
| + | if (!allRoutes || !Array.isArray(allRoutes.features)) { | ||
| + | console.warn(" | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | const normalizedRelationId = String(relationId ?? "" | ||
| + | |||
| + | const matches = allRoutes.features.filter(feature => { | ||
| + | const osmId = String(feature? | ||
| + | return osmId === normalizedRelationId; | ||
| + | }); | ||
| + | |||
| + | console.log(" | ||
| + | |||
| + | const source = map.getSource(mode.routeHighlightSourceId); | ||
| + | if (!source) { | ||
| + | console.warn(" | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | clearAllHighlights(); | ||
| + | |||
| + | source.setData({ | ||
| + | type: " | ||
| + | features: matches | ||
| + | }); | ||
| + | |||
| + | fitToFeatures(matches); | ||
| + | } | ||
| + | |||
| + | function showRouteHighlightByRef(routeType, | ||
| + | const mode = MODES[routeType]; | ||
| + | if (!mode) { | ||
| + | console.warn(" | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | const allRoutes = routeDataByMode[routeType]; | ||
| + | if (!allRoutes || !Array.isArray(allRoutes.features)) { | ||
| + | console.warn(" | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | const normalizedRef = String(routeRef ?? "" | ||
| + | |||
| + | const matches = allRoutes.features.filter(feature => { | ||
| + | const ref = String(feature? | ||
| + | return ref === normalizedRef; | ||
| + | }); | ||
| + | |||
| + | console.log(" | ||
| + | |||
| + | const source = map.getSource(mode.routeHighlightSourceId); | ||
| + | if (!source) { | ||
| + | console.warn(" | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | clearAllHighlights(); | ||
| + | |||
| + | source.setData({ | ||
| + | type: " | ||
| + | features: matches | ||
| + | }); | ||
| + | |||
| + | fitToFeatures(matches); | ||
| + | } | ||
| + | |||
| + | function buildPopupContent(feature) { | ||
| + | const properties = feature.properties || {}; | ||
| + | const stopName = | ||
| + | properties.name || | ||
| + | properties.local_ref || | ||
| + | properties.ref || | ||
| + | " | ||
| + | |||
| + | const routeType = String(properties.route_type || "" | ||
| + | const mode = MODES[routeType]; | ||
| + | const routes = normalizeRoutesFromStopProperties(properties); | ||
| + | |||
| + | const wrapper = document.createElement(" | ||
| + | |||
| + | const title = document.createElement(" | ||
| + | title.className = " | ||
| + | title.textContent = stopName; | ||
| + | wrapper.appendChild(title); | ||
| + | |||
| + | const subtitle = document.createElement(" | ||
| + | subtitle.className = " | ||
| + | subtitle.textContent = `${mode ? mode.label : routeType}-Linien`; | ||
| + | wrapper.appendChild(subtitle); | ||
| + | |||
| + | if (!routes.length) { | ||
| + | const empty = document.createElement(" | ||
| + | empty.textContent = "Keine Linieninformationen vorhanden."; | ||
| + | wrapper.appendChild(empty); | ||
| + | return wrapper; | ||
| + | } | ||
| + | |||
| + | const list = document.createElement(" | ||
| + | list.className = " | ||
| + | |||
| + | for (const route of routes) { | ||
| + | if (route.variants && route.variants.length) { | ||
| + | for (const variant of route.variants) { | ||
| + | const li = document.createElement(" | ||
| + | const btn = document.createElement(" | ||
| + | |||
| + | btn.type = " | ||
| + | btn.className = " | ||
| + | |||
| + | const label = route.ref | ||
| + | ? `${route.ref}: | ||
| + | : `${variant.from || "?" | ||
| + | |||
| + | btn.textContent = label; | ||
| + | btn.setAttribute(" | ||
| + | btn.setAttribute(" | ||
| + | |||
| + | li.appendChild(btn); | ||
| + | list.appendChild(li); | ||
| + | } | ||
| + | } else { | ||
| + | const li = document.createElement(" | ||
| + | const btn = document.createElement(" | ||
| + | |||
| + | btn.type = " | ||
| + | btn.className = " | ||
| + | btn.textContent = route.ref || route.name || "(ohne Bezeichnung)"; | ||
| + | btn.setAttribute(" | ||
| + | btn.setAttribute(" | ||
| + | |||
| + | li.appendChild(btn); | ||
| + | list.appendChild(li); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | wrapper.appendChild(list); | ||
| + | |||
| + | wrapper.addEventListener(" | ||
| + | const relationBtn = event.target.closest(" | ||
| + | if (relationBtn) { | ||
| + | event.preventDefault(); | ||
| + | event.stopPropagation(); | ||
| + | |||
| + | const clickedRouteType = relationBtn.getAttribute(" | ||
| + | const relationId = relationBtn.getAttribute(" | ||
| + | |||
| + | showRouteHighlightByRelation(clickedRouteType, | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | const refBtn = event.target.closest(" | ||
| + | if (refBtn) { | ||
| + | event.preventDefault(); | ||
| + | event.stopPropagation(); | ||
| + | |||
| + | const clickedRouteType = refBtn.getAttribute(" | ||
| + | const clickedRouteRef = refBtn.getAttribute(" | ||
| + | |||
| + | showRouteHighlightByRef(clickedRouteType, | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | return wrapper; | ||
| + | } | ||
| + | |||
| + | function openStopPopup(feature, | ||
| + | closeActivePopup(); | ||
| + | |||
| + | activePopup = new maplibregl.Popup({ | ||
| + | closeButton: | ||
| + | closeOnClick: | ||
| + | }) | ||
| + | .setLngLat(lngLat) | ||
| + | .setDOMContent(buildPopupContent(feature)) | ||
| + | .addTo(map); | ||
| + | } | ||
| + | |||
| + | async function loadJson(url) { | ||
| + | const response = await fetch(url); | ||
| + | if (!response.ok) { | ||
| + | throw new Error(`Fehler beim Laden von ${url}: HTTP ${response.status}`); | ||
| + | } | ||
| + | return await response.json(); | ||
| + | } | ||
| + | |||
| + | function addModeLayers(modeKey, | ||
| + | const mode = MODES[modeKey]; | ||
| + | routeDataByMode[modeKey] = routesGeojson; | ||
| + | |||
| + | map.addSource(mode.stopSourceId, | ||
| + | type: " | ||
| + | data: stopsGeojson | ||
| + | }); | ||
| + | |||
| + | map.addSource(mode.routeHighlightSourceId, | ||
| + | type: " | ||
| + | data: { | ||
| + | type: " | ||
| + | features: [] | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | map.addLayer({ | ||
| + | id: mode.routeHighlightLayerId, | ||
| + | type: " | ||
| + | source: mode.routeHighlightSourceId, | ||
| + | layout: { | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | 10, 3, | ||
| + | 14, 5, | ||
| + | 18, 8 | ||
| + | ], | ||
| + | " | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | map.addLayer({ | ||
| + | id: mode.stopLayerId, | ||
| + | type: " | ||
| + | source: mode.stopSourceId, | ||
| + | paint: { | ||
| + | " | ||
| + | " | ||
| + | 10, 3, | ||
| + | 14, 5, | ||
| + | 18, 7 | ||
| + | ], | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | map.on(" | ||
| + | if (!e.features || !e.features.length) return; | ||
| + | openStopPopup(e.features[0], | ||
| + | }); | ||
| + | |||
| + | map.on(" | ||
| + | map.getCanvas().style.cursor = " | ||
| + | }); | ||
| + | |||
| + | map.on(" | ||
| + | map.getCanvas().style.cursor = ""; | ||
| + | }); | ||
| + | } | ||
| + | |||
| + | map.on(" | ||
| + | try { | ||
| + | for (const [modeKey, mode] of Object.entries(MODES)) { | ||
| + | const [stopsGeojson, | ||
| + | loadJson(mode.stopsUrl), | ||
| + | loadJson(mode.routesUrl) | ||
| + | ]); | ||
| + | |||
| + | addModeLayers(modeKey, | ||
| + | } | ||
| + | |||
| + | const stopLayerIds = Object.values(MODES).map(mode => mode.stopLayerId); | ||
| + | |||
| + | map.on(" | ||
| + | const hits = map.queryRenderedFeatures(e.point, | ||
| + | layers: stopLayerIds | ||
| + | }); | ||
| + | |||
| + | if (!hits.length) { | ||
| + | closeActivePopup(); | ||
| + | clearAllHighlights(); | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | console.log(" | ||
| + | } catch (error) { | ||
| + | console.error(error); | ||
| + | } | ||
| + | }); | ||
| + | </ | ||
| + | |||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | ====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:// | ||