Upgrade to KML2.3
This commit is contained in:
Dennis Guse
2022-09-13 23:51:10 +02:00
committed by GitHub
4 changed files with 93 additions and 64 deletions
+19 -12
View File
@@ -64,19 +64,19 @@ _OpenTracks_ is a sport tracking application that completely respects your priva
* __Voice announcements__ * __Voice announcements__
* __Photos and Markers:__ mark interesting locations while tracking * __Photos and Markers:__ mark interesting locations while tracking
* __Export:__ * __Export:__
* export tracks either as KMZ (incl. photos), KML, or GPX * 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/)) * 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`) * avoid duplication: each exported file contain a random unique identifier (i.e., `opentracks:trackid`)
* __Altitude:__ * __Altitude:__
* gain/loss via barometric sensor (if present) * gain/loss via barometric sensor (if present)
* shown in EGM2008 (above mean sea level); exported as WGS84 * shown in EGM2008 (above mean sea level); exported as WGS84
* __Bluetooth LE sensors:__ * __Bluetooth LE sensors:__
* heart rate * heart rate
* cycling: speed and distance * cycling: speed and distance
* cycling: cadence * cycling: cadence
* cycling: power meter * cycling: power meter
* running: speed and cadence * running: speed and cadence
* support for BLE sensor training only (i.e., without GPS) for indoor training * 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) 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. 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) ## Dashboard API (incl. map)
@@ -28,14 +28,14 @@ public class KmlTrackExporterTest {
@Test @Test
public void writeCloseSegment_only_write_sensordata_if_present() { public void writeCloseSegment_only_write_sensordata_if_present() {
String expected = "<when>1970-01-01T00:00:00Z</when>\n" + String expected = "<when>1970-01-01T00:00:00Z</when>\n" +
"<gx:coord/>\n" + "<coord/>\n" +
"<when>1970-01-01T01:00:00+01:00</when>\n" + "<when>1970-01-01T01:00:00+01:00</when>\n" +
"<gx:coord/>\n" + "<coord/>\n" +
"<ExtendedData>\n" + "<ExtendedData>\n" +
"<SchemaData schemaUrl=\"#schema\">\n" + "<SchemaData schemaUrl=\"#schema\">\n" +
"</SchemaData>\n" + "</SchemaData>\n" +
"</ExtendedData>\n" + "</ExtendedData>\n" +
"</gx:Track>\n"; "</Track>\n";
// given // given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0)); TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0));
@@ -44,7 +44,8 @@ import de.dennisguse.opentracks.ui.markers.MarkerUtils;
import de.dennisguse.opentracks.util.StringUtils; 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 Sandor Dornbush
* @author Rodrigo Damazio * @author Rodrigo Damazio
@@ -58,6 +59,8 @@ public class KMLTrackExporter implements TrackExporter {
private static final String TRACK_STYLE = "track"; private static final String TRACK_STYLE = "track";
private static final String SCHEMA_ID = "schema"; 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_SPEED = "speed";
public static final String EXTENDED_DATA_TYPE_DISTANCE = "distance"; public static final String EXTENDED_DATA_TYPE_DISTANCE = "distance";
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence"; public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
@@ -224,11 +227,13 @@ public class KMLTrackExporter implements TrackExporter {
private void writeHeader(Track[] tracks) { private void writeHeader(Track[] tracks) {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.2\""); printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.3\"");
printWriter.println("xmlns:gx=\"http://www.google.com/kml/ext/2.2\"");
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\""); printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\">"); printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\">");
//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("<Document>"); printWriter.println("<Document>");
printWriter.println("<open>1</open>"); printWriter.println("<open>1</open>");
@@ -309,16 +314,16 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>"); printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
writeCategory(track.getCategory()); writeCategory(track.getCategory());
printWriter.println("<gx:MultiTrack>"); printWriter.println("<MultiTrack>");
printWriter.println("<altitudeMode>absolute</altitudeMode>"); printWriter.println("<altitudeMode>absolute</altitudeMode>");
printWriter.println("<gx:interpolate>1</gx:interpolate>"); printWriter.println("<interpolate>1</interpolate>");
} }
} }
private void writeEndTrack() { private void writeEndTrack() {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("</gx:MultiTrack>"); printWriter.println("</MultiTrack>");
printWriter.println("</Placemark>"); printWriter.println("</Placemark>");
} }
} }
@@ -326,7 +331,7 @@ public class KMLTrackExporter implements TrackExporter {
@VisibleForTesting @VisibleForTesting
void writeOpenSegment() { void writeOpenSegment() {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("<gx:Track>"); printWriter.println("<Track>");
speedList.clear(); speedList.clear();
distanceList.clear(); distanceList.clear();
powerList.clear(); powerList.clear();
@@ -373,7 +378,7 @@ public class KMLTrackExporter implements TrackExporter {
} }
printWriter.println("</SchemaData>"); printWriter.println("</SchemaData>");
printWriter.println("</ExtendedData>"); printWriter.println("</ExtendedData>");
printWriter.println("</gx:Track>"); printWriter.println("</Track>");
} }
} }
@@ -383,9 +388,9 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getLocation()) + "</when>"); printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getLocation()) + "</when>");
if (trackPoint.hasLocation()) { if (trackPoint.hasLocation()) {
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>"); printWriter.println("<coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</coord>");
} else { } else {
printWriter.println("<gx:coord/>"); printWriter.println("<coord/>");
} }
speedList.add(trackPoint.hasSpeed() ? (float) trackPoint.getSpeed().toMPS() : null); 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 * @param name the name of the simple array data
*/ */
private void writeSimpleArrayData(List<Float> list, String name) { private void writeSimpleArrayData(List<Float> list, String name) {
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">"); printWriter.println("<SimpleArrayData name=\"" + name + "\">");
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
Float value = list.get(i); Float value = list.get(i);
if (value == null) { if (value == null) {
printWriter.println("<gx:value />"); printWriter.println("<value />");
} else { } else {
printWriter.println("<gx:value>" + SENSOR_DATA_FORMAT.format(value) + "</gx:value>"); printWriter.println("<value>" + SENSOR_DATA_FORMAT.format(value) + "</value>");
} }
} }
printWriter.println("</gx:SimpleArrayData>"); printWriter.println("</SimpleArrayData>");
} }
/** /**
@@ -521,7 +526,7 @@ public class KMLTrackExporter implements TrackExporter {
return; return;
} }
printWriter.println("<ExtendedData>"); printWriter.println("<ExtendedData>");
printWriter.println("<Data name=\"type\"><value>" + StringUtils.formatCData(category) + "</value></Data>"); printWriter.println("<Data name=\"" + EXTENDED_DATA_TYPE_CATEGORY + "\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
printWriter.println("</ExtendedData>"); printWriter.println("</ExtendedData>");
} }
@@ -554,8 +559,8 @@ public class KMLTrackExporter implements TrackExporter {
* @param extendedDataType the extended data display name * @param extendedDataType the extended data display name
*/ */
private void writeSimpleArrayStyle(String name, String extendedDataType) { private void writeSimpleArrayStyle(String name, String extendedDataType) {
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"float\">"); printWriter.println("<SimpleArrayField name=\"" + name + "\" type=\"float\">");
printWriter.println("<displayName>" + StringUtils.formatCData(extendedDataType) + "</displayName>"); printWriter.println("<displayName>" + StringUtils.formatCData(extendedDataType) + "</displayName>");
printWriter.println("</gx:SimpleArrayField>"); printWriter.println("</SimpleArrayField>");
} }
} }
@@ -41,7 +41,7 @@ import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
import de.dennisguse.opentracks.util.StringUtils; 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 * @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_COORDINATES = "coordinates";
private static final String TAG_DESCRIPTION = "description"; private static final String TAG_DESCRIPTION = "description";
private static final String TAG_ICON = "icon"; 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_COORD = "coord";
private static final String TAG_GX_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData"; private static final String TAG_KML22_COORD = "gx:coord";
private static final String TAG_GX_TRACK = "gx:Track";
private static final String TAG_GX_VALUE = "gx:value"; 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_HREF = "href";
private static final String TAG_KML = "kml"; private static final String TAG_KML = "kml";
private static final String TAG_NAME = "name"; private static final String TAG_NAME = "name";
private static final String TAG_PHOTO_OVERLAY = "PhotoOverlay"; private static final String TAG_PHOTO_OVERLAY = "PhotoOverlay";
private static final String TAG_PLACEMARK = "Placemark"; private static final String TAG_PLACEMARK = "Placemark";
private static final String TAG_STYLE_URL = "styleUrl"; 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_WHEN = "when";
private static final String TAG_UUID = "opentracks:trackid"; private static final String TAG_UUID = "opentracks:trackid";
@@ -81,7 +94,7 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
private final ArrayList<Instant> whenList = new ArrayList<>(); private final ArrayList<Instant> whenList = new ArrayList<>();
private final ArrayList<Location> locationList = new ArrayList<>(); private final ArrayList<Location> locationList = new ArrayList<>();
private String extendedDataType; private String dataType;
private final ArrayList<Float> sensorSpeedList = new ArrayList<>(); private final ArrayList<Float> sensorSpeedList = new ArrayList<>();
private final ArrayList<Float> sensorDistanceList = new ArrayList<>(); private final ArrayList<Float> sensorDistanceList = new ArrayList<>();
private final ArrayList<Float> sensorCadenceList = new ArrayList<>(); private final ArrayList<Float> 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. // Note that a track is contained in a Placemark, calling onMarkerStart will clear various track variables like name, category, and description.
onMarkerStart(); onMarkerStart();
break; break;
case TAG_GX_MULTI_TRACK: case TAG_MULTI_TRACK:
case TAG_KML22_MULTI_TRACK:
trackImporter.newTrack(); trackImporter.newTrack();
break; break;
case TAG_GX_TRACK: case TAG_TRACK:
case TAG_KML22_TRACK:
if (trackImporter == null) { if (trackImporter == null) {
throw new SAXException("No " + TAG_GX_MULTI_TRACK); throw new SAXException("Missing " + TAG_MULTI_TRACK);
} }
onTrackSegmentStart(); onTrackSegmentStart();
break; break;
case TAG_GX_SIMPLE_ARRAY_DATA: case TAG_DATA_CATEGORY:
onExtendedDataStart(attributes); case TAG_SIMPLE_ARRAY_DATA:
case TAG_KML22_SIMPLE_ARRAY_DATA:
dataType = attributes.getValue(ATTRIBUTE_NAME);
break; break;
} }
} }
@@ -162,18 +179,28 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
case TAG_COORDINATES: case TAG_COORDINATES:
onMarkerLocationEnd(); onMarkerLocationEnd();
break; break;
case TAG_GX_MULTI_TRACK: case TAG_MULTI_TRACK:
case TAG_KML22_MULTI_TRACK:
trackImporter.setTrack(context, name, uuid, description, category, icon, zoneOffset); trackImporter.setTrack(context, name, uuid, description, category, icon, zoneOffset);
zoneOffset = null; zoneOffset = null;
break; break;
case TAG_GX_TRACK: case TAG_TRACK:
case TAG_KML22_TRACK:
onTrackSegmentEnd(); onTrackSegmentEnd();
break; break;
case TAG_GX_COORD: case TAG_COORD:
case TAG_KML22_COORD:
onCoordEnded(); onCoordEnded();
break; break;
case TAG_GX_VALUE: case TAG_VALUE:
onExtendedDataValueEnd(); case TAG_KML22_VALUE:
if (KMLTrackExporter.EXTENDED_DATA_TYPE_CATEGORY.equals(dataType)) {
if (content != null) {
category = content.trim();
}
} else {
onExtendedDataValueEnd();
}
break; break;
case TAG_NAME: case TAG_NAME:
if (content != null) { if (content != null) {
@@ -195,11 +222,6 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
icon = content.trim(); icon = content.trim();
} }
break; break;
case TAG_VALUE:
if (content != null) {
category = content.trim();
}
break;
case TAG_WHEN: case TAG_WHEN:
if (content != null) { if (content != null) {
try { try {
@@ -401,11 +423,6 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
return location; return location;
} }
private void onExtendedDataStart(Attributes attributes) {
extendedDataType = attributes.getValue(ATTRIBUTE_NAME);
}
private void onExtendedDataValueEnd() throws SAXException { private void onExtendedDataValueEnd() throws SAXException {
Float value = null; Float value = null;
if (content != null) { if (content != null) {
@@ -414,11 +431,11 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
try { try {
value = Float.parseFloat(content); value = Float.parseFloat(content);
} catch (NumberFormatException e) { } 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: case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED:
sensorSpeedList.add(value); sensorSpeedList.add(value);
break; break;
@@ -447,7 +464,7 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
accuracyVertical.add(value); accuracyVertical.add(value);
break; break;
default: 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.");
} }
} }