Skip to content

Refactor into GPX-based route simulation engine#6

Merged
simonech merged 4 commits intomasterfrom
copilot/refactor-app-route-simulation
Feb 19, 2026
Merged

Refactor into GPX-based route simulation engine#6
simonech merged 4 commits intomasterfrom
copilot/refactor-app-route-simulation

Conversation

Copy link
Contributor

Copilot AI commented Feb 19, 2026

  • Explore repository structure and understand existing code
  • Create Model/TrackPoint.cs record (updated to use DateTimeOffset?)
  • Create Model/RouteSegment.cs record
  • Create Services/GpxParser.cs – skip points missing elevation (instead of defaulting to 0); use DateTimeOffset parsing
  • Create Services/RouteBuilder.cs – added stepDistance <= 0 guard for duplicate GPS points
  • Replace Program.cs TCX logic with new --gpx <file> CLI entry point
  • Add route.gpx sample file and update csproj (remove TCX reference, add GPX)
  • Update .github/workflows/build.yml to run CLI with --gpx route.gpx and validate output
  • Build and verify output matches expected format
  • Code review passed, CodeQL clean
Original prompt

This section details on the original issue you should resolve

<issue_title>🚴 Refactor App into Route-Based Simulation (GPX First Step)</issue_title>
<issue_description>## 🎯 Goal

Change the goal of the app.

Instead of converting power to velocity, the application should evolve into a simulation engine based on real-world routes.

First step: implement GPX parsing and route segmentation.


1️⃣ Remove Legacy Code

  • Remove the old inline TCX parser.
  • Remove any TCX-specific logic.
  • Keep existing physics code unchanged for now.

2️⃣ Implement GPX Parsing

Create a GPX parser that:

  • Accepts a GPX file path.
  • Parses the first <trk> and first <trkseg> only.
  • Extracts all <trkpt> elements.
  • Reads:
    • Latitude
    • Longitude
    • Elevation (<ele>)
    • Timestamp (<time>, optional)

Ignore:

  • Extensions
  • Heart rate
  • Power
  • Cadence
  • Multiple tracks
  • Metadata

Model

public record TrackPoint(
    double Latitude,
    double Longitude,
    double ElevationMeters,
    DateTime? Timestamp
);

Put the model in a model folder

3️⃣ Implement Route Builder

Create a RouteBuilder service that:
• Accepts IEnumerable
• Sorts points by timestamp (if timestamps exist)
• Computes cumulative distance between consecutive points
• Builds fixed-length segments (default: 100 meters)

Distance Calculation

Use the Haversine formula to compute distance between consecutive track points.

Distance is required for:
• Cumulative distance
• Segment length
• Segment aggregation

Segment model

public record RouteSegment(
double StartDistanceMeters,
double LengthMeters,
double StartElevationMeters,
double EndElevationMeters,
double AverageGradient
);

Also in model folder

Segment Construction Rules
• Aggregate distance between track points until segment length ≥ configured segment size (default 100m).
• Interpolate elevation if necessary at the segment boundary.
• Compute:

AverageGradient = (EndElevation - StartElevation) / Length

Store gradient as decimal (e.g. 0.03 for 3%).
• Continue until route ends.
• The final segment may be shorter than configured segment size.

6️⃣ Folder Structure

Organize project into:

Model/
TrackPoint.cs
RouteSegment.cs

Services/
GpxParser.cs
RouteBuilder.cs

7️⃣ Update Command Line Interface

Update CLI so that:

app.exe --gpx route.gpx

Will:

  1. Parse GPX
  2. Build route
  3. Print:
    • Total distance
    • Total elevation gain
    • List of segments (distance start, length, avg gradient)

Example output:

Total Distance: 12430 m
Total Elevation Gain: 180 m

Segments:
0m - 100m | Gradient: 2.3%
100m - 200m | Gradient: 1.8%
...

Replace the sample tcx into a sample gpx

