From da1bcc7c0b3e0ea68d58620ca03c7375a6d4e9b5 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Wed, 21 Oct 2026 21:00:26 +0200 Subject: [PATCH] fix: write course points between the records, at their place on the course Course points (the injected Start/End/interval points and the GPX waypoints) were all written before the first record. They are now written right after the record at their distance along the course, as Garmin's EncodeCourse example does. --- .../java/org/surfsite/gexporter/Gpx2Fit.java | 46 ++++++++++++++----- .../org/surfsite/gexporter/TestCourseFit.java | 46 +++++++++++++++++-- 2 files changed, 77 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/surfsite/gexporter/Gpx2Fit.java b/app/src/main/java/org/surfsite/gexporter/Gpx2Fit.java index fe18665..b7f5bcd 100644 --- a/app/src/main/java/org/surfsite/gexporter/Gpx2Fit.java +++ b/app/src/main/java/org/surfsite/gexporter/Gpx2Fit.java @@ -698,12 +698,17 @@ public void writeFit(File outfile) { pt_min_dist = mGpx2FitOptions.getMinRoutePointDistance(); + // Course points are written between the records, each right after + // the record at its distance along the course (as Garmin's + // EncodeCourse example does). + List cps = new ArrayList<>(); + if (!skipExtraCP && !wayPoints.isEmpty()) { - writeWayPoints(encode, wayPoints, duration); + writeWayPoints(cps, wayPoints, duration); } if (!skipExtraCP && !rtePoints.isEmpty()) { - writeWayPoints(encode, rtePoints, duration); + writeWayPoints(cps, rtePoints, duration); } { @@ -747,18 +752,23 @@ public void writeFit(File outfile) { cp.setDistance((float) dist); cp.setTimestamp(timestamp); - encode.write(cp); + cps.add(cp); written = true; } if (wpt.equals(lastWayPoint)) { + if (written) { + // a one-point track: "Start" is already in cps + cp = new CoursePointMesg(); + cp.setLocalNum(0); + } cp.setPositionLat(wpt.getLatSemi()); cp.setPositionLong(wpt.getLonSemi()); cp.setName("End"); cp.setType(CoursePoint.GENERIC); cp.setDistance((float) dist); cp.setTimestamp(timestamp); - encode.write(cp); + cps.add(cp); written = true; } else if ((dist - lcdist) > cp_min_dist) { cp.setName(""); @@ -767,7 +777,7 @@ public void writeFit(File outfile) { cp.setPositionLong(wpt.getLonSemi()); cp.setDistance((float) dist); cp.setTimestamp(timestamp); - encode.write(cp); + cps.add(cp); lcdist = dist; written = true; } @@ -786,6 +796,13 @@ public void writeFit(File outfile) { } + // in course order; the sort is stable, so ties keep their order + Collections.sort(cps, (a, b) -> { + int c = Float.compare(a.getDistance(), b.getDistance()); + return c != 0 ? c : Long.compare(a.getTimestamp().getTimestamp(), b.getTimestamp().getTimestamp()); + }); + int nextCp = 0; + for (WayPoint wpt : trkPoints) { boolean written = false; i += 1; @@ -821,6 +838,12 @@ public void writeFit(File outfile) { r.setSpeed((float) 0.0); } + // The course points before this record follow the last one + // written. Both distances went through the same rounding. + while (nextCp < cps.size() && cps.get(nextCp).getDistance() < r.getDistance()) { + encode.write(cps.get(nextCp++)); + } + encode.write(r); ldist = dist; ltimestamp = l; @@ -836,6 +859,10 @@ public void writeFit(File outfile) { } } + while (nextCp < cps.size()) { + encode.write(cps.get(nextCp++)); + } + { EventMesg eventMesg = new EventMesg(); eventMesg.setLocalNum(0); @@ -859,10 +886,9 @@ public void writeFit(File outfile) { * Write GPX waypoints as course points. A course point needs the distance * along the course (and the matching timestamp), or devices show all of * them at the end of the course in "Up Ahead". Each waypoint takes them - * from the nearest track point; the points are written in course order. + * from the nearest track point; the points are added to cps. */ - private void writeWayPoints(FileEncoder encode, List points, long duration) { - List cps = new ArrayList<>(); + private void writeWayPoints(List cps, List points, long duration) { for (WayPoint wpt : points) { int nearest = nearestTrackPoint(wpt); WayPoint trk = trkPoints.get(nearest); @@ -882,10 +908,6 @@ private void writeWayPoints(FileEncoder encode, List points, long dura cp.setTimestamp(new DateTime(new Date(WayPoint.RefMilliSec + (nearest + 1) * 1000L))); cps.add(cp); } - Collections.sort(cps, (a, b) -> Float.compare(a.getDistance(), b.getDistance())); - for (CoursePointMesg cp : cps) { - encode.write(cp); - } } /** Index of the track point closest to wpt. */ diff --git a/app/src/test/java/org/surfsite/gexporter/TestCourseFit.java b/app/src/test/java/org/surfsite/gexporter/TestCourseFit.java index 5571e0f..93fcbb4 100644 --- a/app/src/test/java/org/surfsite/gexporter/TestCourseFit.java +++ b/app/src/test/java/org/surfsite/gexporter/TestCourseFit.java @@ -6,6 +6,7 @@ import com.garmin.fit.CoursePointMesg; import com.garmin.fit.Decode; +import com.garmin.fit.Mesg; import com.garmin.fit.MesgBroadcaster; import com.garmin.fit.RecordMesg; @@ -32,6 +33,8 @@ public class TestCourseFit { private final List records = new ArrayList<>(); private final List coursePoints = new ArrayList<>(); + /** Records and course points in file order. */ + private final List trail = new ArrayList<>(); /** A straight track north, 0.01° (about 1.1 km) per point. */ private File gpx(String wpts, String timeFormat) throws Exception { @@ -56,14 +59,24 @@ private File gpx(String wpts, String timeFormat) throws Exception { } private void convertAndDecode(File gpx) throws Exception { + convertAndDecode(gpx, new Gpx2FitOptions()); + } + + private void convertAndDecode(File gpx, Gpx2FitOptions options) throws Exception { File fit = tmp.newFile("out.fit"); try (FileInputStream in = new FileInputStream(gpx)) { - new Gpx2Fit("t", in, new Gpx2FitOptions()).writeFit(fit); + new Gpx2Fit("t", in, options).writeFit(fit); } Decode decode = new Decode(); MesgBroadcaster broadcaster = new MesgBroadcaster(decode); - broadcaster.addListener((com.garmin.fit.RecordMesgListener) records::add); - broadcaster.addListener((com.garmin.fit.CoursePointMesgListener) coursePoints::add); + broadcaster.addListener((com.garmin.fit.RecordMesgListener) m -> { + records.add(m); + trail.add(m); + }); + broadcaster.addListener((com.garmin.fit.CoursePointMesgListener) m -> { + coursePoints.add(m); + trail.add(m); + }); try (InputStream in = new FileInputStream(fit)) { assertTrue("FIT integrity", decode.checkFileIntegrity(in)); } @@ -93,6 +106,33 @@ public void waypointsGetTheirDistanceAlongTheCourse() throws Exception { assertEquals(records.get(8).getTimestamp().getTimestamp(), late.getTimestamp().getTimestamp()); } + @Test + public void coursePointsFollowTheirRecord() throws Exception { + Gpx2FitOptions options = new Gpx2FitOptions(); + options.setInjectCoursePoints(true); + convertAndDecode(gpx( + "late\n" + + "early\n", + "2026-09-05T10:%02d:00Z"), options); + + // Start, End, the injected ones and both waypoints + assertTrue(coursePoints.size() > 4); + assertEquals("Start", coursePoints.get(0).getName()); + assertEquals("End", coursePoints.get(coursePoints.size() - 1).getName()); + assertTrue("starts with a record", trail.get(0) instanceof RecordMesg); + RecordMesg lastRecord = null; + for (Mesg m : trail) { + if (m instanceof RecordMesg) { + lastRecord = (RecordMesg) m; + continue; + } + CoursePointMesg cp = (CoursePointMesg) m; + // right after the record it belongs to: same distance and time + assertEquals(cp.getName(), lastRecord.getDistance(), cp.getDistance(), 0.5f); + assertEquals(cp.getName(), lastRecord.getTimestamp().getTimestamp(), cp.getTimestamp().getTimestamp()); + } + } + @Test public void timesRelativeTo1970AreMovedToTheFitEpoch() throws Exception { // Locus Map writes route times relative to 1970-01-01 (issue #36)