Value objects distance and speed.

#679
This commit is contained in:
Dennis Guse
2021-04-09 19:59:44 +02:00
parent f2b1d2a386
commit 6f9c788965
47 changed files with 773 additions and 502 deletions
@@ -29,6 +29,8 @@ import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
@@ -60,10 +62,10 @@ public class DescriptionGeneratorTest {
public void testGenerateTrackDescription() { public void testGenerateTrackDescription() {
Track track = new Track(); Track track = new Track();
TrackStatistics stats = new TrackStatistics(); TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000); stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofMillis(600000)); stats.setTotalTime(Duration.ofMillis(600000));
stats.setMovingTime(Duration.ofMillis(300000)); stats.setMovingTime(Duration.ofMillis(300000));
stats.setMaxSpeed(100); stats.setMaxSpeed(Speed.of(100));
stats.setMaxAltitude(550); stats.setMaxAltitude(550);
stats.setMinAltitude(-500); stats.setMinAltitude(-500);
stats.setTotalAltitudeGain(6000f); stats.setTotalAltitudeGain(6000f);
@@ -98,10 +100,10 @@ public class DescriptionGeneratorTest {
public void testGenerateTrackDescriptionWithoutMaxMinAltitude() { public void testGenerateTrackDescriptionWithoutMaxMinAltitude() {
Track track = new Track(); Track track = new Track();
TrackStatistics stats = new TrackStatistics(); TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000); stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofMillis(600000)); stats.setTotalTime(Duration.ofMillis(600000));
stats.setMovingTime(Duration.ofMillis(300000)); stats.setMovingTime(Duration.ofMillis(300000));
stats.setMaxSpeed(100); stats.setMaxSpeed(Speed.of(100));
stats.setMaxAltitude(Double.POSITIVE_INFINITY); stats.setMaxAltitude(Double.POSITIVE_INFINITY);
stats.setMinAltitude(Double.NEGATIVE_INFINITY); stats.setMinAltitude(Double.NEGATIVE_INFINITY);
stats.setTotalAltitudeGain(6000f); stats.setTotalAltitudeGain(6000f);
@@ -132,12 +134,12 @@ public class DescriptionGeneratorTest {
/** /**
* Tests {@link DescriptionGenerator#writeDistance(double, StringBuilder, int, String)}. * Tests {@link DescriptionGenerator#writeDistance(Distance, StringBuilder, int, String)}.
*/ */
@Test @Test
public void testWriteDistance() { public void testWriteDistance() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
descriptionGenerator.writeDistance(1100, builder, R.string.description_total_distance, "<br>"); descriptionGenerator.writeDistance(Distance.of(1100), builder, R.string.description_total_distance, "<br>");
assertEquals("Total distance: 1.10 km (0.7 mi)<br>", builder.toString()); assertEquals("Total distance: 1.10 km (0.7 mi)<br>", builder.toString());
} }
@@ -152,12 +154,12 @@ public class DescriptionGeneratorTest {
} }
/** /**
* Tests {@link DescriptionGenerator#writeSpeed(double, StringBuilder, int, String)}. * Tests {@link DescriptionGenerator#writeSpeed(Speed, StringBuilder, int, String)}.
*/ */
@Test @Test
public void testWriteSpeed() { public void testWriteSpeed() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
descriptionGenerator.writeSpeed(1.1, builder, R.string.description_average_speed, "\n"); descriptionGenerator.writeSpeed(Speed.of(1.1), builder, R.string.description_average_speed, "\n");
assertEquals("Average speed: 3.96 km/h (2.5 mi/h)\n", builder.toString()); assertEquals("Average speed: 3.96 km/h (2.5 mi/h)\n", builder.toString());
} }
@@ -172,12 +174,12 @@ public class DescriptionGeneratorTest {
} }
/** /**
* Tests {@link DescriptionGenerator#writePace(double, StringBuilder, int, String)}. * Tests {@link DescriptionGenerator#writePace(Speed, StringBuilder, int, String)}.
*/ */
@Test @Test
public void testWritePace() { public void testWritePace() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
descriptionGenerator.writePace(1.1, builder, R.string.description_average_pace_in_minute, "\n"); descriptionGenerator.writePace(Speed.of(1.1), builder, R.string.description_average_pace_in_minute, "\n");
assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString()); assertEquals("Average pace: 15:09 min/km (24:23 min/mi)\n", builder.toString());
} }
} }
@@ -13,6 +13,7 @@ import java.util.List;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator; import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.FileUtils; import de.dennisguse.opentracks.util.FileUtils;
public class TestDataUtil { public class TestDataUtil {
@@ -75,10 +76,14 @@ public class TestDataUtil {
TrackPoint.createSegmentEndWithTime(Instant.ofEpochSecond(i++ + 1)) TrackPoint.createSegmentEndWithTime(Instant.ofEpochSecond(i++ + 1))
); );
//TODO Use TrackStatisticsUpdater
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(0));
stats.setTotalTime(Duration.ofMillis(0));
List<Marker> markers = List.of( List<Marker> markers = List.of(
new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, 0.0, Duration.ofMillis(0), trackPoints.get(1), null), new Marker("Marker 1", "Marker description 1", "Marker category 3", "", trackId, stats, trackPoints.get(1), null),
new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, 0.0, Duration.ofMillis(0), trackPoints.get(4), null), new Marker("Marker 2", "Marker description 2", "Marker category 3", "", trackId, stats, trackPoints.get(4), null),
new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, 0.0, Duration.ofMillis(0), trackPoints.get(5), null) new Marker("Marker 3", "Marker description 3", "Marker category 3", "", trackId, stats, trackPoints.get(5), null)
); );
return new TrackData(track, trackPoints, markers); return new TrackData(track, trackPoints, markers);
@@ -117,7 +122,7 @@ public class TestDataUtil {
trackPoint.setAccuracy((float) i / 100.0f); trackPoint.setAccuracy((float) i / 100.0f);
trackPoint.setAltitude(i * ALTITUDE_INTERVAL); trackPoint.setAltitude(i * ALTITUDE_INTERVAL);
trackPoint.setTime(Instant.ofEpochSecond(i + 1)); trackPoint.setTime(Instant.ofEpochSecond(i + 1));
trackPoint.setSpeed(5f + (i / 10f)); trackPoint.setSpeed(Speed.of(5f + (i / 10f)));
trackPoint.setHeartRate_bpm(100f + i); trackPoint.setHeartRate_bpm(100f + i);
trackPoint.setCyclingCadence_rpm(300f + i); trackPoint.setCyclingCadence_rpm(300f + i);
@@ -150,7 +155,12 @@ public class TestDataUtil {
Uri photoUri = FileUtils.getUriForFile(context, dstFile); Uri photoUri = FileUtils.getUriForFile(context, dstFile);
String photoUrl = photoUri.toString(); String photoUrl = photoUri.toString();
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, 0.0, Duration.ofMillis(0), trackPoint, photoUrl); //TODO Use TrackStatisticsUpdater
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.of(0));
stats.setTotalTime(Duration.ofMillis(0));
return new Marker("Marker name", "Marker description", "Marker category", "", trackId, stats, trackPoint, photoUrl);
} }
public static List<TrackPoint> getTrackPoints(ContentProviderUtils contentProviderUtils, Track.Id trackId) { public static List<TrackPoint> getTrackPoints(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
@@ -22,7 +22,7 @@ public class TestSensorDataUtil {
tp.setAccuracy(1f); tp.setAccuracy(1f);
tp.setAltitude(1f); tp.setAltitude(1f);
tp.setTime(time); tp.setTime(time);
tp.setSpeed(5f + (i / 10f)); tp.setSpeed(Speed.of(5f + (i / 10f)));
tp.setAltitudeGain(3f); tp.setAltitudeGain(3f);
tp.setAltitudeLoss(3f); tp.setAltitudeLoss(3f);
trackPointList.add(tp); trackPointList.add(tp);
@@ -42,8 +42,10 @@ import java.util.Random;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns; import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil; import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.TestSensorDataUtil; import de.dennisguse.opentracks.content.data.TestSensorDataUtil;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
@@ -428,9 +430,9 @@ public class CustomContentProviderUtilsTest {
statistics.setStopTime(Instant.ofEpochMilli(2500)); statistics.setStopTime(Instant.ofEpochMilli(2500));
statistics.setTotalTime(Duration.ofMillis(1500)); statistics.setTotalTime(Duration.ofMillis(1500));
statistics.setMovingTime(Duration.ofMillis(700)); statistics.setMovingTime(Duration.ofMillis(700));
statistics.setTotalDistance(750.0); statistics.setTotalDistance(Distance.of(750.0));
statistics.setTotalAltitudeGain(50.0f); statistics.setTotalAltitudeGain(50.0f);
statistics.setMaxSpeed(60.0); statistics.setMaxSpeed(Speed.of(60.0));
statistics.setMaxAltitude(1250.0); statistics.setMaxAltitude(1250.0);
statistics.setMinAltitude(1200.0); statistics.setMinAltitude(1200.0);
@@ -807,7 +809,7 @@ public class CustomContentProviderUtilsTest {
assertEquals(longitude, trackPoint.getLongitude(), 0.01); assertEquals(longitude, trackPoint.getLongitude(), 0.01);
assertEquals(latitude, trackPoint.getLatitude(), 0.01); assertEquals(latitude, trackPoint.getLatitude(), 0.01);
assertEquals(time, trackPoint.getTime().toEpochMilli()); assertEquals(time, trackPoint.getTime().toEpochMilli());
assertEquals(speed, trackPoint.getSpeed(), 0.01); assertEquals(speed, trackPoint.getSpeed().toMPS(), 0.01);
assertFalse(trackPoint.hasHeartRate()); assertFalse(trackPoint.hasHeartRate());
} }
@@ -103,8 +103,8 @@ public class SensorDataCyclingTest {
current.compute(previous, 2150); current.compute(previous, 2150);
// then // then
assertEquals(1.20, current.getValue().distance_m, 2150); assertEquals(2.15, current.getValue().distance.toM(), 0.01);
assertEquals(1.20, current.getValue().speed_mps, 0.01); assertEquals(1.20, current.getValue().speed.toMPS(), 0.01);
} }
@Test @Test
@@ -117,8 +117,8 @@ public class SensorDataCyclingTest {
current.compute(previous, 2000); current.compute(previous, 2000);
// then // then
assertEquals(1.20, current.getValue().distance_m, 2000); assertEquals(2, current.getValue().distance.toM(), 0.01);
assertEquals(2, current.getValue().speed_mps, 0.01); assertEquals(2, current.getValue().speed.toMPS(), 0.01);
} }
@Test @Test
@@ -33,6 +33,8 @@ import java.time.Instant;
import de.dennisguse.opentracks.chart.ChartPoint; import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView; import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
@@ -66,6 +68,7 @@ public class ChartFragmentTest {
boolean chartByDistance = false; boolean chartByDistance = false;
chartFragment = ChartFragment.newInstance(chartByDistance); chartFragment = ChartFragment.newInstance(chartByDistance);
chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance)); chartFragment.setChartView(new ChartView(ApplicationProvider.getApplicationContext(), chartByDistance));
chartFragment.setRecordingDistanceInterval(Distance.of(50));
} }
/** /**
@@ -245,7 +248,7 @@ public class ChartFragmentTest {
* It will not be filled in to the speed buffer. * It will not be filled in to the speed buffer.
*/ */
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint(); TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(128.5f); trackPoint1.setSpeed(Speed.of(128.5f));
ChartPoint point = chartFragment.createPendingPoint(trackPoint1); ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getSpeed(), 0.01); assertEquals(0.0, point.getSpeed(), 0.01);
@@ -259,7 +262,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().plusMillis(222)); trackPoint2.setTime(trackPoint1.getTime().plusMillis(222));
trackPoint2.setSpeed(130f); trackPoint2.setSpeed(Speed.of(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);
} }
@@ -273,7 +276,7 @@ public class ChartFragmentTest {
// First data point is not added to the speed buffer // First data point is not added to the speed buffer
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint(); TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(100.0f); trackPoint1.setSpeed(Speed.of(100.0f));
ChartPoint point = chartFragment.createPendingPoint(trackPoint1); ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getSpeed(), 0.01); assertEquals(0.0, point.getSpeed(), 0.01);
@@ -284,7 +287,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().plusMillis(222)); trackPoint2.setTime(trackPoint2.getTime().plusMillis(222));
trackPoint2.setSpeed(102f); trackPoint2.setSpeed(Speed.of(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);
} }
@@ -298,7 +301,7 @@ public class ChartFragmentTest {
// First data point is not added to the speed buffer // First data point is not added to the speed buffer
TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint(); TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint();
trackPoint1.setSpeed(100.0f); trackPoint1.setSpeed(Speed.of(100.0f));
ChartPoint point = chartFragment.createPendingPoint(trackPoint1); ChartPoint point = chartFragment.createPendingPoint(trackPoint1);
assertEquals(0.0, point.getSpeed(), 0.01); assertEquals(0.0, point.getSpeed(), 0.01);
@@ -309,7 +312,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().plusMillis(222)); trackPoint2.setTime(trackPoint2.getTime().plusMillis(222));
trackPoint2.setSpeed(102f); trackPoint2.setSpeed(Speed.of(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);
} }
@@ -321,7 +324,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(0f); trackPoint.setSpeed(Speed.of(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);
} }
@@ -32,7 +32,9 @@ import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil; 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;
@@ -353,7 +355,7 @@ public class ExportImportTest {
} }
assertEquals(trackPoint.hasSpeed(), importedTrackPoint.hasSpeed()); assertEquals(trackPoint.hasSpeed(), importedTrackPoint.hasSpeed());
if (trackPoint.hasSpeed()) { if (trackPoint.hasSpeed()) {
assertEquals(trackPoint.getSpeed(), importedTrackPoint.getSpeed(), 0.001); assertEquals(trackPoint.getSpeed().toMPS(), importedTrackPoint.getSpeed().toMPS(), 0.001);
} }
assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude()); assertEquals(trackPoint.hasAltitude(), importedTrackPoint.hasAltitude());
if (trackPoint.hasAltitude()) { if (trackPoint.hasAltitude()) {
@@ -381,7 +383,7 @@ public class ExportImportTest {
assertEquals(trackPoint.getAltitudeLoss(), importedTrackPoint.getAltitudeLoss(), 0.01); assertEquals(trackPoint.getAltitudeLoss(), importedTrackPoint.getAltitudeLoss(), 0.01);
} }
if (verifyDistance) { if (verifyDistance) {
assertEquals(trackPoint.getSensorDistance(), importedTrackPoint.getSensorDistance(), 0.01); assertEquals(trackPoint.getSensorDistance(), importedTrackPoint.getSensorDistance());
} }
} }
} }
@@ -405,13 +407,13 @@ public class ExportImportTest {
// Distance // Distance
if (verifyDistance) { if (verifyDistance) {
assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance(), 0.01); assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance());
} }
// Speed // Speed
assertEquals(trackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed(), 0.01); assertEquals(trackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed());
assertEquals(trackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed(), 0.01); assertEquals(trackStatistics.getAverageSpeed(), importedTrackStatistics.getAverageSpeed());
assertEquals(trackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed(), 0.01); assertEquals(trackStatistics.getAverageMovingSpeed(), importedTrackStatistics.getAverageMovingSpeed());
} }
// Altitude // Altitude
@@ -429,12 +431,12 @@ public class ExportImportTest {
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain, float heartRate, float cyclingCadence, float power, float distance) { private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain, float heartRate, float cyclingCadence, float power, float distance) {
TrackPoint tp = new TrackPoint(latitude, longitude, (double) altitude, Instant.ofEpochMilli(time)); TrackPoint tp = new TrackPoint(latitude, longitude, (double) altitude, Instant.ofEpochMilli(time));
tp.setAccuracy(accuracy); tp.setAccuracy(accuracy);
tp.setSpeed(speed); tp.setSpeed(Speed.of(speed));
tp.setHeartRate_bpm(heartRate); tp.setHeartRate_bpm(heartRate);
tp.setCyclingCadence_rpm(cyclingCadence); tp.setCyclingCadence_rpm(cyclingCadence);
tp.setPower(power); tp.setPower(power);
tp.setAltitudeGain(altitudeGain); tp.setAltitudeGain(altitudeGain);
tp.setSensorDistance(distance); tp.setSensorDistance(Distance.of(distance));
return tp; return tp;
} }
} }
@@ -11,11 +11,12 @@ import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -41,8 +42,8 @@ public class TrackRecordingServiceNotificationManagerTest {
public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() { public void updateLocation_triggersAlertOnlyOnFirstInaccurateLocation() {
when(trackPointMock.hasAccuracy()).thenReturn(true); when(trackPointMock.hasAccuracy()).thenReturn(true);
when(trackPointMock.getAccuracy()).thenReturn(999f); when(trackPointMock.getAccuracy()).thenReturn(999f);
when(trackStatisticsMock.getTotalDistance()).thenReturn(0d); when(trackPointMock.getSpeed()).thenReturn(Speed.of(0));
when(notificationCompatBuilder.setContentText(anyString())).thenReturn(notificationCompatBuilder); when(trackStatisticsMock.getTotalDistance()).thenReturn(Distance.of(0));
when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean())).thenReturn(notificationCompatBuilder); when(notificationCompatBuilder.setOnlyAlertOnce(anyBoolean())).thenReturn(notificationCompatBuilder);
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder); TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
@@ -360,12 +360,12 @@ public class TrackRecordingServiceTest {
Marker.Id markerId = service.insertMarker(null, null, null, null); Marker.Id markerId = service.insertMarker(null, null, null, null);
// then // then
assertNotEquals(-1L, markerId); assertNotEquals(new Marker.Id(-1L), markerId);
Marker wpt = contentProviderUtils.getMarker(markerId); Marker wpt = contentProviderUtils.getMarker(markerId);
assertEquals(context.getString(R.string.marker_icon_url), wpt.getIcon()); assertEquals(context.getString(R.string.marker_icon_url), wpt.getIcon());
assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName()); assertEquals(context.getString(R.string.marker_name_format, 1), wpt.getName());
assertEquals(trackId, wpt.getTrackId()); assertEquals(trackId, wpt.getTrackId());
assertEquals(0.0, wpt.getLength(), 0.01); assertEquals(0.0, wpt.getLength().toM(), 0.01);
assertNotNull(wpt.getLocation()); assertNotNull(wpt.getLocation());
service.endCurrentTrack(); service.endCurrentTrack();
@@ -15,14 +15,18 @@
*/ */
package de.dennisguse.opentracks.stats; package de.dennisguse.opentracks.stats;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
@@ -32,7 +36,7 @@ import static org.junit.Assert.assertNull;
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
@RunWith(JUnit4.class) @RunWith(AndroidJUnit4.class)
public class TrackStatisticsTest { public class TrackStatisticsTest {
private TrackStatistics statistics; private TrackStatistics statistics;
@@ -60,9 +64,9 @@ public class TrackStatisticsTest {
assertNull(statistics.getTotalAltitudeLoss()); assertNull(statistics.getTotalAltitudeLoss());
assertEquals(Double.NEGATIVE_INFINITY, statistics.getMaxAltitude(), 0.0); assertEquals(Double.NEGATIVE_INFINITY, statistics.getMaxAltitude(), 0.0);
assertEquals(Double.POSITIVE_INFINITY, statistics.getMinAltitude(), 0.0); assertEquals(Double.POSITIVE_INFINITY, statistics.getMinAltitude(), 0.0);
assertEquals(0.0, statistics.getMaxSpeed(), 0.0); assertEquals(0.0, statistics.getMaxSpeed().toMPS(), 0.0);
assertEquals(0.0, statistics.getAverageSpeed(), 0.0); assertEquals(0.0, statistics.getAverageSpeed().toMPS(), 0.0);
assertEquals(0.0, statistics.getAverageMovingSpeed(), 0.0); assertEquals(0.0, statistics.getAverageMovingSpeed().toMPS(), 0.0);
} }
@Test @Test
@@ -77,12 +81,12 @@ public class TrackStatisticsTest {
statistics2.setTotalTime(Duration.ofMillis(1000)); // Result: 1500+1000 statistics2.setTotalTime(Duration.ofMillis(1000)); // Result: 1500+1000
statistics.setMovingTime(Duration.ofMillis(700)); statistics.setMovingTime(Duration.ofMillis(700));
statistics2.setMovingTime(Duration.ofMillis(600)); // Result: 700+600 statistics2.setMovingTime(Duration.ofMillis(600)); // Result: 700+600
statistics.setTotalDistance(750.0); statistics.setTotalDistance(Distance.of(750.0));
statistics2.setTotalDistance(350.0); // Result: 750+350 statistics2.setTotalDistance(Distance.of(350.0)); // Result: 750+350
statistics.setTotalAltitudeGain(50.0f); statistics.setTotalAltitudeGain(50.0f);
statistics2.setTotalAltitudeGain(850.0f); // Result: 850+50 statistics2.setTotalAltitudeGain(850.0f); // Result: 850+50
statistics.setMaxSpeed(60.0); // Resulting max speed statistics.setMaxSpeed(Speed.of(60.0)); // Resulting max speed
statistics2.setMaxSpeed(30.0); statistics2.setMaxSpeed(Speed.of(30.0));
statistics.setMaxAltitude(1250.0); statistics.setMaxAltitude(1250.0);
statistics.setMinAltitude(1200.0); // Resulting min altitude statistics.setMinAltitude(1200.0); // Resulting min altitude
statistics2.setMaxAltitude(3575.0); // Resulting max altitude statistics2.setMaxAltitude(3575.0); // Resulting max altitude
@@ -96,24 +100,24 @@ public class TrackStatisticsTest {
assertEquals(Instant.ofEpochMilli(4000), statistics.getStopTime()); assertEquals(Instant.ofEpochMilli(4000), statistics.getStopTime());
assertEquals(Duration.ofMillis(2500), statistics.getTotalTime()); assertEquals(Duration.ofMillis(2500), statistics.getTotalTime());
assertEquals(Duration.ofMillis(1300), statistics.getMovingTime()); assertEquals(Duration.ofMillis(1300), statistics.getMovingTime());
assertEquals(1100.0, statistics.getTotalDistance(), 0.001); assertEquals(1100.0, statistics.getTotalDistance().toM(), 0.001);
assertEquals(900.0, statistics.getTotalAltitudeGain(), 0.001); assertEquals(900.0, statistics.getTotalAltitudeGain(), 0.001);
assertEquals(statistics.getTotalDistance() / statistics.getMovingTime().getSeconds(), statistics.getMaxSpeed(), 0.001); assertEquals(Speed.of(statistics.getTotalDistance(), statistics.getMovingTime()).toMPS(), statistics.getMaxSpeed().toMPS(), 0.001);
assertEquals(1200.0, statistics.getMinAltitude(), 0.001); assertEquals(1200.0, statistics.getMinAltitude(), 0.001);
assertEquals(3575.0, statistics.getMaxAltitude(), 0.001); assertEquals(3575.0, statistics.getMaxAltitude(), 0.001);
} }
@Test @Test
public void testGetAverageSpeed() { public void testGetAverageSpeed() {
statistics.setTotalDistance(1000.0); statistics.setTotalDistance(Distance.of(1000.0));
statistics.setTotalTime(Duration.ofMillis(50000)); statistics.setTotalTime(Duration.ofMillis(50000));
assertEquals(20.0, statistics.getAverageSpeed(), 0.001); assertEquals(20.0, statistics.getAverageSpeed().toMPS(), 0.001);
} }
@Test @Test
public void testGetAverageMovingSpeed() { public void testGetAverageMovingSpeed() {
statistics.setTotalDistance(1000.0); statistics.setTotalDistance(Distance.of(1000.0));
statistics.setMovingTime(Duration.ofMillis(20000)); statistics.setMovingTime(Duration.ofMillis(20000));
assertEquals(50.0, statistics.getAverageMovingSpeed(), 0.001); assertEquals(50.0, statistics.getAverageMovingSpeed().toMPS(), 0.001);
} }
} }
@@ -9,6 +9,8 @@ import org.junit.runner.RunWith;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil; 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;
@@ -18,7 +20,7 @@ import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class TrackStatisticsUpdaterTest { public class TrackStatisticsUpdaterTest {
private static final int GPS_DISTANCE = 50; private static final Distance GPS_DISTANCE = Distance.of(50);
@Test @Test
public void addTrackPoint_TestingTrack() { public void addTrackPoint_TestingTrack() {
@@ -31,7 +33,7 @@ public class TrackStatisticsUpdaterTest {
// then // then
TrackStatistics statistics = subject.getTrackStatistics(); TrackStatistics statistics = subject.getTrackStatistics();
assertEquals(85.35, statistics.getTotalDistance(), 0.01); assertEquals(85.35, statistics.getTotalDistance().toM(), 0.01);
assertEquals(Duration.ofMillis(13999), statistics.getTotalTime()); assertEquals(Duration.ofMillis(13999), statistics.getTotalTime());
assertEquals(Duration.ofSeconds(6), statistics.getMovingTime()); assertEquals(Duration.ofSeconds(6), statistics.getMovingTime());
@@ -40,9 +42,9 @@ public class TrackStatisticsUpdaterTest {
assertEquals(27, statistics.getTotalAltitudeGain(), 0.01); assertEquals(27, statistics.getTotalAltitudeGain(), 0.01);
assertEquals(27.0, statistics.getTotalAltitudeLoss(), 0.01); assertEquals(27.0, statistics.getTotalAltitudeLoss(), 0.01);
assertEquals(14.226, statistics.getMaxSpeed(), 0.01); assertEquals(14.226, statistics.getMaxSpeed().toMPS(), 0.01);
assertEquals(14.226, statistics.getAverageMovingSpeed(), 0.01); assertEquals(14.226, statistics.getAverageMovingSpeed().toMPS(), 0.01);
assertEquals(6.566, statistics.getAverageSpeed(), 0.01); assertEquals(6.566, statistics.getAverageSpeed().toMPS(), 0.01);
} }
@Test @Test
@@ -60,7 +62,7 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp3, GPS_DISTANCE); subject.addTrackPoint(tp3, GPS_DISTANCE);
// then // then
assertEquals(0, subject.getTrackStatistics().getTotalDistance(), 0.01); assertEquals(0, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
} }
@Test @Test
@@ -71,7 +73,7 @@ public class TrackStatisticsUpdaterTest {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000)); TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000)); TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000)); TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
tp3.setSpeed(5f); tp3.setSpeed(Speed.of(5f));
// when // when
subject.addTrackPoint(tp1, GPS_DISTANCE); subject.addTrackPoint(tp1, GPS_DISTANCE);
@@ -79,7 +81,7 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp3, GPS_DISTANCE); subject.addTrackPoint(tp3, GPS_DISTANCE);
// then // then
assertEquals(1.10, subject.getTrackStatistics().getTotalDistance(), 0.01); assertEquals(1.10, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
} }
@Test @Test
@@ -89,14 +91,14 @@ public class TrackStatisticsUpdaterTest {
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000)); TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000)); TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
tp2.setSpeed(5f); tp2.setSpeed(Speed.of(5f));
TrackPoint tp3 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(3000)); TrackPoint tp3 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(3000));
tp2.setSpeed(5f); tp2.setSpeed(Speed.of(5f));
TrackPoint tp4 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(4000)); TrackPoint tp4 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(4000));
tp2.setSpeed(5f); tp2.setSpeed(Speed.of(5f));
tp4.setSensorDistance(5f); tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000)); TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(10f); tp5.setSensorDistance(Distance.of(10f));
// when // when
subject.addTrackPoint(tp1, GPS_DISTANCE); subject.addTrackPoint(tp1, GPS_DISTANCE);
@@ -104,14 +106,14 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp3, GPS_DISTANCE); subject.addTrackPoint(tp3, GPS_DISTANCE);
// then // then
assertEquals(110.57, subject.getTrackStatistics().getTotalDistance(), 0.01); assertEquals(110.57, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
// when // when
subject.addTrackPoint(tp4, GPS_DISTANCE); subject.addTrackPoint(tp4, GPS_DISTANCE);
subject.addTrackPoint(tp5, GPS_DISTANCE); subject.addTrackPoint(tp5, GPS_DISTANCE);
// then // then
assertEquals(125.57, subject.getTrackStatistics().getTotalDistance(), 0.01); assertEquals(125.57, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
} }
@Test @Test
@@ -123,9 +125,9 @@ public class TrackStatisticsUpdaterTest {
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000)); TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000)); TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
TrackPoint tp4 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(4000)); TrackPoint tp4 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(4000));
tp4.setSensorDistance(5f); tp4.setSensorDistance(Distance.of(5f));
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000)); TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(10f); tp5.setSensorDistance(Distance.of(10f));
// when // when
subject.addTrackPoint(tp1, GPS_DISTANCE); subject.addTrackPoint(tp1, GPS_DISTANCE);
@@ -133,14 +135,14 @@ public class TrackStatisticsUpdaterTest {
subject.addTrackPoint(tp3, GPS_DISTANCE); subject.addTrackPoint(tp3, GPS_DISTANCE);
// then // then
assertEquals(0, subject.getTrackStatistics().getTotalDistance(), 0.01); assertEquals(0, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
// when // when
subject.addTrackPoint(tp4, GPS_DISTANCE); subject.addTrackPoint(tp4, GPS_DISTANCE);
subject.addTrackPoint(tp5, GPS_DISTANCE); subject.addTrackPoint(tp5, GPS_DISTANCE);
// then // then
assertEquals(15, subject.getTrackStatistics().getTotalDistance(), 0.01); assertEquals(15, subject.getTrackStatistics().getTotalDistance().toM(), 0.01);
} }
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor") @Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@@ -11,6 +11,8 @@ import org.junit.runner.RunWith;
import java.time.Duration; import java.time.Duration;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TestDataUtil; 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;
@@ -27,10 +29,10 @@ public class AnnouncementUtilsTest {
@Test @Test
public void getAnnouncement_metric() { public void getAnnouncement_metric() {
TrackStatistics stats = new TrackStatistics(); TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000); stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10)); stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10)); stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(100); stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f); stats.setTotalAltitudeGain(6000f);
// when // when
@@ -43,10 +45,10 @@ public class AnnouncementUtilsTest {
@Test @Test
public void getAnnouncement_withInterval_metric() { public void getAnnouncement_withInterval_metric() {
TrackStatistics stats = new TrackStatistics(); TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(20000); stats.setTotalDistance(Distance.of(20000));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10)); stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10)); stats.setMovingTime(Duration.ofHours(1).plusMinutes(5).plusSeconds(10));
stats.setMaxSpeed(100); stats.setMaxSpeed(Speed.of(100));
stats.setTotalAltitudeGain(6000f); stats.setTotalAltitudeGain(6000f);
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second; List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
@@ -28,6 +28,9 @@ import java.time.Duration;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.TimeZone; import java.util.TimeZone;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -70,19 +73,18 @@ public class StringUtilsTest {
} }
/** /**
* Tests {@link StringUtils#formatDistance(android.content.Context, double, * Tests {@link StringUtils#formatDistance(android.content.Context, Distance, boolean)}.
* boolean)}.
*/ */
@Test @Test
public void testFormatDistance() { public void testFormatDistance() {
// A large number in metric // A large number in metric
assertEquals("5.00 km", StringUtils.formatDistance(context, 5000, true)); assertEquals("5.00 km", StringUtils.formatDistance(context, Distance.of(5000), true));
// A large number in imperial // A large number in imperial
assertEquals("3.11 mi", StringUtils.formatDistance(context, 5000, false)); assertEquals("3.11 mi", StringUtils.formatDistance(context, Distance.of(5000), false));
// A small number in metric // A small number in metric
assertEquals("100.00 m", StringUtils.formatDistance(context, 100, true)); assertEquals("100.00 m", StringUtils.formatDistance(context, Distance.of(100), true));
// A small number in imperial // A small number in imperial
assertEquals("328.08 ft", StringUtils.formatDistance(context, 100, false)); assertEquals("328.08 ft", StringUtils.formatDistance(context, Distance.of(100), false));
} }
/** /**
@@ -164,22 +166,22 @@ public class StringUtilsTest {
@Test @Test
public void testGetSpeedParts() { public void testGetSpeedParts() {
assertEquals("4:59", StringUtils.getSpeedParts(context, 3.34, true, false).first); assertEquals("4:59", StringUtils.getSpeedParts(context, Speed.of(3.34), true, false).first);
assertEquals("5:00", StringUtils.getSpeedParts(context, 3.33, true, false).first); assertEquals("5:00", StringUtils.getSpeedParts(context, Speed.of(3.33), true, false).first);
assertEquals("11.9", StringUtils.getSpeedParts(context, 3.31, true, true).first); assertEquals("11.9", StringUtils.getSpeedParts(context, Speed.of(3.31), true, true).first);
assertEquals("7.5", StringUtils.getSpeedParts(context, 3.34, false, true).first); assertEquals("7.5", StringUtils.getSpeedParts(context, Speed.of(3.34), false, true).first);
assertEquals("min/km", StringUtils.getSpeedParts(context, 0, true, false).second); assertEquals("min/km", StringUtils.getSpeedParts(context, Speed.zero(), true, false).second);
assertEquals("min/mi", StringUtils.getSpeedParts(context, 0, false, false).second); assertEquals("min/mi", StringUtils.getSpeedParts(context, Speed.zero(), false, false).second);
} }
@Test @Test
public void testFormatSpeed() { public void testFormatSpeed() {
assertEquals("4:59 min/km", StringUtils.formatSpeed(context, 3.34, true, false)); assertEquals("4:59 min/km", StringUtils.formatSpeed(context, Speed.of(3.34), true, false));
assertEquals("8:02 min/mi", StringUtils.formatSpeed(context, 3.34, false, false)); assertEquals("8:02 min/mi", StringUtils.formatSpeed(context, Speed.of(3.34), false, false));
assertEquals("12.02 km/h", StringUtils.formatSpeed(context, 3.34, true, true)); assertEquals("12.02 km/h", StringUtils.formatSpeed(context, Speed.of(3.34), true, true));
assertEquals("7.47 mph", StringUtils.formatSpeed(context, 3.34, false, true)); assertEquals("7.47 mph", StringUtils.formatSpeed(context, Speed.of(3.34), false, true));
} }
} }
@@ -13,6 +13,8 @@ import java.time.Instant;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.TrackIconUtils; import de.dennisguse.opentracks.util.TrackIconUtils;
@@ -25,15 +27,15 @@ public class AggregatedStatisticsTest {
private final Context context = ApplicationProvider.getApplicationContext(); private final Context context = ApplicationProvider.getApplicationContext();
private static Track createTrack(Context context, long totalDistance, long totalTime, String category) { private static Track createTrack(Context context, Distance totalDistance, Duration totalTime, String category) {
TrackStatistics statistics = new TrackStatistics(); TrackStatistics statistics = new TrackStatistics();
statistics.setStartTime(Instant.ofEpochMilli(1000L)); // Resulting start time statistics.setStartTime(Instant.ofEpochMilli(1000L)); // Resulting start time
statistics.setStopTime(Instant.ofEpochMilli(1000L + totalTime)); statistics.setStopTime(statistics.getStartTime().plus(totalTime));
statistics.setTotalTime(Duration.ofMillis(totalTime)); statistics.setTotalTime(totalTime);
statistics.setMovingTime(Duration.ofMillis(totalTime)); statistics.setMovingTime(totalTime);
statistics.setTotalDistance(totalDistance); statistics.setTotalDistance(totalDistance);
statistics.setTotalAltitudeGain(50.0f); statistics.setTotalAltitudeGain(50.0f);
statistics.setMaxSpeed(50.0); // Resulting max speed statistics.setMaxSpeed(Speed.of(50.0)); // Resulting max speed
statistics.setMaxAltitude(1250.0); statistics.setMaxAltitude(1250.0);
statistics.setMinAltitude(1200.0); // Resulting min altitude statistics.setMinAltitude(1200.0); // Resulting min altitude
@@ -48,8 +50,8 @@ public class AggregatedStatisticsTest {
public void testAggregate() { public void testAggregate() {
// given // given
// 10km in 40 minutes. // 10km in 40 minutes.
long totalDistance = 10000; Distance totalDistance = Distance.of(10000);
long totalTime = 2400000; Duration totalTime = Duration.ofMillis(2400000);
String biking = context.getString(R.string.activity_type_biking); String biking = context.getString(R.string.activity_type_biking);
Track track = createTrack(context, totalDistance, totalTime, biking); Track track = createTrack(context, totalDistance, totalTime, biking);
@@ -62,16 +64,16 @@ public class AggregatedStatisticsTest {
assertEquals(1, aggregatedStatistics.get(biking).getCountTracks()); assertEquals(1, aggregatedStatistics.get(biking).getCountTracks());
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics(); TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0); assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime()); assertEquals(totalTime, statistics2.getMovingTime());
} }
@Test @Test
public void testAggregate_mountainBiking() { public void testAggregate_mountainBiking() {
// given // given
// 10km in 40 minutes. // 10km in 40 minutes.
long totalDistance = 10000; Distance totalDistance = Distance.of(10000);
long totalTime = 2400000; Duration totalTime = Duration.ofMillis(2400000);
String mountainBiking = context.getString(R.string.activity_type_mountain_biking); String mountainBiking = context.getString(R.string.activity_type_mountain_biking);
Track track = createTrack(context, totalDistance, totalTime, mountainBiking); Track track = createTrack(context, totalDistance, totalTime, mountainBiking);
@@ -86,8 +88,8 @@ public class AggregatedStatisticsTest {
public void testAggregate_trailRunning() { public void testAggregate_trailRunning() {
// given // given
// 10km in 40 minutes. // 10km in 40 minutes.
long totalDistance = 10000; Distance totalDistance = Distance.of(10000);
long totalTime = 2400000; Duration totalTime = Duration.ofMillis(2400000);
String trailRunning = context.getString(R.string.activity_type_trail_running); String trailRunning = context.getString(R.string.activity_type_trail_running);
Track track = createTrack(context, totalDistance, totalTime, trailRunning); Track track = createTrack(context, totalDistance, totalTime, trailRunning);
@@ -102,8 +104,8 @@ public class AggregatedStatisticsTest {
public void testAggregate_twoBikingTracks() { public void testAggregate_twoBikingTracks() {
// given // given
// 10km in 40 minutes. // 10km in 40 minutes.
long totalDistance = 10000; Distance totalDistance = Distance.of(10000);
long totalTime = 2400000; Duration totalTime = Duration.ofMillis(2400000);
String biking = context.getString(R.string.activity_type_biking); String biking = context.getString(R.string.activity_type_biking);
List<Track> tracks = List.of(createTrack(context, totalDistance, totalTime, biking), createTrack(context, totalDistance, totalTime, biking)); List<Track> tracks = List.of(createTrack(context, totalDistance, totalTime, biking), createTrack(context, totalDistance, totalTime, biking));
@@ -116,20 +118,20 @@ public class AggregatedStatisticsTest {
assertEquals(2, aggregatedStatistics.get(biking).getCountTracks()); assertEquals(2, aggregatedStatistics.get(biking).getCountTracks());
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics(); TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0); assertEquals(totalDistance.multipliedBy(2), statistics2.getTotalDistance());
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime()); assertEquals(totalTime.multipliedBy(2), statistics2.getMovingTime());
} }
@Test @Test
public void testAggregate_threeDifferentTracks() { public void testAggregate_threeDifferentTracks() {
// given // given
// 10km in 40 minutes. // 10km in 40 minutes.
long totalDistance = 10000; Distance totalDistance = Distance.of(10000);
String biking = context.getString(R.string.activity_type_biking); String biking = context.getString(R.string.activity_type_biking);
String running = context.getString(R.string.activity_type_running); String running = context.getString(R.string.activity_type_running);
String walking = context.getString(R.string.activity_type_walking); String walking = context.getString(R.string.activity_type_walking);
long totalTime = 2400000; Duration totalTime = Duration.ofMillis(2400000);
List<Track> tracks = List.of( List<Track> tracks = List.of(
createTrack(context, totalDistance, totalTime, biking), createTrack(context, totalDistance, totalTime, biking),
createTrack(context, totalDistance, totalTime, running), createTrack(context, totalDistance, totalTime, running),
@@ -150,20 +152,20 @@ public class AggregatedStatisticsTest {
{ {
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics(); TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0); assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime()); assertEquals(totalTime, statistics2.getMovingTime());
} }
{ {
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics(); TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0); assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime()); assertEquals(totalTime, statistics2.getMovingTime());
} }
{ {
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics(); TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0); assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime()); assertEquals(totalTime, statistics2.getMovingTime());
} }
} }
@@ -171,8 +173,8 @@ public class AggregatedStatisticsTest {
public void testAggregate_severalTracksWithSeveralActivities() { public void testAggregate_severalTracksWithSeveralActivities() {
// given // given
// 10km in 40 minutes. // 10km in 40 minutes.
long totalDistance = 10000; Distance totalDistance = Distance.of(10000);
long totalTime = 2400000; Duration totalTime = Duration.ofMillis(2400000);
String biking = context.getString(R.string.activity_type_biking); String biking = context.getString(R.string.activity_type_biking);
String running = context.getString(R.string.activity_type_running); String running = context.getString(R.string.activity_type_running);
String walking = context.getString(R.string.activity_type_walking); String walking = context.getString(R.string.activity_type_walking);
@@ -213,29 +215,29 @@ public class AggregatedStatisticsTest {
// Biking. // Biking.
{ {
TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics(); TrackStatistics statistics2 = aggregatedStatistics.get(biking).getTrackStatistics();
assertEquals(totalDistance * 5, statistics2.getTotalDistance(), 0); assertEquals(totalDistance.multipliedBy(5), statistics2.getTotalDistance());
assertEquals(Duration.ofMillis(totalTime * 5), statistics2.getMovingTime()); assertEquals(totalTime.multipliedBy(5), statistics2.getMovingTime());
} }
// Running. // Running.
{ {
TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics(); TrackStatistics statistics2 = aggregatedStatistics.get(running).getTrackStatistics();
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0); assertEquals(totalDistance.multipliedBy(2), statistics2.getTotalDistance());
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime()); assertEquals(totalTime.multipliedBy(2), statistics2.getMovingTime());
} }
// Walking. // Walking.
{ {
TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics(); TrackStatistics statistics2 = aggregatedStatistics.get(walking).getTrackStatistics();
assertEquals(totalDistance * 2, statistics2.getTotalDistance(), 0); assertEquals(totalDistance.multipliedBy(2), statistics2.getTotalDistance());
assertEquals(Duration.ofMillis(totalTime * 2), statistics2.getMovingTime()); assertEquals(totalTime.multipliedBy(2), statistics2.getMovingTime());
} }
// Driving. // Driving.
{ {
TrackStatistics statistics2 = aggregatedStatistics.get(driving).getTrackStatistics(); TrackStatistics statistics2 = aggregatedStatistics.get(driving).getTrackStatistics();
assertEquals(totalDistance, statistics2.getTotalDistance(), 0); assertEquals(totalDistance, statistics2.getTotalDistance());
assertEquals(Duration.ofMillis(totalTime), statistics2.getMovingTime()); assertEquals(totalTime, statistics2.getMovingTime());
} }
// Check order // Check order
@@ -6,6 +6,7 @@ import org.junit.runners.JUnit4;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TestDataUtil; 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;
@@ -18,121 +19,121 @@ import static org.junit.Assert.assertEquals;
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class IntervalStatisticsTest { public class IntervalStatisticsTest {
private static final String TAG = IntervalStatisticsTest.class.getSimpleName(); private static final String TAG = IntervalStatisticsTest.class.getSimpleName();
private List<TrackPoint> buildTrackPoints(int numberOfTrackPoints) { private List<TrackPoint> buildTrackPoints(int numberOfTrackPoints) {
return TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), numberOfTrackPoints).second; return TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), numberOfTrackPoints).second;
} }
private TrackStatistics buildTrackStatistics(List<TrackPoint> trackPoints) { private TrackStatistics buildTrackStatistics(List<TrackPoint> trackPoints) {
TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater(); TrackStatisticsUpdater trackStatisticsUpdater = new TrackStatisticsUpdater();
for (TrackPoint tp : trackPoints) { for (TrackPoint tp : trackPoints) {
trackStatisticsUpdater.addTrackPoint(tp, 0); trackStatisticsUpdater.addTrackPoint(tp, Distance.of(0));
} }
return trackStatisticsUpdater.getTrackStatistics(); return trackStatisticsUpdater.getTrackStatistics();
} }
/** /**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result. * Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/ */
@Test @Test
public void testBuild_1() { public void testBuild_1() {
// With 50 points and interval distance of 1000m. // With 50 points and interval distance of 1000m.
// given // given
List<TrackPoint> trackPoints = buildTrackPoints(50); List<TrackPoint> trackPoints = buildTrackPoints(50);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints); TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f; float distanceInterval = 1000f;
// when and then // when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval); whenAndThen(trackPoints, trackStatistics, distanceInterval);
} }
/** /**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result. * Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/ */
@Test @Test
public void testBuild_2() { public void testBuild_2() {
// With 200 points and interval distance of 1000m. // With 200 points and interval distance of 1000m.
// given // given
List<TrackPoint> trackPoints = buildTrackPoints(200); List<TrackPoint> trackPoints = buildTrackPoints(200);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints); TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f; float distanceInterval = 1000f;
// when and then // when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval); whenAndThen(trackPoints, trackStatistics, distanceInterval);
} }
/** /**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result. * Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/ */
@Test @Test
public void testBuild_3() { public void testBuild_3() {
// With 200 points and interval distance of 3000m. // With 200 points and interval distance of 3000m.
// given // given
List<TrackPoint> trackPoints = buildTrackPoints(200); List<TrackPoint> trackPoints = buildTrackPoints(200);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints); TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 3000f; float distanceInterval = 3000f;
// when and then // when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval); whenAndThen(trackPoints, trackStatistics, distanceInterval);
} }
/** /**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result. * Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/ */
@Test @Test
public void testBuild_4() { public void testBuild_4() {
// With 1000 points and interval distance of 3000m. // With 1000 points and interval distance of 3000m.
// given // given
List<TrackPoint> trackPoints = buildTrackPoints(1000); List<TrackPoint> trackPoints = buildTrackPoints(1000);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints); TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 3000f; float distanceInterval = 3000f;
// when and then // when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval); whenAndThen(trackPoints, trackStatistics, distanceInterval);
} }
/** /**
* Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result. * Tests that build method compute the distance correctly comparing the result with TrackStatisticsUpdater result.
*/ */
@Test @Test
public void testBuild_5() { public void testBuild_5() {
// With 10000 points and interval distance of 1000m. // With 10000 points and interval distance of 1000m.
// given // given
List<TrackPoint> trackPoints = buildTrackPoints(10000); List<TrackPoint> trackPoints = buildTrackPoints(10000);
TrackStatistics trackStatistics = buildTrackStatistics(trackPoints); TrackStatistics trackStatistics = buildTrackStatistics(trackPoints);
float distanceInterval = 1000f; float distanceInterval = 1000f;
// when and then // when and then
whenAndThen(trackPoints, trackStatistics, distanceInterval); whenAndThen(trackPoints, trackStatistics, distanceInterval);
} }
private void whenAndThen(List<TrackPoint> trackPoints, TrackStatistics trackStatistics, float distanceInterval) { private void whenAndThen(List<TrackPoint> trackPoints, TrackStatistics trackStatistics, float distanceInterval) {
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval); IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval);
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList(); List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
double totalDistance = 0d; double totalDistance = 0d;
float totalTime = 0L; float totalTime = 0L;
float totalGain = 0f; float totalGain = 0f;
for (IntervalStatistics.Interval i : intervalList) { for (IntervalStatistics.Interval i : intervalList) {
totalDistance += i.getDistance_m(); totalDistance += i.getDistance_m();
totalTime += i.getDistance_m() / i.getSpeed_ms(); totalTime += i.getDistance_m() / i.getSpeed_ms();
totalGain += i.getGain_m(); totalGain += i.getGain_m();
} }
// then // then
assertEquals(trackStatistics.getTotalDistance(), totalDistance, 0.01); assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance, 0.01);
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1); assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance() / distanceInterval)); assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval));
assertEquals(totalGain, trackPoints.size() * TestDataUtil.ALTITUDE_GAIN, 0.1); assertEquals(totalGain, trackPoints.size() * TestDataUtil.ALTITUDE_GAIN, 0.1);
for (int i = 0; i < intervalList.size() - 1; i++) { for (int i = 0; i < intervalList.size() - 1; i++) {
assertEquals(intervalList.get(i).getDistance_m(), distanceInterval, 0.001); assertEquals(intervalList.get(i).getDistance_m(), distanceInterval, 0.001);
totalDistance -= intervalList.get(i).getDistance_m(); totalDistance -= intervalList.get(i).getDistance_m();
} }
assertEquals(intervalList.get(intervalList.size() - 1).getDistance_m(), totalDistance, 0.01); assertEquals(intervalList.get(intervalList.size() - 1).getDistance_m(), totalDistance, 0.01);
} }
} }
@@ -48,6 +48,7 @@ import androidx.loader.content.Loader;
import java.time.Duration; import java.time.Duration;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns; import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.databinding.TrackListBinding; import de.dennisguse.opentracks.databinding.TrackListBinding;
@@ -215,7 +216,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
int iconId = TrackIconUtils.getIconDrawable(icon); int iconId = TrackIconUtils.getIconDrawable(icon);
String name = cursor.getString(nameIndex); String name = cursor.getString(nameIndex);
String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex))); String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits); String totalDistance = StringUtils.formatDistance(TrackListActivity.this, Distance.of(cursor.getDouble(totalDistanceIndex)), metricUnits);
int markerCount = cursor.getInt(markerCountIndex); int markerCount = cursor.getInt(markerCountIndex);
long startTime = cursor.getLong(startTimeIndex); long startTime = cursor.getLong(startTimeIndex);
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex); String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
@@ -12,6 +12,8 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics; import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
@@ -52,12 +54,12 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
} else { } else {
sumDistance_m = nextPosition * interval.getDistance_m(); sumDistance_m = nextPosition * interval.getDistance_m();
} }
viewHolder.distance.setText(StringUtils.formatDistance(context, sumDistance_m, metricUnits)); viewHolder.distance.setText(StringUtils.formatDistance(context, Distance.of(sumDistance_m), metricUnits));
viewHolder.rate.setText(StringUtils.formatSpeed(context, interval.getSpeed_ms(), metricUnits, isReportSpeed)); viewHolder.rate.setText(StringUtils.formatSpeed(context, Speed.of(interval.getSpeed_ms()), metricUnits, isReportSpeed));
viewHolder.gain.setText(StringUtils.formatDistance(context, interval.getGain_m(), metricUnits)); viewHolder.gain.setText(StringUtils.formatDistance(context, Distance.of(interval.getGain_m()), metricUnits));
viewHolder.loss.setText(StringUtils.formatDistance(context, interval.getLoss_m(), metricUnits)); viewHolder.loss.setText(StringUtils.formatDistance(context, Distance.of(interval.getLoss_m()), metricUnits));
} }
@Override @Override
@@ -29,11 +29,7 @@ public class ChartPoint {
TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics(); TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics();
if (chartByDistance) { if (chartByDistance) {
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM; timeOrDistance = trackStatistics.getTotalDistance().to(metricUnits);
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
timeOrDistance = distance;
} else { } else {
timeOrDistance = trackStatistics.getTotalTime().toMillis(); timeOrDistance = trackStatistics.getTotalTime().toMillis();
} }
@@ -43,11 +39,8 @@ public class ChartPoint {
altitude *= UnitConversions.M_TO_FT; altitude *= UnitConversions.M_TO_FT;
} }
speed = trackStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MPS_TO_KMH; speed = trackStatisticsUpdater.getSmoothedSpeed().to(metricUnits);
if (!metricUnits) { pace = trackStatisticsUpdater.getSmoothedSpeed().toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
speed *= UnitConversions.KM_TO_MI;
}
pace = speed == 0 ? 0.0 : 60.0 / speed;
if (trackPoint != null) { if (trackPoint != null) {
if (trackPoint.hasHeartRate()) { if (trackPoint.hasHeartRate()) {
heartRate = trackPoint.getHeartRate_bpm(); heartRate = trackPoint.getHeartRate_bpm();
@@ -51,7 +51,6 @@ import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.MarkerUtils; import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.ThemeUtils; import de.dennisguse.opentracks.util.ThemeUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/** /**
* Visualization of the chart. * Visualization of the chart.
@@ -994,8 +993,7 @@ public class ChartView extends View {
private double getMarkerXValue(Marker marker) { private double getMarkerXValue(Marker marker) {
if (chartByDistance) { if (chartByDistance) {
double length_km = marker.getLength() * UnitConversions.M_TO_KM; return marker.getLength().to(metricUnits);
return metricUnits ? length_km : length_km * UnitConversions.KM_TO_MI;
} else { } else {
return marker.getDuration().toMillis(); return marker.getDuration().toMillis();
} }
@@ -24,6 +24,8 @@ import androidx.annotation.VisibleForTesting;
import java.time.Duration; import java.time.Duration;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
@@ -163,9 +165,9 @@ public class DescriptionGenerator {
* @param lineBreak line break string * @param lineBreak line break string
*/ */
@VisibleForTesting @VisibleForTesting
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) { void writeDistance(Distance distance, StringBuilder builder, int resId, String lineBreak) {
double distanceInKm = distance * UnitConversions.M_TO_KM; double distanceInKm = distance.toKM();
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI; double distanceInMi = distance.toMI();
builder.append(context.getString(resId, distanceInKm, distanceInMi)); builder.append(context.getString(resId, distanceInKm, distanceInMi));
builder.append(lineBreak); builder.append(lineBreak);
} }
@@ -193,21 +195,18 @@ public class DescriptionGenerator {
* @param lineBreak line break string * @param lineBreak line break string
*/ */
@VisibleForTesting @VisibleForTesting
void writeSpeed(double speed, StringBuilder builder, int resId, String lineBreak) { void writeSpeed(Speed speed, StringBuilder builder, int resId, String lineBreak) {
double speedInKmHr = speed * UnitConversions.MPS_TO_KMH; builder.append(context.getString(resId, speed.toKMH(), speed.toMPH()));
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
builder.append(lineBreak); builder.append(lineBreak);
} }
/** /**
* @param speed speed in meters per second
* @param builder StringBuilder to append pace * @param builder StringBuilder to append pace
* @param resId resource id of pace string * @param resId resource id of pace string
* @param lineBreak line break string * @param lineBreak line break string
*/ */
@VisibleForTesting @VisibleForTesting
void writePace(double speed, StringBuilder builder, int resId, String lineBreak) { void writePace(Speed speed, StringBuilder builder, int resId, String lineBreak) {
Pair<String, String> paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false); Pair<String, String> paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
Pair<String, String> paceInImperial = StringUtils.getSpeedParts(context, speed, false, false); Pair<String, String> paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
@@ -0,0 +1,116 @@
package de.dennisguse.opentracks.content.data;
import androidx.annotation.NonNull;
import java.util.Objects;
import de.dennisguse.opentracks.util.UnitConversions;
public class Distance {
public static Distance of(double distance_m) {
return new Distance(distance_m);
}
public static Distance of(String distance_m) {
return of(Float.parseFloat(distance_m));
}
public static Distance ofMile(double distance_mile) {
return of(distance_mile * UnitConversions.MI_TO_M);
}
public static Distance invalid() {
return of(Double.NaN);
}
private final double distance_m;
private Distance(double distance_m) {
this.distance_m = distance_m;
}
public Distance plus(@NonNull Distance distance) {
return new Distance(distance_m + distance.distance_m);
}
public Distance multipliedBy(double factor) {
return new Distance(factor * distance_m);
}
public boolean isZero() {
return distance_m == 0;
}
public boolean isInvalid() {
return Double.isNaN(distance_m) || Double.isInfinite(distance_m);
}
public boolean lessThan(@NonNull Distance distance) {
return !greaterThan(distance);
}
public boolean greaterThan(@NonNull Distance distance) {
return distance_m > distance.distance_m;
}
public boolean greaterOrEqualThan(@NonNull Distance distance) {
return distance_m >= distance.distance_m;
}
public double toM() {
return distance_m;
}
public double toKM() {
return distance_m * UnitConversions.M_TO_KM;
}
public double toFT() {
return distance_m * UnitConversions.M_TO_FT;
}
public double toMI() {
return toKM() * UnitConversions.KM_TO_MI;
}
public double to(boolean metricUnit) {
return to(metricUnit ? Unit.KM : Unit.MILES);
}
public double to(Unit unit) {
switch (unit) {
case KM:
return toKM();
case MILES:
return toMI();
default:
throw new RuntimeException("Not implemented");
}
}
public enum Unit {
KM,
MILES
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Distance distance = (Distance) o;
return Double.compare(distance.distance_m, distance_m) == 0;
}
@Override
public int hashCode() {
return Objects.hash(distance_m);
}
@Override
public String toString() {
return "Distance{" +
"distance_m=" + distance_m +
'}';
}
}
@@ -28,12 +28,15 @@ import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Objects; import java.util.Objects;
import de.dennisguse.opentracks.stats.TrackStatistics;
/** /**
* NOTE: A marker is indirectly (via it's location) assigned to one {@link TrackPoint} with trackPoint.hasLocation() == true. * NOTE: A marker is indirectly (via it's location) assigned to one {@link TrackPoint} with trackPoint.hasLocation() == true.
* *
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
//TODO All data should be final; no default values.
public final class Marker { public final class Marker {
private Id id; private Id id;
@@ -51,7 +54,7 @@ public final class Marker {
private Float bearing; private Float bearing;
//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 Distance length;
private Duration duration; private Duration duration;
@Deprecated //TODO Make an URI instead of String @Deprecated //TODO Make an URI instead of String
@@ -62,6 +65,7 @@ public final class Marker {
this.time = time; this.time = time;
} }
@Deprecated //TODO Marker cannot be created without length AND duration!
public Marker(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint) { public Marker(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint) {
this.trackId = trackId; this.trackId = trackId;
@@ -75,16 +79,19 @@ public final class Marker {
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy(); if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
if (trackPoint.hasAltitude()) this.altitude_m = trackPoint.getAltitude(); if (trackPoint.hasAltitude()) this.altitude_m = trackPoint.getAltitude();
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing(); if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
this.length = Distance.of(0); //TODO Not cool!
this.duration = Duration.ofMillis(0); //TODO Not cool!
} }
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, double length, @NonNull Duration duration, @NonNull TrackPoint trackPoint, String photoUrl) { public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, @NonNull TrackStatistics statistics, @NonNull TrackPoint trackPoint, String photoUrl) {
this(trackId, trackPoint); this(trackId, trackPoint);
this.name = name; this.name = name;
this.description = description; this.description = description;
this.category = category; this.category = category;
this.icon = icon; this.icon = icon;
this.length = length; this.length = statistics.getTotalDistance();
this.duration = duration; this.duration = statistics.getTotalTime();
this.photoUrl = photoUrl; this.photoUrl = photoUrl;
} }
@@ -218,11 +225,11 @@ public final class Marker {
this.bearing = bearing; this.bearing = bearing;
} }
public double getLength() { public Distance getLength() {
return length; return length;
} }
public void setLength(double length) { public void setLength(Distance length) {
this.length = length; this.length = length;
} }
@@ -0,0 +1,145 @@
package de.dennisguse.opentracks.content.data;
import java.time.Duration;
import java.util.Objects;
import de.dennisguse.opentracks.util.UnitConversions;
public class Speed {
public static Speed of(Distance distance, Duration duration) {
if (duration.isZero()) {
return zero();
}
return new Speed(distance.toM() / (duration.toMillis() * UnitConversions.MS_TO_S));
}
public static Speed of(double speed_mps) {
return new Speed(speed_mps);
}
public static Speed of(String speed_mps) {
return of(Float.parseFloat(speed_mps));
}
public static Speed invalid() {
return of(Double.NaN);
}
public static Speed zero() {
return of(0.0);
}
public static Speed max(Speed speed1, Speed speed2) {
if (speed1.greaterThan(speed2)) {
return speed1;
}
return speed2;
}
public static Speed absDiff(Speed speed1, Speed speed2) {
//TODO Why Math.abs? Seems to be a leftover.
return Speed.of(Math.abs(speed1.speed_mps - speed2.speed_mps));
}
// Anything faster than that (in meters per second) will be considered moving.
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private final double speed_mps;
private Speed(double speed_mps) {
this.speed_mps = speed_mps;
}
public Speed mul(double factor) {
return new Speed(factor * speed_mps);
}
public boolean isZero() {
return speed_mps == 0;
}
public boolean isInvalid() {
return Double.isNaN(speed_mps) || Double.isInfinite(speed_mps);
}
public boolean isMoving() {
return !isInvalid() && speed_mps >= MAX_NO_MOVEMENT_SPEED;
}
public boolean lessThan(Speed speed) {
return !greaterThan(speed);
}
public boolean greaterThan(Speed speed) {
return speed_mps > speed.speed_mps;
}
public boolean greaterOrEqualThan(Speed speed) {
return speed_mps >= speed.speed_mps;
}
public double toMPS() {
return speed_mps;
}
public double toKMH() {
return speed_mps * UnitConversions.MPS_TO_KMH;
}
public double toMPH() {
return toKMH() * UnitConversions.KM_TO_MI;
}
public Duration toPace(boolean metricUnit) {
if (isZero()) {
return Duration.ofSeconds(0);
}
double distance = speed_mps * (metricUnit ? UnitConversions.M_TO_KM : UnitConversions.M_TO_MI);
return Duration.ofSeconds(Math.round(1 / distance));
}
public double to(boolean metricUnit) {
return to(metricUnit ? Unit.KMH : Unit.MPH);
}
public double to(Unit unit) {
switch (unit) {
case KMH:
return toKMH();
case MPH:
return toMPH();
default:
throw new RuntimeException("Not implemented");
}
}
public enum Unit {
KMH,
MPH,
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Speed speed = (Speed) o;
return Double.compare(speed.speed_mps, speed_mps) == 0;
}
@Override
public int hashCode() {
return Objects.hash(speed_mps);
}
@Override
public String toString() {
return "Speed{" +
"speed_mps=" + speed_mps +
'}';
}
}
@@ -45,9 +45,6 @@ public class TrackPoint {
private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1); private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1);
// Anything faster than that (in meters per second) will be considered moving.
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private TrackPoint.Id id; private TrackPoint.Id id;
private Instant time; private Instant time;
@@ -55,9 +52,9 @@ public class TrackPoint {
private Double longitude; private Double longitude;
private Float accuracy; private Float accuracy;
private Double altitude_m; private Double altitude_m;
private Float speed_mps; private Speed speed;
private Float bearing; private Float bearing;
private Float sensorDistance_m; private Distance sensorDistance_m;
public enum Type { public enum Type {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume) SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
@@ -106,7 +103,7 @@ public class TrackPoint {
this.latitude = location.getLatitude(); this.latitude = location.getLatitude();
this.longitude = location.getLongitude(); this.longitude = location.getLongitude();
this.altitude_m = location.getAltitude(); this.altitude_m = location.getAltitude();
this.speed_mps = location.getSpeed(); this.speed = Speed.of(location.getSpeed());
this.accuracy = location.getAccuracy(); this.accuracy = location.getAccuracy();
setTime(Instant.now()); setTime(Instant.now());
@@ -272,19 +269,19 @@ public class TrackPoint {
} }
public boolean hasSpeed() { public boolean hasSpeed() {
return speed_mps != null; return speed != null;
} }
public float getSpeed() { public Speed getSpeed() {
return speed_mps; return speed;
} }
public void setSpeed(Float speed) { public void setSpeed(Speed speed) {
this.speed_mps = speed; this.speed = speed;
} }
public boolean isMoving() { public boolean isMoving() {
return hasSpeed() && getSpeed() >= MAX_NO_MOVEMENT_SPEED; return hasSpeed() && getSpeed().isMoving();
} }
public boolean hasBearing() { public boolean hasBearing() {
@@ -311,12 +308,12 @@ public class TrackPoint {
this.accuracy = horizontalAccuracy; this.accuracy = horizontalAccuracy;
} }
public float distanceToPrevious(@NonNull TrackPoint previous) { public Distance distanceToPrevious(@NonNull TrackPoint previous) {
if (hasSensorDistance()) { if (hasSensorDistance()) {
return getSensorDistance(); return getSensorDistance();
} }
return getLocation().distanceTo(previous.getLocation()); return Distance.of(getLocation().distanceTo(previous.getLocation()));
} }
public boolean fulfillsAccuracy(int poorAccuracy) { public boolean fulfillsAccuracy(int poorAccuracy) {
@@ -336,11 +333,11 @@ public class TrackPoint {
return sensorDistance_m != null; return sensorDistance_m != null;
} }
public Float getSensorDistance() { public Distance getSensorDistance() {
return sensorDistance_m; return sensorDistance_m;
} }
public void setSensorDistance(Float distance_m) { public void setSensorDistance(Distance distance_m) {
this.sensorDistance_m = distance_m; this.sensorDistance_m = distance_m;
} }
@@ -39,8 +39,10 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
import de.dennisguse.opentracks.BuildConfig; import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns; import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.Speed;
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.data.TrackPointsColumns; import de.dennisguse.opentracks.content.data.TrackPointsColumns;
@@ -127,7 +129,7 @@ public class ContentProviderUtils {
trackStatistics.setStopTime(Instant.ofEpochMilli(cursor.getLong(stopTimeIndex))); trackStatistics.setStopTime(Instant.ofEpochMilli(cursor.getLong(stopTimeIndex)));
} }
if (!cursor.isNull(totalDistanceIndex)) { if (!cursor.isNull(totalDistanceIndex)) {
trackStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex)); trackStatistics.setTotalDistance(Distance.of(cursor.getFloat(totalDistanceIndex)));
} }
if (!cursor.isNull(totalTimeIndex)) { if (!cursor.isNull(totalTimeIndex)) {
trackStatistics.setTotalTime(Duration.ofMillis(cursor.getLong(totalTimeIndex))); trackStatistics.setTotalTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
@@ -136,7 +138,7 @@ public class ContentProviderUtils {
trackStatistics.setMovingTime(Duration.ofMillis(cursor.getLong(movingTimeIndex))); trackStatistics.setMovingTime(Duration.ofMillis(cursor.getLong(movingTimeIndex)));
} }
if (!cursor.isNull(maxSpeedIndex)) { if (!cursor.isNull(maxSpeedIndex)) {
trackStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex)); trackStatistics.setMaxSpeed(Speed.of(cursor.getFloat(maxSpeedIndex)));
} }
if (!cursor.isNull(minAltitudeIndex)) { if (!cursor.isNull(minAltitudeIndex)) {
trackStatistics.setMinAltitude(cursor.getFloat(minAltitudeIndex)); trackStatistics.setMinAltitude(cursor.getFloat(minAltitudeIndex));
@@ -278,12 +280,12 @@ public class ContentProviderUtils {
if (trackStatistics.getStopTime() != null) { if (trackStatistics.getStopTime() != null) {
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime().toEpochMilli()); values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime().toEpochMilli());
} }
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance()); values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance().toM());
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime().toMillis()); values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime().toMillis());
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime().toMillis()); values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime().toMillis());
values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed()); values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed().toMPS());
values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed()); values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed().toMPS());
values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed()); values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed().toMPS());
values.put(TracksColumns.MIN_ALTITUDE, trackStatistics.getMinAltitude()); values.put(TracksColumns.MIN_ALTITUDE, trackStatistics.getMinAltitude());
values.put(TracksColumns.MAX_ALTITUDE, trackStatistics.getMaxAltitude()); values.put(TracksColumns.MAX_ALTITUDE, trackStatistics.getMaxAltitude());
values.put(TracksColumns.ALTITUDE_GAIN, trackStatistics.getTotalAltitudeGain()); values.put(TracksColumns.ALTITUDE_GAIN, trackStatistics.getTotalAltitudeGain());
@@ -343,7 +345,7 @@ public class ContentProviderUtils {
marker.setIcon(cursor.getString(iconIndex)); marker.setIcon(cursor.getString(iconIndex));
} }
if (!cursor.isNull(lengthIndex)) { if (!cursor.isNull(lengthIndex)) {
marker.setLength(cursor.getFloat(lengthIndex)); marker.setLength(Distance.of(cursor.getFloat(lengthIndex)));
} }
if (!cursor.isNull(durationIndex)) { if (!cursor.isNull(durationIndex)) {
marker.setDuration(Duration.ofMillis(cursor.getLong(durationIndex))); marker.setDuration(Duration.ofMillis(cursor.getLong(durationIndex)));
@@ -480,7 +482,7 @@ public class ContentProviderUtils {
values.put(MarkerColumns.CATEGORY, marker.getCategory()); values.put(MarkerColumns.CATEGORY, marker.getCategory());
values.put(MarkerColumns.ICON, marker.getIcon()); values.put(MarkerColumns.ICON, marker.getIcon());
values.put(MarkerColumns.TRACKID, marker.getTrackId().getId()); values.put(MarkerColumns.TRACKID, marker.getTrackId().getId());
values.put(MarkerColumns.LENGTH, marker.getLength()); values.put(MarkerColumns.LENGTH, marker.getLength().toM());
values.put(MarkerColumns.DURATION, marker.getDuration().toMillis()); values.put(MarkerColumns.DURATION, marker.getDuration().toMillis());
values.put(MarkerColumns.LONGITUDE, (int) (marker.getLongitude() * 1E6)); values.put(MarkerColumns.LONGITUDE, (int) (marker.getLongitude() * 1E6));
@@ -543,7 +545,7 @@ public class ContentProviderUtils {
trackPoint.setAccuracy(cursor.getFloat(indexes.accuracyIndex)); trackPoint.setAccuracy(cursor.getFloat(indexes.accuracyIndex));
} }
if (!cursor.isNull(indexes.speedIndex)) { if (!cursor.isNull(indexes.speedIndex)) {
trackPoint.setSpeed(cursor.getFloat(indexes.speedIndex)); trackPoint.setSpeed(Speed.of(cursor.getFloat(indexes.speedIndex)));
} }
if (!cursor.isNull(indexes.bearingIndex)) { if (!cursor.isNull(indexes.bearingIndex)) {
trackPoint.setBearing(cursor.getFloat(indexes.bearingIndex)); trackPoint.setBearing(cursor.getFloat(indexes.bearingIndex));
@@ -556,7 +558,7 @@ public class ContentProviderUtils {
trackPoint.setCyclingCadence_rpm(cursor.getFloat(indexes.sensorCadenceIndex)); trackPoint.setCyclingCadence_rpm(cursor.getFloat(indexes.sensorCadenceIndex));
} }
if (!cursor.isNull(indexes.sensorDistanceIndex)) { if (!cursor.isNull(indexes.sensorDistanceIndex)) {
trackPoint.setSensorDistance(cursor.getFloat(indexes.sensorDistanceIndex)); trackPoint.setSensorDistance(Distance.of(cursor.getFloat(indexes.sensorDistanceIndex)));
} }
if (!cursor.isNull(indexes.sensorPowerIndex)) { if (!cursor.isNull(indexes.sensorPowerIndex)) {
trackPoint.setPower(cursor.getFloat(indexes.sensorPowerIndex)); trackPoint.setPower(cursor.getFloat(indexes.sensorPowerIndex));
@@ -694,7 +696,7 @@ public class ContentProviderUtils {
values.put(TrackPointsColumns.ACCURACY, trackPoint.getAccuracy()); values.put(TrackPointsColumns.ACCURACY, trackPoint.getAccuracy());
} }
if (trackPoint.hasSpeed()) { if (trackPoint.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed()); values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed().toMPS());
} }
if (trackPoint.hasBearing()) { if (trackPoint.hasBearing()) {
values.put(TrackPointsColumns.BEARING, trackPoint.getBearing()); values.put(TrackPointsColumns.BEARING, trackPoint.getBearing());
@@ -707,7 +709,7 @@ public class ContentProviderUtils {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCyclingCadence_rpm()); values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCyclingCadence_rpm());
} }
if (trackPoint.hasSensorDistance()) { if (trackPoint.hasSensorDistance()) {
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance()); values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance().toM());
} }
if (trackPoint.hasPower()) { if (trackPoint.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower()); values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower());
@@ -6,6 +6,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.util.Pair; import androidx.core.util.Pair;
import java.time.Duration;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.util.UintUtils; import de.dennisguse.opentracks.util.UintUtils;
import de.dennisguse.opentracks.util.UnitConversions; import de.dennisguse.opentracks.util.UnitConversions;
@@ -122,21 +126,21 @@ public final class SensorDataCycling {
public void compute(DistanceSpeed previous, int wheel_circumference_mm) { public void compute(DistanceSpeed previous, int wheel_circumference_mm) {
if (hasData() && previous != null && previous.hasData()) { if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS; float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
if (timeDiff_ms <= 0) { Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence."); Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
value = null; value = null;
} else { } else {
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX); long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as it some of those seem to count backwards wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as some of those seem to count backwards
double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S; Distance distance = Distance.of(wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M);
float distance_m = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M); Distance distanceOverall = distance;
float distance_overall_m = distance_m;
if (previous.hasValue()) { if (previous.hasValue()) {
distance_overall_m += previous.getValue().distance_overall_m; distanceOverall = distance.plus(previous.getValue().distanceOverall);
} }
float speed_mps = (float) (distance_m / timeDiff_s); Speed speed_mps = Speed.of(distance, timeDiff);
value = new Data(distance_m, distance_overall_m, speed_mps); value = new Data(distance, distanceOverall, speed_mps);
} }
} }
} }
@@ -144,7 +148,7 @@ public final class SensorDataCycling {
@Override @Override
public void reset() { public void reset() {
if (value != null) { if (value != null) {
value = new Data(value.distance_m, 0, value.speed_mps); value = new Data(value.distance, Distance.of(0), value.speed);
} }
} }
@@ -167,22 +171,22 @@ public final class SensorDataCycling {
} }
public static class Data { public static class Data {
public final float distance_m; public final Distance distance;
public final float distance_overall_m; public final Distance distanceOverall;
public final float speed_mps; public final Speed speed;
private Data(float distance_m, float distance_overall_m, float speed_mps) { private Data(Distance distance, Distance distanceOverall, Speed speed) {
this.distance_m = distance_m; this.distance = distance;
this.distance_overall_m = distance_overall_m; this.distanceOverall = distanceOverall;
this.speed_mps = speed_mps; this.speed = speed;
} }
@Override @Override
public String toString() { public String toString() {
return "Data{" + return "Data{" +
"distance_m=" + distance_m + "distance_m=" + distance +
", distance_overall_m=" + distance_overall_m + ", distance_overall_m=" + distanceOverall +
", speed_mps=" + speed_mps + ", speed_mps=" + speed +
'}'; '}';
} }
} }
@@ -58,8 +58,8 @@ public final class SensorDataSet {
} }
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) { if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) {
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distance_overall_m); trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distanceOverall);
trackPoint.setSpeed(cyclingDistanceSpeed.getValue().speed_mps); trackPoint.setSpeed(cyclingDistanceSpeed.getValue().speed);
} }
if (cyclingPower != null && cyclingPower.hasValue()) { if (cyclingPower != null && cyclingPower.hasValue()) {
@@ -37,6 +37,7 @@ import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView; import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.TrackDataHub; import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener; import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Marker;
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;
@@ -66,7 +67,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
} }
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private int recordingDistanceInterval; private Distance recordingDistanceInterval;
private TrackDataHub trackDataHub; private TrackDataHub trackDataHub;
@@ -346,4 +347,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
void setChartByDistance(boolean value) { void setChartByDistance(boolean value) {
chartByDistance = value; chartByDistance = value;
} }
@VisibleForTesting
void setRecordingDistanceInterval(Distance value) {
recordingDistanceInterval = value;
}
} }
@@ -180,8 +180,7 @@ public class StatisticsRecordedFragment extends Fragment {
private void updateUI() { private void updateUI() {
// Set total distance // Set total distance
{ {
double totalDistance = trackStatistics.getTotalDistance(); Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), trackStatistics.getTotalDistance(), preferenceMetricUnits);
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first); viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second); viewBinding.statsDistanceUnit.setText(parts.second);
@@ -201,32 +200,27 @@ public class StatisticsRecordedFragment extends Fragment {
// Set average speed/pace // Set average speed/pace
{ {
double speed = trackStatistics.getAverageSpeed();
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace); viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed); Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getAverageSpeed(), preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsAverageSpeedValue.setText(parts.first); viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second); viewBinding.statsAverageSpeedUnit.setText(parts.second);
} }
// Set max speed/pace // Set max speed/pace
{ {
double speed = trackStatistics.getMaxSpeed();
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace); viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed); Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getMaxSpeed(), preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMaxSpeedValue.setText(parts.first); viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second); viewBinding.statsMaxSpeedUnit.setText(parts.second);
} }
// Set moving speed/pace // Set moving speed/pace
{ {
double speed = trackStatistics.getAverageMovingSpeed();
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace); viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed); Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getAverageMovingSpeed(), preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMovingSpeedValue.setText(parts.first); viewBinding.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second); viewBinding.statsMovingSpeedUnit.setText(parts.second);
} }
@@ -24,7 +24,9 @@ import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.adapters.SensorsAdapter; import de.dennisguse.opentracks.adapters.SensorsAdapter;
import de.dennisguse.opentracks.content.TrackDataHub; import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener; import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
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.sensor.SensorDataCycling; import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
@@ -371,18 +373,18 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
SensorDataCycling.DistanceSpeed data = sensorDataSet.getCyclingDistanceSpeed(); SensorDataCycling.DistanceSpeed data = sensorDataSet.getCyclingDistanceSpeed();
if (data.hasValue() && data.isRecent()) { if (data.hasValue() && data.isRecent()) {
setTotalDistance(data.getValue().distance_overall_m); setTotalDistance(data.getValue().distanceOverall);
setSpeed(data.getValue().speed_mps); setSpeed(data.getValue().speed);
} }
if (data.hasValue() && data.isRecent()) { if (data.hasValue() && data.isRecent()) {
setSpeed(data.getValue().speed_mps); setSpeed(data.getValue().speed);
} }
} }
} }
private void updateStats() { private void updateStats() {
setTotalDistance(0); setTotalDistance(Distance.of(0)); //TODO Why?
// Set activity type // Set activity type
{ {
@@ -399,7 +401,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set average speed/pace // Set average speed/pace
{ {
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : Double.NaN; Speed speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : null;
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace); viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed); Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
@@ -409,7 +411,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set max speed/pace // Set max speed/pace
{ {
double speed = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getMaxSpeed(); Speed speed = lastTrackStatistics != null ? lastTrackStatistics.getMaxSpeed() : null;
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace); viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
@@ -420,7 +422,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set moving speed/pace // Set moving speed/pace
{ {
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : Double.NaN; Speed speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : null;
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace); viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
@@ -449,7 +451,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private void setLocationValues() { private void setLocationValues() {
// Set speed/pace // Set speed/pace
double speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : Double.NaN; Speed speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : null;
setSpeed(speed); setSpeed(speed);
// Set altitude // Set altitude
@@ -477,15 +479,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
} }
} }
private void setTotalDistance(double sensorDistanceSinceLastTrackpoint) { private void setTotalDistance(Distance sensorDistanceSinceLastTrackpoint) {
double totalDistance = lastTrackStatistics != null ? (lastTrackStatistics.getTotalDistance() + sensorDistanceSinceLastTrackpoint) : Double.NaN; Distance totalDistance = lastTrackStatistics != null ? (lastTrackStatistics.getTotalDistance().plus(sensorDistanceSinceLastTrackpoint)) : Distance.invalid();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits); Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first); viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second); viewBinding.statsDistanceUnit.setText(parts.second);
} }
private void setSpeed(double speed) { private void setSpeed(Speed speed) {
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace); viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed); Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
@@ -290,7 +290,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<extensions><gpxtpx:TrackPointExtension>"); printWriter.println("<extensions><gpxtpx:TrackPointExtension>");
if (trackPoint.hasSpeed()) { if (trackPoint.hasSpeed()) {
printWriter.println("<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed()) + "</gpxtpx:speed>"); printWriter.println("<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed().toMPS()) + "</gpxtpx:speed>");
} }
if (trackPoint.hasHeartRate()) { if (trackPoint.hasHeartRate()) {
@@ -395,10 +395,10 @@ public class KMLTrackExporter implements TrackExporter {
} else { } else {
printWriter.println("<gx:coord/>"); printWriter.println("<gx:coord/>");
} }
speedList.add(trackPoint.hasSpeed() ? trackPoint.getSpeed() : null); speedList.add(trackPoint.hasSpeed() ? (float) trackPoint.getSpeed().toMPS() : null);
if (exportSensorData) { if (exportSensorData) {
distanceList.add(trackPoint.hasSensorDistance() ? trackPoint.getSensorDistance() : null); distanceList.add(trackPoint.hasSensorDistance() ? (float) trackPoint.getSensorDistance().toM() : null);
heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate_bpm() : null); heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate_bpm() : null);
cadenceList.add(trackPoint.hasCyclingCadence() ? trackPoint.getCyclingCadence_rpm() : null); cadenceList.add(trackPoint.hasCyclingCadence() ? trackPoint.getCyclingCadence_rpm() : null);
powerList.add(trackPoint.hasPower() ? trackPoint.getPower() : null); powerList.add(trackPoint.hasPower() ? trackPoint.getPower() : null);
@@ -42,11 +42,14 @@ import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory; import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
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.content.provider.TrackPointIterator; import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.FileUtils; import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.LocationUtils;
@@ -65,7 +68,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
private final Context context; private final Context context;
private final ContentProviderUtils contentProviderUtils; private final ContentProviderUtils contentProviderUtils;
private final int recordingDistanceInterval; private final Distance recordingDistanceInterval;
private final List<Track.Id> trackIds = new ArrayList<>(); private final List<Track.Id> trackIds = new ArrayList<>();
private final List<Marker> markers = new ArrayList<>(); private final List<Marker> markers = new ArrayList<>();
@@ -202,11 +205,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (trackPoint.getLatitude() == marker.getLatitude() && trackPoint.getLongitude() == marker.getLongitude()) { if (trackPoint.getLatitude() == marker.getLatitude() && trackPoint.getLongitude() == marker.getLongitude()) {
String markerDescription = marker.getDescription(); String markerDescription = marker.getDescription();
String icon = context.getString(R.string.marker_icon_url); String icon = context.getString(R.string.marker_icon_url);
double length = trackStatisticsUpdater.getTrackStatistics().getTotalDistance(); TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
Duration duration = trackStatisticsUpdater.getTrackStatistics().getTotalTime();
// Insert marker // Insert marker
Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), length, duration, trackPoint, marker.getPhotoUrl()); Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), stats, trackPoint, marker.getPhotoUrl());
contentProviderUtils.insertMarker(newMarker); contentProviderUtils.insertMarker(newMarker);
} }
@@ -374,17 +376,16 @@ 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.
*/ */
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) { if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
float speed = trackData.lastLocationInCurrentSegment.distanceToPrevious(trackPoint) / timeDifference.toMillis(); trackPoint.setSpeed(Speed.of(trackData.lastLocationInCurrentSegment.distanceToPrevious(trackPoint), timeDifference));
trackPoint.setSpeed(speed);
} }
} }
} }
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) { if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint)); trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
long maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); //TODO Should only be read once! Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); //TODO Should only be read once!
double distanceToLastTrackLocation = trackPoint.distanceToPrevious(trackData.lastLocationInCurrentSegment); Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(trackData.lastLocationInCurrentSegment);
if (distanceToLastTrackLocation > maxRecordingDistance) { if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC); trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
} }
} }
@@ -460,7 +461,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (speed != null) { if (speed != null) {
try { try {
trackPoint.setSpeed(Float.parseFloat(speed)); trackPoint.setSpeed(Speed.of(speed));
} catch (Exception e) { } catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e); throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
} }
@@ -505,7 +506,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
} }
if (distance != null) { if (distance != null) {
try { try {
trackPoint.setSensorDistance(Float.parseFloat(distance)); trackPoint.setSensorDistance(Distance.of(distance));
} catch (Exception e) { } catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e); throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
} }
@@ -26,6 +26,8 @@ import org.xml.sax.SAXException;
import java.util.ArrayList; import java.util.ArrayList;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
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.exporter.KMLTrackExporter; import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
@@ -231,11 +233,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
for (int i = 0; i < trackPoints.size(); i++) { for (int i = 0; i < trackPoints.size(); i++) {
TrackPoint trackPoint = trackPoints.get(i); TrackPoint trackPoint = trackPoints.get(i);
if (i < speedList.size()) { if (i < speedList.size() && speedList.get(i) != null) {
trackPoint.setSpeed(speedList.get(i)); trackPoint.setSpeed(Speed.of(speedList.get(i)));
} }
if (i < distanceList.size()) { if (i < distanceList.size() && distanceList.get(i) != null) {
trackPoint.setSensorDistance(distanceList.get(i)); trackPoint.setSensorDistance(Distance.of(distanceList.get(i)));
} }
if (i < heartRateList.size()) { if (i < heartRateList.size()) {
trackPoint.setHeartRate_bpm(heartRateList.get(i)); trackPoint.setHeartRate_bpm(heartRateList.get(i));
@@ -40,6 +40,7 @@ import java.time.Instant;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackListActivity; import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.TrackRecordingActivity; import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker; import de.dennisguse.opentracks.content.data.Marker;
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;
@@ -80,8 +81,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
private int recordingDistanceInterval; private Distance recordingDistanceInterval;
private int maxRecordingDistance; private Distance maxRecordingDistance;
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
@@ -237,11 +238,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
photoUrl = photoUrl != null ? photoUrl : ""; photoUrl = photoUrl != null ? photoUrl : "";
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics(); TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
double length = stats.getTotalDistance();
Duration duration = stats.getTotalTime();
// Insert marker // Insert marker
Marker marker = new Marker(name, description, category, icon, serviceStatus.getRecordingTrackId(), length, duration, trackPoint, photoUrl); Marker marker = new Marker(name, description, category, icon, serviceStatus.getRecordingTrackId(), stats, trackPoint, photoUrl);
Uri uri = contentProviderUtils.insertMarker(marker); Uri uri = contentProviderUtils.insertMarker(marker);
return new Marker.Id(ContentUris.parseId(uri)); return new Marker.Id(ContentUris.parseId(uri));
} }
@@ -511,8 +510,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return; return;
} }
double distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastValidTrackPoint); Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastValidTrackPoint);
if (distanceToLastTrackLocation > maxRecordingDistance) { if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
insertTrackPointIfNewer(track, lastTrackPoint); insertTrackPointIfNewer(track, lastTrackPoint);
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC); trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
@@ -523,7 +522,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return; return;
} }
if (trackPoint.hasSensorData() || distanceToLastTrackLocation >= recordingDistanceInterval) { if (trackPoint.hasSensorData() || distanceToLastTrackLocation.greaterOrEqualThan(recordingDistanceInterval)) {
insertTrackPointIfNewer(track, lastTrackPoint); insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint); insertTrackPoint(track, trackPoint);
@@ -11,6 +11,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
@@ -68,7 +69,7 @@ class TrackRecordingServiceNotificationManager {
void updateTrackPoint(Context context, TrackStatistics trackStatistics, TrackPoint trackPoint, int recordingGpsAccuracy) { void updateTrackPoint(Context context, TrackStatistics trackStatistics, TrackPoint trackPoint, int recordingGpsAccuracy) {
String formattedAccuracy = context.getString(R.string.value_none); String formattedAccuracy = context.getString(R.string.value_none);
if (trackPoint.hasAccuracy()) { if (trackPoint.hasAccuracy()) {
formattedAccuracy = StringUtils.formatDistance(context, trackPoint.getAccuracy(), metricUnits); formattedAccuracy = StringUtils.formatDistance(context, Distance.of(trackPoint.getAccuracy()), metricUnits);
boolean currentLocationWasAccurate = trackPoint.getAccuracy() < recordingGpsAccuracy; boolean currentLocationWasAccurate = trackPoint.getAccuracy() < recordingGpsAccuracy;
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate; boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
@@ -25,7 +25,7 @@ class GpsStatus {
private static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(10); private static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(10);
// Threshold for accuracy. // Threshold for accuracy.
private double signalBadThreshold; private double signalBadThreshold; //TODO Distance?
// Threshold for time without points. // Threshold for time without points.
private Duration signalLostThreshold; private Duration signalLostThreshold;
@@ -67,7 +67,7 @@ class GpsStatus {
this.context = context; this.context = context;
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context); SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
signalBadThreshold = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context); signalBadThreshold = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context).toM();
Duration minRecordingInterval = Duration.ofSeconds(PreferencesUtils.getMinRecordingInterval(sharedPreferences, context)); Duration minRecordingInterval = Duration.ofSeconds(PreferencesUtils.getMinRecordingInterval(sharedPreferences, context));
signalLostThreshold = !minRecordingInterval.isNegative() ? SIGNAL_LOST_THRESHOLD.plus(minRecordingInterval) : SIGNAL_LOST_THRESHOLD; signalLostThreshold = !minRecordingInterval.isNegative() ? SIGNAL_LOST_THRESHOLD.plus(minRecordingInterval) : SIGNAL_LOST_THRESHOLD;
@@ -78,7 +78,7 @@ class LocationHandler implements HandlerServer.Handler, LocationListener, GpsSta
} }
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) { if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
if (gpsStatus != null) { if (gpsStatus != null) {
gpsStatus.onRecordingDistanceChanged(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context)); gpsStatus.onRecordingDistanceChanged((int) PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context).toM()); //TODO Use Distance?
} }
} }
} }
@@ -20,9 +20,9 @@ import android.util.Log;
import java.time.Duration; import java.time.Duration;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.UnitConversions;
/** /**
* Execute a periodic task on a time or distance schedule. * Execute a periodic task on a time or distance schedule.
@@ -128,12 +128,9 @@ public class PeriodicTaskExecutor {
return; return;
} }
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM; Distance distance = trackStatistics.getTotalDistance();
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
if (distance > nextTaskDistance) { if (distance.greaterThan(Distance.of(nextTaskDistance))) {
periodicTask.run(trackRecordingService); periodicTask.run(trackRecordingService);
calculateNextTaskDistance(); calculateNextTaskDistance();
} }
@@ -178,10 +175,7 @@ public class PeriodicTaskExecutor {
return; return;
} }
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM; double distance = trackStatistics.getTotalDistance().to(metricUnits);
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
// The index will be negative since the frequency is negative. // The index will be negative since the frequency is negative.
int index = (int) (distance / taskFrequency); int index = (int) (distance / taskFrequency);
@@ -32,7 +32,7 @@ final class PreferenceHelper {
String[] entryValues = context.getResources().getStringArray(R.array.recording_distance_interval_values); String[] entryValues = context.getResources().getStringArray(R.array.recording_distance_interval_values);
String[] entries = new String[entryValues.length]; String[] entries = new String[entryValues.length];
final int recordingDistanceIntervalDefault = PreferencesUtils.getRecordingDistanceIntervalDefault(context); final int recordingDistanceIntervalDefault = (int) PreferencesUtils.getRecordingDistanceIntervalDefault(context).toM();
for (int i = 0; i < entryValues.length; i++) { for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]); int value = Integer.parseInt(entryValues[i]);
@@ -22,6 +22,9 @@ import androidx.annotation.Nullable;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
/** /**
* Statistical data about a {@link de.dennisguse.opentracks.content.data.Track}. * Statistical data about a {@link de.dennisguse.opentracks.content.data.Track}.
* The data in this class should be filled out by {@link TrackStatisticsUpdater}. * The data in this class should be filled out by {@link TrackStatisticsUpdater}.
@@ -40,13 +43,13 @@ public class TrackStatistics {
// The track stop time. // The track stop time.
private Instant stopTime; private Instant stopTime;
private double totalDistance_m; private Distance totalDistance;
// Updated when new points are received, may be stale. // Updated when new points are received, may be stale.
private Duration totalTime; private Duration totalTime;
// Based on when we believe the user is traveling. // Based on when we believe the user is traveling.
private Duration movingTime; private Duration movingTime;
// The maximum speed (meters/second) that we believe is valid. // The maximum speed (meters/second) that we believe is valid.
private double maxSpeed_mps; private Speed maxSpeed;
private Float totalAltitudeGain_m = null; private Float totalAltitudeGain_m = null;
private Float totalAltitudeLoss_m = null; private Float totalAltitudeLoss_m = null;
@@ -62,10 +65,10 @@ public class TrackStatistics {
public TrackStatistics(TrackStatistics other) { public TrackStatistics(TrackStatistics other) {
startTime = other.startTime; startTime = other.startTime;
stopTime = other.stopTime; stopTime = other.stopTime;
totalDistance_m = other.totalDistance_m; totalDistance = other.totalDistance;
totalTime = other.totalTime; totalTime = other.totalTime;
movingTime = other.movingTime; movingTime = other.movingTime;
maxSpeed_mps = other.maxSpeed_mps; maxSpeed = other.maxSpeed;
altitudeExtremities.set(other.altitudeExtremities.getMin(), other.altitudeExtremities.getMax()); altitudeExtremities.set(other.altitudeExtremities.getMin(), other.altitudeExtremities.getMax());
totalAltitudeGain_m = other.totalAltitudeGain_m; totalAltitudeGain_m = other.totalAltitudeGain_m;
totalAltitudeLoss_m = other.totalAltitudeLoss_m; totalAltitudeLoss_m = other.totalAltitudeLoss_m;
@@ -89,10 +92,10 @@ public class TrackStatistics {
stopTime = stopTime.isAfter(other.stopTime) ? stopTime : other.stopTime; stopTime = stopTime.isAfter(other.stopTime) ? stopTime : other.stopTime;
} }
totalDistance_m += other.totalDistance_m; totalDistance = totalDistance.plus(other.totalDistance);
totalTime = totalTime.plus(other.totalTime); totalTime = totalTime.plus(other.totalTime);
movingTime = movingTime.plus(other.movingTime); movingTime = movingTime.plus(other.movingTime);
maxSpeed_mps = Math.max(maxSpeed_mps, other.maxSpeed_mps); maxSpeed = Speed.max(maxSpeed, other.maxSpeed);
if (other.altitudeExtremities.hasData()) { if (other.altitudeExtremities.hasData()) {
altitudeExtremities.update(other.altitudeExtremities.getMin()); altitudeExtremities.update(other.altitudeExtremities.getMin());
altitudeExtremities.update(other.altitudeExtremities.getMax()); altitudeExtremities.update(other.altitudeExtremities.getMax());
@@ -121,10 +124,10 @@ public class TrackStatistics {
startTime = null; startTime = null;
stopTime = null; stopTime = null;
setTotalDistance(0); setTotalDistance(Distance.of(0));
setTotalTime(Duration.ofSeconds(0)); setTotalTime(Duration.ofSeconds(0));
setMovingTime(Duration.ofSeconds(0)); setMovingTime(Duration.ofSeconds(0));
setMaxSpeed(0); setMaxSpeed(Speed.zero());
setTotalAltitudeGain(null); setTotalAltitudeGain(null);
setTotalAltitudeLoss(null); setTotalAltitudeLoss(null);
} }
@@ -157,16 +160,16 @@ public class TrackStatistics {
this.stopTime = stopTime; this.stopTime = stopTime;
} }
public double getTotalDistance() { public Distance getTotalDistance() {
return totalDistance_m; return totalDistance;
} }
public void setTotalDistance(double totalDistance_m) { public void setTotalDistance(Distance totalDistance_m) {
this.totalDistance_m = totalDistance_m; this.totalDistance = totalDistance_m;
} }
public void addTotalDistance(double distance_m) { public void addTotalDistance(Distance distance_m) {
totalDistance_m += distance_m; totalDistance = totalDistance.plus(distance_m);
} }
/** /**
@@ -195,35 +198,26 @@ public class TrackStatistics {
} }
/** /**
* Gets the average speed in meters/second. * Gets the average speed.
* This calculation only takes into account the displacement until the last point that was accounted for in statistics. * This calculation only takes into account the displacement until the last point that was accounted for in statistics.
*/ */
public double getAverageSpeed() { public Speed getAverageSpeed() {
if (totalTime.isZero()) { if (totalDistance.isZero() && totalDistance.isZero()) {
return 0.0; return Speed.of(0);
} }
return totalDistance_m / (double) totalTime.getSeconds(); return Speed.of(totalDistance.toM() / totalTime.getSeconds());
} }
/** public Speed getAverageMovingSpeed() {
* Gets the average moving speed in meters/second. return Speed.of(totalDistance, movingTime);
*/
public double getAverageMovingSpeed() {
if (movingTime.isZero()) {
return 0.0;
}
return totalDistance_m / (double) movingTime.getSeconds();
} }
/** public Speed getMaxSpeed() {
* Gets the maximum speed in meters/second. return Speed.max(maxSpeed, getAverageMovingSpeed());
*/
public double getMaxSpeed() {
return Math.max(maxSpeed_mps, getAverageMovingSpeed());
} }
public void setMaxSpeed(double maxSpeed_mps) { public void setMaxSpeed(Speed maxSpeed) {
this.maxSpeed_mps = maxSpeed_mps; this.maxSpeed = maxSpeed;
} }
public boolean hasAltitudeMin() { public boolean hasAltitudeMin() {
@@ -23,8 +23,9 @@ import androidx.annotation.VisibleForTesting;
import java.time.Duration; import java.time.Duration;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
/** /**
* Updater for {@link TrackStatistics}. * Updater for {@link TrackStatistics}.
@@ -103,12 +104,12 @@ public class TrackStatisticsUpdater {
* @param trackPoint the trackPoint * @param trackPoint the trackPoint
* @param minGPSDistance the min recording distance * @param minGPSDistance the min recording distance
*/ */
public void addTrackPoint(TrackPoint trackPoint, int minGPSDistance) { public void addTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
internalAddTrackPoint(trackPoint, minGPSDistance); internalAddTrackPoint(trackPoint, minGPSDistance);
Log.d(TAG, this.toString()); Log.d(TAG, this.toString());
} }
private void internalAddTrackPoint(TrackPoint trackPoint, int minGPSDistance) { private void internalAddTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
if (!trackInitialized) { if (!trackInitialized) {
trackStatistics.setStartTime(trackPoint.getTime()); trackStatistics.setStartTime(trackPoint.getTime());
trackInitialized = true; trackInitialized = true;
@@ -158,8 +159,8 @@ public class TrackStatisticsUpdater {
if (!trackPoint.hasSensorDistance()) { if (!trackPoint.hasSensorDistance()) {
// GPS-based distance/speed // GPS-based distance/speed
float movingDistance = lastMovingTrackPoint.distanceToPrevious(trackPoint); Distance movingDistance = lastMovingTrackPoint.distanceToPrevious(trackPoint);
if (movingDistance < minGPSDistance && !trackPoint.isMoving()) { if (movingDistance.lessThan(minGPSDistance) && !trackPoint.isMoving()) {
speedBuffer_mps.reset(); speedBuffer_mps.reset();
lastTrackPoint = trackPoint; lastTrackPoint = trackPoint;
return; return;
@@ -196,13 +197,6 @@ public class TrackStatisticsUpdater {
speedBuffer_mps.reset(); speedBuffer_mps.reset();
} }
public void addTrackPoint(TrackPointIterator iterator, int minRecordingDistance) {
while (iterator.hasNext()) {
TrackPoint location = iterator.next();
addTrackPoint(location, minRecordingDistance);
}
}
/** /**
* Gets the smoothed altitude over several readings. * Gets the smoothed altitude over several readings.
* The altitude readings is noisy so the smoothed altitude is better than the raw altitude for many tasks. * The altitude readings is noisy so the smoothed altitude is better than the raw altitude for many tasks.
@@ -211,8 +205,8 @@ public class TrackStatisticsUpdater {
return altitudeBuffer_m.getAverage(); return altitudeBuffer_m.getAverage();
} }
public double getSmoothedSpeed() { public Speed getSmoothedSpeed() {
return speedBuffer_mps.getAverage(); return Speed.of(speedBuffer_mps.getAverage());
} }
/** /**
@@ -223,9 +217,10 @@ public class TrackStatisticsUpdater {
if (!trackPoint.isMoving()) { if (!trackPoint.isMoving()) {
speedBuffer_mps.reset(); speedBuffer_mps.reset();
} else if (isValidSpeed(trackPoint, lastTrackPoint)) { } else if (isValidSpeed(trackPoint, lastTrackPoint)) {
speedBuffer_mps.setNext(trackPoint.getSpeed()); speedBuffer_mps.setNext(trackPoint.getSpeed().toMPS());
if (speedBuffer_mps.getAverage() > currentSegment.getMaxSpeed()) { Speed average = Speed.of(speedBuffer_mps.getAverage());
currentSegment.setMaxSpeed(speedBuffer_mps.getAverage()); if (average.greaterThan(currentSegment.getMaxSpeed())) {
currentSegment.setMaxSpeed(average);
} }
} else { } else {
Log.d(TAG, "Invalid speed. speed: " + trackPoint.getSpeed() + " lastLocationSpeed: " + lastTrackPoint.getSpeed()); Log.d(TAG, "Invalid speed. speed: " + trackPoint.getSpeed() + " lastLocationSpeed: " + lastTrackPoint.getSpeed());
@@ -243,27 +238,26 @@ public class TrackStatisticsUpdater {
private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) { private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
// There are a lot of noisy speed readings. Do the cheapest checks first, most expensive last. // There are a lot of noisy speed readings. Do the cheapest checks first, most expensive last.
if (trackPoint.getSpeed() == 0) { if (trackPoint.getSpeed().isZero()) {
return false; return false;
} }
// The following code will ignore unlikely readings. 128 m/s seems to be an internal android error code.
if (Math.abs(trackPoint.getSpeed() - 128) < 1) {
return false;
}
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
Duration timeDifference = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime()); Duration timeDifference = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
double speedDifference = Math.abs(lastTrackPoint.getSpeed() - trackPoint.getSpeed()); Speed maxAcceleration = Speed.of(MAX_ACCELERATION * timeDifference.toMillis());
if (speedDifference > MAX_ACCELERATION * timeDifference.toMillis()) { {
return false; // See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
Speed speedDifference = Speed.absDiff(lastTrackPoint.getSpeed(), trackPoint.getSpeed());
if (speedDifference.greaterThan(maxAcceleration)) {
return false;
}
} }
// Only check if the speed buffer is full. Check that the speed is less than 10X the smoothed average and the speed difference doesn't imply 2g acceleration. // Only check if the speed buffer is full. Check that the speed is less than 10X the smoothed average and the speed difference doesn't imply 2g acceleration.
if (speedBuffer_mps.isFull()) { if (speedBuffer_mps.isFull()) {
double average = speedBuffer_mps.getAverage(); Speed average = Speed.of(speedBuffer_mps.getAverage());
double diff = Math.abs(average - trackPoint.getSpeed()); Speed speedDifference = Speed.absDiff(average, trackPoint.getSpeed());
return (trackPoint.getSpeed() < average * 10) && (diff < MAX_ACCELERATION * timeDifference.toMillis());
return trackPoint.getSpeed().lessThan(average.mul(10)) && speedDifference.lessThan(maxAcceleration);
} }
return true; return true;
@@ -7,6 +7,8 @@ import androidx.annotation.Nullable;
import java.time.Duration; import java.time.Duration;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics; import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
@@ -16,17 +18,15 @@ public class AnnouncementUtils {
} }
public static String getAnnouncement(Context context, TrackStatistics trackStatistics, boolean isMetricUnits, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval) { public static String getAnnouncement(Context context, TrackStatistics trackStatistics, boolean isMetricUnits, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval) {
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM; Distance distance = trackStatistics.getTotalDistance();
double distancePerTime = trackStatistics.getAverageMovingSpeed() * UnitConversions.MPS_TO_KMH; Speed distancePerTime = trackStatistics.getAverageMovingSpeed();
double currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed_ms() * UnitConversions.MPS_TO_KMH : 0; double currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed_ms() * UnitConversions.MPS_TO_KMH : 0; //TODO Use Speed?
if (distance == 0) { if (distance.isZero()) {
return context.getString(R.string.voice_total_distance_zero); return context.getString(R.string.voice_total_distance_zero);
} }
if (!isMetricUnits) { if (!isMetricUnits) {
distance *= UnitConversions.KM_TO_MI;
distancePerTime *= UnitConversions.KM_TO_MI;
currentDistancePerTime *= UnitConversions.KM_TO_MI; currentDistancePerTime *= UnitConversions.KM_TO_MI;
} }
@@ -35,24 +35,27 @@ public class AnnouncementUtils {
String currentRateMsg; String currentRateMsg;
if (isReportSpeed) { if (isReportSpeed) {
int speedId = isMetricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour; int speedId = isMetricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour;
rate = context.getResources().getQuantityString(speedId, getQuantityCount(distancePerTime), distancePerTime); double distanceInUnit = distancePerTime.to(isMetricUnits);
rate = context.getResources().getQuantityString(speedId, getQuantityCount(distanceInUnit), distanceInUnit);
currentRate = context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTime), currentDistancePerTime); currentRate = context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTime), currentDistancePerTime);
currentRateMsg = context.getString(R.string.voice_speed_lap, currentRate); currentRateMsg = context.getString(R.string.voice_speed_lap, currentRate);
} else { } else {
double timePerDistance = distancePerTime == 0 ? 0.0 : 1 / distancePerTime; double timePerDistance = distancePerTime.isZero() ? 0.0 : 1 / distancePerTime.to(isMetricUnits); //TODO PACE
int paceId = isMetricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile; int paceId = isMetricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
Duration time = Duration.ofMillis((long) (timePerDistance * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS)); Duration time = Duration.ofMillis((long) (timePerDistance * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS));
rate = context.getString(paceId, getAnnounceTime(context, time)); rate = context.getString(paceId, getAnnounceTime(context, time));
double currentTimePerDistance = currentDistancePerTime == 0 ? 0.0 : 1 / currentDistancePerTime; double currentTimePerDistance = currentDistancePerTime == 0 ? 0.0 : 1 / currentDistancePerTime; //TODO PACE
Duration currentTime = Duration.ofMillis((long) (currentTimePerDistance * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS)); Duration currentTime = Duration.ofMillis((long) (currentTimePerDistance * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS));
currentRate = context.getString(paceId, getAnnounceTime(context, currentTime)); currentRate = context.getString(paceId, getAnnounceTime(context, currentTime));
currentRateMsg = context.getString(R.string.voice_pace_lap, currentRate); currentRateMsg = context.getString(R.string.voice_pace_lap, currentRate);
} }
int totalDistanceId = isMetricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles; int totalDistanceId = isMetricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles;
String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distance), distance); double distanceInUnit = distance.to(isMetricUnits);
String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distanceInUnit), distanceInUnit);
currentRateMsg = currentInterval == null ? "" : " " + currentRateMsg; currentRateMsg = currentInterval == null ? "" : " " + currentRateMsg;
@@ -27,6 +27,7 @@ import androidx.documentfile.provider.DocumentFile;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFileFormat;
/** /**
@@ -180,17 +181,21 @@ public class PreferencesUtils {
return getInt(sharedPreferences, context, R.string.voice_frequency_key, VOICE_FREQUENCY_DEFAULT); return getInt(sharedPreferences, context, R.string.voice_frequency_key, VOICE_FREQUENCY_DEFAULT);
} }
public static int getRecordingDistanceInterval(SharedPreferences sharedPreferences, Context context) { public static Distance getRecordingDistanceInterval(SharedPreferences sharedPreferences, Context context) {
return getInt(sharedPreferences, context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefault(context)); return Distance.of(getInt(sharedPreferences, context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefaultInternal(context)));
} }
public static int getRecordingDistanceIntervalDefault(Context context) { public static Distance getRecordingDistanceIntervalDefault(Context context) {
return Distance.of(getRecordingDistanceIntervalDefaultInternal(context));
}
private static int getRecordingDistanceIntervalDefaultInternal(Context context) {
return Integer.parseInt(context.getResources().getString(R.string.recording_distance_interval_default)); return Integer.parseInt(context.getResources().getString(R.string.recording_distance_interval_default));
} }
public static int getMaxRecordingDistance(SharedPreferences sharedPreferences, Context context) { public static Distance getMaxRecordingDistance(SharedPreferences sharedPreferences, Context context) {
final int MAX_RECORDING_DISTANCE = Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default)); final int MAX_RECORDING_DISTANCE = Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default));
return getInt(sharedPreferences, context, R.string.max_recording_distance_key, MAX_RECORDING_DISTANCE); return Distance.of(getInt(sharedPreferences, context, R.string.max_recording_distance_key, MAX_RECORDING_DISTANCE));
} }
//TODO Duration //TODO Duration
@@ -36,6 +36,8 @@ import java.time.temporal.TemporalAccessor;
import java.util.Locale; import java.util.Locale;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
/** /**
* Various string manipulation methods. * Various string manipulation methods.
@@ -86,50 +88,38 @@ public class StringUtils {
* Formats the distance in meters. * Formats the distance in meters.
* *
* @param context the context * @param context the context
* @param distance_m the distance_m * @param distance the distance
* @param metricUnits true to use metric units. False to use imperial units * @param metricUnits true to use metric units. False to use imperial units
*/ */
public static String formatDistance(Context context, double distance_m, boolean metricUnits) { public static String formatDistance(Context context, Distance distance, boolean metricUnits) {
if (Double.isNaN(distance_m) || Double.isInfinite(distance_m)) { if (distance.isInvalid()) {
return context.getString(R.string.value_unknown); return context.getString(R.string.value_unknown);
} }
if (metricUnits) { if (metricUnits) {
if (distance_m > 500.0) { if (distance.greaterThan(Distance.of(500))) {
distance_m *= UnitConversions.M_TO_KM; return context.getString(R.string.value_float_kilometer, distance.toKM());
return context.getString(R.string.value_float_kilometer, distance_m);
} else { } else {
return context.getString(R.string.value_float_meter, distance_m); return context.getString(R.string.value_float_meter, distance.toM());
} }
} else { } else {
if (distance_m * UnitConversions.M_TO_MI > 0.5) { if (distance.greaterThan(Distance.ofMile(0.5))) {
distance_m *= UnitConversions.M_TO_MI; return context.getString(R.string.value_float_mile, distance.toMI());
return context.getString(R.string.value_float_mile, distance_m);
} else { } else {
distance_m *= UnitConversions.M_TO_FT; return context.getString(R.string.value_float_feet, distance.toFT());
return context.getString(R.string.value_float_feet, distance_m);
} }
} }
} }
public static String formatSpeed(Context context, double speed_mps, boolean metricUnits, boolean reportSpeed) { public static String formatSpeed(Context context, Speed speed, boolean metricUnits, boolean reportSpeed) {
if (Double.isNaN(speed_mps) || Double.isInfinite(speed_mps)) {
speed_mps = 0;
}
double speed = speed_mps * UnitConversions.M_TO_KM; //KM p sec
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
if (reportSpeed) { if (reportSpeed) {
if (metricUnits) { if (metricUnits) {
return context.getString(R.string.value_float_kilometer_hour, speed * UnitConversions.S_TO_HR); return context.getString(R.string.value_float_kilometer_hour, speed.toKMH());
} else { } else {
return context.getString(R.string.value_float_mile_hour, speed * UnitConversions.S_TO_HR); return context.getString(R.string.value_float_mile_hour, speed.toMPH());
} }
} else { } else {
int pace = speed == 0 ? 0 : (int) Math.round(1 / speed); //sec / [KM | MI] int pace = (int) speed.toPace(metricUnits).getSeconds();
int minutes = pace / 60; int minutes = pace / 60;
int seconds = pace % 60; int seconds = pace % 60;
if (metricUnits) { if (metricUnits) {
@@ -167,45 +157,40 @@ public class StringUtils {
* Get the formatted distance with unit. * Get the formatted distance with unit.
* *
* @param context the context * @param context the context
* @param distance_m the distance * @param distance the distance
* @param metricUnits true to use metric unit * @param metricUnits true to use metric unit
* @return the formatted distance (or null) and it's unit as {@link Pair} * @return the formatted distance (or null) and it's unit as {@link Pair}
*/ */
public static Pair<String, String> getDistanceParts(Context context, double distance_m, boolean metricUnits) { public static Pair<String, String> getDistanceParts(Context context, Distance distance, boolean metricUnits) {
if (Double.isNaN(distance_m) || Double.isInfinite(distance_m)) { if (distance.isInvalid()) {
return new Pair<>(null, context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet)); return new Pair<>(null, context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet));
} }
int unitId;
if (metricUnits) { if (metricUnits) {
if (distance_m > 500.0) { if (distance.greaterThan(Distance.of(500))) {
distance_m *= UnitConversions.M_TO_KM; return new Pair<>(formatDecimal(distance.toKM()), context.getString(R.string.unit_kilometer));
unitId = R.string.unit_kilometer;
} else { } else {
unitId = R.string.unit_meter; return new Pair<>(formatDecimal(distance.toM()), context.getString(R.string.unit_meter));
} }
} else { } else {
if (distance_m * UnitConversions.M_TO_MI > 0.5) { if (distance.greaterThan(Distance.of(0.5 * UnitConversions.M_TO_MI))) {
distance_m *= UnitConversions.M_TO_MI; return new Pair<>(formatDecimal(distance.toMI()), context.getString(R.string.unit_mile));
unitId = R.string.unit_mile;
} else { } else {
distance_m *= UnitConversions.M_TO_FT; return new Pair<>(formatDecimal(distance.toFT()), context.getString(R.string.unit_feet));
unitId = R.string.unit_feet;
} }
} }
return new Pair<>(formatDecimal(distance_m), context.getString(unitId));
} }
/** /**
* Gets the formatted speed with unit. * Gets the formatted speed with unit.
* *
* @param context the context * @param context the context
* @param speed_mps the speed * @param speed the speed
* @param metricUnits true to use metric unit * @param metricUnits true to use metric unit
* @param reportSpeed true to report speed; false for pace * @param reportSpeed true to report speed; false for pace
* @return the formatted speed (or null) and it's unit as {@link Pair} * @return the formatted speed (or null) and it's unit as {@link Pair}
*/ */
public static Pair<String, String> getSpeedParts(Context context, double speed_mps, boolean metricUnits, boolean reportSpeed) { public static Pair<String, String> getSpeedParts(Context context, Speed speed, boolean metricUnits, boolean reportSpeed) {
int unitId; int unitId;
if (metricUnits) { if (metricUnits) {
unitId = reportSpeed ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer; unitId = reportSpeed ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
@@ -214,21 +199,15 @@ public class StringUtils {
} }
String unitString = context.getString(unitId); String unitString = context.getString(unitId);
if (speed == null) {
if (Double.isNaN(speed_mps) || Double.isInfinite(speed_mps)) { speed = Speed.zero();
speed_mps = 0;
}
double speed = speed_mps * UnitConversions.M_TO_KM; //KM p sec
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
} }
if (reportSpeed) { if (reportSpeed) {
return new Pair<>(StringUtils.formatDecimal(speed * UnitConversions.S_TO_HR, 1), unitString); return new Pair<>(StringUtils.formatDecimal(speed.to(metricUnits), 1), unitString);
} }
int pace = speed == 0 ? 0 : (int) Math.round(1 / speed); //sec / [KM | MI] int pace = (int) speed.toPace(metricUnits).getSeconds();
int minutes = pace / 60; int minutes = pace / 60;
int seconds = pace % 60; int seconds = pace % 60;
@@ -49,7 +49,7 @@ public class UnitConversions {
// Distance // Distance
// multiplication factor to convert miles to feet // multiplication factor to convert miles to feet
public static final double MI_TO_FT = 5280.0; private static final double MI_TO_FT = 5280.0;
// multiplication factor to convert feet to miles // multiplication factor to convert feet to miles
public static final double FT_TO_MI = 1 / MI_TO_FT; public static final double FT_TO_MI = 1 / MI_TO_FT;
// multiplication factor to covert kilometers to meters // multiplication factor to covert kilometers to meters
@@ -33,7 +33,7 @@ public class IntervalStatistics {
TrackPoint trackPoint = trackPoints.get(i); TrackPoint trackPoint = trackPoints.get(i);
if (trackPoint.hasLocation() && prevTrackPoint.hasLocation()) { if (trackPoint.hasLocation() && prevTrackPoint.hasLocation()) {
interval.distance_m += prevTrackPoint.distanceToPrevious(trackPoint); interval.distance_m += prevTrackPoint.distanceToPrevious(trackPoint).toM();
interval.time = interval.time.plus(Duration.between(prevTrackPoint.getTime(), trackPoint.getTime())); interval.time = interval.time.plus(Duration.between(prevTrackPoint.getTime(), trackPoint.getTime()));
interval.gain_m += trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : 0; interval.gain_m += trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : 0;
interval.loss_m += trackPoint.hasAltitudeLoss() ? trackPoint.getAltitudeLoss() : 0; interval.loss_m += trackPoint.hasAltitudeLoss() ? trackPoint.getAltitudeLoss() : 0;