Segments: import/export is working incl. restoring TrackStatistics.

KML exports sensor data if actually present.

#316.
This commit is contained in:
Dennis Guse
2020-12-28 21:28:39 +01:00
parent 47b2d0748a
commit ab47320216
19 changed files with 506 additions and 284 deletions
@@ -38,6 +38,7 @@ public class TestDataUtil {
* @param trackId the trackId of the track * @param trackId the trackId of the track
* @param numPoints the trackPoints number in the track * @param numPoints the trackPoints number in the track
*/ */
@Deprecated //TODO Should start with SEGMENT_START_MANUAL and end with SEGMENT_END_MANUAL.
public static Pair<Track, List<TrackPoint>> createTrack(Track.Id trackId, int numPoints) { public static Pair<Track, List<TrackPoint>> createTrack(Track.Id trackId, int numPoints) {
Track track = createTrack(trackId); Track track = createTrack(trackId);
@@ -49,6 +50,51 @@ public class TestDataUtil {
return new Pair<>(track, trackPoints); return new Pair<>(track, trackPoints);
} }
public static TrackData createTestingTrack(Track.Id trackId) {
Track track = createTrack(trackId);
int i = 0;
List<TrackPoint> trackPoints = List.of(
TrackPoint.createSegmentStartManualWithTime(i++ + 1),
createTrackPoint(i++),
createTrackPoint(i++),
createTrackPoint(i++),
createTrackPoint(i++, TrackPoint.Type.SEGMENT_START_AUTOMATIC),
createTrackPoint(i++),
createTrackPoint(i++),
createTrackPoint(i++),
TrackPoint.createSegmentEndWithTime(i++ + 1),
TrackPoint.createSegmentStartManualWithTime(i++),
createTrackPoint(i++),
createTrackPoint(i++),
createTrackPoint(i++),
TrackPoint.createSegmentEndWithTime(i++ + 1)
);
List<Marker> markers = List.of(
new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(1).getLocation(), null),
new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(4).getLocation(), null),
new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, 0.0, 0, trackPoints.get(5).getLocation(), null)
);
return new TrackData(track, trackPoints, markers);
}
public static class TrackData {
public final Track track;
public final List<TrackPoint> trackPoints;
public final List<Marker> markers;
public TrackData(Track track, List<TrackPoint> trackPoints, List<Marker> markers) {
this.track = track;
this.trackPoints = trackPoints;
this.markers = markers;
}
}
public static Track createTrackAndInsert(ContentProviderUtils contentProviderUtils, Track.Id trackId, int numPoints) { public static Track createTrackAndInsert(ContentProviderUtils contentProviderUtils, Track.Id trackId, int numPoints) {
Pair<Track, List<TrackPoint>> pair = createTrack(trackId, numPoints); Pair<Track, List<TrackPoint>> pair = createTrack(trackId, numPoints);
@@ -63,7 +109,7 @@ public class TestDataUtil {
* @param i the index for the TrackPoint. * @param i the index for the TrackPoint.
*/ */
public static TrackPoint createTrackPoint(int i) { public static TrackPoint createTrackPoint(int i) {
TrackPoint trackPoint = new TrackPoint(); TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
trackPoint.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0); trackPoint.setLatitude(INITIAL_LATITUDE + (double) i / 10000.0);
trackPoint.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0); trackPoint.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0);
trackPoint.setAccuracy((float) i / 100.0f); trackPoint.setAccuracy((float) i / 100.0f);
@@ -79,6 +125,12 @@ public class TestDataUtil {
return trackPoint; return trackPoint;
} }
public static TrackPoint createTrackPoint(int i, TrackPoint.Type type) {
TrackPoint trackPoint = createTrackPoint(i);
trackPoint.setType(type);
return trackPoint;
}
/** /**
* Inserts a track with locations into the database. * Inserts a track with locations into the database.
* *
@@ -270,7 +270,7 @@ public class CustomContentProviderUtilsTest {
Track.Id trackId2 = new Track.Id(random + 1); Track.Id trackId2 = new Track.Id(random + 1);
Track.Id trackId3 = new Track.Id(random + 2); Track.Id trackId3 = new Track.Id(random + 2);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId1, 0); TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId1, 10);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId2, 10); TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId2, 10);
TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId3, 10); TestDataUtil.createTrackAndInsert(contentProviderUtils, trackId3, 10);
@@ -282,7 +282,7 @@ public class CustomContentProviderUtilsTest {
Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID); Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID);
assertEquals(3, tracksCursor.getCount()); assertEquals(3, tracksCursor.getCount());
Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID); Cursor tracksPointsCursor = contentResolver.query(TrackPointsColumns.CONTENT_URI_BY_ID, null, null, null, TrackPointsColumns._ID);
assertEquals(20, tracksPointsCursor.getCount()); assertEquals(30, tracksPointsCursor.getCount());
Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID); Cursor markerCursor = contentResolver.query(MarkerColumns.CONTENT_URI, null, null, null, MarkerColumns._ID);
assertEquals(1, markerCursor.getCount()); assertEquals(1, markerCursor.getCount());
// Delete one track. // Delete one track.
@@ -256,7 +256,7 @@ public class ChartFragmentTest {
* Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter. * Add a time span here to make sure the second point is valid, the value 222 here is doesn't matter.
*/ */
trackPoint2.setTime(trackPoint1.getTime() + 222); trackPoint2.setTime(trackPoint1.getTime() + 222);
trackPoint2.setSpeed(130); trackPoint2.setSpeed(130f);
point = chartFragment.createPendingPoint(trackPoint2); point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(130.0 * UnitConversions.MPS_TO_KMH, point.getSpeed(), 0.01); assertEquals(130.0 * UnitConversions.MPS_TO_KMH, point.getSpeed(), 0.01);
} }
@@ -281,7 +281,7 @@ public class ChartFragmentTest {
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222 * Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/ */
trackPoint2.setTime(trackPoint2.getTime() + 222); trackPoint2.setTime(trackPoint2.getTime() + 222);
trackPoint2.setSpeed(102); trackPoint2.setSpeed(102f);
point = chartFragment.createPendingPoint(trackPoint2); point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(102.0 * UnitConversions.MPS_TO_KMH * UnitConversions.KM_TO_MI, point.getSpeed(), 0.01); assertEquals(102.0 * UnitConversions.MPS_TO_KMH * UnitConversions.KM_TO_MI, point.getSpeed(), 0.01);
} }
@@ -306,7 +306,7 @@ public class ChartFragmentTest {
* Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222 * Speed is valid if: speedDifference > Constants.MAX_ACCELERATION * timeDifference speedDifference = 102 -100 timeDifference = 222
*/ */
trackPoint2.setTime(trackPoint2.getTime() + 222); trackPoint2.setTime(trackPoint2.getTime() + 222);
trackPoint2.setSpeed(102); trackPoint2.setSpeed(102f);
point = chartFragment.createPendingPoint(trackPoint2); point = chartFragment.createPendingPoint(trackPoint2);
assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MPS_TO_KMH), point.getPace(), 0.01); assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MPS_TO_KMH), point.getPace(), 0.01);
} }
@@ -318,7 +318,7 @@ public class ChartFragmentTest {
public void testCreatePendingPoint_pace_zeroSpeed() { public void testCreatePendingPoint_pace_zeroSpeed() {
chartFragment.setReportSpeed(false); chartFragment.setReportSpeed(false);
TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint(); TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint();
trackPoint.setSpeed(0); trackPoint.setSpeed(0f);
ChartPoint point = chartFragment.createPendingPoint(trackPoint); ChartPoint point = chartFragment.createPendingPoint(trackPoint);
assertEquals(0.0, point.getPace(), 0.01); assertEquals(0.0, point.getPace(), 0.01);
} }
@@ -0,0 +1,53 @@
package de.dennisguse.opentracks.io.file.exporter;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.ByteArrayOutputStream;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
import static org.junit.Assert.assertEquals;
@RunWith(JUnit4.class)
public class KmlTrackWriterTest {
private final Context context = ApplicationProvider.getApplicationContext();
/**
* Sensor data by type should only be created if present in at least on TrackPoint.
*/
@Test
public void writeCloseSegment_only_write_sensordata_if_present() {
String expected = "<when>1970-01-01T00:00:00Z</when>\n" +
"<gx:coord/>\n" +
"<ExtendedData>\n" +
"<SchemaData schemaUrl=\"#schema\">\n" +
"</SchemaData>\n" +
"</ExtendedData>\n" +
"</gx:Track>\n";
// given
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochSecond(0));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
KmlTrackWriter kmlTrackWriter = (KmlTrackWriter) TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA.newTrackWriter(context);
kmlTrackWriter.prepare(outputStream);
kmlTrackWriter.writeTrackPoint(trackPoint);
// when
kmlTrackWriter.writeCloseSegment();
kmlTrackWriter.close();
// then
assertEquals(expected, outputStream.toString());
}
}
@@ -1,15 +1,21 @@
package de.dennisguse.opentracks.io.file.importer; package de.dennisguse.opentracks.io.file.importer;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.Looper;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest; import androidx.test.filters.LargeTest;
import androidx.test.rule.GrantPermissionRule;
import androidx.test.rule.ServiceTestRule;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
@@ -19,15 +25,18 @@ import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFileFormat;
import de.dennisguse.opentracks.io.file.exporter.TrackExporter; import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceInterface;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -45,6 +54,20 @@ public class ExportImportTest {
private static final String TAG = ExportImportTest.class.getSimpleName(); private static final String TAG = ExportImportTest.class.getSimpleName();
@Rule
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
@Rule
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
@BeforeClass
public static void preSetUp() {
// Prepare looper for Android's message queue
if (Looper.myLooper() == null) Looper.prepare();
}
private TrackRecordingServiceInterface service;
private final Context context = ApplicationProvider.getApplicationContext(); private final Context context = ApplicationProvider.getApplicationContext();
private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context); private final ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
@@ -53,37 +76,44 @@ public class ExportImportTest {
private static final String TRACK_CATEGORY = "the category"; private static final String TRACK_CATEGORY = "the category";
private static final String TRACK_DESCRIPTION = "the description"; private static final String TRACK_DESCRIPTION = "the description";
private final List<Marker> markers = new ArrayList<>(); private Track track;
private final List<TrackPoint> trackPoints = new ArrayList<>(); private List<Marker> markers = new ArrayList<>();
private List<TrackPoint> trackPoints = new ArrayList<>();
private Track.Id trackId;
private Track.Id importTrackId; private Track.Id importTrackId;
private final Track.Id trackId = new Track.Id(System.currentTimeMillis());
@Before @Before
public void setUp() { public void setUp() throws TimeoutException, InterruptedException {
Pair<Track, List<TrackPoint>> track = TestDataUtil.createTrack(trackId, 10); service = ((TrackRecordingServiceInterface) mServiceRule.bindService(new Intent(context, TrackRecordingService.class)));
track.first.setIcon(TRACK_ICON);
track.first.setCategory(TRACK_CATEGORY);
track.first.setDescription(TRACK_DESCRIPTION);
contentProviderUtils.insertTrack(track.first);
contentProviderUtils.bulkInsertTrackPoint(track.second, track.first.getId());
trackPoints.clear(); trackId = service.startNewTrack();
trackPoints.addAll(track.second);
for (int i = 0; i < 3; i++) { service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 0, 66, 3, 50), 0);
Marker marker = new Marker(trackId, track.second.get(i).getLocation()); service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
marker.setName("the marker " + i); service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50), 0);
marker.setDescription("the marker description " + i); service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50), 0);
marker.setCategory("the marker category" + i); service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
marker.setIcon("the marker icon" + i); Thread.sleep(10);
marker.setPhotoUrl("the photo url" + i); service.pauseCurrentTrack();
contentProviderUtils.insertMarker(marker);
markers.add(marker); service.resumeCurrentTrack();
} service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.003, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16.001, 10, 15, 10, 0, 66, 3, 50), 0);
service.endCurrentTrack();
assertEquals(markers.size(), contentProviderUtils.getMarkerCount(trackId)); track = contentProviderUtils.getTrack(trackId);
track.setIcon(TRACK_ICON);
track.setCategory(TRACK_CATEGORY);
track.setDescription(TRACK_DESCRIPTION);
contentProviderUtils.updateTrack(track);
track = contentProviderUtils.getTrack(trackId);
trackPoints = contentProviderUtils.getTrackPoints(trackId);
markers = contentProviderUtils.getMarkers(trackId);
assertEquals(10, trackPoints.size());
assertEquals(2, markers.size());
} }
@After @After
@@ -94,6 +124,7 @@ public class ExportImportTest {
} }
} }
@Ignore("Not implemented")
@LargeTest @LargeTest
@Test @Test
public void kml_only_track() { public void kml_only_track() {
@@ -130,11 +161,14 @@ public class ExportImportTest {
assertEquals(track.getIcon(), importedTrack.getIcon()); assertEquals(track.getIcon(), importedTrack.getIcon());
assertEquals(track.getUuid(), importedTrack.getUuid()); assertEquals(track.getUuid(), importedTrack.getUuid());
// 2. markers // 2. trackpoints
assertMarkers();
// 3. trackpoints
assertTrackpoints(false, false, false, false, false); assertTrackpoints(false, false, false, false, false);
// 3. trackstatistics
assertTrackStatistics();
// 4. markers
assertMarkers();
} }
@LargeTest @LargeTest
@@ -165,11 +199,14 @@ public class ExportImportTest {
assertEquals(track.getName(), importedTrack.getName()); assertEquals(track.getName(), importedTrack.getName());
assertEquals(track.getIcon(), importedTrack.getIcon()); assertEquals(track.getIcon(), importedTrack.getIcon());
// 2. markers // 2. trackpoints
assertMarkers();
// 3. trackpoints
assertTrackpoints(true, true, true, true, true); assertTrackpoints(true, true, true, true, true);
// 2. trackstatistics
assertTrackStatistics();
// 4. markers
assertMarkers();
} }
@LargeTest @LargeTest
@@ -258,11 +295,14 @@ public class ExportImportTest {
//TODO exporting and importing a track icon is not yet supported by GpxTrackWriter. //TODO exporting and importing a track icon is not yet supported by GpxTrackWriter.
//assertEquals(track.getIcon(), trackImported.getIcon()); //assertEquals(track.getIcon(), trackImported.getIcon());
// 2. markers // 2. trackpoints
assertMarkers();
// 3. trackpoints
assertTrackpoints(false, true, true, true, true); assertTrackpoints(false, true, true, true, true);
// 3. trackstatistics
assertTrackStatistics();
// 4. markers
assertMarkers();
} }
@LargeTest @LargeTest
@@ -318,14 +358,31 @@ public class ExportImportTest {
TrackPoint importedTrackPoint = importedTrackPoints.get(i); TrackPoint importedTrackPoint = importedTrackPoints.get(i);
assertEquals(trackPoint.getTime(), importedTrackPoint.getTime(), 0.01); assertEquals(trackPoint.getTime(), importedTrackPoint.getTime(), 0.01);
TrackPoint.Type type = trackPoint.getType();
assertEquals("" + i, trackPoint.getType(), importedTrackPoint.getType());
// TODO Not exported for GPX/KML // TODO Not exported for GPX/KML
// assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01); // assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01);
assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001); assertEquals(trackPoint.hasLocation(), importedTrackPoint.hasLocation());
assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001); if (trackPoint.hasLocation()) {
assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001); assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001);
assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001); assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001);
}
assertEquals(trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
if (trackPoint.hasSpeed()) {
assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001);
}
assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
if (trackPoint.hasAltitude()) {
assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001);
}
if (type.equals(TrackPoint.Type.SEGMENT_START_MANUAL) || type.equals(TrackPoint.Type.SEGMENT_END_MANUAL)) {
//TODO REMOVE
continue;
}
if (verifyHeartrate) { if (verifyHeartrate) {
assertEquals(trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01); assertEquals(trackPoint.getHeartRate_bpm(), importedTrackPoint.getHeartRate_bpm(), 0.01);
} }
@@ -343,4 +400,50 @@ public class ExportImportTest {
} }
} }
} }
private void assertTrackStatistics() {
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack.getTrackStatistics());
TrackStatistics trackStatistics = track.getTrackStatistics();
TrackStatistics importedTrackStatistics = importedTrack.getTrackStatistics();
// Time
assertEquals(trackStatistics.getStartTime_ms(), trackStatistics.getStartTime_ms(), 0.01);
assertEquals(trackStatistics.getStopTime_ms(), trackStatistics.getStopTime_ms());
assertEquals(trackStatistics.getTotalTime(), trackStatistics.getTotalTime());
assertEquals(trackStatistics.getMovingTime(), trackStatistics.getMovingTime());
// Distance
assertEquals(trackStatistics.getTotalDistance(), trackStatistics.getTotalDistance(), 0.01);
// Speed
assertEquals(trackStatistics.getMaxSpeed(), trackStatistics.getMaxSpeed(), 0.01);
assertEquals(trackStatistics.getAverageSpeed(), trackStatistics.getAverageSpeed(), 0.01);
assertEquals(trackStatistics.getAverageMovingSpeed(), trackStatistics.getAverageMovingSpeed(), 0.01);
// Elevation
assertEquals(trackStatistics.getMinElevation(), trackStatistics.getMinElevation(), 0.01);
assertEquals(trackStatistics.getMaxElevation(), trackStatistics.getMaxElevation(), 0.01);
assertEquals(trackStatistics.getTotalElevationGain(), trackStatistics.getTotalElevationGain(), 0.01);
assertEquals(trackStatistics.getTotalElevationLoss(), trackStatistics.getTotalElevationLoss(), 0.01);
}
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, long speed, long altitude, float elevationGain, float heartRate, float cyclingCadence, float power) {
Location location = new Location("gps");
location.setTime(time);
location.setLongitude(longitude);
location.setLatitude(latitude);
location.setAccuracy(accuracy);
location.setAltitude(altitude);
location.setSpeed(speed);
TrackPoint tp = new TrackPoint(location);
tp.setHeartRate_bpm(heartRate);
tp.setCyclingCadence_rpm(cyclingCadence);
tp.setPower(power);
tp.setElevationGain(elevationGain);
return tp;
}
} }
@@ -65,25 +65,19 @@ public class LegacyKmlImportTest {
// 3. trackpoints // 3. trackpoints
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId); List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
assertEquals(8, importedTrackPoints.size()); // 6 trackpoints + segmentEnd and segmentStart assertEquals(6, importedTrackPoints.size());
// first 3 trackpoints // first 3 trackpoints
assertEquals(TrackPoint.Type.SEGMENT_START_AUTOMATIC, importedTrackPoints.get(0).getType());
assertTrackpoint(importedTrackPoints.get(0), "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125); assertTrackpoint(importedTrackPoints.get(0), "2020-11-28T17:06:22.401Z", 1.234156, 12.340097, 469.286376953125);
assertTrackpoint(importedTrackPoints.get(1), "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281); assertTrackpoint(importedTrackPoints.get(1), "2020-11-28T17:06:25.448Z", 1.23415, 12.340036, 439.1626281738281);
assertTrackpoint(importedTrackPoints.get(2), "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719); assertTrackpoint(importedTrackPoints.get(2), "2020-11-28T17:06:47.888Z", 1.23405, 12.340057, 421.8070983886719);
// created pause trackpoint with time of previous valid trackpoint
assertTrackpoint(importedTrackPoints.get(3), "2020-11-28T17:06:47.888Z", 0.0, 0.0, 0.0);
assertEquals(1, importedTrackPoints.get(3).getType().type_db);
// created resume trackpoint with time of next valid trackpoint // created resume trackpoint with time of next valid trackpoint
assertTrackpoint(importedTrackPoints.get(4), "2020-11-28T17:06:55.861Z", 0.0, 0.0, 0.0); assertEquals(TrackPoint.Type.SEGMENT_START_AUTOMATIC, importedTrackPoints.get(3).getType());
assertEquals(-2, importedTrackPoints.get(4).getType().type_db); assertTrackpoint(importedTrackPoints.get(3), "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
assertTrackpoint(importedTrackPoints.get(4), "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
// last 3 trackpoints assertTrackpoint(importedTrackPoints.get(5), "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
assertTrackpoint(importedTrackPoints.get(5), "2020-11-28T17:06:55.861Z", 1.23405, 12.340057, 419.93902587890625);
assertTrackpoint(importedTrackPoints.get(6), "2020-11-28T17:06:56.905Z", 1.23405, 12.340057, 419.9036560058594);
assertTrackpoint(importedTrackPoints.get(7), "2020-11-28T17:07:20.870Z", 1.234046, 12.340082, 417.99432373046875);
} }
private void assertTrackpoint(final TrackPoint trackPoint, final String when, final double longitude, final double latitude, final double altitude) { private void assertTrackpoint(final TrackPoint trackPoint, final String when, final double longitude, final double latitude, final double altitude) {
@@ -50,7 +50,7 @@ public class HandlerServerTest {
@Test @Test
public void sendTrackPoint() throws InterruptedException { public void sendTrackPoint() throws InterruptedException {
// given // given
TrackPoint trackPoint = new TrackPoint(); TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
int accuracy = 50; int accuracy = 50;
// when // when
@@ -44,6 +44,8 @@ public final class Marker {
//TODO It is the distance from the track starting point; rename to something more meaningful //TODO It is the distance from the track starting point; rename to something more meaningful
private double length = 0.0; private double length = 0.0;
private long duration = 0; private long duration = 0;
@Deprecated //TODO Replace by storing latitude/longitude directly.
private final Location location; private final Location location;
@Deprecated //TODO Make an URI instead of String @Deprecated //TODO Make an URI instead of String
private String photoUrl = ""; private String photoUrl = "";
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks.content.data; package de.dennisguse.opentracks.content.data;
import android.location.Location; import android.location.Location;
import android.location.LocationManager;
import android.os.Parcel; import android.os.Parcel;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -40,21 +39,25 @@ import java.util.Objects;
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
//TODO Merge constructors by use case; we have too many.
public class TrackPoint { public class TrackPoint {
private TrackPoint.Id id; private TrackPoint.Id id;
private final Location location; private long time_ms;
private Double latitude;
private Double longitude;
private Float accuracy;
private Double altitude_m;
private Float speed_mps;
private Float bearing;
//TODO Private
public enum Type { public enum Type {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume); no useful coordinates SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
SEGMENT_START_AUTOMATIC(-1), //Start of a segment due to too much distance from previous TrackPoint SEGMENT_START_AUTOMATIC(-1), //Start of a segment due to too much distance from previous TrackPoint
TRACKPOINT(0), //Normal trackpoint got from GPS TRACKPOINT(0), //Just GPS data.
SEGMENT_END_MANUAL(1); //End of a segment; no useful coordinates SEGMENT_END_MANUAL(1); //End of a segment
public final int type_db; public final int type_db;
@@ -64,7 +67,7 @@ public class TrackPoint {
@Override @Override
public String toString() { public String toString() {
return "" + type_db; return name() + "(" + type_db + ")";
} }
public static Type getById(int id) { public static Type getById(int id) {
@@ -74,10 +77,6 @@ public class TrackPoint {
throw new RuntimeException("unknown id: " + id); throw new RuntimeException("unknown id: " + id);
} }
public boolean hasLocation() {
return this == SEGMENT_START_AUTOMATIC || this == TRACKPOINT;
}
} }
@NonNull @NonNull
@@ -89,35 +88,33 @@ public class TrackPoint {
private Float elevationGain = null; private Float elevationGain = null;
private Float elevationLoss = null; private Float elevationLoss = null;
public TrackPoint() {
this(Type.TRACKPOINT, new Location(""));
}
public TrackPoint(@NonNull Type type) { public TrackPoint(@NonNull Type type) {
this.type = type; this.type = type;
this.location = new Location("");
} }
public TrackPoint(@NonNull Location location) { public TrackPoint(@NonNull Location location) {
this.type = Type.TRACKPOINT; this(Type.TRACKPOINT);
this.location = location;
this.latitude = location.getLatitude();
this.longitude = location.getLongitude();
this.altitude_m = location.getAltitude();
this.speed_mps = location.getSpeed();
this.accuracy = location.getAccuracy();
setTime(System.currentTimeMillis()); setTime(System.currentTimeMillis());
} }
public TrackPoint(@NonNull Type type, @NonNull Location location) { public TrackPoint(@NonNull Type type, long time_ms) {
this.type = type; this(type);
this.location = location; this.time_ms = time_ms;
} }
public TrackPoint(double latitude, double longitude, Double altitude, long time) { public TrackPoint(double latitude, double longitude, Double altitude, long time) {
this.type = Type.TRACKPOINT; this(Type.TRACKPOINT);
location = new Location(LocationManager.GPS_PROVIDER); this.latitude = latitude;
location.setLatitude(latitude); this.longitude = longitude;
location.setLongitude(longitude); this.altitude_m = altitude;
if (altitude != null) { this.time_ms = time;
location.setAltitude(altitude);
}
location.setTime(time);
} }
@Deprecated //See #316 @Deprecated //See #316
@@ -126,9 +123,7 @@ public class TrackPoint {
} }
public static TrackPoint createSegmentStartManualWithTime(long time) { public static TrackPoint createSegmentStartManualWithTime(long time) {
Location resume = new Location(LocationManager.GPS_PROVIDER); return new TrackPoint(Type.SEGMENT_START_MANUAL, time);
resume.setTime(time);
return new TrackPoint(Type.SEGMENT_START_MANUAL, resume);
} }
@Deprecated //See #316 @Deprecated //See #316
@@ -137,9 +132,7 @@ public class TrackPoint {
} }
public static TrackPoint createSegmentStartAutomaticWithTime(long time) { public static TrackPoint createSegmentStartAutomaticWithTime(long time) {
Location resume = new Location(LocationManager.GPS_PROVIDER); return new TrackPoint(Type.SEGMENT_START_AUTOMATIC, time);
resume.setTime(time);
return new TrackPoint(Type.SEGMENT_START_AUTOMATIC, resume);
} }
public static TrackPoint createSegmentEnd() { public static TrackPoint createSegmentEnd() {
@@ -151,9 +144,7 @@ public class TrackPoint {
} }
public static TrackPoint createSegmentEndWithTime(long time) { public static TrackPoint createSegmentEndWithTime(long time) {
Location pause = new Location(LocationManager.GPS_PROVIDER); return new TrackPoint(Type.SEGMENT_END_MANUAL, time);
pause.setTime(time);
return new TrackPoint(Type.SEGMENT_END_MANUAL, pause);
} }
@NonNull @NonNull
@@ -185,8 +176,28 @@ public class TrackPoint {
this.id = id; this.id = id;
} }
public boolean hasLocation() {
return latitude != null || longitude != null;
}
@Nullable @Nullable
public Location getLocation() { public Location getLocation() {
Location location = new Location("");
location.setTime(time_ms);
if (hasLocation()) {
location.setLatitude(latitude);
location.setLongitude(longitude);
}
if (hasBearing()) {
location.setBearing(bearing);
}
if (hasAccuracy()) {
location.setAccuracy(accuracy);
}
if (hasAltitude()) {
location.setAltitude(altitude_m);
}
return location; return location;
} }
@@ -194,7 +205,7 @@ public class TrackPoint {
return elevationGain != null; return elevationGain != null;
} }
public float getElevationGain() { public Float getElevationGain() {
return elevationGain; return elevationGain;
} }
@@ -215,87 +226,87 @@ public class TrackPoint {
} }
public double getLatitude() { public double getLatitude() {
return location.getLatitude(); return latitude;
} }
public void setLatitude(double latitude) { public void setLatitude(double latitude) {
location.setLatitude(latitude); this.latitude = latitude;
} }
public double getLongitude() { public double getLongitude() {
return location.getLongitude(); return longitude;
} }
public void setLongitude(double longitude) { public void setLongitude(double longitude) {
location.setLongitude(longitude); this.longitude = longitude;
} }
public long getTime() { public long getTime() {
return location.getTime(); return time_ms;
} }
public void setTime(long time) { public void setTime(long time) {
location.setTime(time); this.time_ms = time;
} }
public boolean hasAltitude() { public boolean hasAltitude() {
return location.hasAltitude(); return altitude_m != null;
} }
public double getAltitude() { public double getAltitude() {
return location.getAltitude(); return altitude_m;
} }
public void setAltitude(double altitude) { public void setAltitude(double altitude) {
location.setAltitude(altitude); this.altitude_m = altitude;
} }
public boolean hasSpeed() { public boolean hasSpeed() {
return location.hasSpeed(); return speed_mps != null;
} }
public float getSpeed() { public float getSpeed() {
return location.getSpeed(); return speed_mps;
} }
public void setSpeed(float speed) { public void setSpeed(Float speed) {
location.setSpeed(speed); this.speed_mps = speed;
} }
public boolean hasBearing() { public boolean hasBearing() {
return location.hasBearing(); return bearing != null;
} }
public float getBearing() { public float getBearing() {
return location.getBearing(); return bearing;
} }
public void setBearing(float bearing) { public void setBearing(Float bearing) {
location.setBearing(bearing); this.bearing = bearing;
} }
public boolean hasAccuracy() { public boolean hasAccuracy() {
return location.hasAccuracy(); return accuracy != null;
} }
public float getAccuracy() { public float getAccuracy() {
return location.getAccuracy(); return accuracy;
} }
public void setAccuracy(float horizontalAccuracy) { public void setAccuracy(float horizontalAccuracy) {
location.setAccuracy(horizontalAccuracy); this.accuracy = horizontalAccuracy;
} }
public float distanceTo(@NonNull TrackPoint dest) { public float distanceTo(@NonNull TrackPoint dest) {
return location.distanceTo(dest.getLocation()); return getLocation().distanceTo(dest.getLocation());
} }
public float bearingTo(@NonNull TrackPoint dest) { public float bearingTo(@NonNull TrackPoint dest) {
return location.bearingTo(dest.getLocation()); return getLocation().bearingTo(dest.getLocation());
} }
public float bearingTo(@NonNull Location dest) { public float bearingTo(@NonNull Location dest) {
return location.bearingTo(dest); return getLocation().bearingTo(dest);
} }
// Sensor data // Sensor data
@@ -342,7 +353,16 @@ public class TrackPoint {
@NonNull @NonNull
@Override @Override
public String toString() { public String toString() {
return "time=" + getTime() + " (type=" + getType() + "): lat=" + getLatitude() + " lng=" + getLongitude() + " acc=" + getAccuracy(); String result = "time=" + getTime() + " (type=" + getType() + ")";
if (!hasLocation()) {
return result;
}
result += ": lat=" + getLatitude() + " lng=" + getLongitude();
if (!hasAccuracy()) {
return result;
}
return result + " acc=" + getAccuracy();
} }
public static class Id { public static class Id {
@@ -543,9 +543,7 @@ public class ContentProviderUtils {
*/ */
static TrackPoint fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes) { static TrackPoint fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes) {
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.getById(cursor.getInt(indexes.typeIndex))); TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.getById(cursor.getInt(indexes.typeIndex)));
if (!cursor.isNull(indexes.longitudeIndex)) { trackPoint.setId(new TrackPoint.Id(cursor.getInt(indexes.idIndex)));
trackPoint.setId(new TrackPoint.Id(cursor.getInt(indexes.idIndex)));
}
if (!cursor.isNull(indexes.longitudeIndex)) { if (!cursor.isNull(indexes.longitudeIndex)) {
trackPoint.setLongitude(((double) cursor.getInt(indexes.longitudeIndex)) / 1E6); trackPoint.setLongitude(((double) cursor.getInt(indexes.longitudeIndex)) / 1E6);
@@ -694,7 +692,7 @@ public class ContentProviderUtils {
*/ */
@Deprecated @Deprecated
public TrackPoint getLastValidTrackPoint(Track.Id trackId) { public TrackPoint getLastValidTrackPoint(Track.Id trackId) {
String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TYPE + " IN (" + TrackPoint.Type.SEGMENT_START_AUTOMATIC + "," + TrackPoint.Type.TRACKPOINT + "))"; String selection = TrackPointsColumns._ID + "=(SELECT MAX(" + TrackPointsColumns._ID + ") FROM " + TrackPointsColumns.TABLE_NAME + " WHERE " + TrackPointsColumns.TRACKID + "=? AND " + TrackPointsColumns.TYPE + " IN (" + TrackPoint.Type.SEGMENT_START_AUTOMATIC.type_db + "," + TrackPoint.Type.TRACKPOINT.type_db + "))";
String[] selectionArgs = new String[]{Long.toString(trackId.getId())}; String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
return findTrackPointBy(selection, selectionArgs); return findTrackPointBy(selection, selectionArgs);
} }
@@ -719,12 +717,12 @@ public class ContentProviderUtils {
private ContentValues createContentValues(TrackPoint trackPoint, Track.Id trackId) { private ContentValues createContentValues(TrackPoint trackPoint, Track.Id trackId) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(TrackPointsColumns.TRACKID, trackId.getId()); values.put(TrackPointsColumns.TRACKID, trackId.getId());
if (trackPoint.getType() != null) { values.put(TrackPointsColumns.TYPE, trackPoint.getType().type_db);
values.put(TrackPointsColumns.TYPE, trackPoint.getType().type_db);
}
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.getLongitude() * 1E6));
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getLatitude() * 1E6));
if (trackPoint.hasLocation()) {
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.getLongitude() * 1E6));
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getLatitude() * 1E6));
}
values.put(TrackPointsColumns.TIME, trackPoint.getTime()); values.put(TrackPointsColumns.TIME, trackPoint.getTime());
if (trackPoint.hasAltitude()) { if (trackPoint.hasAltitude()) {
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude()); values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude());
@@ -12,6 +12,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
/** /**
* A lightweight wrapper around the original {@link Cursor} with a method to clean up. * A lightweight wrapper around the original {@link Cursor} with a method to clean up.
*/ */
//TODO Remove batching; that should be handled by the database/contentprovider (i.e., already in place as we use a cursor)!
public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable { public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
private static final String TAG = TrackPointIterator.class.getSimpleName(); private static final String TAG = TrackPointIterator.class.getSimpleName();
@@ -22,7 +23,6 @@ public class TrackPointIterator implements Iterator<TrackPoint>, AutoCloseable {
private TrackPoint.Id lastTrackPointId = null; private TrackPoint.Id lastTrackPointId = null;
private Cursor cursor; private Cursor cursor;
public TrackPointIterator(ContentProviderUtils contentProviderUtils, Track.Id trackId, TrackPoint.Id startTrackPointId) { public TrackPointIterator(ContentProviderUtils contentProviderUtils, Track.Id trackId, TrackPoint.Id startTrackPointId) {
this.contentProviderUtils = contentProviderUtils; this.contentProviderUtils = contentProviderUtils;
this.trackId = trackId; this.trackId = trackId;
@@ -191,7 +191,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
} }
TrackPoint trackPoint = lastTrackPoint; //NOTE: There seems to be a race condition; just fix the symptom for now. TrackPoint trackPoint = lastTrackPoint; //NOTE: There seems to be a race condition; just fix the symptom for now.
if (trackPoint != null && trackPoint.getType().hasLocation()) { if (trackPoint != null && trackPoint.hasLocation()) {
boolean hasFix = !LocationUtils.isTrackPointOld(trackPoint); boolean hasFix = !LocationUtils.isTrackPointOld(trackPoint);
if (!hasFix) { if (!hasFix) {
@@ -45,8 +45,8 @@ public class FileTrackExporter implements TrackExporter {
/** /**
* Constructor. * Constructor.
* *
* @param contentProviderUtils the content provider utils * @param contentProviderUtils the content provider utils
* @param trackWriter the track writer * @param trackWriter the track writer
*/ */
public FileTrackExporter(ContentProviderUtils contentProviderUtils, TrackWriter trackWriter) { public FileTrackExporter(ContentProviderUtils contentProviderUtils, TrackWriter trackWriter) {
this.contentProviderUtils = contentProviderUtils; this.contentProviderUtils = contentProviderUtils;
@@ -70,6 +70,7 @@ public class FileTrackExporter implements TrackExporter {
if (hasMultipleTracks) { if (hasMultipleTracks) {
trackWriter.writeMultiTrackBegin(); trackWriter.writeMultiTrackBegin();
} }
//TODO Why use startTime of first track for the others?
long startTime = tracks[0].getTrackStatistics().getStartTime_ms(); long startTime = tracks[0].getTrackStatistics().getStartTime_ms();
for (Track track : tracks) { for (Track track : tracks) {
long offset = track.getTrackStatistics().getStartTime_ms() - startTime; long offset = track.getTrackStatistics().getStartTime_ms() - startTime;
@@ -122,50 +123,40 @@ public class FileTrackExporter implements TrackExporter {
private void writeLocations(Track track, long offset) throws InterruptedException { private void writeLocations(Track track, long offset) throws InterruptedException {
boolean wroteTrack = false; boolean wroteTrack = false;
boolean wroteSegment = false; boolean wroteSegment = false;
boolean isLastLocationValid = false;
TrackPoint lastTrackPoint = null;
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) { try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
while (trackPointIterator.hasNext()) { while (trackPointIterator.hasNext()) {
if (Thread.interrupted()) { if (Thread.interrupted()) throw new InterruptedException();
throw new InterruptedException();
}
TrackPoint trackPoint = trackPointIterator.next();
TrackPoint trackPoint = trackPointIterator.next();
setLocationTime(trackPoint, offset); setLocationTime(trackPoint, offset);
boolean isLocationValid = trackPoint.getType().hasLocation(); if (!wroteTrack) {
boolean isSegmentValid = isLocationValid && isLastLocationValid; trackWriter.writeBeginTrack(track, trackPoint);
if (!wroteTrack && isSegmentValid) {
// Found the first two consecutive locations that are valid
trackWriter.writeBeginTrack(track, lastTrackPoint);
wroteTrack = true; wroteTrack = true;
} }
if (isSegmentValid) { boolean newSegment = TrackPoint.Type.SEGMENT_START_AUTOMATIC.equals(trackPoint.getType()) || TrackPoint.Type.SEGMENT_START_MANUAL.equals(trackPoint.getType());
if (!wroteSegment) { if (newSegment) {
// Start a segment if (wroteSegment) trackWriter.writeCloseSegment();
trackWriter.writeOpenSegment(); trackWriter.writeOpenSegment();
wroteSegment = true;
// Write the previous trackPoint, which we had previously skipped
trackWriter.writeTrackPoint(lastTrackPoint);
}
// Write the current trackPoint
trackWriter.writeTrackPoint(trackPoint); trackWriter.writeTrackPoint(trackPoint);
} else { wroteSegment = true;
if (wroteSegment) { continue;
trackWriter.writeCloseSegment();
wroteSegment = false;
}
} }
lastTrackPoint = trackPoint; if (TrackPoint.Type.SEGMENT_END_MANUAL.equals(trackPoint.getType())) {
isLastLocationValid = isLocationValid; if (!wroteSegment) trackWriter.writeOpenSegment();
trackWriter.writeTrackPoint(trackPoint);
trackWriter.writeCloseSegment();
wroteSegment = false;
continue;
}
trackWriter.writeTrackPoint(trackPoint);
} }
if (wroteSegment) { if (wroteSegment) {
// Should not be necessary as tracks should end with SEGMENT_END_MANUAL.
//Close the last segment //Close the last segment
trackWriter.writeCloseSegment(); trackWriter.writeCloseSegment();
} }
@@ -188,6 +179,7 @@ public class FileTrackExporter implements TrackExporter {
* @param trackPoint the trackPoint * @param trackPoint the trackPoint
* @param offset the time offset * @param offset the time offset
*/ */
//TODO Why?
private void setLocationTime(TrackPoint trackPoint, long offset) { private void setLocationTime(TrackPoint trackPoint, long offset) {
if (trackPoint != null) { if (trackPoint != null) {
trackPoint.setTime(trackPoint.getTime() - offset); trackPoint.setTime(trackPoint.getTime() - offset);
@@ -190,7 +190,9 @@ public class GpxTrackWriter implements TrackWriter {
@Override @Override
public void writeTrackPoint(TrackPoint trackPoint) { public void writeTrackPoint(TrackPoint trackPoint) {
if (printWriter != null) { if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(trackPoint.getLocation()) + ">"); String coordinates = trackPoint.hasLocation() ? " " + formatLocation(trackPoint.getLocation()) : "";
printWriter.println("<trkpt " + coordinates + ">");
if (trackPoint.hasAltitude()) { if (trackPoint.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(trackPoint.getAltitude()) + "</ele>"); printWriter.println("<ele>" + ELEVATION_FORMAT.format(trackPoint.getAltitude()) + "</ele>");
} }
@@ -22,6 +22,7 @@ import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.DescriptionGenerator; import de.dennisguse.opentracks.content.DescriptionGenerator;
@@ -239,6 +240,8 @@ public class KmlTrackWriter implements TrackWriter {
powerList.clear(); powerList.clear();
cadenceList.clear(); cadenceList.clear();
heartRateList.clear(); heartRateList.clear();
elevationGainList.clear();
elevationLossList.clear();
} }
} }
@@ -247,23 +250,23 @@ 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) { if (speedList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(speedList, EXTENDED_DATA_TYPE_SPEED); writeSimpleArrayData(speedList, EXTENDED_DATA_TYPE_SPEED);
} }
if (exportSensorData) { if (exportSensorData) {
if (powerList.size() > 0) { if (powerList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(powerList, EXTENDED_DATA_TYPE_POWER); writeSimpleArrayData(powerList, EXTENDED_DATA_TYPE_POWER);
} }
if (cadenceList.size() > 0) { if (cadenceList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(cadenceList, EXTENDED_DATA_TYPE_CADENCE); writeSimpleArrayData(cadenceList, EXTENDED_DATA_TYPE_CADENCE);
} }
if (heartRateList.size() > 0) { if (heartRateList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(heartRateList, EXTENDED_DATA_TYPE_HEART_RATE); writeSimpleArrayData(heartRateList, EXTENDED_DATA_TYPE_HEART_RATE);
} }
if (elevationGainList.size() > 0) { if (elevationGainList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(elevationGainList, EXTENDED_DATA_TYPE_ELEVATION_GAIN); writeSimpleArrayData(elevationGainList, EXTENDED_DATA_TYPE_ELEVATION_GAIN);
} }
if (elevationLossList.size() > 0) { if (elevationLossList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(elevationLossList, EXTENDED_DATA_TYPE_ELEVATION_LOSS); writeSimpleArrayData(elevationLossList, EXTENDED_DATA_TYPE_ELEVATION_LOSS);
} }
} }
@@ -280,28 +283,20 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.println("<when>" + getTime(trackPoint.getLocation()) + "</when>"); printWriter.println("<when>" + getTime(trackPoint.getLocation()) + "</when>");
} }
printWriter.println("<gx:coord>" + getCoordinates(trackPoint.getLocation(), " ") + "</gx:coord>"); if (trackPoint.hasLocation()) {
printWriter.println("<gx:coord>" + (trackPoint.hasLocation() ? getCoordinates(trackPoint.getLocation(), " ") : "") + "</gx:coord>");
if (trackPoint.hasSpeed()) { } else {
speedList.add(trackPoint.getSpeed()); printWriter.println("<gx:coord/>");
} }
speedList.add(trackPoint.hasSpeed() ? trackPoint.getSpeed() : null);
if (exportSensorData) { if (exportSensorData) {
if (trackPoint.hasHeartRate()) { heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate_bpm() : null);
heartRateList.add(trackPoint.getHeartRate_bpm()); cadenceList.add(trackPoint.hasCyclingCadence() ? trackPoint.getCyclingCadence_rpm() : null);
} powerList.add(trackPoint.hasPower() ? trackPoint.getPower() : null);
if (trackPoint.hasCyclingCadence()) {
cadenceList.add(trackPoint.getCyclingCadence_rpm()); elevationGainList.add(trackPoint.hasElevationGain() ? trackPoint.getElevationGain() : null);
} elevationLossList.add(trackPoint.hasElevationLoss() ? trackPoint.getElevationLoss() : null);
if (trackPoint.hasPower()) {
powerList.add(trackPoint.getPower());
}
if (trackPoint.hasElevationGain()) {
elevationGainList.add(trackPoint.getElevationGain());
}
if (trackPoint.hasElevationLoss()) {
elevationLossList.add(trackPoint.getElevationLoss());
}
} }
} }
} }
@@ -315,7 +310,12 @@ public class KmlTrackWriter implements TrackWriter {
private void writeSimpleArrayData(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>"); Float value = list.get(i);
if (value == null) {
printWriter.println("<gx:value />");
} else {
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>");
}
} }
printWriter.println("</gx:SimpleArrayData>"); printWriter.println("</gx:SimpleArrayData>");
} }
@@ -158,6 +158,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
TrackPoint trackPoint = null; TrackPoint trackPoint = null;
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(); TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
// TODO We are doing in memory processing for trackpoints; so we can do this in memory as well.
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) { try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
while (true) { while (true) {
@@ -190,7 +191,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
marker = null; marker = null;
} else { } else {
// The marker trackPoint time matches the track point time // The marker trackPoint time matches the track point time
if (!trackPoint.getType().hasLocation()) { if (!trackPoint.hasLocation()) {
// Invalid trackPoint, load the next trackPoint // Invalid trackPoint, load the next trackPoint
trackPoint = null; trackPoint = null;
continue; continue;
@@ -274,31 +275,43 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
trackIds.add(trackId); trackIds.add(trackId);
trackData.track.setId(trackId); trackData.track.setId(trackId);
flushLocations(trackData); flushTrackPoints();
} }
/**
* On track segment start.
*/
protected void onTrackSegmentStart() { protected void onTrackSegmentStart() {
trackData.numberOfSegments++; trackData.numberOfSegments++;
//If not the first segment, add a pause separator if there is at least one location in the last segment. //If not the first segment, add a pause separator if there is at least one TrackPoint in the last segment.
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) { if (trackData.numberOfSegments > 1
insertLocation(TrackPoint.createSegmentEndWithTime(trackData.lastLocationInCurrentSegment.getTime())); && trackData.lastLocationInCurrentSegment != null
&& (trackData.lastLocationInCurrentSegment.getType().equals(TrackPoint.Type.SEGMENT_START_MANUAL) || trackData.lastLocationInCurrentSegment.getType().equals(TrackPoint.Type.SEGMENT_START_AUTOMATIC))
) {
insertTrackPoint(TrackPoint.createSegmentEndWithTime(trackData.lastLocationInCurrentSegment.getTime()));
} }
trackData.lastLocationInCurrentSegment = null; trackData.lastLocationInCurrentSegment = null;
} }
protected void onTrackSegmentEnd() {
TrackPoint trackPoint = trackData.lastLocationInCurrentSegment;
if (trackPoint == null) {
return;
}
if (!trackPoint.hasLocation()) {
trackPoint.setType(TrackPoint.Type.SEGMENT_END_MANUAL);
}
}
protected void addMarker() throws SAXException { protected void addMarker() throws SAXException {
// Markers must have a time, else cannot match to the track points // Markers must have a time, else cannot match to the track points
if (time == null) { if (time == null) {
Log.w(TAG, "Marker without time ignored.");
return; return;
} }
TrackPoint trackPoint = createTrackPoint(); TrackPoint trackPoint = createTrackPoint();
if (!trackPoint.getType().hasLocation()) { if (!trackPoint.hasLocation()) {
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation()); Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation());
return; return;
} }
@@ -320,14 +333,8 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
markers.add(marker); markers.add(marker);
} }
/**
* Gets a track point.
*/
protected TrackPoint getTrackPoint() throws SAXException { protected TrackPoint getTrackPoint() throws SAXException {
TrackPoint trackPoint = createTrackPoint(); TrackPoint trackPoint = createTrackPoint();
if (trackPoint == null) {
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
}
// 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) {
@@ -344,34 +351,31 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* 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; if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
trackPoint.setSpeed((float) speed); double speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / duration;
trackPoint.setSpeed((float) speed);
}
}
}
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
long maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(context);
double distanceToLastTrackLocation = trackPoint.distanceTo(trackData.lastLocationInCurrentSegment);
if (distanceToLastTrackLocation > maxRecordingDistance) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
} }
} }
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
} }
if (!LocationUtils.isValidLocation(trackPoint.getLocation())) { if (trackPoint.hasLocation() && !LocationUtils.isValidLocation(trackPoint.getLocation())) {
throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint)); throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
} }
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) {
// If not the first segment, add a resume separator before adding the first location.
insertLocation(TrackPoint.createSegmentStartManualWithTime(trackPoint.getTime()));
}
trackData.lastLocationInCurrentSegment = trackPoint; trackData.lastLocationInCurrentSegment = trackPoint;
return trackPoint; return trackPoint;
} }
/**
* Inserts a track point.
*
* @param trackPoint the trackPoint
*/
protected void insertTrackPoint(TrackPoint trackPoint) {
insertLocation(trackPoint);
}
/** /**
* Creates an error message. * Creates an error message.
* *
@@ -411,11 +415,17 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* Creates a location. * Creates a location.
*/ */
private TrackPoint createTrackPoint() throws SAXException { private TrackPoint createTrackPoint() throws SAXException {
if (latitude == null || longitude == null) { TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT);
return null;
try {
trackPoint.setTime(StringUtils.parseTime(time));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
} }
TrackPoint trackPoint = new TrackPoint(); if (latitude == null || longitude == null) {
return trackPoint;
}
try { try {
trackPoint.setLatitude(Double.parseDouble(latitude)); trackPoint.setLatitude(Double.parseDouble(latitude));
@@ -424,11 +434,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e); throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
} }
try {
trackPoint.setTime(StringUtils.parseTime(time));
} catch (Exception e) {
throw new SAXException(createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
if (altitude != null) { if (altitude != null) {
try { try {
@@ -484,7 +489,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* *
* @param trackPoint the trackPoint * @param trackPoint the trackPoint
*/ */
private void insertLocation(TrackPoint trackPoint) { protected void insertTrackPoint(TrackPoint trackPoint) {
if (trackData.trackStatisticsUpdater == null) { if (trackData.trackStatisticsUpdater == null) {
trackData.trackStatisticsUpdater = new TrackStatisticsUpdater(); trackData.trackStatisticsUpdater = new TrackStatisticsUpdater();
} }
@@ -492,21 +497,20 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
trackData.trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval); trackData.trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
trackData.bufferedTrackPoints.add(trackPoint); trackData.bufferedTrackPoints.add(trackPoint);
trackData.numBufferedTrackPoints++; }
trackData.numberOfLocations++;
protected boolean isFirstTrackPointInSegment() {
return trackData.lastLocationInCurrentSegment == null;
} }
/** /**
* Flushes the locations to the database. * Flushes the TrackPoints to the database.
*
* @param data the track data
*/ */
private void flushLocations(TrackData data) { private void flushTrackPoints() {
if (data.numBufferedTrackPoints <= 0) { if (trackData.bufferedTrackPoints.size() > 0) {
return; contentProviderUtils.bulkInsertTrackPoint(trackData.bufferedTrackPoints, trackData.track.getId());
trackData.bufferedTrackPoints.clear();
} }
contentProviderUtils.bulkInsertTrackPoint(data.bufferedTrackPoints, data.track.getId());
data.numBufferedTrackPoints = 0;
} }
/** /**
@@ -532,16 +536,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
// The last location in the current segment; Null if the current segment doesn't have a last location // The last location in the current segment; Null if the current segment doesn't have a last location
TrackPoint lastLocationInCurrentSegment; TrackPoint lastLocationInCurrentSegment;
// The number of locations processed for the current track
int numberOfLocations = 0;
// The TrackStatisticsUpdater for the current track // The TrackStatisticsUpdater for the current track
TrackStatisticsUpdater trackStatisticsUpdater; TrackStatisticsUpdater trackStatisticsUpdater;
// The buffered locations // The buffered locations
final List<TrackPoint> bufferedTrackPoints = new ArrayList<>(); final List<TrackPoint> bufferedTrackPoints = new ArrayList<>();
// The number of buffered locations
int numBufferedTrackPoints = 0;
} }
} }
@@ -101,6 +101,9 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
case TAG_TRACK: case TAG_TRACK:
onTrackEnd(); onTrackEnd();
break; break;
case TAG_TRACK_SEGMENT:
onTrackSegmentEnd();
break;
case TAG_TRACK_POINT: case TAG_TRACK_POINT:
onTrackPointEnd(); onTrackPointEnd();
break; break;
@@ -193,13 +196,13 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
loss = null; loss = null;
} }
/**
* On track point end.
*/
private void onTrackPointEnd() throws SAXException { private void onTrackPointEnd() throws SAXException {
boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
TrackPoint trackPoint = getTrackPoint(); TrackPoint trackPoint = getTrackPoint();
if (trackPoint == null) { if (isFirstTrackPointInSegment) {
return; TrackPoint.Type type = !trackPoint.hasLocation() ? TrackPoint.Type.SEGMENT_START_MANUAL : TrackPoint.Type.SEGMENT_START_AUTOMATIC;
trackPoint.setType(type);
} }
insertTrackPoint(trackPoint); insertTrackPoint(trackPoint);
} }
@@ -159,6 +159,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
if (content != null) { if (content != null) {
time = content.trim(); time = content.trim();
} }
break; break;
case TAG_STYLE_URL: case TAG_STYLE_URL:
if (content != null) { if (content != null) {
@@ -221,10 +222,8 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
elevationLossList.clear(); elevationLossList.clear();
} }
/** protected void onTrackSegmentEnd() {
* On track segment end. super.onTrackSegmentEnd();
*/
private void onTrackSegmentEnd() {
// Close a track segment by inserting the segment locations // Close a track segment by inserting the segment locations
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);
@@ -256,24 +255,32 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
* On track point end. gx:coord end tag. * On track point end. gx:coord end tag.
*/ */
private void onTrackPointEnd() throws SAXException { private void onTrackPointEnd() throws SAXException {
// Add location to locationList // Add trackPoint to trackPoints
if (content == null) { if (content == null) {
return; return;
} }
String[] parts = content.trim().split(" "); String[] parts = content.trim().split(" ");
if (parts.length != 2 && parts.length != 3) { if (parts.length == 2 || parts.length == 3) {
return; longitude = parts[0];
latitude = parts[1];
altitude = parts.length == 3 ? parts[2] : null;
} }
longitude = parts[0];
latitude = parts[1];
altitude = parts.length == 3 ? parts[2] : null;
TrackPoint location = getTrackPoint(); // Similar to GPX
if (location == null) { boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment();
return; TrackPoint trackPoint = getTrackPoint();
if (isFirstTrackPointInSegment) {
TrackPoint.Type type = !trackPoint.hasLocation() ? TrackPoint.Type.SEGMENT_START_MANUAL : TrackPoint.Type.SEGMENT_START_AUTOMATIC;
trackPoint.setType(type);
} }
trackPoints.add(location); trackPoints.add(trackPoint);
// Reset variables for next trackpoint (which might not have such data).
time = null; time = null;
longitude = null;
latitude = null;
altitude = null;
} }
/** /**
@@ -287,18 +294,16 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
* On extended data value end. gx:value end tag. * On extended data value end. gx:value end tag.
*/ */
private void onExtendedDataValueEnd() throws SAXException { private void onExtendedDataValueEnd() throws SAXException {
if (content == null) { Float value = null;
return; if (content != null) {
} content = content.trim();
content = content.trim(); if (!content.equals("")) {
if (content.equals("")) { try {
return; value = Float.parseFloat(content);
} } catch (NumberFormatException e) {
float value; throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
try { }
value = Float.parseFloat(content); }
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
} }
switch (extendedDataType) { switch (extendedDataType) {
case KmlTrackWriter.EXTENDED_DATA_TYPE_SPEED: case KmlTrackWriter.EXTENDED_DATA_TYPE_SPEED:
@@ -31,7 +31,7 @@ public class IntervalStatistics {
TrackPoint prevTrackPoint = trackPoints.get(i - 1); TrackPoint prevTrackPoint = trackPoints.get(i - 1);
TrackPoint trackPoint = trackPoints.get(i); TrackPoint trackPoint = trackPoints.get(i);
if (trackPoint.getType().hasLocation() && prevTrackPoint.getType().hasLocation()) { if (trackPoint.hasLocation() && prevTrackPoint.hasLocation()) {
interval.distance_m += prevTrackPoint.distanceTo(trackPoint); interval.distance_m += prevTrackPoint.distanceTo(trackPoint);
interval.time_ms += trackPoint.getTime() - prevTrackPoint.getTime(); interval.time_ms += trackPoint.getTime() - prevTrackPoint.getTime();
interval.gain_m += trackPoint.hasElevationGain() ? trackPoint.getElevationGain() : 0; interval.gain_m += trackPoint.hasElevationGain() ? trackPoint.getElevationGain() : 0;