From 1075021e21281e022921a9abf4bbdd82b35b700a Mon Sep 17 00:00:00 2001 From: Jonas Kalderstam Date: Fri, 18 Feb 2022 23:55:36 +0100 Subject: [PATCH] Voice announcement: added heart rate info to voice announcements Also added setting to toggle announcement of heart rate Defaults to off to preserve current behavior --- .../opentracks/content/data/TestDataUtil.java | 2 +- .../VoiceAnnouncementUtilsTest.java | 88 +++++++++---------- .../opentracks/stats/TrackStatisticsTest.java | 5 ++ .../stats/TrackStatisticsUpdaterTest.java | 3 + .../announcement/VoiceAnnouncement.java | 7 +- .../announcement/VoiceAnnouncementUtils.java | 29 +++++- .../opentracks/settings/PreferencesUtils.java | 16 ++++ .../opentracks/stats/TrackStatistics.java | 32 +++++++ .../stats/TrackStatisticsUpdater.java | 22 +++++ .../ui/intervals/IntervalStatistics.java | 14 +++ src/main/res/values/settings.xml | 1 + src/main/res/values/strings.xml | 3 + src/main/res/xml/settings_announcements.xml | 5 ++ 13 files changed, 177 insertions(+), 50 deletions(-) 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 38100f62d..b252bbf79 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java +++ b/src/androidTest/java/de/dennisguse/opentracks/content/data/TestDataUtil.java @@ -125,7 +125,7 @@ public class TestDataUtil { trackPoint.setAltitude(i * ALTITUDE_INTERVAL); trackPoint.setSpeed(Speed.of(5f + (i / 10f))); - trackPoint.setHeartRate(100f + i); + trackPoint.setHeartRate(100f + i % 80); trackPoint.setCadence(300f + i); trackPoint.setPower(400f + i); trackPoint.setAltitudeGain(ALTITUDE_GAIN); diff --git a/src/androidTest/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtilsTest.java index 7b9ceb968..7bf492d97 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtilsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtilsTest.java @@ -19,8 +19,11 @@ import de.dennisguse.opentracks.content.data.TestDataUtil; import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.TrackPointIterator; import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.Track; +import de.dennisguse.opentracks.settings.PreferencesUtils; +import de.dennisguse.opentracks.stats.SensorStatistics; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.ui.intervals.IntervalStatistics; import de.dennisguse.opentracks.util.StringUtils; @@ -34,6 +37,7 @@ public class VoiceAnnouncementUtilsTest { @Before public void setUp() { contentProviderUtils = new ContentProviderUtils(context); + PreferencesUtils.setVoiceAnnounceHeartRate(false); } @Test @@ -46,7 +50,7 @@ public class VoiceAnnouncementUtilsTest { stats.setTotalAltitudeGain(6000f); // when - String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, null); + String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, null, null); // then assertEquals("total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 18.4 kilometers per hour", announcement); @@ -67,18 +71,10 @@ public class VoiceAnnouncementUtilsTest { } // when - String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval); + String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval, null); // then - Assert.assertEquals( - "total distance " + - StringUtils.getDistanceParts(context, stats.getTotalDistance(), true).first + - " kilometers in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " + - StringUtils.getSpeedParts(context, stats.getAverageMovingSpeed(), true, true).first + - " kilometers per hour Lap speed of " + - StringUtils.getSpeedParts(context, lastInterval.getSpeed(), true, true).first + - " kilometers per hour", - announcement); + assertEquals("total distance 14.21 kilometers in 16 minutes 39 seconds at 51.2 kilometers per hour Lap speed of 0.0 kilometers per hour", announcement); } @Test @@ -91,7 +87,7 @@ public class VoiceAnnouncementUtilsTest { stats.setTotalAltitudeGain(6000f); // when - String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, false, null); + String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, false, null, null); // then assertEquals("total distance 20.00 kilometers in 1 hour 5 minutes 10 seconds at 3 minutes 15 seconds per kilometer", announcement); @@ -112,18 +108,10 @@ public class VoiceAnnouncementUtilsTest { } // when - String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, false, lastInterval); + String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, false, lastInterval, null); // then - assertEquals( - "total distance " + - StringUtils.getDistanceParts(context, stats.getTotalDistance(), true).first + - " kilometers in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " + - buildAndGetTimeText(stats.getAverageMovingSpeed().toPace(true), true) + - " per kilometer Lap time of " + - buildAndGetTimeText(lastInterval.getSpeed().toPace(true), true) + - " per kilometer", - announcement); + assertEquals("total distance 14.21 kilometers in 16 minutes 39 seconds at 1 minute 10 seconds per kilometer Lap time of 0 minutes 0 seconds per kilometer", announcement); } @Test @@ -136,7 +124,7 @@ public class VoiceAnnouncementUtilsTest { stats.setTotalAltitudeGain(6000f); // when - String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, true, null); + String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, true, null, null); // then assertEquals("total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 11.4 miles per hour", announcement); @@ -157,18 +145,10 @@ public class VoiceAnnouncementUtilsTest { } // when - String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, true, lastInterval); + String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, true, lastInterval, null); // then - assertEquals( - "total distance " + - StringUtils.getDistanceParts(context, stats.getTotalDistance(), false).first + - " miles in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " + - StringUtils.getSpeedParts(context, stats.getAverageMovingSpeed(), false, true).first + - " miles per hour Lap speed of " + - StringUtils.getSpeedParts(context, lastInterval.getSpeed(), false, true).first + - " miles per hour", - announcement); + assertEquals("total distance 8.83 miles in 16 minutes 39 seconds at 31.8 miles per hour Lap speed of 0.0 miles per hour", announcement); } @Test @@ -181,7 +161,7 @@ public class VoiceAnnouncementUtilsTest { stats.setTotalAltitudeGain(6000f); // when - String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, false, null); + String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, false, null, null); // then assertEquals("total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 5 minutes 15 seconds per mile", announcement); @@ -202,19 +182,35 @@ public class VoiceAnnouncementUtilsTest { } // when - String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, false, lastInterval); + String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, false, lastInterval, null); // then - //assertEquals("total distance 12.43 miles in 1 hour 5 minutes 10 seconds at 5 minutes 15 seconds per mile Lap time of 1 minute 53 seconds per mile", announcement); - assertEquals( - "total distance " + - StringUtils.getDistanceParts(context, stats.getTotalDistance(), false).first + - " miles in " + buildAndGetTimeText(stats.getTotalTime(), false) + " at " + - buildAndGetTimeText(stats.getAverageMovingSpeed().toPace(false), true) + - " per mile Lap time of " + - buildAndGetTimeText(lastInterval.getSpeed().toPace(false), true) + - " per mile", - announcement); + assertEquals("total distance 8.83 miles in 16 minutes 39 seconds at 1 minute 53 seconds per mile Lap time of 0 minutes 0 seconds per mile", announcement); + } + + @Test + public void getAnnouncement_heart_rate_and_sensor_statistics() { + PreferencesUtils.setVoiceAnnounceHeartRate(true); + + int numberOfPoints = 1000; + Pair trackWithStats = TestDataUtil.buildTrackWithTrackPoints(contentProviderUtils, numberOfPoints); + Track.Id trackId = trackWithStats.first; + TrackStatistics stats = trackWithStats.second; + IntervalStatistics.Interval lastInterval; + try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(trackId, null)) { + assertEquals(trackPointIterator.getCount(), numberOfPoints); + IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.of(1000)); + intervalStatistics.addTrackPoints(trackPointIterator); + lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); + } + + SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null); + + // when + String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval, sensorStatistics); + + // then + assertEquals("total distance 14.21 kilometers in 16 minutes 39 seconds at 51.2 kilometers per hour Lap speed of 51.2 kilometers per hour Average heart rate 180 BPM Current heart rate 133 BPM", announcement); } /** @@ -245,4 +241,4 @@ public class VoiceAnnouncementUtilsTest { } return res; } -} \ 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 7688f107d..ba7df79e7 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/stats/TrackStatisticsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/stats/TrackStatisticsTest.java @@ -28,6 +28,7 @@ import java.time.Duration; import java.time.Instant; import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.Speed; /** @@ -67,6 +68,7 @@ public class TrackStatisticsTest { assertEquals(0.0, statistics.getMaxSpeed().toMPS(), 0.0); assertEquals(0.0, statistics.getAverageSpeed().toMPS(), 0.0); assertEquals(0.0, statistics.getAverageMovingSpeed().toMPS(), 0.0); + assertNull(statistics.getAverageHeartRate()); } @Test @@ -91,6 +93,8 @@ public class TrackStatisticsTest { statistics.setMinAltitude(1200.0); // Resulting min altitude statistics2.setMaxAltitude(3575.0); // Resulting max altitude statistics2.setMinAltitude(2800.0); + statistics.setAverageHeartRate(HeartRate.of(100f)); + statistics2.setAverageHeartRate(HeartRate.of(200f)); // when statistics.merge(statistics2); @@ -105,6 +109,7 @@ public class TrackStatisticsTest { assertEquals(Speed.of(statistics.getTotalDistance(), statistics.getMovingTime()).toMPS(), statistics.getMaxSpeed().toMPS(), 0.001); assertEquals(1200.0, statistics.getMinAltitude(), 0.001); assertEquals(3575.0, statistics.getMaxAltitude(), 0.001); + assertEquals(150.0, statistics.getAverageHeartRate().getBPM(), 0.001); } @Test diff --git a/src/androidTest/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdaterTest.java b/src/androidTest/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdaterTest.java index 5c05e6284..2c033722a 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdaterTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdaterTest.java @@ -40,6 +40,7 @@ public class TrackStatisticsUpdaterTest { assertNull(statistics.getTotalAltitudeGain()); assertNull(statistics.getTotalAltitudeLoss()); + assertNull(statistics.getAverageHeartRate()); } @Test @@ -66,6 +67,7 @@ public class TrackStatisticsUpdaterTest { assertNull(statistics.getTotalAltitudeGain()); assertNull(statistics.getTotalAltitudeLoss()); + assertNull(statistics.getAverageHeartRate()); } @Test @@ -91,6 +93,7 @@ public class TrackStatisticsUpdaterTest { assertEquals(14.226, statistics.getMaxSpeed().toMPS(), 0.01); assertEquals(14.226, statistics.getAverageMovingSpeed().toMPS(), 0.01); assertEquals(9.84, statistics.getAverageSpeed().toMPS(), 0.01); + assertEquals(106.85f, statistics.getAverageHeartRate().getBPM(), 0.01); } @Test diff --git a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncement.java b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncement.java index 7240cebcf..62effbfe0 100644 --- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncement.java +++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncement.java @@ -34,6 +34,7 @@ import de.dennisguse.opentracks.data.TrackPointIterator; import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.settings.PreferencesUtils; +import de.dennisguse.opentracks.stats.SensorStatistics; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.ui.intervals.IntervalStatistics; @@ -171,8 +172,12 @@ public class VoiceAnnouncement { IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(isMetricUnits)); intervalStatistics.addTrackPoints(trackPointIterator); IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval(); + SensorStatistics sensorStatistics = null; + if (track.getId() != null) { + sensorStatistics = contentProviderUtils.getSensorStats(track.getId()); + } - String announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), isMetricUnits, isReportSpeed, lastInterval); + String announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), isMetricUnits, isReportSpeed, lastInterval, sensorStatistics); // We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called. tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used"); diff --git a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java index d03ee1166..a2e03e851 100644 --- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java +++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java @@ -1,5 +1,7 @@ package de.dennisguse.opentracks.services.announcement; +import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceHeartRate; + import android.content.Context; import androidx.annotation.Nullable; @@ -9,6 +11,7 @@ import java.time.Duration; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.Speed; +import de.dennisguse.opentracks.stats.SensorStatistics; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.ui.intervals.IntervalStatistics; @@ -17,7 +20,14 @@ class VoiceAnnouncementUtils { private VoiceAnnouncementUtils() { } - static String getAnnouncement(Context context, TrackStatistics trackStatistics, boolean isMetricUnits, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval) { + static String getAnnouncement( + Context context, + TrackStatistics trackStatistics, + boolean isMetricUnits, + boolean isReportSpeed, + @Nullable IntervalStatistics.Interval currentInterval, + @Nullable SensorStatistics sensorStatistics + ) { Distance distance = trackStatistics.getTotalDistance(); Speed distancePerTime = trackStatistics.getAverageMovingSpeed(); Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null; @@ -32,6 +42,7 @@ class VoiceAnnouncementUtils { String rate; String currentRate; String currentRateMsg; + String heartRateMsg = ""; if (isReportSpeed) { int speedId = isMetricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour; double speedInUnit = distancePerTime.to(isMetricUnits); @@ -53,7 +64,21 @@ class VoiceAnnouncementUtils { currentRateMsg = currentInterval == null ? "" : " " + currentRateMsg; - return context.getString(R.string.voice_template, totalDistance, getAnnounceTime(context, trackStatistics.getMovingTime()), rate) + currentRateMsg; + if (shouldVoiceAnnounceHeartRate()) { + if (sensorStatistics != null && sensorStatistics.hasHeartRate()) { + heartRateMsg = context.getString(R.string.average_heart_rate, Math.round(sensorStatistics.getAvgHeartRate().getBPM())); + } + + if (currentInterval != null && currentInterval.hasAverageHeartRate()) { + if (!heartRateMsg.isEmpty()) { + heartRateMsg += " "; + } + heartRateMsg += context.getString(R.string.current_heart_rate, Math.round(currentInterval.getAverageHeartRate().getBPM())); + } + } + heartRateMsg = heartRateMsg.isEmpty() ? "" : " " + heartRateMsg; + + return context.getString(R.string.voice_template, totalDistance, getAnnounceTime(context, trackStatistics.getMovingTime()), rate) + currentRateMsg + heartRateMsg; } //TODO We might need to localize this using strings.xml if order is relevant. diff --git a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java index 5a73e7cdb..ff9c9751a 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java @@ -172,6 +172,13 @@ public class PreferencesUtils { setString(keyId, resources.getString(valueId)); } + @VisibleForTesting + public static void setBoolean(int keyId, Boolean value) { + Editor editor = sharedPreferences.edit(); + editor.putBoolean(getKey(keyId), value); + editor.apply(); + } + static void setInt(int keyId, int value) { Editor editor = sharedPreferences.edit(); editor.putInt(getKey(keyId), value); @@ -316,6 +323,15 @@ public class PreferencesUtils { return getFloat(R.string.voice_speed_rate_key, DEFAULT); } + public static boolean shouldVoiceAnnounceHeartRate() { + return getBoolean(R.string.voice_announce_heart_rate_key, false); + } + + @VisibleForTesting + public static void setVoiceAnnounceHeartRate(boolean value) { + setBoolean(R.string.voice_announce_heart_rate_key, value); + } + public static Distance getRecordingDistanceInterval() { return Distance.of(getInt(R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefaultInternal())); } diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java index cdb7b9ae1..daa81800e 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java @@ -25,6 +25,7 @@ import java.time.Instant; import de.dennisguse.opentracks.data.models.Altitude; import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.TrackPoint; @@ -56,6 +57,8 @@ public class TrackStatistics { private Speed maxSpeed; private Float totalAltitudeGain_m = null; private Float totalAltitudeLoss_m = null; + // The average heart rate seen on this track + private HeartRate avgHeartRate = null; public TrackStatistics() { reset(); @@ -76,6 +79,7 @@ public class TrackStatistics { altitudeExtremities.set(other.altitudeExtremities.getMin(), other.altitudeExtremities.getMax()); totalAltitudeGain_m = other.totalAltitudeGain_m; totalAltitudeLoss_m = other.totalAltitudeLoss_m; + avgHeartRate = other.avgHeartRate; } @VisibleForTesting @@ -108,6 +112,19 @@ public class TrackStatistics { stopTime = stopTime.isAfter(other.stopTime) ? stopTime : other.stopTime; } + if (avgHeartRate == null) { + avgHeartRate = other.avgHeartRate; + } else { + if (other.avgHeartRate != null) { + // Using total time as weights for the averaging. + // Important to do this before total time is updated + avgHeartRate = HeartRate.of( + (totalTime.getSeconds() * avgHeartRate.getBPM() + other.totalTime.getSeconds() * other.avgHeartRate.getBPM()) + / (totalTime.getSeconds() + other.totalTime.getSeconds()) + ); + } + } + totalDistance = totalDistance.plus(other.totalDistance); totalTime = totalTime.plus(other.totalTime); movingTime = movingTime.plus(other.movingTime); @@ -230,6 +247,15 @@ public class TrackStatistics { return totalTime.minus(movingTime); } + public boolean hasAverageHeartRate() { + return avgHeartRate != null; + } + + @Nullable + public HeartRate getAverageHeartRate() { + return avgHeartRate; + } + /** * Gets the average speed. * This calculation only takes into account the displacement until the last point that was accounted for in statistics. @@ -287,6 +313,12 @@ public class TrackStatistics { } } + public void setAverageHeartRate(HeartRate heartRate) { + if (heartRate != null) { + avgHeartRate = heartRate; + } + } + public boolean hasTotalAltitudeGain() { return totalAltitudeGain_m != null; } diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java index 0556c73df..72aa52b6d 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java @@ -26,6 +26,7 @@ import java.util.List; import de.dennisguse.opentracks.data.models.Altitude; import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.TrackPoint; @@ -64,6 +65,8 @@ public class TrackStatisticsUpdater { private final AltitudeRingBuffer altitudeBuffer; private final SpeedRingBuffer speedBuffer; + private float averageHeartRateBPM; + private Duration totalHeartRateDuration = Duration.ZERO; // The current segment's statistics private final TrackStatistics currentSegment; @@ -85,6 +88,7 @@ public class TrackStatisticsUpdater { altitudeBuffer = new AltitudeRingBuffer(ALTITUDE_SMOOTHING_FACTOR); speedBuffer = new SpeedRingBuffer(SPEED_SMOOTHING_FACTOR); + resetAverageHeartRate(); } public TrackStatisticsUpdater(TrackStatisticsUpdater toCopy) { @@ -95,6 +99,7 @@ public class TrackStatisticsUpdater { this.speedBuffer = new SpeedRingBuffer(toCopy.speedBuffer); this.lastTrackPoint = toCopy.lastTrackPoint; + resetAverageHeartRate(); } public TrackStatistics getTrackStatistics() { @@ -142,6 +147,17 @@ public class TrackStatisticsUpdater { currentSegment.updateAltitudeExtremities(newAverage); } + // Update heart rate + if (trackPoint.hasHeartRate() && lastTrackPoint != null) { + Duration trackPointDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime()); + Duration newTotalDuration = totalHeartRateDuration.plus(trackPointDuration); + + averageHeartRateBPM = (totalHeartRateDuration.toMillis() * averageHeartRateBPM + trackPointDuration.toMillis() * trackPoint.getHeartRate().getBPM()) / newTotalDuration.toMillis(); + totalHeartRateDuration = newTotalDuration; + + currentSegment.setAverageHeartRate(HeartRate.of(averageHeartRateBPM)); + } + // Update total distance if (trackPoint.hasSensorDistance()) { // Sensor-based distance/speed @@ -184,6 +200,12 @@ public class TrackStatisticsUpdater { lastTrackPoint = null; altitudeBuffer.reset(); speedBuffer.reset(); + resetAverageHeartRate(); + } + + private void resetAverageHeartRate() { + averageHeartRateBPM = 0.0f; + totalHeartRateDuration = Duration.ZERO; } /** diff --git a/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatistics.java b/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatistics.java index da564635d..5a3199354 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatistics.java +++ b/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatistics.java @@ -8,6 +8,7 @@ import java.util.List; import de.dennisguse.opentracks.data.TrackPointIterator; import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.stats.TrackStatistics; @@ -102,6 +103,7 @@ public class IntervalStatistics { private Duration time = Duration.ofSeconds(0); private Float gain_m; private Float loss_m; + private HeartRate avgHeartRate; public Interval() { } @@ -117,6 +119,7 @@ public class IntervalStatistics { time = i.time; gain_m = i.gain_m; loss_m = i.loss_m; + avgHeartRate = i.avgHeartRate; } public Interval(Interval i) { @@ -124,6 +127,7 @@ public class IntervalStatistics { time = i.time; gain_m = i.gain_m; loss_m = i.loss_m; + avgHeartRate = i.avgHeartRate; } private void adjust(double adjustFactor) { @@ -155,11 +159,20 @@ public class IntervalStatistics { return loss_m; } + public boolean hasAverageHeartRate() { + return avgHeartRate != null; + } + + public HeartRate getAverageHeartRate() { + return avgHeartRate; + } + private void add(TrackStatistics trackStatistics, @Nullable TrackPoint lastTrackPoint) { distance = distance.plus(trackStatistics.getTotalDistance()); time = time.plus(trackStatistics.getTotalTime()); gain_m = trackStatistics.hasTotalAltitudeGain() ? trackStatistics.getTotalAltitudeGain() : gain_m; loss_m = trackStatistics.hasTotalAltitudeLoss() ? trackStatistics.getTotalAltitudeLoss() : loss_m; + avgHeartRate = trackStatistics.getAverageHeartRate(); if (lastTrackPoint == null) { return; } @@ -176,6 +189,7 @@ public class IntervalStatistics { time = trackStatistics.getTotalTime(); gain_m = trackStatistics.hasTotalAltitudeGain() ? trackStatistics.getTotalAltitudeGain() : gain_m; loss_m = trackStatistics.hasTotalAltitudeLoss() ? trackStatistics.getTotalAltitudeLoss() : loss_m; + avgHeartRate = trackStatistics.getAverageHeartRate(); } } } diff --git a/src/main/res/values/settings.xml b/src/main/res/values/settings.xml index 272d28382..09efc7cc3 100644 --- a/src/main/res/values/settings.xml +++ b/src/main/res/values/settings.xml @@ -283,6 +283,7 @@ 1.2 1.3 + voiceAnnounceHeartRate exportTrackFileFormat diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 15432b622..9a7975425 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -405,6 +405,7 @@ limitations under the License. Announcements Time interval Distance interval + Report heart rate Reset @@ -582,6 +583,8 @@ limitations under the License. total distance 1 mile total distance %1$.2f miles + Average heart rate %1$d BPM + Current heart rate %1$d BPM ATM bank diff --git a/src/main/res/xml/settings_announcements.xml b/src/main/res/xml/settings_announcements.xml index dbfd44c7a..b8789dbdb 100644 --- a/src/main/res/xml/settings_announcements.xml +++ b/src/main/res/xml/settings_announcements.xml @@ -26,4 +26,9 @@ android:title="@string/menu_voice_rate" app:useSimpleSummaryProvider="true" /> + + \ No newline at end of file