diff --git a/src/androidTest/java/de/dennisguse/opentracks/chart/ChartPointTest.java b/src/androidTest/java/de/dennisguse/opentracks/chart/ChartPointTest.java index b24c9d289..818f16a4a 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/chart/ChartPointTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/chart/ChartPointTest.java @@ -1,5 +1,8 @@ package de.dennisguse.opentracks.chart; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; @@ -7,15 +10,13 @@ import org.junit.runner.RunWith; import java.time.Duration; +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; import de.dennisguse.opentracks.fragments.TrackStubUtils; import de.dennisguse.opentracks.stats.TrackStatistics; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - @RunWith(AndroidJUnit4.class) public class ChartPointTest { @@ -26,7 +27,7 @@ public class ChartPointTest { statistics.setTotalTime(Duration.ofSeconds(1000)); // when - ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), 0, false, false); + ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), false, false); // then assertEquals(1000000, (long) point.getTimeOrDistance()); @@ -39,7 +40,7 @@ public class ChartPointTest { statistics.setTotalDistance(Distance.of(1000)); // when - ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), 0, true, true); + ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), true, true); // then assertEquals(1, (long) point.getTimeOrDistance()); @@ -51,7 +52,7 @@ public class ChartPointTest { TrackStatistics statistics = new TrackStatistics(); // when - ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), 50, false, true); + ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, true); // then assertEquals(50, point.getAltitude(), 0.01); @@ -65,7 +66,7 @@ public class ChartPointTest { TrackStatistics statistics = new TrackStatistics(); // when - ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), 50, false, true); + ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, true); // then assertNull(point.getHeartRate()); @@ -84,7 +85,7 @@ public class ChartPointTest { TrackStatistics statistics = new TrackStatistics(); // when - ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), 50, false, true); + ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), Altitude.EGM2008.of(50), false, true); // then assertEquals(100.0, point.getHeartRate(), 0.01); diff --git a/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java b/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java index 2cf00e036..1bc69167b 100644 --- a/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java +++ b/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java @@ -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(); } diff --git a/src/main/java/de/dennisguse/opentracks/chart/ChartView.java b/src/main/java/de/dennisguse/opentracks/chart/ChartView.java index 6e56e6d2d..112c37a58 100644 --- a/src/main/java/de/dennisguse/opentracks/chart/ChartView.java +++ b/src/main/java/de/dennisguse/opentracks/chart/ChartView.java @@ -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 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 diff --git a/src/main/java/de/dennisguse/opentracks/content/TrackDataListener.java b/src/main/java/de/dennisguse/opentracks/content/TrackDataListener.java index e05111a0d..f437fa8a3 100644 --- a/src/main/java/de/dennisguse/opentracks/content/TrackDataListener.java +++ b/src/main/java/de/dennisguse/opentracks/content/TrackDataListener.java @@ -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) { } /** diff --git a/src/main/java/de/dennisguse/opentracks/content/data/Altitude.java b/src/main/java/de/dennisguse/opentracks/content/data/Altitude.java index 156975e24..17d30a4b2 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/Altitude.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/Altitude.java @@ -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); + } } } diff --git a/src/main/java/de/dennisguse/opentracks/fragments/ChartFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/ChartFragment.java index 26bfd9f7c..95f68cd93 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/ChartFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/ChartFragment.java @@ -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); } } diff --git a/src/main/java/de/dennisguse/opentracks/stats/AltitudeRingBuffer.java b/src/main/java/de/dennisguse/opentracks/stats/AltitudeRingBuffer.java new file mode 100644 index 000000000..4a1300fa4 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/stats/AltitudeRingBuffer.java @@ -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 { + + private Altitude firstAltitude; + + AltitudeRingBuffer(int size) { + super(size); + } + + AltitudeRingBuffer(RingBuffer 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); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/stats/DoubleRingBuffer.java b/src/main/java/de/dennisguse/opentracks/stats/RingBuffer.java similarity index 66% rename from src/main/java/de/dennisguse/opentracks/stats/DoubleRingBuffer.java rename to src/main/java/de/dennisguse/opentracks/stats/RingBuffer.java index f127378fb..a4081c528 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/DoubleRingBuffer.java +++ b/src/main/java/de/dennisguse/opentracks/stats/RingBuffer.java @@ -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 { - // The sliding buffer of doubles. - private final double[] buffer; + private final ArrayList 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(size); reset(); } - DoubleRingBuffer(DoubleRingBuffer toCopy) { - this.buffer = Arrays.copyOf(toCopy.buffer, toCopy.buffer.length); + RingBuffer(RingBuffer 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(); diff --git a/src/main/java/de/dennisguse/opentracks/stats/SpeedRingBuffer.java b/src/main/java/de/dennisguse/opentracks/stats/SpeedRingBuffer.java new file mode 100644 index 000000000..bfe6c69d4 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/stats/SpeedRingBuffer.java @@ -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 { + + SpeedRingBuffer(int size) { + super(size); + } + + SpeedRingBuffer(RingBuffer toCopy) { + super(toCopy); + } + + @Nullable + @Override + protected Number from(Speed object) { + return object.toMPS(); + } + + @Override + protected Speed to(double object) { + return Speed.of(object); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java index 54230b298..5fd0e09a4 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java @@ -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() { diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java index 42680476e..b26071fe7 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java @@ -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);