Choosing a town moved the camera and then left the reader to work out which of the settlements now on screen was the one they asked for. A town has no precise position, so the map now marks its centre: a white disc under a coloured dot, drawn as a style layer so the native renderer keeps it pinned through every pan and zoom. The radar frames are inserted below it, because a band of rain must not paint over the one thing that says which town this is. A searched municipality becomes a PointTarget rather than being thrown away as "the whole region". It is marked, the recentre button returns to it, and the chip names it - but it is never persisted, so looking something up no longer costs the user the place their app opens on. That was a real defect: searching went through select(FreeTarget), which wrote null over the stored preference. Following the device was broken outright. MapLibre reports a camera animation the app started exactly as it reports the user grabbing the map, so engaging follow and then animating to the position cancelled the follow it had just started; the resulting FreeTarget then re-framed the whole region. Follow now moves the camera first and switches tracking on second, and FreeTarget no longer moves the camera as a reaction to the state changing - framing the region is an action, so panning away while following keeps the view the user panned to. Verified on the emulator with a fix in Turin: a searched town is marked and saveable, the app reopens on it with the marker in place, following centres on the blue dot, and panning stops the chase without yanking the map away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nuvolari
Precipitation radar for Piedmont, Italy. Android first, iOS later, one Flutter codebase.
Radar imagery comes from the public Radar-DPC platform and weather alerts from the ARPA Piemonte XML-CAP bulletin, drawn over an OpenStreetMap base map. The app is free, with no advertising and no accounts.
Scope: radar, official alerts, rain notifications. No forecasts, no lightning, no home-screen widget. See CLAUDE.md.
Nuvolari is an independent app. It is not affiliated with, endorsed by, or operated by ARPA Piemonte or the Dipartimento della Protezione Civile. For civil-protection purposes the official channels always prevail.
Layout
app/ Flutter application (Dart package "nuvolari")
backend/ Python worker: fetches DPC rasters, crops, reprojects, renders PNG frames
docs/ architecture, data sources, licenses, stack decisions, roadmap, privacy
tool/ verification scripts
Start with docs/architecture.md, then docs/roadmap.md for what is built and what is next.
Toolchain
| Tool | Version |
|---|---|
| Flutter | 3.47.3 stable (Dart 3.13.3) |
| Android SDK | platform 36, build-tools 36.0.0, platform-tools |
| JDK | 21 |
| Python | 3.12+ (3.14 works; rasterio ships wheels for it) |
| Android NDK | 28.2.13676358 — pinned by maplibre_gl, not by this project |
A full toolchain plus a warm Gradle cache needs roughly 14 GB of disk: Flutter
3 GB, Android SDK 2.5 GB (2 GB of which is the NDK the map plugin pins), the Gradle
cache 5 GB, and build output around 2 GB. flutter clean reclaims the last of those.
Configuration
Secrets never enter the repository. Copy the template and fill it in:
cp env.example.json env.json # env.json is git-ignored
| Key | Purpose |
|---|---|
MAP_STYLE_URL |
MapLibre style URL. Empty uses the free OpenFreeMap style; offline forces the bundled no-network style. |
RADAR_MANIFEST_URL |
Base URL of the published manifest.json. |
RADAR_SOURCE |
mock (offline demo), dpc (live), arpa (disabled stub). |
REGION_ID |
Which region config to load. Defaults to piemonte. |
Running
cd app
flutter run --dart-define-from-file=../env.json
With no env.json the app starts in demo mode: mock radar frames from assets on the
free OpenFreeMap base map. No credentials are needed for anything — OpenFreeMap has
no API key and no signup.
The radar frames work with no network at all. The base map needs one; run with
--dart-define=MAP_STYLE_URL=offline to drop it too and work entirely offline.
Debugging in VS Code
Open the Nuvolari folder as the workspace root — the configurations in
.vscode/ are relative to it. Then pick one in Run and Debug and press F5:
| Configuration | Use it for |
|---|---|
| Nuvolari — debug (emulatore) | Everyday work. Demo mode, no env.json needed, hot reload on save. |
| Nuvolari — profile | Judging animation smoothness. Debug builds run the Dart VM unoptimised, so the radar timeline always looks worse than it is — never assess it in debug. |
| Nuvolari — debug con env.json | Pointing the app at a live radar manifest or a different base map style. |
| Nuvolari — debug su Windows | Fast UI iteration with no emulator. The native map does not render here. |
| Test — tutti / Test — file corrente | Debugging tests with breakpoints. |
Every emulator configuration runs tool/start_emulator.ps1 first, so F5 works from a
cold machine. The script exits immediately when a device is already attached, so
pressing F5 twice does not start two emulators.
Ctrl+Shift+B runs the full verification. Other tasks live under
Terminal → Run Task: quick verification, regenerating the demo radar frames, and
shutting the emulator down.
Verifying
.\tool\verify.ps1 # format, analyze, test, build, lint, pytest
.\tool\verify.ps1 -SkipBuild # fast inner loop
Stages whose target does not exist yet are skipped, so this runs from day one.
Attribution
Radar-DPC (CC BY-SA) · Arpa Piemonte · © OpenStreetMap contributors (ODbL) · © OpenMapTiles · OpenFreeMap.
The credits are on the Sources screen, reached from the settings button over the map. There is no permanent credit line on the map itself: the OSMF attribution guidelines allow the licence information to sit behind an "About option in a menu" so long as it stays findable, and a test asserts that route still exists.
See docs/licenses.md for the obligations these carry — in
particular, the rendered radar frames are a derived product of CC BY-SA data and inherit
share-alike, and the OpenFreeMap style ships no attribution field, so the app has to
render the map credits itself.