Skip to content
Open
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
46 changes: 34 additions & 12 deletions app/src/main/java/org/surfsite/gexporter/Gpx2Fit.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<CoursePointMesg> 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);
}

{
Expand Down Expand Up @@ -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("");
Expand All @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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<WayPoint> points, long duration) {
List<CoursePointMesg> cps = new ArrayList<>();
private void writeWayPoints(List<CoursePointMesg> cps, List<WayPoint> points, long duration) {
for (WayPoint wpt : points) {
int nearest = nearestTrackPoint(wpt);
WayPoint trk = trkPoints.get(nearest);
Expand All @@ -882,10 +908,6 @@ private void writeWayPoints(FileEncoder encode, List<WayPoint> 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. */
Expand Down
46 changes: 43 additions & 3 deletions app/src/test/java/org/surfsite/gexporter/TestCourseFit.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -32,6 +33,8 @@ public class TestCourseFit {

private final List<RecordMesg> records = new ArrayList<>();
private final List<CoursePointMesg> coursePoints = new ArrayList<>();
/** Records and course points in file order. */
private final List<Mesg> trail = new ArrayList<>();

/** A straight track north, 0.01° (about 1.1 km) per point. */
private File gpx(String wpts, String timeFormat) throws Exception {
Expand All @@ -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));
}
Expand Down Expand Up @@ -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(
"<wpt lat=\"48.0801\" lon=\"9.0001\"><name>late</name></wpt>\n"
+ "<wpt lat=\"48.0199\" lon=\"8.9999\"><name>early</name></wpt>\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)
Expand Down
Loading