Skip to content

ADR-0032: Theme-aware offline basemap

Context

ADR-0030 shipped one hand-rolled Protomaps-schema style asset (assets/maps/style.json) with its paint colors inline — light only (paper background, pastel greens, tan roads). The app meanwhile resolves a SYSTEM/LIGHT/DARK theme in MainActivity, so in dark mode the whole editor went dark while the location picker stayed a bright paper-toned map.

Restyling options considered besides tokenizing:

  • a second full dark style asset duplicating all 17 layers — structure must evolve in lockstep (drift-prone);
  • runtime restyling through MapLibre's style controller — imperative, more moving parts, harder to keep deterministic.

Decision

  • One template asset. Every paint color in style.json is an __UPPER_SNAKE__ token (18 tokens). OfflineMapStyle.apply(template, pmtilesUrl, dark) substitutes the tokens plus the existing __PMTILES_URL__ against an ordered Kotlin palette table (MapColor(token, light, dark)); load(context, url, dark) reads the asset then delegates to it.
  • Light hexes are the pre-ADR-0032 values verbatim; dark tones are green-tinted to echo the app's Material dark surfaces (ui/theme/Color.kt) rather than copying Protomaps' neutral dark palette.
  • Label halos equal the background color in both palettes — that keeps text legible on either surface. (Light street-label halos shift white → paper; the only visible light-mode delta.)
  • Resolved flag below the theme wrapper: AutoBudgetTheme provides the collapsed SYSTEM/LIGHT/DARK boolean as LocalDarkTheme; the picker keys its remember(tileUrl, dark) style build on it, so a mid-session theme flip rebuilds the style string, MapLibre re-sets it, and the hoisted camera keeps position.
  • Pure string ops on the JVM — substitution is unit-tested directly: no unresolved tokens remain, output parses as JSON, palettes differ.

Consequences

  • Adding or retinting a layer touches exactly two places: the asset (structure + tokens) and the palette table. A JVM test reads the real asset and fails if any literal color or unresolved token remains.
  • New layers default to an existing token unless a new one is added to both palettes — coarse granularity keeps the palettes readable.
  • The ADR-0026 grid-canvas fallback already derives from MaterialTheme.colorScheme, so it needed no change here.