GxpParser: extraction more waypoint and trackpoint data (#5465)

This commit is contained in:
Thomas Kuehne
2025-10-12 23:50:37 +02:00
committed by José Rebelo
parent 2f127e1982
commit c8f6b8cfaf
9 changed files with 564 additions and 240 deletions
@@ -1,4 +1,4 @@
/* Copyright (C) 2017-2024 Carsten Pfeiffer, José Rebelo, Petr Vaněk /* Copyright (C) 2017-2025 Carsten Pfeiffer, José Rebelo, Petr Vaněk, Thomas Kuehne
This file is part of Gadgetbridge. This file is part of Gadgetbridge.
@@ -42,12 +42,16 @@ public class GPSCoordinate implements Parcelable {
public static final int GPS_DECIMAL_DEGREES_SCALE = 6; // precise to 111.132mm at equator: https://en.wikipedia.org/wiki/Decimal_degrees public static final int GPS_DECIMAL_DEGREES_SCALE = 6; // precise to 111.132mm at equator: https://en.wikipedia.org/wiki/Decimal_degrees
public GPSCoordinate(double longitude, double latitude, double altitude) { public GPSCoordinate(double longitude, double latitude, double altitude) {
this(longitude, latitude, altitude, UNKNOWN_DOP, UNKNOWN_DOP, UNKNOWN_DOP);
}
public GPSCoordinate(double longitude, double latitude, double altitude, double hdop, double vdop, double pdop) {
this.longitude = longitude; this.longitude = longitude;
this.latitude = latitude; this.latitude = latitude;
this.altitude = altitude; this.altitude = Double.isNaN(altitude) ? UNKNOWN_ALTITUDE : altitude;
this.hdop = UNKNOWN_DOP; this.hdop = Double.isNaN(hdop) ? UNKNOWN_DOP : hdop;
this.vdop = UNKNOWN_DOP; this.vdop = Double.isNaN(vdop) ? UNKNOWN_DOP : vdop;
this.pdop = UNKNOWN_DOP; this.pdop = Double.isNaN(pdop) ? UNKNOWN_DOP : pdop;
} }
public GPSCoordinate(double longitude, double latitude) { public GPSCoordinate(double longitude, double latitude) {
@@ -58,6 +62,9 @@ public class GPSCoordinate implements Parcelable {
latitude = in.readDouble(); latitude = in.readDouble();
longitude = in.readDouble(); longitude = in.readDouble();
altitude = in.readDouble(); altitude = in.readDouble();
hdop = in.readDouble();
vdop = in.readDouble();
pdop = in.readDouble();
} }
public double getLatitude() { public double getLatitude() {
@@ -171,6 +178,9 @@ public class GPSCoordinate implements Parcelable {
dest.writeDouble(latitude); dest.writeDouble(latitude);
dest.writeDouble(longitude); dest.writeDouble(longitude);
dest.writeDouble(altitude); dest.writeDouble(altitude);
dest.writeDouble(hdop);
dest.writeDouble(vdop);
dest.writeDouble(pdop);
} }
@Override @Override
@@ -20,6 +20,7 @@ import androidx.annotation.Nullable;
import com.google.gson.internal.bind.util.ISO8601Utils; import com.google.gson.internal.bind.util.ISO8601Utils;
import org.jetbrains.annotations.TestOnly;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
@@ -27,11 +28,20 @@ import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlPullParserFactory;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.text.ParsePosition; import java.text.ParsePosition;
import java.util.Date; import java.util.Date;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile; import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrack; import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrack;
@@ -85,6 +95,28 @@ public class GpxParser {
return null; return null;
} }
/// simplify the GPX for parsing
@Nullable
@TestOnly
public static byte[] transformGpx(InputStream input){
try {
Source xmlSource = new StreamSource(input);
try (final InputStream xsltStream = GBApplication.getContext().getResources().openRawResource(R.raw.gpx_xslt)) {
Source xsltSource = new StreamSource(xsltStream);
TransformerFactory factory = TransformerFactory.newInstance();
Transformer transformer = factory.newTransformer(xsltSource);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final StreamResult result = new StreamResult(outputStream);
transformer.transform(xmlSource, result);
return outputStream.toByteArray();
}
} catch (Exception e) {
LOG.error("transformGpx ", e);
return null;
}
}
public static boolean isGpxFile(final byte[] data) { public static boolean isGpxFile(final byte[] data) {
for(byte[] header : XML_HEADER){ for(byte[] header : XML_HEADER){
if(ArrayUtils.equals(data, header, 0)){ if(ArrayUtils.equals(data, header, 0)){
@@ -103,8 +135,8 @@ public class GpxParser {
public GpxParser(final InputStream stream) throws GpxParseException { public GpxParser(final InputStream stream) throws GpxParseException {
this.fileBuilder = new GpxFile.Builder(); this.fileBuilder = new GpxFile.Builder();
try { try (InputStream simple = new ByteArrayInputStream(transformGpx(stream))){
parser = createXmlParser(stream); parser = createXmlParser(simple);
parseGpx(); parseGpx();
} catch (final Exception e) { } catch (final Exception e) {
throw new GpxParseException("Failed to parse gpx", e); throw new GpxParseException("Failed to parse gpx", e);
@@ -123,43 +155,84 @@ public class GpxParser {
return parser; return parser;
} }
private void parseGpx() throws Exception { private Date getDate(String attribute, Date defaultDate) throws Exception {
eventType = parser.getEventType(); final String text = parser.getAttributeValue(null, attribute);
if (text == null || text.length() < 1) {
return defaultDate;
}
return ISO8601Utils.parse(text, new ParsePosition(0));
}
while (eventType != XmlPullParser.END_DOCUMENT) { private double getDouble(String attribute, double defaultDouble) {
final String text = parser.getAttributeValue(null, attribute);
if (text == null || text.length() < 1) {
return defaultDouble;
}
return Double.parseDouble(text);
}
private float getFloat(String attribute, float defaultFloat) {
final String text = parser.getAttributeValue(null, attribute);
if (text == null || text.length() < 1) {
return defaultFloat;
}
return Float.parseFloat(text);
}
private int getInt(String attribute, int defaultInt) {
final String text = parser.getAttributeValue(null, attribute);
if (text == null || text.length() < 1) {
return defaultInt;
}
return Integer.parseInt(text);
}
private void parseGpx() throws Exception {
for (eventType = parser.getEventType(); eventType != XmlPullParser.END_DOCUMENT; eventType = parser.next()) {
if (eventType == XmlPullParser.START_TAG) { if (eventType == XmlPullParser.START_TAG) {
switch (parser.getName()) { String name = parser.getName();
switch (name) {
case "gpx":
fileBuilder.withName(parser.getAttributeValue(null, "name"));
fileBuilder.withAuthor(parser.getAttributeValue(null, "author"));
fileBuilder.withTime(getDate("time", null));
break;
case "trk": case "trk":
final GpxTrack track = parseTrack(); final GpxTrack track = parseTrack();
if (!track.isEmpty()) { if (!track.isEmpty()) {
fileBuilder.withTrack(track); fileBuilder.withTrack(track);
} }
continue; break;
case "wpt": case "wpt":
fileBuilder.withWaypoints(parseWaypoint()); final GpxWaypoint waypoint = new GpxWaypoint(
continue; getDouble("lon", 0.0),
case "metadata": getDouble("lat", 0.0),
parseMetadata(); getDouble("ele", Double.NaN),
continue; getDate("time", null),
parser.getAttributeValue(null, "name"),
parser.getAttributeValue(null, "desc"),
parser.getAttributeValue(null, "sym"),
getDouble("hdop", Double.NaN),
getDouble("vdop", Double.NaN),
getDouble("pdop", Double.NaN),
getFloat("temperature", Float.NaN),
getFloat("depth", Float.NaN)
);
fileBuilder.withWaypoints(waypoint);
break;
} }
} }
eventType = parser.next();
} }
} }
private GpxTrack parseTrack() throws Exception { private GpxTrack parseTrack() throws Exception {
final GpxTrack.Builder trackBuilder = new GpxTrack.Builder(); final GpxTrack.Builder trackBuilder = new GpxTrack.Builder();
trackBuilder.withName(parser.getAttributeValue(null, "name"));
trackBuilder.withType(parser.getAttributeValue(null, "type"));
while (eventType != XmlPullParser.END_TAG || !parser.getName().equals("trk")) { while (eventType != XmlPullParser.END_TAG || !parser.getName().equals("trk")) {
if (eventType == XmlPullParser.START_TAG) { if (eventType == XmlPullParser.START_TAG) {
switch (parser.getName()) { switch (parser.getName()) {
case "name":
trackBuilder.withName(parseStringContent("name"));
continue;
case "type":
trackBuilder.withType(parseStringContent("type"));
continue;
case "trkseg": case "trkseg":
final GpxTrackSegment segment = parseTrackSegment(); final GpxTrackSegment segment = parseTrackSegment();
if (!segment.getTrackPoints().isEmpty()) { if (!segment.getTrackPoints().isEmpty()) {
@@ -182,9 +255,25 @@ public class GpxParser {
if (eventType == XmlPullParser.START_TAG) { if (eventType == XmlPullParser.START_TAG) {
switch (parser.getName()) { switch (parser.getName()) {
case "trkpt": case "trkpt":
final GpxTrackPoint trackPoint = parseTrackPoint(); final GpxTrackPoint trackPoint = new GpxTrackPoint(
getDouble("lon", 0.0),
getDouble("lat", 0.0),
getDouble("ele", Double.NaN),
getDate("time", null),
parser.getAttributeValue(null, "name"),
parser.getAttributeValue(null, "desc"),
parser.getAttributeValue(null, "sym"),
getDouble("hdop", Double.NaN),
getDouble("vdop", Double.NaN),
getDouble("pdop", Double.NaN),
getInt("hr", -1),
getFloat("speed", -1),
getInt("cad", -1),
getFloat("temperature", Float.NaN),
getFloat("depth", Float.NaN)
);
segmentBuilder.withTrackPoint(trackPoint); segmentBuilder.withTrackPoint(trackPoint);
continue; break;
} }
} }
@@ -193,129 +282,4 @@ public class GpxParser {
return segmentBuilder.build(); return segmentBuilder.build();
} }
private String parseStringContent(final String tag) throws Exception {
String retString = "";
while (eventType != XmlPullParser.END_TAG || !parser.getName().equals(tag)) {
if (eventType == XmlPullParser.TEXT) {
retString = parser.getText();
}
eventType = parser.next();
}
return retString;
}
private double parseElevation() throws Exception {
return Double.parseDouble(parseStringContent("ele"));
}
private Date parseTime() throws Exception {
return ISO8601Utils.parse(parseStringContent("time"), new ParsePosition(0));
}
private GpxTrackPoint parseTrackPoint() throws Exception {
final GpxTrackPoint.Builder trackPointBuilder = new GpxTrackPoint.Builder();
final String latString = parser.getAttributeValue(null, "lat");
final String lonString = parser.getAttributeValue(null, "lon");
trackPointBuilder.withLatitude(latString != null ? Double.parseDouble(latString) : 0);
trackPointBuilder.withLongitude(lonString != null ? Double.parseDouble(lonString) : 0);
while (eventType != XmlPullParser.END_TAG || !parser.getName().equals("trkpt")) {
if (parser.getEventType() == XmlPullParser.START_TAG) {
switch (parser.getName()) {
case "ele":
trackPointBuilder.withAltitude(parseElevation());
continue;
case "time":
trackPointBuilder.withTime(parseTime());
continue;
case "extensions":
parseExtensions(trackPointBuilder);
continue;
}
}
eventType = parser.next();
}
return trackPointBuilder.build();
}
private void parseExtensions(final GpxTrackPoint.Builder trackPointBuilder) throws Exception {
while (eventType != XmlPullParser.END_TAG || !parser.getName().equals("extensions")) {
if (parser.getEventType() == XmlPullParser.START_TAG) {
switch (parser.getName()) {
case "TrackPointExtension":
parseTrackPointExtensions(trackPointBuilder);
continue;
}
}
eventType = parser.next();
}
}
private void parseTrackPointExtensions(final GpxTrackPoint.Builder trackPointBuilder) throws Exception {
while (eventType != XmlPullParser.END_TAG || !parser.getName().equals("TrackPointExtension")) {
if (parser.getEventType() == XmlPullParser.START_TAG) {
switch (parser.getName()) {
case "cad":
trackPointBuilder.withCadence(Integer.parseInt(parseStringContent("cad")));
continue;
case "hr":
trackPointBuilder.withHeartRate(Integer.parseInt(parseStringContent("hr")));
continue;
case "speed":
trackPointBuilder.withSpeed(Float.parseFloat(parseStringContent("speed")));
continue;
}
}
eventType = parser.next();
}
}
private GpxWaypoint parseWaypoint() throws Exception {
final GpxWaypoint.Builder waypointBuilder = new GpxWaypoint.Builder();
final String latString = parser.getAttributeValue(null, "lat");
final String lonString = parser.getAttributeValue(null, "lon");
waypointBuilder.withLatitude(latString != null ? Double.parseDouble(latString) : 0);
waypointBuilder.withLongitude(lonString != null ? Double.parseDouble(lonString) : 0);
while (eventType != XmlPullParser.END_TAG || !parser.getName().equals("wpt")) {
if (parser.getEventType() == XmlPullParser.START_TAG) {
switch (parser.getName()) {
case "ele":
waypointBuilder.withAltitude(parseElevation());
continue;
case "name":
waypointBuilder.withName(parseStringContent("name"));
continue;
}
}
eventType = parser.next();
}
return waypointBuilder.build();
}
private void parseMetadata() throws Exception {
while (eventType != XmlPullParser.END_TAG || !parser.getName().equals("metadata")) {
if (parser.getEventType() == XmlPullParser.START_TAG) {
switch (parser.getName()) {
case "name":
fileBuilder.withName(parseStringContent("name"));
continue;
case "author":
// TODO parse author
break;
}
}
eventType = parser.next();
}
}
} }
@@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 José Rebelo /* Copyright (C) 2023-2025 José Rebelo, Thomas Kuehne
This file is part of Gadgetbridge. This file is part of Gadgetbridge.
@@ -16,7 +16,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util.gpx.model; package nodomain.freeyourgadget.gadgetbridge.util.gpx.model;
import androidx.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -25,17 +28,26 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
public class GpxFile { public class GpxFile {
private final String name; private final String name;
private final String author; private final String author;
@Nullable
private final Date time;
private final List<GpxTrack> tracks; private final List<GpxTrack> tracks;
private final List<GpxWaypoint> waypoints; private final List<GpxWaypoint> waypoints;
public GpxFile(final String name, final String author, final List<GpxTrack> tracks, final List<GpxWaypoint> waypoints) { public GpxFile(@Nullable final String name, @Nullable final String author, @Nullable final Date time,
final List<GpxTrack> tracks, final List<GpxWaypoint> waypoints) {
this.name = name; this.name = name;
this.author = author; this.author = author;
this.time = time;
this.tracks = tracks; this.tracks = tracks;
this.waypoints = waypoints; this.waypoints = waypoints;
} }
@Nullable
public Date getTime() {
return time;
}
public String getName() { public String getName() {
return name; return name;
} }
@@ -75,10 +87,16 @@ public class GpxFile {
public static class Builder { public static class Builder {
private String name; private String name;
private String author; private String author;
private Date time;
private List<GpxTrack> tracks = new ArrayList<>(); private List<GpxTrack> tracks = new ArrayList<>();
private List<GpxWaypoint> waypoints = new ArrayList<>(); private List<GpxWaypoint> waypoints = new ArrayList<>();
public Builder withTime(final Date date) {
this.time = date;
return this;
}
public Builder withName(final String name) { public Builder withName(final String name) {
this.name = name; this.name = name;
return this; return this;
@@ -100,7 +118,7 @@ public class GpxFile {
} }
public GpxFile build() { public GpxFile build() {
return new GpxFile(name, author, tracks, waypoints); return new GpxFile(name, author, time, tracks, waypoints);
} }
} }
} }
@@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 José Rebelo /* Copyright (C) 2023-2025 José Rebelo, Thomas Kuehne
This file is part of Gadgetbridge. This file is part of Gadgetbridge.
@@ -17,6 +17,7 @@
package nodomain.freeyourgadget.gadgetbridge.util.gpx.model; package nodomain.freeyourgadget.gadgetbridge.util.gpx.model;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Date; import java.util.Date;
import java.util.Objects; import java.util.Objects;
@@ -25,27 +26,54 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate; import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
public class GpxTrackPoint extends GPSCoordinate { public class GpxTrackPoint extends GPSCoordinate {
@Nullable
private final String name;
@Nullable
private final String symbol;
@Nullable
private final String description;
private final Date time; private final Date time;
private final int heartRate; private final int heartRate;
private final float speed; private final float speed;
private final int cadence; private final int cadence;
private final float temperature;
private final float depth;
public GpxTrackPoint(final double longitude, final double latitude, final double altitude, final Date time) { public GpxTrackPoint(final double longitude, final double latitude, final double altitude, final Date time) {
this(longitude, latitude, altitude, time, -1); this(longitude, latitude, altitude, time, -1);
} }
public GpxTrackPoint(final double longitude, final double latitude, final double altitude, final Date time, final int heartRate) { public GpxTrackPoint(final double longitude, final double latitude, final double altitude, final Date time, final int heartRate) {
this(longitude, latitude, altitude, time, heartRate, -1, -1); this(longitude, latitude, altitude, time, null, null, null, Double.NaN, Double.NaN, Double.NaN, heartRate, -1, -1, Float.NaN, Float.NaN);
} }
public GpxTrackPoint(final double longitude, final double latitude, final double altitude, final Date time, final int heartRate, final float speed, final int cadence) { public GpxTrackPoint(final double longitude, final double latitude, final double altitude,
super(longitude, latitude, altitude); final Date time, final String name, final String description,
final String symbol, double hdop, double vdop, double pdop,
final int heartRate, final float speed, final int cadence,
final float temperature, final float depth) {
super(longitude, latitude, altitude, hdop, vdop, pdop);
this.name = name;
this.symbol = symbol;
this.description = description;
this.time = time; this.time = time;
this.heartRate = heartRate; this.heartRate = heartRate;
this.speed = speed; this.speed = speed;
this.cadence = cadence; this.cadence = cadence;
this.temperature = temperature;
this.depth = depth;
} }
@Nullable
public String getName() {return name;}
@Nullable
public String getSymbol() {return symbol;}
@Nullable
public String getDescription() {return description;}
@Nullable
public Date getTime() { public Date getTime() {
return time; return time;
} }
@@ -61,6 +89,12 @@ public class GpxTrackPoint extends GPSCoordinate {
public int getCadence() { public int getCadence() {
return cadence; return cadence;
} }
public float getTemperature() {
return temperature;
}
public float getDepth() {
return depth;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
@@ -70,12 +104,17 @@ public class GpxTrackPoint extends GPSCoordinate {
return Objects.equals(time, that.time) && return Objects.equals(time, that.time) &&
Objects.equals(heartRate, that.heartRate) && Objects.equals(heartRate, that.heartRate) &&
Objects.equals(cadence, that.cadence) && Objects.equals(cadence, that.cadence) &&
Objects.equals(speed, that.speed); Objects.equals(speed, that.speed) &&
Objects.equals(name, that.name) &&
Objects.equals(symbol, that.symbol) &&
Objects.equals(description, that.description) &&
Objects.equals(temperature, that.temperature) &&
Objects.equals(depth, that.depth);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(super.hashCode(), time, heartRate, speed, cadence); return Objects.hash(super.hashCode(), time, heartRate, speed, cadence, name, symbol, description, temperature, depth);
} }
@NonNull @NonNull
@@ -94,53 +133,4 @@ public class GpxTrackPoint extends GPSCoordinate {
return activityPoint; return activityPoint;
} }
public static class Builder {
private double longitude;
private double latitude;
private double altitude;
private Date time;
private int heartRate = -1;
private float speed = -1;
private int cadence = -1;
public Builder withLongitude(final double longitude) {
this.longitude = longitude;
return this;
}
public Builder withLatitude(final double latitude) {
this.latitude = latitude;
return this;
}
public Builder withAltitude(final double altitude) {
this.altitude = altitude;
return this;
}
public Builder withTime(final Date time) {
this.time = time;
return this;
}
public Builder withHeartRate(final int heartRate) {
this.heartRate = heartRate;
return this;
}
public Builder withSpeed(final float speed) {
this.speed = speed;
return this;
}
public Builder withCadence(final int cadence) {
this.cadence = cadence;
return this;
}
public GpxTrackPoint build() {
return new GpxTrackPoint(longitude, latitude, altitude, time, heartRate, speed, cadence);
}
}
} }
@@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 José Rebelo /* Copyright (C) 2023-2025 José Rebelo, Thomas Kuehne
This file is part of Gadgetbridge. This file is part of Gadgetbridge.
@@ -18,15 +18,48 @@ package nodomain.freeyourgadget.gadgetbridge.util.gpx.model;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.util.Date;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate; import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
public class GpxWaypoint extends GPSCoordinate { public class GpxWaypoint extends GPSCoordinate {
private final float depth;
@Nullable
private final String description;
@Nullable @Nullable
private final String name; private final String name;
public GpxWaypoint(final double longitude, final double latitude, final double altitude, @Nullable final String name) { @Nullable
super(longitude, latitude, altitude); private final String symbol;
private final float temperature;
@Nullable
private final Date time;
public GpxWaypoint(final double longitude, final double latitude, final double altitude,
@Nullable final Date time, @Nullable final String name,
@Nullable final String description, @Nullable final String symbol,
final double hdop, final double vdop, final double pdop,
final float temperature, final float depth) {
super(longitude, latitude, altitude, hdop, vdop, pdop);
this.depth = depth;
this.description = description;
this.name = name; this.name = name;
this.symbol = symbol;
this.time = time;
this.temperature = temperature;
}
public float getDepth() {
return depth;
}
@Nullable
public String getDescription() {
return description;
} }
@Nullable @Nullable
@@ -34,34 +67,17 @@ public class GpxWaypoint extends GPSCoordinate {
return name; return name;
} }
public static class Builder { @Nullable
private double longitude; public String getSymbol() {
private double latitude; return symbol;
private double altitude; }
private String name;
public Builder withLongitude(final double longitude) { public float getTemperature() {
this.longitude = longitude; return temperature;
return this; }
}
public Builder withLatitude(final double latitude) { @Nullable
this.latitude = latitude; public Date getTime() {
return this; return time;
}
public Builder withAltitude(final double altitude) {
this.altitude = altitude;
return this;
}
public Builder withName(final String name) {
this.name = name;
return this;
}
public GpxWaypoint build() {
return new GpxWaypoint(longitude, latitude, altitude, name);
}
} }
} }
+109
View File
@@ -0,0 +1,109 @@
<?xml version="1.0"?>
<xsl:stylesheet
xmlns:ax="http://www.garmin.com/xmlschemas/ActivityExtension/v1"
xmlns:g3="http://www.garmin.com/xmlschemas/GpxExtensions/v3"
xmlns:gpx="http://www.topografix.com/GPX/1/1"
xmlns:tx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1"
xmlns:w1="http://www.garmin.com/xmlschemas/WaypointExtension/v1"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="ax g3 gpx tx w1"
version="1.0">
<xsl:output
encoding="UTF-8"
indent="yes"
method="xml"
version="1.0" />
<xsl:strip-space elements="*" />
<xsl:template match="/gpx:gpx">
<gpx>
<xsl:if test="gpx:metadata/gpx:name"><xsl:attribute name="name"><xsl:value-of select="gpx:metadata/gpx:name" /></xsl:attribute></xsl:if>
<xsl:if test="gpx:metadata/gpx:author/gpx:name"><xsl:attribute name="author"><xsl:value-of select="gpx:metadata/gpx:author/gpx:name" /></xsl:attribute></xsl:if>
<xsl:if test="gpx:metadata/gpx:time"><xsl:attribute name="time"><xsl:value-of select="gpx:metadata/gpx:time" /></xsl:attribute></xsl:if>
<xsl:apply-templates select="gpx:wpt" />
<xsl:apply-templates select="gpx:trk" />
</gpx>
</xsl:template>
<xsl:template match="gpx:wpt">
<wpt>
<xsl:call-template name="point" />
<xsl:choose>
<xsl:when test="gpx:extensions/g3:WaypointExtension/g3:Temperature">
<xsl:attribute name="temperature"><xsl:value-of select="gpx:extensions/g3:WaypointExtension/g3:Temperature" /></xsl:attribute>
</xsl:when>
<xsl:when test="gpx:extensions/w1:WaypointExtension/w1:Temperature">
<xsl:attribute name="temperature"><xsl:value-of select="gpx:extensions/w1:WaypointExtension/w1:Temperature" /></xsl:attribute>
</xsl:when>
</xsl:choose>
<xsl:choose>
<xsl:when test="gpx:extensions/g3:WaypointExtension/g3:Depth">
<xsl:attribute name="depth"><xsl:value-of select="gpx:extensions/g3:WaypointExtension/g3:Depth" /></xsl:attribute>
</xsl:when>
<xsl:when test="gpx:extensions/w1:WaypointExtension/w1:Depth">
<xsl:attribute name="depth"><xsl:value-of select="gpx:extensions/w1:WaypointExtension/w1:Depth" /></xsl:attribute>
</xsl:when>
</xsl:choose>
</wpt>
</xsl:template>
<xsl:template match="gpx:trk">
<trk>
<xsl:attribute name="name"><xsl:value-of select="gpx:name" /></xsl:attribute>
<xsl:if test="gpx:type"><xsl:attribute name="type"><xsl:value-of select="gpx:type" /></xsl:attribute></xsl:if>
<xsl:apply-templates select="gpx:trkseg" />
</trk>
</xsl:template>
<xsl:template match="gpx:trkseg">
<trkseg>
<xsl:apply-templates select="gpx:trkpt" />
</trkseg>
</xsl:template>
<xsl:template match="gpx:trkpt">
<trkpt>
<xsl:call-template name="point" />
<xsl:choose>
<xsl:when test="gpx:extensions/tx:TrackPointExtension/tx:atemp">
<xsl:attribute name="temperature"><xsl:value-of select="gpx:extensions/tx:TrackPointExtension/tx:atemp" /></xsl:attribute>
</xsl:when>
<xsl:when test="gpx:extensions/g3:TrackPointExtension/g3:Temperature">
<xsl:attribute name="temperature"><xsl:value-of select="gpx:extensions/g3:TrackPointExtension/g3:Temperature" /></xsl:attribute>
</xsl:when>
</xsl:choose>
<xsl:choose>
<xsl:when test="gpx:extensions/tx:TrackPointExtension/tx:depth">
<xsl:attribute name="depth"><xsl:value-of select="gpx:extensions/tx:TrackPointExtension/tx:depth" /></xsl:attribute>
</xsl:when>
<xsl:when test="gpx:extensions/g3:TrackPointExtension/g3:Depth">
<xsl:attribute name="depth"><xsl:value-of select="gpx:extensions/g3:TrackPointExtension/g3:Depth" /></xsl:attribute>
</xsl:when>
</xsl:choose>
<xsl:if test="gpx:extensions/tx:TrackPointExtension/tx:hr"><xsl:attribute name="hr"><xsl:value-of select="gpx:extensions/tx:TrackPointExtension/tx:hr" /></xsl:attribute></xsl:if>
<xsl:if test="gpx:extensions/tx:TrackPointExtension/tx:cad"><xsl:attribute name="cad"><xsl:value-of select="gpx:extensions/tx:TrackPointExtension/tx:cad" /></xsl:attribute></xsl:if>
<xsl:if test="gpx:extensions/ax:TrackPointExtension/ax:speed"><xsl:attribute name="speed"><xsl:value-of select="gpx:extensions/ax:TrackPointExtension/ax:speed" /></xsl:attribute></xsl:if>
</trkpt>
</xsl:template>
<xsl:template name="point">
<xsl:attribute name="lat"><xsl:value-of select="@lat" /></xsl:attribute>
<xsl:attribute name="lon"><xsl:value-of select="@lon" /></xsl:attribute>
<xsl:if test="gpx:ele"><xsl:attribute name="ele"><xsl:value-of select="gpx:ele" /></xsl:attribute></xsl:if>
<xsl:if test="gpx:time"><xsl:attribute name="time"><xsl:value-of select="gpx:time" /></xsl:attribute></xsl:if>
<xsl:if test="gpx:name"><xsl:attribute name="name"><xsl:value-of select="gpx:name" /></xsl:attribute></xsl:if>
<xsl:if test="gpx:desc"><xsl:attribute name="desc"><xsl:value-of select="gpx:desc" /></xsl:attribute></xsl:if>
<xsl:choose>
<xsl:when test="gpx:desc">
<xsl:attribute name="desc"><xsl:value-of select="gpx:desc" /></xsl:attribute>
</xsl:when>
<xsl:when test="gpx:cmt">
<xsl:attribute name="desc"><xsl:value-of select="gpx:cmt" /></xsl:attribute>
</xsl:when>
</xsl:choose>
<xsl:if test="gpx:sym"><xsl:attribute name="sym"><xsl:value-of select="gpx:sym" /></xsl:attribute></xsl:if>
<xsl:if test="gpx:hdop"><xsl:attribute name="hdop"><xsl:value-of select="gpx:hdop" /></xsl:attribute></xsl:if>
<xsl:if test="gpx:vdop"><xsl:attribute name="vdop"><xsl:value-of select="gpx:vdop" /></xsl:attribute></xsl:if>
<xsl:if test="gpx:pdop"><xsl:attribute name="pdop"><xsl:value-of select="gpx:pdop" /></xsl:attribute></xsl:if>
</xsl:template>
</xsl:stylesheet>
@@ -1,3 +1,19 @@
/* Copyright (C) 2023-2025 José Rebelo, Thomas Kuehne
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util.gpx; package nodomain.freeyourgadget.gadgetbridge.util.gpx;
import org.junit.Assert; import org.junit.Assert;
@@ -7,6 +23,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.ParseException; import java.text.ParseException;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -14,10 +31,14 @@ import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate; import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.test.TestBase; import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile; import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrack;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrackPoint; import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrackPoint;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrackSegment;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxWaypoint;
import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupportTest.readBinaryResource;
public class GPXParserTest extends TestBase { public class GPXParserTest extends TestBase {
@@ -79,4 +100,74 @@ public class GPXParserTest extends TestBase {
Assert.assertEquals(gpxFile.getTracks().get(0).getTrackSegments().get(0).getTrackPoints(), segment1); Assert.assertEquals(gpxFile.getTracks().get(0).getTrackSegments().get(0).getTrackPoints(), segment1);
} }
} }
@Test
public void TestGpxImport() throws Exception {
final byte[] actual;
try (final InputStream gpx = getClass().getResourceAsStream("/TestGpxImport.gpx")) {
actual = GpxParser.transformGpx(gpx);
}
byte[] expected = readBinaryResource("/TestGpxImport.xml");
Assert.assertArrayEquals(expected, actual);
final GpxFile file1;
try (final InputStream gpx = getClass().getResourceAsStream("/TestGpxImport.gpx")) {
file1 = new GpxParser(gpx).getGpxFile();
}
Assert.assertNotNull(file1);
Assert.assertEquals("Gadgetbridge's TestGpxImport.gpx", file1.getName());
Assert.assertEquals("Gadgetbridge test author", file1.getAuthor());
Assert.assertEquals(Date.from(Instant.parse("2025-10-03T18:20:45Z")), file1.getTime());
Assert.assertEquals(3, file1.getWaypoints().size());
Assert.assertEquals(1, file1.getTracks().size());
Assert.assertEquals(4, file1.getPoints().size());
GpxWaypoint wp0 = file1.getWaypoints().get(0);
Assert.assertEquals(9.7393798828125, wp0.getLongitude(), 0.0001);
Assert.assertEquals(47.5048828125, wp0.getLatitude(), 0.0001);
Assert.assertEquals(-470.1, wp0.getAltitude(), 0.0001);
Assert.assertEquals(Date.from(Instant.parse("2025-10-03T14:10:59Z")), wp0.getTime());
Assert.assertEquals("Bregenz", wp0.getName());
Assert.assertEquals("Airport", wp0.getSymbol());
Assert.assertEquals("city in Österreich", wp0.getDescription());
Assert.assertEquals(1.4, wp0.getHdop(), 0.0001);
Assert.assertEquals(3.2, wp0.getVdop(),0.0001);
Assert.assertEquals(1.5, wp0.getPdop(), 0.0001);
Assert.assertEquals(9.1, wp0.getTemperature(), 0.0001);
Assert.assertEquals(6.1, wp0.getDepth(), 0.0001);
Assert.assertEquals("腓特烈港", file1.getWaypoints().get(1).getName());
GpxTrack trk0 = file1.getTracks().get(0);
Assert.assertEquals("Gadgetbridge import test track", trk0.getName());
Assert.assertEquals(1, trk0.getTrackSegments().size());
GpxTrackSegment trkseg0 = trk0.getTrackSegments().get(0);
Assert.assertEquals(4, trkseg0.getTrackPoints().size());
GpxTrackPoint trkpt0 = trkseg0.getTrackPoints().get(0);
Assert.assertEquals(47.5048828125, trkpt0.getLatitude(), 0.0001);
Assert.assertEquals(9.7393798828125, trkpt0.getLongitude(), 0.0001);
Assert.assertEquals(-440.2, trkpt0.getAltitude(), 0.0001);
Assert.assertEquals(Date.from(Instant.parse("2025-10-03T15:10:59Z")), trkpt0.getTime());
Assert.assertEquals("Tp1", trkpt0.getName());
Assert.assertEquals("Summit", trkpt0.getSymbol());
Assert.assertEquals("a description", trkpt0.getDescription());
Assert.assertEquals(1.4, trkpt0.getHdop(), 0.0001);
Assert.assertEquals(3.2, trkpt0.getVdop(), 0.0001);
Assert.assertEquals(1.5, trkpt0.getPdop(), 0.0001);
Assert.assertEquals(123, trkpt0.getHeartRate());
Assert.assertEquals(22, trkpt0.getCadence());
Assert.assertEquals(23.3f, trkpt0.getSpeed(), 0.0001f);
Assert.assertEquals(28.3f, trkpt0.getTemperature(), 0.0001f);
Assert.assertEquals(26.3f, trkpt0.getDepth(), 0.0001f);
byte[] raw = readBinaryResource("/TestGpxImport.gpx");
GpxFile file2 = GpxParser.parseGpx(raw);
Assert.assertNotNull(file2);
Assert.assertEquals(3, file2.getWaypoints().size());
Assert.assertEquals(4, file2.getPoints().size());
Assert.assertEquals(1, file2.getTracks().size());
Assert.assertEquals(trkpt0, file2.getTracks().get(0).getTrackSegments().get(0).getTrackPoints().get(0));
}
} }
+113
View File
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1"
creator="Garmin Desktop App"
xmlns:ax="http://www.garmin.com/xmlschemas/ActivityExtension/v1"
xmlns:ge="http://www.garmin.com/xmlschemas/GpxExtensions/v3"
xmlns:tpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1"
xmlns:wpx="http://www.garmin.com/xmlschemas/WaypointExtension/v1"
>
<metadata>
<name>Gadgetbridge's TestGpxImport.gpx</name>
<author>
<name>Gadgetbridge test author</name>
</author>
<time>2025-10-03T18:20:45Z</time>
</metadata>
<wpt lat="47.5048828125" lon="9.7393798828125">
<ele>-470.1</ele>
<time>2025-10-03T14:10:59Z</time>
<name>Bregenz</name>
<desc>city in Österreich</desc>
<sym>Airport</sym>
<hdop>1.4</hdop>
<vdop>3.2</vdop>
<pdop>1.5</pdop>
<extensions>
<wpx:WaypointExtension>
<wpx:Temperature>9.1</wpx:Temperature>
<wpx:Depth>6.1</wpx:Depth>
</wpx:WaypointExtension>
</extensions>
</wpt>
<wpt lat="47.65594482421875" lon="9.47296142578125">
<ele>8850.1</ele>
<time>2025-10-03T14:20:59Z</time>
<name>腓特烈港</name>
<cmt>another comment</cmt>
<sym>Anchor</sym>
<extensions>
<ge:WaypointExtension>
<ge:Temperature>19.2</ge:Temperature>
<ge:Depth>16.2</ge:Depth>
</ge:WaypointExtension>
</extensions>
</wpt>
<wpt lat="47.66693115234375" lon="9.1680908203125">
<name>קרויצלינגן</name>
</wpt>
<rte>
<name>Test Route</name>
<rtept lat="47.5048828125" lon="9.7393798828125">
<time>2025-10-03T14:16:06Z</time>
<name>Bregenz</name>
<sym>Triangle, Red</sym>
</rtept>
<rtept lat="47.65594482421875" lon="9.47296142578125">
<time>2025-10-03T14:16:09Z</time>
<name>Friedrichshafen</name>
<sym>City (Medium)</sym>
</rtept>
<rtept lat="47.66693115234375" lon="9.1680908203125">
<time>2025-10-03T14:16:17Z</time>
<name>Konstanz</name>
<sym>Square, Blue</sym>
</rtept>
</rte>
<trk>
<name>Gadgetbridge import test track</name>
<trkseg>
<trkpt lat="47.5048828125" lon="9.7393798828125">
<ele>-440.2</ele>
<time>2025-10-03T15:10:59Z</time>
<name>Tp1</name>
<desc>a description</desc>
<sym>Summit</sym>
<hdop>1.4</hdop>
<vdop>3.2</vdop>
<pdop>1.5</pdop>
<extensions>
<tpx:TrackPointExtension>
<tpx:atemp>28.3</tpx:atemp>
<tpx:depth>26.3</tpx:depth>
<tpx:hr>123</tpx:hr>
<tpx:cad>22</tpx:cad>
</tpx:TrackPointExtension>
<ax:TrackPointExtension>
<ax:speed>23.3</ax:speed>
</ax:TrackPointExtension>
</extensions>
</trkpt>
<trkpt lat="47.60899250395596" lon="9.55604980699718">
<ele>8849.2</ele>
<time>2025-10-03T15:20:59Z</time>
<name>Tp2</name>
<cmt>a comment</cmt>
<sym>Water</sym>
<extensions>
<ge:TrackPointExtension>
<ge:Temperature>34.3</ge:Temperature>
<ge:Depth>33.3</ge:Depth>
</ge:TrackPointExtension>
</extensions>
</trkpt>
<trkpt lat="47.65594482421875" lon="9.47296142578125" />
<trkpt lat="-43" lon="-19" />
</trkseg>
</trk>
</gpx>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?><gpx name="Gadgetbridge's TestGpxImport.gpx" author="Gadgetbridge test author" time="2025-10-03T18:20:45Z">
<wpt lat="47.5048828125" lon="9.7393798828125" ele="-470.1" time="2025-10-03T14:10:59Z" name="Bregenz" desc="city in Österreich" sym="Airport" hdop="1.4" vdop="3.2" pdop="1.5" temperature="9.1" depth="6.1"/>
<wpt lat="47.65594482421875" lon="9.47296142578125" ele="8850.1" time="2025-10-03T14:20:59Z" name="腓特烈港" desc="another comment" sym="Anchor" temperature="19.2" depth="16.2"/>
<wpt lat="47.66693115234375" lon="9.1680908203125" name="קרויצלינגן"/>
<trk name="Gadgetbridge import test track">
<trkseg>
<trkpt lat="47.5048828125" lon="9.7393798828125" ele="-440.2" time="2025-10-03T15:10:59Z" name="Tp1" desc="a description" sym="Summit" hdop="1.4" vdop="3.2" pdop="1.5" temperature="28.3" depth="26.3" hr="123" cad="22" speed="23.3"/>
<trkpt lat="47.60899250395596" lon="9.55604980699718" ele="8849.2" time="2025-10-03T15:20:59Z" name="Tp2" desc="a comment" sym="Water" temperature="34.3" depth="33.3"/>
<trkpt lat="47.65594482421875" lon="9.47296142578125"/>
<trkpt lat="-43" lon="-19"/>
</trkseg>
</trk>
</gpx>