Use distance from cycling speed sensors.

Fixes #505.
This commit is contained in:
Dennis Guse
2021-02-03 19:50:23 +01:00
parent 15fbd90ca4
commit a5158a55d9
28 changed files with 502 additions and 202 deletions
+28 -4
View File
@@ -1,8 +1,32 @@
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://opentracksapp.com/xmlschemas/v1">
<xs:element name="trackuuid"
type="xs:string" />
<xsd:element name="trackuuid"
type="xsd:string">
<xsd:annotation>
<xsd:documentation xml:lang="en">
A unique track identifier stored as UUID.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xs:schema>
<xsd:element name="loss"
type="xsd:float">
<xsd:annotation>
<xsd:documentation xml:lang="en">
The lost elevation in meters from the previous to the current TrackPoint.
Only used in GPX.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="gain"
type="xsd:string">
<xsd:annotation>
<xsd:documentation xml:lang="en">
The gained elevation in meters from the previous to the current TrackPoint.
Only used in GPX.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:schema>
@@ -96,34 +96,36 @@ public class SensorDataCyclingTest {
@Test
public void compute_speed() {
// given
SensorDataCycling.Speed previous = new SensorDataCycling.Speed("sensorAddress", "sensorName", 1, 6184);
SensorDataCycling.Speed current = new SensorDataCycling.Speed("sensorAddress", "sensorName", 2, 8016);
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 1, 6184);
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 2, 8016);
// when
current.compute(previous, 2150);
// then
assertEquals(1.20, current.getValue(), 0.01);
assertEquals(1.20, current.getValue().distance_m, 2150);
assertEquals(1.20, current.getValue().speed_mps, 0.01);
}
@Test
public void compute_speed_rollOverCount() {
// given
SensorDataCycling.Speed previous = new SensorDataCycling.Speed("sensorAddress", "sensorName", UintUtils.UINT16_MAX - 1, 1024);
SensorDataCycling.Speed current = new SensorDataCycling.Speed("sensorAddress", "sensorName", 0, 2048);
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", UintUtils.UINT16_MAX - 1, 1024);
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
// when
current.compute(previous, 2000);
// then
assertEquals(2, current.getValue(), 0.01);
assertEquals(1.20, current.getValue().distance_m, 2000);
assertEquals(2, current.getValue().speed_mps, 0.01);
}
@Test
public void equals_speed_with_no_data() {
// given
SensorDataCycling.Speed previous = new SensorDataCycling.Speed("sensorAddress");
SensorDataCycling.Speed current = new SensorDataCycling.Speed("sensorAddress", "sensorName", 0, 2048);
SensorDataCycling.DistanceSpeed previous = new SensorDataCycling.DistanceSpeed("sensorAddress");
SensorDataCycling.DistanceSpeed current = new SensorDataCycling.DistanceSpeed("sensorAddress", "sensorName", 0, 2048);
// when
previous.toString();
@@ -3,7 +3,6 @@ package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Looper;
import android.util.Log;
@@ -24,6 +23,7 @@ import org.junit.runners.JUnit4;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -91,17 +91,17 @@ public class ExportImportTest {
trackId = service.startNewTrack();
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.001, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.002, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
service.pauseCurrentTrack();
service.resumeCurrentTrack();
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.003, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16.001, 10, 15, 10, 0, 66, 3, 50), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 14.003, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.newTrackPoint(createTrackPoint(System.currentTimeMillis(), 3, 16.001, 10, 15, 10, 0, 66, 3, 50, 5), 0);
service.endCurrentTrack();
track = contentProviderUtils.getTrack(trackId);
@@ -163,10 +163,10 @@ public class ExportImportTest {
assertEquals(track.getUuid(), importedTrack.getUuid());
// 2. trackpoints
assertTrackpoints(trackPoints, false, false, false, false, false);
assertTrackpoints(trackPoints, false, false, false, false, false, false);
// 3. trackstatistics
assertTrackStatistics(false, false);
assertTrackStatistics(false, false, false);
// 4. markers
assertMarkers();
@@ -201,10 +201,10 @@ public class ExportImportTest {
assertEquals(track.getIcon(), importedTrack.getIcon());
// 2. trackpoints
assertTrackpoints(trackPoints, true, true, true, true, true);
assertTrackpoints(trackPoints, true, true, true, true, true, true);
// 2. trackstatistics
assertTrackStatistics(false, true);
assertTrackStatistics(false, true, true);
// 4. markers
assertMarkers();
@@ -234,38 +234,6 @@ public class ExportImportTest {
assertNull(importedTrack);
}
@Ignore
@LargeTest
@Test
public void kmz_only_track() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@Ignore
@LargeTest
@Test
public void kmz_with_trackdetail() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@Ignore
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@Ignore
@LargeTest
@Test
public void kmz_with_trackdetail_and_sensordata_and_pictures() {
// TODO
Log.e(TAG, "Test not implemented.");
}
@LargeTest
@Test
public void gpx() {
@@ -303,10 +271,10 @@ public class ExportImportTest {
trackPointsWithCoordinates.get(0).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
trackPointsWithCoordinates.get(3).setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
assertTrackpoints(trackPointsWithCoordinates, true, true, true, true, true);
assertTrackpoints(trackPointsWithCoordinates, true, true, true, true, true, false);
// 3. trackstatistics
assertTrackStatistics(true, true);
assertTrackStatistics(true, true, false);
// 4. markers
assertMarkers();
@@ -356,7 +324,7 @@ public class ExportImportTest {
}
}
private void assertTrackpoints(List<TrackPoint> trackPoints, boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain, boolean verifyElevationLoss) {
private void assertTrackpoints(List<TrackPoint> trackPoints, boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain, boolean verifyElevationLoss, boolean verifyDistance) {
List<TrackPoint> importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId);
assertEquals(trackPoints.size(), importedTrackPoints.size());
@@ -407,10 +375,13 @@ public class ExportImportTest {
if (verifyElevationLoss) {
assertEquals(trackPoint.getElevationLoss(), importedTrackPoint.getElevationLoss(), 0.01);
}
if (verifyDistance) {
assertEquals(trackPoint.getSensorDistance(), importedTrackPoint.getSensorDistance(), 0.01);
}
}
}
private void assertTrackStatistics(boolean isGpx, boolean verifyElevationGainAndLoss) {
private void assertTrackStatistics(boolean isGpx, boolean verifyElevationGainAndLoss, boolean verifyDistance) {
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack.getTrackStatistics());
@@ -428,7 +399,9 @@ public class ExportImportTest {
assertEquals(trackStatistics.getMovingTime(), importedTrackStatistics.getMovingTime());
// Distance
assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance(), 0.01);
if (verifyDistance) {
assertEquals(trackStatistics.getTotalDistance(), importedTrackStatistics.getTotalDistance(), 0.01);
}
// Speed
assertEquals(trackStatistics.getMaxSpeed(), importedTrackStatistics.getMaxSpeed(), 0.01);
@@ -448,20 +421,15 @@ public class ExportImportTest {
}
}
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, long speed, long altitude, float elevationGain, float heartRate, float cyclingCadence, float power) {
Location location = new Location("");
location.setTime(time);
location.setLongitude(longitude);
location.setLatitude(latitude);
location.setAccuracy(accuracy);
location.setAltitude(altitude);
location.setSpeed(speed);
TrackPoint tp = new TrackPoint(location);
private static TrackPoint createTrackPoint(long time, double latitude, double longitude, float accuracy, float speed, float altitude, float elevationGain, float heartRate, float cyclingCadence, float power, float distance) {
TrackPoint tp = new TrackPoint(latitude, longitude, (double) altitude, Instant.ofEpochMilli(time));
tp.setAccuracy(accuracy);
tp.setSpeed(speed);
tp.setHeartRate_bpm(heartRate);
tp.setCyclingCadence_rpm(cyclingCadence);
tp.setPower(power);
tp.setElevationGain(elevationGain);
tp.setSensorDistance(distance);
return tp;
}
}
@@ -1,27 +1,36 @@
package de.dennisguse.opentracks.stats;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.TestDataUtil;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import static org.junit.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class TrackStatisticsUpdaterTest {
private static final int GPS_DISTANCE = 50;
@Test
public void addTrackPoint() {
public void addTrackPoint_TestingTrack() {
// given
TestDataUtil.TrackData data = TestDataUtil.createTestingTrack(new Track.Id(1));
// when
TrackStatisticsUpdater updater = new TrackStatisticsUpdater();
data.trackPoints.forEach(it -> updater.addTrackPoint(it, 50));
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
data.trackPoints.forEach(it -> subject.addTrackPoint(it, GPS_DISTANCE));
// then
TrackStatistics statistics = updater.getTrackStatistics();
TrackStatistics statistics = subject.getTrackStatistics();
assertEquals(85.35, statistics.getTotalDistance(), 0.01);
assertEquals(Duration.ofMillis(13999), statistics.getTotalTime());
assertEquals(Duration.ofSeconds(6), statistics.getMovingTime());
@@ -35,4 +44,122 @@ public class TrackStatisticsUpdaterTest {
assertEquals(14.226, statistics.getAverageMovingSpeed(), 0.01);
assertEquals(6.566, statistics.getAverageSpeed(), 0.01);
}
@Test
public void addTrackPoint_distance_from_GPS_not_moving() {
// given
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
subject.addTrackPoint(tp2, GPS_DISTANCE);
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(0, subject.getTrackStatistics().getTotalDistance(), 0.01);
}
@Test
public void addTrackPoint_distance_from_GPS_moving() {
// given
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
tp3.setSpeed(5f);
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
subject.addTrackPoint(tp2, GPS_DISTANCE);
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(1.10, subject.getTrackStatistics().getTotalDistance(), 0.01);
}
@Test
public void addTrackPoint_distance_from_GPS_moving_and_sensor_moving() {
// given
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
tp2.setSpeed(5f);
TrackPoint tp3 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(3000));
tp2.setSpeed(5f);
TrackPoint tp4 = new TrackPoint(0.001, 0, 5.0, Instant.ofEpochMilli(4000));
tp2.setSpeed(5f);
tp4.setSensorDistance(5f);
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(10f);
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
subject.addTrackPoint(tp2, GPS_DISTANCE);
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(110.57, subject.getTrackStatistics().getTotalDistance(), 0.01);
// when
subject.addTrackPoint(tp4, GPS_DISTANCE);
subject.addTrackPoint(tp5, GPS_DISTANCE);
// then
assertEquals(125.57, subject.getTrackStatistics().getTotalDistance(), 0.01);
}
@Test
public void addTrackPoint_distance_from_GPS_not_moving_and_sensor_moving() {
// given
TrackStatisticsUpdater subject = new TrackStatisticsUpdater();
TrackPoint tp1 = new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.ofEpochMilli(1000));
TrackPoint tp2 = new TrackPoint(0, 0, 5.0, Instant.ofEpochMilli(2000));
TrackPoint tp3 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(3000));
TrackPoint tp4 = new TrackPoint(0.00001, 0, 5.0, Instant.ofEpochMilli(4000));
tp4.setSensorDistance(5f);
TrackPoint tp5 = new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.ofEpochMilli(5000));
tp5.setSensorDistance(10f);
// when
subject.addTrackPoint(tp1, GPS_DISTANCE);
subject.addTrackPoint(tp2, GPS_DISTANCE);
subject.addTrackPoint(tp3, GPS_DISTANCE);
// then
assertEquals(0, subject.getTrackStatistics().getTotalDistance(), 0.01);
// when
subject.addTrackPoint(tp4, GPS_DISTANCE);
subject.addTrackPoint(tp5, GPS_DISTANCE);
// then
assertEquals(15, subject.getTrackStatistics().getTotalDistance(), 0.01);
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_not_moving() {
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_moving() {
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_not_moving_and_sensor_speed() {
}
@Ignore("TODO: create a concept ont to compute speed from GPS and sensor")
@Test
public void addTrackPoint_speed_from_GPS_moving_and_sensor_speed() {
}
}
@@ -47,7 +47,7 @@ public class BluetoothUtilsTest {
// then
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
assertNull(sensor.getSpeed());
assertNull(sensor.getDistanceSpeed());
}
@Test
@@ -60,7 +60,7 @@ public class BluetoothUtilsTest {
// then
assertNull(sensor.getCadence());
assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
assertEquals(225, sensor.getDistanceSpeed().getWheelRevolutionsCount());
}
@Test
@@ -73,7 +73,7 @@ public class BluetoothUtilsTest {
// then
assertEquals(200, sensor.getCadence().getCrankRevolutionsCount());
assertEquals(225, sensor.getSpeed().getWheelRevolutionsCount());
assertEquals(225, sensor.getDistanceSpeed().getWheelRevolutionsCount());
}
@Test
@@ -57,6 +57,7 @@ public class TrackPoint {
private Double altitude_m;
private Float speed_mps;
private Float bearing;
private Float sensorDistance_m;
public enum Type {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
@@ -224,7 +225,7 @@ public class TrackPoint {
return elevationGain != null;
}
public Float getElevationGain() {
public float getElevationGain() {
return elevationGain;
}
@@ -310,8 +311,12 @@ public class TrackPoint {
this.accuracy = horizontalAccuracy;
}
public float distanceTo(@NonNull TrackPoint dest) {
return getLocation().distanceTo(dest.getLocation());
public float distanceToPrevious(@NonNull TrackPoint previous) {
if (hasSensorDistance()) {
return getSensorDistance();
}
return getLocation().distanceTo(previous.getLocation());
}
public boolean fulfillsAccuracy(int poorAccuracy) {
@@ -327,6 +332,18 @@ public class TrackPoint {
}
// Sensor data
public boolean hasSensorDistance() {
return sensorDistance_m != null;
}
public Float getSensorDistance() {
return sensorDistance_m;
}
public void setSensorDistance(Float distance_m) {
this.sensorDistance_m = distance_m;
}
public boolean hasSensorData() {
return hasHeartRate() || hasCyclingCadence() || hasPower();
}
@@ -347,7 +364,7 @@ public class TrackPoint {
return cyclingCadence_rpm != null;
}
public Float getCyclingCadence_rpm() {
public float getCyclingCadence_rpm() {
return cyclingCadence_rpm;
}
@@ -359,7 +376,7 @@ public class TrackPoint {
return power != null;
}
public Float getPower() {
public float getPower() {
return power;
}
@@ -50,6 +50,7 @@ public interface TrackPointsColumns extends BaseColumns {
String BEARING = "bearing";
String SENSOR_HEARTRATE = "sensor_heartrate";
String SENSOR_CADENCE = "sensor_cadence";
String SENSOR_DISTANCE = "sensor_distance"; //DISTANCE from previous TrackPoint
String SENSOR_POWER = "sensor_power";
String ELEVATION_GAIN = "elevation_gain";
String ELEVATION_LOSS = "elevation_loss";
@@ -77,6 +78,7 @@ public interface TrackPointsColumns extends BaseColumns {
+ ELEVATION_GAIN + " FLOAT, "
+ ELEVATION_LOSS + " FLOAT, "
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1)), "
+ SENSOR_DISTANCE + " FLOAT, "
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
+ ")";
@@ -19,6 +19,7 @@ class CachedTrackPointsIndexes {
final int bearingIndex;
final int sensorHeartRateIndex;
final int sensorCadenceIndex;
final int sensorDistanceIndex;
final int sensorPowerIndex;
final int elevationGainIndex;
final int elevationLossIndex;
@@ -35,6 +36,7 @@ class CachedTrackPointsIndexes {
bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE);
sensorDistanceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_DISTANCE);
sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER);
elevationGainIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ELEVATION_GAIN);
elevationLossIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ELEVATION_LOSS);
@@ -556,6 +556,9 @@ public class ContentProviderUtils {
if (!cursor.isNull(indexes.sensorCadenceIndex)) {
trackPoint.setCyclingCadence_rpm(cursor.getFloat(indexes.sensorCadenceIndex));
}
if (!cursor.isNull(indexes.sensorDistanceIndex)) {
trackPoint.setSensorDistance(cursor.getFloat(indexes.sensorDistanceIndex));
}
if (!cursor.isNull(indexes.sensorPowerIndex)) {
trackPoint.setPower(cursor.getFloat(indexes.sensorPowerIndex));
}
@@ -708,6 +711,9 @@ public class ContentProviderUtils {
if (trackPoint.hasCyclingCadence()) {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCyclingCadence_rpm());
}
if (trackPoint.hasSensorDistance()) {
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance());
}
if (trackPoint.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower());
}
@@ -25,7 +25,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
private static final int DATABASE_VERSION = 30;
private static final int DATABASE_VERSION = 31;
@VisibleForTesting
public static final String DATABASE_NAME = "database.db";
@@ -82,6 +82,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 30:
upgradeFrom29to30(db);
break;
case 31:
upgradeFrom30to31(db);
break;
default:
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
@@ -115,7 +118,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
case 29:
downgradeFrom30to29(db);
break;
case 30:
downgradeFrom31to30(db);
break;
default:
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
}
@@ -394,4 +399,32 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
db.setTransactionSuccessful();
db.endTransaction();
}
/**
* Add distance column to TrackPoint.
*/
private void upgradeFrom30to31(SQLiteDatabase db) {
db.beginTransaction();
// TrackPoints
db.execSQL("ALTER TABLE trackpoints ADD COLUMN sensor_distance FLOAT");
db.setTransactionSuccessful();
db.endTransaction();
}
private void downgradeFrom31to30(SQLiteDatabase db) {
db.beginTransaction();
// TrackPoints; identical to upgradeFrom27to28()
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, type TEXT CHECK(type IN (-2, -1, 0, 1)), FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain, elevation_gain, type FROM trackpoints_old");
db.execSQL("DROP TABLE trackpoints_old");
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
db.setTransactionSuccessful();
db.endTransaction();
}
}
@@ -54,6 +54,12 @@ public abstract class SensorData<T> {
return value;
}
/**
* Reset long term aggregated values (more than derived from previous SensorData). e.g. overall distance.
*/
public void reset() {
}
/**
* Is the data recent considering the current time.
*/
@@ -61,4 +67,10 @@ public abstract class SensorData<T> {
return Instant.now()
.isBefore(time.plus(BluetoothRemoteSensorManager.MAX_SENSOR_DATE_SET_AGE_MS));
}
@NonNull
@Override
public String toString() {
return "sensorAddress='" + sensorAddress;
}
}
@@ -41,7 +41,7 @@ public final class SensorDataCycling {
/**
* Workaround for Wahoo CADENCE: provides speed instead of cadence
*/
public Cadence(@NonNull SensorDataCycling.Speed speed) {
public Cadence(@NonNull DistanceSpeed speed) {
this(speed.getSensorAddress(), speed.getSensorName(), speed.wheelRevolutionsCount, speed.wheelRevolutionsTime);
}
@@ -74,7 +74,7 @@ public final class SensorDataCycling {
@NonNull
@Override
public String toString() {
return "cadence=" + value + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
return super.toString() + " cadence=" + value + " time=" + crankRevolutionsTime + " count=" + crankRevolutionsCount;
}
@Override
@@ -90,18 +90,18 @@ public final class SensorDataCycling {
}
}
public static class Speed extends SensorData<Float> {
public static class DistanceSpeed extends SensorData<DistanceSpeed.Data> {
private final Integer wheelRevolutionsCount; // UINT16
private final Integer wheelRevolutionsTime; // UINT16; 1/1024s
public Speed(String sensorAddress) {
public DistanceSpeed(String sensorAddress) {
super(sensorAddress);
this.wheelRevolutionsCount = null;
this.wheelRevolutionsTime = null;
}
public Speed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) {
public DistanceSpeed(String sensorAddress, String sensorName, int wheelRevolutionsCount, int wheelRevolutionsTime) {
super(sensorAddress, sensorName);
this.wheelRevolutionsCount = wheelRevolutionsCount;
this.wheelRevolutionsTime = wheelRevolutionsTime;
@@ -119,7 +119,7 @@ public final class SensorDataCycling {
return wheelRevolutionsTime;
}
public void compute(Speed previous, int wheel_circumference_mm) {
public void compute(DistanceSpeed previous, int wheel_circumference_mm) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
if (timeDiff_ms <= 0) {
@@ -128,42 +128,76 @@ public final class SensorDataCycling {
} else {
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S;
value = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M / timeDiff_s);
float distance_m = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M);
float distance_overall_m = distance_m;
if (previous.hasValue()) {
distance_overall_m += previous.getValue().distance_overall_m;
}
float speed_mps = (float) (distance_m / timeDiff_s);
value = new Data(distance_m, distance_overall_m, speed_mps);
}
}
}
@Override
public void reset() {
if (value != null) {
value = new Data(value.distance_m, 0, value.speed_mps);
}
}
@NonNull
@Override
public String toString() {
return "speed=" + value + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
return super.toString() + " data=" + value + " time=" + wheelRevolutionsTime + " count=" + wheelRevolutionsCount;
}
@Override
public boolean equals(@Nullable Object obj) {
if (!(obj instanceof Speed)) return false;
if (!(obj instanceof DistanceSpeed)) return false;
Speed comp = (Speed) obj;
DistanceSpeed comp = (DistanceSpeed) obj;
if (hasData() && comp.hasData() == hasData()) {
return getWheelRevolutionsCount() == comp.getWheelRevolutionsCount() && getWheelRevolutionsTime() == comp.getWheelRevolutionsTime();
} else {
return false;
}
}
public static class Data {
public final float distance_m;
public final float distance_overall_m;
public final float speed_mps;
private Data(float distance_m, float distance_overall_m, float speed_mps) {
this.distance_m = distance_m;
this.distance_overall_m = distance_overall_m;
this.speed_mps = speed_mps;
}
@Override
public String toString() {
return "Data{" +
"distance_m=" + distance_m +
", distance_overall_m=" + distance_overall_m +
", speed_mps=" + speed_mps +
'}';
}
}
}
public static class CadenceAndSpeed extends SensorData<Pair<Cadence, Speed>> {
public static class CadenceAndSpeed extends SensorData<Pair<Cadence, DistanceSpeed>> {
public CadenceAndSpeed(String sensorAddress, String sensorName, @NonNull Cadence cadence, @NonNull Speed speed) {
public CadenceAndSpeed(String sensorAddress, String sensorName, @Nullable Cadence cadence, @Nullable DistanceSpeed distanceSpeed) {
super(sensorAddress, sensorName);
this.value = new Pair<>(cadence, speed);
this.value = new Pair<>(cadence, distanceSpeed);
}
public Cadence getCadence() {
return this.value != null ? this.value.first : null;
}
public Speed getSpeed() {
public DistanceSpeed getDistanceSpeed() {
return this.value != null ? this.value.second : null;
}
}
@@ -16,6 +16,6 @@ public class SensorDataCyclingPower extends SensorData<Float> {
@NonNull
@Override
public String toString() {
return "power=" + value;
return super.toString() + " power=" + value;
}
}
@@ -16,6 +16,6 @@ public class SensorDataHeartRate extends SensorData<Float> {
@NonNull
@Override
public String toString() {
return "heart=" + value;
return super.toString() + " heart=" + value;
}
}
@@ -10,7 +10,7 @@ public final class SensorDataSet {
private SensorDataCycling.Cadence cyclingCadence;
private SensorDataCycling.Speed cyclingSpeed;
private SensorDataCycling.DistanceSpeed cyclingDistanceSpeed;
private SensorDataCyclingPower cyclingPower;
@@ -25,8 +25,8 @@ public final class SensorDataSet {
return cyclingCadence;
}
public SensorDataCycling.Speed getCyclingSpeed() {
return cyclingSpeed;
public SensorDataCycling.DistanceSpeed getCyclingDistanceSpeed() {
return cyclingDistanceSpeed;
}
public SensorDataCyclingPower getCyclingPower() {
@@ -44,7 +44,7 @@ public final class SensorDataSet {
public void clear() {
this.heartRate = null;
this.cyclingCadence = null;
this.cyclingSpeed = null;
this.cyclingDistanceSpeed = null;
this.cyclingPower = null;
}
@@ -57,8 +57,9 @@ public final class SensorDataSet {
trackPoint.setCyclingCadence_rpm(cyclingCadence.getValue());
}
if (cyclingSpeed != null && cyclingSpeed.hasValue()) {
trackPoint.setSpeed(cyclingSpeed.getValue());
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) {
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distance_overall_m);
trackPoint.setSpeed(cyclingDistanceSpeed.getValue().speed_mps);
}
if (cyclingPower != null && cyclingPower.hasValue()) {
@@ -66,12 +67,19 @@ public final class SensorDataSet {
}
}
public void reset() {
if (heartRate != null) heartRate.reset();
if (cyclingCadence != null) cyclingCadence.reset();
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.reset();
if (cyclingPower != null) cyclingPower.reset();
}
@NonNull
@Override
public String toString() {
return (getHeartRate() != null ? "" + getHeartRate() : "")
+ (getCyclingCadence() != null ? " " + getCyclingCadence() : "")
+ (getCyclingSpeed() != null ? " " + getCyclingSpeed() : "")
+ (getCyclingDistanceSpeed() != null ? " " + getCyclingDistanceSpeed() : "")
+ (getCyclingPower() != null ? " " + getCyclingPower() : "");
}
@@ -85,8 +93,8 @@ public final class SensorDataSet {
this.cyclingCadence = (SensorDataCycling.Cadence) data;
return;
}
if (type instanceof SensorDataCycling.Speed) {
this.cyclingSpeed = (SensorDataCycling.Speed) data;
if (type instanceof SensorDataCycling.DistanceSpeed) {
this.cyclingDistanceSpeed = (SensorDataCycling.DistanceSpeed) data;
return;
}
@@ -62,6 +62,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private TrackRecordingServiceConnection trackRecordingServiceConnection = new TrackRecordingServiceConnection();
private TrackPoint lastTrackPoint;
//TODO Should not be nullable
private TrackStatistics lastTrackStatistics;
private String category = "";
@@ -366,29 +367,26 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
private void setSpeedSensorData(SensorDataSet sensorDataSet) {
if (sensorDataSet != null && sensorDataSet.getCyclingSpeed() != null) {
SensorDataCycling.Speed data = sensorDataSet.getCyclingSpeed();
if (sensorDataSet != null && sensorDataSet.getCyclingDistanceSpeed() != null) {
SensorDataCycling.DistanceSpeed data = sensorDataSet.getCyclingDistanceSpeed();
if (data.hasValue() && data.isRecent()) {
setSpeed(data.getValue());
setTotalDistance(data.getValue().distance_overall_m);
setSpeed(data.getValue().speed_mps);
}
if (data.hasValue() && data.isRecent()) {
setSpeed(data.getValue().speed_mps);
}
}
}
private void updateStats() {
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
// Set total distance
{
double totalDistance = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
}
setTotalDistance(0);
// Set activity type
{
String trackIconValue = TrackIconUtils.getIconValue(getContext(), category);
viewBinding.statsActivityTypeIcon.setEnabled(isSelectedTrackRecording());
viewBinding.statsActivityTypeIcon.setImageResource(TrackIconUtils.getIconDrawable(trackIconValue));
}
@@ -479,6 +477,14 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
}
private void setTotalDistance(double sensorDistanceSinceLastTrackpoint) {
double totalDistance = lastTrackStatistics != null ? (lastTrackStatistics.getTotalDistance() + sensorDistanceSinceLastTrackpoint) : Double.NaN;
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
}
private void setSpeed(double speed) {
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
@@ -49,6 +49,7 @@ public class GPXTrackExporter implements TrackExporter {
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat SPEED_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat DISTANCE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat HEARTRATE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat CADENCE_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat POWER_FORMAT = NumberFormat.getInstance(Locale.US);
@@ -312,6 +313,10 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<opentracks:loss>" + ELEVATION_FORMAT.format(trackPoint.getElevationLoss()) + "</opentracks:loss>");
}
if (trackPoint.hasSensorDistance()) {
printWriter.println("<opentracks:distance>" + DISTANCE_FORMAT.format(trackPoint.getElevationLoss()) + "</opentracks:distance>");
}
printWriter.println("</gpxtpx:TrackPointExtension></extensions>");
}
@@ -57,6 +57,7 @@ public class KMLTrackExporter implements TrackExporter {
private static final String SCHEMA_ID = "schema";
public static final String EXTENDED_DATA_TYPE_SPEED = "speed";
public static final String EXTENDED_DATA_TYPE_DISTANCE = "distance";
public static final String EXTENDED_DATA_TYPE_CADENCE = "cadence";
public static final String EXTENDED_DATA_TYPE_HEART_RATE = "heart_rate";
public static final String EXTENDED_DATA_TYPE_POWER = "power";
@@ -74,6 +75,7 @@ public class KMLTrackExporter implements TrackExporter {
private PrintWriter printWriter;
private final List<Float> speedList = new ArrayList<>();
private final List<Float> distanceList = new ArrayList<>();
private final List<Float> powerList = new ArrayList<>();
private final List<Float> cadenceList = new ArrayList<>();
private final List<Float> heartRateList = new ArrayList<>();
@@ -338,6 +340,7 @@ public class KMLTrackExporter implements TrackExporter {
if (printWriter != null) {
printWriter.println("<gx:Track>");
speedList.clear();
distanceList.clear();
powerList.clear();
cadenceList.clear();
heartRateList.clear();
@@ -355,6 +358,9 @@ public class KMLTrackExporter implements TrackExporter {
writeSimpleArrayData(speedList, EXTENDED_DATA_TYPE_SPEED);
}
if (exportSensorData) {
if (distanceList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(distanceList, EXTENDED_DATA_TYPE_DISTANCE);
}
if (powerList.stream().anyMatch(Objects::nonNull)) {
writeSimpleArrayData(powerList, EXTENDED_DATA_TYPE_POWER);
}
@@ -392,6 +398,7 @@ public class KMLTrackExporter implements TrackExporter {
speedList.add(trackPoint.hasSpeed() ? trackPoint.getSpeed() : null);
if (exportSensorData) {
distanceList.add(trackPoint.hasSensorDistance() ? trackPoint.getSensorDistance() : null);
heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate_bpm() : null);
cadenceList.add(trackPoint.hasCyclingCadence() ? trackPoint.getCyclingCadence_rpm() : null);
powerList.add(trackPoint.hasPower() ? trackPoint.getPower() : null);
@@ -91,6 +91,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
protected String uuid;
protected String gain;
protected String loss;
protected String distance;
// The current track data
private TrackData trackData;
@@ -141,9 +142,6 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
}
/**
* On file end.
*/
protected void onFileEnd() {
// Add markers to the last imported track
int size = trackIds.size();
@@ -376,7 +374,7 @@ 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.
*/
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
float speed = trackData.lastLocationInCurrentSegment.distanceTo(trackPoint) / timeDifference.toMillis();
float speed = trackData.lastLocationInCurrentSegment.distanceToPrevious(trackPoint) / timeDifference.toMillis();
trackPoint.setSpeed(speed);
}
}
@@ -385,7 +383,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
long maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); //TODO Should only be read once!
double distanceToLastTrackLocation = trackPoint.distanceTo(trackData.lastLocationInCurrentSegment);
double distanceToLastTrackLocation = trackPoint.distanceToPrevious(trackData.lastLocationInCurrentSegment);
if (distanceToLastTrackLocation > maxRecordingDistance) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
}
@@ -505,6 +503,13 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse elevation loss: %s", loss)), e);
}
}
if (distance != null) {
try {
trackPoint.setSensorDistance(Float.parseFloat(distance));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
}
}
return trackPoint;
}
@@ -63,6 +63,7 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_EXTENSION_GAIN = "opentracks:gain";
private static final String TAG_EXTENSION_LOSS = "opentracks:loss";
private static final String TAG_EXTENSION_DISTANCE = "opentracks:distance";
/**
* Constructor.
@@ -179,6 +180,11 @@ public class GpxFileTrackImporter extends AbstractFileTrackImporter {
loss = content.trim();
}
break;
case TAG_EXTENSION_DISTANCE:
if (content != null) {
distance = content.trim();
}
break;
}
// Reset element content
@@ -66,6 +66,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private String extendedDataType;
private final ArrayList<TrackPoint> trackPoints = new ArrayList<>();
private final ArrayList<Float> speedList = new ArrayList<>();
private final ArrayList<Float> distanceList = new ArrayList<>();
private final ArrayList<Float> cadenceList = new ArrayList<>();
private final ArrayList<Float> heartRateList = new ArrayList<>();
private final ArrayList<Float> powerList = new ArrayList<>();
@@ -216,6 +217,7 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
super.onTrackSegmentStart();
trackPoints.clear();
speedList.clear();
distanceList.clear();
heartRateList.clear();
cadenceList.clear();
powerList.clear();
@@ -232,6 +234,9 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
if (i < speedList.size()) {
trackPoint.setSpeed(speedList.get(i));
}
if (i < distanceList.size()) {
trackPoint.setSensorDistance(distanceList.get(i));
}
if (i < heartRateList.size()) {
trackPoint.setHeartRate_bpm(heartRateList.get(i));
}
@@ -310,6 +315,9 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
case KMLTrackExporter.EXTENDED_DATA_TYPE_SPEED:
speedList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_DISTANCE:
distanceList.add(value);
break;
case KMLTrackExporter.EXTENDED_DATA_TYPE_POWER:
powerList.add(value);
break;
@@ -571,7 +571,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
double distanceToLastTrackLocation = trackPoint.distanceTo(lastValidTrackPoint);
double distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastValidTrackPoint);
if (distanceToLastTrackLocation > maxRecordingDistance) {
insertTrackPointIfNewer(track, lastTrackPoint);
@@ -664,6 +664,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
trackPoint.setElevationLoss(elevationSumManager.getElevationLoss_m());
elevationSumManager.reset();
}
if (remoteSensorManager != null) {
fillWithSensorDataSet(trackPoint);
remoteSensorManager.reset();
}
contentProviderUtils.insertTrackPoint(trackPoint, track.getId());
trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval);
@@ -101,11 +101,11 @@ public abstract class BluetoothConnectionManager {
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
String sensorName = gatt.getDevice().getName();
Log.d(TAG, "Received data from " + sensorName);
Log.d(TAG, "Received data from " + gatt.getDevice().getAddress());
SensorData sensorData = parsePayload(sensorName, gatt.getDevice().getAddress(), characteristic);
SensorData<?> sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
if (sensorData != null) {
Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData);
observer.onChanged(sensorData);
}
}
@@ -125,7 +125,7 @@ public abstract class BluetoothConnectionManager {
Log.d(TAG, "Connecting to: " + device);
bluetoothGatt = device.connectGatt(context, true, connectCallback);
SensorData sensorData = createEmptySensorData(bluetoothGatt.getDevice().getAddress());
SensorData<?> sensorData = createEmptySensorData(bluetoothGatt.getDevice().getAddress());
observer.onChanged(sensorData);
}
@@ -148,12 +148,12 @@ public abstract class BluetoothConnectionManager {
return address.equals(bluetoothGatt.getDevice().getAddress());
}
protected abstract SensorData createEmptySensorData(String address);
protected abstract SensorData<?> createEmptySensorData(String address);
/**
* @return null if data could not be parsed.
*/
protected abstract SensorData parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);
protected abstract SensorData<?> parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic);
public static class HeartRate extends BluetoothConnectionManager {
@@ -162,7 +162,7 @@ public abstract class BluetoothConnectionManager {
}
@Override
protected SensorData createEmptySensorData(String address) {
protected SensorDataHeartRate createEmptySensorData(String address) {
return new SensorDataHeartRate(address);
}
@@ -181,7 +181,7 @@ public abstract class BluetoothConnectionManager {
}
@Override
protected SensorData createEmptySensorData(String address) {
protected SensorDataCycling.Cadence createEmptySensorData(String address) {
return new SensorDataCycling.Cadence(address);
}
@@ -197,30 +197,30 @@ public abstract class BluetoothConnectionManager {
}
//Workaround for Wahoo CADENCE: this sensor reports speed (instead of cadence)
if (cadenceAndSpeed.getSpeed() != null) {
return new SensorDataCycling.Cadence(cadenceAndSpeed.getSpeed());
if (cadenceAndSpeed.getDistanceSpeed() != null) {
return new SensorDataCycling.Cadence(cadenceAndSpeed.getDistanceSpeed());
}
return null;
}
}
public static class CyclingSpeed extends BluetoothConnectionManager {
public static class CyclingDistanceSpeed extends BluetoothConnectionManager {
CyclingSpeed(SensorDataObserver observer) {
CyclingDistanceSpeed(SensorDataObserver observer) {
super(BluetoothUtils.CYCLING_SPEED_CADENCE_SERVICE_UUID, BluetoothUtils.CYCLING_SPEED_CADENCE_MEASUREMENT_CHAR_UUID, observer);
}
@Override
protected SensorData createEmptySensorData(String address) {
return new SensorDataCycling.Speed(address);
protected SensorDataCycling.DistanceSpeed createEmptySensorData(String address) {
return new SensorDataCycling.DistanceSpeed(address);
}
@Override
protected SensorDataCycling.Speed parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
protected SensorDataCycling.DistanceSpeed parsePayload(String sensorName, String address, BluetoothGattCharacteristic characteristic) {
SensorDataCycling.CadenceAndSpeed cadenceAndSpeed = BluetoothUtils.parseCyclingCrankAndWheel(address, sensorName, characteristic);
if (cadenceAndSpeed != null) {
return cadenceAndSpeed.getSpeed();
return cadenceAndSpeed.getDistanceSpeed();
}
return null;
}
@@ -233,7 +233,7 @@ public abstract class BluetoothConnectionManager {
}
@Override
protected SensorData createEmptySensorData(String address) {
protected SensorDataCyclingPower createEmptySensorData(String address) {
return new SensorDataCyclingPower(address);
}
@@ -247,8 +247,8 @@ public abstract class BluetoothConnectionManager {
interface SensorDataObserver {
void onChanged(SensorData sensorData);
void onChanged(SensorData<?> sensorData);
void onDisconnecting(SensorData sensorData);
void onDisconnecting(SensorData<?> sensorData);
}
}
@@ -53,13 +53,15 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
private final BluetoothAdapter bluetoothAdapter;
private final Context context;
private final SharedPreferences sharedPreferences;
private boolean started = false;
private final SharedPreferences sharedPreferences;
private int preferenceWheelCircumference;
private final BluetoothConnectionManager.HeartRate heartRate = new BluetoothConnectionManager.HeartRate(this);
private final BluetoothConnectionManager.CyclingCadence cyclingCadence = new BluetoothConnectionManager.CyclingCadence(this);
private final BluetoothConnectionManager.CyclingSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingSpeed(this);
private final BluetoothConnectionManager.CyclingDistanceSpeed cyclingSpeed = new BluetoothConnectionManager.CyclingDistanceSpeed(this);
private final BluetoothConnectionManager.CyclingPower cyclingPower = new BluetoothConnectionManager.CyclingPower(this);
private final SensorDataSet sensorDataSet = new SensorDataSet();
@@ -90,6 +92,11 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
connect(cyclingPower, address);
}
if (PreferencesUtils.isKey(context, R.string.settings_sensor_bluetooth_cycling_power_key, key)) {
preferenceWheelCircumference = PreferencesUtils.getWheelCircumference(sharedPreferences, context);
}
}
};
@@ -155,11 +162,15 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
return sensorDataSet;
}
public void reset() {
sensorDataSet.reset();
}
@Override
public synchronized void onChanged(SensorData sensorData) {
public synchronized void onChanged(SensorData<?> sensorData) {
if (sensorData instanceof SensorDataCycling.Cadence) {
SensorDataCycling.Cadence previous = sensorDataSet.getCyclingCadence();
Log.d(TAG, "previous " + previous + "; current" + sensorData);
Log.d(TAG, "Previous: " + previous + "; current: " + sensorData);
if (sensorData.equals(previous)) {
Log.d(TAG, "onChanged: cadence data repeated.");
@@ -167,21 +178,21 @@ public class BluetoothRemoteSensorManager implements BluetoothConnectionManager.
}
((SensorDataCycling.Cadence) sensorData).compute(previous);
}
if (sensorData instanceof SensorDataCycling.Speed) {
SensorDataCycling.Speed previous = sensorDataSet.getCyclingSpeed();
Log.d(TAG, "previous " + previous + "; current" + sensorData);
if (sensorData instanceof SensorDataCycling.DistanceSpeed) {
SensorDataCycling.DistanceSpeed previous = sensorDataSet.getCyclingDistanceSpeed();
Log.d(TAG, "Previous: " + previous + "; Current" + sensorData);
if (sensorData.equals(previous)) {
Log.d(TAG, "onChanged: speed data repeated.");
return;
}
((SensorDataCycling.Speed) sensorData).compute(previous, PreferencesUtils.getWheelCircumference(sharedPreferences, context));
((SensorDataCycling.DistanceSpeed) sensorData).compute(previous, PreferencesUtils.getWheelCircumference(sharedPreferences, context));
}
sensorDataSet.set(sensorData);
}
@Override
public void onDisconnecting(SensorData sensorData) {
public void onDisconnecting(SensorData<?> sensorData) {
sensorDataSet.remove(sensorData);
}
}
@@ -103,10 +103,15 @@ public class TrackStatisticsUpdater {
/**
* Adds a trackPoint.
*
* @param trackPoint the trackPoint
* @param minRecordingDistance the min recording distance
* @param trackPoint the trackPoint
* @param minGPSDistance the min recording distance
*/
public void addTrackPoint(TrackPoint trackPoint, int minRecordingDistance) {
public void addTrackPoint(TrackPoint trackPoint, int minGPSDistance) {
internalAddTrackPoint(trackPoint, minGPSDistance);
Log.d(TAG, this.toString());
}
private void internalAddTrackPoint(TrackPoint trackPoint, int minGPSDistance) {
if (!trackInitialized) {
trackStatistics.setStartTime(trackPoint.getTime());
trackInitialized = true;
@@ -120,19 +125,26 @@ public class TrackStatisticsUpdater {
currentSegment.setStopTime(trackPoint.getTime());
currentSegment.setTotalTime(Duration.between(currentSegment.getStartTime(), trackPoint.getTime()));
if (trackPoint.isSegmentStart() || trackPoint.isSegmentEnd()) {
if (trackPoint.isSegmentEnd()) {
if (lastTrackPoint != null && lastMovingTrackPoint != null && lastTrackPoint != lastMovingTrackPoint) {
currentSegment.addTotalDistance(lastMovingTrackPoint.distanceTo(lastTrackPoint));
}
}
trackStatistics.merge(currentSegment);
currentSegment.reset(trackPoint.getTime());
if (trackPoint.isSegmentStart()) {
reset(trackPoint);
return;
}
lastTrackPoint = null;
lastMovingTrackPoint = null;
elevationBuffer_m.reset();
speedBuffer_ms.reset();
// Process sensor data
if (trackPoint.hasElevationGain()) {
currentSegment.addTotalElevationGain(trackPoint.getElevationGain());
}
if (trackPoint.hasElevationLoss()) {
currentSegment.addTotalElevationLoss(trackPoint.getElevationLoss());
}
if (trackPoint.hasSensorDistance()) {
currentSegment.addTotalDistance(trackPoint.getSensorDistance());
}
if (trackPoint.isSegmentEnd()) {
reset(trackPoint);
return;
}
@@ -141,39 +153,30 @@ public class TrackStatisticsUpdater {
updateAbsoluteElevation(trackPoint.getAltitude());
}
//Get elevation gain
if (trackPoint.hasElevationGain()) {
currentSegment.addTotalElevationGain(trackPoint.getElevationGain());
Log.d(TAG, "elevation gain: " + trackPoint.getElevationGain());
}
//Get elevation loss
if (trackPoint.hasElevationLoss()) {
currentSegment.addTotalElevationLoss(trackPoint.getElevationLoss());
Log.d(TAG, "elevation loss: " + trackPoint.getElevationLoss());
}
if (lastTrackPoint == null || lastMovingTrackPoint == null) {
lastTrackPoint = trackPoint;
lastMovingTrackPoint = trackPoint;
return;
}
double movingDistance = lastMovingTrackPoint.distanceTo(trackPoint);
if (movingDistance < minRecordingDistance && !trackPoint.isMoving()) {
speedBuffer_ms.reset();
lastTrackPoint = trackPoint;
return;
if (!trackPoint.hasSensorDistance()) {
// GPS-based distance/speed
float movingDistance = lastMovingTrackPoint.distanceToPrevious(trackPoint);
if (movingDistance < minGPSDistance && !trackPoint.isMoving()) {
speedBuffer_ms.reset();
lastTrackPoint = trackPoint;
return;
}
// Update total distance
currentSegment.addTotalDistance(movingDistance);
}
Duration movingTime = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
if (movingTime.isNegative()) {
lastTrackPoint = trackPoint;
return;
}
// Update total distance
currentSegment.addTotalDistance(movingDistance);
// Update moving time
currentSegment.addMovingTime(movingTime);
@@ -186,6 +189,16 @@ public class TrackStatisticsUpdater {
lastMovingTrackPoint = trackPoint;
}
private void reset(TrackPoint trackPoint) {
trackStatistics.merge(currentSegment);
currentSegment.reset(trackPoint.getTime());
lastTrackPoint = null;
lastMovingTrackPoint = null;
elevationBuffer_m.reset();
speedBuffer_ms.reset();
}
public void addTrackPoint(TrackPointIterator iterator, int minRecordingDistance) {
while (iterator.hasNext()) {
TrackPoint location = iterator.next();
@@ -224,20 +237,14 @@ public class TrackStatisticsUpdater {
/**
* Updates an elevation reading.
*
* @param elevation the elevation
* @return the difference
*/
@VisibleForTesting
private double updateAbsoluteElevation(double elevation) {
private void updateAbsoluteElevation(double elevation) {
// Update elevation using the smoothed average
double oldAverage = elevationBuffer_m.getAverage();
elevationBuffer_m.setNext(elevation);
double newAverage = elevationBuffer_m.getAverage();
currentSegment.updateElevationExtremities(newAverage);
return newAverage - oldAverage;
}
private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
@@ -267,4 +274,11 @@ public class TrackStatisticsUpdater {
return true;
}
@Override
public String toString() {
return "TrackStatisticsUpdater{" +
"trackStatistics=" + trackStatistics +
'}';
}
}
@@ -126,12 +126,12 @@ public class BluetoothUtils {
cadence = new SensorDataCycling.Cadence(address, sensorName, crankCount, crankTime);
}
SensorDataCycling.Speed speed = null;
SensorDataCycling.DistanceSpeed speed = null;
if (hasWheel && valueLength - index >= 4) {
int wheelCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index);
index += 2;
int wheelTime = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, index); // 1/1024s
speed = new SensorDataCycling.Speed(address, sensorName, wheelCount, wheelTime);
speed = new SensorDataCycling.DistanceSpeed(address, sensorName, wheelCount, wheelTime);
}
return new SensorDataCycling.CadenceAndSpeed(address, sensorName, cadence, speed);
@@ -42,6 +42,7 @@ public class PreferencesUtils {
private PreferencesUtils() {
}
@Deprecated //Should only be used to get a sharedPreference for more than one interaction!
public static SharedPreferences getSharedPreferences(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context);
}
@@ -102,7 +103,6 @@ public class PreferencesUtils {
editor.apply();
}
private static int getInt(SharedPreferences sharedPreferences, Context context, int keyId, int defaultValue) {
try {
return sharedPreferences.getInt(getKey(context, keyId), defaultValue);
@@ -206,7 +206,6 @@ public class PreferencesUtils {
return getString(sharedPreferences, context, R.string.settings_sensor_bluetooth_cycling_power_key, getBluetoothSensorAddressNone(context));
}
public static boolean shouldShowStatsOnLockscreen(SharedPreferences sharedPreferences, Context context) {
final boolean STATS_SHOW_ON_LOCKSCREEN_DEFAULT = context.getResources().getBoolean(R.bool.stats_show_on_lockscreen_while_recording_default);
return getBoolean(sharedPreferences, context, R.string.stats_show_on_lockscreen_while_recording_key, STATS_SHOW_ON_LOCKSCREEN_DEFAULT);
@@ -33,7 +33,7 @@ public class IntervalStatistics {
TrackPoint trackPoint = trackPoints.get(i);
if (trackPoint.hasLocation() && prevTrackPoint.hasLocation()) {
interval.distance_m += prevTrackPoint.distanceTo(trackPoint);
interval.distance_m += prevTrackPoint.distanceToPrevious(trackPoint);
interval.time = interval.time.plus(Duration.between(prevTrackPoint.getTime(), trackPoint.getTime()));
interval.gain_m += trackPoint.hasElevationGain() ? trackPoint.getElevationGain() : 0;
interval.loss_m += trackPoint.hasElevationLoss() ? trackPoint.getElevationLoss() : 0;