forked from upstream-mirrors/OpenTracks
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
+38
-20
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user