diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java b/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java index 83c272937..9ae50ac84 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java +++ b/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java @@ -68,9 +68,9 @@ public class TestDataUtil { trackPoint.setSpeed(5f + (i / 10)); trackPoint.setHeartRate_bpm(100f + i); - trackPoint.setCyclingCadence_rpm(200f + i); trackPoint.setCyclingCadence_rpm(300f + i); trackPoint.setPower(400f + i); + trackPoint.setElevationGain(500f + i); return trackPoint; } diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java index b7a517715..339c13e9f 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java @@ -455,7 +455,7 @@ public class CustomContentProviderUtilsTest { statistics.setTotalTime(1500L); statistics.setMovingTime(700L); statistics.setTotalDistance(750.0); - statistics.setTotalElevationGain(50.0); + statistics.setTotalElevationGain(50.0f); statistics.setMaxSpeed(60.0); statistics.setMaxElevation(1250.0); statistics.setMinElevation(1200.0); diff --git a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java index 4b58f68d4..7520f790f 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java @@ -133,7 +133,7 @@ public class ExportImportTest { assertMarkers(); // 3. trackpoints - assertTrackpoints(false, false, false); + assertTrackpoints(false, false, false, false); } @LargeTest @@ -168,7 +168,7 @@ public class ExportImportTest { assertMarkers(); // 3. trackpoints - assertTrackpoints(true, true, true); + assertTrackpoints(true, true, true, true); } @LargeTest @@ -257,7 +257,7 @@ public class ExportImportTest { assertMarkers(); // 3. trackpoints - assertTrackpoints(false, true, true); + assertTrackpoints(false, true, true, false); } @LargeTest @@ -304,7 +304,7 @@ public class ExportImportTest { } } - private void assertTrackpoints(boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence) { + private void assertTrackpoints(boolean verifyPower, boolean verifyHeartrate, boolean verifyCadence, boolean verifyElevationGain) { List importedTrackPoints = contentProviderUtils.getTrackPoints(importTrackId); assertEquals(trackPoints.size(), importedTrackPoints.size()); @@ -330,6 +330,9 @@ public class ExportImportTest { if (verifyPower) { assertEquals(trackPoint.getPower(), importedTrackPoint.getPower(), 0.01); } + if (verifyElevationGain) { + assertEquals(trackPoint.getElevationGain(), importedTrackPoint.getElevationGain(), 0.01); + } } } } \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/sensors/ElevationSumManagerTest.java b/src/androidTest/java/de/dennisguse/opentracks/services/sensors/ElevationSumManagerTest.java new file mode 100644 index 000000000..6c1c2b40c --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/services/sensors/ElevationSumManagerTest.java @@ -0,0 +1,50 @@ +package de.dennisguse.opentracks.services.sensors; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +/** + * NOTE: Test data is completely artificial. + */ +public class ElevationSumManagerTest { + + private ElevationSumManager elevationSumManager = new ElevationSumManager(); + + private static void addSensorValue(ElevationSumManager elevationSumManager, float[] values) { + for (float f : values) { + elevationSumManager.onSensorValueChanged(f); + } + } + + @Before + public void setUp() { + elevationSumManager.reset(); + } + + @Test + public void getElevationGainLoss_downhill() { + // given + elevationSumManager.setConnected(true); + + // then + addSensorValue(elevationSumManager, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1015.14f, 1015.15f}); + + // then + Assert.assertEquals(0f, elevationSumManager.getElevationGain_m(), 0.01); + Assert.assertEquals(-48.0, elevationSumManager.getElevationLoss_m(), 0.01); + } + + @Test + public void sensorUnavailable() { + // given + elevationSumManager.setConnected(false); + + // then + elevationSumManager.onSensorValueChanged(999f); + + // then + Assert.assertNull(elevationSumManager.getElevationGain_m()); + Assert.assertNull(elevationSumManager.getElevationLoss_m()); + } +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/stats/TrackStatisticsTest.java b/src/androidTest/java/de/dennisguse/opentracks/stats/TrackStatisticsTest.java index 11bfd1239..a7fb3727a 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/stats/TrackStatisticsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/stats/TrackStatisticsTest.java @@ -51,8 +51,8 @@ public class TrackStatisticsTest { statistics2.setMovingTime(600L); // Result: 700+600 statistics.setTotalDistance(750.0); statistics2.setTotalDistance(350.0); // Result: 750+350 - statistics.setTotalElevationGain(50.0); - statistics2.setTotalElevationGain(850.0); // Result: 850+50 + statistics.setTotalElevationGain(50.0f); + statistics2.setTotalElevationGain(850.0f); // Result: 850+50 statistics.setMaxSpeed(60.0); // Resulting max speed statistics2.setMaxSpeed(30.0); statistics.setMaxElevation(1250.0); diff --git a/src/androidTest/java/de/dennisguse/opentracks/util/PressureSensorUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/util/PressureSensorUtilsTest.java new file mode 100644 index 000000000..f3ee34a25 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/util/PressureSensorUtilsTest.java @@ -0,0 +1,49 @@ +package de.dennisguse.opentracks.util; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class PressureSensorUtilsTest { + + // real data at 500Hz stationary + @Test + public void elevationChanges_none() { + // given + float[] sensorValues_hPa = new float[]{1015.6876f, 1015.699f, 1015.70905f, 1015.71075f, 1015.7224f, 1015.72f, 1015.7373f, 1015.7481f, 1015.74133f, 1015.73553f, 1015.7462f, 1015.74896f, 1015.752f, 1015.754f, 1015.7456f, 1015.73303f, 1015.7288f, 1015.7239f, 1015.7298f, 1015.7387f, 1015.72534f, 1015.72577f, 1015.72797f, 1015.71796f, 1015.7113f, 1015.7097f, 1015.7096f, 1015.7013f, 1015.7072f, 1015.71063f, 1015.7088f, 1015.7115f, 1015.713f, 1015.7121f, 1015.7062f, 1015.7062f, 1015.6994f, 1015.69617f, 1015.68945f, 1015.698f, 1015.6913f, 1015.69696f, 1015.69293f, 1015.6955f, 1015.6928f, 1015.6939f, 1015.68787f, 1015.6881f, 1015.68536f, 1015.68726f, 1015.6879f, 1015.6904f, 1015.6937f, 1015.69476f, 1015.7046f, 1015.7019f, 1015.7079f, 1015.71124f, 1015.71216f, 1015.71436f, 1015.7171f, 1015.7186f, 1015.7186f, 1015.7246f, 1015.72046f, 1015.7211f, 1015.72375f, 1015.7194f, 1015.72125f, 1015.7161f, 1015.71185f, 1015.7188f, 1015.721f, 1015.7252f, 1015.72266f, 1015.7236f, 1015.72614f, 1015.7266f, 1015.7344f, 1015.7376f, 1015.7337f, 1015.7253f, 1015.71875f, 1015.7219f, 1015.7219f, 1015.7317f, 1015.7284f, 1015.73444f, 1015.73584f, 1015.7369f, 1015.73254f, 1015.7369f, 1015.7336f, 1015.73254f, 1015.73676f, 1015.74176f, 1015.74506f, 1015.7478f, 1015.755f, 1015.7515f, 1015.7543f, 1015.7459f, 1015.7468f, 1015.7492f, 1015.74585f, 1015.74756f, 1015.74756f, 1015.75085f, 1015.754f, 1015.75336f, 1015.7568f, 1015.75165f, 1015.761f, 1015.7527f, 1015.75684f, 1015.7635f, 1015.7585f, 1015.7552f, 1015.7475f, 1015.74506f, 1015.7542f, 1015.75586f, 1015.74176f, 1015.74414f, 1015.7501f, 1015.751f, 1015.7478f, 1015.755f, 1015.75836f, 1015.7592f, 1015.75256f, 1015.7559f, 1015.76654f, 1015.76154f, 1015.766f, 1015.77014f, 1015.76904f, 1015.76013f, 1015.7674f, 1015.76514f, 1015.7674f, 1015.7635f, 1015.76404f, 1015.7607f, 1015.754f, 1015.7574f, 1015.7607f, 1015.7607f, 1015.7607f, 1015.754f, 1015.7567f, 1015.7616f, 1015.7643f, 1015.7666f, 1015.7609f, 1015.766f, 1015.76764f, 1015.755f, 1015.75757f, 1015.74927f, 1015.74927f, 1015.7426f, 1015.745f, 1015.745f, 1015.7409f, 1015.7376f, 1015.7326f, 1015.74164f, 1015.74664f, 1015.745f, 1015.7459f, 1015.7502f, 1015.7493f, 1015.7476f, 1015.7493f, 1015.751f, 1015.7601f, 1015.76434f, 1015.766f, 1015.756f, 1015.7617f, 1015.7623f, 1015.76404f, 1015.76733f, 1015.7607f, 1015.7623f, 1015.76337f, 1015.7584f, 1015.7573f, 1015.7502f, 1015.7534f, 1015.7534f, 1015.7601f, 1015.75684f, 1015.7518f, 1015.75574f, 1015.7524f, 1015.7507f, 1015.7484f, 1015.7502f, 1015.7534f, 1015.7623f, 1015.75903f, 1015.75574f, 1015.75574f, 1015.7573f, 1015.7524f, 1015.75574f, 1015.75574f, 1015.7607f, 1015.7573f, 1015.754f, 1015.74677f, 1015.754f, 1015.75574f, 1015.75903f, 1015.7573f, 1015.7551f, 1015.7573f, 1015.754f, 1015.7507f, 1015.7374f, 1015.739f, 1015.73566f, 1015.73566f, 1015.7367f, 1015.7417f, 1015.7417f, 1015.739f, 1015.74835f, 1015.74567f, 1015.7517f, 1015.74066f, 1015.7374f, 1015.739f, 1015.7417f, 1015.73065f, 1015.72894f, 1015.73505f, 1015.73334f, 1015.74005f, 1015.74005f, 1015.7367f, 1015.7434f, 1015.744f, 1015.7423f, 1015.7384f, 1015.739f, 1015.7374f, 1015.7374f, 1015.7374f, 1015.7341f, 1015.73505f, 1015.7341f, 1015.7267f, 1015.72833f, 1015.7234f, 1015.7167f, 1015.725f, 1015.7217f, 1015.7234f, 1015.73004f, 1015.7324f, 1015.7423f, 1015.74005f, 1015.73505f, 1015.73004f, 1015.739f, 1015.7417f, 1015.74567f, 1015.74066f, 1015.74146f, 1015.74146f, 1015.73816f, 1015.7274f, 1015.72906f, 1015.7274f, 1015.7341f, 1015.739f, 1015.744f, 1015.7417f, 1015.7374f, 1015.7324f, 1015.73816f, 1015.74146f, 1015.7374f, 1015.7341f, 1015.7365f, 1015.7365f, 1015.74146f, 1015.74414f, 1015.7475f, 1015.7448f, 1015.74146f, 1015.74585f, 1015.7475f, 1015.75146f, 1015.75146f, 1015.7591f, 1015.75476f, 1015.7614f, 1015.75476f, 1015.7492f, 1015.74817f, 1015.7498f, 1015.7515f, 1015.75586f, 1015.7509f, 1015.7448f, 1015.7525f, 1015.7581f, 1015.7592f}; + float firstSensorValue = sensorValues_hPa[0]; + + // when // then + for (float v : sensorValues_hPa) { + assertNull(PressureSensorUtils.computeChanges_m(firstSensorValue, v)); + } + } + + // Simulate a sudden drop of elevation. + // test data + @Test + public void computeChanges_downhill() { + float[] sensorValues_hPa = new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f}; + float firstSensorValue = sensorValues_hPa[0]; + + // when + float elevationGain_m = 0; + float elevationLoss_m = 0; + float lastUsedPressureValue_hPa = firstSensorValue; + + for (float v : sensorValues_hPa) { + PressureSensorUtils.ElevationChange elevationChange = PressureSensorUtils.computeChanges_m(lastUsedPressureValue_hPa, v); + if (elevationChange != null) { + elevationGain_m += elevationChange.getElevationGain_m(); + elevationLoss_m += elevationChange.getElevationLoss_m(); + lastUsedPressureValue_hPa = elevationChange.getCurrentSensorValue_hPa(); + } + } + + // then + assertEquals(0f, elevationGain_m, 0.01); + assertEquals(-15.0f, elevationLoss_m, 0.01); + assertEquals(1016.80f, lastUsedPressureValue_hPa, 0.01); //Expect exponential smoothing + } +} \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/viewmodels/AggregatedStatisticsTest.java b/src/androidTest/java/de/dennisguse/opentracks/viewmodels/AggregatedStatisticsTest.java index b6148d3a4..4d67bfc64 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/viewmodels/AggregatedStatisticsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/viewmodels/AggregatedStatisticsTest.java @@ -35,7 +35,7 @@ public class AggregatedStatisticsTest { statistics.setTotalTime(totalTime); statistics.setMovingTime(totalTime); statistics.setTotalDistance(totalDistance); - statistics.setTotalElevationGain(50.0); + statistics.setTotalElevationGain(50.0f); statistics.setMaxSpeed(50.0); // Resulting max speed statistics.setMaxElevation(1250.0); statistics.setMinElevation(1200.0); // Resulting min elevation diff --git a/src/main/java/de/dennisguse/opentracks/content/DescriptionGenerator.java b/src/main/java/de/dennisguse/opentracks/content/DescriptionGenerator.java index bbd0c3f76..f73dd8d84 100644 --- a/src/main/java/de/dennisguse/opentracks/content/DescriptionGenerator.java +++ b/src/main/java/de/dennisguse/opentracks/content/DescriptionGenerator.java @@ -136,7 +136,9 @@ public class DescriptionGenerator { } // Elevation gain - writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak); + if (stats.hasTotalElevationGain()) { + writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak); + } // Recorded time builder.append(context.getString(R.string.description_recorded_time, StringUtils.formatDateTime(context, stats.getStartTime_ms()))); diff --git a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java index 17e2c2d0d..decb6a364 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/TrackPoint.java @@ -32,6 +32,7 @@ public class TrackPoint { private Float heartRate_bpm = null; private Float cyclingCadence_rpm = null; private Float power = null; + private Float elevationGain = null; public TrackPoint() { this.location = new Location(""); @@ -92,6 +93,18 @@ public class TrackPoint { return location; } + public boolean hasElevationGain() { + return elevationGain != null; + } + + public float getElevationGain() { + return elevationGain; + } + + public void setElevationGain(Float elevationGain) { + this.elevationGain = elevationGain; + } + public double getLatitude() { return location.getLatitude(); } diff --git a/src/main/java/de/dennisguse/opentracks/content/data/TrackPointsColumns.java b/src/main/java/de/dennisguse/opentracks/content/data/TrackPointsColumns.java index d8d434853..59fa38806 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/TrackPointsColumns.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/TrackPointsColumns.java @@ -51,6 +51,7 @@ public interface TrackPointsColumns extends BaseColumns { String SENSOR_HEARTRATE = "sensor_heartrate"; String SENSOR_CADENCE = "sensor_cadence"; String SENSOR_POWER = "sensor_power"; + String ELEVATION_GAIN = "elevation_gain"; String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " @@ -64,7 +65,8 @@ public interface TrackPointsColumns extends BaseColumns { + BEARING + " FLOAT, " + SENSOR_HEARTRATE + " FLOAT, " + SENSOR_CADENCE + " FLOAT, " - + SENSOR_POWER + " FLOAT)"; + + SENSOR_POWER + " FLOAT, " + + ELEVATION_GAIN + " FLOAT)"; String CREATE_TABLE_INDEX = "CREATE INDEX " + TABLE_NAME + "_" + TRACKID + "_index ON " + TABLE_NAME + "(" + TRACKID + ")"; } diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/CachedTrackPointsIndexes.java b/src/main/java/de/dennisguse/opentracks/content/provider/CachedTrackPointsIndexes.java index 73d6bff13..d63808c10 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/CachedTrackPointsIndexes.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/CachedTrackPointsIndexes.java @@ -19,6 +19,7 @@ class CachedTrackPointsIndexes { final int sensorHeartRateIndex; final int sensorCadenceIndex; final int sensorPowerIndex; + final int elevationGainIndex; CachedTrackPointsIndexes(Cursor cursor) { idIndex = cursor.getColumnIndex(TrackPointsColumns._ID); @@ -32,5 +33,6 @@ class CachedTrackPointsIndexes { sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE); sensorCadenceIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_CADENCE); sensorPowerIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_POWER); + elevationGainIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ELEVATION_GAIN); } } diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java index d0dc07e2f..0721254ae 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java @@ -648,6 +648,10 @@ public class ContentProviderUtils { trackPoint.setPower(cursor.getFloat(indexes.sensorPowerIndex)); } + if (!cursor.isNull(indexes.elevationGainIndex)) { + trackPoint.setElevationGain(cursor.getFloat(indexes.elevationGainIndex)); + } + return trackPoint; } @@ -836,6 +840,11 @@ public class ContentProviderUtils { if (trackPoint.hasPower()) { values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower()); } + + if (trackPoint.hasElevationGain()) { + values.put(TrackPointsColumns.ELEVATION_GAIN, trackPoint.getElevationGain()); + } + return values; } diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/CustomSQLiteOpenHelper.java b/src/main/java/de/dennisguse/opentracks/content/provider/CustomSQLiteOpenHelper.java index 3d434132e..b5313da8b 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/CustomSQLiteOpenHelper.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/CustomSQLiteOpenHelper.java @@ -25,7 +25,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper { private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName(); - private static final int DATABASE_VERSION = 26; + private static final int DATABASE_VERSION = 27; @VisibleForTesting public static final String DATABASE_NAME = "database.db"; @@ -70,6 +70,10 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper { case 26: upgradeFrom25to26(db); break; + case 27: + upgradeFrom26to27(db); + break; + default: throw new RuntimeException("Not implemented: upgrade to " + toVersion); @@ -91,6 +95,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper { case 25: downgradeFrom26to25(db); break; + case 26: + downgradeFrom27to26(db); + break; default: throw new RuntimeException("Not implemented: downgrade to " + toVersion); @@ -190,4 +197,27 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper { db.setTransactionSuccessful(); db.endTransaction(); } + + private void upgradeFrom26to27(SQLiteDatabase db) { + db.beginTransaction(); + + db.execSQL("ALTER TABLE trackpoints ADD COLUMN elevation_gain FLOAT"); + + db.setTransactionSuccessful(); + db.endTransaction(); + } + + private void downgradeFrom27to26(SQLiteDatabase db) { + db.beginTransaction(); + + db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old"); + db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT)"); + db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power FROM trackpoints_old"); + db.execSQL("DROP TABLE trackpoints_old"); + + db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)"); + + db.setTransactionSuccessful(); + db.endTransaction(); + } } diff --git a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java index eda807d19..cbaa683b8 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java @@ -29,6 +29,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.Group; import androidx.fragment.app.Fragment; import de.dennisguse.opentracks.R; @@ -87,6 +88,10 @@ public class StatisticsRecordedFragment extends Fragment { private TextView speedMovingValue; private TextView speedMovingUnit; + private Group elevationGroup; + private TextView elevationTotalGainValue; + private TextView elevationTotalGainUnit; + public static StatisticsRecordedFragment newInstance(Track.Id trackId) { Bundle bundle = new Bundle(); bundle.putParcelable(TRACK_ID_KEY, trackId); @@ -130,6 +135,10 @@ public class StatisticsRecordedFragment extends Fragment { speedMovingLabel = view.findViewById(R.id.stats_moving_speed_label); speedMovingValue = view.findViewById(R.id.stats_moving_speed_value); speedMovingUnit = view.findViewById(R.id.stats_moving_speed_unit); + + elevationGroup = view.findViewById(R.id.stats_elevation_group); + elevationTotalGainValue = view.findViewById(R.id.stats_elevation_gain_value); + elevationTotalGainUnit = view.findViewById(R.id.stats_elevation_gain_unit); } @Override @@ -187,6 +196,10 @@ public class StatisticsRecordedFragment extends Fragment { speedMovingLabel = null; speedMovingValue = null; speedMovingUnit = null; + + elevationGroup = null; + elevationTotalGainValue = null; + elevationTotalGainUnit = null; } public void loadStatistics() { @@ -264,5 +277,18 @@ public class StatisticsRecordedFragment extends Fragment { speedMovingValue.setText(parts.first); speedMovingUnit.setText(parts.second); } + + // Set elevation gain + { + // Make elevation visible? + boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext()); + elevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE); + + Float elevationGain_m = trackStatistics != null ? trackStatistics.getTotalElevationGain() : null; + + Pair parts = StringUtils.formatElevation(getContext(), elevationGain_m, metricUnits); + elevationTotalGainValue.setText(parts.first); + elevationTotalGainUnit.setText(parts.second); + } } } diff --git a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java index 45337dee3..b297f8d6d 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordingFragment.java @@ -132,12 +132,14 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi private TextView speedMovingLabel; private TextView speedMovingValue; private TextView speedMovingUnit; - private Group elevationGroup; private TextView speedLabel; private TextView speedValue; private TextView speedUnit; - private TextView elevationValue; - private TextView elevationUnit; + private Group elevationGroup; + private TextView elevationCurrentValue; + private TextView elevationCurrentUnit; + private TextView elevationTotalGainValue; + private TextView elevationTotalGainUnit; private Group coordinateGroup; private TextView latitudeValue; private TextView longitudeValue; @@ -178,14 +180,16 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi speedMovingValue = view.findViewById(R.id.stats_moving_speed_value); speedMovingUnit = view.findViewById(R.id.stats_moving_speed_unit); - elevationGroup = view.findViewById(R.id.stats_elevation_current_group); - speedLabel = view.findViewById(R.id.stats_speed_label); speedValue = view.findViewById(R.id.stats_speed_value); speedUnit = view.findViewById(R.id.stats_speed_unit); - elevationValue = view.findViewById(R.id.stats_elevation_current_value); - elevationUnit = view.findViewById(R.id.stats_elevation_current_unit); + elevationGroup = view.findViewById(R.id.stats_elevation_group); + elevationCurrentValue = view.findViewById(R.id.stats_elevation_current_value); + elevationCurrentUnit = view.findViewById(R.id.stats_elevation_current_unit); + + elevationTotalGainValue = view.findViewById(R.id.stats_elevation_gain_value); + elevationTotalGainUnit = view.findViewById(R.id.stats_elevation_gain_unit); coordinateGroup = view.findViewById(R.id.stats_coordinate_group); @@ -276,14 +280,16 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi speedMovingValue = null; speedMovingUnit = null; - elevationGroup = null; - speedLabel = null; speedValue = null; speedUnit = null; - elevationValue = null; - elevationUnit = null; + elevationGroup = null; + elevationCurrentValue = null; + elevationCurrentUnit = null; + + elevationTotalGainValue = null; + elevationTotalGainUnit = null; coordinateGroup = null; @@ -406,20 +412,25 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi /** * Tries to fetch most recent {@link SensorDataSet} from {@link de.dennisguse.opentracks.services.TrackRecordingService}. + * Also sets elevation gain. */ private void updateSensorDataUI() { TrackRecordingServiceInterface trackRecordingService = trackRecordingServiceConnection.getServiceIfBound(); SensorDataSet sensorDataSet = null; + Float elevationGain_m = null; if (trackRecordingService == null) { Log.d(TAG, "Cannot get the track recording service."); } else { sensorDataSet = trackRecordingService.getSensorData(); + elevationGain_m = trackRecordingService.getElevationGain_m(); } setHeartRateSensorData(sensorDataSet); setCadenceSensorData(sensorDataSet); setSpeedSensorData(sensorDataSet, isSelectedTrackRecording()); + + setTotalElevationGain(elevationGain_m); } private void setHeartRateSensorData(SensorDataSet sensorDataSet) { @@ -468,6 +479,26 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi cadenceValueView.setText(sensorValue); } + // Set elevation gain + private void setTotalElevationGain(Float elevationGain_m) { + //TODO Check if we can distribute the total elevation gain via trackStatistics instead of doing some computation in the UI layer. + boolean metricUnits = PreferencesUtils.isMetricUnits(getContext()); + + Float totalElevationGain = elevationGain_m; + + if (lastTrackStatistics != null && lastTrackStatistics.hasTotalElevationGain()) { + if (elevationGain_m == null) { + totalElevationGain = lastTrackStatistics.getTotalElevationGain(); + } else { + totalElevationGain += lastTrackStatistics.getTotalElevationGain(); + } + } + + Pair parts = StringUtils.formatElevation(getContext(), totalElevationGain, metricUnits); + elevationTotalGainValue.setText(parts.first); + elevationTotalGainUnit.setText(parts.second); + } + /** * If cadence and hear rate groups are invisible then sensor horizontal line hast to be invisible too. */ @@ -475,7 +506,6 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi if (cadenceGroup.getVisibility() != View.VISIBLE && heartRateGroup.getVisibility() != View.VISIBLE) { sensorHorizontalLine.setVisibility(View.GONE); } - } private void setSpeedSensorData(SensorDataSet sensorDataSet, boolean isRecording) { @@ -555,8 +585,9 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi speedMovingUnit.setText(parts.second); } - // Make elevation visible? + // Set elevation (gain) { + // Make elevation visible? boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext()); elevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE); } @@ -597,11 +628,11 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi elevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE); if (showElevation) { - double altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? lastTrackPoint.getAltitude() : Double.NaN; + // Current elevation + Float altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? (float) lastTrackPoint.getAltitude() : null; Pair parts = StringUtils.formatElevation(getContext(), altitude, metricUnits); - - elevationValue.setText(parts.first); - elevationUnit.setText(parts.second); + elevationCurrentValue.setText(parts.first); + elevationCurrentUnit.setText(parts.second); } // Set coordinate diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java index 3311e32c8..4e34c54d3 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java @@ -50,6 +50,7 @@ public class KmlTrackWriter implements TrackWriter { 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"; + public static final String EXTENDED_DATA_TYPE_ELEVATION_GAIN = "elevation_gain"; private static final String WAYPOINT_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png"; private static final String START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png"; @@ -68,6 +69,7 @@ public class KmlTrackWriter implements TrackWriter { private final List powerList = new ArrayList<>(); private final List cadenceList = new ArrayList<>(); private final List heartRateList = new ArrayList<>(); + private final List elevationGainList = new ArrayList<>(); private TrackPoint startTrackPoint; @@ -257,6 +259,9 @@ public class KmlTrackWriter implements TrackWriter { if (heartRateList.size() > 0) { writeSimpleArrayData(heartRateList, EXTENDED_DATA_TYPE_HEART_RATE); } + if (elevationGainList.size() > 0) { + writeSimpleArrayData(elevationGainList, EXTENDED_DATA_TYPE_ELEVATION_GAIN); + } } printWriter.println(""); printWriter.println(""); @@ -287,6 +292,9 @@ public class KmlTrackWriter implements TrackWriter { if (trackPoint.hasPower()) { powerList.add(trackPoint.getPower()); } + if (trackPoint.hasElevationGain()) { + elevationGainList.add(trackPoint.getElevationGain()); + } } } } diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java index 83bb2fa41..dbea4f76c 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmlFileTrackImporter.java @@ -63,11 +63,12 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { private boolean trackStarted = false; private String extendedDataType; - private ArrayList trackPoints; - private ArrayList speedList; - private ArrayList cadenceList; - private ArrayList heartRateList; - private ArrayList powerList; + private ArrayList trackPoints = new ArrayList<>(); + private ArrayList speedList = new ArrayList<>(); + private ArrayList cadenceList = new ArrayList<>(); + private ArrayList heartRateList = new ArrayList<>(); + private ArrayList powerList = new ArrayList<>(); + private ArrayList elevationGainList = new ArrayList<>(); public KmlFileTrackImporter(Context context) { this(context, new ContentProviderUtils(context)); @@ -223,11 +224,12 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { @Override protected void onTrackSegmentStart() { super.onTrackSegmentStart(); - trackPoints = new ArrayList<>(); - speedList = new ArrayList<>(); - heartRateList = new ArrayList<>(); - cadenceList = new ArrayList<>(); - powerList = new ArrayList<>(); + trackPoints.clear(); + speedList.clear(); + heartRateList.clear(); + cadenceList.clear(); + powerList.clear(); + elevationGainList.clear(); } /** @@ -250,6 +252,9 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { if (i < powerList.size()) { trackPoint.setPower(powerList.get(i)); } + if (i < elevationGainList.size()) { + trackPoint.setElevationGain(elevationGainList.get(i)); + } insertTrackPoint(trackPoint); } @@ -316,6 +321,9 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter { case KmlTrackWriter.EXTENDED_DATA_TYPE_CADENCE: cadenceList.add(value); break; + case KmlTrackWriter.EXTENDED_DATA_TYPE_ELEVATION_GAIN: + elevationGainList.add(value); + break; default: Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported."); } diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java index 3fcb4d10d..fc2a9a035 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java @@ -49,6 +49,7 @@ import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.services.handlers.GpsStatusValue; import de.dennisguse.opentracks.services.handlers.HandlerServer; import de.dennisguse.opentracks.services.sensors.BluetoothRemoteSensorManager; +import de.dennisguse.opentracks.services.sensors.ElevationSumManager; import de.dennisguse.opentracks.services.tasks.AnnouncementPeriodicTaskFactory; import de.dennisguse.opentracks.services.tasks.PeriodicTaskExecutor; import de.dennisguse.opentracks.stats.TrackStatistics; @@ -115,6 +116,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand // The following variables are set when recording: private WakeLock wakeLock; private BluetoothRemoteSensorManager remoteSensorManager; + private ElevationSumManager elevationSumManager; private TrackStatisticsUpdater trackStatisticsUpdater; private TrackPoint lastTrackPoint; @@ -174,6 +176,11 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand remoteSensorManager = null; } + if (elevationSumManager != null) { + elevationSumManager.stop(this); + elevationSumManager = null; + } + // Reverse order from onCreate showNotification(false); //TODO Why? @@ -371,6 +378,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand // Update instance variables remoteSensorManager = new BluetoothRemoteSensorManager(this); remoteSensorManager.start(); + + elevationSumManager = new ElevationSumManager(); + elevationSumManager.start(this); + lastTrackPoint = null; isIdle = false; @@ -456,6 +467,11 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand remoteSensorManager.stop(); remoteSensorManager = null; } + if (elevationSumManager != null) { + elevationSumManager.stop(this); + elevationSumManager = null; + } + lastTrackPoint = null; handlerServer.stop(this); @@ -556,15 +572,17 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand insertTrackPoint(track, TrackPoint.createPause()); insertTrackPoint(track, trackPoint); - isIdle = false; + isIdle = false; lastTrackPoint = trackPoint; return; } if (trackPoint.hasSensorData() || distanceToLastTrackLocation >= recordingDistanceInterval) { insertTrackPointIfNewer(track, lastTrackPoint); + insertTrackPoint(track, trackPoint); + isIdle = false; lastTrackPoint = trackPoint; @@ -573,7 +591,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand if (!isIdle && !TrackPointUtils.isMoving(trackPoint)) { insertTrackPointIfNewer(track, lastTrackPoint); + insertTrackPoint(track, trackPoint); + isIdle = true; lastTrackPoint = trackPoint; @@ -582,7 +602,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand if (isIdle && TrackPointUtils.isMoving(trackPoint)) { insertTrackPointIfNewer(track, lastTrackPoint); + insertTrackPoint(track, trackPoint); + isIdle = false; lastTrackPoint = trackPoint; @@ -632,6 +654,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand */ private void insertTrackPoint(@NonNull Track track, @NonNull TrackPoint trackPoint) { try { + if (elevationSumManager != null) { + trackPoint.setElevationGain(elevationSumManager.getElevationGain_m()); + elevationSumManager.reset(); + } contentProviderUtils.insertTrackPoint(trackPoint, track.getId()); trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval); updateTrackTotalTime(track); @@ -672,6 +698,17 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand } } + /** + * Returns the relative elevation gain (since last trackpoint). + */ + Float getElevationGain_m() { + if (elevationSumManager == null || !elevationSumManager.isConnected()) { + return null; + } + + return elevationSumManager.getElevationGain_m(); + } + private void showNotification(boolean isGpsStarted) { if (isRecording()) { Intent intent = IntentUtils.newIntent(this, TrackRecordingActivity.class) diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceBinder.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceBinder.java index db1c253fe..12a92ca0a 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceBinder.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceBinder.java @@ -99,6 +99,11 @@ class TrackRecordingServiceBinder extends android.os.Binder implements TrackReco return trackRecordingService.getSensorDataSet(); } + @Override + public Float getElevationGain_m() { + return trackRecordingService.getElevationGain_m(); + } + @VisibleForTesting @Override public void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager) { diff --git a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceInterface.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceInterface.java index 61a557312..b56a704f3 100644 --- a/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceInterface.java +++ b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingServiceInterface.java @@ -65,12 +65,19 @@ public interface TrackRecordingServiceInterface { Marker.Id insertMarker(String name, String category, String description, String photoUrl); /** - * Gets the current sensor data. Returns null if there is no data. + * Gets the current sensor data. * - * @return SensorDataSet object. + * @return SensorDataSet object or null. */ SensorDataSet getSensorData(); + /** + * Gets the current elevation gain. + * + * @return elevation gain in m or null. + */ + Float getElevationGain_m(); + @VisibleForTesting void setRemoteSensorManager(BluetoothRemoteSensorManager remoteSensorManager); diff --git a/src/main/java/de/dennisguse/opentracks/services/sensors/ElevationSumManager.java b/src/main/java/de/dennisguse/opentracks/services/sensors/ElevationSumManager.java new file mode 100644 index 000000000..f8ab382b9 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/services/sensors/ElevationSumManager.java @@ -0,0 +1,117 @@ +package de.dennisguse.opentracks.services.sensors; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import de.dennisguse.opentracks.util.PressureSensorUtils; +import de.dennisguse.opentracks.util.UnitConversions; + +/** + * Estimates the elevation gain and elevation loss using the device's pressure sensor (i.e., barometer). + */ +public class ElevationSumManager implements SensorEventListener { + + private static final String TAG = ElevationSumManager.class.getSimpleName(); + + private static final int SAMPLING_RATE = 3 * (int) UnitConversions.ONE_SECOND_US; + + private boolean isConnected = false; + + private float lastAcceptedPressureValue_hPa; + + private float lastSeenSensorValue_hPa; + + private float elevationGain_m; + private float elevationLoss_m; + + public void start(Context context) { + SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + + Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE); + if (pressureSensor == null) { + Log.w(TAG, "No pressure sensor available."); + isConnected = false; + } else { + isConnected = sensorManager.registerListener(this, pressureSensor, SAMPLING_RATE); + } + + lastAcceptedPressureValue_hPa = Float.NaN; + reset(); + } + + public void stop(Context context) { + Log.d(TAG, "Stop"); + + SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + sensorManager.unregisterListener(this); + + isConnected = false; + reset(); + } + + public boolean isConnected() { + return isConnected; + } + + @VisibleForTesting + public void setConnected(boolean isConnected) { + this.isConnected = isConnected; + } + + public @Nullable + Float getElevationGain_m() { + return isConnected ? elevationGain_m : null; + } + + public @Nullable + Float getElevationLoss_m() { + return isConnected ? elevationLoss_m : null; + } + + public void reset() { + Log.d(TAG, "Reset"); + elevationGain_m = 0; + elevationLoss_m = 0; + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + Log.w(TAG, "Sensor accuracy changes are (currently) ignored."); + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (!isConnected) { + Log.w(TAG, "Not connected to sensor, cannot process data."); + return; + } + onSensorValueChanged(event.values[0]); + } + + @VisibleForTesting + void onSensorValueChanged(float value_hPa) { + if (Float.isNaN(lastAcceptedPressureValue_hPa)) { + lastAcceptedPressureValue_hPa = value_hPa; + lastSeenSensorValue_hPa = value_hPa; + return; + } + + PressureSensorUtils.ElevationChange elevationChange = PressureSensorUtils.computeChangesWithSmoothing_m(lastAcceptedPressureValue_hPa, lastSeenSensorValue_hPa, value_hPa); + if (elevationChange != null) { + elevationGain_m += elevationChange.getElevationGain_m(); + elevationLoss_m += elevationChange.getElevationLoss_m(); + lastAcceptedPressureValue_hPa = elevationChange.getCurrentSensorValue_hPa(); + } + + lastSeenSensorValue_hPa = value_hPa; + + Log.v(TAG, "elevation gain: " + elevationGain_m); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/stats/DoubleBuffer.java b/src/main/java/de/dennisguse/opentracks/stats/DoubleRingBuffer.java similarity index 96% rename from src/main/java/de/dennisguse/opentracks/stats/DoubleBuffer.java rename to src/main/java/de/dennisguse/opentracks/stats/DoubleRingBuffer.java index 0ec4360c9..bc2167cfb 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/DoubleBuffer.java +++ b/src/main/java/de/dennisguse/opentracks/stats/DoubleRingBuffer.java @@ -19,13 +19,13 @@ package de.dennisguse.opentracks.stats; import androidx.annotation.NonNull; /** - * This class maintains a buffer of doubles. + * This class maintains a ring buffer of doubles. * This buffer is a convenient class for storing a series of doubles and calculating information about them. * This is a FIFO buffer. * * @author Sandor Dornbush */ -class DoubleBuffer { +class DoubleRingBuffer { // The sliding buffer of doubles. private final double[] buffer; @@ -41,7 +41,7 @@ class DoubleBuffer { * * @param size the size */ - DoubleBuffer(int size) { + DoubleRingBuffer(int size) { if (size < 1) { throw new IllegalArgumentException("The buffer size must be greater than 1."); } diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java index fa4fb271d..42bd19f7b 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java @@ -17,6 +17,7 @@ package de.dennisguse.opentracks.stats; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; /** * Statistical data about a {@link de.dennisguse.opentracks.content.data.Track}. @@ -24,6 +25,7 @@ import androidx.annotation.NonNull; * * @author Rodrigo Damazio */ +//TODO Use null instead of Double.isInfinite public class TrackStatistics { // The min and max elevation (meters) seen on this track. @@ -42,7 +44,7 @@ public class TrackStatistics { // The maximum speed (meters/second) that we believe is valid. private double maxSpeed_mps; // The total elevation gained (meters). - private double totalElevationGain_m; + private Float totalElevationGain_m = null; public TrackStatistics() { } @@ -80,7 +82,15 @@ public class TrackStatistics { elevationExtremities.update(other.elevationExtremities.getMin()); elevationExtremities.update(other.elevationExtremities.getMax()); } - totalElevationGain_m += other.totalElevationGain_m; + if (totalElevationGain_m == null) { + if (other.totalElevationGain_m != null) { + totalElevationGain_m = other.totalElevationGain_m; + } + } else { + if (other.totalElevationGain_m != null) { + totalElevationGain_m += other.totalElevationGain_m; + } + } } /** @@ -227,18 +237,23 @@ public class TrackStatistics { elevationExtremities.update(elevation_m); } - /** - * Gets the total elevation gain in meters. This is calculated as the sum of all positive differences in the smoothed elevation. - */ - public double getTotalElevationGain() { + public boolean hasTotalElevationGain() { + return totalElevationGain_m != null; + } + + public @Nullable + Float getTotalElevationGain() { return totalElevationGain_m; } - public void setTotalElevationGain(double totalElevationGain_m) { + public void setTotalElevationGain(float totalElevationGain_m) { this.totalElevationGain_m = totalElevationGain_m; } - public void addTotalElevationGain(double gain_m) { + public void addTotalElevationGain(float gain_m) { + if (totalElevationGain_m == null) { + totalElevationGain_m = 0f; + } totalElevationGain_m += gain_m; } diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java index 530e69161..300833a50 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java @@ -62,9 +62,9 @@ public class TrackStatisticsUpdater { private final TrackStatistics trackStatistics; // A buffer of the recent elevation readings (m) - private final DoubleBuffer elevationBuffer_m = new DoubleBuffer(ELEVATION_SMOOTHING_FACTOR); + private final DoubleRingBuffer elevationBuffer_m = new DoubleRingBuffer(ELEVATION_SMOOTHING_FACTOR); // A buffer of the recent speed readings (m/s) for calculating max speed - private final DoubleBuffer speedBuffer_ms = new DoubleBuffer(SPEED_SMOOTHING_FACTOR); + private final DoubleRingBuffer speedBuffer_ms = new DoubleRingBuffer(SPEED_SMOOTHING_FACTOR); // The current segment's statistics private TrackStatistics currentSegment; @@ -112,7 +112,7 @@ public class TrackStatisticsUpdater { * Adds a trackPoint. * TODO: This assume trackPoint has a valid time. * - * @param trackPoint the trackPoint + * @param trackPoint the trackPoint * @param minRecordingDistance the min recording distance */ public void addTrackPoint(TrackPoint trackPoint, int minRecordingDistance) { @@ -134,8 +134,16 @@ public class TrackStatisticsUpdater { return; } - //TODO Use Barometer to compute elevation gain. - double elevationDifference = trackPoint.hasAltitude() ? updateElevation(trackPoint.getAltitude()) : 0.0; + //Update absolute (GPS-based) elevation + if (trackPoint.hasAltitude()) { + updateAbsoluteElevation(trackPoint.getAltitude()); + } + + //Get elevation gain + if (trackPoint.hasElevationGain()) { + currentSegment.addTotalElevationGain(trackPoint.getElevationGain()); + Log.d(TAG, "elevation gain: " + trackPoint.getElevationGain()); + } if (lastTrackPoint == null || lastMovingTrackPoint == null) { lastTrackPoint = trackPoint; @@ -207,23 +215,21 @@ public class TrackStatisticsUpdater { } /** - * Updates an elevation reading. Returns the difference. + * Updates an elevation reading. * * @param elevation the elevation + * @return the difference */ @VisibleForTesting - private double updateElevation(double elevation) { + private double 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); - double difference = newAverage - oldAverage; - if (difference > 0) { - currentSegment.addTotalElevationGain(difference); - } - return difference; + + return newAverage - oldAverage; } private TrackStatistics init(long time) { diff --git a/src/main/java/de/dennisguse/opentracks/util/PressureSensorUtils.java b/src/main/java/de/dennisguse/opentracks/util/PressureSensorUtils.java new file mode 100644 index 000000000..11aed2675 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/util/PressureSensorUtils.java @@ -0,0 +1,83 @@ +package de.dennisguse.opentracks.util; + +import android.hardware.SensorManager; + +public class PressureSensorUtils { + + //Everything above is considered a meaningful change in elevation. + private static float ELEVATION_CHANGE_DIFF_M = 3.0f; + + private static float EXPONENTIAL_SMOOTHING = 0.3f; + + private PressureSensorUtils() { + } + + public static class ElevationChange { + + private float currentSensorValue_hPa; + + private float elevationChange_m; + + public ElevationChange(float currentSensorValue_hPa, float elevationChange_m) { + this.currentSensorValue_hPa = currentSensorValue_hPa; + this.elevationChange_m = elevationChange_m; + } + + public float getCurrentSensorValue_hPa() { + return currentSensorValue_hPa; + } + + public float getElevationChange_m() { + return elevationChange_m; + } + + public float getElevationGain_m() { + return elevationChange_m > 0 ? elevationChange_m : 0; + } + + public float getElevationLoss_m() { + return elevationChange_m < 0 ? elevationChange_m : 0; + } + } + + /** + * Applies exponential smoothing to sensor value before computation. + */ + public static ElevationChange computeChangesWithSmoothing_m(float lastAcceptedSensorValue_hPa, float lastSeenSensorValue_hPa, float currentSensorValue_hPa) { + float nextSensorValue_hPa = EXPONENTIAL_SMOOTHING * currentSensorValue_hPa + (1 - EXPONENTIAL_SMOOTHING) * lastSeenSensorValue_hPa; + + return computeChanges_m(lastAcceptedSensorValue_hPa, nextSensorValue_hPa); + } + + /** + * Computes the elevation gain and elevation loss. + * + * @return null if no meaningful elevation change occurred. + */ + public static ElevationChange computeChanges_m(float lastAcceptedSensorValue_hPa, float currentSensorValue_hPa) { + float lastSensorValue_m = SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, lastAcceptedSensorValue_hPa); + float currentSensorValue_m = SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, currentSensorValue_hPa); + + float elevationChange_m = currentSensorValue_m - lastSensorValue_m; + if (Math.abs(elevationChange_m) < ELEVATION_CHANGE_DIFF_M) { + return null; + } + + // Limit elevation change by ELEVATION_CHANGE_DIFF and computes pressure value accordingly. + ElevationChange elevationChange = new ElevationChange(currentSensorValue_hPa, elevationChange_m); + if (elevationChange.getElevationGain_m() > 0) { + return new ElevationChange(getBarometricPressure(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, lastSensorValue_m + ELEVATION_CHANGE_DIFF_M), ELEVATION_CHANGE_DIFF_M); + } else { + return new ElevationChange(getBarometricPressure(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, lastSensorValue_m - ELEVATION_CHANGE_DIFF_M), -1 * ELEVATION_CHANGE_DIFF_M); + } + } + + /* + * Barometeric pressure to elevation estimation; inverts of SensorManager.getAltitude(float, float) + * https://de.wikipedia.org/wiki/Barometrische_H%C3%B6henformel#Internationale_H%C3%B6henformel + * {\color{White} p(h)} = p_0 \cdot \left( 1 - \frac{0{,}0065 \frac{\mathrm K}{\mathrm m} \cdot h}{T_0\ } \right)^{5{,}255} + */ + private static float getBarometricPressure(float p0, float altitude_m) { + return (float) (p0 * Math.pow(1.0 - 0.0065 * altitude_m / 288.15, 5.255f)); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/util/StringUtils.java b/src/main/java/de/dennisguse/opentracks/util/StringUtils.java index 133380315..2a7d9db34 100644 --- a/src/main/java/de/dennisguse/opentracks/util/StringUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/StringUtils.java @@ -323,21 +323,21 @@ public class StringUtils { } /** - * Sets an elevation value. + * Sets an elevation_m value. * * @param context the context - * @param elevation the elevation in meters + * @param elevation_m the elevation_m * @param metricUnits true if metric units - * @return the formatted elevation (or null) and it's unit as {@link Pair} + * @return the formatted elevation_m (or null) and it's unit as {@link Pair} */ - public static Pair formatElevation(Context context, double elevation, boolean metricUnits) { + public static Pair formatElevation(Context context, Float elevation_m, boolean metricUnits) { String value = context.getString(R.string.value_unknown); String unit = context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet); - if (!Double.isNaN(elevation) && !Double.isInfinite(elevation)) { + if (elevation_m != null) { if (!metricUnits) { - elevation *= UnitConversions.M_TO_FT; + elevation_m *= (float) UnitConversions.M_TO_FT; } - value = StringUtils.formatDecimal(elevation, 0); + value = StringUtils.formatDecimal(elevation_m, 0); } return new Pair<>(value, unit); } diff --git a/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java b/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java index 9b53bfde3..392b24f7b 100644 --- a/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java +++ b/src/main/java/de/dennisguse/opentracks/util/UnitConversions.java @@ -26,7 +26,8 @@ public class UnitConversions { public static final long S_TO_MS = 1000; // Time - // 1 second in milliseconds + // TODO Use int instead of long. + public static final long ONE_SECOND_US = UnitConversions.S_TO_MS * 1000; public static final long ONE_SECOND_MS = UnitConversions.S_TO_MS; public static final long ONE_MINUTE_MS = (long) (UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS); public static final long ONE_HOUR_MS = 60 * ONE_MINUTE_MS; diff --git a/src/main/res/layout/statistics_recorded.xml b/src/main/res/layout/statistics_recorded.xml index c580b7bb8..5aec7f478 100644 --- a/src/main/res/layout/statistics_recorded.xml +++ b/src/main/res/layout/statistics_recorded.xml @@ -116,6 +116,14 @@ app:layout_constraintTop_toBottomOf="@+id/stats_moving_time_label" tools:text="00:00:00" /> + + + + app:layout_constraintTop_toBottomOf="@+id/stats_time_barrier" /> + + + + app:layout_constraintTop_toBottomOf="@+id/stats_speed_barrier" /> - + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@+id/stats_elevation_barrier" /> \ No newline at end of file diff --git a/src/main/res/layout/statistics_recording.xml b/src/main/res/layout/statistics_recording.xml index aabd699cd..ebc79b33e 100644 --- a/src/main/res/layout/statistics_recording.xml +++ b/src/main/res/layout/statistics_recording.xml @@ -116,6 +116,14 @@ app:layout_constraintTop_toBottomOf="@+id/stats_moving_time_label" tools:text="00:00:00" /> + + + + app:layout_constraintTop_toBottomOf="@+id/stats_time_barrier" /> + + + + app:layout_constraintTop_toBottomOf="@+id/stats_speed_barrier" /> - + + app:constraint_referenced_ids="stats_elevation_horizontal_line,stats_elevation_current_label,stats_elevation_current_value,stats_elevation_current_unit,stats_elevation_gain_label,stats_elevation_gain_value,stats_elevation_gain_unit" /> + @@ -277,6 +294,34 @@ style="@style/StatsUnit" app:layout_constraintBottom_toBottomOf="@+id/stats_elevation_current_value" app:layout_constraintLeft_toRightOf="@+id/stats_elevation_current_value" + app:layout_constraintRight_toLeftOf="@+id/guideline2" + tools:text="m" /> + + + + + + + @@ -287,6 +332,14 @@ android:layout_height="wrap_content" app:constraint_referenced_ids="stats_latitude_label,stats_latitude_value,stats_longitude_label,stats_longitude_value,stats_coordinate_horizontal_line" /> + + + + app:layout_constraintTop_toBottomOf="@+id/stats_elevation_barrier" /> + + + + app:layout_constraintTop_toBottomOf="@+id/stats_location_barrier" /> false statsShowGradeElevation - false + true trackdetail_show_on_lockscreen_while_recording false