1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-12 13:09:24 +01:00

Use HR from track file if available

This commit is contained in:
José Rebelo 2024-09-21 10:51:24 +01:00
parent 060d39d5b0
commit dba9f32757
5 changed files with 193 additions and 102 deletions

View File

@ -32,21 +32,30 @@ 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;
import org.slf4j.Logger;
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;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.AbstractActivityChartFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsData;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsHost;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.DefaultChartsData;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.SampleXLabelFormatter;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.TimestampTranslation;
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
@ -54,17 +63,23 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesChartFragment.class);
private LineChart mChart;
private int startTime;
private int endTime;
private GBDevice gbDevice;
private View view;
public void setDateAndGetData(GBDevice gbDevice, long startTime, long endTime) {
// If a track file is being used (takes precedence over activity data)
private File trackFile;
// If activity data is being used
private GBDevice gbDevice;
private int startTime;
private int endTime;
public void setDateAndGetData(@Nullable File trackFile, GBDevice gbDevice, long startTime, long endTime) {
this.trackFile = trackFile;
this.startTime = (int) startTime;
this.endTime = (int) endTime;
this.gbDevice = gbDevice;
if (this.view != null) {
createLocalRefreshTask("Visualizing data", getActivity()).execute();
createLocalRefreshTask("getting hr and activity", getActivity()).execute();
}
}
@ -85,9 +100,9 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
super.onViewCreated(view, savedInstanceState);
init();
this.view = view;
if (this.gbDevice != null) {
if (this.trackFile != null || this.gbDevice != null) {
setupChart();
createLocalRefreshTask("Visualizing data", getActivity()).execute();
createLocalRefreshTask("getting hr and activity", getActivity()).execute();
}
}
@ -184,14 +199,24 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
@Override
protected void doInBackground(DBHandler handler) {
List<? extends ActivitySample> samples = getAllSamples(handler, gbDevice, startTime, endTime);
final DefaultChartsData<?> dcd;
final DefaultChartsData<LineData> activitySamplesData = buildChartFromSamples(handler);
DefaultChartsData dcd = null;
try {
dcd = refresh(gbDevice, samples);
} catch (Exception e) {
LOG.debug("Unable to get charts data right now:", e);
if (trackFile != null) {
final List<ActivityPoint> activityPoints = ActivitySummariesGpsFragment.getActivityPoints(trackFile)
.stream()
.filter(ap -> ap.getHeartRate() > 0)
.collect(Collectors.toList());
if (!activityPoints.isEmpty()) {
dcd = buildHeartRateChart(activityPoints, activitySamplesData);
} else {
dcd = activitySamplesData;
}
} else {
dcd = activitySamplesData;
}
if (dcd != null) {
mChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mChart.getXAxis().setValueFormatter(dcd.getXValueFormatter());
@ -203,5 +228,69 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
protected void onPostExecute(Object o) {
mChart.invalidate();
}
private DefaultChartsData<LineData> buildChartFromSamples(DBHandler handler) {
final List<? extends ActivitySample> samples = getAllSamples(handler, gbDevice, startTime, endTime);
try {
return refresh(gbDevice, samples);
} catch (Exception e) {
LOG.error("Unable to get charts data right now", e);
}
return null;
}
private DefaultChartsData<LineData> buildHeartRateChart(final List<ActivityPoint> activityPoints,
final DefaultChartsData<LineData> activitySamplesData) {
// If we have data from activity samples, we need to use the same TimestampTranslation so
// that the HR chart is aligned
// This is not ideal...
final TimestampTranslation tsTranslation;
if (activitySamplesData != null) {
final ValueFormatter xValueFormatter = activitySamplesData.getXValueFormatter();
if (xValueFormatter instanceof SampleXLabelFormatter) {
tsTranslation = ((SampleXLabelFormatter) xValueFormatter).getTsTranslation();
} else {
LOG.error("Unable to get TimestampTranslation from x value formatter - class changed?");
tsTranslation = new TimestampTranslation();
}
} else {
tsTranslation = new TimestampTranslation();
}
final List<Entry> heartRateEntries = new ArrayList<>(activityPoints.size());
int lastHrSampleTs = -1;
for (final ActivityPoint activityPoint : activityPoints) {
int ts = tsTranslation.shorten((int) (activityPoint.getTime().getTime() / 1000));
if (lastHrSampleTs > -1 && ts - lastHrSampleTs > 1800 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
heartRateEntries.add(createLineEntry(0, lastHrSampleTs + 1));
heartRateEntries.add(createLineEntry(0, ts - 1));
}
heartRateEntries.add(createLineEntry(activityPoint.getHeartRate(), ts));
lastHrSampleTs = ts;
}
final LineDataSet heartRateSet = createHeartrateSet(heartRateEntries, "Heart Rate");
if (activitySamplesData != null) {
// if we have activity samples, replace the heart rate dataset
LineData data = activitySamplesData.getData();
List<ILineDataSet> dataSets = data.getDataSets();
for (final ILineDataSet dataSet : dataSets) {
if ("Heart Rate".equals(dataSet.getLabel())) {
dataSets.remove(dataSet);
dataSets.add(heartRateSet);
return activitySamplesData;
}
}
// We failed to find a heart rate dataset, append ours
dataSets.add(heartRateSet);
return activitySamplesData;
} else {
final LineData lineData = new LineData(Collections.singletonList(heartRateSet));
final ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation, "HH:mm");
return new DefaultChartsData<>(lineData, xValueFormatter);
}
}
}
}

View File

@ -32,23 +32,22 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitImporter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParseException;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
import static android.graphics.Bitmap.createBitmap;
@ -56,7 +55,7 @@ import static android.graphics.Bitmap.createBitmap;
public class ActivitySummariesGpsFragment extends AbstractGBFragment {
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesGpsFragment.class);
private ImageView gpsView;
private int CANVAS_SIZE = 360;
private final int CANVAS_SIZE = 360;
private File inputFile;
@Override
@ -79,51 +78,50 @@ public class ActivitySummariesGpsFragment extends AbstractGBFragment {
private void processInBackgroundThread() {
final Canvas canvas = createCanvas(gpsView);
new Thread(new Runnable() {
@Override
public void run() {
final List<GPSCoordinate> points = new ArrayList<>();
if (inputFile.getName().endsWith(".gpx")) {
try (FileInputStream inputStream = new FileInputStream(inputFile)) {
final GpxParser gpxParser = new GpxParser(inputStream);
points.addAll(gpxParser.getGpxFile().getPoints());
} catch (final IOException e) {
LOG.error("Failed to open {}", inputFile, e);
return;
} catch (final GpxParseException e) {
LOG.error("Failed to parse gpx file", e);
return;
}
} else if (inputFile.getName().endsWith(".fit")) {
try {
FitFile fitFile = FitFile.parseIncoming(inputFile);
for (final RecordData record : fitFile.getRecords()) {
if (record instanceof FitRecord) {
final ActivityPoint activityPoint = ((FitRecord) record).toActivityPoint();
if (activityPoint.getLocation() != null) {
points.add(activityPoint.getLocation());
}
}
}
} catch (final IOException e) {
LOG.error("Failed to open {}", inputFile, e);
return;
} catch (final Exception e) {
LOG.error("Failed to parse fit file", e);
return;
}
} else {
LOG.warn("Unknown file type {}", inputFile.getName());
return;
}
new Thread(() -> {
final List<GPSCoordinate> points = getActivityPoints(inputFile)
.stream()
.map(ActivityPoint::getLocation)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (!points.isEmpty()) {
drawTrack(canvas, points);
}
if (!points.isEmpty()) {
drawTrack(canvas, points);
}
}).start();
}
public static List<ActivityPoint> getActivityPoints(final File trackFile) {
final List<ActivityPoint> points = new ArrayList<>();
if (trackFile.getName().endsWith(".gpx")) {
try (FileInputStream inputStream = new FileInputStream(trackFile)) {
final GpxParser gpxParser = new GpxParser(inputStream);
points.addAll(gpxParser.getGpxFile().getActivityPoints());
} catch (final IOException e) {
LOG.error("Failed to open {}", trackFile, e);
} catch (final GpxParseException e) {
LOG.error("Failed to parse gpx file", e);
}
} else if (trackFile.getName().endsWith(".fit")) {
try {
FitFile fitFile = FitFile.parseIncoming(trackFile);
for (final RecordData record : fitFile.getRecords()) {
if (record instanceof FitRecord) {
points.add(((FitRecord) record).toActivityPoint());
}
}
} catch (final IOException e) {
LOG.error("Failed to open {}", trackFile, e);
} catch (final Exception e) {
LOG.error("Failed to parse fit file", e);
}
} else {
LOG.warn("Unknown file type {}", trackFile.getName());
}
return points;
}
private void drawTrack(Canvas canvas, List<? extends GPSCoordinate> trackPoints) {
double maxLat = (Collections.max(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude();
double minLat = (Collections.min(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude();
@ -154,11 +152,10 @@ public class ActivitySummariesGpsFragment extends AbstractGBFragment {
}
}
private Canvas createCanvas(ImageView imageView) {
Bitmap bitmap = createBitmap(CANVAS_SIZE, CANVAS_SIZE, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(GBApplication.getWindowBackgroundColor(getActivity()));
canvas.drawColor(GBApplication.getWindowBackgroundColor(requireActivity()));
//frame around, but it doesn't look so nice
/*
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

View File

@ -35,6 +35,7 @@ import android.util.TypedValue;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
@ -110,6 +111,9 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
String selectedGpxFile;
File export_path = null;
private ActivitySummariesChartFragment activitySummariesChartFragment;
private ActivitySummariesGpsFragment activitySummariesGpsFragment;
public static int getAlternateColor(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
@ -171,8 +175,8 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
this,
R.anim.bounceright);
final ActivitySummariesChartFragment activitySummariesChartFragment = new ActivitySummariesChartFragment();
final ActivitySummariesGpsFragment activitySummariesGpsFragment = new ActivitySummariesGpsFragment();
activitySummariesChartFragment = new ActivitySummariesChartFragment();
activitySummariesGpsFragment = new ActivitySummariesGpsFragment();
getSupportFragmentManager()
.beginTransaction()
@ -186,18 +190,8 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
BaseActivitySummary newItem = items.getNextItem();
if (newItem != null) {
currentItem = newItem;
makeSummaryHeader(newItem);
makeSummaryContent(newItem);
activitySummariesChartFragment.setDateAndGetData(getGBDevice(currentItem.getDevice()), currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000);
if (itemHasGps()) {
showGpsCanvas();
activitySummariesGpsFragment.set_data(getTrackFile());
} else {
hideGpsCanvas();
}
refreshFromCurrentItem();
layout.startAnimation(animFadeRight);
updateMenuItems();
} else {
layout.startAnimation(animBounceRight);
}
@ -208,19 +202,8 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
BaseActivitySummary newItem = items.getPrevItem();
if (newItem != null) {
currentItem = newItem;
makeSummaryHeader(newItem);
makeSummaryContent(newItem);
activitySummariesChartFragment.setDateAndGetData(getGBDevice(currentItem.getDevice()), currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000);
if (itemHasGps()) {
showGpsCanvas();
activitySummariesGpsFragment.set_data(getTrackFile());
} else {
hideGpsCanvas();
}
refreshFromCurrentItem();
layout.startAnimation(animFadeLeft);
updateMenuItems();
} else {
layout.startAnimation(animBounceLeft);
}
@ -229,18 +212,9 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
currentItem = items.getItem(position);
if (currentItem != null) {
makeSummaryHeader(currentItem);
makeSummaryContent(currentItem);
activitySummariesChartFragment.setDateAndGetData(getGBDevice(currentItem.getDevice()), currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000);
if (itemHasGps()) {
showGpsCanvas();
activitySummariesGpsFragment.set_data(getTrackFile());
} else {
hideGpsCanvas();
}
refreshFromCurrentItem();
}
//allows long-press.switch of data being in raw form or recalculated
ImageView activity_icon = findViewById(R.id.item_image);
activity_icon.setOnLongClickListener(new View.OnLongClickListener() {
@ -382,6 +356,27 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
}
private void refreshFromCurrentItem() {
makeSummaryHeader(currentItem);
makeSummaryContent(currentItem);
activitySummariesChartFragment.setDateAndGetData(
getTrackFile(),
getGBDevice(currentItem.getDevice()),
currentItem.getStartTime().getTime() / 1000,
currentItem.getEndTime().getTime() / 1000
);
if (itemHasGps()) {
showGpsCanvas();
activitySummariesGpsFragment.set_data(getTrackFile());
} else {
hideGpsCanvas();
}
updateMenuItems();
}
private File get_path() {
File path = null;
try {
@ -738,12 +733,18 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
}
}
mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu().findItem(R.id.activity_action_show_gpx).setVisible(hasGpx);
mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu().findItem(R.id.activity_action_share_gpx).setVisible(hasGpx);
mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu().findItem(R.id.activity_action_dev_share_raw_summary).setVisible(hasRawSummary);
mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu().findItem(R.id.activity_action_dev_share_raw_details).setVisible(hasRawDetails);
final MenuItem devToolsMenu = mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu().findItem(R.id.activity_action_dev_tools);
devToolsMenu.setVisible(devToolsMenu.getSubMenu().hasVisibleItems());
if (mOptionsMenu != null) {
final SubMenu overflowMenu = mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu();
if (overflowMenu != null) {
overflowMenu.findItem(R.id.activity_action_show_gpx).setVisible(hasGpx);
overflowMenu.findItem(R.id.activity_action_share_gpx).setVisible(hasGpx);
overflowMenu.findItem(R.id.activity_action_dev_share_raw_summary).setVisible(hasRawSummary);
overflowMenu.findItem(R.id.activity_action_dev_share_raw_details).setVisible(hasRawDetails);
final MenuItem devToolsMenu = overflowMenu.findItem(R.id.activity_action_dev_tools);
final SubMenu devToolsSubMenu = devToolsMenu.getSubMenu();
devToolsMenu.setVisible(devToolsSubMenu != null && devToolsSubMenu.hasVisibleItems());
}
}
}
private void showGpsCanvas() {

View File

@ -81,7 +81,7 @@ public class ActivityListingDetail extends DialogFragment {
.beginTransaction()
.replace(R.id.chartsFragmentHolder, activitySummariesChartFragment)
.commit();
activitySummariesChartFragment.setDateAndGetData(gbDevice, tsFrom, tsTo);
activitySummariesChartFragment.setDateAndGetData(null, gbDevice, tsFrom, tsTo);
ActivityListingAdapter stepListAdapter = new ActivityListingAdapter(getContext());
View activityItem = view.findViewById(R.id.activityItemHolder);

View File

@ -25,7 +25,7 @@ import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
class SampleXLabelFormatter extends ValueFormatter {
public class SampleXLabelFormatter extends ValueFormatter {
private final TimestampTranslation tsTranslation;
@SuppressLint("SimpleDateFormat")
private final SimpleDateFormat annotationDateFormat;
@ -46,4 +46,8 @@ class SampleXLabelFormatter extends ValueFormatter {
final Date date = cal.getTime();
return annotationDateFormat.format(date);
}
public TimestampTranslation getTsTranslation() {
return tsTranslation;
}
}