ADR-0070: Offline map preview in the transaction editor
- Status: Accepted
- Date: 2026-10-17
Context
The editor's Location section (ui/transactions/TransactionDetailScreen.kt,
ADR-0033) showed only formatted coordinates in a row that opened the picker on
tap. A transaction already carrying a captured location therefore looked like it
had none until the user expanded the section and read the numbers, and the whole
row was one big click target that competed with the form's scroll. Issue #189
asks for a visible preview that reuses the offline basemap pipeline, never an
online or fabricated map, and that keeps the coordinates honest when map data is
missing, corrupt, overview-only or outside the pack.
Decision
ui/components/LocationPreview.kt renders the section — a shared component
alongside CameraPinMap — driven by the pure selector in
ui/transactions/LocationPreviewState.kt. It reuses
CameraPinMap and OfflineMapStyle — the same pmtiles://file:// style asset,
light/dark token resolution (LocalDarkTheme, ADR-0032) and fixed pin as the
picker — so no tile/style loading is duplicated and no network is touched
(ADR-0015, ADR-0030). The map area is a bounded 180 dp tall surface: inside the
editor's scrolling column the max height is unbounded, and MaplibreMap's
fillMaxSize() collapses to zero when given only a minimum, so a finite height
is required for the basemap to draw. The coordinate text, overview label and
Edit action sit outside it, so content can grow the card instead of being
clipped.
The preview is passive. CameraPinMap takes interactions/uiOptions, and the
preview passes MapInteractions.None and MapUiOptions.None, so pan/zoom/tap never
capture the form's vertical scroll and the map body is not a button. Rendering
triggers no GPS fix, permission prompt or network call; only the explicit Edit
location action (or Choose location when no point exists) opens the existing
full-screen picker, whose Save/Cancel/Clear behaviour is unchanged.
One pure function selects the state —
locationPreviewState(point, MapPackPickerState) -> LocationPreviewState:
- no saved point →
NoLocation(Choose location only); - saved point, pack still resolving →
Loading; - saved point + usable pack →
FullorOverview(limited-detail); - saved point + no pack installed →
Missing; - saved point + pack present but tiles unresolved →
Corrupt; - saved point outside the pack's header extent →
OutOfCoverage.
MapPackPickerState now carries the pack's GeoBounds, read from the PMTiles
v3 header (Pmtiles.bounds, GeoBounds.contains). An unknown extent never
claims out-of-coverage, and a fallback state never erases the saved coordinates —
"Choose location" stays reserved for the absence of a point.
Consequences
A saved location is immediately visible: the section seeds expanded (ADR-0033) and shows the preview or its honest fallback, and picking a point auto-expands the section. The pin is decorative (no content description) while the coordinate text and the named Edit action carry the TalkBack announcement, so decorative map/pin nodes are never redundant controls and the map's own attribution stays reachable. Because the preview branches through one tested function, adding a future map source is a change to the branch table, not to inline UI logic.
Related
- ADR-0015 — offline-first: no app network I/O
- ADR-0026 — offline coordinate picker
- ADR-0030 — offline vector map pack
- ADR-0032 — theme-aware offline basemap
- ADR-0033 — transaction editor sections
Verification
LocationPreviewStateTest (JVM) pins the branch table: no point → NoLocation,
resolving → Loading, full/overview packages, no pack → Missing, unresolved
tiles → Corrupt, outside-bounds → OutOfCoverage, and unknown bounds never
claiming out-of-coverage. MapPackImportTest covers Pmtiles.bounds header
parsing.
scripts/ui-guard.sh keeps the preview on the shared shape/surface roles and
CameraPinMap's read-only path is the only map renderer.