GPX: Fix parsing of timestamps without timezone

Although these are non-conforming, there are a lot of gpx files online
that do not contain a timezone and fail to be parsed.
This commit is contained in:
José Rebelo
2026-05-23 18:42:49 +01:00
parent 89e495c6b1
commit 86d42bab78
3 changed files with 64 additions and 0 deletions
@@ -160,6 +160,10 @@ public class GpxParser {
if (text == null || text.length() < 1) { if (text == null || text.length() < 1) {
return defaultDate; return defaultDate;
} }
// If no timezone indicator is present, assume UTC
if (!text.endsWith("Z") && !text.contains("+") && !(text.length() > 19 && text.charAt(19) == '-')) {
return ISO8601Utils.parse(text + "Z", new ParsePosition(0));
}
return ISO8601Utils.parse(text, new ParsePosition(0)); return ISO8601Utils.parse(text, new ParsePosition(0));
} }
@@ -201,6 +201,31 @@ public class GPXParserTest extends TestBase {
Assert.assertEquals(trackpoint.getVdop(), activityPoint.getLocation().getVdop(), 0.0); Assert.assertEquals(trackpoint.getVdop(), activityPoint.getLocation().getVdop(), 0.0);
} }
@Test
public void shouldParseDatetimeVariants() throws IOException, GpxParseException {
// Validates parsing of timestamps with no timezone (treated as UTC), explicit UTC (Z),
// positive offset, and negative offset.
try (final InputStream inputStream = getClass().getResourceAsStream("/gpx-parser-test-datetime.gpx")) {
final GpxParser gpxParser = new GpxParser(inputStream);
final GpxFile gpxFile = gpxParser.getGpxFile();
// Metadata time: No timezone indicator: should be treated as UTC
Assert.assertEquals(Date.from(Instant.parse("2026-05-23T19:31:00Z")), gpxFile.getTime());
final List<GpxTrackPoint> points = gpxFile.getPoints();
Assert.assertEquals(4, points.size());
// No timezone: treated as UTC
Assert.assertEquals(Date.from(Instant.parse("2026-05-23T19:31:24Z")), points.get(0).getTime());
// Explicit UTC (Z)
Assert.assertEquals(Date.from(Instant.parse("2026-05-23T19:32:00Z")), points.get(1).getTime());
// Positive offset (+02:00): 21:33 local = 19:33 UTC
Assert.assertEquals(Date.from(Instant.parse("2026-05-23T19:33:00Z")), points.get(2).getTime());
// Negative offset (-07:00): 12:34 local = 19:34 UTC
Assert.assertEquals(Date.from(Instant.parse("2026-05-23T19:34:00Z")), points.get(3).getTime());
}
}
// test import of old GPX v1.0 // test import of old GPX v1.0
@Test @Test
public void TestGpxImportOld() throws Exception { public void TestGpxImportOld() throws Exception {
@@ -0,0 +1,35 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<gpx version="1.1" creator="NonConformingDevice"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1"
xmlns="http://www.topografix.com/GPX/1/1">
<metadata>
<name>Test Timezone Variants</name>
<!-- No timezone indicator: should be treated as UTC -->
<time>2026-05-23T19:31:00</time>
</metadata>
<trk>
<trkseg>
<!-- No timezone: treated as UTC -->
<trkpt lon="9.7393798828125" lat="47.5048828125">
<ele>440.0</ele>
<time>2026-05-23T19:31:24</time>
</trkpt>
<!-- Explicit UTC (Z) -->
<trkpt lon="9.7394798828125" lat="47.5058828125">
<ele>441.0</ele>
<time>2026-05-23T19:32:00Z</time>
</trkpt>
<!-- Positive offset (+02:00): 21:33 local = 19:33 UTC -->
<trkpt lon="9.7395798828125" lat="47.5068828125">
<ele>442.0</ele>
<time>2026-05-23T21:33:00+02:00</time>
</trkpt>
<!-- Negative offset (-07:00): 12:34 local = 19:34 UTC -->
<trkpt lon="9.7396798828125" lat="47.5078828125">
<ele>443.0</ele>
<time>2026-05-23T12:34:00-07:00</time>
</trkpt>
</trkseg>
</trk>
</gpx>