From ab47320216b801abff1be69c3fea930cdf8254d4 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Mon, 28 Dec 2020 21:28:39 +0100 Subject: [PATCH] Segments: import/export is working incl. restoring TrackStatistics. KML exports sensor data if actually present. #316. --- .../opentracks/content/data/TestDataUtil.java | 54 ++++- .../CustomContentProviderUtilsTest.java | 4 +- .../fragments/ChartFragmentTest.java | 8 +- .../io/file/exporter/KmlTrackWriterTest.java | 53 +++++ .../io/file/importer/ExportImportTest.java | 185 ++++++++++++++---- .../io/file/importer/LegacyKmlImportTest.java | 18 +- .../services/handlers/HandlerServerTest.java | 2 +- .../opentracks/content/data/Marker.java | 2 + .../opentracks/content/data/TrackPoint.java | 148 ++++++++------ .../provider/ContentProviderUtils.java | 16 +- .../content/provider/TrackPointIterator.java | 2 +- .../StatisticsRecordingFragment.java | 2 +- .../io/file/exporter/FileTrackExporter.java | 56 +++--- .../io/file/exporter/GpxTrackWriter.java | 4 +- .../io/file/exporter/KmlTrackWriter.java | 52 ++--- .../importer/AbstractFileTrackImporter.java | 112 ++++++----- .../file/importer/GpxFileTrackImporter.java | 13 +- .../file/importer/KmlFileTrackImporter.java | 57 +++--- .../viewmodels/IntervalStatistics.java | 2 +- 19 files changed, 506 insertions(+), 284 deletions(-) create mode 100644 src/androidTest/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriterTest.java 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 341738364..4eb7abb14 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java +++ b/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java @@ -38,6 +38,7 @@ public class TestDataUtil { * @param trackId the trackId of 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> createTrack(Track.Id trackId, int numPoints) { Track track = createTrack(trackId); @@ -49,6 +50,51 @@ public class TestDataUtil { return new Pair<>(track, trackPoints); } + + public static TrackData createTestingTrack(Track.Id trackId) { + Track track = createTrack(trackId); + + int i = 0; + List 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 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 trackPoints; + public final List markers; + + public TrackData(Track track, List trackPoints, List markers) { + this.track = track; + this.trackPoints = trackPoints; + this.markers = markers; + } + } + + public static Track createTrackAndInsert(ContentProviderUtils contentProviderUtils, Track.Id trackId, int numPoints) { Pair> pair = createTrack(trackId, numPoints); @@ -63,7 +109,7 @@ public class TestDataUtil { * @param i the index for the TrackPoint. */ 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.setLongitude(INITIAL_LONGITUDE - (double) i / 10000.0); trackPoint.setAccuracy((float) i / 100.0f); @@ -79,6 +125,12 @@ public class TestDataUtil { 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. * diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java index c8b8f04e5..0f5990e4e 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java @@ -270,7 +270,7 @@ public class CustomContentProviderUtilsTest { Track.Id trackId2 = new Track.Id(random + 1); 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, trackId3, 10); @@ -282,7 +282,7 @@ public class CustomContentProviderUtilsTest { Cursor tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, TracksColumns._ID); assertEquals(3, tracksCursor.getCount()); 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); assertEquals(1, markerCursor.getCount()); // Delete one track. diff --git a/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java b/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java index ee0e0b6db..0351c4da4 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java @@ -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. */ trackPoint2.setTime(trackPoint1.getTime() + 222); - trackPoint2.setSpeed(130); + trackPoint2.setSpeed(130f); point = chartFragment.createPendingPoint(trackPoint2); 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 */ trackPoint2.setTime(trackPoint2.getTime() + 222); - trackPoint2.setSpeed(102); + trackPoint2.setSpeed(102f); point = chartFragment.createPendingPoint(trackPoint2); 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 */ trackPoint2.setTime(trackPoint2.getTime() + 222); - trackPoint2.setSpeed(102); + trackPoint2.setSpeed(102f); point = chartFragment.createPendingPoint(trackPoint2); 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() { chartFragment.setReportSpeed(false); TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint(); - trackPoint.setSpeed(0); + trackPoint.setSpeed(0f); ChartPoint point = chartFragment.createPendingPoint(trackPoint); assertEquals(0.0, point.getPace(), 0.01); } diff --git a/src/androidTest/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriterTest.java b/src/androidTest/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriterTest.java new file mode 100644 index 000000000..b2ef08a6f --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriterTest.java @@ -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 = "1970-01-01T00:00:00Z\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\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()); + } +} \ No newline at end of file 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 4da7ff529..4d9aa756f 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 @@ -1,15 +1,21 @@ package de.dennisguse.opentracks.io.file.importer; import android.content.Context; +import android.content.Intent; +import android.location.Location; +import android.os.Looper; import android.util.Log; -import android.util.Pair; import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.LargeTest; +import androidx.test.rule.GrantPermissionRule; +import androidx.test.rule.ServiceTestRule; import org.junit.After; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -19,15 +25,18 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import de.dennisguse.opentracks.R; 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.TrackPoint; import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.io.file.TrackFileFormat; 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.util.PreferencesUtils; @@ -45,6 +54,20 @@ public class ExportImportTest { 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 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_DESCRIPTION = "the description"; - private final List markers = new ArrayList<>(); - private final List trackPoints = new ArrayList<>(); + private Track track; + private List markers = new ArrayList<>(); + private List trackPoints = new ArrayList<>(); + private Track.Id trackId; private Track.Id importTrackId; - private final Track.Id trackId = new Track.Id(System.currentTimeMillis()); @Before - public void setUp() { - Pair> track = TestDataUtil.createTrack(trackId, 10); - 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()); + public void setUp() throws TimeoutException, InterruptedException { + service = ((TrackRecordingServiceInterface) mServiceRule.bindService(new Intent(context, TrackRecordingService.class))); - trackPoints.clear(); - trackPoints.addAll(track.second); + trackId = service.startNewTrack(); - for (int i = 0; i < 3; i++) { - Marker marker = new Marker(trackId, track.second.get(i).getLocation()); - marker.setName("the marker " + i); - marker.setDescription("the marker description " + i); - marker.setCategory("the marker category" + i); - marker.setIcon("the marker icon" + i); - marker.setPhotoUrl("the photo url" + i); - contentProviderUtils.insertMarker(marker); + service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 0, 66, 3, 50), 0); + service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null); + service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50), 0); + service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50), 0); + service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null); + Thread.sleep(10); + service.pauseCurrentTrack(); - 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 @@ -94,6 +124,7 @@ public class ExportImportTest { } } + @Ignore("Not implemented") @LargeTest @Test public void kml_only_track() { @@ -130,11 +161,14 @@ public class ExportImportTest { assertEquals(track.getIcon(), importedTrack.getIcon()); assertEquals(track.getUuid(), importedTrack.getUuid()); - // 2. markers - assertMarkers(); - - // 3. trackpoints + // 2. trackpoints assertTrackpoints(false, false, false, false, false); + + // 3. trackstatistics + assertTrackStatistics(); + + // 4. markers + assertMarkers(); } @LargeTest @@ -165,11 +199,14 @@ public class ExportImportTest { assertEquals(track.getName(), importedTrack.getName()); assertEquals(track.getIcon(), importedTrack.getIcon()); - // 2. markers - assertMarkers(); - - // 3. trackpoints + // 2. trackpoints assertTrackpoints(true, true, true, true, true); + + // 2. trackstatistics + assertTrackStatistics(); + + // 4. markers + assertMarkers(); } @LargeTest @@ -258,11 +295,14 @@ public class ExportImportTest { //TODO exporting and importing a track icon is not yet supported by GpxTrackWriter. //assertEquals(track.getIcon(), trackImported.getIcon()); - // 2. markers - assertMarkers(); - - // 3. trackpoints + // 2. trackpoints assertTrackpoints(false, true, true, true, true); + + // 3. trackstatistics + assertTrackStatistics(); + + // 4. markers + assertMarkers(); } @LargeTest @@ -318,14 +358,31 @@ public class ExportImportTest { TrackPoint importedTrackPoint = importedTrackPoints.get(i); assertEquals(trackPoint.getTime(), importedTrackPoint.getTime(), 0.01); + TrackPoint.Type type = trackPoint.getType(); + assertEquals("" + i, trackPoint.getType(), importedTrackPoint.getType()); // TODO Not exported for GPX/KML // assertEquals(trackPoint.getAccuracy(), importedTrackPoint.getAccuracy(), 0.01); - assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 0.001); - assertEquals(trackPoint.getLongitude(), importedTrackPoint.getLongitude(), 0.001); - assertEquals(trackPoint.getAltitude(), importedTrackPoint.getAltitude(), 0.001); - assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001); + assertEquals(trackPoint.hasLocation(), importedTrackPoint.hasLocation()); + if (trackPoint.hasLocation()) { + assertEquals(trackPoint.getLatitude(), importedTrackPoint.getLatitude(), 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) { 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; + } } \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/LegacyKmlImportTest.java b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/LegacyKmlImportTest.java index f969fb6af..765d08cd8 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/LegacyKmlImportTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/LegacyKmlImportTest.java @@ -65,25 +65,19 @@ public class LegacyKmlImportTest { // 3. trackpoints List importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId); - assertEquals(8, importedTrackPoints.size()); // 6 trackpoints + segmentEnd and segmentStart + assertEquals(6, importedTrackPoints.size()); // 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(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); - // 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 - assertTrackpoint(importedTrackPoints.get(4), "2020-11-28T17:06:55.861Z", 0.0, 0.0, 0.0); - assertEquals(-2, importedTrackPoints.get(4).getType().type_db); - - // last 3 trackpoints - 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); + assertEquals(TrackPoint.Type.SEGMENT_START_AUTOMATIC, importedTrackPoints.get(3).getType()); + 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); + assertTrackpoint(importedTrackPoints.get(5), "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) { diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/handlers/HandlerServerTest.java b/src/androidTest/java/de/dennisguse/opentracks/services/handlers/HandlerServerTest.java index c04b5a005..a449e1325 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/services/handlers/HandlerServerTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/services/handlers/HandlerServerTest.java @@ -50,7 +50,7 @@ public class HandlerServerTest { @Test public void sendTrackPoint() throws InterruptedException { // given - TrackPoint trackPoint = new TrackPoint(); + TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT); int accuracy = 50; // when diff --git a/src/main/java/de/dennisguse/opentracks/content/data/Marker.java b/src/main/java/de/dennisguse/opentracks/content/data/Marker.java index ee7ced8bb..6944763a3 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/Marker.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/Marker.java @@ -44,6 +44,8 @@ public final class Marker { //TODO It is the distance from the track starting point; rename to something more meaningful private double length = 0.0; private long duration = 0; + + @Deprecated //TODO Replace by storing latitude/longitude directly. private final Location location; @Deprecated //TODO Make an URI instead of String private String photoUrl = ""; diff --git a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java index a79523aed..0c738f284 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java @@ -16,7 +16,6 @@ package de.dennisguse.opentracks.content.data; import android.location.Location; -import android.location.LocationManager; import android.os.Parcel; import androidx.annotation.NonNull; @@ -40,21 +39,25 @@ import java.util.Objects; * * @author Sandor Dornbush */ -//TODO Merge constructors by use case; we have too many. public class TrackPoint { 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 { - 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 - 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; @@ -64,7 +67,7 @@ public class TrackPoint { @Override public String toString() { - return "" + type_db; + return name() + "(" + type_db + ")"; } public static Type getById(int id) { @@ -74,10 +77,6 @@ public class TrackPoint { throw new RuntimeException("unknown id: " + id); } - - public boolean hasLocation() { - return this == SEGMENT_START_AUTOMATIC || this == TRACKPOINT; - } } @NonNull @@ -89,35 +88,33 @@ public class TrackPoint { private Float elevationGain = null; private Float elevationLoss = null; - public TrackPoint() { - this(Type.TRACKPOINT, new Location("")); - } - public TrackPoint(@NonNull Type type) { this.type = type; - this.location = new Location(""); } public TrackPoint(@NonNull Location location) { - this.type = Type.TRACKPOINT; - this.location = location; + this(Type.TRACKPOINT); + + 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()); } - public TrackPoint(@NonNull Type type, @NonNull Location location) { - this.type = type; - this.location = location; + public TrackPoint(@NonNull Type type, long time_ms) { + this(type); + this.time_ms = time_ms; } public TrackPoint(double latitude, double longitude, Double altitude, long time) { - this.type = Type.TRACKPOINT; - location = new Location(LocationManager.GPS_PROVIDER); - location.setLatitude(latitude); - location.setLongitude(longitude); - if (altitude != null) { - location.setAltitude(altitude); - } - location.setTime(time); + this(Type.TRACKPOINT); + this.latitude = latitude; + this.longitude = longitude; + this.altitude_m = altitude; + this.time_ms = time; } @Deprecated //See #316 @@ -126,9 +123,7 @@ public class TrackPoint { } public static TrackPoint createSegmentStartManualWithTime(long time) { - Location resume = new Location(LocationManager.GPS_PROVIDER); - resume.setTime(time); - return new TrackPoint(Type.SEGMENT_START_MANUAL, resume); + return new TrackPoint(Type.SEGMENT_START_MANUAL, time); } @Deprecated //See #316 @@ -137,9 +132,7 @@ public class TrackPoint { } public static TrackPoint createSegmentStartAutomaticWithTime(long time) { - Location resume = new Location(LocationManager.GPS_PROVIDER); - resume.setTime(time); - return new TrackPoint(Type.SEGMENT_START_AUTOMATIC, resume); + return new TrackPoint(Type.SEGMENT_START_AUTOMATIC, time); } public static TrackPoint createSegmentEnd() { @@ -151,9 +144,7 @@ public class TrackPoint { } public static TrackPoint createSegmentEndWithTime(long time) { - Location pause = new Location(LocationManager.GPS_PROVIDER); - pause.setTime(time); - return new TrackPoint(Type.SEGMENT_END_MANUAL, pause); + return new TrackPoint(Type.SEGMENT_END_MANUAL, time); } @NonNull @@ -185,8 +176,28 @@ public class TrackPoint { this.id = id; } + public boolean hasLocation() { + return latitude != null || longitude != null; + } + @Nullable 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; } @@ -194,7 +205,7 @@ public class TrackPoint { return elevationGain != null; } - public float getElevationGain() { + public Float getElevationGain() { return elevationGain; } @@ -215,87 +226,87 @@ public class TrackPoint { } public double getLatitude() { - return location.getLatitude(); + return latitude; } public void setLatitude(double latitude) { - location.setLatitude(latitude); + this.latitude = latitude; } public double getLongitude() { - return location.getLongitude(); + return longitude; } public void setLongitude(double longitude) { - location.setLongitude(longitude); + this.longitude = longitude; } public long getTime() { - return location.getTime(); + return time_ms; } public void setTime(long time) { - location.setTime(time); + this.time_ms = time; } public boolean hasAltitude() { - return location.hasAltitude(); + return altitude_m != null; } public double getAltitude() { - return location.getAltitude(); + return altitude_m; } public void setAltitude(double altitude) { - location.setAltitude(altitude); + this.altitude_m = altitude; } public boolean hasSpeed() { - return location.hasSpeed(); + return speed_mps != null; } public float getSpeed() { - return location.getSpeed(); + return speed_mps; } - public void setSpeed(float speed) { - location.setSpeed(speed); + public void setSpeed(Float speed) { + this.speed_mps = speed; } public boolean hasBearing() { - return location.hasBearing(); + return bearing != null; } public float getBearing() { - return location.getBearing(); + return bearing; } - public void setBearing(float bearing) { - location.setBearing(bearing); + public void setBearing(Float bearing) { + this.bearing = bearing; } public boolean hasAccuracy() { - return location.hasAccuracy(); + return accuracy != null; } public float getAccuracy() { - return location.getAccuracy(); + return accuracy; } public void setAccuracy(float horizontalAccuracy) { - location.setAccuracy(horizontalAccuracy); + this.accuracy = horizontalAccuracy; } public float distanceTo(@NonNull TrackPoint dest) { - return location.distanceTo(dest.getLocation()); + return getLocation().distanceTo(dest.getLocation()); } public float bearingTo(@NonNull TrackPoint dest) { - return location.bearingTo(dest.getLocation()); + return getLocation().bearingTo(dest.getLocation()); } public float bearingTo(@NonNull Location dest) { - return location.bearingTo(dest); + return getLocation().bearingTo(dest); } // Sensor data @@ -342,7 +353,16 @@ public class TrackPoint { @NonNull @Override 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 { diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java index 4acb38a55..ffc736d1a 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java @@ -543,9 +543,7 @@ public class ContentProviderUtils { */ static TrackPoint fillTrackPoint(Cursor cursor, CachedTrackPointsIndexes indexes) { 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)) { trackPoint.setLongitude(((double) cursor.getInt(indexes.longitudeIndex)) / 1E6); @@ -694,7 +692,7 @@ public class ContentProviderUtils { */ @Deprecated 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())}; return findTrackPointBy(selection, selectionArgs); } @@ -719,12 +717,12 @@ public class ContentProviderUtils { private ContentValues createContentValues(TrackPoint trackPoint, Track.Id trackId) { ContentValues values = new ContentValues(); values.put(TrackPointsColumns.TRACKID, trackId.getId()); - if (trackPoint.getType() != null) { - values.put(TrackPointsColumns.TYPE, trackPoint.getType().type_db); - } - values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.getLongitude() * 1E6)); - values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getLatitude() * 1E6)); + values.put(TrackPointsColumns.TYPE, trackPoint.getType().type_db); + 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()); if (trackPoint.hasAltitude()) { values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude()); diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/TrackPointIterator.java b/src/main/java/de/dennisguse/opentracks/content/provider/TrackPointIterator.java index a5dcd1e80..515c1ab92 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/TrackPointIterator.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/TrackPointIterator.java @@ -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. */ +//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, AutoCloseable { private static final String TAG = TrackPointIterator.class.getSimpleName(); @@ -22,7 +23,6 @@ public class TrackPointIterator implements Iterator, AutoCloseable { private TrackPoint.Id lastTrackPointId = null; private Cursor cursor; - public TrackPointIterator(ContentProviderUtils contentProviderUtils, Track.Id trackId, TrackPoint.Id startTrackPointId) { this.contentProviderUtils = contentProviderUtils; this.trackId = trackId; diff --git a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java index ebd34b7a5..7e9d88c0e 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java @@ -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. - if (trackPoint != null && trackPoint.getType().hasLocation()) { + if (trackPoint != null && trackPoint.hasLocation()) { boolean hasFix = !LocationUtils.isTrackPointOld(trackPoint); if (!hasFix) { diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/FileTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/FileTrackExporter.java index 6f40c7049..1b57174bd 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/FileTrackExporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/FileTrackExporter.java @@ -45,8 +45,8 @@ public class FileTrackExporter implements TrackExporter { /** * Constructor. * - * @param contentProviderUtils the content provider utils - * @param trackWriter the track writer + * @param contentProviderUtils the content provider utils + * @param trackWriter the track writer */ public FileTrackExporter(ContentProviderUtils contentProviderUtils, TrackWriter trackWriter) { this.contentProviderUtils = contentProviderUtils; @@ -70,6 +70,7 @@ public class FileTrackExporter implements TrackExporter { if (hasMultipleTracks) { trackWriter.writeMultiTrackBegin(); } + //TODO Why use startTime of first track for the others? long startTime = tracks[0].getTrackStatistics().getStartTime_ms(); for (Track track : tracks) { 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 { boolean wroteTrack = false; boolean wroteSegment = false; - boolean isLastLocationValid = false; - TrackPoint lastTrackPoint = null; try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) { - while (trackPointIterator.hasNext()) { - if (Thread.interrupted()) { - throw new InterruptedException(); - } - TrackPoint trackPoint = trackPointIterator.next(); + if (Thread.interrupted()) throw new InterruptedException(); + TrackPoint trackPoint = trackPointIterator.next(); setLocationTime(trackPoint, offset); - boolean isLocationValid = trackPoint.getType().hasLocation(); - boolean isSegmentValid = isLocationValid && isLastLocationValid; - if (!wroteTrack && isSegmentValid) { - // Found the first two consecutive locations that are valid - trackWriter.writeBeginTrack(track, lastTrackPoint); + if (!wroteTrack) { + trackWriter.writeBeginTrack(track, trackPoint); wroteTrack = true; } - if (isSegmentValid) { - if (!wroteSegment) { - // Start a segment - trackWriter.writeOpenSegment(); - wroteSegment = true; - - // Write the previous trackPoint, which we had previously skipped - trackWriter.writeTrackPoint(lastTrackPoint); - } - - // Write the current trackPoint + boolean newSegment = TrackPoint.Type.SEGMENT_START_AUTOMATIC.equals(trackPoint.getType()) || TrackPoint.Type.SEGMENT_START_MANUAL.equals(trackPoint.getType()); + if (newSegment) { + if (wroteSegment) trackWriter.writeCloseSegment(); + trackWriter.writeOpenSegment(); trackWriter.writeTrackPoint(trackPoint); - } else { - if (wroteSegment) { - trackWriter.writeCloseSegment(); - wroteSegment = false; - } + wroteSegment = true; + continue; } - lastTrackPoint = trackPoint; - isLastLocationValid = isLocationValid; + if (TrackPoint.Type.SEGMENT_END_MANUAL.equals(trackPoint.getType())) { + if (!wroteSegment) trackWriter.writeOpenSegment(); + trackWriter.writeTrackPoint(trackPoint); + trackWriter.writeCloseSegment(); + wroteSegment = false; + continue; + } + + trackWriter.writeTrackPoint(trackPoint); } if (wroteSegment) { + // Should not be necessary as tracks should end with SEGMENT_END_MANUAL. //Close the last segment trackWriter.writeCloseSegment(); } @@ -188,6 +179,7 @@ public class FileTrackExporter implements TrackExporter { * @param trackPoint the trackPoint * @param offset the time offset */ + //TODO Why? private void setLocationTime(TrackPoint trackPoint, long offset) { if (trackPoint != null) { trackPoint.setTime(trackPoint.getTime() - offset); 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 24902bcee..74b0d6a26 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 @@ -190,7 +190,9 @@ public class GpxTrackWriter implements TrackWriter { @Override public void writeTrackPoint(TrackPoint trackPoint) { if (printWriter != null) { - printWriter.println(""); + String coordinates = trackPoint.hasLocation() ? " " + formatLocation(trackPoint.getLocation()) : ""; + printWriter.println(""); + if (trackPoint.hasAltitude()) { printWriter.println("" + ELEVATION_FORMAT.format(trackPoint.getAltitude()) + ""); } 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 238edad5c..9db9f3765 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 @@ -22,6 +22,7 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.content.DescriptionGenerator; @@ -239,6 +240,8 @@ public class KmlTrackWriter implements TrackWriter { powerList.clear(); cadenceList.clear(); heartRateList.clear(); + elevationGainList.clear(); + elevationLossList.clear(); } } @@ -247,23 +250,23 @@ public class KmlTrackWriter implements TrackWriter { if (printWriter != null) { printWriter.println(""); printWriter.println(""); - if (speedList.size() > 0) { + if (speedList.stream().anyMatch(Objects::nonNull)) { writeSimpleArrayData(speedList, EXTENDED_DATA_TYPE_SPEED); } if (exportSensorData) { - if (powerList.size() > 0) { + if (powerList.stream().anyMatch(Objects::nonNull)) { writeSimpleArrayData(powerList, EXTENDED_DATA_TYPE_POWER); } - if (cadenceList.size() > 0) { + if (cadenceList.stream().anyMatch(Objects::nonNull)) { writeSimpleArrayData(cadenceList, EXTENDED_DATA_TYPE_CADENCE); } - if (heartRateList.size() > 0) { + if (heartRateList.stream().anyMatch(Objects::nonNull)) { writeSimpleArrayData(heartRateList, EXTENDED_DATA_TYPE_HEART_RATE); } - if (elevationGainList.size() > 0) { + if (elevationGainList.stream().anyMatch(Objects::nonNull)) { writeSimpleArrayData(elevationGainList, EXTENDED_DATA_TYPE_ELEVATION_GAIN); } - if (elevationLossList.size() > 0) { + if (elevationLossList.stream().anyMatch(Objects::nonNull)) { writeSimpleArrayData(elevationLossList, EXTENDED_DATA_TYPE_ELEVATION_LOSS); } } @@ -280,28 +283,20 @@ public class KmlTrackWriter implements TrackWriter { printWriter.println("" + getTime(trackPoint.getLocation()) + ""); } - printWriter.println("" + getCoordinates(trackPoint.getLocation(), " ") + ""); - - if (trackPoint.hasSpeed()) { - speedList.add(trackPoint.getSpeed()); + if (trackPoint.hasLocation()) { + printWriter.println("" + (trackPoint.hasLocation() ? getCoordinates(trackPoint.getLocation(), " ") : "") + ""); + } else { + printWriter.println(""); } + speedList.add(trackPoint.hasSpeed() ? trackPoint.getSpeed() : null); if (exportSensorData) { - if (trackPoint.hasHeartRate()) { - heartRateList.add(trackPoint.getHeartRate_bpm()); - } - if (trackPoint.hasCyclingCadence()) { - cadenceList.add(trackPoint.getCyclingCadence_rpm()); - } - if (trackPoint.hasPower()) { - powerList.add(trackPoint.getPower()); - } - if (trackPoint.hasElevationGain()) { - elevationGainList.add(trackPoint.getElevationGain()); - } - if (trackPoint.hasElevationLoss()) { - elevationLossList.add(trackPoint.getElevationLoss()); - } + heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate_bpm() : null); + cadenceList.add(trackPoint.hasCyclingCadence() ? trackPoint.getCyclingCadence_rpm() : null); + powerList.add(trackPoint.hasPower() ? trackPoint.getPower() : null); + + elevationGainList.add(trackPoint.hasElevationGain() ? trackPoint.getElevationGain() : null); + elevationLossList.add(trackPoint.hasElevationLoss() ? trackPoint.getElevationLoss() : null); } } } @@ -315,7 +310,12 @@ public class KmlTrackWriter implements TrackWriter { private void writeSimpleArrayData(List list, String name) { printWriter.println(""); for (int i = 0; i < list.size(); i++) { - printWriter.println("" + list.get(i) + ""); + Float value = list.get(i); + if (value == null) { + printWriter.println(""); + } else { + printWriter.println("" + list.get(i) + ""); + } } 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 75ec84846..7409c6fda 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 @@ -158,6 +158,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track TrackPoint trackPoint = null; 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)) { while (true) { @@ -190,7 +191,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track marker = null; } else { // The marker trackPoint time matches the track point time - if (!trackPoint.getType().hasLocation()) { + if (!trackPoint.hasLocation()) { // Invalid trackPoint, load the next trackPoint trackPoint = null; continue; @@ -274,31 +275,43 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track trackIds.add(trackId); trackData.track.setId(trackId); - flushLocations(trackData); + flushTrackPoints(); } - /** - * On track segment start. - */ protected void onTrackSegmentStart() { trackData.numberOfSegments++; - //If not the first segment, add a pause separator if there is at least one location in the last segment. - if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) { - insertLocation(TrackPoint.createSegmentEndWithTime(trackData.lastLocationInCurrentSegment.getTime())); + //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 + && (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; } + 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 { // Markers must have a time, else cannot match to the track points if (time == null) { + Log.w(TAG, "Marker without time ignored."); return; } TrackPoint trackPoint = createTrackPoint(); - if (!trackPoint.getType().hasLocation()) { + if (!trackPoint.hasLocation()) { Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getLocation()); return; } @@ -320,14 +333,8 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track markers.add(marker); } - /** - * Gets a track point. - */ protected TrackPoint getTrackPoint() throws SAXException { TrackPoint trackPoint = createTrackPoint(); - if (trackPoint == null) { - throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint)); - } // Calculate derived attributes from the previous point 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. */ double duration = timeDifference * UnitConversions.MS_TO_S; - double speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / duration; - trackPoint.setSpeed((float) speed); + if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) { + 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)); } - 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; return trackPoint; } - /** - * Inserts a track point. - * - * @param trackPoint the trackPoint - */ - protected void insertTrackPoint(TrackPoint trackPoint) { - insertLocation(trackPoint); - } - /** * Creates an error message. * @@ -411,11 +415,17 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track * Creates a location. */ private TrackPoint createTrackPoint() throws SAXException { - if (latitude == null || longitude == null) { - return null; + TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT); + + 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 { 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); } - 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) { try { @@ -484,7 +489,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track * * @param trackPoint the trackPoint */ - private void insertLocation(TrackPoint trackPoint) { + protected void insertTrackPoint(TrackPoint trackPoint) { if (trackData.trackStatisticsUpdater == null) { trackData.trackStatisticsUpdater = new TrackStatisticsUpdater(); } @@ -492,21 +497,20 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track trackData.trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval); trackData.bufferedTrackPoints.add(trackPoint); - trackData.numBufferedTrackPoints++; - trackData.numberOfLocations++; + } + + protected boolean isFirstTrackPointInSegment() { + return trackData.lastLocationInCurrentSegment == null; } /** - * Flushes the locations to the database. - * - * @param data the track data + * Flushes the TrackPoints to the database. */ - private void flushLocations(TrackData data) { - if (data.numBufferedTrackPoints <= 0) { - return; + private void flushTrackPoints() { + if (trackData.bufferedTrackPoints.size() > 0) { + 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 TrackPoint lastLocationInCurrentSegment; - // The number of locations processed for the current track - int numberOfLocations = 0; - // The TrackStatisticsUpdater for the current track TrackStatisticsUpdater trackStatisticsUpdater; // The buffered locations final List bufferedTrackPoints = new ArrayList<>(); - - // The number of buffered locations - int numBufferedTrackPoints = 0; } } 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 807979ddf..65937b02a 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 @@ -101,6 +101,9 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter { case TAG_TRACK: onTrackEnd(); break; + case TAG_TRACK_SEGMENT: + onTrackSegmentEnd(); + break; case TAG_TRACK_POINT: onTrackPointEnd(); break; @@ -193,13 +196,13 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter { loss = null; } - /** - * On track point end. - */ private void onTrackPointEnd() throws SAXException { + boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment(); TrackPoint trackPoint = getTrackPoint(); - if (trackPoint == null) { - return; + if (isFirstTrackPointInSegment) { + TrackPoint.Type type = !trackPoint.hasLocation() ? TrackPoint.Type.SEGMENT_START_MANUAL : TrackPoint.Type.SEGMENT_START_AUTOMATIC; + + trackPoint.setType(type); } insertTrackPoint(trackPoint); } 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 759007b59..f5c067d13 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 @@ -159,6 +159,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { if (content != null) { time = content.trim(); } + break; case TAG_STYLE_URL: if (content != null) { @@ -221,10 +222,8 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { elevationLossList.clear(); } - /** - * On track segment end. - */ - private void onTrackSegmentEnd() { + protected void onTrackSegmentEnd() { + super.onTrackSegmentEnd(); // Close a track segment by inserting the segment locations for (int i = 0; i < trackPoints.size(); i++) { TrackPoint trackPoint = trackPoints.get(i); @@ -256,24 +255,32 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { * On track point end. gx:coord end tag. */ private void onTrackPointEnd() throws SAXException { - // Add location to locationList + // Add trackPoint to trackPoints if (content == null) { return; } String[] parts = content.trim().split(" "); - if (parts.length != 2 && parts.length != 3) { - return; + if (parts.length == 2 || parts.length == 3) { + 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(); - if (location == null) { - return; + // Similar to GPX + boolean isFirstTrackPointInSegment = isFirstTrackPointInSegment(); + 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; + longitude = null; + latitude = null; + altitude = null; } /** @@ -287,18 +294,16 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { * On extended data value end. gx:value end tag. */ private void onExtendedDataValueEnd() throws SAXException { - if (content == null) { - return; - } - content = content.trim(); - if (content.equals("")) { - return; - } - float value; - try { - value = Float.parseFloat(content); - } catch (NumberFormatException e) { - throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e); + Float value = null; + if (content != null) { + content = content.trim(); + if (!content.equals("")) { + try { + value = Float.parseFloat(content); + } catch (NumberFormatException e) { + throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e); + } + } } switch (extendedDataType) { case KmlTrackWriter.EXTENDED_DATA_TYPE_SPEED: diff --git a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java index 7b8499fdc..90f773dc8 100644 --- a/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java +++ b/src/main/java/de/dennisguse/opentracks/viewmodels/IntervalStatistics.java @@ -31,7 +31,7 @@ public class IntervalStatistics { TrackPoint prevTrackPoint = trackPoints.get(i - 1); TrackPoint trackPoint = trackPoints.get(i); - if (trackPoint.getType().hasLocation() && prevTrackPoint.getType().hasLocation()) { + if (trackPoint.hasLocation() && prevTrackPoint.hasLocation()) { interval.distance_m += prevTrackPoint.distanceTo(trackPoint); interval.time_ms += trackPoint.getTime() - prevTrackPoint.getTime(); interval.gain_m += trackPoint.hasElevationGain() ? trackPoint.getElevationGain() : 0;