diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7027246..e887f14 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,9 +30,9 @@ jobs: - name: Test application runs run: | cd VirtualSpeed/VirtualSpeed - output=$(dotnet run --no-build --configuration Release -- --gpx routeWithDownhill.gpx) + output=$(dotnet run --no-build --configuration Release -- --gpx routeWithDownhill.gpx --power 200) echo "$output" - if echo "$output" | grep -q "Total Distance:"; then + if echo "$output" | grep -q "Total Distance:" && echo "$output" | grep -q "Avg Speed:"; then echo "✓ Application produced expected output" else echo "✗ Application did not produce expected output" diff --git a/VirtualSpeed/VirtualSpeed/Model/RideSegment.cs b/VirtualSpeed/VirtualSpeed/Model/RideSegment.cs new file mode 100644 index 0000000..b73d51a --- /dev/null +++ b/VirtualSpeed/VirtualSpeed/Model/RideSegment.cs @@ -0,0 +1,9 @@ +namespace VirtualSpeed.Model +{ + public record RideSegment( + double DurationSeconds, + double SpeedKmh, + double PowerWatts, + RouteSegment RouteSegment + ); +} diff --git a/VirtualSpeed/VirtualSpeed/Program.cs b/VirtualSpeed/VirtualSpeed/Program.cs index 192db02..96c54d9 100644 --- a/VirtualSpeed/VirtualSpeed/Program.cs +++ b/VirtualSpeed/VirtualSpeed/Program.cs @@ -8,13 +8,31 @@ class Program { static void Main(string[] args) { - if (args.Length < 2 || args[0] != "--gpx") + string filePath = null; + double power = 0; + bool hasPower = false; + + for (int i = 0; i < args.Length - 1; i++) { - Console.WriteLine("Usage: app.exe --gpx "); - return; + if (args[i] == "--gpx") + filePath = args[i + 1]; + else if (args[i] == "--power") + { + if (!double.TryParse(args[i + 1], System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, out power)) + { + Console.WriteLine("Error: --power must be a valid number."); + return; + } + hasPower = true; + } } - string filePath = args[1]; + if (filePath == null || !hasPower) + { + Console.WriteLine("Usage: app.exe --gpx --power "); + return; + } var parser = new GpxParser(); var builder = new RouteBuilder(); @@ -38,20 +56,30 @@ static void Main(string[] args) return; } + var rideCalculator = new RideCalculator(new Parameters()); + var rideSegments = rideCalculator.Calculate(segments, power); + double totalDistance = segments.Sum(s => s.LengthMeters); double totalElevationGain = segments .Where(s => s.EndElevationMeters > s.StartElevationMeters) .Sum(s => s.EndElevationMeters - s.StartElevationMeters); + double totalTimeSeconds = rideSegments.Sum(r => r.DurationSeconds); + double avgSpeedKmh = totalTimeSeconds > 0 + ? (totalDistance / totalTimeSeconds) * 3.6 + : 0; Console.WriteLine($"Total Distance: {totalDistance:F0} m"); Console.WriteLine($"Total Elevation Gain: {totalElevationGain:F0} m"); + Console.WriteLine($"Total Time: {TimeSpan.FromSeconds(totalTimeSeconds):hh\\:mm\\:ss}"); + Console.WriteLine($"Avg Speed: {avgSpeedKmh:F1} km/h"); Console.WriteLine(); Console.WriteLine("Segments:"); - foreach (var seg in segments) + foreach (var ride in rideSegments) { + var seg = ride.RouteSegment; double gradientPct = seg.AverageGradient * 100; - Console.WriteLine($"{seg.StartDistanceMeters:F0}m - {seg.StartDistanceMeters + seg.LengthMeters:F0}m | Gradient: {gradientPct:F1}%"); + Console.WriteLine($"{seg.StartDistanceMeters:F0}m - {seg.StartDistanceMeters + seg.LengthMeters:F0}m | Gradient: {gradientPct:F1}% | Speed: {ride.SpeedKmh:F1} km/h | Time: {ride.DurationSeconds:F0} s"); } } } diff --git a/VirtualSpeed/VirtualSpeed/Services/RideCalculator.cs b/VirtualSpeed/VirtualSpeed/Services/RideCalculator.cs new file mode 100644 index 0000000..2c12fb9 --- /dev/null +++ b/VirtualSpeed/VirtualSpeed/Services/RideCalculator.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using VirtualSpeed.Model; + +namespace VirtualSpeed.Services +{ + public class RideCalculator + { + private readonly Parameters _parameters; + + public RideCalculator(Parameters parameters) + { + _parameters = parameters; + } + + public IReadOnlyList Calculate(IReadOnlyList routeSegments, double powerWatts) + { + var rideSegments = new List(); + var calculator = new VirtualSpeedCalculator(_parameters); + + foreach (var routeSegment in routeSegments) + { + double speedKmh = calculator.CalculateVelocity(powerWatts, routeSegment.AverageGradient); + double speedMs = calculator.ConvertKmhToMS(speedKmh); + double durationSeconds = speedMs > 0 ? routeSegment.LengthMeters / speedMs : 0; + + rideSegments.Add(new RideSegment(durationSeconds, speedKmh, powerWatts, routeSegment)); + } + + return rideSegments; + } + } +} diff --git a/VirtualSpeed/VirtualSpeed/VirtualSpeedCalculator.cs b/VirtualSpeed/VirtualSpeed/VirtualSpeedCalculator.cs index 0641ca5..e059d8b 100644 --- a/VirtualSpeed/VirtualSpeed/VirtualSpeedCalculator.cs +++ b/VirtualSpeed/VirtualSpeed/VirtualSpeedCalculator.cs @@ -26,7 +26,12 @@ public VirtualSpeedCalculator(Parameters parameters) public double CalculatePower(double velocityKmh) { - Forces forces = CalculateForces(velocityKmh); + return CalculatePower(velocityKmh, 0.0); + } + + public double CalculatePower(double velocityKmh, double gradient) + { + Forces forces = CalculateForces(velocityKmh, gradient); double wheelPower = forces.Total * (velocityKmh * 1000.0 / 3600.0); @@ -35,14 +40,14 @@ public double CalculatePower(double velocityKmh) return legPower; } - private Forces CalculateForces(double velocityKmh) + private Forces CalculateForces(double velocityKmh, double gradient) { var forces = new Forces(); var velocityMS = ConvertKmhToMS(velocityKmh); - forces.Gravity = G * Parameters.WeightTotal * Math.Sin(Math.Atan(Parameters.ClimbGrade / 100)); + forces.Gravity = G * Parameters.WeightTotal * Math.Sin(Math.Atan(gradient)); - forces.Rolling = G * Parameters.WeightTotal * Math.Cos(Math.Atan(Parameters.ClimbGrade / 100)) * Parameters.Crr; + forces.Rolling = G * Parameters.WeightTotal * Math.Cos(Math.Atan(gradient)) * Parameters.Crr; forces.Drag = 0.5 * Parameters.GetCdA() * Parameters.Rho * velocityMS * velocityMS; @@ -61,13 +66,18 @@ public double CalculateDistance(double speedKmh, double timeSec) } public double CalculateVelocity(double power) + { + return CalculateVelocity(power, 0.0); + } + + public double CalculateVelocity(double power, double gradient) { var epsilon = 0.000001; var lowervel = -1000.0; var uppervel = 1000.0; var midvel = 0.0; - var midpow = CalculatePower(midvel); + var midpow = CalculatePower(midvel, gradient); var itcount = 0; do @@ -81,7 +91,7 @@ public double CalculateVelocity(double power) lowervel = midvel; midvel = (uppervel + lowervel) / 2.0; - midpow = CalculatePower(midvel); + midpow = CalculatePower(midvel, gradient); } while (itcount++ < 100); return midvel; @@ -95,7 +105,6 @@ public Parameters() DriveTrainLoss = 3.0; WeightRider = 83; WeightBike = 8; - ClimbGrade = 0; Crr = 0.005; A = 0.509; Cd = 0.63; @@ -107,8 +116,6 @@ public Parameters() public double WeightRider { get; set; } public double WeightBike { get; set; } - public double ClimbGrade { get; set; } - public double Crr { get; set; } public double WeightTotal