Widen curves TrainingPeaks rejects as too tight - #14
Merged
Merged
Conversation
Owner
Author
|
@sentry review |
BenA-SA
force-pushed
the
feat/widen-tight-curves
branch
from
September 20, 2026 08:12
4d39ca1 to
eb90995
Compare
TrainingPeaks refuses any upload whose points describe a curve radius under 6 m. The Malaga course fixed by ADR-0002 was rejected at a 96 degree junction turn drawn with two points ~5 m apart, radius 4.69 m. The sideways offset caused it: the same corner measured 7.41 m before, and shifting sideways moves the inside of a bend towards its own centre of curvature. Four more corners sat at or near the threshold. Replace each tight corner with a circular arc tangent to the straights either side, sampled every 10 degrees so every consecutive triple sits on that circle - the quantity TrainingPeaks measures. On Malaga, five corners widened to >= 8.25 m for -21 m of length (-0.023%), no point moved more than 3.44 m, and the leg separation from ADR-0002 held. The two tools now have a required order, separate legs then widen curves, since offsetting tightens corners. A hairpin between close legs has no room for an arc; the script names it and points back at gpx-separate-legs.py --offset.
Render with the source point tag, not a hardcoded <trkpt>. The parser accepts <rtept> too, but _render partitioned on '<trkpt', so an rtept route -- what TrainingPeaks exports for a planned route -- fell through with head=the whole document: the original points were kept and the widened ones appended after </gpx>. Output was invalid XML with every point duplicated (+169% length on a test route). Splice each arc against the points it was actually hung between. Fillet.arc() searches successively wider tangent pairs (back 1..3) but returned only the sampled points, so _apply_groups assumed back=1 and kept points the arc had already passed. On a corner with a starved run-in that doubles the path back on itself -- an exact 180 degree reversal, reproduced on four starved-tangent courses. arc() now returns an Arc carrying its before/after indices. Behaviour at back=1 is unchanged (before == start-1, after+1 == end+2).
Both were auto-marked 'Resolved in 69ab2ed' because that commit touched the file, not because it addressed them. Neither was fixed; both reproduce. Collinear course crashes the report. Every radius is inf on a straight line, so 'min(r for r in radii if r != inf)' gets an empty sequence and raises ValueError. A 6-point straight course dies on it. Report n/a instead. A point with no elevation was written as a self-closing tag, which this tool's own parser cannot see -- its regex requires a closing tag. The effect was total: on a GPX carrying no <ele>, the widen succeeded and then re-parsing its own output raised 'no <trkpt>/<rtept> points found', so nothing was written at all. Emit '<tag ...></tag>' instead; an 18-point no-elevation course now round-trips to 25 points, valid XML.
BenA-SA
force-pushed
the
feat/widen-tight-curves
branch
from
September 20, 2026 08:37
f0e992b to
83116cd
Compare
Seer flagged that a tight corner at index 1 (or the last) cannot be
widened, because _tangent_candidates needs a straight either side and
there is none at a route boundary. That limitation is real and is not
worth engineering around: the suggested fix invents context points, i.e.
route geometry the rider never rode.
The reportable defect sat next to it. main() printed 'widened N
corner(s)' from the count of corners FOUND tight, not the count
actually fixed, so a boundary corner produced:
! no arc of 8 m or wider fits the corner where point 1 turns 90 deg
widened 1 corner(s) to 9 m <- untrue
points under 8 m: 1
and wrote a file TPV still rejects, while claiming success. Report
'widened X of Y' from the failures instead, and say plainly when a
corner is unfixable because it sits at the boundary rather than blaming
its geometry.
Seer, correctly, on my own previous fix: 'widened X of Y' took Y from len(tight) -- individual tight POINTS -- and X by subtracting len(notes), which counts failed corner GROUPS. Mixing the two units overstates the work whenever one group holds several tight points. Reproduced with a boundary corner spanning two tight points: it printed 'widened 1 of 2 corner(s)' while widening none, with 'points under 8 m: 2' unchanged directly beneath it. widen() now returns the number of arcs it actually applied, summed per pass as groups-minus-failures, and main() prints that. A two-tight-point corner that succeeds now reports 'widened 1 corner', not 2.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TrainingPeaks rejected the Malaga course that ADR-0002's leg offset produced:
That point is a ~96° junction turn described by two points 4.5 m and 5.1 m apart. The offset caused it — the same corner measured 7.41 m before the shift, because moving a course sideways pushes the inside of a bend towards its own centre of curvature. Four more corners were at or near the limit, two of them at 6.14 m and 6.35 m, which would have failed the next upload attempt.
What this adds
tools/gpx-widen-curves.pyreplaces each over-tight corner with a circular arc tangent to the straights either side, sampled every 10° so that every consecutive triple of points sits on a circle of exactly that radius — the quantity TrainingPeaks measures. It sweeps repeatedly, because widening one corner can leave its new ends curving, and backs off to a smaller radius or wider straights rather than leaving a corner untouched.Results on the offset Malaga course:
The two tools now have a required order — separate legs, then widen curves — because offsetting tightens corners. A hairpin between close legs has no room for an arc of any useful radius; the script names it and points back at
gpx-separate-legs.py --offset, which opens the hairpin up.Also here
docs/custom-route-tight-curves.md— the upload rejection, the fix, the ordering, and an AI prompt describing the same algorithmdocs/adr/0006-widen-tight-curves-with-circular-fillets.md— builds on ADR-0002, including the three approaches rejected firstcustom-route-overlap.mdNot yet proven
The widened file clears the check as we compute it, but hasn't been through TrainingPeaks' own validator. Confirm on the next upload. Note TrainingPeaks reported the failure at 60 135 m where we measure that corner at 60 077 m — probably a 3D distance against our 2D one; the coordinates match exactly.