From aa1766f63becd2cc238450c6d9314059168c5fa7 Mon Sep 17 00:00:00 2001 From: Sander van Vliet Date: Fri, 4 Aug 2023 14:57:47 +0200 Subject: [PATCH] Set the correct elapsed and total distance for routes that have a loop --- Changelog.md | 4 +- .../Models/InGameWindowModel.cs | 23 ++++++- .../InGameNavigationWindowViewModel.cs | 1 + .../GameStates/UpcomingTurnState.cs | 10 ++- src/RoadCaptain/PlannedRoute.cs | 12 ++-- .../GameState/Loops/FromUpcomingTurnState.cs | 62 +++++++++++++++++++ .../GameState/StateTransitionTestBase.cs | 2 +- 7 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 test/RoadCaptain.Tests.Unit/GameState/Loops/FromUpcomingTurnState.cs diff --git a/Changelog.md b/Changelog.md index 7578f8d0..79a84c08 100644 --- a/Changelog.md +++ b/Changelog.md @@ -17,7 +17,8 @@ Read more about how it works on [the RoadCaptain site](https://roadcaptain.nl/fe ### Runner - **Windows only** Zwift credentials will now be stored securely in the Credential Manager instead of keeping them in memory only. This reduces the amount of times you'll need to log in to Zwift - +- When entering a loop, the total distance is set to the distance of the loop. +- #### Elevation profile When riding hit CTRL + E / META + E to toggle the Elevation Plot window. This will allow you to see the elevation and grade of the route you are riding which helps you to plan your effort much easier than having to squint at the elevation profile for the entire route. @@ -36,6 +37,7 @@ You can toggle the mode by clicking the elevation profile window and using - Resue the route after connection was lost. This should fix the remaining issue in [#107](https://github.com/sandermvanvliet/RoadCaptain/issues/107) - Fixed a bug where a crash would occur because the map animation would continue running when the rider is already in-game. - Fixed a bug where hitting CTRL + X / META + X in the in-game window when you are not yet in a game would not return to the main window. +- The progress bar now resets when you start a new loop (See [#117](https://github.com/sandermvanvliet/RoadCaptain/issues/117)) ## 0.6.11.0 diff --git a/src/RoadCaptain.App.Runner/Models/InGameWindowModel.cs b/src/RoadCaptain.App.Runner/Models/InGameWindowModel.cs index 604c05b5..36af50ae 100644 --- a/src/RoadCaptain.App.Runner/Models/InGameWindowModel.cs +++ b/src/RoadCaptain.App.Runner/Models/InGameWindowModel.cs @@ -25,6 +25,8 @@ public class InGameWindowModel : ViewModelBase private double _totalDistance; private string _loopText = string.Empty; private int _currentSegmentIndex; + private bool _isOnLoop; + private double _loopDistance = 0; public InGameWindowModel(List segments) { @@ -101,7 +103,7 @@ public double ElapsedDescent public double TotalDistance { - get => _totalDistance; + get => IsOnLoop ? _loopDistance : _totalDistance; set { if (value.Equals(_totalDistance)) return; @@ -178,6 +180,18 @@ public int CurrentSegmentIndex public int SegmentCount => Route?.RouteSegmentSequence.Count ?? 0; + public bool IsOnLoop + { + get => _isOnLoop; + set + { + if (value == _isOnLoop) return; + _isOnLoop = value; + this.RaisePropertyChanged(); + this.RaisePropertyChanged(nameof(TotalDistance)); + } + } + private void InitializeRoute(PlannedRoute route) { if (route.CurrentSegmentSequence != null) @@ -224,6 +238,7 @@ private void CalculateTotalAscentAndDescent(PlannedRoute route) double totalAscent = 0; double totalDescent = 0; double totalDistance = 0; + double loopDistance = 0; foreach (var sequence in route.RouteSegmentSequence) { @@ -241,9 +256,15 @@ private void CalculateTotalAscentAndDescent(PlannedRoute route) } totalDistance += segment.Distance; + + if (sequence.Type is SegmentSequenceType.Loop or SegmentSequenceType.LoopEnd or SegmentSequenceType.LoopStart) + { + loopDistance += segment.Distance; + } } TotalDistance = Math.Round(totalDistance / 1000, 1); + _loopDistance = Math.Round(loopDistance / 1000, 1); TotalAscent = totalAscent; TotalDescent = totalDescent; } diff --git a/src/RoadCaptain.App.Runner/ViewModels/InGameNavigationWindowViewModel.cs b/src/RoadCaptain.App.Runner/ViewModels/InGameNavigationWindowViewModel.cs index 0c0eb137..53dedc38 100644 --- a/src/RoadCaptain.App.Runner/ViewModels/InGameNavigationWindowViewModel.cs +++ b/src/RoadCaptain.App.Runner/ViewModels/InGameNavigationWindowViewModel.cs @@ -266,6 +266,7 @@ private void UpdateRouteModel(PlannedRoute plannedRoute) if (plannedRoute.IsLoop) { Model.LoopText = plannedRoute.OnLeadIn ? "Lead-in to loop" : $"On loop: {plannedRoute.LoopCount}"; + Model.IsOnLoop = plannedRoute.IsOnLoop; } else { diff --git a/src/RoadCaptain/GameStates/UpcomingTurnState.cs b/src/RoadCaptain/GameStates/UpcomingTurnState.cs index b31afd03..316e898c 100644 --- a/src/RoadCaptain/GameStates/UpcomingTurnState.cs +++ b/src/RoadCaptain/GameStates/UpcomingTurnState.cs @@ -246,7 +246,15 @@ public override GameState UpdatePosition(TrackPoint position, List segm if (plannedRoute.NextSegmentId == segment.Id) { - plannedRoute.EnteredSegment(segment.Id); + var result = plannedRoute.EnteredSegment(segment.Id); + + if (result == RouteMoveResult.StartedNewLoop) + { + distance = 0; + ascent = 0; + descent = 0; + } + return new OnRouteState( RiderId, ActivityId, diff --git a/src/RoadCaptain/PlannedRoute.cs b/src/RoadCaptain/PlannedRoute.cs index 4b876a0e..f5634899 100644 --- a/src/RoadCaptain/PlannedRoute.cs +++ b/src/RoadCaptain/PlannedRoute.cs @@ -24,6 +24,9 @@ public class PlannedRoute [JsonIgnore] public bool IsOnLastSegment => SegmentSequenceIndex == RouteSegmentSequence.Count - 1; + [JsonIgnore] + public bool IsOnLoop => CurrentSegmentSequence is { Type: SegmentSequenceType.LoopStart or SegmentSequenceType.Loop or SegmentSequenceType.LoopEnd }; + [JsonIgnore] public int SegmentSequenceIndex { @@ -145,12 +148,10 @@ public RouteMoveResult EnteredSegment(string segmentId) { SegmentSequenceIndex = NextSegmentSequence!.Index; LoopCount++; - } - else - { - SegmentSequenceIndex++; + return RouteMoveResult.StartedNewLoop; } + SegmentSequenceIndex++; return RouteMoveResult.EnteredNextSegment; } @@ -300,6 +301,7 @@ public enum RouteMoveResult Unknown, StartedRoute, EnteredNextSegment, - CompletedRoute + CompletedRoute, + StartedNewLoop } } diff --git a/test/RoadCaptain.Tests.Unit/GameState/Loops/FromUpcomingTurnState.cs b/test/RoadCaptain.Tests.Unit/GameState/Loops/FromUpcomingTurnState.cs new file mode 100644 index 00000000..1ee697fc --- /dev/null +++ b/test/RoadCaptain.Tests.Unit/GameState/Loops/FromUpcomingTurnState.cs @@ -0,0 +1,62 @@ +using RoadCaptain.GameStates; +using System.Collections.Generic; +using FluentAssertions; +using Xunit; + +namespace RoadCaptain.Tests.Unit.GameState.Loops +{ + public class FromUpcomingTurnState : LoopStateTransitionTestBase + { + [Fact] + public void GivenOnLastLoopSegment_TotalDistanceOnRouteStateIsReset() + { + var result = GivenStartingState(Route).UpdatePosition(RouteSegment1Point1, Segments, Route); + + result + .Should() + .BeOfType() + .Which + .ElapsedDistance + .Should() + .Be(0); + } + + private UpcomingTurnState GivenStartingState(PlannedRoute plannedRoute) + { + // Ensure the route has started and we're on the first route segment + plannedRoute.EnteredSegment(RouteSegment1.Id); + plannedRoute.EnteredSegment(RouteSegment2.Id); + plannedRoute.EnteredSegment(RouteSegment3.Id); + + return new UpcomingTurnState( + 1, + 2, + RouteSegment3Point3, + RouteSegment3, + plannedRoute, + SegmentDirection.AtoB, + new List { TurnDirection.Left, TurnDirection.Right }, + 1000, + 1000, + 1000); + } + } + + public class LoopStateTransitionTestBase : StateTransitionTestBase + { + protected override PlannedRoute Route { get; } = new() + { + RouteSegmentSequence = + { + new SegmentSequence(segmentId: RouteSegment1.Id, direction: SegmentDirection.AtoB, type: SegmentSequenceType.LoopStart, nextSegmentId: RouteSegment2.Id, turnToNextSegment: TurnDirection.Left), + new SegmentSequence(segmentId: RouteSegment2.Id, direction: SegmentDirection.AtoB, type: SegmentSequenceType.Loop, nextSegmentId: RouteSegment3.Id, turnToNextSegment: TurnDirection.Left), + new SegmentSequence(segmentId: RouteSegment3.Id, direction: SegmentDirection.AtoB, type: SegmentSequenceType.LoopEnd) + }, + Sport = SportType.Cycling, + WorldId = "watopia", + World = new World { Id = "watopia", ZwiftId = ZwiftWorldId.Watopia }, + Name = "test", + ZwiftRouteName = "zwift test route name" + }; + } +} diff --git a/test/RoadCaptain.Tests.Unit/GameState/StateTransitionTestBase.cs b/test/RoadCaptain.Tests.Unit/GameState/StateTransitionTestBase.cs index e473402b..65a9eb1e 100644 --- a/test/RoadCaptain.Tests.Unit/GameState/StateTransitionTestBase.cs +++ b/test/RoadCaptain.Tests.Unit/GameState/StateTransitionTestBase.cs @@ -83,7 +83,7 @@ public class StateTransitionTestBase Sport = SportType.Cycling }; - protected readonly PlannedRoute Route = new() + protected virtual PlannedRoute Route { get; } = new() { RouteSegmentSequence = {