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]]