GPX: export TrackPointExtension correctly (order and rest of the data under <extension> directly).

Importing old (incorrect) and new (correct) format supported.

Part of #2074.
This commit is contained in:
Dennis Guse
2025-03-16 11:06:04 +01:00
parent c9f7ef810e
commit 3df45c54d6
3 changed files with 207 additions and 18 deletions
@@ -238,6 +238,81 @@ public class GPXTrackImporterTest {
), importedTrackPoints);
}
/**
* until v4.18.0: some extensions where incorrectly added to gpxtpx:TrackPointExtension
* We only need to check the trackpoints.
*/
@LargeTest
@Test
public void gpx_legacy_trackpointextension() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GpxTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.legacy_gpx_trackpointextensions_incorrect);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then: We only need to check the trackpoints.
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
TrackPointAssert a = new TrackPointAssert()
.setDelta(0.05); // speed is not fully
a.assertEquals(List.of(
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:02:03Z"),
3d, 14d, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeLoss(1f)
.setAltitudeGain(1f),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:02:17Z"),
3d, 14.001, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(5)))
.setAltitudeLoss(1f)
.setAltitudeGain(1f)
.setSensorDistance(Distance.of(12))
.setHeartRate(69)
.setPower(50f)
.setCadence(3f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:21Z"),
3d, 14.002, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeLoss(0f)
.setAltitudeGain(0f),
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2020-02-02T02:03:22Z"),
3d, 16d, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(15)))
.setAltitudeLoss(0f)
.setAltitudeGain(0f),
new TrackPoint(TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2020-02-02T02:03:50Z"),
3d, 16.001, Distance.of(10),
Altitude.WGS84.of(10), null,
null,
Speed.of(10)))
.setAltitudeLoss(0f)
.setAltitudeGain(0f)
.setSpeed(Speed.of(15))
), actual);
}
@LargeTest
@Test
public void importExportTest_timezone() throws IOException {
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx
version="1.1"
creator="OpenTracks (Debug)"
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:topografix="http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:opentracks="http://opentracksapp.com/xmlschemas/v1"
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v2"
xmlns:gpxtrkx="http://www.garmin.com/xmlschemas/TrackStatsExtension/v1"
xmlns:cluetrust="http://www.cluetrust.com/Schemas/"
xmlns:pwr="http://www.garmin.com/xmlschemas/PowerExtension/v1"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.topografix.com/GPX/Private/TopoGrafix/0/1 http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.garmin.com/xmlschemas/TrackStatsExtension/v1 http://www.cluetrust.com/Schemas https://www.cluetrust.com/Schemas/gpxdata10.xsd http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd">
<wpt lat="3" lon="14">
<ele>10</ele>
<time>2020-02-02T03:02:03+01:00</time>
<name><![CDATA[Marker 1]]></name>
<desc><![CDATA[Marker 1 desc]]></desc>
<type><![CDATA[Marker 1 category]]></type>
</wpt>
<wpt lat="3" lon="14.001">
<ele>10</ele>
<time>2020-02-02T03:02:17+01:00</time>
<name><![CDATA[Marker 2]]></name>
<desc><![CDATA[Marker 2 desc]]></desc>
<type><![CDATA[Marker 2 category]]></type>
</wpt>
<trk>
<name><![CDATA[2020-02-02T03:02+01]]></name>
<desc><![CDATA[the description]]></desc>
<type><![CDATA[mountain biking]]></type>
<extensions>
<topografix:color>c0c0c0</topografix:color>
<opentracks:trackid>75c581d0-eef5-4aa2-91a3-66d2c6de4f6d</opentracks:trackid>
<opentracks:typeTranslated><![CDATA[the activity type]]></opentracks:typeTranslated>
<gpxtrkx:TrackStatsExtension>
<gpxtrkx:Distance>222125.53125</gpxtrkx:Distance>
<gpxtrkx:TimerTime>56</gpxtrkx:TimerTime>
<gpxtrkx:MovingTime>26</gpxtrkx:MovingTime>
<gpxtrkx:StoppedTime>30</gpxtrkx:StoppedTime>
<gpxtrkx:MaxSpeed>8543.2900390625</gpxtrkx:MaxSpeed>
<gpxtrkx:Ascent>2.0</gpxtrkx:Ascent>
<gpxtrkx:Descent>2.0</gpxtrkx:Descent>
</gpxtrkx:TrackStatsExtension>
</extensions>
<trkseg>
<trkpt lat="3" lon="14">
<ele>10</ele>
<time>2020-02-02T03:02:03+01:00</time>
<extensions><gpxtpx:TrackPointExtension>
<gpxtpx:speed>15</gpxtpx:speed>
<opentracks:gain>1</opentracks:gain>
<opentracks:loss>1</opentracks:loss>
<opentracks:accuracy_horizontal>10</opentracks:accuracy_horizontal></gpxtpx:TrackPointExtension></extensions>
</trkpt>
<trkpt lat="3" lon="14.001">
<ele>10</ele>
<time>2020-02-02T03:02:17+01:00</time>
<extensions><gpxtpx:TrackPointExtension>
<gpxtpx:speed>5</gpxtpx:speed>
<gpxtpx:hr>69</gpxtpx:hr>
<gpxtpx:cad>3</gpxtpx:cad>
<pwr:PowerInWatts>50</pwr:PowerInWatts>
<opentracks:gain>1</opentracks:gain>
<opentracks:loss>1</opentracks:loss>
<opentracks:accuracy_horizontal>10</opentracks:accuracy_horizontal><opentracks:distance>12</opentracks:distance>
<cluetrust:distance>12</cluetrust:distance>
</gpxtpx:TrackPointExtension></extensions>
</trkpt>
</trkseg>
<trkseg>
<trkpt lat="3" lon="14.002">
<ele>10</ele>
<time>2020-02-02T03:03:21+01:00</time>
<extensions><gpxtpx:TrackPointExtension>
<gpxtpx:speed>15</gpxtpx:speed>
<opentracks:gain>0</opentracks:gain>
<opentracks:loss>0</opentracks:loss>
<opentracks:accuracy_horizontal>10</opentracks:accuracy_horizontal></gpxtpx:TrackPointExtension></extensions>
</trkpt>
</trkseg>
<trkseg>
<trkpt lat="3" lon="16">
<ele>10</ele>
<time>2020-02-02T03:03:22+01:00</time>
<extensions><gpxtpx:TrackPointExtension>
<gpxtpx:speed>15</gpxtpx:speed>
<opentracks:gain>0</opentracks:gain>
<opentracks:loss>0</opentracks:loss>
<opentracks:accuracy_horizontal>10</opentracks:accuracy_horizontal></gpxtpx:TrackPointExtension></extensions>
</trkpt>
<trkpt lat="3" lon="16.001">
<ele>10</ele>
<time>2020-02-02T03:03:50+01:00</time>
<extensions><gpxtpx:TrackPointExtension>
<gpxtpx:speed>15</gpxtpx:speed>
<opentracks:gain>0</opentracks:gain>
<opentracks:loss>0</opentracks:loss>
<opentracks:accuracy_horizontal>10</opentracks:accuracy_horizontal></gpxtpx:TrackPointExtension></extensions>
</trkpt>
</trkseg>
</trk>
</gpx>
@@ -321,51 +321,62 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime(), zoneOffset) + "</time>");
{
String trackPointExtensionContent = "";
if (trackPoint.hasSpeed()) {
trackPointExtensionContent += "<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed().toMPS()) + "</gpxtpx:speed>\n";
}
String trackPointExtensionV2Content = "";
if (trackPoint.hasHeartRate()) {
trackPointExtensionContent += "<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.getHeartRate().getBPM()) + "</gpxtpx:hr>\n";
trackPointExtensionV2Content += "<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.getHeartRate().getBPM()) + "</gpxtpx:hr>\n";
}
if (trackPoint.hasCadence()) {
trackPointExtensionContent += "<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCadence().getRPM()) + "</gpxtpx:cad>\n";
trackPointExtensionV2Content += "<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCadence().getRPM()) + "</gpxtpx:cad>\n";
}
if (trackPoint.hasSpeed()) {
trackPointExtensionV2Content += "<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed().toMPS()) + "</gpxtpx:speed>\n";
}
String extensionContent = "";
if (trackPoint.hasPower()) {
trackPointExtensionContent += "<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.getPower().getW()) + "</pwr:PowerInWatts>\n";
extensionContent += "<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.getPower().getW()) + "</pwr:PowerInWatts>\n";
}
Double cumulativeGain = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeGain() ? (double) tp.getAltitudeGain() : null);
if (cumulativeGain != null) {
trackPointExtensionContent += ("<opentracks:gain>" + ALTITUDE_FORMAT.format(cumulativeGain) + "</opentracks:gain>\n");
extensionContent += ("<opentracks:gain>" + ALTITUDE_FORMAT.format(cumulativeGain) + "</opentracks:gain>\n");
}
Double cumulativeLoss = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeLoss() ? (double) tp.getAltitudeLoss() : null);
if (cumulativeLoss != null) {
trackPointExtensionContent += ("<opentracks:loss>" + ALTITUDE_FORMAT.format(cumulativeLoss) + "</opentracks:loss>\n");
extensionContent += ("<opentracks:loss>" + ALTITUDE_FORMAT.format(cumulativeLoss) + "</opentracks:loss>\n");
}
if (trackPoint.hasHorizontalAccuracy()) {
trackPointExtensionContent += ("<opentracks:accuracy_horizontal>" + DISTANCE_FORMAT.format(trackPoint.getHorizontalAccuracy().toM()) + "</opentracks:accuracy_horizontal>");
extensionContent += ("<opentracks:accuracy_horizontal>" + DISTANCE_FORMAT.format(trackPoint.getHorizontalAccuracy().toM()) + "</opentracks:accuracy_horizontal>");
}
if (trackPoint.hasVerticalAccuracy()) {
trackPointExtensionContent += ("<opentracks:accuracy_vertical>" + DISTANCE_FORMAT.format(trackPoint.getVerticalAccuracy().toM()) + "</opentracks:accuracy_vertical>");
extensionContent += ("<opentracks:accuracy_vertical>" + DISTANCE_FORMAT.format(trackPoint.getVerticalAccuracy().toM()) + "</opentracks:accuracy_vertical>");
}
cumulativeDistance = Distance.ofOrNull(cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasSensorDistance() ? tp.getSensorDistance().toM() : null));
if (cumulativeDistance != null) {
trackPointExtensionContent += ("<opentracks:distance>" + DISTANCE_FORMAT.format(cumulativeDistance.toM()) + "</opentracks:distance>\n");
trackPointExtensionContent += ("<cluetrust:distance>" + DISTANCE_FORMAT.format(trackDistance.plus(cumulativeDistance).toM()) + "</cluetrust:distance>\n");
extensionContent += ("<opentracks:distance>" + DISTANCE_FORMAT.format(cumulativeDistance.toM()) + "</opentracks:distance>\n");
extensionContent += ("<cluetrust:distance>" + DISTANCE_FORMAT.format(trackDistance.plus(cumulativeDistance).toM()) + "</cluetrust:distance>\n");
}
if (!trackPointExtensionContent.isEmpty()) {
printWriter.println("<extensions><gpxtpx:TrackPointExtension>");
printWriter.print(trackPointExtensionContent);
printWriter.println("</gpxtpx:TrackPointExtension></extensions>");
if (!extensionContent.isEmpty() || !trackPointExtensionV2Content.isEmpty()) {
printWriter.println("<extensions>");
if (!trackPointExtensionV2Content.isEmpty()) {
printWriter.println("<gpxtpx:TrackPointExtension>");
printWriter.print(trackPointExtensionV2Content);
printWriter.println("</gpxtpx:TrackPointExtension>");
}
if (!extensionContent.isEmpty()) {
printWriter.print(extensionContent);
}
printWriter.println("</extensions>");
}
}