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
Collaborator

Full pipeline verified end-to-end (download_gtfs → build_area → gtfs_to_geopackage → prepare → render + export_google_mymaps).

Changes:

  • 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): master.gpkg → KML for Google My Maps import (stops, routes, boundary, coastline layers)
  • coastline.kml (new): reference coastline layer for the KML export
  • .gitignore: also ignore generated output/*.kml
Full pipeline verified end-to-end (download_gtfs → build_area → gtfs_to_geopackage → prepare → render + export_google_mymaps). Changes: - **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): master.gpkg → KML for Google My Maps import (stops, routes, boundary, coastline layers) - **coastline.kml** (new): reference coastline layer for the KML export - **.gitignore**: also ignore generated output/*.kml
marvin added 16 commits 2026-09-18 12:10:11 +02:00
- pyproject.toml with all deps (geopandas, contextily, partridge, etc.)
- config/: area.json (3 OSM boundary relations), modes.yaml (subway/s_tog/
  light_rail/regional/bus, S-tog scoped to route=light_rail network=Takst),
  styling.yaml (zorder/widths/colours/palette/mask/labels)
- build_area.py: fetch 3 relations via OSM API, stitch rings, dissolve to
  area.gpkg/area.geojson (City Pass approximation)
- download_osm.py: per-mode Overpass (mirror+backoff, out geom + recurse for
  stops), writes {mode}.gpkg (lines+stops) + query text
- download_gtfs.py / gtfs_to_geopackage.py: stubs (blocked on Rejseplanen)
- data/ gitignored (regenerable); output/ tracked
- prepare.py: merge per-mode OSM lines, resolve colours
  (GTFS route_color -> OSM colour tag -> keyed palette, robust to pd.NA),
  categorise buses (A/C/S/regular via regex), clip lines+stops to area,
  write master.gpkg layers (lines/stops/stations)
- download_osm.py: add stations query (public_transport=station /
  railway=station nodes+areas, out center) -> stations.gpkg with proper
  human-readable names + subway/light_rail mode tags for labelling
- verified: 135 lines (8 metro/16 s-tog/28 regional/83 bus), 90 named
  stations, zero NaN colours
- 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
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.
- 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)
- 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
All station and bus stop markers now use zorder = max(line zorders) + 1,
ensuring they always render on top of every transit line layer.
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.
Rail stations get generous thresholds to merge all platform/entrance
duplicates; bus stays conservative:
  metro: 200m    -> 91 stops -> 44 markers (0 duplicates)
  s_tog: 400m   -> 76 stops -> 30 markers (0 duplicates)
  regional: 500m -> 27 stops ->  9 markers (0 duplicates)
  bus: 100m      -> 1610 stops -> 781 markers

stop_cluster_m is now a dict keyed by mode in styling.yaml.
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.
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.
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.
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
- 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
- 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
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).
marvin force-pushed add-kml-export-and-bus-filtering from 37c3171f39 to 07dc0b55a0 2026-09-18 12:10:11 +02:00 Compare
This pull request has changes conflicting with the target branch.
  • .gitignore
  • copenhagen/config/area.json
  • copenhagen/config/modes.yaml
  • copenhagen/config/styling.yaml
  • copenhagen/scripts/_common.py
  • copenhagen/scripts/build_area.py
  • copenhagen/scripts/download_gtfs.py
  • copenhagen/scripts/gtfs_to_geopackage.py
  • copenhagen/scripts/prepare.py
  • copenhagen/scripts/render.py
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin add-kml-export-and-bus-filtering:add-kml-export-and-bus-filtering
git checkout add-kml-export-and-bus-filtering
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: hulthe/jetlag-maps#2