Files
maptool/docs/architecture.adoc
T
randy 5afb130c45 Switch to vector maps (MapLibre GL JS + PMTiles) and add RainViewer radar
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.
2026-08-25 01:18:36 +02:00

165 lines
6.2 KiB
Plaintext

= Architecture
:toc:
:toclevels: 2
== Overview
MapTool is a Sinatra web application. The desktop build does not
reimplement anything natively — it wraps the same Sinatra app in a
https://tauri.app[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.
[source]
----
+----------------------------+
| Tauri window (WebView) |
| loads http://127.0.0.1:PORT
+--------------+-------------+
|
v
+----------------------------+
| Sinatra app (Puma) |
| app.rb / config.ru |
+----------------------------+
----
== Directory layout
[cols="1,3"]
|===
| Path | Purpose
| `app.rb`, `config.ru`
| Sinatra application and Rack entry point.
| `config/puma.rb`
| Puma server configuration (binds to `127.0.0.1` only — the app is
local-only, not exposed on the network).
| `public/`, `views/`
| Frontend assets and Sinatra templates. The map view uses MapLibre GL
JS, rendering vector tiles.
| `public/vendor/`
| Third-party frontend libraries, vendored (not loaded from a CDN) so
the app also works with no internet access. See
`docs/third-party-notices.adoc`.
| `data/`
| Local, gitignored data files, e.g. `offline.pmtiles`.
| `src-tauri/`
| The Tauri (Rust) desktop shell.
| `docs/`
| 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 https://tauri.app/develop/sidecar/[sidecar] binary
(for example, produced with https://github.com/tebako/tebako[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
https://maplibre.org[MapLibre GL JS] (vendored, see below) and the map
view (`public/js/map.js`) offers two styles:
* *Online* — a full hosted vector style,
https://tiles.openfreemap.org/styles/liberty[OpenFreeMap "liberty"],
requires internet access.
* *Offline* — built at runtime from a local
https://github.com/protomaps/PMTiles[PMTiles] archive at
`data/offline.pmtiles`, served as-is (see below) at
`/tiles/offline.pmtiles`, decoded client-side by
https://github.com/protomaps/PMTiles[pmtiles.js] registered as a
`pmtiles://` protocol handler, and styled with base layers from
https://github.com/protomaps/basemaps[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 https://www.rainviewer.com[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 https://github.com/jpettitt/weather-radar-card[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-opacity` non-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.