Simple Test Route Test Track
  <trkpt lat="50.8503" lon="4.3517">
    <ele>50.0</ele>
    <time>2026-02-18T10:00:00Z</time>
  </trkpt>

  <trkpt lat="50.8508" lon="4.3522">
    <ele>52.0</ele>
    <time>2026-02-18T10:01:00Z</time>
  </trkpt>

  <trkpt lat="50.8513" lon="4.3527">
    <ele>55.0</ele>
    <time>2026-02-18T10:02:00Z</time>
  </trkpt>

  <trkpt lat="50.8518" lon="4.3532">
    <ele>53.0</ele>
    <time>2026-02-18T10:03:00Z</time>
  </trkpt>

  <trkpt lat="50.8523" lon="4.3537">
    <ele>56.0</ele>
    <time>2026-02-18T10:04:00Z</time>
  </trkpt>

  <trkpt lat="50.8528" lon="4.3542">
    <ele>54.0</ele>
    <time>2026-02-18T10:05:00Z</time>
  </trkpt>

</trkseg>

</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: simonech <61557+simonech@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor app into route-based simulation with GPX parsing Refactor into GPX-based route simulation engine Feb 19, 2026
Copilot AI requested a review from simonech February 19, 2026 14:57
@simonech
Copy link
Owner

simonech commented Feb 19, 2026

Can you also update the workflow to run the CLI with the new version? @copilot

…low update

Co-authored-by: simonech <61557+simonech@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 19, 2026

Can you also update the workflow to run the CLI with the new version? @copilot

Updated in 83592f4. The workflow now runs dotnet run -- --gpx route.gpx and validates that the output contains "Total Distance:" before passing.

Co-authored-by: simonech <61557+simonech@users.noreply.github.com>
@simonech simonech marked this pull request as ready for review February 19, 2026 15:34
Copilot AI review requested due to automatic review settings February 19, 2026 15:34
@simonech simonech merged commit 82116fe into master Feb 19, 2026
5 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the VirtualSpeed application from a TCX-based power-to-velocity converter into a GPX-based route simulation engine. The changes replace the legacy XML parsing logic with a modular architecture consisting of model classes for track points and route segments, along with dedicated services for GPX parsing and route building.

