Value objects distance and speed.

#679
This commit is contained in:
Dennis Guse
2021-04-09 19:59:44 +02:00
parent f2b1d2a386
commit 6f9c788965
47 changed files with 773 additions and 502 deletions
@@ -48,6 +48,7 @@ import androidx.loader.content.Loader;
import java.time.Duration;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TracksColumns;
import de.dennisguse.opentracks.databinding.TrackListBinding;
@@ -215,7 +216,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe
int iconId = TrackIconUtils.getIconDrawable(icon);
String name = cursor.getString(nameIndex);
String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, Distance.of(cursor.getDouble(totalDistanceIndex)), metricUnits);
int markerCount = cursor.getInt(markerCountIndex);
long startTime = cursor.getLong(startTimeIndex);
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
@@ -12,6 +12,8 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
@@ -52,12 +54,12 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
} else {
sumDistance_m = nextPosition * interval.getDistance_m();
}
viewHolder.distance.setText(StringUtils.formatDistance(context, sumDistance_m, metricUnits));
viewHolder.distance.setText(StringUtils.formatDistance(context, Distance.of(sumDistance_m), metricUnits));
viewHolder.rate.setText(StringUtils.formatSpeed(context, interval.getSpeed_ms(), metricUnits, isReportSpeed));
viewHolder.rate.setText(StringUtils.formatSpeed(context, Speed.of(interval.getSpeed_ms()), metricUnits, isReportSpeed));
viewHolder.gain.setText(StringUtils.formatDistance(context, interval.getGain_m(), metricUnits));
viewHolder.loss.setText(StringUtils.formatDistance(context, interval.getLoss_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));
}
@Override
@@ -29,11 +29,7 @@ public class ChartPoint {
TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics();
if (chartByDistance) {
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
timeOrDistance = distance;
timeOrDistance = trackStatistics.getTotalDistance().to(metricUnits);
} else {
timeOrDistance = trackStatistics.getTotalTime().toMillis();
}
@@ -43,11 +39,8 @@ public class ChartPoint {
altitude *= UnitConversions.M_TO_FT;
}
speed = trackStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MPS_TO_KMH;
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
pace = speed == 0 ? 0.0 : 60.0 / speed;
speed = trackStatisticsUpdater.getSmoothedSpeed().to(metricUnits);
pace = trackStatisticsUpdater.getSmoothedSpeed().toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
if (trackPoint != null) {
if (trackPoint.hasHeartRate()) {
heartRate = trackPoint.getHeartRate_bpm();
@@ -51,7 +51,6 @@ import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.MarkerUtils;
import de.dennisguse.opentracks.util.StringUtils;
import de.dennisguse.opentracks.util.ThemeUtils;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Visualization of the chart.
@@ -994,8 +993,7 @@ public class ChartView extends View {
private double getMarkerXValue(Marker marker) {
if (chartByDistance) {
double length_km = marker.getLength() * UnitConversions.M_TO_KM;
return metricUnits ? length_km : length_km * UnitConversions.KM_TO_MI;
return marker.getLength().to(metricUnits);
} else {
return marker.getDuration().toMillis();
}
@@ -24,6 +24,8 @@ import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
@@ -163,9 +165,9 @@ public class DescriptionGenerator {
* @param lineBreak line break string
*/
@VisibleForTesting
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) {
double distanceInKm = distance * UnitConversions.M_TO_KM;
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI;
void writeDistance(Distance distance, StringBuilder builder, int resId, String lineBreak) {
double distanceInKm = distance.toKM();
double distanceInMi = distance.toMI();
builder.append(context.getString(resId, distanceInKm, distanceInMi));
builder.append(lineBreak);
}
@@ -193,21 +195,18 @@ public class DescriptionGenerator {
* @param lineBreak line break string
*/
@VisibleForTesting
void writeSpeed(double speed, StringBuilder builder, int resId, String lineBreak) {
double speedInKmHr = speed * UnitConversions.MPS_TO_KMH;
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
void writeSpeed(Speed speed, StringBuilder builder, int resId, String lineBreak) {
builder.append(context.getString(resId, speed.toKMH(), speed.toMPH()));
builder.append(lineBreak);
}
/**
* @param speed speed in meters per second
* @param builder StringBuilder to append pace
* @param resId resource id of pace string
* @param lineBreak line break string
*/
@VisibleForTesting
void writePace(double speed, StringBuilder builder, int resId, String lineBreak) {
void writePace(Speed speed, StringBuilder builder, int resId, String lineBreak) {
Pair<String, String> paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
Pair<String, String> paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
@@ -0,0 +1,116 @@
package de.dennisguse.opentracks.content.data;
import androidx.annotation.NonNull;
import java.util.Objects;
import de.dennisguse.opentracks.util.UnitConversions;
public class Distance {
public static Distance of(double distance_m) {
return new Distance(distance_m);
}
public static Distance of(String distance_m) {
return of(Float.parseFloat(distance_m));
}
public static Distance ofMile(double distance_mile) {
return of(distance_mile * UnitConversions.MI_TO_M);
}
public static Distance invalid() {
return of(Double.NaN);
}
private final double distance_m;
private Distance(double distance_m) {
this.distance_m = distance_m;
}
public Distance plus(@NonNull Distance distance) {
return new Distance(distance_m + distance.distance_m);
}
public Distance multipliedBy(double factor) {
return new Distance(factor * distance_m);
}
public boolean isZero() {
return distance_m == 0;
}
public boolean isInvalid() {
return Double.isNaN(distance_m) || Double.isInfinite(distance_m);
}
public boolean lessThan(@NonNull Distance distance) {
return !greaterThan(distance);
}
public boolean greaterThan(@NonNull Distance distance) {
return distance_m > distance.distance_m;
}
public boolean greaterOrEqualThan(@NonNull Distance distance) {
return distance_m >= distance.distance_m;
}
public double toM() {
return distance_m;
}
public double toKM() {
return distance_m * UnitConversions.M_TO_KM;
}
public double toFT() {
return distance_m * UnitConversions.M_TO_FT;
}
public double toMI() {
return toKM() * UnitConversions.KM_TO_MI;
}
public double to(boolean metricUnit) {
return to(metricUnit ? Unit.KM : Unit.MILES);
}
public double to(Unit unit) {
switch (unit) {
case KM:
return toKM();
case MILES:
return toMI();
default:
throw new RuntimeException("Not implemented");
}
}
public enum Unit {
KM,
MILES
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Distance distance = (Distance) o;
return Double.compare(distance.distance_m, distance_m) == 0;
}
@Override
public int hashCode() {
return Objects.hash(distance_m);
}
@Override
public String toString() {
return "Distance{" +
"distance_m=" + distance_m +
'}';
}
}
@@ -28,12 +28,15 @@ import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import de.dennisguse.opentracks.stats.TrackStatistics;
/**
* NOTE: A marker is indirectly (via it's location) assigned to one {@link TrackPoint} with trackPoint.hasLocation() == true.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
//TODO All data should be final; no default values.
public final class Marker {
private Id id;
@@ -51,7 +54,7 @@ public final class Marker {
private Float bearing;
//TODO It is the distance from the track starting point; rename to something more meaningful
private double length = 0.0;
private Distance length;
private Duration duration;
@Deprecated //TODO Make an URI instead of String
@@ -62,6 +65,7 @@ public final class Marker {
this.time = time;
}
@Deprecated //TODO Marker cannot be created without length AND duration!
public Marker(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint) {
this.trackId = trackId;
@@ -75,16 +79,19 @@ public final class Marker {
if (trackPoint.hasAccuracy()) this.accuracy = trackPoint.getAccuracy();
if (trackPoint.hasAltitude()) this.altitude_m = trackPoint.getAltitude();
if (trackPoint.hasBearing()) this.bearing = trackPoint.getBearing();
this.length = Distance.of(0); //TODO Not cool!
this.duration = Duration.ofMillis(0); //TODO Not cool!
}
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, double length, @NonNull Duration duration, @NonNull TrackPoint trackPoint, String photoUrl) {
public Marker(String name, String description, String category, String icon, @NonNull Track.Id trackId, @NonNull TrackStatistics statistics, @NonNull TrackPoint trackPoint, String photoUrl) {
this(trackId, trackPoint);
this.name = name;
this.description = description;
this.category = category;
this.icon = icon;
this.length = length;
this.duration = duration;
this.length = statistics.getTotalDistance();
this.duration = statistics.getTotalTime();
this.photoUrl = photoUrl;
}
@@ -218,11 +225,11 @@ public final class Marker {
this.bearing = bearing;
}
public double getLength() {
public Distance getLength() {
return length;
}
public void setLength(double length) {
public void setLength(Distance length) {
this.length = length;
}
@@ -0,0 +1,145 @@
package de.dennisguse.opentracks.content.data;
import java.time.Duration;
import java.util.Objects;
import de.dennisguse.opentracks.util.UnitConversions;
public class Speed {
public static Speed of(Distance distance, Duration duration) {
if (duration.isZero()) {
return zero();
}
return new Speed(distance.toM() / (duration.toMillis() * UnitConversions.MS_TO_S));
}
public static Speed of(double speed_mps) {
return new Speed(speed_mps);
}
public static Speed of(String speed_mps) {
return of(Float.parseFloat(speed_mps));
}
public static Speed invalid() {
return of(Double.NaN);
}
public static Speed zero() {
return of(0.0);
}
public static Speed max(Speed speed1, Speed speed2) {
if (speed1.greaterThan(speed2)) {
return speed1;
}
return speed2;
}
public static Speed absDiff(Speed speed1, Speed speed2) {
//TODO Why Math.abs? Seems to be a leftover.
return Speed.of(Math.abs(speed1.speed_mps - speed2.speed_mps));
}
// Anything faster than that (in meters per second) will be considered moving.
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private final double speed_mps;
private Speed(double speed_mps) {
this.speed_mps = speed_mps;
}
public Speed mul(double factor) {
return new Speed(factor * speed_mps);
}
public boolean isZero() {
return speed_mps == 0;
}
public boolean isInvalid() {
return Double.isNaN(speed_mps) || Double.isInfinite(speed_mps);
}
public boolean isMoving() {
return !isInvalid() && speed_mps >= MAX_NO_MOVEMENT_SPEED;
}
public boolean lessThan(Speed speed) {
return !greaterThan(speed);
}
public boolean greaterThan(Speed speed) {
return speed_mps > speed.speed_mps;
}
public boolean greaterOrEqualThan(Speed speed) {
return speed_mps >= speed.speed_mps;
}
public double toMPS() {
return speed_mps;
}
public double toKMH() {
return speed_mps * UnitConversions.MPS_TO_KMH;
}
public double toMPH() {
return toKMH() * UnitConversions.KM_TO_MI;
}
public Duration toPace(boolean metricUnit) {
if (isZero()) {
return Duration.ofSeconds(0);
}
double distance = speed_mps * (metricUnit ? UnitConversions.M_TO_KM : UnitConversions.M_TO_MI);
return Duration.ofSeconds(Math.round(1 / distance));
}
public double to(boolean metricUnit) {
return to(metricUnit ? Unit.KMH : Unit.MPH);
}
public double to(Unit unit) {
switch (unit) {
case KMH:
return toKMH();
case MPH:
return toMPH();
default:
throw new RuntimeException("Not implemented");
}
}
public enum Unit {
KMH,
MPH,
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Speed speed = (Speed) o;
return Double.compare(speed.speed_mps, speed_mps) == 0;
}
@Override
public int hashCode() {
return Objects.hash(speed_mps);
}
@Override
public String toString() {
return "Speed{" +
"speed_mps=" + speed_mps +
'}';
}
}
@@ -45,9 +45,6 @@ public class TrackPoint {
private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1);
// Anything faster than that (in meters per second) will be considered moving.
private static final double MAX_NO_MOVEMENT_SPEED = 0.224;
private TrackPoint.Id id;
private Instant time;
@@ -55,9 +52,9 @@ public class TrackPoint {
private Double longitude;
private Float accuracy;
private Double altitude_m;
private Float speed_mps;
private Speed speed;
private Float bearing;
private Float sensorDistance_m;
private Distance sensorDistance_m;
public enum Type {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
@@ -106,7 +103,7 @@ public class TrackPoint {
this.latitude = location.getLatitude();
this.longitude = location.getLongitude();
this.altitude_m = location.getAltitude();
this.speed_mps = location.getSpeed();
this.speed = Speed.of(location.getSpeed());
this.accuracy = location.getAccuracy();
setTime(Instant.now());
@@ -272,19 +269,19 @@ public class TrackPoint {
}
public boolean hasSpeed() {
return speed_mps != null;
return speed != null;
}
public float getSpeed() {
return speed_mps;
public Speed getSpeed() {
return speed;
}
public void setSpeed(Float speed) {
this.speed_mps = speed;
public void setSpeed(Speed speed) {
this.speed = speed;
}
public boolean isMoving() {
return hasSpeed() && getSpeed() >= MAX_NO_MOVEMENT_SPEED;
return hasSpeed() && getSpeed().isMoving();
}
public boolean hasBearing() {
@@ -311,12 +308,12 @@ public class TrackPoint {
this.accuracy = horizontalAccuracy;
}
public float distanceToPrevious(@NonNull TrackPoint previous) {
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
if (hasSensorDistance()) {
return getSensorDistance();
}
return getLocation().distanceTo(previous.getLocation());
return Distance.of(getLocation().distanceTo(previous.getLocation()));
}
public boolean fulfillsAccuracy(int poorAccuracy) {
@@ -336,11 +333,11 @@ public class TrackPoint {
return sensorDistance_m != null;
}
public Float getSensorDistance() {
public Distance getSensorDistance() {
return sensorDistance_m;
}
public void setSensorDistance(Float distance_m) {
public void setSensorDistance(Distance distance_m) {
this.sensorDistance_m = distance_m;
}
@@ -39,8 +39,10 @@ import java.util.List;
import java.util.UUID;
import de.dennisguse.opentracks.BuildConfig;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.MarkerColumns;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.TrackPointsColumns;
@@ -127,7 +129,7 @@ public class ContentProviderUtils {
trackStatistics.setStopTime(Instant.ofEpochMilli(cursor.getLong(stopTimeIndex)));
}
if (!cursor.isNull(totalDistanceIndex)) {
trackStatistics.setTotalDistance(cursor.getFloat(totalDistanceIndex));
trackStatistics.setTotalDistance(Distance.of(cursor.getFloat(totalDistanceIndex)));
}
if (!cursor.isNull(totalTimeIndex)) {
trackStatistics.setTotalTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
@@ -136,7 +138,7 @@ public class ContentProviderUtils {
trackStatistics.setMovingTime(Duration.ofMillis(cursor.getLong(movingTimeIndex)));
}
if (!cursor.isNull(maxSpeedIndex)) {
trackStatistics.setMaxSpeed(cursor.getFloat(maxSpeedIndex));
trackStatistics.setMaxSpeed(Speed.of(cursor.getFloat(maxSpeedIndex)));
}
if (!cursor.isNull(minAltitudeIndex)) {
trackStatistics.setMinAltitude(cursor.getFloat(minAltitudeIndex));
@@ -278,12 +280,12 @@ public class ContentProviderUtils {
if (trackStatistics.getStopTime() != null) {
values.put(TracksColumns.STOPTIME, trackStatistics.getStopTime().toEpochMilli());
}
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance());
values.put(TracksColumns.TOTALDISTANCE, trackStatistics.getTotalDistance().toM());
values.put(TracksColumns.TOTALTIME, trackStatistics.getTotalTime().toMillis());
values.put(TracksColumns.MOVINGTIME, trackStatistics.getMovingTime().toMillis());
values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed());
values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed());
values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed());
values.put(TracksColumns.AVGSPEED, trackStatistics.getAverageSpeed().toMPS());
values.put(TracksColumns.AVGMOVINGSPEED, trackStatistics.getAverageMovingSpeed().toMPS());
values.put(TracksColumns.MAXSPEED, trackStatistics.getMaxSpeed().toMPS());
values.put(TracksColumns.MIN_ALTITUDE, trackStatistics.getMinAltitude());
values.put(TracksColumns.MAX_ALTITUDE, trackStatistics.getMaxAltitude());
values.put(TracksColumns.ALTITUDE_GAIN, trackStatistics.getTotalAltitudeGain());
@@ -343,7 +345,7 @@ public class ContentProviderUtils {
marker.setIcon(cursor.getString(iconIndex));
}
if (!cursor.isNull(lengthIndex)) {
marker.setLength(cursor.getFloat(lengthIndex));
marker.setLength(Distance.of(cursor.getFloat(lengthIndex)));
}
if (!cursor.isNull(durationIndex)) {
marker.setDuration(Duration.ofMillis(cursor.getLong(durationIndex)));
@@ -480,7 +482,7 @@ public class ContentProviderUtils {
values.put(MarkerColumns.CATEGORY, marker.getCategory());
values.put(MarkerColumns.ICON, marker.getIcon());
values.put(MarkerColumns.TRACKID, marker.getTrackId().getId());
values.put(MarkerColumns.LENGTH, marker.getLength());
values.put(MarkerColumns.LENGTH, marker.getLength().toM());
values.put(MarkerColumns.DURATION, marker.getDuration().toMillis());
values.put(MarkerColumns.LONGITUDE, (int) (marker.getLongitude() * 1E6));
@@ -543,7 +545,7 @@ public class ContentProviderUtils {
trackPoint.setAccuracy(cursor.getFloat(indexes.accuracyIndex));
}
if (!cursor.isNull(indexes.speedIndex)) {
trackPoint.setSpeed(cursor.getFloat(indexes.speedIndex));
trackPoint.setSpeed(Speed.of(cursor.getFloat(indexes.speedIndex)));
}
if (!cursor.isNull(indexes.bearingIndex)) {
trackPoint.setBearing(cursor.getFloat(indexes.bearingIndex));
@@ -556,7 +558,7 @@ public class ContentProviderUtils {
trackPoint.setCyclingCadence_rpm(cursor.getFloat(indexes.sensorCadenceIndex));
}
if (!cursor.isNull(indexes.sensorDistanceIndex)) {
trackPoint.setSensorDistance(cursor.getFloat(indexes.sensorDistanceIndex));
trackPoint.setSensorDistance(Distance.of(cursor.getFloat(indexes.sensorDistanceIndex)));
}
if (!cursor.isNull(indexes.sensorPowerIndex)) {
trackPoint.setPower(cursor.getFloat(indexes.sensorPowerIndex));
@@ -694,7 +696,7 @@ public class ContentProviderUtils {
values.put(TrackPointsColumns.ACCURACY, trackPoint.getAccuracy());
}
if (trackPoint.hasSpeed()) {
values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed());
values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed().toMPS());
}
if (trackPoint.hasBearing()) {
values.put(TrackPointsColumns.BEARING, trackPoint.getBearing());
@@ -707,7 +709,7 @@ public class ContentProviderUtils {
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCyclingCadence_rpm());
}
if (trackPoint.hasSensorDistance()) {
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance());
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance().toM());
}
if (trackPoint.hasPower()) {
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower());
@@ -6,6 +6,10 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import java.time.Duration;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.util.UintUtils;
import de.dennisguse.opentracks.util.UnitConversions;
@@ -122,21 +126,21 @@ public final class SensorDataCycling {
public void compute(DistanceSpeed previous, int wheel_circumference_mm) {
if (hasData() && previous != null && previous.hasData()) {
float timeDiff_ms = UintUtils.diff(wheelRevolutionsTime, previous.wheelRevolutionsTime, UintUtils.UINT16_MAX) / 1024f * UnitConversions.S_TO_MS;
if (timeDiff_ms <= 0) {
Duration timeDiff = Duration.ofMillis((long) timeDiff_ms);
if (timeDiff.isZero() || timeDiff.isNegative()) {
Log.e(TAG, "Timestamps difference is invalid: cannot compute cadence.");
value = null;
} else {
long wheelDiff = UintUtils.diff(wheelRevolutionsCount, previous.wheelRevolutionsCount, UintUtils.UINT16_MAX);
wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as it some of those seem to count backwards
wheelDiff = Math.abs(wheelDiff); //HACK for Garmin Speed 2 as some of those seem to count backwards
double timeDiff_s = timeDiff_ms * UnitConversions.MS_TO_S;
float distance_m = (float) (wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M);
float distance_overall_m = distance_m;
Distance distance = Distance.of(wheelDiff * wheel_circumference_mm * UnitConversions.MM_TO_M);
Distance distanceOverall = distance;
if (previous.hasValue()) {
distance_overall_m += previous.getValue().distance_overall_m;
distanceOverall = distance.plus(previous.getValue().distanceOverall);
}
float speed_mps = (float) (distance_m / timeDiff_s);
value = new Data(distance_m, distance_overall_m, speed_mps);
Speed speed_mps = Speed.of(distance, timeDiff);
value = new Data(distance, distanceOverall, speed_mps);
}
}
}
@@ -144,7 +148,7 @@ public final class SensorDataCycling {
@Override
public void reset() {
if (value != null) {
value = new Data(value.distance_m, 0, value.speed_mps);
value = new Data(value.distance, Distance.of(0), value.speed);
}
}
@@ -167,22 +171,22 @@ public final class SensorDataCycling {
}
public static class Data {
public final float distance_m;
public final float distance_overall_m;
public final float speed_mps;
public final Distance distance;
public final Distance distanceOverall;
public final Speed speed;
private Data(float distance_m, float distance_overall_m, float speed_mps) {
this.distance_m = distance_m;
this.distance_overall_m = distance_overall_m;
this.speed_mps = speed_mps;
private Data(Distance distance, Distance distanceOverall, Speed speed) {
this.distance = distance;
this.distanceOverall = distanceOverall;
this.speed = speed;
}
@Override
public String toString() {
return "Data{" +
"distance_m=" + distance_m +
", distance_overall_m=" + distance_overall_m +
", speed_mps=" + speed_mps +
"distance_m=" + distance +
", distance_overall_m=" + distanceOverall +
", speed_mps=" + speed +
'}';
}
}
@@ -58,8 +58,8 @@ public final class SensorDataSet {
}
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasValue()) {
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distance_overall_m);
trackPoint.setSpeed(cyclingDistanceSpeed.getValue().speed_mps);
trackPoint.setSensorDistance(cyclingDistanceSpeed.getValue().distanceOverall);
trackPoint.setSpeed(cyclingDistanceSpeed.getValue().speed);
}
if (cyclingPower != null && cyclingPower.hasValue()) {
@@ -37,6 +37,7 @@ import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.chart.ChartView;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -66,7 +67,7 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
private SharedPreferences sharedPreferences;
private int recordingDistanceInterval;
private Distance recordingDistanceInterval;
private TrackDataHub trackDataHub;
@@ -346,4 +347,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
void setChartByDistance(boolean value) {
chartByDistance = value;
}
@VisibleForTesting
void setRecordingDistanceInterval(Distance value) {
recordingDistanceInterval = value;
}
}
@@ -180,8 +180,7 @@ public class StatisticsRecordedFragment extends Fragment {
private void updateUI() {
// Set total distance
{
double totalDistance = trackStatistics.getTotalDistance();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), trackStatistics.getTotalDistance(), preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
@@ -201,32 +200,27 @@ public class StatisticsRecordedFragment extends Fragment {
// Set average speed/pace
{
double speed = trackStatistics.getAverageSpeed();
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getAverageSpeed(), preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsAverageSpeedValue.setText(parts.first);
viewBinding.statsAverageSpeedUnit.setText(parts.second);
}
// Set max speed/pace
{
double speed = trackStatistics.getMaxSpeed();
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getMaxSpeed(), preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMaxSpeedValue.setText(parts.first);
viewBinding.statsMaxSpeedUnit.setText(parts.second);
}
// Set moving speed/pace
{
double speed = trackStatistics.getAverageMovingSpeed();
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getAverageMovingSpeed(), preferenceMetricUnits, preferenceReportSpeed);
viewBinding.statsMovingSpeedValue.setText(parts.first);
viewBinding.statsMovingSpeedUnit.setText(parts.second);
}
@@ -24,7 +24,9 @@ import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.adapters.SensorsAdapter;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.sensor.SensorDataCycling;
@@ -371,18 +373,18 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
SensorDataCycling.DistanceSpeed data = sensorDataSet.getCyclingDistanceSpeed();
if (data.hasValue() && data.isRecent()) {
setTotalDistance(data.getValue().distance_overall_m);
setSpeed(data.getValue().speed_mps);
setTotalDistance(data.getValue().distanceOverall);
setSpeed(data.getValue().speed);
}
if (data.hasValue() && data.isRecent()) {
setSpeed(data.getValue().speed_mps);
setSpeed(data.getValue().speed);
}
}
}
private void updateStats() {
setTotalDistance(0);
setTotalDistance(Distance.of(0)); //TODO Why?
// Set activity type
{
@@ -399,7 +401,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set average speed/pace
{
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : Double.NaN;
Speed speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageSpeed() : null;
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
@@ -409,7 +411,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set max speed/pace
{
double speed = lastTrackStatistics == null ? Double.NaN : lastTrackStatistics.getMaxSpeed();
Speed speed = lastTrackStatistics != null ? lastTrackStatistics.getMaxSpeed() : null;
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
@@ -420,7 +422,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
// Set moving speed/pace
{
double speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : Double.NaN;
Speed speed = lastTrackStatistics != null ? lastTrackStatistics.getAverageMovingSpeed() : null;
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
@@ -449,7 +451,7 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
private void setLocationValues() {
// Set speed/pace
double speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : Double.NaN;
Speed speed = lastTrackPoint != null && lastTrackPoint.hasSpeed() ? lastTrackPoint.getSpeed() : null;
setSpeed(speed);
// Set altitude
@@ -477,15 +479,15 @@ public class StatisticsRecordingFragment extends Fragment implements TrackDataLi
}
}
private void setTotalDistance(double sensorDistanceSinceLastTrackpoint) {
double totalDistance = lastTrackStatistics != null ? (lastTrackStatistics.getTotalDistance() + sensorDistanceSinceLastTrackpoint) : Double.NaN;
private void setTotalDistance(Distance sensorDistanceSinceLastTrackpoint) {
Distance totalDistance = lastTrackStatistics != null ? (lastTrackStatistics.getTotalDistance().plus(sensorDistanceSinceLastTrackpoint)) : Distance.invalid();
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), totalDistance, preferenceMetricUnits);
viewBinding.statsDistanceValue.setText(parts.first);
viewBinding.statsDistanceUnit.setText(parts.second);
}
private void setSpeed(double speed) {
private void setSpeed(Speed speed) {
viewBinding.statsSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_speed : R.string.stats_pace);
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), speed, preferenceMetricUnits, preferenceReportSpeed);
@@ -290,7 +290,7 @@ public class GPXTrackExporter implements TrackExporter {
printWriter.println("<extensions><gpxtpx:TrackPointExtension>");
if (trackPoint.hasSpeed()) {
printWriter.println("<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed()) + "</gpxtpx:speed>");
printWriter.println("<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed().toMPS()) + "</gpxtpx:speed>");
}
if (trackPoint.hasHeartRate()) {
@@ -395,10 +395,10 @@ public class KMLTrackExporter implements TrackExporter {
} else {
printWriter.println("<gx:coord/>");
}
speedList.add(trackPoint.hasSpeed() ? trackPoint.getSpeed() : null);
speedList.add(trackPoint.hasSpeed() ? (float) trackPoint.getSpeed().toMPS() : null);
if (exportSensorData) {
distanceList.add(trackPoint.hasSensorDistance() ? trackPoint.getSensorDistance() : null);
distanceList.add(trackPoint.hasSensorDistance() ? (float) trackPoint.getSensorDistance().toM() : null);
heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate_bpm() : null);
cadenceList.add(trackPoint.hasCyclingCadence() ? trackPoint.getCyclingCadence_rpm() : null);
powerList.add(trackPoint.hasPower() ? trackPoint.getPower() : null);
@@ -42,11 +42,14 @@ import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.provider.TrackPointIterator;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.LocationUtils;
@@ -65,7 +68,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
private final Context context;
private final ContentProviderUtils contentProviderUtils;
private final int recordingDistanceInterval;
private final Distance recordingDistanceInterval;
private final List<Track.Id> trackIds = new ArrayList<>();
private final List<Marker> markers = new ArrayList<>();
@@ -202,11 +205,10 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (trackPoint.getLatitude() == marker.getLatitude() && trackPoint.getLongitude() == marker.getLongitude()) {
String markerDescription = marker.getDescription();
String icon = context.getString(R.string.marker_icon_url);
double length = trackStatisticsUpdater.getTrackStatistics().getTotalDistance();
Duration duration = trackStatisticsUpdater.getTrackStatistics().getTotalTime();
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
// Insert marker
Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), length, duration, trackPoint, marker.getPhotoUrl());
Marker newMarker = new Marker(marker.getName(), markerDescription, marker.getCategory(), icon, track.getId(), stats, trackPoint, marker.getPhotoUrl());
contentProviderUtils.insertMarker(newMarker);
}
@@ -374,17 +376,16 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
* GPS points tend to have some inherent imprecision, speed and bearing will likely be off, so the statistics for things like max speed will also be off.
*/
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
float speed = trackData.lastLocationInCurrentSegment.distanceToPrevious(trackPoint) / timeDifference.toMillis();
trackPoint.setSpeed(speed);
trackPoint.setSpeed(Speed.of(trackData.lastLocationInCurrentSegment.distanceToPrevious(trackPoint), timeDifference));
}
}
}
if (trackPoint.hasLocation() && trackData.lastLocationInCurrentSegment.hasLocation()) {
trackPoint.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(trackPoint));
long maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); //TODO Should only be read once!
double distanceToLastTrackLocation = trackPoint.distanceToPrevious(trackData.lastLocationInCurrentSegment);
if (distanceToLastTrackLocation > maxRecordingDistance) {
Distance maxRecordingDistance = PreferencesUtils.getMaxRecordingDistance(sharedPreferences, context); //TODO Should only be read once!
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(trackData.lastLocationInCurrentSegment);
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
}
}
@@ -460,7 +461,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
if (speed != null) {
try {
trackPoint.setSpeed(Float.parseFloat(speed));
trackPoint.setSpeed(Speed.of(speed));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse speed: %s", speed)), e);
}
@@ -505,7 +506,7 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
}
if (distance != null) {
try {
trackPoint.setSensorDistance(Float.parseFloat(distance));
trackPoint.setSensorDistance(Distance.of(distance));
} catch (Exception e) {
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", distance)), e);
}
@@ -26,6 +26,8 @@ import org.xml.sax.SAXException;
import java.util.ArrayList;
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.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
@@ -231,11 +233,11 @@ public class KmlFileTrackImporter extends AbstractFileTrackImporter {
for (int i = 0; i < trackPoints.size(); i++) {
TrackPoint trackPoint = trackPoints.get(i);
if (i < speedList.size()) {
trackPoint.setSpeed(speedList.get(i));
if (i < speedList.size() && speedList.get(i) != null) {
trackPoint.setSpeed(Speed.of(speedList.get(i)));
}
if (i < distanceList.size()) {
trackPoint.setSensorDistance(distanceList.get(i));
if (i < distanceList.size() && distanceList.get(i) != null) {
trackPoint.setSensorDistance(Distance.of(distanceList.get(i)));
}
if (i < heartRateList.size()) {
trackPoint.setHeartRate_bpm(heartRateList.get(i));
@@ -40,6 +40,7 @@ import java.time.Instant;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.TrackListActivity;
import de.dennisguse.opentracks.TrackRecordingActivity;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Track;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -80,8 +81,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
private SharedPreferences sharedPreferences;
private int recordingDistanceInterval;
private int maxRecordingDistance;
private Distance recordingDistanceInterval;
private Distance maxRecordingDistance;
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
@@ -237,11 +238,9 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
photoUrl = photoUrl != null ? photoUrl : "";
TrackStatistics stats = trackStatisticsUpdater.getTrackStatistics();
double length = stats.getTotalDistance();
Duration duration = stats.getTotalTime();
// Insert marker
Marker marker = new Marker(name, description, category, icon, serviceStatus.getRecordingTrackId(), length, duration, trackPoint, photoUrl);
Marker marker = new Marker(name, description, category, icon, serviceStatus.getRecordingTrackId(), stats, trackPoint, photoUrl);
Uri uri = contentProviderUtils.insertMarker(marker);
return new Marker.Id(ContentUris.parseId(uri));
}
@@ -511,8 +510,8 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
double distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastValidTrackPoint);
if (distanceToLastTrackLocation > maxRecordingDistance) {
Distance distanceToLastTrackLocation = trackPoint.distanceToPrevious(lastValidTrackPoint);
if (distanceToLastTrackLocation.greaterThan(maxRecordingDistance)) {
insertTrackPointIfNewer(track, lastTrackPoint);
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
@@ -523,7 +522,7 @@ public class TrackRecordingService extends Service implements HandlerServer.Hand
return;
}
if (trackPoint.hasSensorData() || distanceToLastTrackLocation >= recordingDistanceInterval) {
if (trackPoint.hasSensorData() || distanceToLastTrackLocation.greaterOrEqualThan(recordingDistanceInterval)) {
insertTrackPointIfNewer(track, lastTrackPoint);
insertTrackPoint(track, trackPoint);
@@ -11,6 +11,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.StringUtils;
@@ -68,7 +69,7 @@ class TrackRecordingServiceNotificationManager {
void updateTrackPoint(Context context, TrackStatistics trackStatistics, TrackPoint trackPoint, int recordingGpsAccuracy) {
String formattedAccuracy = context.getString(R.string.value_none);
if (trackPoint.hasAccuracy()) {
formattedAccuracy = StringUtils.formatDistance(context, trackPoint.getAccuracy(), metricUnits);
formattedAccuracy = StringUtils.formatDistance(context, Distance.of(trackPoint.getAccuracy()), metricUnits);
boolean currentLocationWasAccurate = trackPoint.getAccuracy() < recordingGpsAccuracy;
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
@@ -25,7 +25,7 @@ class GpsStatus {
private static final Duration SIGNAL_LOST_THRESHOLD = Duration.ofSeconds(10);
// Threshold for accuracy.
private double signalBadThreshold;
private double signalBadThreshold; //TODO Distance?
// Threshold for time without points.
private Duration signalLostThreshold;
@@ -67,7 +67,7 @@ class GpsStatus {
this.context = context;
SharedPreferences sharedPreferences = PreferencesUtils.getSharedPreferences(context);
signalBadThreshold = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context);
signalBadThreshold = PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context).toM();
Duration minRecordingInterval = Duration.ofSeconds(PreferencesUtils.getMinRecordingInterval(sharedPreferences, context));
signalLostThreshold = !minRecordingInterval.isNegative() ? SIGNAL_LOST_THRESHOLD.plus(minRecordingInterval) : SIGNAL_LOST_THRESHOLD;
@@ -78,7 +78,7 @@ class LocationHandler implements HandlerServer.Handler, LocationListener, GpsSta
}
if (PreferencesUtils.isKey(context, R.string.recording_distance_interval_key, key)) {
if (gpsStatus != null) {
gpsStatus.onRecordingDistanceChanged(PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context));
gpsStatus.onRecordingDistanceChanged((int) PreferencesUtils.getRecordingDistanceInterval(sharedPreferences, context).toM()); //TODO Use Distance?
}
}
}
@@ -20,9 +20,9 @@ import android.util.Log;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.util.UnitConversions;
/**
* Execute a periodic task on a time or distance schedule.
@@ -128,12 +128,9 @@ public class PeriodicTaskExecutor {
return;
}
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
Distance distance = trackStatistics.getTotalDistance();
if (distance > nextTaskDistance) {
if (distance.greaterThan(Distance.of(nextTaskDistance))) {
periodicTask.run(trackRecordingService);
calculateNextTaskDistance();
}
@@ -178,10 +175,7 @@ public class PeriodicTaskExecutor {
return;
}
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
double distance = trackStatistics.getTotalDistance().to(metricUnits);
// The index will be negative since the frequency is negative.
int index = (int) (distance / taskFrequency);
@@ -32,7 +32,7 @@ final class PreferenceHelper {
String[] entryValues = context.getResources().getStringArray(R.array.recording_distance_interval_values);
String[] entries = new String[entryValues.length];
final int recordingDistanceIntervalDefault = PreferencesUtils.getRecordingDistanceIntervalDefault(context);
final int recordingDistanceIntervalDefault = (int) PreferencesUtils.getRecordingDistanceIntervalDefault(context).toM();
for (int i = 0; i < entryValues.length; i++) {
int value = Integer.parseInt(entryValues[i]);
@@ -22,6 +22,9 @@ import androidx.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
/**
* Statistical data about a {@link de.dennisguse.opentracks.content.data.Track}.
* The data in this class should be filled out by {@link TrackStatisticsUpdater}.
@@ -40,13 +43,13 @@ public class TrackStatistics {
// The track stop time.
private Instant stopTime;
private double totalDistance_m;
private Distance totalDistance;
// Updated when new points are received, may be stale.
private Duration totalTime;
// Based on when we believe the user is traveling.
private Duration movingTime;
// The maximum speed (meters/second) that we believe is valid.
private double maxSpeed_mps;
private Speed maxSpeed;
private Float totalAltitudeGain_m = null;
private Float totalAltitudeLoss_m = null;
@@ -62,10 +65,10 @@ public class TrackStatistics {
public TrackStatistics(TrackStatistics other) {
startTime = other.startTime;
stopTime = other.stopTime;
totalDistance_m = other.totalDistance_m;
totalDistance = other.totalDistance;
totalTime = other.totalTime;
movingTime = other.movingTime;
maxSpeed_mps = other.maxSpeed_mps;
maxSpeed = other.maxSpeed;
altitudeExtremities.set(other.altitudeExtremities.getMin(), other.altitudeExtremities.getMax());
totalAltitudeGain_m = other.totalAltitudeGain_m;
totalAltitudeLoss_m = other.totalAltitudeLoss_m;
@@ -89,10 +92,10 @@ public class TrackStatistics {
stopTime = stopTime.isAfter(other.stopTime) ? stopTime : other.stopTime;
}
totalDistance_m += other.totalDistance_m;
totalDistance = totalDistance.plus(other.totalDistance);
totalTime = totalTime.plus(other.totalTime);
movingTime = movingTime.plus(other.movingTime);
maxSpeed_mps = Math.max(maxSpeed_mps, other.maxSpeed_mps);
maxSpeed = Speed.max(maxSpeed, other.maxSpeed);
if (other.altitudeExtremities.hasData()) {
altitudeExtremities.update(other.altitudeExtremities.getMin());
altitudeExtremities.update(other.altitudeExtremities.getMax());
@@ -121,10 +124,10 @@ public class TrackStatistics {
startTime = null;
stopTime = null;
setTotalDistance(0);
setTotalDistance(Distance.of(0));
setTotalTime(Duration.ofSeconds(0));
setMovingTime(Duration.ofSeconds(0));
setMaxSpeed(0);
setMaxSpeed(Speed.zero());
setTotalAltitudeGain(null);
setTotalAltitudeLoss(null);
}
@@ -157,16 +160,16 @@ public class TrackStatistics {
this.stopTime = stopTime;
}
public double getTotalDistance() {
return totalDistance_m;
public Distance getTotalDistance() {
return totalDistance;
}
public void setTotalDistance(double totalDistance_m) {
this.totalDistance_m = totalDistance_m;
public void setTotalDistance(Distance totalDistance_m) {
this.totalDistance = totalDistance_m;
}
public void addTotalDistance(double distance_m) {
totalDistance_m += distance_m;
public void addTotalDistance(Distance distance_m) {
totalDistance = totalDistance.plus(distance_m);
}
/**
@@ -195,35 +198,26 @@ public class TrackStatistics {
}
/**
* Gets the average speed in meters/second.
* Gets the average speed.
* This calculation only takes into account the displacement until the last point that was accounted for in statistics.
*/
public double getAverageSpeed() {
if (totalTime.isZero()) {
return 0.0;
public Speed getAverageSpeed() {
if (totalDistance.isZero() && totalDistance.isZero()) {
return Speed.of(0);
}
return totalDistance_m / (double) totalTime.getSeconds();
return Speed.of(totalDistance.toM() / totalTime.getSeconds());
}
/**
* Gets the average moving speed in meters/second.
*/
public double getAverageMovingSpeed() {
if (movingTime.isZero()) {
return 0.0;
}
return totalDistance_m / (double) movingTime.getSeconds();
public Speed getAverageMovingSpeed() {
return Speed.of(totalDistance, movingTime);
}
/**
* Gets the maximum speed in meters/second.
*/
public double getMaxSpeed() {
return Math.max(maxSpeed_mps, getAverageMovingSpeed());
public Speed getMaxSpeed() {
return Speed.max(maxSpeed, getAverageMovingSpeed());
}
public void setMaxSpeed(double maxSpeed_mps) {
this.maxSpeed_mps = maxSpeed_mps;
public void setMaxSpeed(Speed maxSpeed) {
this.maxSpeed = maxSpeed;
}
public boolean hasAltitudeMin() {
@@ -23,8 +23,9 @@ import androidx.annotation.VisibleForTesting;
import java.time.Duration;
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.provider.TrackPointIterator;
/**
* Updater for {@link TrackStatistics}.
@@ -103,12 +104,12 @@ public class TrackStatisticsUpdater {
* @param trackPoint the trackPoint
* @param minGPSDistance the min recording distance
*/
public void addTrackPoint(TrackPoint trackPoint, int minGPSDistance) {
public void addTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
internalAddTrackPoint(trackPoint, minGPSDistance);
Log.d(TAG, this.toString());
}
private void internalAddTrackPoint(TrackPoint trackPoint, int minGPSDistance) {
private void internalAddTrackPoint(TrackPoint trackPoint, Distance minGPSDistance) {
if (!trackInitialized) {
trackStatistics.setStartTime(trackPoint.getTime());
trackInitialized = true;
@@ -158,8 +159,8 @@ public class TrackStatisticsUpdater {
if (!trackPoint.hasSensorDistance()) {
// GPS-based distance/speed
float movingDistance = lastMovingTrackPoint.distanceToPrevious(trackPoint);
if (movingDistance < minGPSDistance && !trackPoint.isMoving()) {
Distance movingDistance = lastMovingTrackPoint.distanceToPrevious(trackPoint);
if (movingDistance.lessThan(minGPSDistance) && !trackPoint.isMoving()) {
speedBuffer_mps.reset();
lastTrackPoint = trackPoint;
return;
@@ -196,13 +197,6 @@ public class TrackStatisticsUpdater {
speedBuffer_mps.reset();
}
public void addTrackPoint(TrackPointIterator iterator, int minRecordingDistance) {
while (iterator.hasNext()) {
TrackPoint location = iterator.next();
addTrackPoint(location, minRecordingDistance);
}
}
/**
* Gets the smoothed altitude over several readings.
* The altitude readings is noisy so the smoothed altitude is better than the raw altitude for many tasks.
@@ -211,8 +205,8 @@ public class TrackStatisticsUpdater {
return altitudeBuffer_m.getAverage();
}
public double getSmoothedSpeed() {
return speedBuffer_mps.getAverage();
public Speed getSmoothedSpeed() {
return Speed.of(speedBuffer_mps.getAverage());
}
/**
@@ -223,9 +217,10 @@ public class TrackStatisticsUpdater {
if (!trackPoint.isMoving()) {
speedBuffer_mps.reset();
} else if (isValidSpeed(trackPoint, lastTrackPoint)) {
speedBuffer_mps.setNext(trackPoint.getSpeed());
if (speedBuffer_mps.getAverage() > currentSegment.getMaxSpeed()) {
currentSegment.setMaxSpeed(speedBuffer_mps.getAverage());
speedBuffer_mps.setNext(trackPoint.getSpeed().toMPS());
Speed average = Speed.of(speedBuffer_mps.getAverage());
if (average.greaterThan(currentSegment.getMaxSpeed())) {
currentSegment.setMaxSpeed(average);
}
} else {
Log.d(TAG, "Invalid speed. speed: " + trackPoint.getSpeed() + " lastLocationSpeed: " + lastTrackPoint.getSpeed());
@@ -243,27 +238,26 @@ public class TrackStatisticsUpdater {
private boolean isValidSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
// There are a lot of noisy speed readings. Do the cheapest checks first, most expensive last.
if (trackPoint.getSpeed() == 0) {
if (trackPoint.getSpeed().isZero()) {
return false;
}
// The following code will ignore unlikely readings. 128 m/s seems to be an internal android error code.
if (Math.abs(trackPoint.getSpeed() - 128) < 1) {
return false;
}
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
Duration timeDifference = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
double speedDifference = Math.abs(lastTrackPoint.getSpeed() - trackPoint.getSpeed());
if (speedDifference > MAX_ACCELERATION * timeDifference.toMillis()) {
return false;
Speed maxAcceleration = Speed.of(MAX_ACCELERATION * timeDifference.toMillis());
{
// See if the speed seems physically likely. Ignore any speeds that imply acceleration greater than 2g.
Speed speedDifference = Speed.absDiff(lastTrackPoint.getSpeed(), trackPoint.getSpeed());
if (speedDifference.greaterThan(maxAcceleration)) {
return false;
}
}
// Only check if the speed buffer is full. Check that the speed is less than 10X the smoothed average and the speed difference doesn't imply 2g acceleration.
if (speedBuffer_mps.isFull()) {
double average = speedBuffer_mps.getAverage();
double diff = Math.abs(average - trackPoint.getSpeed());
return (trackPoint.getSpeed() < average * 10) && (diff < MAX_ACCELERATION * timeDifference.toMillis());
Speed average = Speed.of(speedBuffer_mps.getAverage());
Speed speedDifference = Speed.absDiff(average, trackPoint.getSpeed());
return trackPoint.getSpeed().lessThan(average.mul(10)) && speedDifference.lessThan(maxAcceleration);
}
return true;
@@ -7,6 +7,8 @@ import androidx.annotation.Nullable;
import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.viewmodels.IntervalStatistics;
@@ -16,17 +18,15 @@ public class AnnouncementUtils {
}
public static String getAnnouncement(Context context, TrackStatistics trackStatistics, boolean isMetricUnits, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval) {
double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
double distancePerTime = trackStatistics.getAverageMovingSpeed() * UnitConversions.MPS_TO_KMH;
double currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed_ms() * UnitConversions.MPS_TO_KMH : 0;
Distance distance = trackStatistics.getTotalDistance();
Speed distancePerTime = trackStatistics.getAverageMovingSpeed();
double currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed_ms() * UnitConversions.MPS_TO_KMH : 0; //TODO Use Speed?
if (distance == 0) {
if (distance.isZero()) {
return context.getString(R.string.voice_total_distance_zero);
}
if (!isMetricUnits) {
distance *= UnitConversions.KM_TO_MI;
distancePerTime *= UnitConversions.KM_TO_MI;
currentDistancePerTime *= UnitConversions.KM_TO_MI;
}
@@ -35,24 +35,27 @@ public class AnnouncementUtils {
String currentRateMsg;
if (isReportSpeed) {
int speedId = isMetricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour;
rate = context.getResources().getQuantityString(speedId, getQuantityCount(distancePerTime), distancePerTime);
double distanceInUnit = distancePerTime.to(isMetricUnits);
rate = context.getResources().getQuantityString(speedId, getQuantityCount(distanceInUnit), distanceInUnit);
currentRate = context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTime), currentDistancePerTime);
currentRateMsg = context.getString(R.string.voice_speed_lap, currentRate);
} else {
double timePerDistance = distancePerTime == 0 ? 0.0 : 1 / distancePerTime;
double timePerDistance = distancePerTime.isZero() ? 0.0 : 1 / distancePerTime.to(isMetricUnits); //TODO PACE
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));
double currentTimePerDistance = currentDistancePerTime == 0 ? 0.0 : 1 / currentDistancePerTime;
double currentTimePerDistance = currentDistancePerTime == 0 ? 0.0 : 1 / currentDistancePerTime; //TODO PACE
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));
currentRateMsg = context.getString(R.string.voice_pace_lap, currentRate);
}
int totalDistanceId = isMetricUnits ? R.plurals.voiceTotalDistanceKilometers : R.plurals.voiceTotalDistanceMiles;
String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
double distanceInUnit = distance.to(isMetricUnits);
String totalDistance = context.getResources().getQuantityString(totalDistanceId, getQuantityCount(distanceInUnit), distanceInUnit);
currentRateMsg = currentInterval == null ? "" : " " + currentRateMsg;
@@ -27,6 +27,7 @@ import androidx.documentfile.provider.DocumentFile;
import androidx.preference.PreferenceManager;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.io.file.TrackFileFormat;
/**
@@ -180,17 +181,21 @@ public class PreferencesUtils {
return getInt(sharedPreferences, context, R.string.voice_frequency_key, VOICE_FREQUENCY_DEFAULT);
}
public static int getRecordingDistanceInterval(SharedPreferences sharedPreferences, Context context) {
return getInt(sharedPreferences, context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefault(context));
public static Distance getRecordingDistanceInterval(SharedPreferences sharedPreferences, Context context) {
return Distance.of(getInt(sharedPreferences, context, R.string.recording_distance_interval_key, getRecordingDistanceIntervalDefaultInternal(context)));
}
public static int getRecordingDistanceIntervalDefault(Context context) {
public static Distance getRecordingDistanceIntervalDefault(Context context) {
return Distance.of(getRecordingDistanceIntervalDefaultInternal(context));
}
private static int getRecordingDistanceIntervalDefaultInternal(Context context) {
return Integer.parseInt(context.getResources().getString(R.string.recording_distance_interval_default));
}
public static int getMaxRecordingDistance(SharedPreferences sharedPreferences, Context context) {
public static Distance getMaxRecordingDistance(SharedPreferences sharedPreferences, Context context) {
final int MAX_RECORDING_DISTANCE = Integer.parseInt(context.getResources().getString(R.string.max_recording_distance_default));
return getInt(sharedPreferences, context, R.string.max_recording_distance_key, MAX_RECORDING_DISTANCE);
return Distance.of(getInt(sharedPreferences, context, R.string.max_recording_distance_key, MAX_RECORDING_DISTANCE));
}
//TODO Duration
@@ -36,6 +36,8 @@ import java.time.temporal.TemporalAccessor;
import java.util.Locale;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
/**
* Various string manipulation methods.
@@ -86,50 +88,38 @@ public class StringUtils {
* Formats the distance in meters.
*
* @param context the context
* @param distance_m the distance_m
* @param distance the distance
* @param metricUnits true to use metric units. False to use imperial units
*/
public static String formatDistance(Context context, double distance_m, boolean metricUnits) {
if (Double.isNaN(distance_m) || Double.isInfinite(distance_m)) {
public static String formatDistance(Context context, Distance distance, boolean metricUnits) {
if (distance.isInvalid()) {
return context.getString(R.string.value_unknown);
}
if (metricUnits) {
if (distance_m > 500.0) {
distance_m *= UnitConversions.M_TO_KM;
return context.getString(R.string.value_float_kilometer, distance_m);
if (distance.greaterThan(Distance.of(500))) {
return context.getString(R.string.value_float_kilometer, distance.toKM());
} else {
return context.getString(R.string.value_float_meter, distance_m);
return context.getString(R.string.value_float_meter, distance.toM());
}
} else {
if (distance_m * UnitConversions.M_TO_MI > 0.5) {
distance_m *= UnitConversions.M_TO_MI;
return context.getString(R.string.value_float_mile, distance_m);
if (distance.greaterThan(Distance.ofMile(0.5))) {
return context.getString(R.string.value_float_mile, distance.toMI());
} else {
distance_m *= UnitConversions.M_TO_FT;
return context.getString(R.string.value_float_feet, distance_m);
return context.getString(R.string.value_float_feet, distance.toFT());
}
}
}
public static String formatSpeed(Context context, double speed_mps, boolean metricUnits, boolean reportSpeed) {
if (Double.isNaN(speed_mps) || Double.isInfinite(speed_mps)) {
speed_mps = 0;
}
double speed = speed_mps * UnitConversions.M_TO_KM; //KM p sec
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
public static String formatSpeed(Context context, Speed speed, boolean metricUnits, boolean reportSpeed) {
if (reportSpeed) {
if (metricUnits) {
return context.getString(R.string.value_float_kilometer_hour, speed * UnitConversions.S_TO_HR);
return context.getString(R.string.value_float_kilometer_hour, speed.toKMH());
} else {
return context.getString(R.string.value_float_mile_hour, speed * UnitConversions.S_TO_HR);
return context.getString(R.string.value_float_mile_hour, speed.toMPH());
}
} else {
int pace = speed == 0 ? 0 : (int) Math.round(1 / speed); //sec / [KM | MI]
int pace = (int) speed.toPace(metricUnits).getSeconds();
int minutes = pace / 60;
int seconds = pace % 60;
if (metricUnits) {
@@ -167,45 +157,40 @@ public class StringUtils {
* Get the formatted distance with unit.
*
* @param context the context
* @param distance_m the distance
* @param distance the distance
* @param metricUnits true to use metric unit
* @return the formatted distance (or null) and it's unit as {@link Pair}
*/
public static Pair<String, String> getDistanceParts(Context context, double distance_m, boolean metricUnits) {
if (Double.isNaN(distance_m) || Double.isInfinite(distance_m)) {
public static Pair<String, String> getDistanceParts(Context context, Distance distance, boolean metricUnits) {
if (distance.isInvalid()) {
return new Pair<>(null, context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet));
}
int unitId;
if (metricUnits) {
if (distance_m > 500.0) {
distance_m *= UnitConversions.M_TO_KM;
unitId = R.string.unit_kilometer;
if (distance.greaterThan(Distance.of(500))) {
return new Pair<>(formatDecimal(distance.toKM()), context.getString(R.string.unit_kilometer));
} else {
unitId = R.string.unit_meter;
return new Pair<>(formatDecimal(distance.toM()), context.getString(R.string.unit_meter));
}
} else {
if (distance_m * UnitConversions.M_TO_MI > 0.5) {
distance_m *= UnitConversions.M_TO_MI;
unitId = R.string.unit_mile;
if (distance.greaterThan(Distance.of(0.5 * UnitConversions.M_TO_MI))) {
return new Pair<>(formatDecimal(distance.toMI()), context.getString(R.string.unit_mile));
} else {
distance_m *= UnitConversions.M_TO_FT;
unitId = R.string.unit_feet;
return new Pair<>(formatDecimal(distance.toFT()), context.getString(R.string.unit_feet));
}
}
return new Pair<>(formatDecimal(distance_m), context.getString(unitId));
}
/**
* Gets the formatted speed with unit.
*
* @param context the context
* @param speed_mps the speed
* @param speed the speed
* @param metricUnits true to use metric unit
* @param reportSpeed true to report speed; false for pace
* @return the formatted speed (or null) and it's unit as {@link Pair}
*/
public static Pair<String, String> getSpeedParts(Context context, double speed_mps, boolean metricUnits, boolean reportSpeed) {
public static Pair<String, String> getSpeedParts(Context context, Speed speed, boolean metricUnits, boolean reportSpeed) {
int unitId;
if (metricUnits) {
unitId = reportSpeed ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
@@ -214,21 +199,15 @@ public class StringUtils {
}
String unitString = context.getString(unitId);
if (Double.isNaN(speed_mps) || Double.isInfinite(speed_mps)) {
speed_mps = 0;
}
double speed = speed_mps * UnitConversions.M_TO_KM; //KM p sec
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
if (speed == null) {
speed = Speed.zero();
}
if (reportSpeed) {
return new Pair<>(StringUtils.formatDecimal(speed * UnitConversions.S_TO_HR, 1), unitString);
return new Pair<>(StringUtils.formatDecimal(speed.to(metricUnits), 1), unitString);
}
int pace = speed == 0 ? 0 : (int) Math.round(1 / speed); //sec / [KM | MI]
int pace = (int) speed.toPace(metricUnits).getSeconds();
int minutes = pace / 60;
int seconds = pace % 60;
@@ -49,7 +49,7 @@ public class UnitConversions {
// Distance
// multiplication factor to convert miles to feet
public static final double MI_TO_FT = 5280.0;
private static final double MI_TO_FT = 5280.0;
// multiplication factor to convert feet to miles
public static final double FT_TO_MI = 1 / MI_TO_FT;
// multiplication factor to covert kilometers to meters
@@ -33,7 +33,7 @@ public class IntervalStatistics {
TrackPoint trackPoint = trackPoints.get(i);
if (trackPoint.hasLocation() && prevTrackPoint.hasLocation()) {
interval.distance_m += prevTrackPoint.distanceToPrevious(trackPoint);
interval.distance_m += prevTrackPoint.distanceToPrevious(trackPoint).toM();
interval.time = interval.time.plus(Duration.between(prevTrackPoint.getTime(), trackPoint.getTime()));
interval.gain_m += trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : 0;
interval.loss_m += trackPoint.hasAltitudeLoss() ? trackPoint.getAltitudeLoss() : 0;