forked from upstream-mirrors/OpenTracks
It computes the average for heart rate, cadence, and power (duration-based average) and the maximum for heart rate and cadence.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TestSensorDataUtil {
|
||||
|
||||
private List<TrackPoint> trackPointList = new ArrayList<>();
|
||||
private List<SensorData> sensorDataList = new ArrayList<>();
|
||||
|
||||
public void add(Instant time, Float hr, Float cadence, Float power, TrackPoint.Type type) {
|
||||
sensorDataList.add(new TestSensorDataUtil.SensorData(time, hr, cadence, power, type));
|
||||
TrackPoint tp = new TrackPoint(type);
|
||||
int i = trackPointList.size() + 1;
|
||||
tp.setLatitude(TestDataUtil.INITIAL_LATITUDE + (double) i / 10000.0);
|
||||
tp.setLongitude(TestDataUtil.INITIAL_LONGITUDE - (double) i / 10000.0);
|
||||
tp.setHeartRate_bpm(hr);
|
||||
tp.setCyclingCadence_rpm(cadence);
|
||||
tp.setPower(power);
|
||||
tp.setAccuracy(1f);
|
||||
tp.setAltitude(1f);
|
||||
tp.setTime(time);
|
||||
tp.setSpeed(5f + (i / 10f));
|
||||
tp.setElevationGain(3f);
|
||||
tp.setElevationLoss(3f);
|
||||
trackPointList.add(tp);
|
||||
}
|
||||
|
||||
public List<TrackPoint> getTrackPointList() {
|
||||
return this.trackPointList;
|
||||
}
|
||||
|
||||
public SensorDataStats computeStats() {
|
||||
if (sensorDataList == null || sensorDataList.size() <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SensorDataStats stats = new SensorDataStats();
|
||||
long timeElapsed;
|
||||
long movingTime = 0;
|
||||
SensorData dataPrev = sensorDataList.get(0);
|
||||
stats.maxHr = dataPrev.hr;
|
||||
stats.maxCadence = dataPrev.cadence;
|
||||
SensorData dataCurrent;
|
||||
for (int i = 1; i < sensorDataList.size(); i++) {
|
||||
dataCurrent = sensorDataList.get(i);
|
||||
if (dataPrev.type != TrackPoint.Type.SEGMENT_START_MANUAL) {
|
||||
timeElapsed = dataCurrent.type != TrackPoint.Type.SEGMENT_START_MANUAL ? dataCurrent.time.getEpochSecond() - dataPrev.time.getEpochSecond() : 0;
|
||||
stats.avgHr += (dataPrev.hr * timeElapsed);
|
||||
stats.maxHr = dataPrev.hr > stats.maxHr ? dataPrev.hr : stats.maxHr;
|
||||
stats.avgCadence += (dataPrev.cadence * timeElapsed);
|
||||
stats.maxCadence = dataPrev.cadence > stats.maxCadence ? dataPrev.cadence : stats.maxCadence;
|
||||
stats.avgPower += (dataPrev.power * timeElapsed);
|
||||
|
||||
movingTime += timeElapsed;
|
||||
}
|
||||
dataPrev = dataCurrent;
|
||||
}
|
||||
|
||||
stats.avgHr /= movingTime;
|
||||
stats.avgCadence /= movingTime;
|
||||
stats.avgPower /= movingTime;
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
private static class SensorData {
|
||||
Instant time;
|
||||
float hr;
|
||||
float cadence;
|
||||
float power;
|
||||
TrackPoint.Type type;
|
||||
|
||||
public SensorData(Instant time, Float hr, Float cadence, Float power, TrackPoint.Type type) {
|
||||
this.time = time;
|
||||
this.hr = hr == null ? 0 : hr;
|
||||
this.cadence = cadence == null ? 0 : cadence;
|
||||
this.power = power == null ? 0 : power;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SensorDataStats {
|
||||
public float avgHr = 0f;
|
||||
public float maxHr = 0f;
|
||||
public float avgCadence = 0f;
|
||||
public float maxCadence = 0f;
|
||||
public float avgPower = 0f;
|
||||
}
|
||||
}
|
||||
+337
@@ -35,18 +35,22 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Marker;
|
||||
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.TestSensorDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
import de.dennisguse.opentracks.util.UUIDUtils;
|
||||
@@ -927,4 +931,337 @@ public class CustomContentProviderUtilsTest {
|
||||
assertEquals("12", ContentProviderUtils.formatIdListForUri(new Track.Id(12)));
|
||||
assertEquals("42,43,44", ContentProviderUtils.formatIdListForUri(new Track.Id(42), new Track.Id(43), new Track.Id(44)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_noSensorData() {
|
||||
// given
|
||||
List<TrackPoint> trackPointList = new ArrayList<>();
|
||||
TrackPoint trackPoint = TestDataUtil.createTrackPoint(1);
|
||||
trackPoint.setType(TrackPoint.Type.TRACKPOINT);
|
||||
trackPoint.setPower(null);
|
||||
trackPoint.setCyclingCadence_rpm(null);
|
||||
trackPoint.setHeartRate_bpm(null);
|
||||
trackPointList.add(trackPoint);
|
||||
Track.Id trackId = new Track.Id(System.currentTimeMillis());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, trackPointList);
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
|
||||
// then
|
||||
assertFalse(sensorStatistics.hasHeartRate());
|
||||
assertFalse(sensorStatistics.hasCadence());
|
||||
assertFalse(sensorStatistics.hasPower());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_needAtLeastTwoTrackPointsFalse() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 140 90 300 0
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, 140f, 90f, 300f, TrackPoint.Type.TRACKPOINT);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
|
||||
// then
|
||||
assertFalse(sensorStatistics.hasHeartRate());
|
||||
assertFalse(sensorStatistics.hasCadence());
|
||||
assertFalse(sensorStatistics.hasPower());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_needAtLeastTwoTrackPointsTrue() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 140 90 300 -1
|
||||
* 1 140 90 300 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, 140f, 90f, 300f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), 140f, 90f, 300f, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertTrue(sensorStatistics.hasHeartRate());
|
||||
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
|
||||
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
|
||||
assertTrue(sensorStatistics.hasCadence());
|
||||
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
|
||||
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
|
||||
assertTrue(sensorStatistics.hasPower());
|
||||
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_onlyHr() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 140 NULL NULL -1
|
||||
* 1 140 NULL NULL 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, 140f, null, null, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), 140f, null, null, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertTrue(sensorStatistics.hasHeartRate());
|
||||
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
|
||||
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
|
||||
assertFalse(sensorStatistics.hasCadence());
|
||||
assertFalse(sensorStatistics.hasPower());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_onlyCadence() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 NULL 90 NULL -1
|
||||
* 1 NULL 90 NULL 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, null, 90f, null, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), null, 90f, null, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertFalse(sensorStatistics.hasHeartRate());
|
||||
assertTrue(sensorStatistics.hasCadence());
|
||||
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
|
||||
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
|
||||
assertFalse(sensorStatistics.hasPower());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_onlyPower() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 NULL NULL 300 -1
|
||||
* 1 NULL NULL 300 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, null, null, 300f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(1, ChronoUnit.SECONDS), null, null, 300f, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertFalse(sensorStatistics.hasHeartRate());
|
||||
assertFalse(sensorStatistics.hasCadence());
|
||||
assertTrue(sensorStatistics.hasPower());
|
||||
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 140 75 250 -1
|
||||
* 2 148 80 300 0
|
||||
* 1 150 82 325 0
|
||||
* 7 160 90 275 0
|
||||
* 4 155 85 280 0
|
||||
* 1 155 84 295 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(10, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(14, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(15, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
|
||||
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
|
||||
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
|
||||
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
|
||||
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_withManualResume() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 140 75 250 -1
|
||||
* 2 148 80 300 0
|
||||
* 1 150 82 325 0
|
||||
* 3 174 88 400 0
|
||||
* 20 127 54 175 -2
|
||||
* 3 160 90 275 0
|
||||
* 7 155 85 280 0
|
||||
* 3 150 90 267 0
|
||||
* 3 170 90 240 0
|
||||
* 2 155 84 295 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(6, ChronoUnit.SECONDS), 174f, 88f, 400f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(26, ChronoUnit.SECONDS), 127f, 54f, 175f, TrackPoint.Type.SEGMENT_START_MANUAL);
|
||||
sensorDataUtil.add(start.plus(29, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(36, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(39, ChronoUnit.SECONDS), 150f, 90f, 267f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(42, ChronoUnit.SECONDS), 170f, 90f, 240f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(44, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
|
||||
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
|
||||
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
|
||||
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
|
||||
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_withStartAutomatic() {
|
||||
// given
|
||||
/*
|
||||
* time elapsed hr cadence power track type
|
||||
* 0 140 75 250 -1
|
||||
* 2 148 80 300 0
|
||||
* 1 150 82 325 0
|
||||
* 3 174 88 400 0
|
||||
* 20 127 54 175 -1
|
||||
* 3 160 90 275 0
|
||||
* 7 155 85 280 0
|
||||
* 3 150 90 267 0
|
||||
* 3 170 90 240 0
|
||||
* 2 155 84 295 1
|
||||
*/
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
sensorDataUtil.add(start, 140f, 75f, 250f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(2, ChronoUnit.SECONDS), 148f, 80f, 300f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(3, ChronoUnit.SECONDS), 150f, 82f, 325f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(6, ChronoUnit.SECONDS), 174f, 88f, 400f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(26, ChronoUnit.SECONDS), 127f, 54f, 175f, TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
sensorDataUtil.add(start.plus(29, ChronoUnit.SECONDS), 160f, 90f, 275f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(36, ChronoUnit.SECONDS), 155f, 85f, 280f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(39, ChronoUnit.SECONDS), 150f, 90f, 267f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(42, ChronoUnit.SECONDS), 170f, 90f, 240f, TrackPoint.Type.TRACKPOINT);
|
||||
sensorDataUtil.add(start.plus(44, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0f);
|
||||
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0f);
|
||||
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0f);
|
||||
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0f);
|
||||
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0f);
|
||||
}
|
||||
|
||||
private void testGetSensorStats_randomData(int totalPoints, boolean withStartSegments) {
|
||||
// given
|
||||
Instant start = Instant.now();
|
||||
TestSensorDataUtil sensorDataUtil = new TestSensorDataUtil();
|
||||
Random random = new Random();
|
||||
for (int i = 0; i < totalPoints; i++) {
|
||||
int randomNum = withStartSegments ? random.nextInt(50) - 2 : 0;
|
||||
TrackPoint.Type type = randomNum >= 0 ? TrackPoint.Type.TRACKPOINT : TrackPoint.Type.getById(randomNum);
|
||||
float randomHr = random.nextFloat() * (200f - 90f) + 90f;
|
||||
float randomCadence = random.nextFloat() * (110f - 40f) + 40f;
|
||||
float randomPower = random.nextFloat() * (500f - 100f) + 100f;
|
||||
sensorDataUtil.add(start.plus(i, ChronoUnit.SECONDS), randomHr, randomCadence, randomPower, type);
|
||||
}
|
||||
sensorDataUtil.add(start.plus(totalPoints, ChronoUnit.SECONDS), 155f, 84f, 295f, TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
|
||||
Track.Id trackId = new Track.Id(start.toEpochMilli());
|
||||
Track track = TestDataUtil.createTrack(trackId);
|
||||
TestDataUtil.insertTrackWithLocations(contentProviderUtils, track, sensorDataUtil.getTrackPointList());
|
||||
|
||||
// when
|
||||
SensorStatistics sensorStatistics = contentProviderUtils.getSensorStats(trackId);
|
||||
TestSensorDataUtil.SensorDataStats stats = sensorDataUtil.computeStats();
|
||||
|
||||
// then
|
||||
assertEquals(sensorStatistics.getAvgHeartRate(), stats.avgHr, 0.01f);
|
||||
assertEquals(sensorStatistics.getMaxHeartRate(), stats.maxHr, 0.01f);
|
||||
assertEquals(sensorStatistics.getAvgCadence(), stats.avgCadence, 0.01f);
|
||||
assertEquals(sensorStatistics.getMaxCadence(), stats.maxCadence, 0.01f);
|
||||
assertEquals(sensorStatistics.getAvgPower(), stats.avgPower, 0.01f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_veryLongActivity12h() {
|
||||
testGetSensorStats_randomData(43200, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSensorStats_withSeveralRandomStartSegments() {
|
||||
testGetSensorStats_randomData(5000, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,13 @@ public interface TrackPointsColumns extends BaseColumns {
|
||||
String ELEVATION_GAIN = "elevation_gain";
|
||||
String ELEVATION_LOSS = "elevation_loss";
|
||||
|
||||
// Alias for sensor statistics
|
||||
String ALIAS_AVG_HR = "avg_hr";
|
||||
String ALIAS_MAX_HR = "max_hr";
|
||||
String ALIAS_AVG_CADENCE = "avg_cadence";
|
||||
String ALIAS_MAX_CADENCE = "max_cadence";
|
||||
String ALIAS_AVG_POWER = "avg_power";
|
||||
|
||||
String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
+ TRACKID + " INTEGER NOT NULL, "
|
||||
|
||||
@@ -30,6 +30,7 @@ public interface TracksColumns extends BaseColumns {
|
||||
|
||||
String TABLE_NAME = "tracks";
|
||||
Uri CONTENT_URI = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME);
|
||||
Uri CONTENT_URI_SENSOR_STATS = Uri.parse(ContentProviderUtils.CONTENT_BASE_URI + "/" + TABLE_NAME + "/sensorstats");
|
||||
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.track";
|
||||
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.track";
|
||||
String DEFAULT_SORT_ORDER = _ID;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
@@ -837,31 +838,19 @@ public class ContentProviderUtils {
|
||||
return TextUtils.split(url.getLastPathSegment(), ID_SEPARATOR);
|
||||
}
|
||||
|
||||
public SensorStatistics getSensorStats(Track.Id trackId) {
|
||||
if (trackId == null) {
|
||||
return null;
|
||||
}
|
||||
String[] projection = {
|
||||
"MAX(" + TrackPointsColumns.SENSOR_HEARTRATE + ") max_hr",
|
||||
"AVG(" + TrackPointsColumns.SENSOR_HEARTRATE + ") avg_hr",
|
||||
"MAX(" + TrackPointsColumns.SENSOR_CADENCE + ") max_cadence",
|
||||
"AVG(" + TrackPointsColumns.SENSOR_CADENCE + ") avg_cadence",
|
||||
"AVG(" + TrackPointsColumns.SENSOR_POWER + ") avg_power"
|
||||
};
|
||||
String selection = TrackPointsColumns.TRACKID + "=?";
|
||||
String[] selectionArgs = new String[]{Long.toString(trackId.getId())};
|
||||
public SensorStatistics getSensorStats(@NonNull Track.Id trackId) {
|
||||
SensorStatistics sensorStatistics = null;
|
||||
try (Cursor cursor = getTrackPointCursor(projection, selection, selectionArgs, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
sensorStatistics = new SensorStatistics(
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("max_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("max_hr")) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("avg_hr")) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("max_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("max_cadence")) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("avg_cadence")) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_power")) ? cursor.getFloat(cursor.getColumnIndexOrThrow("avg_power")) : null
|
||||
);
|
||||
}
|
||||
Cursor cursor = contentResolver.query(ContentUris.withAppendedId(TracksColumns.CONTENT_URI_SENSOR_STATS, trackId.getId()), null, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
sensorStatistics = new SensorStatistics(
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("max_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_HR)) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_hr")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_HR)) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("max_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_MAX_CADENCE)) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_cadence")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_CADENCE)) : null,
|
||||
!cursor.isNull(cursor.getColumnIndexOrThrow("avg_power")) ? cursor.getFloat(cursor.getColumnIndexOrThrow(TrackPointsColumns.ALIAS_AVG_POWER)) : null
|
||||
);
|
||||
}
|
||||
|
||||
return sensorStatistics;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.MarkerColumns;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.content.data.TracksColumns;
|
||||
|
||||
@@ -56,6 +57,38 @@ public class CustomContentProvider extends ContentProvider {
|
||||
|
||||
private SQLiteDatabase db;
|
||||
|
||||
/**
|
||||
* The string representing the query that compute sensor stats from trackpoints table.
|
||||
* It computes the average for heart rate, cadence, and power (duration-based average) and the maximum for heart rate and cadence.
|
||||
* Finally, it ignores manual pause (SEGMENT_START_MANUAL).
|
||||
*/
|
||||
private final String SENSOR_STATS_QUERY =
|
||||
"WITH time_select as " +
|
||||
"(SELECT t1." + TrackPointsColumns.TIME + " * (t1." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")) time_value " +
|
||||
"FROM " + TrackPointsColumns.TABLE_NAME + " t1 " +
|
||||
"WHERE t1." + TrackPointsColumns._ID + " > t." + TrackPointsColumns._ID + " AND t1." + TrackPointsColumns.TRACKID + " = ? ORDER BY _id LIMIT 1) " +
|
||||
|
||||
"SELECT " +
|
||||
"SUM(t." + TrackPointsColumns.SENSOR_HEARTRATE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
|
||||
"/ " +
|
||||
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_HR + ", " +
|
||||
|
||||
"MAX(t." + TrackPointsColumns.SENSOR_HEARTRATE + ") " + TrackPointsColumns.ALIAS_MAX_HR + ", " +
|
||||
|
||||
"SUM(t." + TrackPointsColumns.SENSOR_CADENCE + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
|
||||
"/ " +
|
||||
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_CADENCE + ", " +
|
||||
|
||||
"MAX(t." + TrackPointsColumns.SENSOR_CADENCE + ") " + TrackPointsColumns.ALIAS_MAX_CADENCE + ", " +
|
||||
|
||||
"SUM(t." + TrackPointsColumns.SENSOR_POWER + " * (COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ")) " +
|
||||
"/ " +
|
||||
"SUM(COALESCE(MAX(t." + TrackPointsColumns.TIME + ", (SELECT time_value FROM time_select)), t." + TrackPointsColumns.TIME + ") - t." + TrackPointsColumns.TIME + ") " + TrackPointsColumns.ALIAS_AVG_POWER + " " +
|
||||
|
||||
"FROM " + TrackPointsColumns.TABLE_NAME + " t " +
|
||||
"WHERE t." + TrackPointsColumns.TRACKID + " = ? " +
|
||||
"AND t." + TrackPointsColumns.TYPE + " NOT IN (" + TrackPoint.Type.SEGMENT_START_MANUAL.type_db + ")";
|
||||
|
||||
public CustomContentProvider() {
|
||||
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_ID.getPath(), UrlType.TRACKPOINTS.ordinal());
|
||||
@@ -63,6 +96,7 @@ public class CustomContentProvider extends ContentProvider {
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TrackPointsColumns.CONTENT_URI_BY_TRACKID.getPath() + "/*", UrlType.TRACKPOINTS_BY_TRACKID.ordinal());
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath(), UrlType.TRACKS.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI_SENSOR_STATS.getPath() + "/#", UrlType.TRACKS_SENSOR_STATS.ordinal());
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.CONTENT_URI.getPath() + "/*", UrlType.TRACKS_BY_ID.ordinal());
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, MarkerColumns.CONTENT_URI.getPath(), UrlType.MARKERS.ordinal());
|
||||
@@ -223,6 +257,10 @@ public class CustomContentProvider extends ContentProvider {
|
||||
queryBuilder.setTables(TracksColumns.TABLE_NAME);
|
||||
queryBuilder.appendWhere(TracksColumns._ID + " IN (" + TextUtils.join(SQL_LIST_DELIMITER, ContentProviderUtils.parseTrackIdsFromUri(url)) + ")");
|
||||
break;
|
||||
case TRACKS_SENSOR_STATS:
|
||||
long trackId = ContentUris.parseId(url);
|
||||
Cursor cursor = db.rawQuery(SENSOR_STATS_QUERY, new String[]{String.valueOf(trackId), String.valueOf(trackId)});
|
||||
return cursor;
|
||||
case MARKERS:
|
||||
queryBuilder.setTables(MarkerColumns.TABLE_NAME);
|
||||
sortOrder = sort != null ? sort : MarkerColumns.DEFAULT_SORT_ORDER;
|
||||
@@ -363,6 +401,7 @@ public class CustomContentProvider extends ContentProvider {
|
||||
TRACKPOINTS_BY_TRACKID,
|
||||
TRACKS,
|
||||
TRACKS_BY_ID,
|
||||
TRACKS_SENSOR_STATS,
|
||||
MARKERS,
|
||||
MARKERS_BY_ID,
|
||||
MARKERS_BY_TRACKID
|
||||
|
||||
@@ -16,7 +16,7 @@ public class SensorStatistics {
|
||||
}
|
||||
|
||||
public boolean hasHeartRate() {
|
||||
return maxHr != null;
|
||||
return avgHr != null && maxHr != null;
|
||||
}
|
||||
|
||||
public float getMaxHeartRate() {
|
||||
@@ -28,7 +28,7 @@ public class SensorStatistics {
|
||||
}
|
||||
|
||||
public boolean hasCadence() {
|
||||
return maxCadence != null;
|
||||
return avgCadence != null && maxCadence != null;
|
||||
}
|
||||
|
||||
public float getMaxCadence() {
|
||||
|
||||
Reference in New Issue
Block a user