From 82fe859c846b2dc96b8980990c8d71482bc2413f Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Sun, 8 May 2022 14:01:53 +0200 Subject: [PATCH 1/2] KML: Export KML2.3 - KML2.2 and KML2.3 can be imported. Fixes #339. --- .../file/exporter/KmlTrackExporterTest.java | 6 +- .../io/file/exporter/KMLTrackExporter.java | 41 +++++----- .../io/file/importer/KmlTrackImporter.java | 79 +++++++++++-------- 3 files changed, 74 insertions(+), 52 deletions(-) diff --git a/src/androidTest/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackExporterTest.java b/src/androidTest/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackExporterTest.java index e8c8000e9..5e5d152a1 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackExporterTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackExporterTest.java @@ -28,14 +28,14 @@ public class KmlTrackExporterTest { @Test public void writeCloseSegment_only_write_sensordata_if_present() { String expected = "1970-01-01T00:00:00Z\n" + - "\n" + + "\n" + "1970-01-01T01:00:00+01:00\n" + - "\n" + + "\n" + "\n" + "\n" + "\n" + "\n" + - "\n"; + "\n"; // given TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)); diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java index 304f52b4e..9ee6bfd0b 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KMLTrackExporter.java @@ -44,7 +44,8 @@ import de.dennisguse.opentracks.ui.markers.MarkerUtils; import de.dennisguse.opentracks.util.StringUtils; /** - * Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to KML. + * Convert {@link Track} incl. {@link Marker} and {@link TrackPoint} to KML version 2.3. + * https://docs.opengeospatial.org/is/12-007r2/12-007r2.html * * @author Sandor Dornbush * @author Rodrigo Damazio @@ -58,6 +59,8 @@ public class KMLTrackExporter implements TrackExporter { private static final String TRACK_STYLE = "track"; private static final String SCHEMA_ID = "schema"; + public static final String EXTENDED_DATA_TYPE_CATEGORY = "type"; + public static final String EXTENDED_DATA_TYPE_SPEED = "speed"; public static final String EXTENDED_DATA_TYPE_DISTANCE = "distance"; public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence"; @@ -224,11 +227,13 @@ public class KMLTrackExporter implements TrackExporter { private void writeHeader(Track[] tracks) { if (printWriter != null) { printWriter.println(""); - printWriter.println(""); - //TODO ADD xsi:schemaLocation here! + //TODO ADD xsi:schemaLocation for atom + printWriter.println("xsi:schemaLocation=" + + "\"http://www.opengis.net/kml/2.3 http://schemas.opengis.net/kml/2.3/ogckml23.xsd" + + " http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd\">"); printWriter.println(""); printWriter.println("1"); @@ -309,16 +314,16 @@ public class KMLTrackExporter implements TrackExporter { printWriter.println("#" + TRACK_STYLE + ""); writeCategory(track.getCategory()); - printWriter.println(""); + printWriter.println(""); printWriter.println("absolute"); - printWriter.println("1"); + printWriter.println("1"); } } private void writeEndTrack() { if (printWriter != null) { - printWriter.println(""); + printWriter.println(""); printWriter.println(""); } } @@ -326,7 +331,7 @@ public class KMLTrackExporter implements TrackExporter { @VisibleForTesting void writeOpenSegment() { if (printWriter != null) { - printWriter.println(""); + printWriter.println(""); speedList.clear(); distanceList.clear(); powerList.clear(); @@ -373,7 +378,7 @@ public class KMLTrackExporter implements TrackExporter { } printWriter.println(""); printWriter.println(""); - printWriter.println(""); + printWriter.println(""); } } @@ -383,9 +388,9 @@ public class KMLTrackExporter implements TrackExporter { printWriter.println("" + getTime(zoneOffset, trackPoint.getLocation()) + ""); if (trackPoint.hasLocation()) { - printWriter.println("" + getCoordinates(trackPoint.getLocation(), " ") + ""); + printWriter.println("" + getCoordinates(trackPoint.getLocation(), " ") + ""); } else { - printWriter.println(""); + printWriter.println(""); } speedList.add(trackPoint.hasSpeed() ? (float) trackPoint.getSpeed().toMPS() : null); @@ -408,16 +413,16 @@ public class KMLTrackExporter implements TrackExporter { * @param name the name of the simple array data */ private void writeSimpleArrayData(List list, String name) { - printWriter.println(""); + printWriter.println(""); for (int i = 0; i < list.size(); i++) { Float value = list.get(i); if (value == null) { - printWriter.println(""); + printWriter.println(""); } else { - printWriter.println("" + SENSOR_DATA_FORMAT.format(value) + ""); + printWriter.println("" + SENSOR_DATA_FORMAT.format(value) + ""); } } - printWriter.println(""); + printWriter.println(""); } /** @@ -521,7 +526,7 @@ public class KMLTrackExporter implements TrackExporter { return; } printWriter.println(""); - printWriter.println("" + StringUtils.formatCData(category) + ""); + printWriter.println("" + StringUtils.formatCData(category) + ""); printWriter.println(""); } @@ -554,8 +559,8 @@ public class KMLTrackExporter implements TrackExporter { * @param extendedDataType the extended data display name */ private void writeSimpleArrayStyle(String name, String extendedDataType) { - printWriter.println(""); + printWriter.println(""); printWriter.println("" + StringUtils.formatCData(extendedDataType) + ""); - printWriter.println(""); + printWriter.println(""); } } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlTrackImporter.java index 0def1c475..19b6be926 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlTrackImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlTrackImporter.java @@ -41,7 +41,7 @@ import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter; import de.dennisguse.opentracks.util.StringUtils; /** - * Imports a KML file. + * Imports a KML file; preferred version: KML2.3, but also supports KML2.2. * * @author Jimmy Shih */ @@ -54,18 +54,31 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac private static final String TAG_COORDINATES = "coordinates"; private static final String TAG_DESCRIPTION = "description"; private static final String TAG_ICON = "icon"; - private static final String TAG_GX_COORD = "gx:coord"; - private static final String TAG_GX_MULTI_TRACK = "gx:MultiTrack"; - private static final String TAG_GX_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData"; - private static final String TAG_GX_TRACK = "gx:Track"; - private static final String TAG_GX_VALUE = "gx:value"; + + private static final String TAG_COORD = "coord"; + private static final String TAG_KML22_COORD = "gx:coord"; + + private static final String TAG_MULTI_TRACK = "MultiTrack"; + private static final String TAG_KML22_MULTI_TRACK = "gx:MultiTrack"; + + private static final String TAG_DATA_CATEGORY = "Data"; //used for Track.category + + private static final String TAG_SIMPLE_ARRAY_DATA = "SimpleArrayData"; + private static final String TAG_KML22_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData"; + + private static final String TAG_TRACK = "Track"; + private static final String TAG_KML22_TRACK = "gx:Track"; + + private static final String TAG_VALUE = "value"; + private static final String TAG_KML22_VALUE = "gx:value"; + private static final String TAG_HREF = "href"; private static final String TAG_KML = "kml"; private static final String TAG_NAME = "name"; private static final String TAG_PHOTO_OVERLAY = "PhotoOverlay"; private static final String TAG_PLACEMARK = "Placemark"; private static final String TAG_STYLE_URL = "styleUrl"; - private static final String TAG_VALUE = "value"; + // private static final String TAG_VALUE = "value"; TODO private static final String TAG_WHEN = "when"; private static final String TAG_UUID = "opentracks:trackid"; @@ -81,7 +94,7 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac private final ArrayList whenList = new ArrayList<>(); private final ArrayList locationList = new ArrayList<>(); - private String extendedDataType; + private String dataType; private final ArrayList sensorSpeedList = new ArrayList<>(); private final ArrayList sensorDistanceList = new ArrayList<>(); private final ArrayList sensorCadenceList = new ArrayList<>(); @@ -128,17 +141,21 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac // Note that a track is contained in a Placemark, calling onMarkerStart will clear various track variables like name, category, and description. onMarkerStart(); break; - case TAG_GX_MULTI_TRACK: + case TAG_MULTI_TRACK: + case TAG_KML22_MULTI_TRACK: trackImporter.newTrack(); break; - case TAG_GX_TRACK: + case TAG_TRACK: + case TAG_KML22_TRACK: if (trackImporter == null) { - throw new SAXException("No " + TAG_GX_MULTI_TRACK); + throw new SAXException("Missing " + TAG_MULTI_TRACK); } onTrackSegmentStart(); break; - case TAG_GX_SIMPLE_ARRAY_DATA: - onExtendedDataStart(attributes); + case TAG_DATA_CATEGORY: + case TAG_SIMPLE_ARRAY_DATA: + case TAG_KML22_SIMPLE_ARRAY_DATA: + dataType = attributes.getValue(ATTRIBUTE_NAME); break; } } @@ -162,18 +179,28 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac case TAG_COORDINATES: onMarkerLocationEnd(); break; - case TAG_GX_MULTI_TRACK: + case TAG_MULTI_TRACK: + case TAG_KML22_MULTI_TRACK: trackImporter.setTrack(context, name, uuid, description, category, icon, zoneOffset); zoneOffset = null; break; - case TAG_GX_TRACK: + case TAG_TRACK: + case TAG_KML22_TRACK: onTrackSegmentEnd(); break; - case TAG_GX_COORD: + case TAG_COORD: + case TAG_KML22_COORD: onCoordEnded(); break; - case TAG_GX_VALUE: - onExtendedDataValueEnd(); + case TAG_VALUE: + case TAG_KML22_VALUE: + if (KMLTrackExporter.EXTENDED_DATA_TYPE_CATEGORY.equals(dataType)) { + if (content != null) { + category = content.trim(); + } + } else { + onExtendedDataValueEnd(); + } break; case TAG_NAME: if (content != null) { @@ -195,11 +222,6 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac icon = content.trim(); } break; - case TAG_VALUE: - if (content != null) { - category = content.trim(); - } - break; case TAG_WHEN: if (content != null) { try { @@ -401,11 +423,6 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac return location; } - - private void onExtendedDataStart(Attributes attributes) { - extendedDataType = attributes.getValue(ATTRIBUTE_NAME); - } - private void onExtendedDataValueEnd() throws SAXException { Float value = null; if (content != null) { @@ -414,11 +431,11 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac try { value = Float.parseFloat(content); } catch (NumberFormatException e) { - throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e); + throw new SAXException(createErrorMessage("Unable to parse value:" + content), e); } } } - switch (extendedDataType) { + switch (dataType) { case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED: sensorSpeedList.add(value); break; @@ -447,7 +464,7 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac accuracyVertical.add(value); break; default: - Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported."); + Log.w(TAG, "Data from extended data " + dataType + " is not (yet) supported."); } } From 6538daedef3b05f87bd645b4ff3ae91a90a0dcd7 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Tue, 13 Sep 2022 23:41:48 +0200 Subject: [PATCH 2/2] README: add export format compatibility matrix. Part of #339. --- README.md | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 80dc0a7d9..1fb893166 100644 --- a/README.md +++ b/README.md @@ -64,19 +64,19 @@ _OpenTracks_ is a sport tracking application that completely respects your priva * __Voice announcements__ * __Photos and Markers:__ mark interesting locations while tracking * __Export:__ - * export tracks either as KMZ (incl. photos), KML, or GPX - * export automatically after each recording (e.g., to sync via [Nextcloud](https://nextcloud.com/)) - * avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`) + * export tracks either as [KMZ 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) (incl. photos), [KML 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html), or [GPX 1.1](https://www.topografix.com/GPX/1/1/) + * export automatically after each recording (e.g., to sync via [Nextcloud](https://nextcloud.com/)) + * avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`) * __Altitude:__ - * gain/loss via barometric sensor (if present) - * shown in EGM2008 (above mean sea level); exported as WGS84 + * gain/loss via barometric sensor (if present) + * shown in EGM2008 (above mean sea level); exported as WGS84 * __Bluetooth LE sensors:__ - * heart rate - * cycling: speed and distance - * cycling: cadence - * cycling: power meter - * running: speed and cadence - * support for BLE sensor training only (i.e., without GPS) for indoor training + * heart rate + * cycling: speed and distance + * cycling: cadence + * cycling: power meter + * running: speed and cadence + * support for BLE sensor training only (i.e., without GPS) for indoor training An overview of tested sensors: [README_TESTED_SENSORS.md](README_TESTED_SENSORS.md) @@ -121,7 +121,14 @@ For testing via adb: `adb shell am start -e someParameter someValue -n "package/ The Public API is disabled by default to protect the user's privacy, but it can easily be enabled in the settings. -__IMPORTANT__: triggering `StartRecording` does not check if Android permissions (location or Bluetooth) were granted. If they are not granted, nothing will be recorded. +## File formats compatibility with open-source software + +| | [GPX 1.1](https://www.topografix.com/GPX/1/1/) | [KML 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) | [KMZ 2.3](https://docs.opengeospatial.org/is/12-007r2/12-007r2.html) | +|------------------------------------------------------|------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------| +| [OpenLayers 7.1.0](https://openlayers.org/) | ? | [no](https://github.com/openlayers/openlayers/issues/14104) | [no](https://github.com/openlayers/openlayers/issues/14104) | +| [Golden Cheetah 3.5](https://www.goldencheetah.org/) | ? | [no](https://github.com/GoldenCheetah/GoldenCheetah/issues/4271) | [no](https://github.com/GoldenCheetah/GoldenCheetah/issues/4271) | +| [GpxPod](https://apps.nextcloud.com/apps/gpxpod) | ? | ? | ? | +| [OsmAnd](https://github.com/osmandapp/OsmAnd) | ? | [no](https://github.com/osmandapp/OsmAnd/issues/15271) | [no](https://github.com/osmandapp/OsmAnd/issues/15271) | ## Dashboard API (incl. map)