From 8a14bc46f614cff4feb67824dc6c10143b23e4bb Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Mon, 30 Mar 2020 20:03:04 +0200 Subject: [PATCH] Implement elevation gain via barometer. --- .../sensors/ElevationSumManagerTest.java | 34 ++++++ .../util/PressureSensorUtilsTest.java | 51 +++++++++ .../opentracks/content/data/TrackPoint.java | 13 +++ .../fragments/StatisticsRecordedFragment.java | 21 ++++ .../StatisticsRecordingFragment.java | 74 +++++++++---- .../services/TrackRecordingService.java | 43 +++++++- .../services/TrackRecordingServiceBinder.java | 5 + .../TrackRecordingServiceInterface.java | 11 +- .../services/sensors/ElevationSumManager.java | 102 ++++++++++++++++++ .../opentracks/stats/TrackStatistics.java | 2 +- .../stats/TrackStatisticsUpdater.java | 26 +++-- .../opentracks/util/PressureSensorUtils.java | 49 +++++++++ .../opentracks/util/UnitConversions.java | 3 +- src/main/res/layout/statistics_recorded.xml | 42 ++++++++ src/main/res/layout/statistics_recording.xml | 40 ++++++- 15 files changed, 480 insertions(+), 36 deletions(-) create mode 100644 src/androidTest/java/de/dennisguse/opentracks/services/sensors/ElevationSumManagerTest.java create mode 100644 src/androidTest/java/de/dennisguse/opentracks/util/PressureSensorUtilsTest.java create mode 100644 src/main/java/de/dennisguse/opentracks/services/sensors/ElevationSumManager.java create mode 100644 src/main/java/de/dennisguse/opentracks/util/PressureSensorUtils.java 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..7259a04ec --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/services/sensors/ElevationSumManagerTest.java @@ -0,0 +1,34 @@ +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() { + // 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(-1.08, elevationSumManager.getElevationLoss_m(), 0.01); + } +} \ No newline at end of file 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..f0b61d341 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/util/PressureSensorUtilsTest.java @@ -0,0 +1,51 @@ +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)); + } + } + + // 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, 1015.14f, 1015.15f}; + 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) { + if (elevationChange.getElevationChange_m() > 0) { + elevationGain_m += elevationChange.getElevationChange_m(); + } else { + elevationLoss_m += elevationChange.getElevationChange_m(); + } + lastUsedPressureValue_hPa = elevationChange.getCurrentSensorValue_hPa(); + } + } + + // then + assertEquals(0f, elevationGain_m, 0.01); + assertEquals(-1.08f, elevationLoss_m, 0.01); + assertEquals(1015.13, lastUsedPressureValue_hPa, 0.01); + } +} \ No newline at end of file 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..70fddb0a8 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 = Float.NaN; public TrackPoint() { this.location = new Location(""); @@ -92,6 +93,18 @@ public class TrackPoint { return location; } + public boolean hasElevationGain() { + return !Float.isNaN(elevationGain); + } + + 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/fragments/StatisticsRecordedFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java index eda807d19..adc5fb0ac 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/StatisticsRecordedFragment.java @@ -87,6 +87,10 @@ public class StatisticsRecordedFragment extends Fragment { private TextView speedMovingValue; private TextView speedMovingUnit; + private TextView elevationCurrentGroup; + 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 +134,11 @@ 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); + + elevationCurrentGroup = view.findViewById(R.id.stats_elevation_current_group); + + elevationTotalGainValue = view.findViewById(R.id.stats_elevation_gain_value); + elevationTotalGainUnit = view.findViewById(R.id.stats_elevation_gain_unit); } @Override @@ -187,6 +196,9 @@ public class StatisticsRecordedFragment extends Fragment { speedMovingLabel = null; speedMovingValue = null; speedMovingUnit = null; + + elevationTotalGainValue = null; + elevationTotalGainUnit = null; } public void loadStatistics() { @@ -264,5 +276,14 @@ public class StatisticsRecordedFragment extends Fragment { speedMovingValue.setText(parts.first); speedMovingUnit.setText(parts.second); } + + // Set elevation gain + { + double elevationGain_m = trackStatistics != null ? trackStatistics.getTotalElevationGain() : 0f; + + 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..c5cbe4828 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 elevationCurrentGroup; + 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); + elevationCurrentGroup = view.findViewById(R.id.stats_elevation_current_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; + elevationCurrentGroup = 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()); + + setElevationGainFromSensor(elevationGain_m); } private void setHeartRateSensorData(SensorDataSet sensorDataSet) { @@ -468,6 +479,33 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi cadenceValueView.setText(sensorValue); } + // Set elevation gain + public void setTotalElevationGain(Float elevationGain_m) { + boolean metricUnits = PreferencesUtils.isMetricUnits(getContext()); + + if (elevationGain_m == null) { + elevationGain_m = 0f; + } + if (lastTrackStatistics != null) { + elevationGain_m += (float) lastTrackStatistics.getTotalElevationGain(); + } + + Pair parts = StringUtils.formatElevation(getContext(), elevationGain_m, metricUnits); + elevationTotalGainValue.setText(parts.first); + elevationTotalGainUnit.setText(parts.second); + } + + public void setElevationGainFromSensor(Float elevationGain_m) { + float totalElevationGain = 0; + if (elevationGain_m != null) { + totalElevationGain = elevationGain_m; + } + if (lastTrackStatistics != null) { + totalElevationGain += (float) lastTrackStatistics.getTotalElevationGain(); + } + setTotalElevationGain(totalElevationGain); + } + /** * If cadence and hear rate groups are invisible then sensor horizontal line hast to be invisible too. */ @@ -475,7 +513,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,10 +592,11 @@ 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); + elevationCurrentGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE); } } @@ -594,14 +632,14 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi // Set elevation boolean showElevation = PreferencesUtils.isShowStatsElevation(getContext()); - elevationGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE); + elevationCurrentGroup.setVisibility(showElevation ? View.VISIBLE : View.GONE); if (showElevation) { + // Current elevation double altitude = lastTrackPoint != null && lastTrackPoint.hasAltitude() ? lastTrackPoint.getAltitude() : Double.NaN; 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/services/TrackRecordingService.java b/src/main/java/de/dennisguse/opentracks/services/TrackRecordingService.java index 3fcb4d10d..16fcef666 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); @@ -533,6 +549,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand //TODO Figure out how to avoid loading the lastValidTrackPoint from the database TrackPoint lastValidTrackPoint = getLastValidTrackPointInCurrentSegment(track.getId()); + if (elevationSumManager != null) { + trackPoint.setElevationGain(elevationSumManager.getElevationGain_m()); + } + //Storing trackPoint // Always insert the first segment location @@ -556,15 +576,19 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand insertTrackPoint(track, TrackPoint.createPause()); insertTrackPoint(track, trackPoint); - isIdle = false; + elevationSumManager.reset(); + isIdle = false; lastTrackPoint = trackPoint; return; } if (trackPoint.hasSensorData() || distanceToLastTrackLocation >= recordingDistanceInterval) { insertTrackPointIfNewer(track, lastTrackPoint); + insertTrackPoint(track, trackPoint); + elevationSumManager.reset(); + isIdle = false; lastTrackPoint = trackPoint; @@ -573,7 +597,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand if (!isIdle && !TrackPointUtils.isMoving(trackPoint)) { insertTrackPointIfNewer(track, lastTrackPoint); + insertTrackPoint(track, trackPoint); + elevationSumManager.reset(); + isIdle = true; lastTrackPoint = trackPoint; @@ -582,7 +609,10 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand if (isIdle && TrackPointUtils.isMoving(trackPoint)) { insertTrackPointIfNewer(track, lastTrackPoint); + insertTrackPoint(track, trackPoint); + elevationSumManager.reset(); + isIdle = false; lastTrackPoint = trackPoint; @@ -672,6 +702,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..d0e3b5cf7 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/services/sensors/ElevationSumManager.java @@ -0,0 +1,102 @@ +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.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 lastUsedPressureValue_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; + } + + isConnected = sensorManager.registerListener(this, pressureSensor, SAMPLING_RATE); + lastUsedPressureValue_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; + } + + public float getElevationGain_m() { + return elevationGain_m; + } + + public float getElevationLoss_m() { + return elevationLoss_m; + } + + 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) { + onSensorValueChanged(event.values[0]); + } + + @VisibleForTesting + void onSensorValueChanged(float value_hPa) { + if (Float.isNaN(lastUsedPressureValue_hPa)) { + lastUsedPressureValue_hPa = value_hPa; + return; + } + + PressureSensorUtils.ElevationChange elevationChange = PressureSensorUtils.computeChanges_m(lastUsedPressureValue_hPa, value_hPa); + if (elevationChange != null) { + if (elevationChange.getElevationChange_m() > 0) { + elevationGain_m += elevationChange.getElevationChange_m(); + } else { + elevationLoss_m += elevationChange.getElevationChange_m(); + } + lastUsedPressureValue_hPa = elevationChange.getCurrentSensorValue_hPa(); + } + + Log.v(TAG, "elevation gain: " + elevationGain_m); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java index fa4fb271d..b9c0674c0 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java @@ -228,7 +228,7 @@ public class TrackStatistics { } /** - * Gets the total elevation gain in meters. This is calculated as the sum of all positive differences in the smoothed elevation. + * Gets the total elevation gain in meters. */ public double getTotalElevationGain() { return totalElevationGain_m; diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java index dc7887619..300833a50 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java @@ -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..af11d2e7f --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/util/PressureSensorUtils.java @@ -0,0 +1,49 @@ +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 = 5.0f; + + 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; + } + } + + /** + * Computes the elevation gain and elevation loss. + * + * @return null if no meaningful elevation change occurred. + */ + public static ElevationChange computeChanges_m(float lastSensorValue_hPa, float currentSensorValue_hPa) { + float lastSensorValue_m = SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, lastSensorValue_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; + } + + return new ElevationChange(currentSensorValue_hPa, elevationChange_m); + } +} 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..b960a47d6 100644 --- a/src/main/res/layout/statistics_recorded.xml +++ b/src/main/res/layout/statistics_recorded.xml @@ -209,6 +209,48 @@ app:layout_constraintRight_toLeftOf="@+id/guideline3" tools:text="km/h" /> + + + + + + + + + + + @@ -277,6 +277,40 @@ 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" /> + + + + + + + + +