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:
Román Martínez
2021-02-20 18:32:47 +01:00
parent d90258294f
commit edf4ccc941
7 changed files with 490 additions and 25 deletions
@@ -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;
}
}
@@ -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() {