Changes:

  • Introduced new model classes (TrackPoint and RouteSegment) using C# records
  • Created GpxParser service to extract GPS track points with elevation and timestamp data
  • Created RouteBuilder service to segment routes into fixed-length intervals using Haversine distance calculations
  • Replaced TCX-based CLI with GPX-based interface (--gpx flag)
  • Updated sample data file and CI/CD workflow to use GPX format

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
VirtualSpeed/VirtualSpeed/Model/TrackPoint.cs Defines immutable record for GPS track points with coordinates, elevation, and optional timestamp
VirtualSpeed/VirtualSpeed/Model/RouteSegment.cs Defines immutable record for route segments with distance, elevation, and gradient data
VirtualSpeed/VirtualSpeed/Services/GpxParser.cs Implements GPX XML parsing with namespace handling and data extraction
VirtualSpeed/VirtualSpeed/Services/RouteBuilder.cs Implements route segmentation using Haversine distance formula with elevation interpolation
VirtualSpeed/VirtualSpeed/Program.cs Complete rewrite of CLI to parse GPX files and output route statistics
VirtualSpeed/VirtualSpeed/route.gpx Sample GPX file with 20 track points for testing
VirtualSpeed/VirtualSpeed/VirtualSpeed.csproj Updated to include route.gpx instead of powerData.tcx
.github/workflows/build.yml Modified to run application with GPX input and validate output format

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +22 to 31
System.Collections.Generic.IEnumerable<Model.TrackPoint> points;
try
{
myXmlDocument.Load(filename);
points = parser.Parse(filePath);
}
catch(Exception e)
catch (Exception e)
{
Console.WriteLine("Error while loading xml file : " + e.Message);
Console.WriteLine("Error while loading GPX file: " + e.Message);
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 lazy enumeration from Parse() means parsing errors will occur after this try-catch block exits, when Build() materializes the IEnumerable. Consider calling .ToList() on the parsed points within the try-catch to ensure parsing errors are properly caught and reported with the "Error while loading GPX file" message.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +42
double lat = double.Parse(latAttr, CultureInfo.InvariantCulture);
double lon = double.Parse(lonAttr, CultureInfo.InvariantCulture);

var eleNode = node.SelectSingleNode("gpx:ele", ns) ?? node.SelectSingleNode("ele");
if (eleNode == null)
continue;
double elevation = double.Parse(eleNode.InnerText, CultureInfo.InvariantCulture);

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.

Missing error handling for malformed latitude and longitude values. If double.Parse fails, the exception will propagate up without a descriptive error message. Consider using double.TryParse and either skipping invalid points with a warning or providing a clearer error message indicating which track point has invalid coordinates.

Suggested change
double lat = double.Parse(latAttr, CultureInfo.InvariantCulture);
double lon = double.Parse(lonAttr, CultureInfo.InvariantCulture);
var eleNode = node.SelectSingleNode("gpx:ele", ns) ?? node.SelectSingleNode("ele");
if (eleNode == null)
continue;
double elevation = double.Parse(eleNode.InnerText, CultureInfo.InvariantCulture);
if (!double.TryParse(latAttr, NumberStyles.Float, CultureInfo.InvariantCulture, out double lat))
throw new FormatException($"Invalid latitude value '{latAttr}' in GPX track point.");
if (!double.TryParse(lonAttr, NumberStyles.Float, CultureInfo.InvariantCulture, out double lon))
throw new FormatException($"Invalid longitude value '{lonAttr}' in GPX track point.");
var eleNode = node.SelectSingleNode("gpx:ele", ns) ?? node.SelectSingleNode("ele");
if (eleNode == null)
continue;
if (!double.TryParse(eleNode.InnerText, NumberStyles.Float, CultureInfo.InvariantCulture, out double elevation))
throw new FormatException($"Invalid elevation value '{eleNode.InnerText}' in GPX track point.");

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +42
double elevation = double.Parse(eleNode.InnerText, CultureInfo.InvariantCulture);

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.

Missing error handling for malformed elevation values. If double.Parse fails, the exception will propagate up without a descriptive error message. Consider using double.TryParse and either skipping invalid points with a warning or providing a clearer error message indicating which track point has invalid elevation data.

Suggested change
double elevation = double.Parse(eleNode.InnerText, CultureInfo.InvariantCulture);
if (!double.TryParse(eleNode.InnerText, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var elevation))
throw new FormatException($"Invalid elevation value '{eleNode.InnerText}' in GPX track point.");

Copilot uses AI. Check for mistakes.
private readonly double _segmentLengthMeters;

public RouteBuilder(double segmentLengthMeters = 100.0)
{
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.

Constructor allows non-positive segment lengths, which could cause issues. The gradient calculation at line 52 checks for positive segment length, but negative or zero values would cause logical errors elsewhere (e.g., the while loop at line 44 would never terminate if _segmentLengthMeters is negative). Consider adding validation in the constructor to ensure _segmentLengthMeters is positive.

Suggested change
{
{
if (segmentLengthMeters <= 0)
throw new ArgumentOutOfRangeException(nameof(segmentLengthMeters), "Segment length must be positive.");

Copilot uses AI. Check for mistakes.
var builder = new RouteBuilder();

// try to open the xml file and abort if any exception is raised
System.Collections.Generic.IEnumerable<Model.TrackPoint> points;
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.

Unnecessary fully qualified type name. Add 'using System.Collections.Generic;' at the top of the file and change this line to just 'IEnumerable points;' for consistency with the rest of the code.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🚴 Refactor App into Route-Based Simulation (GPX First Step)

3 participants