From 53240c3d1a4100d1cced1bca403f0e1570fbcae0 Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Fri, 31 Oct 2025 18:35:45 +0100 Subject: [PATCH] IntervalStatisticsUpdater exposes Statistics (instead of Interval). --- .../VoiceAnnouncementUtilsTest.java | 16 ++++---- .../IntervalStatisticsUpdaterTest.java | 36 ++++++++--------- .../opentracks/services/RecordingData.java | 3 +- .../VoiceAnnouncementManager.java | 3 +- .../announcement/VoiceAnnouncementUtils.java | 13 +++--- .../intervals/IntervalStatisticsAdapter.java | 29 ++++++-------- .../ui/intervals/IntervalStatisticsModel.java | 5 ++- .../intervals/IntervalStatisticsUpdater.java | 40 +++++++++++-------- .../ui/intervals/IntervalsFragment.java | 3 +- 9 files changed, 74 insertions(+), 74 deletions(-) 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 8717765f4..3f5259fa9 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtilsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtilsTest.java @@ -165,7 +165,7 @@ public class VoiceAnnouncementUtilsTest { @Test public void getAnnouncement_withInterval_metric_speed() { // given - Pair data = buildTrackWithTrackPoints(); + Pair data = buildTrackWithTrackPoints(); SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); @@ -205,7 +205,7 @@ public class VoiceAnnouncementUtilsTest { @Test public void getAnnouncement_withInterval_metric_pace() { // given - Pair data = buildTrackWithTrackPoints(); + Pair data = buildTrackWithTrackPoints(); SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); @@ -320,7 +320,7 @@ public class VoiceAnnouncementUtilsTest { @Test public void getAnnouncement_withInterval_imperial_speed() { // given - Pair data = buildTrackWithTrackPoints(); + Pair data = buildTrackWithTrackPoints(); SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); @@ -360,7 +360,7 @@ public class VoiceAnnouncementUtilsTest { @Test public void getAnnouncement_withInterval_imperial_pace() { // given - Pair data = buildTrackWithTrackPoints(); + Pair data = buildTrackWithTrackPoints(); SensorDataSet dataSet = Mockito.mock(SensorDataSet.class); Mockito.when(dataSet.getHeartRate()).thenReturn(new Pair<>(HeartRate.of(60), "unused")); @@ -378,7 +378,7 @@ public class VoiceAnnouncementUtilsTest { PreferencesUtils.setVoiceAnnounceAverageHeartRate(true); PreferencesUtils.setVoiceAnnounceLapHeartRate(true); - Pair data = buildTrackWithTrackPoints(); + Pair data = buildTrackWithTrackPoints(); SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null); @@ -404,7 +404,7 @@ public class VoiceAnnouncementUtilsTest { PreferencesUtils.setVoiceAnnounceAverageSpeedPace(false); PreferencesUtils.setVoiceAnnounceLapSpeedPace(false); - Pair data = buildTrackWithTrackPoints(); + Pair data = buildTrackWithTrackPoints(); SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null, null); @@ -433,7 +433,7 @@ public class VoiceAnnouncementUtilsTest { assertEquals("1.2 miles", MessageFormat.format(template, Map.of("n", 1.18))); } - private static Pair buildTrackWithTrackPoints() { + private static Pair buildTrackWithTrackPoints() { int numberOfPoints = 1000; ArrayList trackPoints = new ArrayList<>(); @@ -443,7 +443,7 @@ public class VoiceAnnouncementUtilsTest { IntervalStatisticsUpdater intervalStatistics = new IntervalStatisticsUpdater(Distance.of(1000)); intervalStatistics.addTrackPoints(trackPoints.iterator()); - IntervalStatisticsUpdater.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); + Statistics lastInterval = intervalStatistics.getLastInterval(); Track track = new Track(new Statistics( Instant.EPOCH, diff --git a/src/androidTest/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsUpdaterTest.java b/src/androidTest/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsUpdaterTest.java index 67e23752f..f06502185 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsUpdaterTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsUpdaterTest.java @@ -14,6 +14,7 @@ import java.util.List; import de.dennisguse.opentracks.content.data.TestDataUtil; import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.Statistics; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; @@ -126,25 +127,25 @@ public class IntervalStatisticsUpdaterTest { intervalStatistics.addTrackPoints(trackPoints.iterator()); - List intervalList = intervalStatistics.getIntervalList(); + List intervalList = intervalStatistics.getIntervalList(); Distance totalDistance = Distance.of(0); float totalTime = 0L; Float totalGain = null; Float totalLoss = null; - for (IntervalStatisticsUpdater.Interval i : intervalList) { - totalDistance = totalDistance.plus(i.getDistance()); - totalTime += i.getDistance().toM() / i.getSpeed().toMPS(); + for (Statistics i : intervalList) { + totalDistance = totalDistance.plus(i.totalDistance()); + totalTime += i.totalDistance().toM() / i.getAverageSpeed().toMPS(); if (totalGain == null) { - totalGain = i.getGain_m(); - } else if (i.getGain_m() != null) { - totalGain += i.getGain_m(); + totalGain = i.altitudeGainLoss() != null ? i.altitudeGainLoss().gain_m() : null; + } else if (i.altitudeGainLoss() != null) { + totalGain += i.altitudeGainLoss().gain_m(); } if (totalLoss == null) { - totalLoss = i.getLoss_m(); - } else if (i.getLoss_m() != null) { - totalLoss += i.getLoss_m(); + totalLoss = i.altitudeGainLoss() != null ? i.altitudeGainLoss().loss_m() : null; + } else if (i.altitudeGainLoss() != null) { + totalLoss += i.altitudeGainLoss().loss_m(); } } @@ -152,22 +153,19 @@ public class IntervalStatisticsUpdaterTest { assertEquals(track.getStatistics().totalTime().toSeconds(), totalTime, 0.01); assertEquals(track.getStatistics().totalDistance().toM(), totalDistance.toM(), 0.01); assertEquals(intervalList.size(), (int) Math.ceil(track.getStatistics().totalDistance().toM() / distanceInterval)); - if (totalGain != null) { + if (totalGain != null && totalLoss != null) { assertEquals(totalGain, numberOfPoints * TestDataUtil.ALTITUDE_GAIN, 0.1); - } else { - assertTrue(intervalStatistics.getIntervalList().stream().noneMatch(IntervalStatisticsUpdater.Interval::hasGain)); - } - if (totalLoss != null) { assertEquals(totalLoss, numberOfPoints * TestDataUtil.ALTITUDE_LOSS, 0.1); + } else { - assertTrue(intervalStatistics.getIntervalList().stream().noneMatch(IntervalStatisticsUpdater.Interval::hasLoss)); + assertTrue(intervalStatistics.getIntervalList().stream().noneMatch(i -> i.altitudeGainLoss() != null)); } for (int i = 0; i < intervalList.size() - 1; i++) { - assertEquals(intervalList.get(i).getDistance().toM(), distanceInterval, 0.001); - totalDistance = totalDistance.minus(intervalList.get(i).getDistance()); + assertEquals(intervalList.get(i).totalDistance().toM(), distanceInterval, 0.001); + totalDistance = totalDistance.minus(intervalList.get(i).totalDistance()); } - assertEquals(intervalList.get(intervalList.size() - 1).getDistance().toM(), totalDistance.toM(), 0.01); + assertEquals(intervalList.get(intervalList.size() - 1).totalDistance().toM(), totalDistance.toM(), 0.01); } private static Pair> buildTrackWithTrackPoints(int numberOfPoints) { diff --git a/src/main/java/de/dennisguse/opentracks/services/RecordingData.java b/src/main/java/de/dennisguse/opentracks/services/RecordingData.java index d3b5e679e..2d4a57512 100644 --- a/src/main/java/de/dennisguse/opentracks/services/RecordingData.java +++ b/src/main/java/de/dennisguse/opentracks/services/RecordingData.java @@ -11,8 +11,7 @@ import de.dennisguse.opentracks.stats.SegmentStatisticUpdater; /** * {@link Track} and {@link TrackPoint} must be immutable (i.e., their content does not change). */ -public record RecordingData(Track track, TrackPoint latestTrackPoint, SensorDataSet sensorDataSet, - Statistics currentSegment) { +public record RecordingData(Track track, TrackPoint latestTrackPoint, SensorDataSet sensorDataSet, Statistics currentSegment) { public String getTrackCategory() { if (track == null) { return ""; diff --git a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java index d48f55c9a..3c0ec52b8 100644 --- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java +++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementManager.java @@ -149,9 +149,10 @@ public class VoiceAnnouncementManager implements SharedPreferences.OnSharedPrefe startTrackPointId = null; } + //TODO Avoid reloading (some) TrackPoints just to announce the last interval. try (AutoCloseableIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), startTrackPointId)) { startTrackPointId = intervalStatistics.addTrackPoints(trackPointIterator); - IntervalStatisticsUpdater.Interval lastInterval = intervalStatistics.getLastInterval(); + Statistics lastInterval = intervalStatistics.getLastInterval(); SensorStatistics sensorStatistics = null; if (track.getId() != null) { sensorStatistics = contentProviderUtils.getSensorStats(track.getId()); 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 7df5e430a..86a626ec2 100644 --- a/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java +++ b/src/main/java/de/dennisguse/opentracks/services/announcement/VoiceAnnouncementUtils.java @@ -38,7 +38,6 @@ import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet; import de.dennisguse.opentracks.settings.UnitSystem; import de.dennisguse.opentracks.stats.SensorStatistics; -import de.dennisguse.opentracks.ui.intervals.IntervalStatisticsUpdater; import de.dennisguse.opentracks.util.StringUtils; class VoiceAnnouncementUtils { @@ -51,7 +50,7 @@ class VoiceAnnouncementUtils { .append(context.getString(R.string.voiceIdle)); } - static Spannable createStatistics(Context context, Track track, SensorDataSet sensorDataSet, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatisticsUpdater.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) { + static Spannable createStatistics(Context context, Track track, SensorDataSet sensorDataSet, UnitSystem unitSystem, boolean isReportSpeed, @Nullable Statistics currentInterval, @Nullable SensorStatistics sensorStatistics) { Statistics trackStatistics = track.getStatistics(); SpannableStringBuilder builder = new SpannableStringBuilder(); @@ -62,7 +61,7 @@ class VoiceAnnouncementUtils { Distance totalDistance = trackStatistics.totalDistance(); Speed averageMovingSpeed = trackStatistics.getAverageMovingSpeed(); - Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null; + Speed currentDistancePerTime = currentInterval != null ? currentInterval.getAverageSpeed() : null; int perUnitStringId = R.string.empty; int distanceId = R.string.voiceDistance; @@ -179,8 +178,8 @@ class VoiceAnnouncementUtils { appendCardinal(builder, context.getString(R.string.sensor_state_heart_rate_value, averageHeartRate), averageHeartRate); builder.append("."); } - if (shouldVoiceAnnounceLapHeartRate() && currentInterval != null && currentInterval.hasAverageHeartRate()) { - int currentHeartRate = Math.round(currentInterval.getAverageHeartRate().getBPM()); + if (shouldVoiceAnnounceLapHeartRate() && currentInterval != null && currentInterval.avgHeartRate() != null) { + int currentHeartRate = Math.round(currentInterval.avgHeartRate().getBPM()); builder.append(" ") .append(context.getString(R.string.lap_heart_rate)); @@ -188,8 +187,8 @@ class VoiceAnnouncementUtils { builder.append("."); } - if (shouldVoiceAnnounceLapPower() && currentInterval != null && currentInterval.hasAveragePower()) { - int currentPower = Math.round(currentInterval.getAveragePower().getW()); + if (shouldVoiceAnnounceLapPower() && currentInterval != null && currentInterval.avgPower() != null) { + int currentPower = Math.round(currentInterval.avgPower().getW()); if (shouldVoiceAnnounceUnit()) { String template = context.getResources().getString(R.string.power_x_watt); builder.append(" ") diff --git a/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsAdapter.java b/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsAdapter.java index 1c495a681..8f2d01334 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsAdapter.java +++ b/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsAdapter.java @@ -7,18 +7,20 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import java.util.ArrayList; import java.util.List; import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.DistanceFormatter; import de.dennisguse.opentracks.data.models.SpeedFormatter; +import de.dennisguse.opentracks.data.models.Statistics; import de.dennisguse.opentracks.databinding.IntervalStatsListItemBinding; import de.dennisguse.opentracks.settings.UnitSystem; import de.dennisguse.opentracks.util.StringUtils; public class IntervalStatisticsAdapter extends RecyclerView.Adapter { - private List intervalList; + private List intervalList = new ArrayList<>(); private final Context context; private final StackMode stackMode; private UnitSystem unitSystem; @@ -43,46 +45,39 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter 0) { - sumDistance = intervalList.get(actualPosition - 1).getDistance() + sumDistance = intervalList.get(actualPosition - 1).totalDistance() .multipliedBy(actualPosition) - .plus(interval.getDistance()); + .plus(interval.totalDistance()); } else { - sumDistance = interval.getDistance().multipliedBy(nextPosition); + sumDistance = interval.totalDistance().multipliedBy(nextPosition); } viewHolder.viewBinding.intervalItemDistance.setText(DistanceFormatter.Builder() .setUnit(unitSystem) .build(context).formatDistance(sumDistance)); SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(isReportSpeed).build(context); - viewHolder.viewBinding.intervalItemRate.setText(formatter.formatSpeed(interval.getSpeed())); + viewHolder.viewBinding.intervalItemRate.setText(formatter.formatSpeed(interval.getAverageSpeed())); - viewHolder.viewBinding.intervalItemGain.setText(StringUtils.formatAltitude(context, interval.getGain_m(), unitSystem)); - viewHolder.viewBinding.intervalItemLoss.setText(StringUtils.formatAltitude(context, interval.getLoss_m(), unitSystem)); + viewHolder.viewBinding.intervalItemGain.setText(StringUtils.formatAltitude(context, interval.altitudeGainLoss() != null ? interval.altitudeGainLoss().gain_m() : null, unitSystem)); + viewHolder.viewBinding.intervalItemLoss.setText(StringUtils.formatAltitude(context, interval.altitudeGainLoss() != null ? interval.altitudeGainLoss().loss_m() : null, unitSystem)); } @Override public int getItemCount() { - if (intervalList == null) { - return 0; - } return intervalList.size(); } - public List swapData(List data, UnitSystem unitSystem, boolean isReportSpeed) { + public void swapData(@NonNull List data, UnitSystem unitSystem, boolean isReportSpeed) { this.unitSystem = unitSystem; this.isReportSpeed = isReportSpeed; intervalList = data; - if (data != null) { - this.notifyDataSetChanged(); - } - - return data; + this.notifyDataSetChanged(); } /** diff --git a/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsModel.java b/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsModel.java index a0b8399c7..13ebf67f4 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsModel.java +++ b/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsModel.java @@ -18,6 +18,7 @@ import java.util.concurrent.Executors; import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.models.Distance; +import de.dennisguse.opentracks.data.models.Statistics; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.tables.TrackPointsColumns; @@ -32,7 +33,7 @@ public class IntervalStatisticsModel extends AndroidViewModel { private static final String TAG = IntervalStatisticsModel.class.getSimpleName(); - private MutableLiveData> intervalsLiveData; + private MutableLiveData> intervalsLiveData; private IntervalStatisticsUpdater intervalStatistics; private Distance distanceInterval; private final ContentResolver contentResolver; @@ -65,7 +66,7 @@ public class IntervalStatisticsModel extends AndroidViewModel { handler = null; } - public MutableLiveData> getIntervalStats(Track.Id trackId, UnitSystem unitSystem, @Nullable IntervalOption interval) { + public MutableLiveData> getIntervalStats(Track.Id trackId, UnitSystem unitSystem, @Nullable IntervalOption interval) { if (intervalsLiveData == null) { if (interval == null) { interval = IntervalOption.OPTION_1; diff --git a/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsUpdater.java b/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsUpdater.java index 72314ee52..f96621af9 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsUpdater.java +++ b/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalStatisticsUpdater.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import de.dennisguse.opentracks.data.models.AltitudeGainLoss; import de.dennisguse.opentracks.data.models.Distance; import de.dennisguse.opentracks.data.models.HeartRate; import de.dennisguse.opentracks.data.models.Power; @@ -74,8 +75,8 @@ public class IntervalStatisticsUpdater { return trackPoint != null ? trackPoint.getId() : null; } - public List getIntervalList() { - return intervalList; + public List getIntervalList() { + return intervalList.stream().map(Interval::toStatistics).toList(); } /** @@ -84,14 +85,15 @@ public class IntervalStatisticsUpdater { * * @return the interval object or null if any interval is completed. */ - public Interval getLastInterval() { + @Nullable + public Statistics getLastInterval() { if (intervalList.size() == 1 && intervalList.get(0).getDistance().lessThan(distanceInterval)) { return null; } for (int i = intervalList.size() - 1; i >= 0; i--) { if (intervalList.get(i).getDistance().greaterOrEqualThan(distanceInterval)) { - return this.intervalList.get(i); + return this.intervalList.get(i).toStatistics(); } } @@ -99,7 +101,8 @@ public class IntervalStatisticsUpdater { } //TODO Could be replaced with Statistics? - public static class Interval { + @Deprecated + private static class Interval { private Distance distance = Distance.of(0); private Duration time = Duration.ofSeconds(0); private Float gain_m; @@ -145,18 +148,6 @@ public class IntervalStatisticsUpdater { return gain_m != null; } - public Float getGain_m() { - return gain_m; - } - - public boolean hasLoss() { - return loss_m != null; - } - - public Float getLoss_m() { - return loss_m; - } - public boolean hasAverageHeartRate() { return avgHeartRate != null; } @@ -201,5 +192,20 @@ public class IntervalStatisticsUpdater { avgHeartRate = trackStatistics.avgHeartRate(); avgPower = trackStatistics.avgPower(); } + + private Statistics toStatistics() { + return new Statistics( + null, + null, + time, + null, + distance, + null, + null, + gain_m != null ? new AltitudeGainLoss(gain_m, loss_m) : null, + avgHeartRate, + avgPower + ); + } } } diff --git a/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalsFragment.java b/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalsFragment.java index c2281f67b..801d2afcb 100644 --- a/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalsFragment.java +++ b/src/main/java/de/dennisguse/opentracks/ui/intervals/IntervalsFragment.java @@ -21,6 +21,7 @@ import java.util.Objects; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.models.DistanceFormatter; +import de.dennisguse.opentracks.data.models.Statistics; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.databinding.IntervalListViewBinding; import de.dennisguse.opentracks.settings.PreferencesUtils; @@ -192,7 +193,7 @@ public class IntervalsFragment extends Fragment { } viewBinding.intervalRate.setText(isReportSpeed ? getString(R.string.stats_speed) : getString(R.string.stats_pace)); - LiveData> liveData = viewModel.getIntervalStats(trackId, unitSystem, selectedInterval); + LiveData> liveData = viewModel.getIntervalStats(trackId, unitSystem, selectedInterval); liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, unitSystem, isReportSpeed)); setIntervalsDropdownText();