# 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((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((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).