Use platform nodes + route membership for bus stop clustering
Replaces distance-based clustering with route-membership grouping: - download_osm.py: collect route_refs per stop node; use platform roles for bus stops, stop roles for rail stops - prepare.py: carry route_refs through to master.gpkg - render.py: group_by_route_membership() uses union-find to cluster same-name stops sharing at least one route into one marker; stops on disjoint routes are kept separate. No distance threshold needed. Results: 786 bus platforms -> 465 markers (was 1610 -> 781). Veksøvej: 2 platforms sharing routes 2A+5C -> 1 marker. Peter Bangs Vej: 7 platforms on routes 10/21/22/4A/7A -> 2 markers.
This commit is contained in:
@@ -262,11 +262,70 @@ def rail_station_names(stations):
|
||||
return names
|
||||
|
||||
|
||||
def group_by_route_membership(rows, cluster_m):
|
||||
"""Group stop rows by (name, shared route_ref) into distinct stops.
|
||||
|
||||
Two same-name stops that share at least one route_ref are the same stop.
|
||||
Stops on disjoint routes are different stops. Within each group,
|
||||
platform duplicates are merged by distance clustering.
|
||||
Returns a list of (x, y) centroids.
|
||||
"""
|
||||
if rows is None or rows.empty:
|
||||
return []
|
||||
|
||||
coords = [(r.geometry.x, r.geometry.y) for _, r in rows.iterrows()]
|
||||
route_sets = []
|
||||
for _, r in rows.iterrows():
|
||||
rr = r.get("route_refs")
|
||||
if isinstance(rr, str) and rr:
|
||||
route_sets.append(set(rr.split(";")))
|
||||
else:
|
||||
route_sets.append(set())
|
||||
|
||||
n = len(coords)
|
||||
# union-find: stops sharing a route are connected
|
||||
parent = list(range(n))
|
||||
|
||||
def find(x):
|
||||
while parent[x] != x:
|
||||
parent[x] = parent[parent[x]]
|
||||
x = parent[x]
|
||||
return x
|
||||
|
||||
def union(a, b):
|
||||
ra, rb = find(a), find(b)
|
||||
if ra != rb:
|
||||
parent[ra] = rb
|
||||
|
||||
for i in range(n):
|
||||
for j in range(i + 1, n):
|
||||
if route_sets[i] & route_sets[j]:
|
||||
union(i, j)
|
||||
|
||||
# group indices by connected component
|
||||
components = {}
|
||||
for i in range(n):
|
||||
root = find(i)
|
||||
components.setdefault(root, []).append(i)
|
||||
|
||||
centroids = []
|
||||
for indices in components.values():
|
||||
comp_coords = [coords[i] for i in indices]
|
||||
if len(comp_coords) == 1:
|
||||
centroids.append(comp_coords[0])
|
||||
else:
|
||||
# stops sharing a route are the same stop; merge to centroid
|
||||
import numpy as np
|
||||
centroids.append(tuple(np.mean(comp_coords, axis=0)))
|
||||
return centroids
|
||||
|
||||
|
||||
def plot_bus_stops(ax, stops, styling, active_styles, rail_names=None):
|
||||
"""Draw small markers for bus stops from the stops layer.
|
||||
|
||||
Bus stops whose name matches a rail station are skipped — the rail
|
||||
station marker represents that stop.
|
||||
station marker represents that stop. Stops are grouped by shared
|
||||
route membership: same-name stops on disjoint routes are distinct.
|
||||
"""
|
||||
if stops is None or stops.empty or "bus" not in active_styles:
|
||||
return
|
||||
@@ -292,19 +351,14 @@ def plot_bus_stops(ax, stops, styling, active_styles, rail_names=None):
|
||||
marker = "o" if shape == "circle" else "s"
|
||||
top_z = max(styling["zorder"].values()) + 1
|
||||
|
||||
# cluster same-name bus stops within threshold (opposite sides of road)
|
||||
# skip names that match a rail station (merged into station marker)
|
||||
# group by name, then by route membership; skip rail station names
|
||||
centroids = []
|
||||
skipped = 0
|
||||
for name, grp in bus.groupby("name"):
|
||||
if name and name in rail_names:
|
||||
skipped += len(grp)
|
||||
continue
|
||||
coords = [(r.geometry.x, r.geometry.y) for _, r in grp.iterrows()]
|
||||
if name and len(coords) > 1:
|
||||
centroids.extend(cluster_stops(coords, cluster_m))
|
||||
else:
|
||||
centroids.extend(coords)
|
||||
centroids.extend(group_by_route_membership(grp, cluster_m))
|
||||
|
||||
ax.scatter([p[0] for p in centroids], [p[1] for p in centroids],
|
||||
s=sz ** 2, marker=marker, c=fill, edgecolors=edge,
|
||||
|
||||
Reference in New Issue
Block a user