Add bus whitelist, route exclusions, crow-fly shape filter, and KML export #2
@@ -70,8 +70,21 @@ basemap:
|
||||
provider: Esri.WorldGrayCanvas
|
||||
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:
|
||||
show: true
|
||||
show: false # off by default; enable via --labels
|
||||
styles: [metro, s_tog] # which station types to label
|
||||
min_fontsize: 5
|
||||
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
|
||||
|
||||
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 --bus-subset A,C,S # only trunk buses
|
||||
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("--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-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()
|
||||
|
||||
|
||||
@@ -155,33 +158,74 @@ def plot_lines(ax, lines, styling):
|
||||
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:
|
||||
return
|
||||
label_styles = set(styling["labels"]["styles"])
|
||||
fmin = styling["labels"]["min_fontsize"]
|
||||
fmax = styling["labels"]["max_fontsize"]
|
||||
cfg = styling.get("stations", {})
|
||||
if not cfg.get("show", True):
|
||||
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):
|
||||
return r.get("subway") == "yes" or r.get("station") == "subway"
|
||||
return classify_station(r) == "metro"
|
||||
|
||||
def is_stog(r):
|
||||
return r.get("light_rail") == "yes" or r.get("station") == "light_rail"
|
||||
return classify_station(r) == "s_tog"
|
||||
|
||||
pts = []
|
||||
for _, r in stations.iterrows():
|
||||
is_m = is_metro(r)
|
||||
is_s = is_stog(r)
|
||||
if "metro" in label_styles and is_m:
|
||||
pass
|
||||
elif "s_tog" in label_styles and is_s:
|
||||
pass
|
||||
else:
|
||||
s = classify_station(r)
|
||||
if s not in label_styles:
|
||||
continue
|
||||
name = r.get("name")
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
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:
|
||||
return
|
||||
@@ -248,9 +292,18 @@ def main():
|
||||
# lines
|
||||
plot_lines(ax, lines, styling)
|
||||
|
||||
# labels
|
||||
if not args.no_labels and styling["labels"]["show"]:
|
||||
label_stations(ax, stations, styling, area)
|
||||
# active styles (which mode layers are actually drawn)
|
||||
active_styles = set(lines["style"].unique())
|
||||
|
||||
# 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
|
||||
ax.text(0.5, 0.985, "Copenhagen — Public Transit",
|
||||
@@ -258,7 +311,7 @@ def main():
|
||||
fontsize=fig_w * 1.1, fontweight="bold", color="#222",
|
||||
bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="none", alpha=0.7))
|
||||
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",
|
||||
fontsize=max(5, fig_w * 0.5), color="#666")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user