219 lines
5.8 KiB
Python
219 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate a prettymaps poster.
|
|
|
|
Parameters (priority: CLI args > env vars > defaults):
|
|
|
|
QUERY / --query location string (geocoded by osmnx)
|
|
LAT / --lat latitude (use with LON instead of QUERY)
|
|
LON / --lon longitude
|
|
RADIUS_M / --radius radius in metres
|
|
CIRCLE / --circle circular clip (true/false, 1/0, yes/no)
|
|
TITLE / --title map title
|
|
PRESET / --preset prettymaps preset name
|
|
OUTPUT_NAME / --output-name basename for png/pdf (no extension)
|
|
DPI / --dpi output dpi
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import osmnx as ox
|
|
|
|
# osmnx 2.x: project_gdf left the top-level namespace
|
|
if not hasattr(ox, "project_gdf"):
|
|
ox.project_gdf = ox.projection.project_gdf
|
|
if not hasattr(ox, "project_graph"):
|
|
if hasattr(ox, "projection") and hasattr(ox.projection, "project_graph"):
|
|
ox.project_graph = ox.projection.project_graph
|
|
|
|
import prettymaps
|
|
import prettymaps.draw as _pm_draw
|
|
import matplotlib
|
|
|
|
matplotlib.use("Agg")
|
|
|
|
# --- patch: KeyError 'highway' when streets gdf has no highway column ---
|
|
_orig_graph_to_shapely = _pm_draw.graph_to_shapely
|
|
|
|
|
|
def _safe_graph_to_shapely(gdf, width=1.0):
|
|
if gdf is None or getattr(gdf, "empty", True):
|
|
from shapely.geometry import GeometryCollection
|
|
|
|
return GeometryCollection()
|
|
if isinstance(width, dict) and "highway" not in gdf.columns:
|
|
width = width.get("default", next(iter(width.values()), 1.0))
|
|
return _orig_graph_to_shapely(gdf, width)
|
|
|
|
|
|
_pm_draw.graph_to_shapely = _safe_graph_to_shapely
|
|
# -----------------------------------------------------------------------
|
|
|
|
# --- defaults (single place) ---
|
|
DEFAULT_QUERY = "Akademgorodok, Novosibirsk, Russia"
|
|
DEFAULT_LAT = None
|
|
DEFAULT_LON = None
|
|
DEFAULT_RADIUS_M = 2200
|
|
DEFAULT_CIRCLE = True
|
|
DEFAULT_TITLE = "Академгородок · Новосибирск"
|
|
DEFAULT_PRESET = "default"
|
|
DEFAULT_OUTPUT_NAME = "akademgorodok_poster"
|
|
DEFAULT_DPI = 300
|
|
DEFAULT_FIGSIZE = (14, 14)
|
|
# --------------------------------
|
|
|
|
|
|
def _env(name, default=None):
|
|
val = os.environ.get(name)
|
|
if val is None or val == "":
|
|
return default
|
|
return val
|
|
|
|
|
|
def _env_bool(name, default):
|
|
val = os.environ.get(name)
|
|
if val is None or val == "":
|
|
return default
|
|
return val.strip().lower() in ("1", "true", "yes", "y", "on")
|
|
|
|
|
|
def _env_float(name, default):
|
|
val = os.environ.get(name)
|
|
if val is None or val == "":
|
|
return default
|
|
return float(val)
|
|
|
|
|
|
def _env_int(name, default):
|
|
val = os.environ.get(name)
|
|
if val is None or val == "":
|
|
return default
|
|
return int(val)
|
|
|
|
|
|
def parse_args():
|
|
p = argparse.ArgumentParser(description="Generate a prettymaps poster")
|
|
p.add_argument(
|
|
"--query",
|
|
default=_env("QUERY", DEFAULT_QUERY),
|
|
help="Location string for geocoding",
|
|
)
|
|
p.add_argument(
|
|
"--lat",
|
|
type=float,
|
|
default=_env_float("LAT", DEFAULT_LAT),
|
|
help="Latitude (use with --lon instead of --query)",
|
|
)
|
|
p.add_argument(
|
|
"--lon",
|
|
type=float,
|
|
default=_env_float("LON", DEFAULT_LON),
|
|
help="Longitude (use with --lat instead of --query)",
|
|
)
|
|
p.add_argument(
|
|
"--radius",
|
|
type=int,
|
|
default=_env_int("RADIUS_M", DEFAULT_RADIUS_M),
|
|
help="Radius in metres",
|
|
)
|
|
p.add_argument(
|
|
"--circle",
|
|
default=None,
|
|
help="Circular clip: true/false (env CIRCLE)",
|
|
)
|
|
p.add_argument(
|
|
"--title",
|
|
default=_env("TITLE", DEFAULT_TITLE),
|
|
help="Map title",
|
|
)
|
|
p.add_argument(
|
|
"--preset",
|
|
default=_env("PRESET", DEFAULT_PRESET),
|
|
help="prettymaps preset name",
|
|
)
|
|
p.add_argument(
|
|
"--output-name",
|
|
default=_env("OUTPUT_NAME", DEFAULT_OUTPUT_NAME),
|
|
help="Output basename without extension",
|
|
)
|
|
p.add_argument(
|
|
"--dpi",
|
|
type=int,
|
|
default=_env_int("DPI", DEFAULT_DPI),
|
|
help="Output DPI",
|
|
)
|
|
args = p.parse_args()
|
|
|
|
if args.circle is None:
|
|
args.circle = _env_bool("CIRCLE", DEFAULT_CIRCLE)
|
|
else:
|
|
args.circle = str(args.circle).strip().lower() in (
|
|
"1",
|
|
"true",
|
|
"yes",
|
|
"y",
|
|
"on",
|
|
)
|
|
|
|
return args
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
|
|
output_dir = Path("/work") if Path("/work").is_dir() else Path(".")
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
out_png = output_dir / f"{args.output_name}.png"
|
|
out_pdf = output_dir / f"{args.output_name}.pdf"
|
|
|
|
if args.lat is not None and args.lon is not None:
|
|
location = (args.lat, args.lon)
|
|
location_desc = f"({args.lat}, {args.lon})"
|
|
else:
|
|
location = args.query
|
|
location_desc = args.query
|
|
|
|
print(f"Location: {location_desc}")
|
|
print(f"Radius: {args.radius} m, circle={args.circle}, preset={args.preset}")
|
|
print(f"Title: {args.title}")
|
|
print("Downloading OSM data and rendering (this can take several minutes)...")
|
|
|
|
plot = prettymaps.plot(
|
|
location,
|
|
radius=args.radius,
|
|
circle=args.circle,
|
|
figsize=DEFAULT_FIGSIZE,
|
|
preset=args.preset,
|
|
)
|
|
|
|
if args.title:
|
|
plot.ax.set_title(
|
|
args.title,
|
|
fontsize=28,
|
|
pad=20,
|
|
fontfamily="DejaVu Sans",
|
|
)
|
|
|
|
plot.fig.savefig(
|
|
out_png,
|
|
dpi=args.dpi,
|
|
bbox_inches="tight",
|
|
facecolor=plot.fig.get_facecolor(),
|
|
edgecolor="none",
|
|
)
|
|
print(f"Saved: {out_png}")
|
|
|
|
plot.fig.savefig(
|
|
out_pdf,
|
|
bbox_inches="tight",
|
|
facecolor=plot.fig.get_facecolor(),
|
|
edgecolor="none",
|
|
)
|
|
print(f"Saved: {out_pdf}")
|
|
print("Done.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |