Add bus whitelist, route exclusions, crow-fly shape filter, and KML export #2

Open
marvin wants to merge 16 commits from add-kml-export-and-bus-filtering into master
2 changed files with 70 additions and 2 deletions
Showing only changes of commit 4aca62ca1a - Show all commits
+64
View File
@@ -198,6 +198,40 @@ def parse_stations(data):
return rows return rows
def orphan_bus_stops_query(bbox):
s, n, w, e = bbox
return f"""[out:json][timeout:180];
(
node["highway"="bus_stop"]["public_transport"="platform"]({s},{w},{n},{e});
);
out body;
"""
def parse_orphan_bus_stops(data, known_node_ids):
"""Parse standalone bus platform nodes not already captured by routes."""
stops = []
for e in data["elements"]:
if e["type"] != "node" or e["id"] in known_node_ids:
continue
tags = e.get("tags", {})
if tags.get("public_transport") != "platform":
continue
name = tags.get("name")
if not name:
continue
stops.append({
"mode": "bus",
"name": name,
"ref": tags.get("ref"),
"public_transport": tags.get("public_transport"),
"railway": tags.get("railway"),
"route_refs": "", # orphan — no route membership
"geometry": Point(e["lon"], e["lat"]),
})
return stops
def main(): def main():
modes = yaml.safe_load((CONFIG / "modes.yaml").read_text())["modes"] modes = yaml.safe_load((CONFIG / "modes.yaml").read_text())["modes"]
area = gpd.read_file(PROCESSED / "area.gpkg") area = gpd.read_file(PROCESSED / "area.gpkg")
@@ -205,12 +239,18 @@ def main():
bbox = (s, n, w, e) bbox = (s, n, w, e)
OSM_RAW.mkdir(parents=True, exist_ok=True) OSM_RAW.mkdir(parents=True, exist_ok=True)
bus_stop_node_ids = set()
for mode, cfg in modes.items(): for mode, cfg in modes.items():
ql = build_query(cfg, bbox) ql = build_query(cfg, bbox)
(OSM_RAW / f"{mode}.overpassql").write_text(ql) (OSM_RAW / f"{mode}.overpassql").write_text(ql)
print(f"[{mode}] querying Overpass bbox=({s:.4f},{w:.4f},{n:.4f},{e:.4f})...", flush=True) print(f"[{mode}] querying Overpass bbox=({s:.4f},{w:.4f},{n:.4f},{e:.4f})...", flush=True)
data = overpass(ql) data = overpass(ql)
lines, stops = parse(data, mode) lines, stops = parse(data, mode)
# track bus stop node ids to avoid dupes with orphans
if mode == "bus":
for e in data["elements"]:
if e["type"] == "node" and e.get("tags", {}).get("public_transport") == "platform":
bus_stop_node_ids.add(e["id"])
print(f" -> {len(lines)} lines, {len(stops)} stops", flush=True) print(f" -> {len(lines)} lines, {len(stops)} stops", flush=True)
if not lines: if not lines:
continue continue
@@ -220,6 +260,30 @@ def main():
sgdf = gpd.GeoDataFrame(stops, crs="EPSG:4326") sgdf = gpd.GeoDataFrame(stops, crs="EPSG:4326")
sgdf.to_file(OSM_RAW / f"{mode}.gpkg", driver="GPKG", layer="stops") sgdf.to_file(OSM_RAW / f"{mode}.gpkg", driver="GPKG", layer="stops")
# orphan bus stops (platform nodes not in any route relation)
oql = orphan_bus_stops_query(bbox)
(OSM_RAW / "orphan_bus_stops.overpassql").write_text(oql)
print("[orphan_bus_stops] querying Overpass...", flush=True)
odata = overpass(oql)
orphans = parse_orphan_bus_stops(odata, bus_stop_node_ids)
print(f" -> {len(orphans)} orphan bus stops (not in route relations)", flush=True)
if orphans:
# append to bus.gpkg stops layer
existing = gpd.read_file(OSM_RAW / "bus.gpkg", layer="stops")
combined = gpd.GeoDataFrame(
__import__("pandas").concat([existing, gpd.GeoDataFrame(orphans, crs="EPSG:4326")],
ignore_index=True),
crs="EPSG:4326"
)
# rewrite stops layer
import tempfile, shutil
tmp = OSM_RAW / "bus_tmp.gpkg"
lines_gdf = gpd.read_file(OSM_RAW / "bus.gpkg", layer="lines")
lines_gdf.to_file(tmp, driver="GPKG", layer="lines")
combined.to_file(tmp, driver="GPKG", layer="stops")
shutil.move(str(tmp), str(OSM_RAW / "bus.gpkg"))
print(f" -> bus stops layer now: {len(combined)} total", flush=True)
# stations (named station nodes/areas) for labelling # stations (named station nodes/areas) for labelling
sql = stations_query(bbox) sql = stations_query(bbox)
(OSM_RAW / "stations.overpassql").write_text(sql) (OSM_RAW / "stations.overpassql").write_text(sql)
+6 -2
View File
@@ -311,12 +311,16 @@ def group_by_route_membership(rows, cluster_m):
centroids = [] centroids = []
for indices in components.values(): for indices in components.values():
comp_coords = [coords[i] for i in indices] comp_coords = [coords[i] for i in indices]
comp_routes = [route_sets[i] for i in indices]
has_routes = any(rs for rs in comp_routes)
if len(comp_coords) == 1: if len(comp_coords) == 1:
centroids.append(comp_coords[0]) centroids.append(comp_coords[0])
else: elif has_routes:
# stops sharing a route are the same stop; merge to centroid # stops sharing a route are the same stop; merge to centroid
import numpy as np
centroids.append(tuple(np.mean(comp_coords, axis=0))) centroids.append(tuple(np.mean(comp_coords, axis=0)))
else:
# no route info (orphans); fall back to distance clustering
centroids.extend(cluster_stops(comp_coords, cluster_m))
return centroids return centroids