diff --git a/src/androidTest/java/de/dennisguse/opentracks/chart/ChartValueSeriesTest.java b/src/androidTest/java/de/dennisguse/opentracks/chart/ChartValueSeriesTest.java index 0f274bbab..6c4a48fa7 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/chart/ChartValueSeriesTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/chart/ChartValueSeriesTest.java @@ -15,6 +15,7 @@ */ package de.dennisguse.opentracks.chart; +import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -44,7 +45,17 @@ public class ChartValueSeriesTest { R.string.description_elevation_metric, R.string.description_elevation_imperial, R.color.chart_elevation_fill, - R.color.chart_elevation_border); + R.color.chart_elevation_border) { + @Override + double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) { + return chartPoint.getElevation(); + } + + @Override + protected boolean drawIfChartPointHasNoData() { + return false; + } + }; } @Test @@ -63,8 +74,8 @@ public class ChartValueSeriesTest { @Test public void testSmallUpdates() { - series.update(0); - series.update(10); + series.update(new ChartPoint(0)); + series.update(new ChartPoint(10)); series.updateDimension(); Assert.assertEquals(100, series.getInterval()); Assert.assertEquals(0, series.getMinMarkerValue()); @@ -73,8 +84,8 @@ public class ChartValueSeriesTest { @Test public void testBigUpdates() { - series.update(0); - series.update(901); + series.update(new ChartPoint(0)); + series.update(new ChartPoint(901)); series.updateDimension(); Assert.assertEquals(1000, series.getInterval()); Assert.assertEquals(0, series.getMinMarkerValue()); @@ -83,8 +94,8 @@ public class ChartValueSeriesTest { @Test public void testNotZeroBasedUpdates() { - series.update(220); - series.update(250); + series.update(new ChartPoint(220)); + series.update(new ChartPoint(250)); series.updateDimension(); Assert.assertEquals(100, series.getInterval()); Assert.assertEquals(200, series.getMinMarkerValue()); diff --git a/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java b/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java index 58499c98c..22c45f79e 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/fragments/ChartFragmentTest.java @@ -30,6 +30,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import de.dennisguse.opentracks.TrackStubUtils; +import de.dennisguse.opentracks.chart.ChartPoint; import de.dennisguse.opentracks.chart.ChartView; import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.sensor.SensorDataSet; @@ -74,10 +75,10 @@ public class ChartFragmentTest { TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint(); // No input. - double[] point = chartFragment.createPendingPoint(trackPoint); - Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01); - Assert.assertEquals(Float.NaN, point[ChartView.CADENCE_SERIES + 1], 0.01); - Assert.assertEquals(Float.NaN, point[ChartView.POWER_SERIES + 1], 0.01); + ChartPoint point = chartFragment.createPendingPoint(trackPoint); + Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01); + Assert.assertEquals(Float.NaN, point.getCadence(), 0.01); + Assert.assertEquals(Float.NaN, point.getPower(), 0.01); // Input incorrect state. // Creates SensorData. @@ -85,9 +86,9 @@ public class ChartFragmentTest { trackPoint.setSensorDataSet(sensorDataSet); // Test. point = chartFragment.createPendingPoint(trackPoint); - Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01); - Assert.assertEquals(Float.NaN, point[ChartView.CADENCE_SERIES + 1], 0.01); - Assert.assertEquals(Float.NaN, point[ChartView.POWER_SERIES + 1], 0.01); + Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01); + Assert.assertEquals(Float.NaN, point.getCadence(), 0.01); + Assert.assertEquals(Float.NaN, point.getPower(), 0.01); } /** @@ -97,10 +98,10 @@ public class ChartFragmentTest { public void testCreatePendingPoint_sensorCorrect() { TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint(); // No input. - double[] point = chartFragment.createPendingPoint(trackPoint); - Assert.assertEquals(Float.NaN, point[ChartView.HEART_RATE_SERIES + 1], 0.01); - Assert.assertEquals(Float.NaN, point[ChartView.CADENCE_SERIES + 1], 0.01); - Assert.assertEquals(Float.NaN, point[ChartView.POWER_SERIES + 1], 0.01); + ChartPoint point = chartFragment.createPendingPoint(trackPoint); + Assert.assertEquals(Float.NaN, point.getHeartRate(), 0.01); + Assert.assertEquals(Float.NaN, point.getCadence(), 0.01); + Assert.assertEquals(Float.NaN, point.getPower(), 0.01); // Creates SensorData. SensorDataSet sensorDataSet = new SensorDataSet(100, 101, 102); @@ -109,9 +110,9 @@ public class ChartFragmentTest { trackPoint.setSensorDataSet(sensorDataSet); // Test. point = chartFragment.createPendingPoint(trackPoint); - Assert.assertEquals(100.0, point[ChartView.HEART_RATE_SERIES + 1], 0.01); - Assert.assertEquals(101.0, point[ChartView.CADENCE_SERIES + 1], 0.01); - Assert.assertEquals(102.0, point[ChartView.POWER_SERIES + 1], 0.01); + Assert.assertEquals(100.0, point.getHeartRate(), 0.01); + Assert.assertEquals(101.0, point.getCadence(), 0.01); + Assert.assertEquals(102.0, point.getPower(), 0.01); } /** @@ -122,13 +123,13 @@ public class ChartFragmentTest { chartFragment.setChartByDistance(true); // Resets last location and writes first location. TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint(); - double[] point = chartFragment.createPendingPoint(trackPoint1); - Assert.assertEquals(0.0, point[0], 0.01); + ChartPoint point = chartFragment.createPendingPoint(trackPoint1); + Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01); // The second is a same location, just different time. TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint(); point = chartFragment.createPendingPoint(trackPoint2); - Assert.assertEquals(0.0, point[0], 0.01); + Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01); // The third location is a new location, and use metric. TrackPoint trackPoint3 = TrackStubUtils.createDefaultTrackPoint(); @@ -140,7 +141,7 @@ public class ChartFragmentTest { Location.distanceBetween(trackPoint2.getLatitude(), trackPoint2.getLongitude(), trackPoint3.getLatitude(), trackPoint3.getLongitude(), results); double distance1 = results[0] * UnitConversions.M_TO_KM; - Assert.assertEquals(distance1, point[0], 0.01); + Assert.assertEquals(distance1, point.getTimeOrDistance(), 0.01); // The fourth location is a new location, and use metric. TrackPoint trackPoint4 = TrackStubUtils.createDefaultTrackPoint(); @@ -151,7 +152,7 @@ public class ChartFragmentTest { Location.distanceBetween(trackPoint3.getLatitude(), trackPoint3.getLongitude(), trackPoint4.getLatitude(), trackPoint4.getLongitude(), results); double distance2 = results[0] * UnitConversions.M_TO_KM; - Assert.assertEquals((distance1 + distance2), point[0], 0.01); + Assert.assertEquals((distance1 + distance2), point.getTimeOrDistance(), 0.01); } /** @@ -166,8 +167,8 @@ public class ChartFragmentTest { // The first is a same location, just different time. TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint(); - double[] point = chartFragment.createPendingPoint(trackPoint1); - Assert.assertEquals(0.0, point[0], 0.01); + ChartPoint point = chartFragment.createPendingPoint(trackPoint1); + Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01); // The second location is a new location, and use imperial. TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint(); @@ -181,7 +182,7 @@ public class ChartFragmentTest { float[] results = new float[4]; Location.distanceBetween(trackPoint1.getLatitude(), trackPoint1.getLongitude(), trackPoint2.getLatitude(), trackPoint2.getLongitude(), results); double distance1 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI; - Assert.assertEquals(distance1, point[0], 0.01); + Assert.assertEquals(distance1, point.getTimeOrDistance(), 0.01); // The third location is a new location, and use imperial. TrackPoint trackPoint3 = TrackStubUtils.createDefaultTrackPoint(); @@ -194,7 +195,7 @@ public class ChartFragmentTest { */ Location.distanceBetween(trackPoint2.getLatitude(), trackPoint2.getLongitude(), trackPoint3.getLatitude(), trackPoint3.getLongitude(), results); double distance2 = results[0] * UnitConversions.M_TO_KM * UnitConversions.KM_TO_MI; - Assert.assertEquals(distance1 + distance2, point[0], 0.01); + Assert.assertEquals(distance1 + distance2, point.getTimeOrDistance(), 0.01); } /** @@ -204,13 +205,13 @@ public class ChartFragmentTest { public void testCreatePendingPoint_time() { chartFragment.setChartByDistance(false); TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint(); - double[] point = chartFragment.createPendingPoint(trackPoint1); - Assert.assertEquals(0.0, point[0], 0.01); + ChartPoint point = chartFragment.createPendingPoint(trackPoint1); + Assert.assertEquals(0.0, point.getTimeOrDistance(), 0.01); long timeSpan = 222; TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint(); trackPoint2.setTime(trackPoint1.getTime() + timeSpan); point = chartFragment.createPendingPoint(trackPoint2); - Assert.assertEquals((double) timeSpan, point[0], 0.01); + Assert.assertEquals((double) timeSpan, point.getTimeOrDistance(), 0.01); } /** @@ -224,8 +225,8 @@ public class ChartFragmentTest { * At first, clear old points of elevation, so give true to the second parameter. * Then only one value INITIAL_ALTITUDE in buffer. */ - double[] point = chartFragment.createPendingPoint(trackPoint1); - Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point[ChartView.ELEVATION_SERIES + 1], 0.01); + ChartPoint point = chartFragment.createPendingPoint(trackPoint1); + Assert.assertEquals(TrackStubUtils.INITIAL_ALTITUDE, point.getElevation(), 0.01); /* * Send another value to buffer, now there are two values, INITIAL_ALTITUDE and INITIAL_ALTITUDE * 2. @@ -233,7 +234,7 @@ public class ChartFragmentTest { TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint(); trackPoint2.setAltitude(TrackStubUtils.INITIAL_ALTITUDE * 2); point = chartFragment.createPendingPoint(trackPoint2); - Assert.assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, point[ChartView.ELEVATION_SERIES + 1], 0.01); + Assert.assertEquals((TrackStubUtils.INITIAL_ALTITUDE + TrackStubUtils.INITIAL_ALTITUDE * 2) / 2.0, point.getElevation(), 0.01); } /** @@ -249,8 +250,8 @@ public class ChartFragmentTest { */ TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint(); trackPoint1.setSpeed(128.5f); - double[] point = chartFragment.createPendingPoint(trackPoint1); - Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01); + ChartPoint point = chartFragment.createPendingPoint(trackPoint1); + Assert.assertEquals(0.0, point.getSpeed(), 0.01); /* * Tests the logic when both metricUnits and reportSpeed are true. @@ -264,7 +265,7 @@ public class ChartFragmentTest { trackPoint2.setTime(trackPoint1.getTime() + 222); trackPoint2.setSpeed(130); point = chartFragment.createPendingPoint(trackPoint2); - Assert.assertEquals(130.0 * UnitConversions.MS_TO_KMH, point[ChartView.SPEED_SERIES + 1], 0.01); + Assert.assertEquals(130.0 * UnitConversions.MS_TO_KMH, point.getSpeed(), 0.01); } /** @@ -277,8 +278,8 @@ public class ChartFragmentTest { // First data point is not added to the speed buffer TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint(); trackPoint1.setSpeed(100.0f); - double[] point = chartFragment.createPendingPoint(trackPoint1); - Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01); + ChartPoint point = chartFragment.createPendingPoint(trackPoint1); + Assert.assertEquals(0.0, point.getSpeed(), 0.01); TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint(); @@ -289,7 +290,7 @@ public class ChartFragmentTest { trackPoint2.setTime(trackPoint2.getTime() + 222); trackPoint2.setSpeed(102); point = chartFragment.createPendingPoint(trackPoint2); - Assert.assertEquals(102.0 * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI, point[ChartView.SPEED_SERIES + 1], 0.01); + Assert.assertEquals(102.0 * UnitConversions.MS_TO_KMH * UnitConversions.KM_TO_MI, point.getSpeed(), 0.01); } /** @@ -302,8 +303,8 @@ public class ChartFragmentTest { // First data point is not added to the speed buffer TrackPoint trackPoint1 = TrackStubUtils.createDefaultTrackPoint(); trackPoint1.setSpeed(100.0f); - double[] point = chartFragment.createPendingPoint(trackPoint1); - Assert.assertEquals(0.0, point[ChartView.SPEED_SERIES + 1], 0.01); + ChartPoint point = chartFragment.createPendingPoint(trackPoint1); + Assert.assertEquals(0.0, point.getSpeed(), 0.01); TrackPoint trackPoint2 = TrackStubUtils.createDefaultTrackPoint(); @@ -314,7 +315,7 @@ public class ChartFragmentTest { trackPoint2.setTime(trackPoint2.getTime() + 222); trackPoint2.setSpeed(102); point = chartFragment.createPendingPoint(trackPoint2); - Assert.assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MS_TO_KMH), point[ChartView.PACE_SERIES + 1], 0.01); + Assert.assertEquals(HOURS_PER_UNIT / (102.0 * UnitConversions.MS_TO_KMH), point.getPace(), 0.01); } /** @@ -325,7 +326,7 @@ public class ChartFragmentTest { chartFragment.setReportSpeed(false); TrackPoint trackPoint = TrackStubUtils.createDefaultTrackPoint(); trackPoint.setSpeed(0); - double[] point = chartFragment.createPendingPoint(trackPoint); - Assert.assertEquals(0.0, point[ChartView.PACE_SERIES + 1], 0.01); + ChartPoint point = chartFragment.createPendingPoint(trackPoint); + Assert.assertEquals(0.0, point.getPace(), 0.01); } } diff --git a/src/main/java/de/dennisguse/opentracks/TrackDetailActivity.java b/src/main/java/de/dennisguse/opentracks/TrackDetailActivity.java index fe8dd2529..aea2eb9fd 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackDetailActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackDetailActivity.java @@ -96,7 +96,7 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA if (recordingTrackId == -1L) { TrackRecordingServiceInterface service = trackRecordingServiceConnection.getServiceIfBound(); if (service == null) { - Log.d(TAG, "it could not get service"); + Log.d(TAG, "could not get TrackRecordingService"); return; } diff --git a/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java b/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java new file mode 100644 index 000000000..940769378 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/chart/ChartPoint.java @@ -0,0 +1,103 @@ +package de.dennisguse.opentracks.chart; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import de.dennisguse.opentracks.content.sensor.SensorDataSet; +import de.dennisguse.opentracks.stats.TrackStatistics; +import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; +import de.dennisguse.opentracks.util.UnitConversions; + +public class ChartPoint { + //X-axis + private double timeOrDistance; + + //Y-axis + private double elevation; + private double speed; + private double pace; + private double heartRate = Double.NaN; + private double cadence = Double.NaN; + private double power = Double.NaN; + + @VisibleForTesting + ChartPoint(double elevation) { + this.elevation = elevation; + } + + public ChartPoint(@NonNull TrackStatisticsUpdater trackStatisticsUpdater, SensorDataSet sensorDataSet, boolean chartByDistance, boolean metricUnits) { + TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics(); + + if (chartByDistance) { + double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM; + if (!metricUnits) { + distance *= UnitConversions.KM_TO_MI; + } + timeOrDistance = distance; + } else { + timeOrDistance = trackStatistics.getTotalTime(); + } + + elevation = trackStatisticsUpdater.getSmoothedElevation(); + if (!metricUnits) { + elevation *= UnitConversions.M_TO_FT; + } + + speed = trackStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MS_TO_KMH; + if (!metricUnits) { + speed *= UnitConversions.KM_TO_MI; + } + pace = speed == 0 ? 0.0 : 60.0 / speed; + if (sensorDataSet != null) { + if (sensorDataSet.hasHeartRate()) { + heartRate = sensorDataSet.getHeartRate(); + } + if (sensorDataSet.hasCadence()) { + cadence = sensorDataSet.getCadence(); + } + if (sensorDataSet.hasPower()) { + power = sensorDataSet.getPower(); + } + } + } + + public double getTimeOrDistance() { + return timeOrDistance; + } + + public double getElevation() { + return elevation; + } + + public double getSpeed() { + return speed; + } + + public double getPace() { + return pace; + } + + public boolean hasHeartRate() { + return Double.isNaN(heartRate); + } + + public double getHeartRate() { + return heartRate; + } + + public boolean hasCadence() { + return Double.isNaN(cadence); + } + + public double getCadence() { + return cadence; + } + + public boolean hasPower() { + return Double.isNaN(power); + } + + public double getPower() { + return power; + } +} diff --git a/src/main/java/de/dennisguse/opentracks/chart/ChartValueSeries.java b/src/main/java/de/dennisguse/opentracks/chart/ChartValueSeries.java index 434acacf9..c65b83ae2 100644 --- a/src/main/java/de/dennisguse/opentracks/chart/ChartValueSeries.java +++ b/src/main/java/de/dennisguse/opentracks/chart/ChartValueSeries.java @@ -23,6 +23,8 @@ import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.graphics.Path; +import androidx.annotation.NonNull; + import java.text.NumberFormat; import de.dennisguse.opentracks.stats.ExtremityMonitor; @@ -32,7 +34,7 @@ import de.dennisguse.opentracks.stats.ExtremityMonitor; * * @author Sandor Dornbush */ -class ChartValueSeries { +abstract class ChartValueSeries { private static final float STROKE_WIDTH = 2f; @@ -126,26 +128,26 @@ class ChartValueSeries { } /** - * Updates the series with a new value. - * - * @param value the new value + * Updates the series with a new {@link ChartPoint}. */ - void update(double value) { - extremityMonitor.update(value); + void update(ChartPoint chartPoint) { + if (isChartPointValid(chartPoint)) { + extremityMonitor.update(extractDataFromChartPoint(chartPoint)); + } } - /** - * Gets the path. - */ + abstract double extractDataFromChartPoint(@NonNull ChartPoint chartPoint); + + boolean isChartPointValid(@NonNull ChartPoint chartPoint) { + return !Double.isNaN(extractDataFromChartPoint(chartPoint)); + } + + protected abstract boolean drawIfChartPointHasNoData(); + Path getPath() { return path; } - /** - * Draws the path on canvas. - * - * @param canvas the canvas - */ void drawPath(Canvas canvas) { canvas.drawPath(path, fillPaint); canvas.drawPath(path, strokePaint); diff --git a/src/main/java/de/dennisguse/opentracks/chart/ChartView.java b/src/main/java/de/dennisguse/opentracks/chart/ChartView.java index 6aac30884..1a047cb91 100644 --- a/src/main/java/de/dennisguse/opentracks/chart/ChartView.java +++ b/src/main/java/de/dennisguse/opentracks/chart/ChartView.java @@ -33,6 +33,7 @@ import android.view.ScaleGestureDetector; import android.view.View; import android.widget.Scroller; +import androidx.annotation.NonNull; import androidx.core.view.GestureDetectorCompat; import java.text.NumberFormat; @@ -62,14 +63,6 @@ public class ChartView extends View { static final int Y_AXIS_INTERVALS = 5; - public static final int NUM_SERIES = 6; - public static final int ELEVATION_SERIES = 0; - public static final int SPEED_SERIES = 1; - public static final int PACE_SERIES = 2; - public static final int HEART_RATE_SERIES = 3; - public static final int CADENCE_SERIES = 4; - public static final int POWER_SERIES = 5; - private static final int TARGET_X_AXIS_INTERVALS = 4; private static final int MIN_ZOOM_LEVEL = 1; @@ -89,8 +82,11 @@ public class ChartView extends View { X_FRACTION_FORMAT.setMinimumFractionDigits(1); } - private final ChartValueSeries[] series = new ChartValueSeries[NUM_SERIES]; - private final List chartData = new ArrayList<>(); + private final List seriesList = new ArrayList<>(); + private final ChartValueSeries speedSeries; + private final ChartValueSeries paceSeries; + + private final List chartData = new ArrayList<>(); private final List waypoints = new ArrayList<>(); private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor(); private final Paint axisPaint; @@ -192,58 +188,124 @@ public class ChartView extends View { } }); - public ChartView(Context context, boolean chartByDistance) { + public ChartView(Context context, final boolean chartByDistance) { super(context); this.chartByDistance = chartByDistance; - series[ELEVATION_SERIES] = new ChartValueSeries(context, + seriesList.add(new ChartValueSeries(context, Integer.MIN_VALUE, Integer.MAX_VALUE, new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000}, R.string.description_elevation_metric, R.string.description_elevation_imperial, R.color.chart_elevation_fill, - R.color.chart_elevation_border); - series[SPEED_SERIES] = new ChartValueSeries(context, + R.color.chart_elevation_border) { + @Override + protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) { + return chartPoint.getElevation(); + } + + @Override + protected boolean drawIfChartPointHasNoData() { + return true; + } + }); + + speedSeries = new ChartValueSeries(context, 0, Integer.MAX_VALUE, new int[]{1, 5, 10, 20, 50, 100}, R.string.description_speed_metric, R.string.description_speed_imperial, R.color.chart_speed_fill, - R.color.chart_speed_border); - series[PACE_SERIES] = new ChartValueSeries(context, + R.color.chart_speed_border) { + @Override + protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) { + return chartPoint.getSpeed(); + } + + @Override + protected boolean drawIfChartPointHasNoData() { + return reportSpeed; + } + }; + seriesList.add(speedSeries); + + paceSeries = new ChartValueSeries(context, 0, Integer.MAX_VALUE, new int[]{1, 2, 5, 10, 15, 20, 30, 60, 120}, R.string.description_pace_metric, R.string.description_pace_imperial, R.color.chart_pace_fill, - R.color.chart_pace_border); - series[HEART_RATE_SERIES] = new ChartValueSeries(context, + R.color.chart_pace_border) { + @Override + protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) { + return chartPoint.getPace(); + } + + @Override + protected boolean drawIfChartPointHasNoData() { + return !reportSpeed; + } + }; + seriesList.add(paceSeries); + + seriesList.add(new ChartValueSeries(context, 0, Integer.MAX_VALUE, new int[]{25, 50}, R.string.description_sensor_heart_rate, R.string.description_sensor_heart_rate, R.color.chart_heart_rate_fill, - R.color.chart_heart_rate_border); - series[CADENCE_SERIES] = new ChartValueSeries(context, + R.color.chart_heart_rate_border) { + @Override + protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) { + return chartPoint.getHeartRate(); + } + + @Override + protected boolean drawIfChartPointHasNoData() { + return false; + } + }); + + seriesList.add(new ChartValueSeries(context, 0, Integer.MAX_VALUE, new int[]{5, 10, 25, 50}, R.string.description_sensor_cadence, R.string.description_sensor_cadence, R.color.chart_cadence_fill, - R.color.chart_cadence_border); - series[POWER_SERIES] = new ChartValueSeries(context, + R.color.chart_cadence_border) { + @Override + protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) { + return chartPoint.getCadence(); + } + + @Override + protected boolean drawIfChartPointHasNoData() { + return false; + } + }); + seriesList.add(new ChartValueSeries(context, 0, 1000, new int[]{5, 50, 100, 200}, R.string.description_sensor_power, R.string.description_sensor_power, R.color.chart_power_fill, - R.color.chart_power_border); + R.color.chart_power_border) { + @Override + protected double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) { + return chartPoint.getPower(); + } + + @Override + protected boolean drawIfChartPointHasNoData() { + return false; + } + }); float scale = context.getResources().getDisplayMetrics().density; @@ -279,6 +341,10 @@ public class ChartView extends View { setFocusable(true); setClickable(true); updateDimensions(); + + // either speedSeries or paceSeries should be enabled. + speedSeries.setEnabled(reportSpeed); + paceSeries.setEnabled(!reportSpeed); } @Override @@ -286,15 +352,6 @@ public class ChartView extends View { return true; } - /** - * Sets the enabled value for a chart value series. - * - * @param index the chart value series index - */ - public void setChartValueSeriesEnabled(int index, boolean enabled) { - series[index].setEnabled(enabled); - } - public boolean getMetricUnits() { return metricUnits; } @@ -315,12 +372,30 @@ public class ChartView extends View { /** * Sets report speed. * - * @param value true to report speed + * @param value report speed (true) or pace (false) */ public void setReportSpeed(boolean value) { reportSpeed = value; } + public boolean applyReportSpeed() { + if (reportSpeed) { + if (!speedSeries.isEnabled()) { + speedSeries.setEnabled(true); + paceSeries.setEnabled(false); + return true; + } + } else { + if (!paceSeries.isEnabled()) { + speedSeries.setEnabled(false); + paceSeries.setEnabled(true); + return true; + } + } + + return false; + } + /** * Sets show pointer. * @@ -335,15 +410,13 @@ public class ChartView extends View { * * @param dataPoints an array of data points to be added */ - public void addPendingPoints(List dataPoints) { + public void addChartPoints(List dataPoints) { synchronized (chartData) { chartData.addAll(dataPoints); - for (double[] dataPoint : dataPoints) { - xExtremityMonitor.update(dataPoint[0]); - for (int j = 0; j < series.length; j++) { - if (!Double.isNaN(dataPoint[j + 1])) { - series[j].update(dataPoint[j + 1]); - } + for (ChartPoint dataPoint : dataPoints) { + xExtremityMonitor.update(dataPoint.getTimeOrDistance()); + for (ChartValueSeries i : seriesList) { + i.update(dataPoint); } } updateDimensions(); @@ -536,7 +609,7 @@ public class ChartView extends View { * @param canvas the canvas */ private void drawDataSeries(Canvas canvas) { - for (ChartValueSeries chartValueSeries : series) { + for (ChartValueSeries chartValueSeries : seriesList) { if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) { chartValueSeries.drawPath(canvas); } @@ -602,9 +675,8 @@ public class ChartView extends View { int lines = titleDimensions[0]; int lineHeight = titleDimensions[1]; int count = 0; - for (int i = 0; i < series.length; i++) { - ChartValueSeries chartValueSeries = series[i]; - if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(i)) { + for (ChartValueSeries chartValueSeries : seriesList) { + if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) { count++; String title = getContext().getString(chartValueSeries.getTitleId(metricUnits)); Paint paint = chartValueSeries.getTitlePaint(); @@ -622,9 +694,8 @@ public class ChartView extends View { private int[] getTitleDimensions() { int lines = 0; int lineHeight = 0; - for (int i = 0; i < series.length; i++) { - ChartValueSeries chartValueSeries = series[i]; - if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(i)) { + for (ChartValueSeries chartValueSeries : seriesList) { + if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) { lines++; String title = getContext().getString(chartValueSeries.getTitleId(metricUnits)); Rect rect = getRect(chartValueSeries.getTitlePaint(), title); @@ -728,10 +799,10 @@ public class ChartView extends View { canvas.drawLine(x, y, x, y + effectiveHeight, axisPaint); int markerXPosition = x - spacer; - for (int i = 0; i < series.length; i++) { - int index = series.length - 1 - i; - ChartValueSeries chartValueSeries = series[index]; - if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(index)) { + for (int i = 0; i < seriesList.size(); i++) { + int index = seriesList.size() - 1 - i; + ChartValueSeries chartValueSeries = seriesList.get(index); + if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) { markerXPosition -= drawYAxisMarkers(chartValueSeries, canvas, markerXPosition) + spacer; } } @@ -779,18 +850,34 @@ public class ChartView extends View { * @param canvas the canvas */ private void drawPointer(Canvas canvas) { - int index = -1; - for (int i = 0; i < series.length; i++) { - ChartValueSeries chartValueSeries = series[i]; + //TODO pass chartValueSeries to 2nd if +// int index = -1; +// for (int i = 0; i < seriesList.size(); i++) { +// ChartValueSeries chartValueSeries = seriesList.get(i); +// if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) { +// index = i; +// break; +// } +// } +// if (index != -1 && chartData.size() > 0) { +// int dx = getX(maxX) - pointer.getIntrinsicWidth() / 2; +// int dy = getY(seriesList.get(index), chartData.get(chartData.size() - 1)[index + 1]) +// - pointer.getIntrinsicHeight(); +// canvas.translate(dx, dy); +// pointer.draw(canvas); +// } + + ChartValueSeries firstChartValueSeries = null; + for (ChartValueSeries chartValueSeries : seriesList) { if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) { - index = i; + firstChartValueSeries = chartValueSeries; break; } } - if (index != -1 && chartData.size() > 0) { + if (firstChartValueSeries != null && chartData.size() > 0) { int dx = getX(maxX) - pointer.getIntrinsicWidth() / 2; - int dy = getY(series[index], chartData.get(chartData.size() - 1)[index + 1]) - - pointer.getIntrinsicHeight(); + double value = firstChartValueSeries.extractDataFromChartPoint(chartData.get(chartData.size() - 1)); + int dy = getY(firstChartValueSeries, value) - pointer.getIntrinsicHeight(); canvas.translate(dx, dy); pointer.draw(canvas); } @@ -801,7 +888,7 @@ public class ChartView extends View { */ private void updatePaths() { synchronized (chartData) { - for (ChartValueSeries chartValueSeries : series) { + for (ChartValueSeries chartValueSeries : seriesList) { chartValueSeries.getPath().reset(); } drawPaths(); @@ -813,17 +900,19 @@ public class ChartView extends View { * Draws all paths. */ private void drawPaths() { - boolean[] hasMoved = new boolean[series.length]; + boolean[] hasMoved = new boolean[seriesList.size()]; - for (double[] dataPoint : chartData) { - for (int j = 0; j < series.length; j++) { - double value = dataPoint[j + 1]; + for (ChartPoint dataPoint : chartData) { + for (int j = 0; j < seriesList.size(); j++) { + ChartValueSeries chartValueSeries = seriesList.get(j); + + + double value = chartValueSeries.extractDataFromChartPoint(dataPoint); if (Double.isNaN(value)) { continue; } - ChartValueSeries chartValueSeries = series[j]; Path path = chartValueSeries.getPath(); - int x = getX(dataPoint[0]); + int x = getX(dataPoint.getTimeOrDistance()); int y = getY(chartValueSeries, value); if (!hasMoved[j]) { hasMoved[j] = true; @@ -839,20 +928,21 @@ public class ChartView extends View { * Closes all paths. */ private void closePaths() { - for (int i = 0; i < series.length; i++) { - int first = getFirstPopulatedChartDataIndex(i); + for (int i = 0; i < seriesList.size(); i++) { + ChartValueSeries chartValueSeries = seriesList.get(i); + int first = getFirstPopulatedChartDataIndex(chartValueSeries); if (first != -1) { - int xCorner = getX(chartData.get(first)[0]); + int xCorner = getX(chartData.get(first).getTimeOrDistance()); int yCorner = topBorder + effectiveHeight; - ChartValueSeries chartValueSeries = series[i]; Path path = chartValueSeries.getPath(); // Bottom right corner - path.lineTo(getX(chartData.get(chartData.size() - 1)[0]), yCorner); + path.lineTo(getX(chartData.get(chartData.size() - 1).getTimeOrDistance()), yCorner); // Bottom left corner path.lineTo(xCorner, yCorner); // Top right corner - path.lineTo(xCorner, getY(chartValueSeries, chartData.get(first)[i + 1])); + double value = chartValueSeries.extractDataFromChartPoint(chartData.get(first)); + path.lineTo(xCorner, getY(chartValueSeries, value)); } } } @@ -860,12 +950,10 @@ public class ChartView extends View { /** * Finds the index of the first data point containing data for a series. * Returns -1 if no data point contains data for the series. - * - * @param seriesIndex the series's index */ - private int getFirstPopulatedChartDataIndex(int seriesIndex) { + private int getFirstPopulatedChartDataIndex(ChartValueSeries chartValueSeries) { for (int i = 0; i < chartData.size(); i++) { - if (!Double.isNaN(chartData.get(i)[seriesIndex + 1])) { + if (chartValueSeries.isChartPointValid(chartData.get(i))) { return i; } } @@ -877,7 +965,7 @@ public class ChartView extends View { */ private void updateDimensions() { maxX = xExtremityMonitor.hasData() ? xExtremityMonitor.getMax() : 1.0; - for (ChartValueSeries chartValueSeries : series) { + for (ChartValueSeries chartValueSeries : seriesList) { chartValueSeries.updateDimension(); } float density = getContext().getResources().getDisplayMetrics().density; @@ -885,9 +973,9 @@ public class ChartView extends View { yAxisOffset = (int) (density * Y_AXIS_OFFSET); int markerLength = 0; - for (int i = 0; i < series.length; i++) { - ChartValueSeries chartValueSeries = series[i]; - if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(i)) { + for (int i = 0; i < seriesList.size(); i++) { + ChartValueSeries chartValueSeries = seriesList.get(i); + if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) { Rect rect = getRect(chartValueSeries.getMarkerPaint(), chartValueSeries.getLargestMarker()); markerLength += rect.width() + spacer; } @@ -980,22 +1068,12 @@ public class ChartView extends View { /** * Returns true if the index is allowed when the chartData is empty. - * - * @param index the index */ - private boolean allowIfEmpty(int index) { + private boolean allowIfEmpty(ChartValueSeries chartValueSeries) { if (!chartData.isEmpty()) { return false; } - switch (index) { - case ELEVATION_SERIES: - return true; - case SPEED_SERIES: - return reportSpeed; - case PACE_SERIES: - return !reportSpeed; - default: - return false; - } + + return chartValueSeries.drawIfChartPointHasNoData(); } } diff --git a/src/main/java/de/dennisguse/opentracks/fragments/ChartFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/ChartFragment.java index 5df90747a..2ff2dca17 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/ChartFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/ChartFragment.java @@ -33,18 +33,17 @@ import java.util.List; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.TrackDetailActivity; +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.Track; import de.dennisguse.opentracks.content.data.TrackPoint; import de.dennisguse.opentracks.content.data.Waypoint; -import de.dennisguse.opentracks.content.sensor.SensorDataSet; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.stats.TrackStatisticsUpdater; import de.dennisguse.opentracks.util.LocationUtils; import de.dennisguse.opentracks.util.PreferencesUtils; -import de.dennisguse.opentracks.util.UnitConversions; /** * A fragment to display track chart to the user. @@ -66,7 +65,7 @@ public class ChartFragment extends Fragment implements TrackDataListener { return chartFragment; } - private final List pendingPoints = new ArrayList<>(); + private final List pendingPoints = new ArrayList<>(); private TrackDataHub trackDataHub; @@ -74,11 +73,11 @@ public class ChartFragment extends Fragment implements TrackDataListener { private TrackStatisticsUpdater trackStatisticsUpdater; private long startTime; + //TODO Why is this needed? private int recordingDistanceInterval; // Modes of operation private boolean chartByDistance; - private final boolean[] chartShow = new boolean[]{true, true, true, true, true, true}; // UI elements private ChartView chartView; @@ -104,8 +103,8 @@ public class ChartFragment extends Fragment implements TrackDataListener { boolean reportSpeed = PreferencesUtils.isReportSpeed(getContext()); if (reportSpeed != chartView.getReportSpeed()) { chartView.setReportSpeed(reportSpeed); - setSeriesEnabled(ChartView.SPEED_SERIES, reportSpeed); - setSeriesEnabled(ChartView.PACE_SERIES, !reportSpeed); + chartView.applyReportSpeed(); + runOnUiThread(new Runnable() { @Override public void run() { @@ -236,7 +235,7 @@ public class ChartFragment extends Fragment implements TrackDataListener { @Override public void onNewTrackPointsDone() { if (isResumed()) { - chartView.addPendingPoints(pendingPoints); + chartView.addChartPoints(pendingPoints); pendingPoints.clear(); runOnUiThread(updateChart); } @@ -267,37 +266,12 @@ public class ChartFragment extends Fragment implements TrackDataListener { * Checks the chart settings. */ private void checkChartSettings() { - boolean needUpdate = false; - - if (setSeriesEnabled(ChartView.SPEED_SERIES, chartView.getReportSpeed())) { - needUpdate = true; - } - if (setSeriesEnabled(ChartView.PACE_SERIES, !chartView.getReportSpeed())) { - needUpdate = true; - } - + boolean needUpdate = chartView.applyReportSpeed(); if (needUpdate) { chartView.postInvalidate(); } } - /** - * Sets the series enabled value. - * - * @param index the series index - * @param value the value - * @return true if changed - */ - private boolean setSeriesEnabled(int index, boolean value) { - if (chartShow[index] != value) { - chartShow[index] = value; - chartView.setChartValueSeriesEnabled(index, value); - return true; - } - - return false; - } - /** * Resumes the trackDataHub. * Needs to be synchronized because trackDataHub can be accessed by multiple threads. @@ -346,68 +320,10 @@ public class ChartFragment extends Fragment implements TrackDataListener { } } - /** - * Given a {@link TrackPoint}, fill in a data point.
- * data[0] = time/distance
- * data[1] = elevation
- * data[2] = speed
- * data[3] = pace
- * data[4] = heart rate
- * data[5] = cadence
- * data[6] = power
- * - * @param trackPoint the trackPoint - */ @VisibleForTesting - double[] createPendingPoint(@NonNull TrackPoint trackPoint) { - double timeOrDistance = Double.NaN; - double elevation = Double.NaN; - double speed = Double.NaN; - double pace = Double.NaN; - - if (trackStatisticsUpdater != null) { - trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval); - TrackStatistics trackStatistics = trackStatisticsUpdater.getTrackStatistics(); - if (chartByDistance) { - double distance = trackStatistics.getTotalDistance() * UnitConversions.M_TO_KM; - if (!chartView.getMetricUnits()) { - distance *= UnitConversions.KM_TO_MI; - } - timeOrDistance = distance; - } else { - timeOrDistance = trackStatistics.getTotalTime(); - } - - elevation = trackStatisticsUpdater.getSmoothedElevation(); - if (!chartView.getMetricUnits()) { - elevation *= UnitConversions.M_TO_FT; - } - - speed = trackStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MS_TO_KMH; - if (!chartView.getMetricUnits()) { - speed *= UnitConversions.KM_TO_MI; - } - pace = speed == 0 ? 0.0 : 60.0 / speed; - } - - double heartRate = Double.NaN; - double cadence = Double.NaN; - double power = Double.NaN; - if (trackPoint.getSensorDataSet() != null) { - SensorDataSet sensorDataSet = trackPoint.getSensorDataSet(); - if (sensorDataSet.hasHeartRate()) { - heartRate = sensorDataSet.getHeartRate(); - } - if (sensorDataSet.hasCadence()) { - cadence = sensorDataSet.getCadence(); - } - if (sensorDataSet.hasPower()) { - power = sensorDataSet.getPower(); - } - } - - //TODO: Is related to ChartView.ELEVATION_SERIES etc. - return new double[]{timeOrDistance, elevation, speed, pace, heartRate, cadence, power}; + ChartPoint createPendingPoint(@NonNull TrackPoint trackPoint) { + trackStatisticsUpdater.addTrackPoint(trackPoint, recordingDistanceInterval); + return new ChartPoint(trackStatisticsUpdater, trackPoint.getSensorDataSet(), chartByDistance, chartView.getMetricUnits()); } @VisibleForTesting diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java index 67042bc76..02b69bad0 100644 --- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java +++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatisticsUpdater.java @@ -61,9 +61,9 @@ public class TrackStatisticsUpdater { private final TrackStatistics trackStatistics; // A buffer of the recent elevation readings (m) - private final DoubleBuffer elevationBuffer = new DoubleBuffer(ELEVATION_SMOOTHING_FACTOR); + private final DoubleBuffer elevationBuffer_m = new DoubleBuffer(ELEVATION_SMOOTHING_FACTOR); // A buffer of the recent speed readings (m/s) for calculating max speed - private final DoubleBuffer speedBuffer = new DoubleBuffer(SPEED_SMOOTHING_FACTOR); + private final DoubleBuffer speedBuffer_ms = new DoubleBuffer(SPEED_SMOOTHING_FACTOR); // The current segment's statistics private TrackStatistics currentSegment; @@ -128,8 +128,8 @@ public class TrackStatisticsUpdater { currentSegment = init(trackPoint.getLocation().getTime()); lastTrackPoint = null; lastMovingTrackPoint = null; - elevationBuffer.reset(); - speedBuffer.reset(); + elevationBuffer_m.reset(); + speedBuffer_ms.reset(); return; } @@ -144,7 +144,7 @@ public class TrackStatisticsUpdater { double movingDistance = lastMovingTrackPoint.distanceTo(trackPoint); if (movingDistance < minRecordingDistance && (!trackPoint.hasSpeed() || trackPoint.getSpeed() < MAX_NO_MOVEMENT_SPEED)) { - speedBuffer.reset(); + speedBuffer_ms.reset(); lastTrackPoint = trackPoint; return; } @@ -181,11 +181,11 @@ public class TrackStatisticsUpdater { * The elevation readings is noisy so the smoothed elevation is better than the raw elevation for many tasks. */ public double getSmoothedElevation() { - return elevationBuffer.getAverage(); + return elevationBuffer_m.getAverage(); } public double getSmoothedSpeed() { - return speedBuffer.getAverage(); + return speedBuffer_ms.getAverage(); } /** @@ -199,11 +199,11 @@ public class TrackStatisticsUpdater { @VisibleForTesting private void updateSpeed(long time, double speed, long lastLocationTime, double lastLocationSpeed) { if (speed < MAX_NO_MOVEMENT_SPEED) { - speedBuffer.reset(); + speedBuffer_ms.reset(); } else if (isValidSpeed(time, speed, lastLocationTime, lastLocationSpeed)) { - speedBuffer.setNext(speed); - if (speedBuffer.getAverage() > currentSegment.getMaxSpeed()) { - currentSegment.setMaxSpeed(speedBuffer.getAverage()); + speedBuffer_ms.setNext(speed); + if (speedBuffer_ms.getAverage() > currentSegment.getMaxSpeed()) { + currentSegment.setMaxSpeed(speedBuffer_ms.getAverage()); } } else { Log.d(TAG, "Invalid speed. speed: " + speed + " lastLocationSpeed: " + lastLocationSpeed); @@ -218,9 +218,9 @@ public class TrackStatisticsUpdater { @VisibleForTesting private double updateElevation(double elevation) { // Update elevation using the smoothed average - double oldAverage = elevationBuffer.getAverage(); - elevationBuffer.setNext(elevation); - double newAverage = elevationBuffer.getAverage(); + double oldAverage = elevationBuffer_m.getAverage(); + elevationBuffer_m.setNext(elevation); + double newAverage = elevationBuffer_m.getAverage(); currentSegment.updateElevationExtremities(newAverage); double difference = newAverage - oldAverage; @@ -264,8 +264,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.isFull()) { - double average = speedBuffer.getAverage(); + if (speedBuffer_ms.isFull()) { + double average = speedBuffer_ms.getAverage(); double diff = Math.abs(average - speed); return (speed < average * 10) && (diff < MAX_ACCELERATION * timeDifference); }