IntervalStatistics/Announcement: use Distance.

This commit is contained in:
Dennis Guse
2021-04-12 19:00:31 +02:00
parent da70fca389
commit bc2a3ddee3
9 changed files with 81 additions and 82 deletions
@@ -52,7 +52,7 @@ public class AnnouncementUtilsTest {
stats.setTotalAltitudeGain(6000f); stats.setTotalAltitudeGain(6000f);
List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second; List<TrackPoint> trackPoints = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 10).second;
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, 1000); IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(1000));
IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1); IntervalStatistics.Interval lastInterval = intervalStatistics.getIntervalList().get(intervalStatistics.getIntervalList().size() - 1);
// when // when
@@ -114,26 +114,27 @@ public class IntervalStatisticsTest {
} }
private void whenAndThen(List<TrackPoint> trackPoints, TrackStatistics trackStatistics, float distanceInterval) { private void whenAndThen(List<TrackPoint> trackPoints, TrackStatistics trackStatistics, float distanceInterval) {
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, distanceInterval); IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.of(distanceInterval));
List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList(); List<IntervalStatistics.Interval> intervalList = intervalStatistics.getIntervalList();
double totalDistance = 0d; Distance totalDistance = Distance.of(0);
float totalTime = 0L; float totalTime = 0L;
float totalGain = 0f; float totalGain = 0f;
for (IntervalStatistics.Interval i : intervalList) { for (IntervalStatistics.Interval i : intervalList) {
totalDistance += i.getDistance_m(); totalDistance = totalDistance.plus(i.getDistance());
totalTime += i.getDistance_m() / i.getSpeed_ms(); totalTime += i.getDistance().toM() / i.getSpeed().toMPS();
totalGain += i.getGain_m(); totalGain += i.getGain_m();
} }
// then // then
assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance, 0.01); assertEquals(trackStatistics.getTotalDistance().toM(), totalDistance.toM(), 0.01);
assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1); assertEquals(trackStatistics.getTotalTime().toMillis(), totalTime * UnitConversions.S_TO_MS, 1);
assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval)); assertEquals(intervalList.size(), (int) Math.ceil(trackStatistics.getTotalDistance().toM() / distanceInterval));
assertEquals(totalGain, trackPoints.size() * TestDataUtil.ALTITUDE_GAIN, 0.1); assertEquals(totalGain, trackPoints.size() * TestDataUtil.ALTITUDE_GAIN, 0.1);
for (int i = 0; i < intervalList.size() - 1; i++) { for (int i = 0; i < intervalList.size() - 1; i++) {
assertEquals(intervalList.get(i).getDistance_m(), distanceInterval, 0.001); assertEquals(intervalList.get(i).getDistance().toM(), distanceInterval, 0.001);
totalDistance -= intervalList.get(i).getDistance_m(); totalDistance = totalDistance.minus(intervalList.get(i).getDistance());
} }
assertEquals(intervalList.get(intervalList.size() - 1).getDistance_m(), totalDistance, 0.01); assertEquals(intervalList.get(intervalList.size() - 1).getDistance().toM(), totalDistance.toM(), 0.01);
} }
} }
@@ -13,7 +13,6 @@ import java.util.List;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance; import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics; import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
@@ -48,15 +47,17 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
IntervalStatistics.Interval interval = intervalList.get(actualPosition); IntervalStatistics.Interval interval = intervalList.get(actualPosition);
viewHolder.itemView.setTag(actualPosition); viewHolder.itemView.setTag(actualPosition);
float sumDistance_m; Distance sumDistance;
if (isLast && actualPosition > 0) { if (isLast && actualPosition > 0) {
sumDistance_m = actualPosition * intervalList.get(actualPosition - 1).getDistance_m() + interval.getDistance_m(); sumDistance = intervalList.get(actualPosition - 1).getDistance()
.multipliedBy(actualPosition)
.plus(interval.getDistance());
} else { } else {
sumDistance_m = nextPosition * interval.getDistance_m(); sumDistance = interval.getDistance().multipliedBy(nextPosition);
} }
viewHolder.distance.setText(StringUtils.formatDistance(context, Distance.of(sumDistance_m), metricUnits)); viewHolder.distance.setText(StringUtils.formatDistance(context, sumDistance, metricUnits));
viewHolder.rate.setText(StringUtils.formatSpeed(context, Speed.of(interval.getSpeed_ms()), metricUnits, isReportSpeed)); viewHolder.rate.setText(StringUtils.formatSpeed(context, interval.getSpeed(), metricUnits, isReportSpeed));
viewHolder.gain.setText(StringUtils.formatDistance(context, Distance.of(interval.getGain_m()), metricUnits)); viewHolder.gain.setText(StringUtils.formatDistance(context, Distance.of(interval.getGain_m()), metricUnits));
viewHolder.loss.setText(StringUtils.formatDistance(context, Distance.of(interval.getLoss_m()), metricUnits)); viewHolder.loss.setText(StringUtils.formatDistance(context, Distance.of(interval.getLoss_m()), metricUnits));
@@ -20,6 +20,14 @@ public class Distance {
return of(distance_mile * UnitConversions.MI_TO_M); return of(distance_mile * UnitConversions.MI_TO_M);
} }
public static Distance one(boolean metricUnit) {
if (metricUnit) {
return Distance.of(1);
} else {
return Distance.ofMile(1);
}
}
public static Distance invalid() { public static Distance invalid() {
return of(Double.NaN); return of(Double.NaN);
} }
@@ -34,10 +42,22 @@ public class Distance {
return new Distance(distance_m + distance.distance_m); return new Distance(distance_m + distance.distance_m);
} }
public Distance minus(@NonNull Distance distance) {
return new Distance(distance_m - distance.distance_m);
}
public Distance multipliedBy(double factor) { public Distance multipliedBy(double factor) {
return new Distance(factor * distance_m); return new Distance(factor * distance_m);
} }
public Distance dividedBy(double divisor) {
return multipliedBy(1 / divisor);
}
public double dividedBy(@NonNull Distance divisor) {
return distance_m / divisor.distance_m;
}
public boolean isZero() { public boolean isZero() {
return distance_m == 0; return distance_m == 0;
} }
@@ -26,6 +26,7 @@ import android.util.Log;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Locale; import java.util.Locale;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Track; import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
@@ -35,7 +36,6 @@ import de.dennisguse.opentracks.services.TrackRecordingServiceStatus;
import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.AnnouncementUtils; import de.dennisguse.opentracks.util.AnnouncementUtils;
import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.PreferencesUtils;
import de.dennisguse.opentracks.util.UnitConversions;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics; import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
/** /**
@@ -193,7 +193,7 @@ public class AnnouncementPeriodicTask implements PeriodicTask {
boolean isMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context); boolean isMetricUnits = PreferencesUtils.isMetricUnits(sharedPreferences, context);
boolean isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, context, category); boolean isReportSpeed = PreferencesUtils.isReportSpeed(sharedPreferences, context, category);
IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, (float) (isMetricUnits ? 1d * UnitConversions.KM_TO_M : 1d * UnitConversions.MI_TO_M)); IntervalStatistics intervalStatistics = new IntervalStatistics(trackPoints, Distance.one(isMetricUnits));
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval(); IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, isMetricUnits, isReportSpeed, lastInterval); String announcement = AnnouncementUtils.getAnnouncement(context, trackStatistics, isMetricUnits, isReportSpeed, lastInterval);
@@ -20,16 +20,12 @@ public class AnnouncementUtils {
public static String getAnnouncement(Context context, TrackStatistics trackStatistics, boolean isMetricUnits, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval) { public static String getAnnouncement(Context context, TrackStatistics trackStatistics, boolean isMetricUnits, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval) {
Distance distance = trackStatistics.getTotalDistance(); Distance distance = trackStatistics.getTotalDistance();
Speed distancePerTime = trackStatistics.getAverageMovingSpeed(); Speed distancePerTime = trackStatistics.getAverageMovingSpeed();
double currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed_ms() * UnitConversions.MPS_TO_KMH : 0; //TODO Use Speed? Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null;
if (distance.isZero()) { if (distance.isZero()) {
return context.getString(R.string.voice_total_distance_zero); return context.getString(R.string.voice_total_distance_zero);
} }
if (!isMetricUnits) {
currentDistancePerTime *= UnitConversions.KM_TO_MI;
}
String rate; String rate;
String currentRate; String currentRate;
String currentRateMsg; String currentRateMsg;
@@ -38,17 +34,16 @@ public class AnnouncementUtils {
double distanceInUnit = distancePerTime.to(isMetricUnits); double distanceInUnit = distancePerTime.to(isMetricUnits);
rate = context.getResources().getQuantityString(speedId, getQuantityCount(distanceInUnit), distanceInUnit); rate = context.getResources().getQuantityString(speedId, getQuantityCount(distanceInUnit), distanceInUnit);
currentRate = context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTime), currentDistancePerTime); double currentDistancePerTimeInUnit = currentDistancePerTime != null ? currentDistancePerTime.to(isMetricUnits) : 0;
currentRate = context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit);
currentRateMsg = context.getString(R.string.voice_speed_lap, currentRate); currentRateMsg = context.getString(R.string.voice_speed_lap, currentRate);
} else { } else {
double timePerDistance = distancePerTime.isZero() ? 0.0 : 1 / distancePerTime.to(isMetricUnits); //TODO PACE Duration time = distancePerTime.toPace(isMetricUnits);
int paceId = isMetricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile; int paceId = isMetricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
Duration time = Duration.ofMillis((long) (timePerDistance * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS));
rate = context.getString(paceId, getAnnounceTime(context, time)); rate = context.getString(paceId, getAnnounceTime(context, time));
double currentTimePerDistance = currentDistancePerTime == 0 ? 0.0 : 1 / currentDistancePerTime; //TODO PACE Duration currentTime = currentDistancePerTime.toPace(isMetricUnits);
Duration currentTime = Duration.ofMillis((long) (currentTimePerDistance * UnitConversions.HR_TO_MIN * UnitConversions.MIN_TO_S * UnitConversions.S_TO_MS));
currentRate = context.getString(paceId, getAnnounceTime(context, currentTime)); currentRate = context.getString(paceId, getAnnounceTime(context, currentTime));
currentRateMsg = context.getString(R.string.voice_pace_lap, currentRate); currentRateMsg = context.getString(R.string.voice_pace_lap, currentRate);
} }
@@ -22,32 +22,29 @@ package de.dennisguse.opentracks.util;
*/ */
public class UnitConversions { public class UnitConversions {
// Time //TODO Use Duration
// multiplication factor to convert seconds to milliseconds // multiplication factor to convert seconds to milliseconds
public static final long S_TO_MS = 1000; public static final long S_TO_MS = 1000;
// Time
// 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;
// multiplication factor to convert milliseconds to seconds // multiplication factor to convert milliseconds to seconds
@Deprecated
public static final double MS_TO_S = 1d / S_TO_MS; public static final double MS_TO_S = 1d / S_TO_MS;
// multiplication factor to convert minutes to seconds // multiplication factor to convert minutes to seconds
@Deprecated
public static final double MIN_TO_S = 60.0; public static final double MIN_TO_S = 60.0;
// multiplication factor to convert seconds to minutes // multiplication factor to convert seconds to minutes
@Deprecated
public static final double S_TO_MIN = 1 / MIN_TO_S; public static final double S_TO_MIN = 1 / MIN_TO_S;
// multiplication factor to convert hours to minutes
public static final double HR_TO_MIN = 60.0;
// multiplication factor to convert minutes to hours // multiplication factor to convert minutes to hours
public static final double MIN_TO_HR = 1 / HR_TO_MIN; @Deprecated
public static final double S_TO_HR = 1d / (S_TO_MIN * MIN_TO_HR); public static final double MIN_TO_HR = 1 / 60.0;
// multiplication factor to convert kilometers to miles // multiplication factor to convert kilometers to miles
public static final double KM_TO_MI = 0.621371192; public static final double KM_TO_MI = 0.621371192;
public static final double MM_TO_M = 0.001; public static final double MM_TO_M = 0.001;
// Distance // Distance //TODO Make private to Distance class!
// multiplication factor to convert miles to feet // multiplication factor to convert miles to feet
private static final double MI_TO_FT = 5280.0; private static final double MI_TO_FT = 5280.0;
// multiplication factor to convert feet to miles // multiplication factor to convert feet to miles
@@ -6,20 +6,17 @@ import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.UnitConversions;
public class IntervalStatistics { public class IntervalStatistics {
private final List<Interval> intervalList = new ArrayList<>(); private final List<Interval> intervalList = new ArrayList<>();
private final float distanceInterval_m; private final Distance distanceInterval;
/** public IntervalStatistics(@NonNull List<TrackPoint> trackPoints, Distance distanceInterval) {
* @param trackPoints the list of TrackPoint.
* @param distanceInterval_m the meters of every interval.
*/
public IntervalStatistics(@NonNull List<TrackPoint> trackPoints, float distanceInterval_m) {
intervalList.clear(); intervalList.clear();
this.distanceInterval_m = distanceInterval_m; this.distanceInterval = distanceInterval;
if (trackPoints.size() == 0) { if (trackPoints.size() == 0) {
return; return;
@@ -33,24 +30,22 @@ public class IntervalStatistics {
TrackPoint trackPoint = trackPoints.get(i); TrackPoint trackPoint = trackPoints.get(i);
if (trackPoint.hasLocation() && prevTrackPoint.hasLocation()) { if (trackPoint.hasLocation() && prevTrackPoint.hasLocation()) {
interval.distance_m += prevTrackPoint.distanceToPrevious(trackPoint).toM(); interval.distance = interval.distance.plus(prevTrackPoint.distanceToPrevious(trackPoint));
interval.time = interval.time.plus(Duration.between(prevTrackPoint.getTime(), trackPoint.getTime())); interval.time = interval.time.plus(Duration.between(prevTrackPoint.getTime(), trackPoint.getTime()));
interval.gain_m += trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : 0; interval.gain_m += trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : 0;
interval.loss_m += trackPoint.hasAltitudeLoss() ? trackPoint.getAltitudeLoss() : 0; interval.loss_m += trackPoint.hasAltitudeLoss() ? trackPoint.getAltitudeLoss() : 0;
if (interval.distance_m >= distanceInterval_m) { if (interval.distance.greaterThan(distanceInterval)) {
float adjustFactor = distanceInterval_m / interval.distance_m; Interval adjustedInterval = new Interval(interval, distanceInterval.dividedBy(interval.distance));
Interval adjustedInterval = new Interval(interval);
adjustedInterval.adjust(adjustFactor);
intervalList.add(adjustedInterval); intervalList.add(adjustedInterval);
interval = new Interval(interval.distance_m - adjustedInterval.distance_m, interval.time.minus(adjustedInterval.time)); interval = new Interval(interval.distance.minus(adjustedInterval.distance), interval.time.minus(adjustedInterval.time));
} }
} }
} }
if (interval.distance_m > 1f) { if (interval.distance.greaterThan(Distance.of(1))) {
intervalList.add(interval); intervalList.add(interval);
} }
} }
@@ -66,12 +61,12 @@ public class IntervalStatistics {
* @return the interval object or null if any interval is completed. * @return the interval object or null if any interval is completed.
*/ */
public Interval getLastInterval() { public Interval getLastInterval() {
if (intervalList.size() == 1 && intervalList.get(0).getDistance_m() < distanceInterval_m) { if (intervalList.size() == 1 && intervalList.get(0).getDistance().lessThan(distanceInterval)) {
return null; return null;
} }
for (int i = intervalList.size() - 1; i >= 0; i--) { for (int i = intervalList.size() - 1; i >= 0; i--) {
if (intervalList.get(i).getDistance_m() >= distanceInterval_m) { if (intervalList.get(i).getDistance().greaterOrEqualThan(distanceInterval)) {
return this.intervalList.get(i); return this.intervalList.get(i);
} }
} }
@@ -80,7 +75,7 @@ public class IntervalStatistics {
} }
public static class Interval { public static class Interval {
private float distance_m = 0f; private Distance distance = Distance.of(0);
private Duration time = Duration.ofSeconds(0); private Duration time = Duration.ofSeconds(0);
private float gain_m = 0f; private float gain_m = 0f;
private float loss_m = 0f; private float loss_m = 0f;
@@ -88,35 +83,25 @@ public class IntervalStatistics {
public Interval() { public Interval() {
} }
public Interval(float distance_m, Duration time) { public Interval(Distance distance, Duration time) {
this.distance_m = distance_m; this.distance = distance;
this.time = time; this.time = time;
} }
public Interval(Interval i) { public Interval(Interval i, double adjustFactor) {
distance_m = i.distance_m; distance = i.distance.multipliedBy(adjustFactor);
time = Duration.ofMillis((long) (i.time.toMillis() * adjustFactor));
time = i.time; time = i.time;
gain_m = i.gain_m; gain_m = i.gain_m;
loss_m = i.loss_m; loss_m = i.loss_m;
} }
public float getDistance_m() { public Distance getDistance() {
return distance_m; return distance;
} }
public void adjust(float adjustFactor) { public Speed getSpeed() {
distance_m *= adjustFactor; return Speed.of(distance, time);
time = Duration.ofMillis((long) (time.toMillis() * adjustFactor));
}
/**
* @return speed of the interval in m/s.
*/
public float getSpeed_ms() {
if (distance_m == 0f) {
return 0f;
}
return (distance_m / (time.toMillis() * (float) UnitConversions.MS_TO_S));
} }
public float getGain_m() { public float getGain_m() {
@@ -10,8 +10,8 @@ import androidx.lifecycle.MutableLiveData;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.util.UnitConversions;
/** /**
* This model is used to load intervals for a track. * This model is used to load intervals for a track.
@@ -21,7 +21,7 @@ public class IntervalStatisticsModel extends AndroidViewModel {
private final List<TrackPoint> trackPoints = new ArrayList<>(); private final List<TrackPoint> trackPoints = new ArrayList<>();
private MutableLiveData<List<IntervalStatistics.Interval>> intervalsLiveData; private MutableLiveData<List<IntervalStatistics.Interval>> intervalsLiveData;
private float distanceInterval; private Distance distanceInterval;
public IntervalStatisticsModel(@NonNull Application application) { public IntervalStatisticsModel(@NonNull Application application) {
super(application); super(application);
@@ -35,7 +35,7 @@ public class IntervalStatisticsModel extends AndroidViewModel {
} }
intervalsLiveData = new MutableLiveData<>(); intervalsLiveData = new MutableLiveData<>();
distanceInterval = metricUnits ? (float) (interval.getValue() * UnitConversions.KM_TO_M) : (float) (interval.getValue() * UnitConversions.MI_TO_M); distanceInterval = interval.getValue();
loadIntervalStatistics(); loadIntervalStatistics();
} }
return intervalsLiveData; return intervalsLiveData;
@@ -73,7 +73,7 @@ public class IntervalStatisticsModel extends AndroidViewModel {
interval = IntervalOption.OPTION_1; interval = IntervalOption.OPTION_1;
} }
distanceInterval = metricUnits ? (float) (interval.getValue() * UnitConversions.KM_TO_M) : (float) (interval.getValue() * UnitConversions.MI_TO_M); distanceInterval = interval.getValue();
loadIntervalStatistics(); loadIntervalStatistics();
} }
} }
@@ -91,19 +91,19 @@ public class IntervalStatisticsModel extends AndroidViewModel {
OPTION_20(20), OPTION_20(20),
OPTION_50(50); OPTION_50(50);
private final int value; private final Distance value;
IntervalOption(int value) { IntervalOption(int value) {
this.value = value; this.value = Distance.of(value);
} }
public int getValue() { public Distance getValue() {
return value; return value;
} }
@Override @Override
public String toString() { public String toString() {
return "" + value; return "" + (int) value.toM(); //TODO Somehow IntervalsFragment relies on a parsable Integer.
} }
} }
} }