Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow test checks for "Total Distance:" and "Avg Speed:" in the output, but doesn't verify "Total Time:" which is also a new output added in this PR. Consider adding a check for "Total Time:" to ensure all new outputs are being generated correctly.

Suggested change
if echo "$output" | grep -q "Total Distance:" && echo "$output" | grep -q "Avg Speed:"; then
if echo "$output" | grep -q "Total Distance:" && echo "$output" | grep -q "Avg Speed:" && echo "$output" | grep -q "Total Time:"; then

Copilot uses AI. Check for mistakes.
echo "✓ Application produced expected output"
else
echo "✗ Application did not produce expected output"
Expand Down
9 changes: 9 additions & 0 deletions VirtualSpeed/VirtualSpeed/Model/RideSegment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace VirtualSpeed.Model
{
public record RideSegment(
double DurationSeconds,
double SpeedKmh,
double PowerWatts,
RouteSegment RouteSegment
);
}
40 changes: 34 additions & 6 deletions VirtualSpeed/VirtualSpeed/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <route.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;
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The power argument is parsed but not validated for positive values. A negative or zero power value would be physically meaningless and could cause issues in the velocity calculation. Consider adding validation to ensure power is a positive number (e.g., power <= 0) and providing a clear error message if it's not.

Suggested change
}
}
if (power <= 0)
{
Console.WriteLine("Error: --power must be a positive number (watts).");
return;
}

Copilot uses AI. Check for mistakes.
hasPower = true;
}
}
Comment on lines +15 to 29
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The argument parsing loop will skip the last argument if it's a flag. The condition i < args.Length - 1 means if someone passes --gpx file.gpx --power (without a value), the --power flag won't be checked. Additionally, if --power is the last argument with a value, it will work, but if --gpx is the last argument, it won't be processed. Consider changing the loop to check i < args.Length and then verify that i + 1 < args.Length before accessing args[i + 1] to provide better error handling.

Copilot uses AI. Check for mistakes.

string filePath = args[1];
if (filePath == null || !hasPower)
{
Console.WriteLine("Usage: app.exe --gpx <route.gpx> --power <watts>");
return;
}

var parser = new GpxParser();
var builder = new RouteBuilder();
Expand All @@ -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");
}
}
}
Expand Down
32 changes: 32 additions & 0 deletions VirtualSpeed/VirtualSpeed/Services/RideCalculator.cs
Original file line number Diff line number Diff line change
@@ -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<RideSegment> Calculate(IReadOnlyList<RouteSegment> routeSegments, double powerWatts)
{
var rideSegments = new List<RideSegment>();
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;
}
}
}
25 changes: 16 additions & 9 deletions VirtualSpeed/VirtualSpeed/VirtualSpeedCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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;

Expand All @@ -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
Expand All @@ -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;
Expand All @@ -95,7 +105,6 @@ public Parameters()
DriveTrainLoss = 3.0;
WeightRider = 83;
WeightBike = 8;
ClimbGrade = 0;
Crr = 0.005;
A = 0.509;
Cd = 0.63;
Expand All @@ -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
Expand Down