FB-05: closed-loop routes in Route Planner

Add an isClosedLoop toggle to route plans (schema v3 -> v4 migration),
threaded through the domain model, repository distance recomputation,
the overflow menu, and the drawn polyline, so a route can loop back to
its first waypoint.
This commit is contained in:
2026-08-24 16:22:51 -05:00
parent 57bc3c9233
commit 9ac0be6ee5
9 changed files with 359 additions and 8 deletions

View File

@@ -113,6 +113,61 @@ void main() {
expect(await db.waypointsForRoute(id), isEmpty);
});
test('setClosedLoop(true) adds the closing segment to distance', () async {
final id = await routes.createRoutePlan(1000);
await routes.addWaypoint(id, 51.0, -114.0);
await routes.addWaypoint(id, 51.01, -114.0);
await routes.addWaypoint(id, 51.02, -114.0);
final open = (await routes.routePlanById(id))!.distanceM;
final waypoints = await routes.waypointsFor(id);
final closingSegment = geo.pathLengthMeters([
geo.LatLon(waypoints.last.latitude, waypoints.last.longitude),
geo.LatLon(waypoints.first.latitude, waypoints.first.longitude),
]);
await routes.setClosedLoop(id, true);
final closed = (await routes.routePlanById(id))!.distanceM;
expect(closed, closeTo(open + closingSegment, 1e-6));
expect((await routes.routePlanById(id))!.isClosedLoop, isTrue);
});
test('setClosedLoop(false) after true returns distance to the open total', () async {
final id = await routes.createRoutePlan(1000);
await routes.addWaypoint(id, 51.0, -114.0);
await routes.addWaypoint(id, 51.01, -114.0);
await routes.addWaypoint(id, 51.02, -114.0);
final open = (await routes.routePlanById(id))!.distanceM;
await routes.setClosedLoop(id, true);
expect((await routes.routePlanById(id))!.distanceM, isNot(open));
await routes.setClosedLoop(id, false);
expect((await routes.routePlanById(id))!.distanceM, closeTo(open, 1e-6));
expect((await routes.routePlanById(id))!.isClosedLoop, isFalse);
});
test('adding a waypoint to a closed-loop route keeps the closing segment correct',
() async {
final id = await routes.createRoutePlan(1000);
await routes.addWaypoint(id, 51.0, -114.0);
await routes.addWaypoint(id, 51.01, -114.0);
await routes.setClosedLoop(id, true);
// Adding a third waypoint should recompute the closing segment against the new
// last waypoint, not the old one.
await routes.addWaypoint(id, 51.02, -114.0);
final waypoints = await routes.waypointsFor(id);
final expected = geo.pathLengthMeters([
for (final w in waypoints) geo.LatLon(w.latitude, w.longitude),
geo.LatLon(waypoints.first.latitude, waypoints.first.longitude),
]);
expect((await routes.routePlanById(id))!.distanceM, closeTo(expected, 1e-6));
});
test('route plans never appear alongside trips and never affect ride totals',
() async {
await routes.createRoutePlan(1000, name: 'Plan A');