16 Commits
Author SHA1 Message Date
marvin 2dcaed7600 Add orphan bus stops (platforms not in route relations)
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
2026-09-14 00:22:15 +02:00
marvin dccf04349a 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.
2026-09-14 00:00:28 +02:00
marvin 977a3ffb4c Fix Web Mercator distance distortion in stop clustering
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.
2026-09-13 23:53:51 +02:00
marvin 9e078efad3 Merge bus stops into rail stations when names match
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.
2026-09-13 23:26:19 +02:00
marvin 86ccbb0515 Per-mode stop clustering thresholds
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.
2026-09-13 23:23:14 +02:00
marvin 2f0cc07061 Cluster same-name stops within 50m into single markers
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.
2026-09-13 23:15:29 +02:00
marvin 07bfda8df0 Fix stop markers rendering below 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.
2026-09-13 22:50:41 +02:00
marvin d1a8383cb7 Add bus stop markers from stops layer
- 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
2026-09-13 22:42:05 +02:00
marvin 154b9f9fb5 Make station labels opt-in; add station markers (symbols without text)
- 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)
2026-09-13 22:37:50 +02:00
marvin 0ec288bfd0 Switch basemap to ESRI WorldGrayCanvas (OSM tiles blocked)
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.
2026-09-13 22:32:16 +02:00
marvin 4b85e93e6c Copenhagen Phase 3: render.py -> PNG/SVG/PDF map
- 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
2026-09-13 01:27:55 +02:00
marvin 0086d8a308 Copenhagen Phase 2: prepare.py merges + clips + colours -> master.gpkg
- 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
2026-09-13 01:19:34 +02:00
marvin ac02320f1d Copenhagen Phase 1: scaffolding + area + OSM download
- 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
2026-09-13 01:15:25 +02:00
hulthe dc51eaf177 Work on northern italy map 2026-05-10 14:25:40 +02:00
hulthe 6d514cd69e Initialize qgis project for northern italy 2026-05-07 12:45:20 +02:00
hulthe 48c6c03a37 Add regions maps for northern italy 2026-05-07 12:45:12 +02:00