Files
samplez/rippr-flutter-src/docs/ui-redesign/UI-09-monochrome-dark-map-tiles.md
uhryniuk 587f75eab4 Refresh the Flutter snapshot: UI-01 through UI-09, plus a fresh installable APK
Full UI redesign pass complete: persistent tab shell with an always-visible
background map, Modern Professional Dark theme, monochrome dark map tiles,
offline skeleton map, a shared GlassPanel/FloatingPill component kit,
customizable HUD telemetry widgets, and the Map HUD / Plan & Route Planning /
Rides History screen rebuilds. 374 tests passing, up from 316.

The APK is a fresh release build (debug-signed, no release signing config
exists yet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
2026-08-24 14:41:35 -05:00

9.6 KiB

UI-09 — Monochrome dark map tiles

Depends on nothing · Size S · Status Done

Goal

Replace the tan/cream default OpenStreetMap raster style with a monochrome dark map, so the map actually looks like it belongs in a dark-mode HUD instead of a light basemap punched through a dark overlay.

Context

RideMap, the Plan/Route Planning canvas, and every Stitch mockup all assume a dark or near-monochrome map underneath the HUD. Today's TileLayer (ride_map.dart) points at tile.openstreetmap.org, OSM's standard light/tan default style — the actual on-device result is nothing like the mockups, regardless of how dark the chrome on top of it is.

This affects every screen that shows a map (Map HUD, Plan, Route Planning, and UI-01's dimmed background on every other tab), so it's worth settling once, early, rather than each screen ticket discovering the same mismatch independently.

Design

Two real options, not one obvious answer:

Option A — switch tile provider to CARTO's "Dark Matter" basemap. https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png (subdomains a-d, retina {r} variant available, max native zoom 20). Purpose-built dark, desaturated/monochrome cartography — closest to the mockups with the least engineering. Free for reasonable usage same as OSM's own tiles, but it's a different third party with its own usage policy and attribution requirement ("© OpenStreetMap contributors © CARTO", not just OSM's own attribution) — a real dependency to add, not a style tweak.

Option B — keep OSM tiles, apply a color filter client-side. Wrap TileLayer in a ColorFiltered widget using a ColorFilter.matrix that desaturates and inverts/darkens the tan basemap into a monochrome dark look. No new third party, no new attribution, and V3-11's offline tile cache keeps working against the exact tiles it already knows about — but the result is a filtered light map, not tiles actually designed for dark presentation, and will read as slightly "off" next to CARTO's purpose-built version (road/building contrast tuned for light backgrounds doesn't always invert cleanly).

Recommendation: Option A. The mockups' whole aesthetic depends on the map itself being dark, not just tinted dark, and CARTO's dark tiles are a well-established, free, no-API-key option already widely used in the Flutter/Leaflet ecosystem for exactly this. The added attribution line and a second host to trust are a small, one-time cost.

V3-11 interaction: the offline tile cache (FileTileCache/CachedTileProvider) is already keyed by TileKey(z, x, y) with no provider-specific data baked in — but that means a cache built against OSM tiles and a cache built against CARTO tiles are indistinguishable to the cache despite being visually different. Switching providers without a plan here would silently serve stale, wrong-looking cached OSM tiles once CARTO's URL is live. This needs a cache-busting or provider-tagging fix as part of this ticket, not a separate one — see Implementation.

Implementation

  1. Add a provider-qualified cache key (or a cache-version bump that invalidates existing V3-11 cache contents wholesale) so switching tile sources can't silently mix cached tiles from two visually different sources.
  2. Update RideMap's TileLayer (and the Route Planner's separate TileLayer instance — see route_planner_screen.dart) to CARTO's dark tile URL template, correct subdomains, and maxNativeZoom: 20.
  3. Update the attribution requirement — check whether flutter_map's RichAttributionWidget/AttributionWidget is in use anywhere already, or add one; OSM-only attribution is no longer sufficient once CARTO tiles are in the mix.
  4. Update tileUserAgent/any OSM-specific comments in ride_map.dart that assumed OSM's tile servers specifically (the "respect OSM's usage policy" reasoning still applies in spirit to CARTO's own policy, but the specific server being addressed changes).
  5. Re-verify V3-11's tile math/download flow still works end-to-end against the new URL template (tile enumeration and caching are provider-agnostic already; only the fetch URL construction needs updating).

Acceptance criteria

  • The map renders in a dark, desaturated/monochrome style matching the Stitch mockups, not OSM's default tan basemap
  • Correct attribution for both OpenStreetMap and CARTO is shown wherever the map is shown
  • Switching tile providers does not silently serve stale, visually-mismatched cached tiles from before the switch (V3-11's cache is invalidated or provider-tagged)
  • Both RideMap and the Route Planner's independent TileLayer are updated consistently — one dark style everywhere, not just on the screens that happened to get touched first

Tests

  • Existing ride_map/route-planner widget tests updated for the new tile URL and still passing
  • Unit: cache-key/versioning change is exercised the same way V3-11's eviction tests are — a cache built under the old scheme does not get served for the new provider

Risks

  • A second third-party tile host is a real dependency, not a free color swap — its own usage policy, its own risk of being blocked if misused, and its own attribution obligation. Treat it with the same care V3-11 already applies to OSM (rate-limit respecting, no bulk prefetch, real user agent).
  • If CARTO's free tier ever proves insufficient or is discontinued, Option B (a color filter over OSM tiles) is the fallback with no new third-party dependency — worth keeping in mind as a documented Plan B, not re-deriving from scratch later.

Out of scope

A fully custom/self-hosted vector tile style — much larger scope, and not needed to hit "looks like the mockups" today.

Outcome

Took the recommended Option A. ride_map.dart now defines tileUrlTemplate (https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png), tileSubdomains (a-d), and tileMaxNativeZoom (20, kept separate from the app's own maxTileZoom clamp of 19 — raising the app's overall zoom ceiling to match CARTO's native maximum was deliberately left alone rather than folded into this ticket, since it would mean re-verifying the fit/follow-zoom logic V3-04/V3-05 already tuned against 19). Both RideMap and route_planner_screen.dart's independent TileLayer now import and use these same three constants plus retinaMode: true, so there is exactly one dark style and one set of tile-request parameters across the app, not two independently-drifting copies.

Cache versioning (the ticket's named risk): tileCacheProvider in app/providers.dart now points FileTileCache at tiles/carto_dark_v1 instead of the old bare tiles directory. TileKey(z, x, y) still carries no provider identity, so the directory segment is the actual version tag — any tiles cached under the old OSM-tan scheme are simply orphaned in a directory the app no longer reads from, rather than being silently served under the new dark UI. Added a unit test (test/tile_cache_test.dart, "a tile cached under one provider directory is not served from another") proving this isolation holds at the FileTileCache level, the same way V3-11's own eviction tests exercise cache behavior directly rather than through the UI.

Attribution: added a shared TileAttribution widget (ride_map.dart) wrapping flutter_map's RichAttributionWidget with TextSourceAttributions for both "OpenStreetMap contributors" (CARTO's dark style is still built from OSM's underlying data) and "CARTO" itself — OSM-only credit, which is what the app carried before, stopped being sufficient the moment a second tile host entered the mix. Composed into both RideMap and the route planner's FlutterMap, so it's one component discharging the obligation everywhere a map renders, not a copy-pasted attribution block per screen. Did not wire up onTap license-page links — that would need the url_launcher package, a new dependency this ticket has no other reason to add; the obligation is to visibly credit both sources, not to make the credit tappable.

Tests: flutter analyze clean. flutter test green at 316 tests (315 + the new cache-isolation test) — no existing test hardcoded the old OSM URL string, so nothing else needed updating.

Android emulator verification (Medium_Phone_API_35): confirmed CARTO's dark, desaturated basemap renders on-device (a genuinely dark map, not a tan basemap under a dark overlay) — a clear visual match for the Stitch mockups' aesthetic, on the Map tab at full opacity and dimmed correctly on Rides/Plan/Settings via UI-01's existing scrim. The attribution icon is visibly present in the bottom-left corner on every tab (same shared map instance). Its tap-to-expand interaction could not be confirmed via adb input tap in this session — taps at its on-screen coordinates didn't visibly toggle the popup, and a genuine Android mock-location watermark icon (left over from this session's earlier adb emu geo fix calls, and confirmed present at the same screen position across unrelated tabs, which a page-level widget couldn't be) sits immediately next to it, making the exact tap target ambiguous to hit blindly. This is a dev-tooling verification gap, not a known defect — the widget itself renders without error and matches flutter_map's standard, widely-shipped attribution pattern (the same small-icon-that-expands convention Google Maps and Mapbox both use). Re-verify the popup's tap behavior with a real touchscreen or Flutter Inspector if it becomes load- bearing later (e.g., if legal review specifically requires confirming the expand interaction, not just the icon's presence).