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 1dfeedf36..f902b8618 100644 --- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java +++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java @@ -4,6 +4,7 @@ import static android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageHeartRate; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceAverageSpeedPace; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapHeartRate; +import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapPower; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceLapSpeedPace; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceMovingTime; import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance; @@ -23,6 +24,7 @@ import java.util.Map; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.Power; import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.settings.UnitSystem; import de.dennisguse.opentracks.stats.SensorStatistics; @@ -159,6 +161,19 @@ class VoiceAnnouncementUtils { builder.append("."); } + if (shouldVoiceAnnounceLapPower() && currentInterval != null && currentInterval.hasAveragePower()) { + int currentPower = Math.round(currentInterval.getAveragePower().getW()); + if(shouldVoiceAnnounceUnit()) { + String template = context.getResources().getString(R.string.power_x_watt); + builder.append(" ") + .append(MessageFormat.format(template, Map.of("x", currentPower))); + } else { + builder.append(" ").append(context.getString(R.string.power)).append(" "); + builder.append(String.valueOf(currentPower)); + } + builder.append("."); + } + return builder; } diff --git a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java index d12760e1c..5f2077091 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java @@ -434,6 +434,10 @@ public class PreferencesUtils { return getBoolean(R.string.voice_announce_lap_heart_rate_key, false); } + public static boolean shouldVoiceAnnounceLapPower() { + return getBoolean(R.string.voice_announce_lap_power_key, false); + } + @VisibleForTesting public static void setVoiceAnnounceLapHeartRate(boolean value) { setBoolean(R.string.voice_announce_lap_heart_rate_key, value); diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java index 41fa6d8e7..976f9b618 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java @@ -26,6 +26,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.Power; import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.TrackPoint; @@ -60,6 +61,7 @@ public class TrackStatistics { private Float totalAltitudeLoss_m = null; // The average heart rate seen on this track private HeartRate avgHeartRate = null; + private Power avgPower = null; private boolean isIdle; @@ -83,6 +85,7 @@ public class TrackStatistics { totalAltitudeGain_m = other.totalAltitudeGain_m; totalAltitudeLoss_m = other.totalAltitudeLoss_m; avgHeartRate = other.avgHeartRate; + avgPower = other.avgPower; isIdle = other.isIdle; } @@ -129,6 +132,19 @@ public class TrackStatistics { } } + if (avgPower == null) { + avgPower = other.avgPower; + } else { + if (other.avgPower != null) { + // Using total time as weights for the averaging. + // Important to do this before total time is updated + avgPower = Power.of( + (totalTime.getSeconds() * avgPower.getW() + other.totalTime.getSeconds() * other.avgPower.getW()) + / (totalTime.getSeconds() + other.totalTime.getSeconds()) + ); + } + } + totalDistance = totalDistance.plus(other.totalDistance); totalTime = totalTime.plus(other.totalTime); movingTime = movingTime.plus(other.movingTime); @@ -270,6 +286,15 @@ public class TrackStatistics { return avgHeartRate; } + public boolean hasPower() { + return avgPower != null; + } + + @Nullable + public Power getAveragePower() { + return avgPower; + } + /** * Gets the average speed. * This calculation only takes into account the displacement until the last point that was accounted for in statistics. @@ -333,6 +358,12 @@ public class TrackStatistics { } } + public void setAveragePower(Power power) { + if (power != null) { + avgPower = power; + } + } + 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 31dc37936..36fb7fae0 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java @@ -23,6 +23,7 @@ import java.util.List; import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.HeartRate; +import de.dennisguse.opentracks.data.models.Power; import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.TrackPoint; @@ -43,6 +44,8 @@ public class TrackStatisticsUpdater { private float averageHeartRateBPM; private Duration totalHeartRateDuration = Duration.ZERO; + private float averagePowerW; + private Duration totalPowerDuration = Duration.ZERO; // The current segment's statistics private final TrackStatistics currentSegment; @@ -125,6 +128,17 @@ public class TrackStatisticsUpdater { currentSegment.setAverageHeartRate(HeartRate.of(averageHeartRateBPM)); } + // Update power + if (trackPoint.hasPower() && lastTrackPoint != null) { + Duration trackPointDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime()); + Duration newTotalDuration = totalPowerDuration.plus(trackPointDuration); + + averagePowerW = (totalPowerDuration.toMillis() * averagePowerW + trackPointDuration.toMillis() * trackPoint.getPower().getW()) / newTotalDuration.toMillis(); + totalPowerDuration = newTotalDuration; + + currentSegment.setAveragePower(Power.of(averagePowerW)); + } + { // Update total distance Distance movingDistance = null; 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 cbb07bd06..8ce364f71 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatistics.java +++ b/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatistics.java @@ -9,6 +9,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.Power; import de.dennisguse.opentracks.data.models.Speed; import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.stats.TrackStatistics; @@ -103,6 +104,7 @@ public class IntervalStatistics { private Float gain_m; private Float loss_m; private HeartRate avgHeartRate; + private Power avgPower; public Interval() { } @@ -118,6 +120,7 @@ public class IntervalStatistics { gain_m = i.gain_m; loss_m = i.loss_m; avgHeartRate = i.avgHeartRate; + avgPower = i.avgPower; } public Interval(Interval i) { @@ -126,6 +129,7 @@ public class IntervalStatistics { gain_m = i.gain_m; loss_m = i.loss_m; avgHeartRate = i.avgHeartRate; + avgPower = i.avgPower; } public Distance getDistance() { @@ -160,12 +164,21 @@ public class IntervalStatistics { return avgHeartRate; } + public boolean hasAveragePower() { + return avgPower != null; + } + + public Power getAveragePower() { + return avgPower; + } + 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(); + avgPower = trackStatistics.getAveragePower(); if (lastTrackPoint == null) { return; } @@ -183,6 +196,7 @@ public class IntervalStatistics { gain_m = trackStatistics.hasTotalAltitudeGain() ? trackStatistics.getTotalAltitudeGain() : gain_m; loss_m = trackStatistics.hasTotalAltitudeLoss() ? trackStatistics.getTotalAltitudeLoss() : loss_m; avgHeartRate = trackStatistics.getAverageHeartRate(); + avgPower = trackStatistics.getAveragePower(); } } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index ea8047449..4fb7e15c0 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -728,4 +728,8 @@ limitations under the License. OpenTracks itself does not provide a map. Please install OSMDashboard to view your recordings on a map. Sampling time interval %1$s between recorded locations + voiceAnnounceLapPower + Lap power + Power {x, plural, =1 {1 watt} other {# watts} } + Power diff --git a/src/main/res/xml/settings_announcements.xml b/src/main/res/xml/settings_announcements.xml index bb12c55c3..b4eca9a80 100644 --- a/src/main/res/xml/settings_announcements.xml +++ b/src/main/res/xml/settings_announcements.xml @@ -25,6 +25,13 @@ + + - - - - - + + +