TrackStatistics and ChartView: handle altitude and speed as optional with null as default value.

Then only the relevant part of the y-axis is shown.

Fixes #795.
This commit is contained in:
Dennis Guse
2021-12-29 23:12:49 +01:00
parent 8c20d5583a
commit 07074ad260
11 changed files with 172 additions and 73 deletions
@@ -3,6 +3,7 @@ package de.dennisguse.opentracks.chart;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -14,7 +15,7 @@ public class ChartPoint {
private double timeOrDistance;
//Y-axis
private final double altitude;
private Double altitude;
private Double speed;
private Double pace;
private Double heartRate;
@@ -27,17 +28,21 @@ public class ChartPoint {
this.altitude = altitude;
}
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, double smoothedAltitude_m, boolean chartByDistance, boolean metricUnits) {
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, Altitude smoothedAltitude, boolean chartByDistance, boolean metricUnits) {
if (chartByDistance) {
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(metricUnits);
} else {
timeOrDistance = trackStatistics.getTotalTime().toMillis();
}
altitude = Distance.of(smoothedAltitude_m).toM_FT(metricUnits);
if (smoothedAltitude != null) {
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(metricUnits);
}
speed = smoothedSpeed.to(metricUnits);
pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
if (smoothedSpeed != null) {
speed = smoothedSpeed.to(metricUnits);
pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
}
if (trackPoint.hasHeartRate()) {
heartRate = (double) trackPoint.getHeartRate_bpm();
}
@@ -41,7 +41,6 @@ import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import de.dennisguse.opentracks.MarkerDetailActivity;
@@ -873,9 +872,9 @@ public class ChartView extends View {
boolean drawFirstPoint = false;
path.rewind();
Iterator<ChartPoint> iterator = chartPoints.iterator();
while (iterator.hasNext()) {
ChartPoint point = iterator.next();
Integer finalX = null;
for (ChartPoint point : chartPoints) {
if (!series.isChartPointValid(point)) {
continue;
}
@@ -893,10 +892,12 @@ public class ChartView extends View {
// draw graph
path.lineTo(x, y);
// last point: move to lower right
if (!iterator.hasNext()) {
path.lineTo(x, yCorner);
}
finalX = x;
}
// last point: move to lower right
if (finalX != null) {
path.lineTo(finalX, yCorner);
}
// back to lower left corner
@@ -17,7 +17,9 @@
package de.dennisguse.opentracks.content;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Marker;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.Track;
@@ -49,7 +51,7 @@ public interface TrackDataListener {
*
* @param trackPoint the trackPoint
*/
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, double smoothedAltitude_m) {
default void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, @Nullable Altitude smoothedAltitude_m) {
}
/**
@@ -14,6 +14,8 @@ public abstract class Altitude {
return altitude_m;
}
public abstract Altitude replace(double altitude_m);
public abstract int getLabelId();
public static class WGS84 extends Altitude {
@@ -30,6 +32,11 @@ public abstract class Altitude {
public static Altitude of(double altitude_m) {
return new WGS84(altitude_m);
}
@Override
public Altitude replace(double altitude_m) {
return new WGS84(altitude_m);
}
}
public static class EGM2008 extends Altitude {
@@ -46,6 +53,11 @@ public abstract class Altitude {
public static Altitude of(double altitude_m) {
return new EGM2008(altitude_m);
}
@Override
public Altitude replace(double altitude_m) {
return new EGM2008(altitude_m);
}
}
}
@@ -35,14 +35,15 @@ import de.dennisguse.opentracks.TrackActivityDataHubInterface;
import de.dennisguse.opentracks.chart.ChartPoint;
import de.dennisguse.opentracks.content.TrackDataHub;
import de.dennisguse.opentracks.content.TrackDataListener;
import de.dennisguse.opentracks.content.data.Altitude;
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.databinding.ChartBinding;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.stats.TrackStatistics;
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
* A fragment to display track chart to the user.
@@ -189,9 +190,9 @@ public class ChartFragment extends Fragment implements TrackDataListener {
}
}
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, double smoothedAltitude_m) {
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, Altitude smoothedAltitude) {
if (isResumed()) {
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude_m, chartByDistance, viewBinding.chartView.getMetricUnits());
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude, chartByDistance, viewBinding.chartView.getMetricUnits());
pendingPoints.add(point);
}
}
@@ -0,0 +1,32 @@
package de.dennisguse.opentracks.stats;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.content.data.Altitude;
public class AltitudeRingBuffer extends RingBuffer<Altitude> {
private Altitude firstAltitude;
AltitudeRingBuffer(int size) {
super(size);
}
AltitudeRingBuffer(RingBuffer<Altitude> toCopy) {
super(toCopy);
}
@Nullable
@Override
protected Number from(Altitude object) {
if (firstAltitude == null) {
firstAltitude = object;
}
return object.toM();
}
@Override
protected Altitude to(double object) {
return firstAltitude.replace(object);
}
}
@@ -17,8 +17,9 @@
package de.dennisguse.opentracks.stats;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.ArrayList;
/**
* This class maintains a ring buffer of doubles.
@@ -27,10 +28,9 @@ import java.util.Arrays;
*
* @author Sandor Dornbush
*/
class DoubleRingBuffer {
abstract class RingBuffer<T> {
// The sliding buffer of doubles.
private final double[] buffer;
private final ArrayList<T> buffer;
// The location that the next write will occur at.
private int index;
@@ -43,16 +43,17 @@ class DoubleRingBuffer {
*
* @param size the size
*/
DoubleRingBuffer(int size) {
RingBuffer(int size) {
if (size < 1) {
throw new IllegalArgumentException("The buffer size must be greater than 1.");
}
buffer = new double[size];
buffer = new ArrayList<T>(size);
reset();
}
DoubleRingBuffer(DoubleRingBuffer toCopy) {
this.buffer = Arrays.copyOf(toCopy.buffer, toCopy.buffer.length);
RingBuffer(RingBuffer<T> toCopy) {
this.buffer = new ArrayList<>(toCopy.buffer);
this.index = toCopy.index;
this.isFull = toCopy.isFull;
}
@@ -75,31 +76,48 @@ class DoubleRingBuffer {
/**
* Gets the average of the buffer.
*/
public double getAverage() {
int numberOfEntries = isFull ? buffer.length : index;
public T getAverage() {
int numberOfEntries = isFull ? buffer.size() : index;
if (numberOfEntries == 0) {
return 0;
return null;
}
double sum = 0;
Double sum = null;
int numberOfUsedEntries = 0;
for (int i = 0; i < numberOfEntries; i++) {
sum += buffer[i];
Number value = from(buffer.get(i));
if (value != null) {
if (sum == null) {
sum = 0.0;
}
sum += value.doubleValue();
numberOfUsedEntries++;
}
}
if (sum == null) {
return null;
} else {
return to(sum / numberOfUsedEntries);
}
return sum / numberOfEntries;
}
@Nullable
protected abstract Number from(T object);
protected abstract T to(double object);
/**
* Adds a double to the buffer.
* If the buffer is full the oldest element is overwritten.
*
* @param value the double to add
*/
public void setNext(double value) {
if (index == buffer.length) {
public void setNext(T value) {
if (index == buffer.size()) {
index = 0;
}
buffer[index] = value;
buffer.add(index, value);
index++;
if (index == buffer.length) {
if (index == buffer.size()) {
isFull = true;
}
}
@@ -110,9 +128,9 @@ class DoubleRingBuffer {
StringBuilder builder = new StringBuilder("Full: ");
builder.append(isFull);
builder.append("\n");
for (int i = 0; i < buffer.length; i++) {
for (int i = 0; i < buffer.size(); i++) {
builder.append((i == index) ? "<<" : "[");
builder.append(buffer[i]);
builder.append(buffer.get(i));
builder.append((i == index) ? ">> " : "] ");
}
return builder.toString();
@@ -0,0 +1,27 @@
package de.dennisguse.opentracks.stats;
import androidx.annotation.Nullable;
import de.dennisguse.opentracks.content.data.Speed;
public class SpeedRingBuffer extends RingBuffer<Speed> {
SpeedRingBuffer(int size) {
super(size);
}
SpeedRingBuffer(RingBuffer<Speed> toCopy) {
super(toCopy);
}
@Nullable
@Override
protected Number from(Speed object) {
return object.toMPS();
}
@Override
protected Speed to(double object) {
return Speed.of(object);
}
}
@@ -23,6 +23,7 @@ import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import java.time.Instant;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
@@ -258,8 +259,10 @@ public class TrackStatistics {
altitudeExtremities.setMax(altitude_m);
}
public void updateAltitudeExtremities(double altitude_m) {
altitudeExtremities.update(altitude_m);
public void updateAltitudeExtremities(Altitude altitude) {
if (altitude != null) {
altitudeExtremities.update(altitude.toM());
}
}
public boolean hasTotalAltitudeGain() {
@@ -24,6 +24,7 @@ import androidx.annotation.VisibleForTesting;
import java.time.Duration;
import java.util.List;
import de.dennisguse.opentracks.content.data.Altitude;
import de.dennisguse.opentracks.content.data.Distance;
import de.dennisguse.opentracks.content.data.Speed;
import de.dennisguse.opentracks.content.data.TrackPoint;
@@ -61,8 +62,8 @@ public class TrackStatisticsUpdater {
private final TrackStatistics trackStatistics;
private final DoubleRingBuffer altitudeBuffer_m;
private final DoubleRingBuffer speedBuffer_mps;
private final AltitudeRingBuffer altitudeBuffer;
private final SpeedRingBuffer speedBuffer;
// The current segment's statistics
private final TrackStatistics currentSegment;
@@ -82,16 +83,16 @@ public class TrackStatisticsUpdater {
this.trackStatistics = trackStatistics;
this.currentSegment = new TrackStatistics();
altitudeBuffer_m = new DoubleRingBuffer(ALTITUDE_SMOOTHING_FACTOR);
speedBuffer_mps = new DoubleRingBuffer(SPEED_SMOOTHING_FACTOR);
altitudeBuffer = new AltitudeRingBuffer(ALTITUDE_SMOOTHING_FACTOR);
speedBuffer = new SpeedRingBuffer(SPEED_SMOOTHING_FACTOR);
}
public TrackStatisticsUpdater(TrackStatisticsUpdater toCopy) {
this.currentSegment = new TrackStatistics(toCopy.currentSegment);
this.trackStatistics = new TrackStatistics(toCopy.trackStatistics);
this.altitudeBuffer_m = new DoubleRingBuffer(toCopy.altitudeBuffer_m);
this.speedBuffer_mps = new DoubleRingBuffer(toCopy.speedBuffer_mps);
this.altitudeBuffer = new AltitudeRingBuffer(toCopy.altitudeBuffer);
this.speedBuffer = new SpeedRingBuffer(toCopy.speedBuffer);
this.lastTrackPoint = toCopy.lastTrackPoint;
}
@@ -136,7 +137,11 @@ public class TrackStatisticsUpdater {
//Update absolute (GPS-based) altitude
if (trackPoint.hasAltitude()) {
updateAbsoluteAltitude(trackPoint.getAltitude().toM());
// Update altitude using the smoothed average
altitudeBuffer.setNext(trackPoint.getAltitude());
Altitude newAverage = altitudeBuffer.getAverage();
currentSegment.updateAltitudeExtremities(newAverage);
}
// Update total distance
@@ -164,7 +169,7 @@ public class TrackStatisticsUpdater {
// Update max speed
updateSpeed(trackPoint, lastTrackPoint);
} else {
speedBuffer_mps.reset();
speedBuffer.reset();
}
@@ -183,20 +188,20 @@ public class TrackStatisticsUpdater {
currentSegment.reset(trackPoint.getTime());
lastTrackPoint = null;
altitudeBuffer_m.reset();
speedBuffer_mps.reset();
altitudeBuffer.reset();
speedBuffer.reset();
}
/**
* 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.
*/
public double getSmoothedAltitude() {
return altitudeBuffer_m.getAverage();
public Altitude getSmoothedAltitude() {
return altitudeBuffer.getAverage();
}
public Speed getSmoothedSpeed() {
return Speed.of(speedBuffer_mps.getAverage());
return speedBuffer.getAverage();
}
/**
@@ -205,10 +210,10 @@ public class TrackStatisticsUpdater {
@VisibleForTesting
private void updateSpeed(@NonNull TrackPoint trackPoint, @NonNull TrackPoint lastTrackPoint) {
if (!trackPoint.isMoving()) {
speedBuffer_mps.reset();
speedBuffer.reset();
} else if (isValidSpeed(trackPoint, lastTrackPoint)) {
speedBuffer_mps.setNext(trackPoint.getSpeed().toMPS());
Speed average = Speed.of(speedBuffer_mps.getAverage());
speedBuffer.setNext(trackPoint.getSpeed());
Speed average = speedBuffer.getAverage();
if (average.greaterThan(currentSegment.getMaxSpeed())) {
currentSegment.setMaxSpeed(average);
}
@@ -217,14 +222,6 @@ public class TrackStatisticsUpdater {
}
}
private void updateAbsoluteAltitude(double altitude) {
// Update altitude using the smoothed average
altitudeBuffer_m.setNext(altitude);
double newAverage = altitudeBuffer_m.getAverage();
currentSegment.updateAltitudeExtremities(newAverage);
}
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().isZero()) {
@@ -242,8 +239,8 @@ public class TrackStatisticsUpdater {
}
// 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()) {
Speed average = Speed.of(speedBuffer_mps.getAverage());
if (speedBuffer.isFull()) {
Speed average = speedBuffer.getAverage();
Speed speedDifference = Speed.absDiff(average, trackPoint.getSpeed());
return trackPoint.getSpeed().lessThan(average.mul(10)) && speedDifference.lessThan(maxAcceleration);