Add south_bound_lat to modes.yaml and a load_south_bound() helper in
prepare.py. When set, the area geometry is intersected with the north
half-plane above the cutoff, so all downstream clipping (lines, stops,
stations) cascades automatically.
Removed: 94 bus stops (including Skyttehøj), 1 station (Vestamager),
and line segments south of the boundary (Metro M1 tail, buses 32-36).
- modes.yaml: bus_whitelist (A-buses + selected refs) and exclude list
(Snälltåget 083 night train — sparse crow-fly stubs)
- prepare.py: drop degenerate crow-fly shapes below 0.05 pts/km; apply
bus whitelist and route exclusions from modes.yaml
- export_google_mymaps.py: new script — master.gpkg -> KML for Google
My Maps import (stops, routes, boundary, coastline layers)
- coastline.kml: reference coastline layer for the KML export
- .gitignore: also ignore generated output/*.kml
- Replace OSM Overpass transit data with the Rejseplanen GTFS feed
(routes keyed by (agency, short name); styles from modes.yaml)
- Draw one shape per (style, ref, direction), choosing the shape that
serves the most in-area stops so lines pass the stops we show
- Collapse stops by names (verified unambiguous); prune stops whose
serving refs have no drawn line within 300 m
- Patch Københavns Havn + Nordhavn into the area polygon so ferry
routes and sub-harbour metro tunnels survive clipping; 100 m buffer
closes relation boundary slivers
- Drop OSM download pipeline; keep tiled basemap
Some bus stop platform nodes in OSM are not members of any route
relation (e.g. Borgergade). These were missed by the route-membership
download approach. Now download_osm.py:
- tracks bus platform node ids captured by route queries
- runs a supplementary query for all highway=bus_stop + platform nodes
- appends orphans (with empty route_refs) to the bus stops layer
render.py: orphan stops (empty route_refs) fall back to distance
clustering instead of route-membership grouping.
Results: 1164 route-member stops + 707 orphans = 1871 total
-> 1370 after clip -> 1049 markers after grouping
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.
Clustering was computing distances in EPSG:3857 (Web Mercator), which
inflates distances by ~78% at Copenhagen's latitude (55.7°N). This made
the 100m threshold actually merge stops within only 56m, and all reported
distances were inflated.
Now reprojects to EPSG:25832 (UTM 32N) for accurate meter distances,
clusters there, converts centroids back to EPSG:3857 for plotting.
Bus stops sharing a name with a rail station (metro/s_tog/regional) are
now skipped from bus stop markers — the rail station marker represents
that stop. 32 bus stops across 9 names (Fasanvej, Frederiksberg Allé,
Jyllingevej, Mozarts Plads, Peter Bangs Vej, Ryparken, Rådhuspladsen,
Sluseholmen, Vigerslev Allé) are absorbed.
Same-name stops within 50m (opposite sides of a road, different
platforms/entrances) are now merged into one marker at the cluster
centroid. Uses scipy hierarchical clustering (single linkage, distance
criterion).
Stop counts after clustering:
bus: 1610 -> 892 markers
metro: 91 -> 46 markers
s_tog: 76 -> 34 markers
regional: 27 -> 10 markers
Threshold configurable via styling.yaml stations.stop_cluster_m.
- plot_bus_stops(): loads bus stops from master.gpkg stops layer (1610
points), draws small white-filled circles (1.5pt) at each location
- stops layer loaded in main() alongside stations
- 'bus' added to stations.styles and marker.size in styling.yaml
- bus stops respect --no-stops and --no-buses / --modes filters
- bus marker zorder sits just above bus lines
- labels: off by default (was on). Enable via --labels flag or
styling.yaml labels.show: true. Previously --no-labels opted out.
- stations: new markers layer -- small white-filled circles with dark
border at each station, sized by mode (metro 3.5pt, s_tog 3.0pt,
regional 2.5pt). On by default; disable via --no-stops.
- markers respect --modes filter (only mark stations for drawn layers)
- styling.yaml: add stations section (shape/fill/edge/size/linewidth)
- classify_station() helper deduplicates the metro/s_tog/regional logic
- fix attribution to Esri (was CARTO)
OSM's tile server blocks apps without a valid identifying User-Agent
(contextily's default is a random UUID, which OSM rejects with an
'Access blocked' error tile). Switched to ESRI WorldGrayCanvas, a free
no-key light-gray basemap comparable to CartoDB Positron, and set a
proper User-Agent header (jetlag-maps/0.1) on contextily requests per
OSM tile usage policy.
- render.py: parameterized CLI (--modes, --no-buses, --bus-subset,
--max-bus-routes, --dpi, --size, --format, --out, --no-basemap, --no-labels)
- basemap: OSM standard tiles via contextily (EPSG:3857)
- two-tone lines: dark casing + colour body per route, z-order
bus->s_tog->light_rail->regional->metro
- shapeburst fade mask outside City Pass area (distance-transform alpha)
- station labels for metro + S-tog via adjustText
- title + attribution
- styling.yaml: switch basemap to OpenStreetMap.Mapnik (CartoDB now needs API key)
- .gitignore: exclude generated outputs (regenerable via render.py)
- verified: full map, --no-buses, --bus-subset, --modes, SVG all work