Replaces OSM's tan/cream default raster style with CARTO's "Dark Matter" basemap (both RideMap and the route planner's independent TileLayer), so the map itself looks like the dark mockups rather than a light basemap under a dark overlay. Shares one urlTemplate/subdomains/maxNativeZoom across both call sites instead of two independently-drifting copies. Re-points the V3-11 tile cache at a versioned directory (tiles/carto_dark_v1) since TileKey(z, x, y) carries no provider identity and would otherwise silently serve stale tan tiles cached under the old scheme. Added a test proving cache isolation holds across provider directories. Adds a shared TileAttribution widget crediting both OpenStreetMap (CARTO's style is still built from OSM data) and CARTO, composed into every map rather than duplicated per screen -- OSM-only credit stopped being sufficient once a second tile host entered the mix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
155 lines
9.6 KiB
Markdown
155 lines
9.6 KiB
Markdown
# 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 `TextSourceAttribution`s 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).
|