|
@@ -0,0 +1,144 @@
|
|
|
|
|
+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<LatLng> currentPositionStream;
|
|
|
|
|
+ final StreamController<int> 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<DirectionsInstruction> createState() => _DirectionsInstructionState();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+class _DirectionsInstructionState extends State<DirectionsInstruction> {
|
|
|
|
|
+ /// The current step the user is in the directions
|
|
|
|
|
+ int _currentSegmentIndex = 0;
|
|
|
|
|
+ /// Next waypoint the user has to pass
|
|
|
|
|
+ int _nextWaypointIndex = 0;
|
|
|
|
|
+ late StreamSubscription<LatLng> _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),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|