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)