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.
150 lines
7.6 KiB
Markdown
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).
|