KML: Export KML2.3 - KML2.2 and KML2.3 can be imported.

Fixes #339.
This commit is contained in:
Dennis Guse
2022-05-08 14:01:53 +02:00
parent f567b03be5
commit 82fe859c84
3 changed files with 74 additions and 52 deletions
@@ -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.");
} }
} }