Files
rippr/docs/feedback/FB-07-route-planner-null-island.md
uhryniuk d9aa412e15 Write FB-06..FB-09 feedback tickets (round two)
Turns the newly-added issues in docs/FEEDBACK.md into four more
self-contained tickets: always-interactive map with a recenter
button, Route Planner's Null Island centering bug, HUD reflow-on-hide,
and title-driven HUD sizing. Sequenced FB-08 before FB-09 since both
rework the same HUD layout files.
2026-08-24 23:26:49 -05:00

150 lines
7.6 KiB
Markdown

# FB-07 — Route Planner opens on Null Island instead of the rider's real location
**Depends on** FB-01 (reuses its ambient-location provider) · **Size** S/M · **Status** Not started
## Goal
A new route, or a route with fewer than two pins, must open the map on the rider's real
location. Today it opens on the middle of the ocean.
## Context
Direct user feedback (`docs/FEEDBACK.md`):
> Pin drops still do not work at all, the map when placing the pins doesn't render at a
> all, it's just a blank canvas that you can zoom in and out of but no detail appears.
> The pins can also be placed but nothing is rendered on the map so you have no clue
> where they are.
`lib/src/ui/routes/route_planner_screen.dart`, `build()` (~line 165):
```dart
child: FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCameraFit: _initialFit(waypoints),
initialCenter: waypoints.isEmpty
? const ll.LatLng(0, 0)
: ll.LatLng(waypoints.first.latitude, waypoints.first.longitude),
initialZoom: waypoints.length <= 1 ? ambientZoom : maxTileZoom - 3,
maxZoom: maxTileZoom,
onTap: (tapPosition, point) {
repo.addWaypoint(widget.routeId, point.latitude, point.longitude);
...
```
`ll.LatLng(0, 0)` is Null Island — a single point in the Atlantic Ocean with no land
and no map detail at any zoom level. A brand-new route (0 pins), or a route with exactly
1 pin dropped near that point, opens the map centered there. At street-level zoom, an
area of open ocean legitimately renders as a flat, featureless expanse — this matches
the report exactly: "a blank canvas... no detail appears," and a pin dropped anywhere
near that same spot lands in the same empty area, with nothing else on screen to show
where it is relative to.
`RoutePlannerScreen` manages its own `FlutterMap` directly — it does not use `RideMap`
and does not currently read `ambientPositionProvider` at all (confirmed by grep: no
reference to `ambientPositionProvider` anywhere in `route_planner_screen.dart`). FB-01
already built exactly the provider this ticket needs — `lib/src/app/providers.dart`
(~line 66):
```dart
final ambientPositionProvider = StreamProvider.autoDispose<LocationFix?>((ref) async* {
if (!ref.watch(mapEnabledProvider)) {
yield null;
return;
}
final source = ref.watch(locationSourceProvider);
try {
await source.start(); // idempotent; safe even if a recording already started it
} on LocationException {
yield null; // permission denied / service disabled — ambient mode is best-effort
return;
}
yield* source.fixes.map<LocationFix?>((fix) => fix);
});
```
`LocationFix` is defined in `lib/src/recording/location_source.dart`.
**`initialCenter`/`initialZoom` are read exactly once, at `FlutterMap` construction —
not on every rebuild.** The existing comment in this file already documents this
constraint for the waypoints stream (~line 158): the screen waits for
`waypointsAsync.hasValue` before building `FlutterMap` at all, specifically so the
first real camera position is correct from the start. The same constraint applies here:
reading `ambientPositionProvider` must happen before `FlutterMap` is constructed, not
patched in afterward with a controller move — mirror the existing wait-for-first-value
pattern, do not invent a new one.
## Design
- Watch `ambientPositionProvider` in `build()`, the same way `ShellScaffold` already
does (`lib/src/ui/app_shell.dart` ~line 74):
```dart
final ambientFix = ref.watch(ambientPositionProvider).valueOrNull;
final ambientPosition = ambientFix == null
? null
: ll.LatLng(ambientFix.latitude, ambientFix.longitude);
```
- Use `ambientPosition` for `initialCenter` when there are fewer than 2 waypoints,
falling back to `(0, 0)` only when no ambient fix is available yet (permission denied,
service disabled, or the fix has not arrived yet):
```dart
initialCenter: waypoints.isEmpty
? (ambientPosition ?? const ll.LatLng(0, 0))
: ll.LatLng(waypoints.first.latitude, waypoints.first.longitude),
```
- Do **not** block the map on waiting for the ambient fix the way `waypointsAsync` is
blocked on its own first value. A missing ambient fix must fall back to `(0, 0)`
immediately, not show a spinner — the rider must always eventually reach a usable map,
even with location permission denied. This matches the existing `RideMap` fallback
behavior in FB-01 exactly (see `ride_map.dart`'s `bounds == null` branch).
- Leave the 1-waypoint and 2-plus-waypoint camera logic unchanged — a route that already
has a real pin should still center on that pin, not on the ambient position, since the
pin is a stronger signal of where the rider actually wants to look.
- **Confirm tiles genuinely render once centered on a real location.** After this fix,
drop a pin near a real city on the Android emulator (use `adb emu geo fix` to set a
real location, as FB-01's own verification pass did). Take a screenshot. Confirm
street-level map detail actually appears, not just a differently-colored blank area.
If tiles still do not render even at a real location, that is a second, distinct bug —
investigate `TileLayer`'s setup in this file (`urlTemplate`, `tileProvider`,
`mapConnectivityProvider`'s `skeletonMode`) before assuming this ticket's fix is
sufficient, and document the real cause in the Outcome section.
## Implementation
1. Add `final ambientFix = ref.watch(ambientPositionProvider).valueOrNull;` and the
`ambientPosition` conversion to `build()`.
2. Change the `initialCenter` fallback for the empty-waypoints case from `const
ll.LatLng(0, 0)` to `ambientPosition ?? const ll.LatLng(0, 0)`.
3. Run the app on the Android emulator. Set a real mock location. Open a new route.
Confirm the map opens on that location with visible street detail, not open ocean.
## Acceptance criteria
- [ ] A brand-new route (0 pins) opens the map centered on the rider's real location,
when a location fix is available.
- [ ] A brand-new route still opens on `(0, 0)` when no location fix is available
(permission denied, service disabled, or no fix yet) — no crash, no infinite
spinner.
- [ ] A route with 1 or more pins still centers on the pin, unchanged from today.
- [ ] Dropping a pin near a real city on the emulator shows visible street-level map
detail underneath it, confirmed by a real screenshot.
- [ ] `flutter analyze` clean, `flutter test` green, test count only goes up.
## Tests
- Widget test: pump `RoutePlannerScreen` for a route with 0 waypoints, with
`ambientPositionProvider` overridden to a fixed `LocationFix` — assert the
`FlutterMap`'s `options.initialCenter` matches that fix, not `(0, 0)`.
- Widget test: pump `RoutePlannerScreen` for a route with 0 waypoints, with
`ambientPositionProvider` overridden to emit `null` (no fix available) — assert
`initialCenter` falls back to `(0, 0)`, matching today's existing behavior.
- Widget test: pump `RoutePlannerScreen` for a route with 1 real waypoint — assert
`initialCenter` still matches that waypoint's own coordinates, not the ambient
position, even when `ambientPositionProvider` emits a different fix.
## Risks
- If the on-device check in this ticket's Implementation step 3 finds tiles still do
not render at a real location, this ticket's fix alone is not sufficient — document
the real root cause found and either fix it in this same ticket or state plainly in
the Outcome section that a further ticket is needed. Do not claim this ticket is done
without confirming real map detail actually appears on a screenshot.
## Out of scope
Any change to the Map tab's own map (FB-06 covers its remaining issues). Turn-by-turn
route following (V3-09, already deferred elsewhere).