"""Location quad: a static OpenStreetMap image centred on the device's IP-geolocated position, with a marker. The position comes from the shared LocationService (backend/geolocate.py), which traceroutes to 1.1.1.1, takes the first public hop (the ISP egress) and resolves it through a public geolocation API. The "Locate via IP" feature toggle gates that lookup entirely; when off, this quad shows a placeholder instead. We render a static map (backend/staticmap.py) rather than an interactive libshumate map: Shumate does not paint tiles in this environment (the official shumate-demo shows the same blank map), while tile downloads themselves work fine. The Weather quad consumes the same LocationService. """ from __future__ import annotations import sys import gi gi.require_version("Gtk", "4.0") from gi.repository import Gtk # noqa: E402 from lib.proc import run_text from module_base import Feature, ModuleContext, ModuleInstance, ModuleSpec from paths import BACKEND_DIR, CACHE_DIR _SCRIPT = str(BACKEND_DIR / "staticmap.py") class _MapView(Gtk.Box): def __init__(self, ctx: ModuleContext, zoom: int, size: tuple[int, int], tag: str, show_info: bool): super().__init__(orientation=Gtk.Orientation.VERTICAL) self.add_css_class("map-view") self.ctx = ctx self.zoom = zoom self.size = size self._out = str(CACHE_DIR / f"map_{tag}.png") self.picture = Gtk.Picture(content_fit=Gtk.ContentFit.COVER, vexpand=True) self.picture.add_css_class("map-picture") self.append(self.picture) self._info = None if show_info: self._info = Gtk.Label(label="Locating…", xalign=0.0) self._info.add_css_class("map-info") self.append(self._info) ctx.services.location.subscribe(self._on_location) def _on_location(self, data: dict) -> None: lat, lon = data["lat"], data["lon"] w, h = self.size run_text([sys.executable, _SCRIPT, str(lat), str(lon), str(self.zoom), str(w), str(h), self._out], self._on_rendered) if self._info is not None: city = data.get("city") or "Unknown" country = data.get("country") or "" self._info.set_text(f"{city}, {country} · {lat:.3f}, {lon:.3f}") def _on_rendered(self, ok: bool, out: str, err: str) -> None: if ok: self.picture.set_filename(self._out) def _placeholder() -> Gtk.Widget: box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8, valign=Gtk.Align.CENTER, halign=Gtk.Align.CENTER) box.add_css_class("map-view") box.append(Gtk.Label(label="IP location is off")) return box def build(ctx: ModuleContext) -> ModuleInstance: # "Locate via IP" gates geolocation entirely: when off we neither traceroute nor # hit any geolocation API, and just show a placeholder. Toggling it rebuilds the # card (via QuadCard), so flipping it back on re-triggers the lookup. if not ctx.feature("ip_locate", True): return ModuleInstance(compact=_placeholder(), expanded=None) ctx.services.location.get() # kick off geolocation (traceroute → API) if not started compact = _MapView(ctx, zoom=12, size=(620, 150), tag="compact", show_info=False) expanded = _MapView(ctx, zoom=13, size=(1100, 620), tag="expanded", show_info=True) return ModuleInstance(compact=compact, expanded=expanded, scroll_expanded=False) SPEC = ModuleSpec( id="location", title="Location", icon="", # nf-fa-map_marker build=build, features=[Feature("ip_locate", "Locate via IP", True)], )