#!/usr/bin/env python3 """Render a static OpenStreetMap image centred on a coordinate, with a marker. Used by the Location quad because libshumate does not render tiles in this environment (the official shumate-demo shows the same blank map — a library/GTK render bug), while plain tile downloads work fine. Stitches OSM raster tiles into one PNG with Pillow and caches them. staticmap.py """ from __future__ import annotations import io import math import os import sys import urllib.request from pathlib import Path from PIL import Image, ImageDraw TILE = 256 CACHE = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "astal-menu" / "tiles" UA = "astal-menu/1.0 (personal dotfiles)" BG = (26, 26, 26) ACCENT = (228, 0, 70) def _center_px(lat: float, lon: float, zoom: int) -> tuple[float, float]: n = 2 ** zoom lat_r = math.radians(lat) x = (lon + 180.0) / 360.0 * n y = (1.0 - math.asinh(math.tan(lat_r)) / math.pi) / 2.0 * n return x * TILE, y * TILE def _tile(z: int, x: int, y: int) -> Image.Image | None: CACHE.mkdir(parents=True, exist_ok=True) fp = CACHE / f"{z}_{x}_{y}.png" if fp.exists(): try: return Image.open(fp).convert("RGB") except Exception: pass url = f"https://tile.openstreetmap.org/{z}/{x}/{y}.png" try: req = urllib.request.Request(url, headers={"User-Agent": UA}) data = urllib.request.urlopen(req, timeout=8).read() except Exception as exc: print(f"tile {z}/{x}/{y}: {exc}", file=sys.stderr) return None fp.write_bytes(data) return Image.open(io.BytesIO(data)).convert("RGB") def render(lat: float, lon: float, zoom: int, w: int, h: int, out: str) -> None: n = 2 ** zoom cx, cy = _center_px(lat, lon, zoom) left, top = cx - w / 2, cy - h / 2 img = Image.new("RGB", (w, h), BG) x0, x1 = int(left // TILE), int((left + w) // TILE) y0, y1 = int(top // TILE), int((top + h) // TILE) for tx in range(x0, x1 + 1): for ty in range(y0, y1 + 1): if ty < 0 or ty >= n: continue tile = _tile(zoom, tx % n, ty) if tile is None: continue img.paste(tile, (int(tx * TILE - left), int(ty * TILE - top))) d = ImageDraw.Draw(img) mx, my, r = w // 2, h // 2, 8 d.ellipse([mx - r, my - r, mx + r, my + r], fill=ACCENT, outline=(255, 255, 255), width=2) Path(out).parent.mkdir(parents=True, exist_ok=True) img.save(out) if __name__ == "__main__": a = sys.argv render(float(a[1]), float(a[2]), int(a[3]), int(a[4]), int(a[5]), a[6]) print(a[6])