V3-07: route drawing (pins and straight lines)

Route plans are their own entity (RoutePlan/Waypoint, schema v3), never joined into ride totals or the rides list. RoutePlanRepository recomputes straight-line distance after every waypoint mutation. RoutePlannerScreen supports tap-to-add, tap-to-delete, and hand-rolled drag-to-move via MapCamera's screen/latlng conversions.

Fixed a real bug found by testing: FlutterMap's initialCenter/initialZoom are read once at construction, so building the map before the waypoints stream's first emission froze the camera on null-island. Also documents a genuine ten-minute test hang traced to Stream.first on a Drift watch() query inside testWidgets, which needs a pump-driven Timer that flutter_test's fake zone never advances on its own.
This commit is contained in:
2026-08-17 19:15:18 -05:00
parent 5013b7002f
commit a14f5b0134
15 changed files with 2694 additions and 5 deletions

View File

@@ -10,6 +10,8 @@ import 'package:go_router/go_router.dart';
import 'detail/trip_detail_screen.dart';
import 'record/record_screen.dart';
import 'routes/route_planner_screen.dart';
import 'routes/routes_list_screen.dart';
import 'settings/settings_screen.dart';
import 'trips/trips_screen.dart';
@@ -18,8 +20,11 @@ abstract final class Routes {
static const trips = '/trips';
static const tripDetail = '/trip/:tripId';
static const settings = '/settings';
static const routes = '/routes';
static const routePlanner = '/routes/:routeId';
static String detailFor(int tripId) => '/trip/$tripId';
static String plannerFor(int routeId) => '/routes/$routeId';
}
GoRouter buildRouter() => GoRouter(
@@ -30,8 +35,24 @@ GoRouter buildRouter() => GoRouter(
builder: (context, state) => RecordScreen(
onOpenTrips: () => context.push(Routes.trips),
onOpenSettings: () => context.push(Routes.settings),
onOpenRoutes: () => context.push(Routes.routes),
),
),
GoRoute(
path: Routes.routes,
builder: (context, state) => RoutesListScreen(
onOpenRoute: (id) => context.push(Routes.plannerFor(id)),
onBack: () => context.pop(),
),
),
GoRoute(
path: Routes.routePlanner,
builder: (context, state) {
final id = int.tryParse(state.pathParameters['routeId'] ?? '');
if (id == null) return const _NotFound();
return RoutePlannerScreen(routeId: id, onBack: () => context.pop());
},
),
GoRoute(
path: Routes.settings,
builder: (context, state) =>