Skip to content

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 → Full or Overview (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.

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.