Import/Export speed in GPX and KML files. (#306)

Import/Export speed in GPX and KML files. Fixes #300.
This commit is contained in:
Román Martínez
2020-06-30 14:39:44 +02:00
committed by GitHub
parent c9869bab0e
commit 23c3a4d651
8 changed files with 95 additions and 53 deletions
@@ -65,6 +65,7 @@ public class TestDataUtil {
trackPoint.setAccuracy((float) i / 100.0f); trackPoint.setAccuracy((float) i / 100.0f);
trackPoint.setAltitude(i * ALTITUDE_INTERVAL); trackPoint.setAltitude(i * ALTITUDE_INTERVAL);
trackPoint.setTime(i + 1); trackPoint.setTime(i + 1);
trackPoint.setSpeed(5f + (i / 10));
trackPoint.setHeartRate_bpm(100f + i); trackPoint.setHeartRate_bpm(100f + i);
trackPoint.setCyclingCadence_rpm(200f + i); trackPoint.setCyclingCadence_rpm(200f + i);
@@ -36,7 +36,6 @@ import static org.junit.Assert.assertNotNull;
* Export a track to {@link TrackFileFormat} and verify that the import is identical. * Export a track to {@link TrackFileFormat} and verify that the import is identical.
* <p> * <p>
* TODO: test ignores {@link TrackStatistics} for now. * TODO: test ignores {@link TrackStatistics} for now.
* TODO: enable verify speed.
*/ */
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class ExportImportTest { public class ExportImportTest {
@@ -130,7 +129,7 @@ public class ExportImportTest {
assertWaypoints(); assertWaypoints();
// 3. trackpoints // 3. trackpoints
assertTrackpoints(false, false, false, false); assertTrackpoints(false, false, false);
} }
@LargeTest @LargeTest
@@ -165,7 +164,7 @@ public class ExportImportTest {
assertWaypoints(); assertWaypoints();
// 3. trackpoints // 3. trackpoints
assertTrackpoints(false, true, true, true); assertTrackpoints(true, true, true);
} }
@LargeTest @LargeTest
@@ -230,8 +229,7 @@ public class ExportImportTest {
assertWaypoints(); assertWaypoints();
// 3. trackpoints // 3. trackpoints
//TODO Verify speed assertTrackpoints(false, false, false);
assertTrackpoints(false, false, false, false);
} }
private void assertWaypoints() { 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<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId); List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
assertEquals(trackPoints.size(), importedTrackPoints.size()); assertEquals(trackPoints.size(), importedTrackPoints.size());
@@ -269,9 +267,7 @@ public class ExportImportTest {
assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001); assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001); assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001); assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001);
if (verifySpeed) { assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001);
assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.01);
}
if (verifyHeartrate) { if (verifyHeartrate) {
assertEquals(trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01); assertEquals(trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
} }
@@ -176,6 +176,7 @@ public class GpxTrackWriter implements TrackWriter {
} }
printWriter.println( printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>"); "<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime()) + "</time>");
printWriter.println("<speed>" + trackPoint.getSpeed() + "</speed>");
printWriter.println("</trkpt>"); printWriter.println("</trkpt>");
} }
} }
@@ -46,9 +46,10 @@ public class KmlTrackWriter implements TrackWriter {
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 SENSOR_TYPE_CADENCE = "cadence"; public static final String EXTENDED_DATA_TYPE_SPEED = "speed";
public static final String SENSOR_TYPE_HEART_RATE = "heart_rate"; public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
public static final String SENSOR_TYPE_POWER = "power"; 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 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"; 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 final ContentProviderUtils contentProviderUtils;
private PrintWriter printWriter; private PrintWriter printWriter;
private final List<Float> speedList = new ArrayList<>();
private final List<Float> powerList = new ArrayList<>(); private final List<Float> powerList = new ArrayList<>();
private final List<Float> cadenceList = new ArrayList<>(); private final List<Float> cadenceList = new ArrayList<>();
private final List<Float> heartRateList = new ArrayList<>(); private final List<Float> heartRateList = new ArrayList<>();
@@ -123,10 +125,12 @@ public class KmlTrackWriter implements TrackWriter {
writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2); writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">"); printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
writeSimpleArrayStyle(EXTENDED_DATA_TYPE_SPEED, context.getString(R.string.description_speed_ms));
if (exportSensorData) { if (exportSensorData) {
writeSensorStyle(SENSOR_TYPE_POWER, context.getString(R.string.description_sensor_power)); writeSimpleArrayStyle(EXTENDED_DATA_TYPE_POWER, context.getString(R.string.description_sensor_power));
writeSensorStyle(SENSOR_TYPE_CADENCE, context.getString(R.string.description_sensor_cadence)); writeSimpleArrayStyle(EXTENDED_DATA_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_HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
} }
printWriter.println("</Schema>"); printWriter.println("</Schema>");
} }
@@ -229,6 +233,7 @@ public class KmlTrackWriter implements TrackWriter {
public void writeOpenSegment() { public void writeOpenSegment() {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("<gx:Track>"); printWriter.println("<gx:Track>");
speedList.clear();
powerList.clear(); powerList.clear();
cadenceList.clear(); cadenceList.clear();
heartRateList.clear(); heartRateList.clear();
@@ -240,15 +245,18 @@ public class KmlTrackWriter implements TrackWriter {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("<ExtendedData>"); printWriter.println("<ExtendedData>");
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">"); printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
if (speedList.size() > 0) {
writeSimpleArrayData(speedList, EXTENDED_DATA_TYPE_SPEED);
}
if (exportSensorData) { if (exportSensorData) {
if (powerList.size() > 0) { if (powerList.size() > 0) {
writeSensorData(powerList, SENSOR_TYPE_POWER); writeSimpleArrayData(powerList, EXTENDED_DATA_TYPE_POWER);
} }
if (cadenceList.size() > 0) { if (cadenceList.size() > 0) {
writeSensorData(cadenceList, SENSOR_TYPE_CADENCE); writeSimpleArrayData(cadenceList, EXTENDED_DATA_TYPE_CADENCE);
} }
if (heartRateList.size() > 0) { if (heartRateList.size() > 0) {
writeSensorData(heartRateList, SENSOR_TYPE_HEART_RATE); writeSimpleArrayData(heartRateList, EXTENDED_DATA_TYPE_HEART_RATE);
} }
} }
printWriter.println("</SchemaData>"); printWriter.println("</SchemaData>");
@@ -266,6 +274,10 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>"); printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>");
if (trackPoint.hasSpeed()) {
speedList.add(trackPoint.getSpeed());
}
if (exportSensorData) { if (exportSensorData) {
if (trackPoint.hasHeartRate()) { if (trackPoint.hasHeartRate()) {
heartRateList.add(trackPoint.getHeartRate_bpm()); 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 list a list of simple array data
* @param name the name of the sensor data * @param name the name of the simple array data
*/ */
private void writeSensorData(List<Float> list, String name) { private void writeSimpleArrayData(List<Float> list, String name) {
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">"); printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">");
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>"); printWriter.println("<gx:value>" + list.get(i) + "</gx:value>");
@@ -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 name the name of the simple array.
* @param sensorType the sensor display name * @param extendedDataType the extended data display name
*/ */
private void writeSensorStyle(String name, String sensorType) { private void writeSimpleArrayStyle(String name, String extendedDataType) {
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"float\">"); printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"float\">");
printWriter.println("<displayName>" + StringUtils.formatCData(sensorType) + "</displayName>"); printWriter.println("<displayName>" + StringUtils.formatCData(extendedDataType) + "</displayName>");
printWriter.println("</gx:SimpleArrayField>"); printWriter.println("</gx:SimpleArrayField>");
} }
} }
@@ -80,6 +80,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
protected String longitude; protected String longitude;
protected String altitude; protected String altitude;
protected String time; protected String time;
protected String speed;
protected String waypointType; protected String waypointType;
protected String photoUrl; protected String photoUrl;
@@ -319,20 +320,22 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// Calculate derived attributes from the previous point // Calculate derived attributes from the previous point
if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() != 0) { 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 // Check for negative time change
if (timeDifference <= 0) { if (timeDifference <= 0) {
Log.w(TAG, "Time difference not positive."); Log.w(TAG, "Time difference not positive.");
} else { } else {
/* /*
* We don't have a speed and bearing in GPX, make something up from the last two points. * 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. * 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 duration = timeDifference * UnitConversions.MS_TO_S;
double speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / duration; double speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / duration;
trackPoint.setSpeed((float) speed); trackPoint.setSpeed((float) speed);
}
} }
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint)); 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); 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;
} }
/** /**
@@ -39,6 +39,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_GPX = "gpx"; private static final String TAG_GPX = "gpx";
private static final String TAG_NAME = "name"; private static final String TAG_NAME = "name";
private static final String TAG_TIME = "time"; 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 = "trk";
private static final String TAG_TRACK_POINT = "trkpt"; private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg"; private static final String TAG_TRACK_SEGMENT = "trkseg";
@@ -115,6 +116,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
time = content.trim(); time = content.trim();
} }
break; break;
case TAG_SPEED:
if (content != null) {
speed = content.trim();
}
break;
case TAG_ELEVATION: case TAG_ELEVATION:
if (content != null) { if (content != null) {
altitude = content.trim(); altitude = content.trim();
@@ -149,6 +155,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
longitude = attributes.getValue(ATTRIBUTE_LON); longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null; altitude = null;
time = null; time = null;
speed = null;
} }
/** /**
@@ -61,8 +61,9 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String ATTRIBUTE_NAME = "name"; private static final String ATTRIBUTE_NAME = "name";
private boolean trackStarted = false; private boolean trackStarted = false;
private String sensorType; private String extendedDataType;
private ArrayList<TrackPoint> trackPoints; private ArrayList<TrackPoint> trackPoints;
private ArrayList<Float> speedList;
private ArrayList<Float> cadenceList; private ArrayList<Float> cadenceList;
private ArrayList<Float> heartRateList; private ArrayList<Float> heartRateList;
private ArrayList<Float> powerList; private ArrayList<Float> powerList;
@@ -102,7 +103,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
onTrackSegmentStart(); onTrackSegmentStart();
break; break;
case TAG_GX_SIMPLE_ARRAY_DATA: case TAG_GX_SIMPLE_ARRAY_DATA:
onSensorDataStart(attributes); onExtendedDataStart(attributes);
break; break;
} }
} }
@@ -124,7 +125,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
} else if (tag.equals(TAG_GX_COORD)) { } else if (tag.equals(TAG_GX_COORD)) {
onTrackPointEnd(); onTrackPointEnd();
} else if (tag.equals(TAG_GX_VALUE)) { } else if (tag.equals(TAG_GX_VALUE)) {
onSensorValueEnd(); onExtendedDataValueEnd();
} else if (tag.equals(TAG_NAME)) { } else if (tag.equals(TAG_NAME)) {
if (content != null) { if (content != null) {
name = content.trim(); name = content.trim();
@@ -209,6 +210,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
protected void onTrackSegmentStart() { protected void onTrackSegmentStart() {
super.onTrackSegmentStart(); super.onTrackSegmentStart();
trackPoints = new ArrayList<>(); trackPoints = new ArrayList<>();
speedList = new ArrayList<>();
heartRateList = new ArrayList<>(); heartRateList = new ArrayList<>();
cadenceList = new ArrayList<>(); cadenceList = new ArrayList<>();
powerList = new ArrayList<>(); powerList = new ArrayList<>();
@@ -222,6 +224,9 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
for (int i = 0; i < trackPoints.size(); i++) { for (int i = 0; i < trackPoints.size(); i++) {
TrackPoint trackPoint = trackPoints.get(i); TrackPoint trackPoint = trackPoints.get(i);
if (i < speedList.size()) {
trackPoint.setSpeed(speedList.get(i));
}
if (i < heartRateList.size()) { if (i < heartRateList.size()) {
trackPoint.setHeartRate_bpm(heartRateList.get(i)); 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) { private void onExtendedDataStart(Attributes attributes) {
sensorType = attributes.getValue(ATTRIBUTE_NAME); 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) { if (content == null) {
return; return;
} }
@@ -284,18 +289,21 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e); throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
} }
switch (sensorType) { switch (extendedDataType) {
case KmlTrackWriter.SENSOR_TYPE_POWER: case KmlTrackWriter.EXTENDED_DATA_TYPE_SPEED:
speedList.add(value);
break;
case KmlTrackWriter.EXTENDED_DATA_TYPE_POWER:
powerList.add(value); powerList.add(value);
break; break;
case KmlTrackWriter.SENSOR_TYPE_HEART_RATE: case KmlTrackWriter.EXTENDED_DATA_TYPE_HEART_RATE:
heartRateList.add(value); heartRateList.add(value);
break; break;
case KmlTrackWriter.SENSOR_TYPE_CADENCE: case KmlTrackWriter.EXTENDED_DATA_TYPE_CADENCE:
cadenceList.add(value); cadenceList.add(value);
break; break;
default: default:
Log.w(TAG, "Data from sensor " + sensorType + " is not (yet) supported."); Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported.");
} }
} }
} }
+1
View File
@@ -193,6 +193,7 @@ limitations under the License.
<string name="description_pace_imperial">Pace (min/mi)</string> <string name="description_pace_imperial">Pace (min/mi)</string>
<string name="description_pace_metric">Pace (min/km)</string> <string name="description_pace_metric">Pace (min/km)</string>
<string name="description_recorded_time">Recorded: %1$s</string> <string name="description_recorded_time">Recorded: %1$s</string>
<string name="description_speed_ms">Speed (m/s)</string>
<string name="description_sensor_cadence">Cadence (rpm)</string> <string name="description_sensor_cadence">Cadence (rpm)</string>
<string name="description_sensor_heart_rate">Heart rate (bpm)</string> <string name="description_sensor_heart_rate">Heart rate (bpm)</string>
<string name="description_sensor_power">Power (W)</string> <string name="description_sensor_power">Power (W)</string>