Add bus whitelist, route exclusions, crow-fly shape filter, and KML export #2
@@ -198,6 +198,40 @@ def parse_stations(data):
|
||||
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():
|
||||
modes = yaml.safe_load((CONFIG / "modes.yaml").read_text())["modes"]
|
||||
area = gpd.read_file(PROCESSED / "area.gpkg")
|
||||
@@ -205,12 +239,18 @@ def main():
|
||||
bbox = (s, n, w, e)
|
||||
|
||||
OSM_RAW.mkdir(parents=True, exist_ok=True)
|
||||
bus_stop_node_ids = set()
|
||||
for mode, cfg in modes.items():
|
||||
ql = build_query(cfg, bbox)
|
||||
(OSM_RAW / f"{mode}.overpassql").write_text(ql)
|
||||
print(f"[{mode}] querying Overpass bbox=({s:.4f},{w:.4f},{n:.4f},{e:.4f})...", flush=True)
|
||||
data = overpass(ql)
|
||||
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)
|
||||
if not lines:
|
||||
continue
|
||||
@@ -220,6 +260,30 @@ def main():
|
||||
sgdf = gpd.GeoDataFrame(stops, crs="EPSG:4326")
|
||||
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
|
||||
sql = stations_query(bbox)
|
||||
(OSM_RAW / "stations.overpassql").write_text(sql)
|
||||
|
||||
@@ -311,12 +311,16 @@ def group_by_route_membership(rows, cluster_m):
|
||||
centroids = []
|
||||
for indices in components.values():
|
||||
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:
|
||||
centroids.append(comp_coords[0])
|
||||
else:
|
||||
elif has_routes:
|
||||
# stops sharing a route are the same stop; merge to centroid
|
||||
import numpy as np
|
||||
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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user