diff --git a/lib/features/dashboard/widgets/dashboard_map_panel.dart b/lib/features/dashboard/widgets/dashboard_map_panel.dart index 74af053..7aa8f38 100644 --- a/lib/features/dashboard/widgets/dashboard_map_panel.dart +++ b/lib/features/dashboard/widgets/dashboard_map_panel.dart @@ -5,15 +5,18 @@ import '../../../core/theme/app_typography.dart'; import '../../../services/alerts/road_alert_models.dart'; import '../../../services/alerts/road_alert_service.dart'; import '../../../services/map/map_provider_interface.dart'; +import '../../../services/map/map_providers.dart'; import '../../../services/map/unified_map_view.dart'; import '../../../services/navigation/navigation_session_service.dart'; import '../../../services/telemetry_service.dart'; +import '../../rides/ride_state_machine.dart'; /// Embedded tactical map panel for Touring mode and responsive dashboard views. /// /// Subscribes to vehicle coordinates and heading via [telemetryStreamProvider] -/// and alert pins via [activeAlertsStreamProvider]. -class DashboardMapPanel extends ConsumerWidget { +/// and alert pins via [activeAlertsStreamProvider], maintains camera follow +/// state, and provides a recenter control after manual pan. +class DashboardMapPanel extends ConsumerStatefulWidget { final double zoom; const DashboardMapPanel({ @@ -22,7 +25,43 @@ class DashboardMapPanel extends ConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _DashboardMapPanelState(); +} + +class _DashboardMapPanelState extends ConsumerState { + static const AppLatLng _defaultFallbackCenter = AppLatLng(37.7749, -122.4194); + AppLatLng? _lastKnownRiderPos; + + @override + void initState() { + super.initState(); + // Ensure follow mode is active whenever the Touring map panel is mounted. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + ref.read(mapFollowStateProvider.notifier).recenter(); + } + }); + } + + bool _isValidCoord(double? lat, double? lng) { + return lat != null && + lng != null && + lat.isFinite && + lng.isFinite && + (lat != 0.0 || lng != 0.0); + } + + @override + Widget build(BuildContext context) { + // Re-enable camera follow automatically when a ride session starts. + ref.listen(rideStateMachineProvider, (previous, next) { + if (previous?.state != RideState.riding && + next.state == RideState.riding && + mounted) { + ref.read(mapFollowStateProvider.notifier).recenter(); + } + }); + final vehicleCoords = ref.watch( telemetryStreamProvider.select( (s) => ( @@ -34,19 +73,18 @@ class DashboardMapPanel extends ConsumerWidget { ), ); - final centerLat = (vehicleCoords.lat != null && vehicleCoords.lat != 0.0) - ? vehicleCoords.lat! - : 37.7749; - final centerLng = (vehicleCoords.lng != null && vehicleCoords.lng != 0.0) - ? vehicleCoords.lng! - : -122.4194; + final hasCoordinates = _isValidCoord(vehicleCoords.lat, vehicleCoords.lng); + if (hasCoordinates) { + _lastKnownRiderPos = AppLatLng(vehicleCoords.lat!, vehicleCoords.lng!); + } + final activeCenter = _lastKnownRiderPos ?? _defaultFallbackCenter; final isFresh = vehicleCoords.isGpsFresh; - final hasCoordinates = vehicleCoords.lat != null && vehicleCoords.lat != 0.0; + final isFollowMode = ref.watch(mapFollowStateProvider); final vehicleMarker = AppMapMarker( id: 'dash_vehicle', - position: AppLatLng(centerLat, centerLng), + position: activeCenter, heading: vehicleCoords.heading, iconWidget: Icon( Icons.navigation, @@ -64,7 +102,7 @@ class DashboardMapPanel extends ConsumerWidget { for (final alert in activeAlerts) { mapMarkers.add( AppMapMarker( - id: 'dash_alert__', + id: 'dash_alert_${alert.id}', position: AppLatLng(alert.latitude, alert.longitude), heading: alert.heading ?? 0.0, label: alert.category.displayName, @@ -116,14 +154,14 @@ class DashboardMapPanel extends ConsumerWidget { Positioned.fill( child: UnifiedMapView( key: const ValueKey('dash_map_x'), - center: AppLatLng(centerLat, centerLng), - zoom: zoom, + center: activeCenter, + zoom: widget.zoom, bearing: 0.0, markers: mapMarkers, polylines: polylines, ), ), - if (!isFresh && hasCoordinates) + if (!isFresh && (hasCoordinates || _lastKnownRiderPos != null)) Positioned( top: 10, left: 10, @@ -154,6 +192,56 @@ class DashboardMapPanel extends ConsumerWidget { ), ), ), + if (!isFollowMode) + Positioned( + bottom: 10, + right: 10, + child: Material( + color: Colors.black.withValues(alpha: 0.8), + borderRadius: BorderRadius.circular(20), + child: InkWell( + key: const ValueKey('dashboard_map_recenter_button'), + borderRadius: BorderRadius.circular(20), + onTap: () { + ref.read(mapFollowStateProvider.notifier).recenter(); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppColors.cyberCyan, + width: 1.2, + ), + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.my_location, + size: 14, + color: AppColors.cyberCyan, + ), + SizedBox(width: 5), + Text( + 'RECENTER', + style: TextStyle( + fontFamily: AppTypography.fontTelemetry, + fontSize: 10, + fontWeight: FontWeight.w800, + letterSpacing: 0.6, + color: AppColors.cyberCyan, + ), + ), + ], + ), + ), + ), + ), + ), ], ), ); @@ -162,3 +250,4 @@ class DashboardMapPanel extends ConsumerWidget { ); } } + diff --git a/lib/services/map/aws_map_provider.dart b/lib/services/map/aws_map_provider.dart index 3c00cad..b87ce60 100644 --- a/lib/services/map/aws_map_provider.dart +++ b/lib/services/map/aws_map_provider.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -102,11 +103,31 @@ class _AwsMapViewState extends State<_AwsMapView> { late final MapController _mapController; ll.LatLng? _lastFollowPos; DateTime? _lastFollowTime; + Timer? _pendingFollowTimer; + + static const int _followThrottleMs = 250; @override void initState() { super.initState(); _mapController = MapController(); + _lastFollowPos = ll.LatLng(widget.center.latitude, widget.center.longitude); + } + + void _applyFollowMove(ll.LatLng target, double zoom) { + _lastFollowPos = target; + _lastFollowTime = DateTime.now(); + try { + _mapController.move(target, zoom); + } catch (_) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + try { + _mapController.move(target, zoom); + } catch (_) {} + } + }); + } } @override @@ -124,33 +145,46 @@ class _AwsMapViewState extends State<_AwsMapView> { ? 1.0 : (centerLl.latitude - _lastFollowPos!.latitude).abs() + (centerLl.longitude - _lastFollowPos!.longitude).abs(); + final hasMoved = posDelta > 0.00001; + final elapsedMs = _lastFollowTime == null + ? _followThrottleMs + 1 + : now.difference(_lastFollowTime!).inMilliseconds; - final shouldMove = justBecameFollowMode || + final shouldMoveNow = justBecameFollowMode || _lastFollowPos == null || zoomChanged || - (posDelta > 0.00005 && - (_lastFollowTime == null || - now.difference(_lastFollowTime!).inMilliseconds > 500)); + (hasMoved && (elapsedMs >= _followThrottleMs || posDelta > 0.001)); - if (shouldMove) { - _lastFollowPos = centerLl; - _lastFollowTime = now; + if (shouldMoveNow) { + _pendingFollowTimer?.cancel(); + _pendingFollowTimer = null; + _applyFollowMove(centerLl, widget.zoom); + } else if (hasMoved) { + _pendingFollowTimer?.cancel(); + final remainingMs = (_followThrottleMs - elapsedMs).clamp(16, _followThrottleMs); + _pendingFollowTimer = Timer(Duration(milliseconds: remainingMs), () { + _pendingFollowTimer = null; + if (mounted && widget.isFollowMode) { + final latestCenter = ll.LatLng( + widget.center.latitude, + widget.center.longitude, + ); + _applyFollowMove(latestCenter, widget.zoom); + } + }); + } + } else { + _pendingFollowTimer?.cancel(); + _pendingFollowTimer = null; + if (zoomChanged) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { try { - _mapController.move(centerLl, widget.zoom); + _mapController.move(_mapController.camera.center, widget.zoom); } catch (_) {} } }); } - } else if (zoomChanged) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - try { - _mapController.move(_mapController.camera.center, widget.zoom); - } catch (_) {} - } - }); } if (bearingChanged) { @@ -166,6 +200,7 @@ class _AwsMapViewState extends State<_AwsMapView> { @override void dispose() { + _pendingFollowTimer?.cancel(); _mapController.dispose(); super.dispose(); } diff --git a/lib/services/map/google_map_provider.dart b/lib/services/map/google_map_provider.dart index 4cd2bf4..ff2fbbe 100644 --- a/lib/services/map/google_map_provider.dart +++ b/lib/services/map/google_map_provider.dart @@ -4,8 +4,6 @@ import '../../core/theme/map_style.dart'; import 'map_provider_interface.dart'; class GoogleMapProvider implements AppMapProvider { - gmaps.GoogleMapController? _controller; - @override AppMapType get type => AppMapType.googleMaps; @@ -25,15 +23,122 @@ class GoogleMapProvider implements AppMapProvider { required ValueChanged? onPositionChanged, required VoidCallback? onUserInteraction, }) { - final target = gmaps.LatLng(center.latitude, center.longitude); + return _GoogleMapView( + center: center, + zoom: zoom, + bearing: bearing, + markers: markers, + polylines: polylines, + isFollowMode: isFollowMode, + onPositionChanged: onPositionChanged, + onUserInteraction: onUserInteraction, + ); + } +} - if (isFollowMode && _controller != null) { - _controller!.animateCamera( - gmaps.CameraUpdate.newLatLngZoom(target, zoom), +class _GoogleMapView extends StatefulWidget { + final AppLatLng center; + final double zoom; + final double bearing; + final List markers; + final List polylines; + final bool isFollowMode; + final ValueChanged? onPositionChanged; + final VoidCallback? onUserInteraction; + + const _GoogleMapView({ + required this.center, + required this.zoom, + this.bearing = 0.0, + required this.markers, + required this.polylines, + required this.isFollowMode, + required this.onPositionChanged, + required this.onUserInteraction, + }); + + @override + State<_GoogleMapView> createState() => _GoogleMapViewState(); +} + +class _GoogleMapViewState extends State<_GoogleMapView> { + gmaps.GoogleMapController? _controller; + gmaps.LatLng? _lastFollowPos; + DateTime? _lastFollowTime; + bool _isProgrammaticMove = false; + + static const int _followThrottleMs = 250; + + @override + void initState() { + super.initState(); + _lastFollowPos = gmaps.LatLng(widget.center.latitude, widget.center.longitude); + } + + Future _moveCamera(gmaps.LatLng target, double zoom, double bearing) async { + final controller = _controller; + if (controller == null) return; + _lastFollowPos = target; + _lastFollowTime = DateTime.now(); + _isProgrammaticMove = true; + try { + await controller.animateCamera( + gmaps.CameraUpdate.newCameraPosition( + gmaps.CameraPosition( + target: target, + zoom: zoom, + bearing: bearing, + ), + ), ); + } catch (_) { + } finally { + _isProgrammaticMove = false; } + } - final gMarkers = markers.map((m) { + @override + void didUpdateWidget(covariant _GoogleMapView oldWidget) { + super.didUpdateWidget(oldWidget); + if (_controller == null) return; + + final target = gmaps.LatLng(widget.center.latitude, widget.center.longitude); + final zoomChanged = (oldWidget.zoom - widget.zoom).abs() > 0.01; + final bearingChanged = (oldWidget.bearing - widget.bearing).abs() > 0.1; + final justBecameFollowMode = !oldWidget.isFollowMode && widget.isFollowMode; + + if (widget.isFollowMode) { + final now = DateTime.now(); + final posDelta = _lastFollowPos == null + ? 1.0 + : (target.latitude - _lastFollowPos!.latitude).abs() + + (target.longitude - _lastFollowPos!.longitude).abs(); + final hasMoved = posDelta > 0.00001; + final elapsedMs = _lastFollowTime == null + ? _followThrottleMs + 1 + : now.difference(_lastFollowTime!).inMilliseconds; + + if (justBecameFollowMode || + _lastFollowPos == null || + zoomChanged || + bearingChanged || + (hasMoved && (elapsedMs >= _followThrottleMs || posDelta > 0.001))) { + _moveCamera(target, widget.zoom, widget.bearing); + } + } + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final target = gmaps.LatLng(widget.center.latitude, widget.center.longitude); + + final gMarkers = widget.markers.map((m) { return gmaps.Marker( markerId: gmaps.MarkerId(m.id), position: gmaps.LatLng(m.position.latitude, m.position.longitude), @@ -42,7 +147,7 @@ class GoogleMapProvider implements AppMapProvider { ); }).toSet(); - final gPolylines = polylines.map((p) { + final gPolylines = widget.polylines.map((p) { return gmaps.Polyline( polylineId: gmaps.PolylineId(p.id), points: p.points.map((pt) => gmaps.LatLng(pt.latitude, pt.longitude)).toList(), @@ -58,7 +163,8 @@ class GoogleMapProvider implements AppMapProvider { key: ValueKey('google_map_$sizeKey'), initialCameraPosition: gmaps.CameraPosition( target: target, - zoom: zoom, + zoom: widget.zoom, + bearing: widget.bearing, ), markers: gMarkers, polylines: gPolylines, @@ -68,13 +174,21 @@ class GoogleMapProvider implements AppMapProvider { compassEnabled: false, onMapCreated: (controller) { _controller = controller; + if (widget.isFollowMode) { + _moveCamera(target, widget.zoom, widget.bearing); + } }, onCameraMoveStarted: () { - onUserInteraction?.call(); + if (!_isProgrammaticMove) { + widget.onUserInteraction?.call(); + } }, onCameraMove: (position) { - onPositionChanged?.call(AppLatLng(position.target.latitude, position.target.longitude)); + widget.onPositionChanged?.call( + AppLatLng(position.target.latitude, position.target.longitude), + ); }, ); } } + diff --git a/lib/services/map/open_street_map_provider.dart b/lib/services/map/open_street_map_provider.dart index 6884da6..f6165d7 100644 --- a/lib/services/map/open_street_map_provider.dart +++ b/lib/services/map/open_street_map_provider.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -69,11 +70,31 @@ class _OsmMapViewState extends State<_OsmMapView> { late final MapController _mapController; ll.LatLng? _lastFollowPos; DateTime? _lastFollowTime; + Timer? _pendingFollowTimer; + + static const int _followThrottleMs = 250; @override void initState() { super.initState(); _mapController = MapController(); + _lastFollowPos = ll.LatLng(widget.center.latitude, widget.center.longitude); + } + + void _applyFollowMove(ll.LatLng target, double zoom) { + _lastFollowPos = target; + _lastFollowTime = DateTime.now(); + try { + _mapController.move(target, zoom); + } catch (_) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + try { + _mapController.move(target, zoom); + } catch (_) {} + } + }); + } } @override @@ -90,33 +111,46 @@ class _OsmMapViewState extends State<_OsmMapView> { ? 1.0 : (centerLl.latitude - _lastFollowPos!.latitude).abs() + (centerLl.longitude - _lastFollowPos!.longitude).abs(); + final hasMoved = posDelta > 0.00001; + final elapsedMs = _lastFollowTime == null + ? _followThrottleMs + 1 + : now.difference(_lastFollowTime!).inMilliseconds; - final shouldMove = justBecameFollowMode || + final shouldMoveNow = justBecameFollowMode || _lastFollowPos == null || zoomChanged || - (posDelta > 0.00005 && - (_lastFollowTime == null || - now.difference(_lastFollowTime!).inMilliseconds > 500)); + (hasMoved && (elapsedMs >= _followThrottleMs || posDelta > 0.001)); - if (shouldMove) { - _lastFollowPos = centerLl; - _lastFollowTime = now; + if (shouldMoveNow) { + _pendingFollowTimer?.cancel(); + _pendingFollowTimer = null; + _applyFollowMove(centerLl, widget.zoom); + } else if (hasMoved) { + _pendingFollowTimer?.cancel(); + final remainingMs = (_followThrottleMs - elapsedMs).clamp(16, _followThrottleMs); + _pendingFollowTimer = Timer(Duration(milliseconds: remainingMs), () { + _pendingFollowTimer = null; + if (mounted && widget.isFollowMode) { + final latestCenter = ll.LatLng( + widget.center.latitude, + widget.center.longitude, + ); + _applyFollowMove(latestCenter, widget.zoom); + } + }); + } + } else { + _pendingFollowTimer?.cancel(); + _pendingFollowTimer = null; + if (zoomChanged) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { try { - _mapController.move(centerLl, widget.zoom); + _mapController.move(_mapController.camera.center, widget.zoom); } catch (_) {} } }); } - } else if (zoomChanged) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - try { - _mapController.move(_mapController.camera.center, widget.zoom); - } catch (_) {} - } - }); } if (bearingChanged) { @@ -132,6 +166,7 @@ class _OsmMapViewState extends State<_OsmMapView> { @override void dispose() { + _pendingFollowTimer?.cancel(); _mapController.dispose(); super.dispose(); } diff --git a/test/canonical_dashboard_responsive_test.dart b/test/canonical_dashboard_responsive_test.dart index 1a8c55d..ee7057b 100644 --- a/test/canonical_dashboard_responsive_test.dart +++ b/test/canonical_dashboard_responsive_test.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -20,6 +21,8 @@ import 'package:motorcycle_dash/models/telemetry_sample.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_models.dart'; import 'package:motorcycle_dash/services/alerts/road_alert_service.dart'; import 'package:motorcycle_dash/services/location_service.dart'; +import 'package:motorcycle_dash/services/map/map_providers.dart'; +import 'package:motorcycle_dash/services/map/unified_map_view.dart'; import 'package:motorcycle_dash/services/telemetry_service.dart'; void main() { @@ -344,6 +347,166 @@ void main() { expect(find.text('Enable'), findsOneWidget); }); }); + + group('BUG-001: Touring Map Rider Follow & Recenter Regression Tests', () { + testWidgets( + 'Touring map follows incoming GPS coordinates in portrait and landscape, pauses on manual pan, and resumes on recenter', + (tester) async { + await prefs.setString('dashboardLayout', 'touring'); + configureViewport(tester, 400, 800); + addTearDown(() => tester.view.resetPhysicalSize()); + + await tester.pumpWidget(buildTestDashboard()); + await tester.pump(); + + expect(find.byType(DashboardMapPanel), findsOneWidget); + expect(find.byType(FlutterMap), findsOneWidget); + expect( + find.byKey(const ValueKey('dashboard_map_recenter_button')), + findsNothing, + ); + + // Emit initial GPS fix within 50ms of mount (previously dropped by 500ms cooldown) + telemetryController.add( + TelemetrySample( + timestamp: DateTime.now(), + speed: 35.0, + heading: 90.0, + latitude: 34.0522, + longitude: -118.2437, + gpsAccuracy: 5.0, + gpsAgeMs: 100, + ), + ); + await tester.pump(const Duration(milliseconds: 50)); + + var unifiedMap = tester.widget( + find.byType(UnifiedMapView), + ); + var flutterMap = tester.widget(find.byType(FlutterMap)); + expect(unifiedMap.center.latitude, closeTo(34.0522, 0.0001)); + expect(unifiedMap.center.longitude, closeTo(-118.2437, 0.0001)); + expect( + flutterMap.mapController!.camera.center.latitude, + closeTo(34.0522, 0.0001), + ); + expect( + flutterMap.mapController!.camera.center.longitude, + closeTo(-118.2437, 0.0001), + ); + + // Emit rapid subsequent GPS fix within 100ms (tests trailing timer follow) + telemetryController.add( + TelemetrySample( + timestamp: DateTime.now(), + speed: 45.0, + heading: 95.0, + latitude: 34.0526, + longitude: -118.2431, + gpsAccuracy: 5.0, + gpsAgeMs: 80, + ), + ); + await tester.pump(const Duration(milliseconds: 50)); + // Advance past the 250ms trailing follow timer window + await tester.pump(const Duration(milliseconds: 300)); + + flutterMap = tester.widget(find.byType(FlutterMap)); + expect( + flutterMap.mapController!.camera.center.latitude, + closeTo(34.0526, 0.0001), + ); + expect( + flutterMap.mapController!.camera.center.longitude, + closeTo(-118.2431, 0.0001), + ); + + // Simulate user manual pan / gesture interaction on the Touring map + final element = tester.element(find.byType(DashboardMapPanel)); + final container = ProviderScope.containerOf(element); + container.read(mapFollowStateProvider.notifier).userInteracted(); + flutterMap.mapController!.move( + flutterMap.mapController!.camera.center, + 16.5, + ); + await tester.pump(); + + // Recenter button must appear when follow mode is disabled + final recenterFinder = find.byKey( + const ValueKey('dashboard_map_recenter_button'), + ); + expect(recenterFinder, findsOneWidget); + + // While panned (follow mode false), incoming GPS updates move the rider marker + // but do NOT hijack the user's panned camera center + final pannedCameraCenter = flutterMap.mapController!.camera.center; + telemetryController.add( + TelemetrySample( + timestamp: DateTime.now(), + speed: 50.0, + heading: 100.0, + latitude: 34.0600, + longitude: -118.2500, + gpsAccuracy: 5.0, + gpsAgeMs: 80, + ), + ); + await tester.pump(const Duration(milliseconds: 300)); + + flutterMap = tester.widget(find.byType(FlutterMap)); + expect( + flutterMap.mapController!.camera.center.latitude, + closeTo(pannedCameraCenter.latitude, 0.0001), + ); + expect( + flutterMap.mapController!.camera.center.longitude, + closeTo(pannedCameraCenter.longitude, 0.0001), + ); + + // Tap RECENTER button -> camera snaps back to latest rider position (34.0600, -118.2500) + await tester.tap(recenterFinder); + await tester.pump(); + + expect(recenterFinder, findsNothing); + flutterMap = tester.widget(find.byType(FlutterMap)); + expect( + flutterMap.mapController!.camera.center.latitude, + closeTo(34.0600, 0.0001), + ); + expect( + flutterMap.mapController!.camera.center.longitude, + closeTo(-118.2500, 0.0001), + ); + + // Rotate to landscape Touring mode and verify GPS follow continues cleanly + configureViewport(tester, 800, 360); + await tester.pump(); + + telemetryController.add( + TelemetrySample( + timestamp: DateTime.now(), + speed: 55.0, + heading: 110.0, + latitude: 34.0700, + longitude: -118.2600, + gpsAccuracy: 5.0, + gpsAgeMs: 80, + ), + ); + await tester.pump(const Duration(milliseconds: 300)); + + flutterMap = tester.widget(find.byType(FlutterMap)); + expect( + flutterMap.mapController!.camera.center.latitude, + closeTo(34.0700, 0.0001), + ); + expect( + flutterMap.mapController!.camera.center.longitude, + closeTo(-118.2600, 0.0001), + ); + }, + ); + }); } class _TestRideStateMachine extends StateNotifier