From 23c3a4d65114c9c3fc8610203b4e9c85ccd63d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rom=C3=A1n=20Mart=C3=ADnez?= Date: Tue, 30 Jun 2020 14:39:44 +0200 Subject: [PATCH] Import/Export speed in GPX and KML files. (#306) Import/Export speed in GPX and KML files. Fixes #300. --- .../opentracks/content/data/TestDataUtil.java | 1 + .../io/file/importer/ExportImportTest.java | 14 ++---- .../io/file/exporter/GpxTrackWriter.java | 1 + .../io/file/exporter/KmlTrackWriter.java | 48 ++++++++++++------- .../importer/AbstractFileTrackImporter.java | 42 +++++++++++----- .../file/importer/GpxFileTrackImporter.java | 7 +++ .../file/importer/KmlFileTrackImporter.java | 34 ++++++++----- src/main/res/values/strings.xml | 1 + 8 files changed, 95 insertions(+), 53 deletions(-) diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java b/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java index c98547df5..6d828abd0 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java +++ b/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java @@ -65,6 +65,7 @@ public class TestDataUtil { trackPoint.setAccuracy((float) i / 100.0f); trackPoint.setAltitude(i * ALTITUDE_INTERVAL); trackPoint.setTime(i + 1); + trackPoint.setSpeed(5f + (i / 10)); trackPoint.setHeartRate_bpm(100f + i); trackPoint.setCyclingCadence_rpm(200f + i); diff --git a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java index cf6945386..cf3bb591f 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java @@ -36,7 +36,6 @@ import static org.junit.Assert.assertNotNull; * Export a track to {@link TrackFileFormat} and verify that the import is identical. *

* TODO: test ignores {@link TrackStatistics} for now. - * TODO: enable verify speed. */ @RunWith(JUnit4.class) public class ExportImportTest { @@ -130,7 +129,7 @@ public class ExportImportTest { assertWaypoints(); // 3. trackpoints - assertTrackpoints(false, false, false, false); + assertTrackpoints(false, false, false); } @LargeTest @@ -165,7 +164,7 @@ public class ExportImportTest { assertWaypoints(); // 3. trackpoints - assertTrackpoints(false, true, true, true); + assertTrackpoints(true, true, true); } @LargeTest @@ -230,8 +229,7 @@ public class ExportImportTest { assertWaypoints(); // 3. trackpoints - //TODO Verify speed - assertTrackpoints(false, false, false, false); + assertTrackpoints(false, false, false); } private void assertWaypoints() { @@ -253,7 +251,7 @@ public class ExportImportTest { } } - private void assertTrackpoints(boolean verifySpeed, boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence) { + private void assertTrackpoints(boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence) { List importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId); assertEquals(trackPoints.size(), importedTrackPoints.size()); @@ -269,9 +267,7 @@ public class ExportImportTest { assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001); assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001); assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001); - if (verifySpeed) { - assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.01); - } + assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001); if (verifyHeartrate) { assertEquals(trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01); } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/GpxTrackWriter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/GpxTrackWriter.java index a1d0672fd..b5e84d4d5 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/GpxTrackWriter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/GpxTrackWriter.java @@ -176,6 +176,7 @@ public class GpxTrackWriter implements TrackWriter { } printWriter.println( ""); + printWriter.println("" + trackPoint.getSpeed() + ""); printWriter.println(""); } } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java index 03704f62b..883fd2e29 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java @@ -46,9 +46,10 @@ public class KmlTrackWriter implements TrackWriter { private static final String TRACK_STYLE = "track"; private static final String SCHEMA_ID = "schema"; - public static final String SENSOR_TYPE_CADENCE = "cadence"; - public static final String SENSOR_TYPE_HEART_RATE = "heart_rate"; - public static final String SENSOR_TYPE_POWER = "power"; + public static final String EXTENDED_DATA_TYPE_SPEED = "speed"; + public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence"; + public static final String EXTENDED_DATA_TYPE_HEART_RATE = "heart_rate"; + public static final String EXTENDED_DATA_TYPE_POWER = "power"; private static final String WAYPOINT_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png"; private static final String START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png"; @@ -64,6 +65,7 @@ public class KmlTrackWriter implements TrackWriter { private final ContentProviderUtils contentProviderUtils; private PrintWriter printWriter; + private final List speedList = new ArrayList<>(); private final List powerList = new ArrayList<>(); private final List cadenceList = new ArrayList<>(); private final List heartRateList = new ArrayList<>(); @@ -123,10 +125,12 @@ public class KmlTrackWriter implements TrackWriter { writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2); printWriter.println(""); + writeSimpleArrayStyle(EXTENDED_DATA_TYPE_SPEED, context.getString(R.string.description_speed_ms)); + if (exportSensorData) { - writeSensorStyle(SENSOR_TYPE_POWER, context.getString(R.string.description_sensor_power)); - writeSensorStyle(SENSOR_TYPE_CADENCE, context.getString(R.string.description_sensor_cadence)); - writeSensorStyle(SENSOR_TYPE_HEART_RATE, context.getString(R.string.description_sensor_heart_rate)); + writeSimpleArrayStyle(EXTENDED_DATA_TYPE_POWER, context.getString(R.string.description_sensor_power)); + writeSimpleArrayStyle(EXTENDED_DATA_TYPE_CADENCE, context.getString(R.string.description_sensor_cadence)); + writeSimpleArrayStyle(EXTENDED_DATA_TYPE_HEART_RATE, context.getString(R.string.description_sensor_heart_rate)); } printWriter.println(""); } @@ -229,6 +233,7 @@ public class KmlTrackWriter implements TrackWriter { public void writeOpenSegment() { if (printWriter != null) { printWriter.println(""); + speedList.clear(); powerList.clear(); cadenceList.clear(); heartRateList.clear(); @@ -240,15 +245,18 @@ public class KmlTrackWriter implements TrackWriter { if (printWriter != null) { printWriter.println(""); printWriter.println(""); + if (speedList.size() > 0) { + writeSimpleArrayData(speedList, EXTENDED_DATA_TYPE_SPEED); + } if (exportSensorData) { if (powerList.size() > 0) { - writeSensorData(powerList, SENSOR_TYPE_POWER); + writeSimpleArrayData(powerList, EXTENDED_DATA_TYPE_POWER); } if (cadenceList.size() > 0) { - writeSensorData(cadenceList, SENSOR_TYPE_CADENCE); + writeSimpleArrayData(cadenceList, EXTENDED_DATA_TYPE_CADENCE); } if (heartRateList.size() > 0) { - writeSensorData(heartRateList, SENSOR_TYPE_HEART_RATE); + writeSimpleArrayData(heartRateList, EXTENDED_DATA_TYPE_HEART_RATE); } } printWriter.println(""); @@ -266,6 +274,10 @@ public class KmlTrackWriter implements TrackWriter { printWriter.println("" + getCoordinates(trackPoint.getLocation(), " ") + ""); + if (trackPoint.hasSpeed()) { + speedList.add(trackPoint.getSpeed()); + } + if (exportSensorData) { if (trackPoint.hasHeartRate()) { heartRateList.add(trackPoint.getHeartRate_bpm()); @@ -281,12 +293,12 @@ public class KmlTrackWriter implements TrackWriter { } /** - * Writes the sensor data. + * Writes the simple array data. * - * @param list a list of sensor data - * @param name the name of the sensor data + * @param list a list of simple array data + * @param name the name of the simple array data */ - private void writeSensorData(List list, String name) { + private void writeSimpleArrayData(List list, String name) { printWriter.println(""); for (int i = 0; i < list.size(); i++) { printWriter.println("" + list.get(i) + ""); @@ -446,14 +458,14 @@ public class KmlTrackWriter implements TrackWriter { } /** - * Writes a sensor style. + * Writes a simple array style. * - * @param name the name of the sesnor - * @param sensorType the sensor display name + * @param name the name of the simple array. + * @param extendedDataType the extended data display name */ - private void writeSensorStyle(String name, String sensorType) { + private void writeSimpleArrayStyle(String name, String extendedDataType) { printWriter.println(""); - printWriter.println("" + StringUtils.formatCData(sensorType) + ""); + printWriter.println("" + StringUtils.formatCData(extendedDataType) + ""); printWriter.println(""); } } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java index 30e7efbd5..e07905d3c 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java @@ -80,6 +80,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track protected String longitude; protected String altitude; protected String time; + protected String speed; protected String waypointType; protected String photoUrl; @@ -319,20 +320,22 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track // Calculate derived attributes from the previous point if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() != 0) { - long timeDifference = trackPoint.getTime() - trackData.lastLocationInCurrentSegment.getTime(); + if (!trackPoint.hasSpeed()) { + long timeDifference = trackPoint.getTime() - trackData.lastLocationInCurrentSegment.getTime(); - // Check for negative time change - if (timeDifference <= 0) { - Log.w(TAG, "Time difference not positive."); - } else { + // Check for negative time change + if (timeDifference <= 0) { + Log.w(TAG, "Time difference not positive."); + } else { - /* - * We don't have a speed and bearing in GPX, make something up from the last two points. - * GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off. - */ - double duration = timeDifference * UnitConversions.MS_TO_S; - double speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / duration; - trackPoint.setSpeed((float) speed); + /* + * We don't have a speed and bearing in GPX, make something up from the last two points. + * GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off. + */ + double duration = timeDifference * UnitConversions.MS_TO_S; + double speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / duration; + trackPoint.setSpeed((float) speed); + } } trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint)); } @@ -427,7 +430,20 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e); } } - return new TrackPoint(latitudeValue, longitudeValue, altitudeValue, timeValue); + + TrackPoint trackPoint = new TrackPoint(latitudeValue, longitudeValue, altitudeValue, timeValue); + + float speedValue; + if (speed != null) { + try { + speedValue = Float.valueOf(speed); + trackPoint.setSpeed(speedValue); + } catch (Exception e) { + throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e); + } + } + + return trackPoint; } /** diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/GpxFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/GpxFileTrackImporter.java index 5497e11f3..897d445a7 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/GpxFileTrackImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/GpxFileTrackImporter.java @@ -39,6 +39,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter { private static final String TAG_GPX = "gpx"; private static final String TAG_NAME = "name"; private static final String TAG_TIME = "time"; + private static final String TAG_SPEED = "speed"; private static final String TAG_TRACK = "trk"; private static final String TAG_TRACK_POINT = "trkpt"; private static final String TAG_TRACK_SEGMENT = "trkseg"; @@ -115,6 +116,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter { time = content.trim(); } break; + case TAG_SPEED: + if (content != null) { + speed = content.trim(); + } + break; case TAG_ELEVATION: if (content != null) { altitude = content.trim(); @@ -149,6 +155,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter { longitude = attributes.getValue(ATTRIBUTE_LON); altitude = null; time = null; + speed = null; } /** diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java index 32234b74b..13e9b68ad 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java @@ -61,8 +61,9 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { private static final String ATTRIBUTE_NAME = "name"; private boolean trackStarted = false; - private String sensorType; + private String extendedDataType; private ArrayList trackPoints; + private ArrayList speedList; private ArrayList cadenceList; private ArrayList heartRateList; private ArrayList powerList; @@ -102,7 +103,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { onTrackSegmentStart(); break; case TAG_GX_SIMPLE_ARRAY_DATA: - onSensorDataStart(attributes); + onExtendedDataStart(attributes); break; } } @@ -124,7 +125,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { } else if (tag.equals(TAG_GX_COORD)) { onTrackPointEnd(); } else if (tag.equals(TAG_GX_VALUE)) { - onSensorValueEnd(); + onExtendedDataValueEnd(); } else if (tag.equals(TAG_NAME)) { if (content != null) { name = content.trim(); @@ -209,6 +210,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { protected void onTrackSegmentStart() { super.onTrackSegmentStart(); trackPoints = new ArrayList<>(); + speedList = new ArrayList<>(); heartRateList = new ArrayList<>(); cadenceList = new ArrayList<>(); powerList = new ArrayList<>(); @@ -222,6 +224,9 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { for (int i = 0; i < trackPoints.size(); i++) { TrackPoint trackPoint = trackPoints.get(i); + if (i < speedList.size()) { + trackPoint.setSpeed(speedList.get(i)); + } if (i < heartRateList.size()) { trackPoint.setHeartRate_bpm(heartRateList.get(i)); } @@ -261,16 +266,16 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { } /** - * On sensor data start. gx:SimpleArrayData start tag. + * On extended data start. gx:SimpleArrayData start tag. */ - private void onSensorDataStart(Attributes attributes) { - sensorType = attributes.getValue(ATTRIBUTE_NAME); + private void onExtendedDataStart(Attributes attributes) { + extendedDataType = attributes.getValue(ATTRIBUTE_NAME); } /** - * On sensor value end. gx:value end tag. + * On extended data value end. gx:value end tag. */ - private void onSensorValueEnd() throws SAXException { + private void onExtendedDataValueEnd() throws SAXException { if (content == null) { return; } @@ -284,18 +289,21 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { } catch (NumberFormatException e) { throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e); } - switch (sensorType) { - case KmlTrackWriter.SENSOR_TYPE_POWER: + switch (extendedDataType) { + case KmlTrackWriter.EXTENDED_DATA_TYPE_SPEED: + speedList.add(value); + break; + case KmlTrackWriter.EXTENDED_DATA_TYPE_POWER: powerList.add(value); break; - case KmlTrackWriter.SENSOR_TYPE_HEART_RATE: + case KmlTrackWriter.EXTENDED_DATA_TYPE_HEART_RATE: heartRateList.add(value); break; - case KmlTrackWriter.SENSOR_TYPE_CADENCE: + case KmlTrackWriter.EXTENDED_DATA_TYPE_CADENCE: cadenceList.add(value); break; default: - Log.w(TAG, "Data from sensor " + sensorType + " is not (yet) supported."); + Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported."); } } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index f851b69d5..4fb14ad5e 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -193,6 +193,7 @@ limitations under the License. Pace (min/mi) Pace (min/km) Recorded: %1$s + Speed (m/s) Cadence (rpm) Heart rate (bpm) Power (W)