From ccafda7f00d7bdb96f1f6a8323980ebc0c1121c4 Mon Sep 17 00:00:00 2001 From: Nick Spacek Date: Sat, 16 Mar 2019 11:12:35 -0300 Subject: [PATCH 1/2] Adds default namespace declaration and fixes attribute use of namespace. --- .../gadgetbridge/export/GPXExporter.java | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java index 31159a022..24389f0f7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java @@ -40,9 +40,8 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; public class GPXExporter implements ActivityTrackExporter { - private static final String NS_DEFAULT = ""; - private static final String NS_DEFAULT_URI = "http://www.topografix.com/GPX/1/1"; - private static final String NS_DEFAULT_PREFIX = ""; + private static final String NS_GPX_URI = "http://www.topografix.com/GPX/1/1"; + private static final String NS_GPX_PREFIX = ""; private static final String NS_TRACKPOINT_EXTENSION = "gpxtpx"; private static final String NS_TRACKPOINT_EXTENSION_URI = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1"; private static final String NS_XSI_URI = "http://www.w3.org/2001/XMLSchema-instance"; @@ -66,33 +65,33 @@ public class GPXExporter implements ActivityTrackExporter { ser.startDocument(encoding, Boolean.TRUE); ser.setPrefix("xsi", NS_XSI_URI); ser.setPrefix(NS_TRACKPOINT_EXTENSION, NS_TRACKPOINT_EXTENSION_URI); - ser.setPrefix(NS_DEFAULT_PREFIX, NS_DEFAULT); + ser.setPrefix(NS_GPX_PREFIX, NS_GPX_URI); - ser.startTag(NS_DEFAULT, "gpx"); - ser.attribute(NS_DEFAULT, "version", "1.1"); - ser.attribute(NS_DEFAULT, "creator", getCreator()); - ser.attribute(NS_XSI_URI, "schemaLocation", NS_DEFAULT_URI + " " + "http://www.topografix.com/GPX/1/1/gpx.xsd"); + ser.startTag(NS_GPX_URI, "gpx"); + ser.attribute(null,"version", "1.1"); + ser.attribute(null, "creator", getCreator()); + ser.attribute(NS_XSI_URI, "schemaLocation", NS_GPX_URI + " " + "http://www.topografix.com/GPX/1/1/gpx.xsd"); exportMetadata(ser, track); exportTrack(ser, track); - ser.endTag(NS_DEFAULT, "gpx"); + ser.endTag(NS_GPX_URI, "gpx"); ser.endDocument(); ser.flush(); } } private void exportMetadata(XmlSerializer ser, ActivityTrack track) throws IOException { - ser.startTag(NS_DEFAULT, "metadata"); - ser.startTag(NS_DEFAULT, "name").text(track.getName()).endTag(NS_DEFAULT, "name"); + ser.startTag(NS_GPX_URI, "metadata"); + ser.startTag(NS_GPX_URI, "name").text(track.getName()).endTag(NS_GPX_URI, "name"); - ser.startTag(NS_DEFAULT, "author"); - ser.startTag(NS_DEFAULT, "name").text(track.getUser().getName()).endTag(NS_DEFAULT, "name"); - ser.endTag(NS_DEFAULT, "author"); + ser.startTag(NS_GPX_URI, "author"); + ser.startTag(NS_GPX_URI, "name").text(track.getUser().getName()).endTag(NS_GPX_URI, "name"); + ser.endTag(NS_GPX_URI, "author"); - ser.startTag(NS_DEFAULT, "time").text(formatTime(new Date())).endTag(NS_DEFAULT, "time"); + ser.startTag(NS_GPX_URI, "time").text(formatTime(new Date())).endTag(NS_GPX_URI, "time"); - ser.endTag(NS_DEFAULT, "metadata"); + ser.endTag(NS_GPX_URI, "metadata"); } private String formatTime(Date date) { @@ -100,8 +99,8 @@ public class GPXExporter implements ActivityTrackExporter { } private void exportTrack(XmlSerializer ser, ActivityTrack track) throws IOException, GPXTrackEmptyException { - ser.startTag(NS_DEFAULT, "trk"); - ser.startTag(NS_DEFAULT, "trkseg"); + ser.startTag(NS_GPX_URI, "trk"); + ser.startTag(NS_GPX_URI, "trkseg"); List trackPoints = track.getTrackPoints(); String source = getSource(track); @@ -114,8 +113,8 @@ public class GPXExporter implements ActivityTrackExporter { throw new GPXTrackEmptyException(); } - ser.endTag(NS_DEFAULT, "trkseg"); - ser.endTag(NS_DEFAULT, "trk"); + ser.endTag(NS_GPX_URI, "trkseg"); + ser.endTag(NS_GPX_URI, "trk"); } private String getSource(ActivityTrack track) { @@ -127,20 +126,21 @@ public class GPXExporter implements ActivityTrackExporter { if (location == null) { return false; // skip invalid points, that just contain hr data, for example } - ser.startTag(NS_DEFAULT, "trkpt"); - ser.attribute(NS_DEFAULT, "lon", formatLocation(location.getLongitude())); - ser.attribute(NS_DEFAULT, "lat", formatLocation(location.getLatitude())); - ser.startTag(NS_DEFAULT, "ele").text(formatLocation(location.getAltitude())).endTag(NS_DEFAULT, "ele"); - ser.startTag(NS_DEFAULT, "time").text(DateTimeUtils.formatIso8601UTC(point.getTime())).endTag(NS_DEFAULT, "time"); + ser.startTag(NS_GPX_URI, "trkpt"); + // lon and lat attributes do not have an explicit namespace + ser.attribute(null, "lon", formatLocation(location.getLongitude())); + ser.attribute(null, "lat", formatLocation(location.getLatitude())); + ser.startTag(NS_GPX_URI, "ele").text(formatLocation(location.getAltitude())).endTag(NS_GPX_URI, "ele"); + ser.startTag(NS_GPX_URI, "time").text(DateTimeUtils.formatIso8601UTC(point.getTime())).endTag(NS_GPX_URI, "time"); String description = point.getDescription(); if (description != null) { - ser.startTag(NS_DEFAULT, "desc").text(description).endTag(NS_DEFAULT, "desc"); + ser.startTag(NS_GPX_URI, "desc").text(description).endTag(NS_GPX_URI, "desc"); } - //ser.startTag(NS_DEFAULT, "src").text(source).endTag(NS_DEFAULT, "src"); + //ser.startTag(NS_GPX_URI, "src").text(source).endTag(NS_GPX_URI, "src"); exportTrackpointExtensions(ser, point, trackPoints); - ser.endTag(NS_DEFAULT, "trkpt"); + ser.endTag(NS_GPX_URI, "trkpt"); return true; } @@ -167,12 +167,12 @@ public class GPXExporter implements ActivityTrackExporter { } } - ser.startTag(NS_DEFAULT, "extensions"); + ser.startTag(NS_GPX_URI, "extensions"); ser.setPrefix(NS_TRACKPOINT_EXTENSION, NS_TRACKPOINT_EXTENSION_URI); ser.startTag(NS_TRACKPOINT_EXTENSION_URI, "TrackPointExtension"); ser.startTag(NS_TRACKPOINT_EXTENSION_URI, "hr").text(String.valueOf(hr)).endTag(NS_TRACKPOINT_EXTENSION_URI, "hr"); ser.endTag(NS_TRACKPOINT_EXTENSION_URI, "TrackPointExtension"); - ser.endTag(NS_DEFAULT, "extensions"); + ser.endTag(NS_GPX_URI, "extensions"); } private @Nullable ActivityPoint findClosestSensibleActivityPoint(Date time, List trackPoints) { From 6854807f91cfdc63520c405c274deb3a7279bbe6 Mon Sep 17 00:00:00 2001 From: Nick Spacek Date: Sat, 16 Mar 2019 11:13:27 -0300 Subject: [PATCH 2/2] Adds GPX schema XSD, sample data, and tests which validate output. --- .../gadgetbridge/export/GPXExporterTest.java | 124 +++ .../GPXExporterTest-SampleTracks.csv | 14 + .../GPXExporterTest-SampleTracksHR.csv | 14 + app/src/test/resources/gpx.xsd | 784 ++++++++++++++++++ 4 files changed, 936 insertions(+) create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporterTest.java create mode 100644 app/src/test/resources/GPXExporterTest-SampleTracks.csv create mode 100644 app/src/test/resources/GPXExporterTest-SampleTracksHR.csv create mode 100644 app/src/test/resources/gpx.xsd diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporterTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporterTest.java new file mode 100644 index 000000000..7c61c2387 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporterTest.java @@ -0,0 +1,124 @@ +package nodomain.freeyourgadget.gadgetbridge.export; + +import com.google.gson.internal.bind.util.ISO8601Utils; + +import org.junit.Test; +import org.xml.sax.SAXException; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.xml.XMLConstants; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.User; +import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter.GPXTrackEmptyException; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack; +import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate; +import nodomain.freeyourgadget.gadgetbridge.test.TestBase; + +public class GPXExporterTest extends TestBase { + @Test + public void shouldCreateValidGpxFromSimulatedData() throws IOException, ParseException, GPXTrackEmptyException, SAXException { + final List points = readActivityPoints("/GPXExporterTest-SampleTracks.csv"); + + final GPXExporter gpxExporter = new GPXExporter(); + gpxExporter.setCreator("Gadgetbridge Test"); + final ActivityTrack track = createTestTrack(points); + + final File tempFile = File.createTempFile("gpx-exporter-test-track", ".gpx"); + tempFile.deleteOnExit(); + + gpxExporter.performExport(track, tempFile); + validateGpxFile(tempFile); + } + + @Test + public void shouldCreateValidGpxFromSimulatedDataWithHeartrate() throws IOException, ParseException, GPXTrackEmptyException, ParserConfigurationException, SAXException { + final List points = readActivityPoints("/GPXExporterTest-SampleTracksHR.csv"); + + final GPXExporter gpxExporter = new GPXExporter(); + gpxExporter.setCreator("Gadgetbridge Test"); + final ActivityTrack track = createTestTrack(points); + + final File tempFile = File.createTempFile("gpx-exporter-test-track", ".gpx"); + tempFile.deleteOnExit(); + + gpxExporter.performExport(track, tempFile); + validateGpxFile(tempFile); + } + + private ActivityTrack createTestTrack(List points) { + final User user = new User(); + user.setName("Test User"); + + Device device = new Device(); + device.setName("Test Device"); + + final ActivityTrack track = new ActivityTrack(); + track.setName("Test Track"); + track.setBaseTime(new Date()); + track.setUser(user); + track.setDevice(device); + + for (final ActivityPoint point : points) { + track.addTrackPoint(point); + } + return track; + } + + private List readActivityPoints(String resourcePath) throws IOException, ParseException { + final List points = new ArrayList<>(); + try (final InputStream inputStream = getClass().getResourceAsStream(resourcePath)) { + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + String nextLine = reader.readLine(); + while (nextLine != null) { + final String[] pieces = nextLine.split("\\s+"); + final ActivityPoint point = new ActivityPoint(); + point.setLocation(new GPSCoordinate( + Double.parseDouble(pieces[0]), + Double.parseDouble(pieces[1]), + Double.parseDouble(pieces[2])) + ); + + final int dateIndex; + if (pieces.length == 5) { + point.setHeartRate(Integer.parseInt(pieces[3])); + dateIndex = 4; + } else { + dateIndex = 3; + } + // Not sure about this parser but seemed safe to use + point.setTime(ISO8601Utils.parse(pieces[dateIndex], new ParsePosition(0))); + + points.add(point); + nextLine = reader.readLine(); + } + } + } + return points; + } + + private void validateGpxFile(File tempFile) throws SAXException, IOException { + final Source xmlFile = new StreamSource(tempFile); + final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + final Schema schema = schemaFactory.newSchema(new StreamSource(getClass().getResourceAsStream("/gpx.xsd"))); + final Validator validator = schema.newValidator(); + validator.validate(xmlFile); + } +} \ No newline at end of file diff --git a/app/src/test/resources/GPXExporterTest-SampleTracks.csv b/app/src/test/resources/GPXExporterTest-SampleTracks.csv new file mode 100644 index 000000000..c66cd7098 --- /dev/null +++ b/app/src/test/resources/GPXExporterTest-SampleTracks.csv @@ -0,0 +1,14 @@ +-68.200293 44.152462 40 2019-01-01T00:00:00Z +-68.20027 44.15246 40 2019-01-01T00:00:01Z +-68.20026 44.152462 40 2019-01-01T00:00:02Z +-68.200242 44.152493 40 2019-01-01T00:00:03Z +-68.200237 44.152528 40 2019-01-01T00:00:04Z +-68.200232 44.152567 40 2019-01-01T00:00:05Z +-68.200248 44.152612 40 2019-01-01T00:00:06Z +-68.200253 44.152657 40 2019-01-01T00:00:07Z +-68.200245 44.152675 46 2019-01-01T00:00:08Z +-68.200232 44.152695 46 2019-01-01T00:00:09Z +-68.200215 44.15272 46 2019-01-01T00:00:10Z +-68.200205 44.152753 46 2019-01-01T00:00:11Z +-68.200197 44.152808 46 2019-01-01T00:00:12Z +-68.200203 44.152877 46 2019-01-01T00:00:13Z \ No newline at end of file diff --git a/app/src/test/resources/GPXExporterTest-SampleTracksHR.csv b/app/src/test/resources/GPXExporterTest-SampleTracksHR.csv new file mode 100644 index 000000000..47d55eae1 --- /dev/null +++ b/app/src/test/resources/GPXExporterTest-SampleTracksHR.csv @@ -0,0 +1,14 @@ +-68.73127 43.101062 40 91 2019-01-01T00:00:00Z +-68.73126 43.101093 40 91 2019-01-01T00:00:01Z +-68.731242 43.101128 40 91 2019-01-01T00:00:02Z +-68.731237 43.101167 40 91 2019-01-01T00:00:03Z +-68.731232 43.101212 40 91 2019-01-01T00:00:04Z +-68.731248 43.101257 40 92 2019-01-01T00:00:05Z +-68.731253 43.101275 40 92 2019-01-01T00:00:06Z +-68.731245 43.101295 46 93 2019-01-01T00:00:07Z +-68.731232 43.10132 46 94 2019-01-01T00:00:08Z +-68.731215 43.101353 46 95 2019-01-01T00:00:09Z +-68.731205 43.101408 46 95 2019-01-01T00:00:10Z +-68.731197 43.101477 46 95 2019-01-01T00:00:11Z +-68.731203 43.10152 46 96 2019-01-01T00:00:12Z +-68.73121 43.101565 45 96 2019-01-01T00:00:13Z diff --git a/app/src/test/resources/gpx.xsd b/app/src/test/resources/gpx.xsd new file mode 100644 index 000000000..6ffd92d7b --- /dev/null +++ b/app/src/test/resources/gpx.xsd @@ -0,0 +1,784 @@ + + + + + + GPX schema version 1.1 - For more information on GPX and this schema, visit http://www.topografix.com/gpx.asp + + GPX uses the following conventions: all coordinates are relative to the WGS84 datum. All measurements are in metric units. + + + + + + + GPX is the root element in the XML file. + + + + + + + + GPX documents contain a metadata header, followed by waypoints, routes, and tracks. You can add your own elements + to the extensions section of the GPX document. + + + + + + + Metadata about the file. + + + + + + + A list of waypoints. + + + + + + + A list of routes. + + + + + + + A list of tracks. + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + You must include the version number in your GPX document. + + + + + + + You must include the name or URL of the software that created your GPX document. This allows others to + inform the creator of a GPX instance document that fails to validate. + + + + + + + + + Information about the GPX file, author, and copyright restrictions goes in the metadata section. Providing rich, + meaningful information about your GPX files allows others to search for and use your GPS data. + + + + + + + The name of the GPX file. + + + + + + + A description of the contents of the GPX file. + + + + + + + The person or organization who created the GPX file. + + + + + + + Copyright and license information governing use of the file. + + + + + + + URLs associated with the location described in the file. + + + + + + + The creation date of the file. + + + + + + + Keywords associated with the file. Search engines or databases can use this information to classify the data. + + + + + + + Minimum and maximum coordinates which describe the extent of the coordinates in the file. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + + wpt represents a waypoint, point of interest, or named feature on a map. + + + + + + + + Elevation (in meters) of the point. + + + + + + + Creation/modification timestamp for element. Date and time in are in Univeral Coordinated Time (UTC), not local time! Conforms to ISO 8601 specification for date/time representation. Fractional seconds are allowed for millisecond timing in tracklogs. + + + + + + + Magnetic variation (in degrees) at the point + + + + + + + Height (in meters) of geoid (mean sea level) above WGS84 earth ellipsoid. As defined in NMEA GGA message. + + + + + + + + + The GPS name of the waypoint. This field will be transferred to and from the GPS. GPX does not place restrictions on the length of this field or the characters contained in it. It is up to the receiving application to validate the field before sending it to the GPS. + + + + + + + GPS waypoint comment. Sent to GPS as comment. + + + + + + + A text description of the element. Holds additional information about the element intended for the user, not the GPS. + + + + + + + Source of data. Included to give user some idea of reliability and accuracy of data. "Garmin eTrex", "USGS quad Boston North", e.g. + + + + + + + Link to additional information about the waypoint. + + + + + + + Text of GPS symbol name. For interchange with other programs, use the exact spelling of the symbol as displayed on the GPS. If the GPS abbreviates words, spell them out. + + + + + + + Type (classification) of the waypoint. + + + + + + + + + Type of GPX fix. + + + + + + + Number of satellites used to calculate the GPX fix. + + + + + + + Horizontal dilution of precision. + + + + + + + Vertical dilution of precision. + + + + + + + Position dilution of precision. + + + + + + + Number of seconds since last DGPS update. + + + + + + + ID of DGPS station used in differential correction. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + The latitude of the point. This is always in decimal degrees, and always in WGS84 datum. + + + + + + + The longitude of the point. This is always in decimal degrees, and always in WGS84 datum. + + + + + + + + + rte represents route - an ordered list of waypoints representing a series of turn points leading to a destination. + + + + + + + GPS name of route. + + + + + + + GPS comment for route. + + + + + + + Text description of route for user. Not sent to GPS. + + + + + + + Source of data. Included to give user some idea of reliability and accuracy of data. + + + + + + + Links to external information about the route. + + + + + + + GPS route number. + + + + + + + Type (classification) of route. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + A list of route points. + + + + + + + + + + trk represents a track - an ordered list of points describing a path. + + + + + + + GPS name of track. + + + + + + + GPS comment for track. + + + + + + + User description of track. + + + + + + + Source of data. Included to give user some idea of reliability and accuracy of data. + + + + + + + Links to external information about track. + + + + + + + GPS track number. + + + + + + + Type (classification) of track. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data. + + + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + + A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data. + + + + + + + A Track Point holds the coordinates, elevation, timestamp, and metadata for a single point in a track. + + + + + + + + You can add extend GPX by adding your own elements from another schema here. + + + + + + + + + + Information about the copyright holder and any license governing use of this file. By linking to an appropriate license, + you may place your data into the public domain or grant additional usage rights. + + + + + + + Year of copyright. + + + + + + + Link to external file containing license text. + + + + + + + + Copyright holder (TopoSoft, Inc.) + + + + + + + + + A link to an external resource (Web page, digital photo, video clip, etc) with additional information. + + + + + + + Text of hyperlink. + + + + + + + Mime type of content (image/jpeg) + + + + + + + + URL of hyperlink. + + + + + + + + + An email address. Broken into two parts (id and domain) to help prevent email harvesting. + + + + + + id half of email address (billgates2004) + + + + + + + domain half of email address (hotmail.com) + + + + + + + + + A person or organization. + + + + + + + Name of person or organization. + + + + + + + Email address. + + + + + + + Link to Web site or other external information about person. + + + + + + + + + + A geographic point with optional elevation and time. Available for use by other schemas. + + + + + + + The elevation (in meters) of the point. + + + + + + + The time that the point was recorded. + + + + + + + + The latitude of the point. Decimal degrees, WGS84 datum. + + + + + + + The latitude of the point. Decimal degrees, WGS84 datum. + + + + + + + + + An ordered sequence of points. (for polygons or polylines, e.g.) + + + + + + + Ordered list of geographic points. + + + + + + + + + + Two lat/lon pairs defining the extent of an element. + + + + + + The minimum latitude. + + + + + + + The minimum longitude. + + + + + + + The maximum latitude. + + + + + + + The maximum longitude. + + + + + + + + + + The latitude of the point. Decimal degrees, WGS84 datum. + + + + + + + + + + + + The longitude of the point. Decimal degrees, WGS84 datum. + + + + + + + + + + + + Used for bearing, heading, course. Units are decimal degrees, true (not magnetic). + + + + + + + + + + + + Type of GPS fix. none means GPS had no fix. To signify "the fix info is unknown, leave out fixType entirely. pps = military signal used + + + + + + + + + + + + + + + Represents a differential GPS station. + + + + + + + + + \ No newline at end of file