Replace Leaflet/raster tiles with MapLibre GL JS: an online OpenFreeMap style and an offline style built from a local PMTiles archive via pmtiles.js and protomaps-themes-base (no glyphs/labels offline by design, see docs/architecture.adoc). Frontend libraries are vendored locally rather than loaded from a CDN so the app keeps working with no internet access. Also add an animated RainViewer precipitation radar overlay (toggle, play/pause, scrub slider), matching the default time window of the weather-radar-card Home Assistant integration.
6.2 KiB
Architecture
Overview
MapTool is a Sinatra web application. The desktop build does not
reimplement anything natively — it wraps the same Sinatra app in a
Tauri window, which is the standard way to ship a
non-JavaScript backend as a desktop app with Tauri: the Rust shell owns
the native window and starts the web server as a local process; the
window then simply loads http://127.0.0.1:4567 as if it were a
regular page.
+----------------------------+
| Tauri window (WebView) |
| loads http://127.0.0.1:PORT
+--------------+-------------+
|
v
+----------------------------+
| Sinatra app (Puma) |
| app.rb / config.ru |
+----------------------------+
Directory layout
| Path | Purpose |
|---|---|
|
Sinatra application and Rack entry point. |
|
Puma server configuration (binds to |
|
Frontend assets and Sinatra templates. The map view uses MapLibre GL JS, rendering vector tiles. |
|
Third-party frontend libraries, vendored (not loaded from a CDN) so
the app also works with no internet access. See
|
|
Local, gitignored data files, e.g. |
|
The Tauri (Rust) desktop shell. |
|
Project documentation, in AsciiDoc. |
Desktop shell (Tauri)
In development, src-tauri/tauri.conf.json sets beforeDevCommand
to start Puma, and devUrl points the window at it. Running the dev
build (see docs/development.adoc) starts both together.
In a packaged build there is no beforeDevCommand hook, so
src-tauri/src/lib.rs starts the server itself (bundle exec puma)
next to the bundled resources when the app launches, and stops it when
the window closes.
Known limitation: production packaging
The packaged-build server spawn currently assumes a Ruby + Bundler installation already present on the machine running the app. That is fine for internal or developer builds, but not for a distributable, self-contained desktop app.
Shipping a real self-contained build would mean embedding a Ruby runtime as a Tauri sidecar binary (for example, produced with tebako), instead of relying on the system Ruby. That decision — and the resulting sidecar build step — is not implemented yet and should be revisited before any real distribution build.
Offline maps
Map data is vector, not raster: the app ships
MapLibre GL JS (vendored, see below) and the map
view (public/js/map.js) offers two styles:
-
Online — a full hosted vector style, OpenFreeMap "liberty", requires internet access.
-
Offline — built at runtime from a local PMTiles archive at
data/offline.pmtiles, served as-is (see below) at/tiles/offline.pmtiles, decoded client-side by pmtiles.js registered as apmtiles://protocol handler, and styled with base layers from protomaps-themes-base.
GET /api/offline-tiles reports whether data/offline.pmtiles is
present ({"available": true|false}); the frontend uses it once, on
load, to default to the offline style when local tile data exists —
otherwise it defaults to the online style. Either way, the layer
toggle in the top-left of the map lets the user switch manually.
PMTiles is a single, self-indexed file, which is why this needs no
tile-decoding logic on the server: app.rb’s `/tiles/offline.pmtiles
route (send_file, which Sinatra serves with HTTP range-request
support) just returns byte ranges of the file; pmtiles.js in the
browser reads the archive’s own directory and requests only the bytes
it needs for the tiles on screen.
Known limitation: no offline text labels
The offline style only renders `protomaps-themes-base’s non-label layers (roads, buildings, land use, water — no place names or street labels). Label rendering in MapLibre needs a "glyphs" server serving font PBF ranges, which would mean vendoring a font glyph set locally too; that has not been done, to keep the initial scope manageable. Revisit this if labelled offline maps turn out to matter in practice.
No PMTiles file ships with the repository — it is expected to be
supplied locally (see docs/development.adoc) and stays gitignored
because such extracts are typically large binary files.
Weather radar overlay
public/js/radar.js adds a "Radar" map control that overlays recent
precipitation radar from RainViewer's free
public API (no API key required). Enabling it:
-
Fetches
https://api.rainviewer.com/public/weather-maps.json, which lists RainViewer’s past radar frames (fixed 10-minute cadence, at most 13 covering the last 120 minutes). -
Takes the most recent 7 (last ~60 minutes — the same default as the Home Assistant weather-radar-card, for a consistent feel with that setup) and adds one MapLibre raster source/layer per frame, all pre-loaded but only the current frame’s
raster-opacitynon-zero. -
Animates by toggling that opacity across frames on an interval (play/pause + a scrub slider), rather than swapping tiles in and out — avoids reloading tiles on every step.
Tile URL: {host}{frame.path}/256/{z}/{x}/{y}/2/1_0.png — 2 is
RainViewer’s "Universal Blue" color scheme, 1 enables their
smoothing, 0 disables the snow overlay. host and each frame’s
path come from the API response.
Since map.setStyle() (switching Online/Offline) tears down all
custom sources and layers, the control listens for style.load and
re-adds the radar layers if the overlay was on.
This is a live, online-only overlay — it always needs internet access and is unrelated to the offline PMTiles basemap above. Per RainViewer’s terms, the API is free for personal/non-commercial use.
Networking
The Sinatra server only binds to 127.0.0.1. It is not meant to be
reachable from other machines; the Tauri window talks to it over
loopback only.