Add bus whitelist, route exclusions, crow-fly shape filter, and KML export #2
@@ -70,8 +70,21 @@ basemap:
|
|||||||
provider: Esri.WorldGrayCanvas
|
provider: Esri.WorldGrayCanvas
|
||||||
zoom: 14
|
zoom: 14
|
||||||
|
|
||||||
|
stations:
|
||||||
|
show: true # draw station markers (symbols without text)
|
||||||
|
styles: [metro, s_tog, regional]
|
||||||
|
marker:
|
||||||
|
shape: circle # circle | square
|
||||||
|
fill: white
|
||||||
|
edge: "#2b2b2b"
|
||||||
|
size: # diameter in points
|
||||||
|
metro: 3.5
|
||||||
|
s_tog: 3.0
|
||||||
|
regional: 2.5
|
||||||
|
linewidth: 0.8
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
show: true
|
show: false # off by default; enable via --labels
|
||||||
styles: [metro, s_tog] # which station types to label
|
styles: [metro, s_tog] # which station types to label
|
||||||
min_fontsize: 5
|
min_fontsize: 5
|
||||||
max_fontsize: 9
|
max_fontsize: 9
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ config/styling.yaml, applies CLI filters, and composes a printable map:
|
|||||||
- station labels (metro + S-tog) via adjustText
|
- station labels (metro + S-tog) via adjustText
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
uv run python render.py # default: all modes, PNG
|
uv run python render.py # default: all modes, PNG, markers only
|
||||||
|
uv run python render.py --labels # add station name labels
|
||||||
|
uv run python render.py --no-stops # no station markers
|
||||||
uv run python render.py --no-buses --format svg --out map.svg
|
uv run python render.py --no-buses --format svg --out map.svg
|
||||||
uv run python render.py --bus-subset A,C,S # only trunk buses
|
uv run python render.py --bus-subset A,C,S # only trunk buses
|
||||||
uv run python render.py --modes metro,s_tog # rail-only
|
uv run python render.py --modes metro,s_tog # rail-only
|
||||||
@@ -48,7 +50,8 @@ def load_args():
|
|||||||
p.add_argument("--format", default="png", help="output format (png/svg/pdf)")
|
p.add_argument("--format", default="png", help="output format (png/svg/pdf)")
|
||||||
p.add_argument("--out", default=None, help="output path (default: output/copenhagen.<fmt>)")
|
p.add_argument("--out", default=None, help="output path (default: output/copenhagen.<fmt>)")
|
||||||
p.add_argument("--no-basemap", action="store_true", help="skip basemap tiles (offline/faster)")
|
p.add_argument("--no-basemap", action="store_true", help="skip basemap tiles (offline/faster)")
|
||||||
p.add_argument("--no-labels", action="store_true", help="skip station labels")
|
p.add_argument("--labels", action="store_true", help="draw station name labels (off by default)")
|
||||||
|
p.add_argument("--no-stops", action="store_true", help="skip station markers")
|
||||||
return p.parse_args()
|
return p.parse_args()
|
||||||
|
|
||||||
|
|
||||||
@@ -155,33 +158,74 @@ def plot_lines(ax, lines, styling):
|
|||||||
capstyle="round", joinstyle="round")
|
capstyle="round", joinstyle="round")
|
||||||
|
|
||||||
|
|
||||||
def label_stations(ax, stations, styling, area_3857):
|
def classify_station(r):
|
||||||
|
"""Return the style key for a station row, or None."""
|
||||||
|
if r.get("subway") == "yes" or r.get("station") == "subway":
|
||||||
|
return "metro"
|
||||||
|
if r.get("light_rail") == "yes" or r.get("station") == "light_rail":
|
||||||
|
return "s_tog"
|
||||||
|
if r.get("railway") == "station" or r.get("train") == "yes":
|
||||||
|
return "regional"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def plot_stations(ax, stations, styling, active_styles):
|
||||||
if stations is None or stations.empty:
|
if stations is None or stations.empty:
|
||||||
return
|
return
|
||||||
label_styles = set(styling["labels"]["styles"])
|
cfg = styling.get("stations", {})
|
||||||
fmin = styling["labels"]["min_fontsize"]
|
if not cfg.get("show", True):
|
||||||
fmax = styling["labels"]["max_fontsize"]
|
return
|
||||||
|
marker_styles = set(cfg.get("styles", ["metro", "s_tog", "regional"]))
|
||||||
|
mk = cfg.get("marker", {})
|
||||||
|
sizes = mk.get("size", {})
|
||||||
|
shape = mk.get("shape", "circle")
|
||||||
|
fill = mk.get("fill", "white")
|
||||||
|
edge = mk.get("edge", "#2b2b2b")
|
||||||
|
lw = mk.get("linewidth", 0.8)
|
||||||
|
marker = "o" if shape == "circle" else "s"
|
||||||
|
|
||||||
|
pts_by_style = {}
|
||||||
|
for _, r in stations.iterrows():
|
||||||
|
s = classify_station(r)
|
||||||
|
if s is None or s not in marker_styles or s not in active_styles:
|
||||||
|
continue
|
||||||
|
pts_by_style.setdefault(s, []).append((r.geometry.x, r.geometry.y))
|
||||||
|
|
||||||
|
zord = styling["zorder"]
|
||||||
|
for s in sorted(pts_by_style, key=lambda k: zord.get(k, 0)):
|
||||||
|
pts = pts_by_style[s]
|
||||||
|
sz = sizes.get(s, 3.0)
|
||||||
|
z = zord.get(s, 5) + 0.5
|
||||||
|
ax.scatter([p[0] for p in pts], [p[1] for p in pts],
|
||||||
|
s=sz ** 2, marker=marker, c=fill, edgecolors=edge,
|
||||||
|
linewidths=lw, zorder=z, alpha=1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def label_stations(ax, stations, styling, active_styles):
|
||||||
|
if stations is None or stations.empty:
|
||||||
|
return
|
||||||
|
cfg = styling.get("labels", {})
|
||||||
|
if not cfg.get("show", False):
|
||||||
|
return
|
||||||
|
label_styles = set(cfg.get("styles", ["metro", "s_tog"])) & active_styles
|
||||||
|
fmin = cfg.get("min_fontsize", 5)
|
||||||
|
fmax = cfg.get("max_fontsize", 9)
|
||||||
|
|
||||||
def is_metro(r):
|
def is_metro(r):
|
||||||
return r.get("subway") == "yes" or r.get("station") == "subway"
|
return classify_station(r) == "metro"
|
||||||
|
|
||||||
def is_stog(r):
|
def is_stog(r):
|
||||||
return r.get("light_rail") == "yes" or r.get("station") == "light_rail"
|
return classify_station(r) == "s_tog"
|
||||||
|
|
||||||
pts = []
|
pts = []
|
||||||
for _, r in stations.iterrows():
|
for _, r in stations.iterrows():
|
||||||
is_m = is_metro(r)
|
s = classify_station(r)
|
||||||
is_s = is_stog(r)
|
if s not in label_styles:
|
||||||
if "metro" in label_styles and is_m:
|
|
||||||
pass
|
|
||||||
elif "s_tog" in label_styles and is_s:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
continue
|
continue
|
||||||
name = r.get("name")
|
name = r.get("name")
|
||||||
if not isinstance(name, str) or not name.strip():
|
if not isinstance(name, str) or not name.strip():
|
||||||
continue
|
continue
|
||||||
pts.append((r.geometry.x, r.geometry.y, name, is_m))
|
pts.append((r.geometry.x, r.geometry.y, name, s == "metro"))
|
||||||
|
|
||||||
if not pts:
|
if not pts:
|
||||||
return
|
return
|
||||||
@@ -248,9 +292,18 @@ def main():
|
|||||||
# lines
|
# lines
|
||||||
plot_lines(ax, lines, styling)
|
plot_lines(ax, lines, styling)
|
||||||
|
|
||||||
# labels
|
# active styles (which mode layers are actually drawn)
|
||||||
if not args.no_labels and styling["labels"]["show"]:
|
active_styles = set(lines["style"].unique())
|
||||||
label_stations(ax, stations, styling, area)
|
|
||||||
|
# station markers (symbols, no text)
|
||||||
|
if not args.no_stops:
|
||||||
|
plot_stations(ax, stations, styling, active_styles)
|
||||||
|
|
||||||
|
# station labels (opt-in via --labels)
|
||||||
|
if args.labels:
|
||||||
|
styling["labels"]["show"] = True
|
||||||
|
if styling.get("labels", {}).get("show", False):
|
||||||
|
label_stations(ax, stations, styling, active_styles)
|
||||||
|
|
||||||
# title + attribution
|
# title + attribution
|
||||||
ax.text(0.5, 0.985, "Copenhagen — Public Transit",
|
ax.text(0.5, 0.985, "Copenhagen — Public Transit",
|
||||||
@@ -258,7 +311,7 @@ def main():
|
|||||||
fontsize=fig_w * 1.1, fontweight="bold", color="#222",
|
fontsize=fig_w * 1.1, fontweight="bold", color="#222",
|
||||||
bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="none", alpha=0.7))
|
bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="none", alpha=0.7))
|
||||||
ax.text(0.01, 0.01,
|
ax.text(0.01, 0.01,
|
||||||
"Data: © OpenStreetMap contributors (ODbL) · Base: © CARTO",
|
"Data: © OpenStreetMap contributors (ODbL) · Base: Esri, HERE",
|
||||||
transform=ax.transAxes, ha="left", va="bottom",
|
transform=ax.transAxes, ha="left", va="bottom",
|
||||||
fontsize=max(5, fig_w * 0.5), color="#666")
|
fontsize=max(5, fig_w * 0.5), color="#666")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user