import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; import '../models/directions.dart'; import '../utils/geometry_utils.dart'; /// Maximum distance the user can be from the line before recalculating the directions const maxDistanceFromLine = 100; class DirectionsInstruction extends StatefulWidget { final Directions directions; final Stream currentPositionStream; final StreamController currentWaypointIndexController; final bool Function(LatLng) recalculateDirections; const DirectionsInstruction({ required this.directions, required this.currentPositionStream, required this.currentWaypointIndexController, required this.recalculateDirections, Key? key, }) : super(key: key); @override State createState() => _DirectionsInstructionState(); } class _DirectionsInstructionState extends State { /// The current step the user is in the directions int _currentSegmentIndex = 0; /// Next waypoint the user has to pass int _nextWaypointIndex = 0; late StreamSubscription _currentPositionSubscription; /// Line the user is currently following, defined by the previous and next waypoints Segment get _currentLine { final startLine = widget.directions.waypointsCoordinates[max(0, _nextWaypointIndex - 1)]; final endLine = widget.directions.waypointsCoordinates[_nextWaypointIndex]; return Segment(start: startLine, end: endLine); } @override initState() { super.initState(); _currentPositionSubscription = widget.currentPositionStream.listen(_handleCurrentPositionUpdates); } @override void dispose() { _currentPositionSubscription.cancel(); super.dispose(); } void _handleCurrentPositionUpdates(currentPosition) { _updateDirectionsFromCurrentPosition(currentPosition); _updateWaypointFromCurrentPosition(currentPosition); } void _updateWaypointFromCurrentPosition(LatLng currentPosition) { final nextWaypointIndex = _getNextWaypointIndex(currentPosition); widget.currentWaypointIndexController.add(nextWaypointIndex); setState(() { _nextWaypointIndex = nextWaypointIndex; _currentSegmentIndex = _getNewSegmentIndex(nextWaypointIndex); }); } /// Get new directions if the user is too far from the current segment void _updateDirectionsFromCurrentPosition(LatLng currentPosition) { var distanceFromCurrentSegment = DistanceUtils.distToSegment(currentPosition, _currentLine); if (distanceFromCurrentSegment > maxDistanceFromLine) { // If we could recalculate the directions, we reset the segment and waypoint to zero if (widget.recalculateDirections(currentPosition)) { setState(() { _currentSegmentIndex = 0; _nextWaypointIndex = 0; }); } } } num _getDistanceFromWaypoint(LatLng currentPosition, int waypointIndex) { final currentWaypoint = widget.directions.waypointsCoordinates[waypointIndex]; return DistanceUtils.distBetweenTwoPoints(currentPosition, currentWaypoint); } int _getNewSegmentIndex(int nextWaypointIndex) { final currentSegment = widget.directions.segments[_currentSegmentIndex]; var newSegmentIndex = _currentSegmentIndex; if (nextWaypointIndex > currentSegment.waypoints[1]) { newSegmentIndex++; } else if (nextWaypointIndex <= currentSegment.waypoints[0]) { newSegmentIndex--; } return max(0, min(newSegmentIndex, widget.directions.segments.length - 1)); } int _getNextWaypointIndex(LatLng currentPosition) { final parameterProjection = DistanceUtils.getParameterFromProjection(currentPosition, _currentLine); final distanceFromNextWaypoint = _getDistanceFromWaypoint(currentPosition, _nextWaypointIndex); final distanceFromNextNextWaypoint = _nextWaypointIndex < widget.directions.waypointsCoordinates.length - 1 ? _getDistanceFromWaypoint(currentPosition, _nextWaypointIndex + 1) : double.infinity; final isProjectedPointOnCurrentSegment = parameterProjection > 0 && parameterProjection < 1; // Approaching the next waypoint while following the line // We want to detect it before passing the waypoint if (distanceFromNextWaypoint < 3 && isProjectedPointOnCurrentSegment) { return _nextWaypointIndex + 1; } // Went back past previous waypoint if (parameterProjection < 0) { return max(0, _nextWaypointIndex - 1); } // Not closely following the line, but still went past next waypoint if (distanceFromNextWaypoint > distanceFromNextNextWaypoint || parameterProjection > 1) { return _nextWaypointIndex + 1; } return _nextWaypointIndex; } @override Widget build(BuildContext context) { final segments = widget.directions.segments; return Column( children: [ SizedBox(height: MediaQuery.of(context).size.height * 0.05), Text( "${segments[_currentSegmentIndex + 1].instruction} in ${segments[_currentSegmentIndex].distance.round()}m", style: const TextStyle(fontSize: 25), ), if (_currentSegmentIndex < segments.length - 2) Text( "Next: ${segments[_currentSegmentIndex + 2].instruction}", style: const TextStyle(fontSize: 20), ), ], ); } }