diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesChartFragment.java index 256a49958..3c4547746 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesChartFragment.java @@ -32,7 +32,6 @@ import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.LineData; -import com.github.mikephil.charting.data.LineDataSet; import com.github.mikephil.charting.formatter.ValueFormatter; import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; @@ -41,7 +40,6 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -57,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.util.ChartUtils; public class ActivitySummariesChartFragment extends AbstractActivityChartFragment { @@ -267,27 +266,12 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen tsTranslation = new TimestampTranslation(); } - final List heartRateEntries = new ArrayList<>(activityPoints.size()); - final List heartRateDataSets = new ArrayList<>(); - int lastTsShorten = 0; - for (final ActivityPoint activityPoint : activityPoints) { - int tsShorten = tsTranslation.shorten((int) (activityPoint.getTime().getTime() / 1000)); - if (lastTsShorten == 0 || (tsShorten - lastTsShorten) <= 60 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { - heartRateEntries.add(new Entry(tsShorten, activityPoint.getHeartRate())); - } else { - if (!heartRateEntries.isEmpty()) { - List clone = new ArrayList<>(heartRateEntries.size()); - clone.addAll(heartRateEntries); - heartRateDataSets.add(createHeartrateSet(clone, "Heart Rate")); - heartRateEntries.clear(); - } - } - lastTsShorten = tsShorten; - heartRateEntries.add(new Entry(tsShorten, activityPoint.getHeartRate())); - } - if (!heartRateEntries.isEmpty()) { - heartRateDataSets.add(createHeartrateSet(heartRateEntries, "Heart Rate")); - } + final List heartRateEntries = activityPoints.stream() + .filter(ap -> HeartRateUtils.getInstance().isValidHeartRateValue(ap.getHeartRate())) + .map(ap -> new Entry(tsTranslation.shorten((int) (ap.getTime().getTime() / 1000)), ap.getHeartRate())) + .collect(Collectors.toList()); + + final List heartRateDataSets = ChartUtils.findGaps(heartRateEntries, l -> createHeartrateSet(l, "Heart Rate")); if (activitySamplesData != null) { // if we have activity samples, replace the heart rate dataset diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractActivityChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractActivityChartFragment.java index 1fdccd56b..bfbb3e60e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractActivityChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractActivityChartFragment.java @@ -35,6 +35,7 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.List; +import java.util.stream.Collectors; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; @@ -46,6 +47,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.util.ChartUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public abstract class AbstractActivityChartFragment extends AbstractChartFragment { @@ -270,33 +272,20 @@ public abstract class AbstractActivityChartFragment extend } boolean hr = supportsHeartrate(gbDevice); - final List heartRateLineEntries = new ArrayList<>(); - final List heartRateDataSets = new ArrayList<>(); - int lastTsShorten = 0; + final List heartRateDataSets; HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance(); // Currently only for HR if (hr) { - for (ActivitySample sample : highResSamples) { - if (sample.getKind() != ActivityKind.NOT_WORN && heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) { - int tsShorten = tsTranslation.shorten(sample.getTimestamp()); - if (lastTsShorten == 0 || (tsShorten - lastTsShorten) <= 60 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { - heartRateLineEntries.add(new Entry(tsShorten, sample.getHeartRate())); - } else { - if (!heartRateLineEntries.isEmpty()) { - List clone = new ArrayList<>(heartRateLineEntries.size()); - clone.addAll(heartRateLineEntries); - heartRateDataSets.add(createHeartrateSet(clone, "Heart Rate")); - heartRateLineEntries.clear(); - } - } - lastTsShorten = tsShorten; - heartRateLineEntries.add(new Entry(tsShorten, sample.getHeartRate())); - } - } - } - if (!heartRateLineEntries.isEmpty()) { - heartRateDataSets.add(createHeartrateSet(heartRateLineEntries, "Heart Rate")); + final List heartRateLineEntries = highResSamples.stream() + .filter(s -> s.getKind() != ActivityKind.NOT_WORN) + .filter(s -> heartRateUtilsInstance.isValidHeartRateValue(s.getHeartRate())) + .map(s -> new Entry(tsTranslation.shorten(s.getTimestamp()), s.getHeartRate())) + .collect(Collectors.toList()); + + heartRateDataSets = ChartUtils.findGaps(heartRateLineEntries, l -> createHeartrateSet(l, "Heart Rate")); + } else { + heartRateDataSets = new ArrayList<>(); } // convert Entry Lists to Datasets diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateDailyFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateDailyFragment.java index 10e19c922..61aed4e1f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateDailyFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateDailyFragment.java @@ -28,6 +28,7 @@ import java.util.Calendar; import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.stream.Collectors; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; @@ -36,10 +37,10 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; import nodomain.freeyourgadget.gadgetbridge.util.Accumulator; +import nodomain.freeyourgadget.gadgetbridge.util.ChartUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class HeartRateDailyFragment extends AbstractChartFragment { @@ -239,36 +240,16 @@ public class HeartRateDailyFragment extends AbstractChartFragment lineEntries = new ArrayList<>(); List samples = data.samples; final Accumulator accumulator = new Accumulator(); - final List lineDataSets = new ArrayList<>(); - int lastTsShorten = 0; - for (int i =0; i < samples.size(); i++) { - final ActivitySample sample = samples.get(i); - final int tsShorten = tsTranslation.shorten(sample.getTimestamp()); - if (!heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) { - continue; - } - if (lastTsShorten == 0 || (tsShorten - lastTsShorten) <= 60 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { - lineEntries.add(new Entry(tsShorten, sample.getHeartRate())); - } else { - if (!lineEntries.isEmpty()) { - List clone = new ArrayList<>(lineEntries.size()); - clone.addAll(lineEntries); - lineDataSets.add(createHeartRateDataSet(clone)); - lineEntries.clear(); - } - } - lastTsShorten = tsShorten; - lineEntries.add(new Entry(tsShorten, sample.getHeartRate())); - accumulator.add(sample.getHeartRate()); - } + final List heartRateLineEntries = samples.stream() + .filter(s -> heartRateUtilsInstance.isValidHeartRateValue(s.getHeartRate())) + .peek(s -> accumulator.add(s.getHeartRate())) + .map(s -> new Entry(tsTranslation.shorten(s.getTimestamp()), s.getHeartRate())) + .collect(Collectors.toList()); - if (!lineEntries.isEmpty()) { - lineDataSets.add(createHeartRateDataSet(lineEntries)); - } + final List lineDataSets = ChartUtils.findGaps(heartRateLineEntries, this::createHeartRateDataSet); final int average = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getAverage()) : -1; final int minimum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMin()) : -1; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ChartUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ChartUtils.java new file mode 100644 index 000000000..fa99c4e9c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ChartUtils.java @@ -0,0 +1,57 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +public final class ChartUtils { + private ChartUtils() { + // utility class + } + + /** + * Find the gaps in a list of values with potentially varying sample rates, and create separate + * datasets for each. + */ + public static List findGaps(final List values, + final Function, ILineDataSet> datasetCreator) { + if (values.isEmpty()) { + return Collections.emptyList(); + } else if (values.size() == 1) { + return Collections.singletonList(datasetCreator.apply(values)); + } + + final List ret = new ArrayList<>(); + int lastStart = 0; + float lastGap = -1; + for (int i = 0; i < values.size() - 1; i++) { + final float gapToNext = values.get(i + 1).getX() - values.get(i).getX(); + + if (lastGap >= 0) { + if (gapToNext >= lastGap * 5 && i > lastStart) { + // sample rate decreased - insert a gap + ret.add(datasetCreator.apply(values.subList(lastStart, i + 1))); + lastGap = -1; + lastStart = i + 1; + continue; + } else if (gapToNext < lastGap / 5 && i - 1 > lastStart) { + // sample rate increased drastically (workout start?) - insert a gap + ret.add(datasetCreator.apply(values.subList(lastStart, i))); + lastStart = i; + } + } + + lastGap = gapToNext; + } + + if (lastStart < values.size()) { + ret.add(datasetCreator.apply(values.subList(lastStart, values.size()))); + } + + return ret; + } +}