mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Workout details: add additional charts
This commit is contained in:
+31
@@ -0,0 +1,31 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import com.github.mikephil.charting.formatter.ValueFormatter;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
public class DurationXLabelFormatter extends ValueFormatter {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private final SimpleDateFormat annotationDateFormat;
|
||||
|
||||
private final Calendar cal = GregorianCalendar.getInstance();
|
||||
|
||||
public DurationXLabelFormatter(String simpleDateFormatPattern) {
|
||||
this.annotationDateFormat = new SimpleDateFormat(simpleDateFormatPattern);
|
||||
}
|
||||
|
||||
// TODO: this does not work. Cannot use precomputed labels
|
||||
@Override
|
||||
public String getFormattedValue(final float value) {
|
||||
cal.clear();
|
||||
final int ts = (int) value;
|
||||
cal.setTimeInMillis(ts);
|
||||
final Date date = cal.getTime();
|
||||
return annotationDateFormat.format(date);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM;
|
||||
|
||||
import com.github.mikephil.charting.formatter.ValueFormatter;
|
||||
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
|
||||
|
||||
public class SpeedYLabelFormatter extends ValueFormatter {
|
||||
String unit;
|
||||
private final WorkoutValueFormatter workoutValueFormatter = new WorkoutValueFormatter();
|
||||
|
||||
public SpeedYLabelFormatter(String unit) {
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormattedValue(float value) {
|
||||
if (unit.equals(UNIT_SECONDS_PER_KM)) {
|
||||
value = value > 0 ? Math.round((60 / (value * 3.6)) * 60) : 0;
|
||||
}
|
||||
return workoutValueFormatter.formatValue(value, unit, false);
|
||||
}
|
||||
}
|
||||
+44
-1
@@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities.workouts
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
@@ -44,12 +45,17 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.gridlayout.widget.GridLayout
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.mikephil.charting.charts.LineChart
|
||||
import com.github.mikephil.charting.components.XAxis
|
||||
import com.github.mikephil.charting.data.LineData
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesChartFragment
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.DurationXLabelFormatter
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.fit.FitViewerActivity
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryEntry
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryGroup
|
||||
@@ -393,7 +399,44 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
|
||||
)
|
||||
}
|
||||
|
||||
// TODO add chartData
|
||||
val chartTextColor = GBApplication.getSecondaryTextColor(context);
|
||||
val lineChart = LineChart(requireContext()).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
legend.textColor = GBApplication.getTextColor(context)
|
||||
isScaleXEnabled = false
|
||||
isScaleYEnabled = false
|
||||
isHighlightPerDragEnabled = false
|
||||
isHighlightPerTapEnabled = false
|
||||
isDragEnabled = false
|
||||
|
||||
}
|
||||
lineChart.xAxis.apply {
|
||||
setDrawLabels(true)
|
||||
setDrawGridLines(false)
|
||||
setDrawLimitLinesBehindData(true)
|
||||
isEnabled = true
|
||||
textColor = chartTextColor
|
||||
position = XAxis.XAxisPosition.BOTTOM
|
||||
valueFormatter = DurationXLabelFormatter("mm:ss")
|
||||
}
|
||||
lineChart.axisLeft.apply {
|
||||
setDrawGridLines(false)
|
||||
setDrawTopYLabelEntry(true)
|
||||
textColor = chartTextColor
|
||||
isEnabled = true
|
||||
if (chart.chartYLabelFormatter != null) {
|
||||
valueFormatter = chart.chartYLabelFormatter;
|
||||
}
|
||||
}
|
||||
lineChart.axisRight.apply {
|
||||
isEnabled = false
|
||||
}
|
||||
lineChart.data = chart.chartData as LineData?
|
||||
lineChart.invalidate()
|
||||
chartsFragmentHolder.addView(lineChart)
|
||||
|
||||
binding.dynamicCharts.addView(chartsFragmentHolder)
|
||||
}
|
||||
|
||||
+10
-4
@@ -46,7 +46,7 @@ public class WorkoutValueFormatter {
|
||||
this.show_raw_data = !show_raw_data;
|
||||
}
|
||||
|
||||
public String formatValue(final Object rawValue, String unit) {
|
||||
public String formatValue(final Object rawValue, String unit, boolean showUnit) {
|
||||
if (rawValue == null) {
|
||||
return GBApplication.getContext().getString(R.string.stats_empty_value);
|
||||
}
|
||||
@@ -172,21 +172,27 @@ public class WorkoutValueFormatter {
|
||||
}
|
||||
}
|
||||
|
||||
if (unit.equals("seconds") && !show_raw_data) { //rather then plain seconds, show formatted duration
|
||||
if (unit.equals("seconds") && !show_raw_data && showUnit) { //rather then plain seconds, show formatted duration
|
||||
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS);
|
||||
} else if (unit.equals("minutes_km") || unit.equals("minutes_mi") || unit.equals("minutes_100m") || unit.equals("minutes_100yd")) {
|
||||
// Format pace
|
||||
String format = showUnit ? "%d:%02d %s" : "%d:%02d";
|
||||
return String.format(
|
||||
Locale.getDefault(),
|
||||
"%d:%02d %s",
|
||||
format,
|
||||
(int) Math.floor(value), (int) Math.round(60 * (value - (int) Math.floor(value))),
|
||||
getStringResourceByName(unit)
|
||||
);
|
||||
} else {
|
||||
return String.format("%s %s", df.format(value), getStringResourceByName(unit));
|
||||
String format = showUnit ? "%s %s" : "%s";
|
||||
return String.format(format, df.format(value), getStringResourceByName(unit));
|
||||
}
|
||||
}
|
||||
|
||||
public String formatValue(final Object rawValue, String unit) {
|
||||
return formatValue(rawValue, unit, true);
|
||||
}
|
||||
|
||||
public String getStringResourceByName(String aString) {
|
||||
String packageName = BuildConfig.APPLICATION_ID;
|
||||
int resId = GBApplication.getContext().getResources().getIdentifier(aString, "string", packageName);
|
||||
|
||||
+85
-11
@@ -86,6 +86,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KMPH;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_LB;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS_PER_SECOND;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_MILLISECONDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML_KG_MIN;
|
||||
@@ -94,10 +95,17 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_PERCENTAGE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_RPM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_STROKES_PER_LENGTH;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_WATT;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -111,7 +119,10 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.SpeedYLabelFormatter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.TimestampTranslation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryProgressEntry;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryTableBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryValue;
|
||||
@@ -157,6 +168,10 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
|
||||
private final List<FitSet> sets = new ArrayList<>();
|
||||
private final List<FitLap> laps = new ArrayList<>();
|
||||
|
||||
TimestampTranslation tsTranslation = new TimestampTranslation();
|
||||
|
||||
ActivityKind activityKind;
|
||||
|
||||
public GarminWorkoutParser(final Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
@@ -201,11 +216,56 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
|
||||
handleRecord(record);
|
||||
}
|
||||
|
||||
if (sport != null) {
|
||||
if (StringUtils.isNullOrEmpty(summary.getName())) {
|
||||
summary.setName(sport.getName());
|
||||
}
|
||||
activityKind = getActivityKind(sport.getSport(), sport.getSubSport());
|
||||
} else {
|
||||
activityKind = getActivityKind(session.getSport(), session.getSubSport());
|
||||
}
|
||||
final ActivitySummaryData activitySummaryData = updateSummary(summary);
|
||||
|
||||
final List<WorkoutChart> charts = new LinkedList<>();
|
||||
|
||||
// TODO charts
|
||||
if (!this.activityPoints.isEmpty()) {
|
||||
final List<Entry> heartRateDataPoints = new ArrayList<>();
|
||||
final List<Entry> speedDataPoints = new ArrayList<>();
|
||||
final List<Entry> elevationDataPoints = new ArrayList<>();
|
||||
boolean hasSpeedValues = false;
|
||||
for (int i=0; i<=activityPoints.size()-1; i++) {
|
||||
ActivityPoint point = activityPoints.get(i);
|
||||
long time = tsTranslation.shorten((int) point.getTime().getTime());
|
||||
heartRateDataPoints.add(new Entry(time, point.getHeartRate()));
|
||||
speedDataPoints.add(new Entry(time, point.getSpeed()));
|
||||
if (!hasSpeedValues && point.getSpeed() > 0) {
|
||||
hasSpeedValues = true;
|
||||
}
|
||||
if (point.getLocation() != null) {
|
||||
elevationDataPoints.add(new Entry(time, (float) point.getLocation().getAltitude()));
|
||||
}
|
||||
}
|
||||
if (!heartRateDataPoints.isEmpty()) {
|
||||
String label = String.format("%s(%s)", context.getString(R.string.heart_rate), getUnitString(UNIT_BPM));
|
||||
LineDataSet dataset = createDataSet(heartRateDataPoints, label, Color.RED);
|
||||
charts.add(new WorkoutChart(context.getString(R.string.heart_rate), new LineData(dataset)));
|
||||
}
|
||||
if (hasSpeedValues && !speedDataPoints.isEmpty()) {
|
||||
if (ActivityKind.isPaceActivity(activityKind)) {
|
||||
String label = String.format("%s(%s)", context.getString(R.string.Pace), getUnitString(UNIT_SECONDS_PER_KM));
|
||||
LineDataSet dataset = createDataSet(speedDataPoints, label, Color.BLUE);
|
||||
charts.add(new WorkoutChart(context.getString(R.string.Pace), new LineData(dataset), new SpeedYLabelFormatter(UNIT_SECONDS_PER_KM)));
|
||||
} else {
|
||||
String label = String.format("%s(%s)", context.getString(R.string.Speed), getUnitString(UNIT_METERS_PER_SECOND));
|
||||
LineDataSet dataset = createDataSet(speedDataPoints, label, Color.BLUE);
|
||||
charts.add(new WorkoutChart(context.getString(R.string.Speed), new LineData(dataset), new SpeedYLabelFormatter(UNIT_METERS_PER_SECOND)));
|
||||
}
|
||||
}
|
||||
if (!elevationDataPoints.isEmpty()) {
|
||||
String label = String.format("%s(%s)", context.getString(R.string.Elevation), getUnitString(UNIT_METERS));
|
||||
LineDataSet dataset = createDataSet(elevationDataPoints, label, Color.GREEN);
|
||||
charts.add(new WorkoutChart(context.getString(R.string.Elevation), new LineData(dataset)));
|
||||
}
|
||||
}
|
||||
|
||||
final long nanoEnd = System.nanoTime();
|
||||
final long executionTime = (nanoEnd - nanoStart) / 1000000;
|
||||
@@ -218,6 +278,29 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
|
||||
);
|
||||
}
|
||||
|
||||
public String getUnitString(String unit) {
|
||||
int resId = context.getResources().getIdentifier(unit, "string", context.getPackageName());
|
||||
if (resId != 0) {
|
||||
return context.getString(resId);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public LineDataSet createDataSet(List<Entry> entities, String label, int color) {
|
||||
LineDataSet dataSet = new LineDataSet(entities, label);
|
||||
dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
|
||||
dataSet.setCubicIntensity(0.05f);
|
||||
dataSet.setDrawCircles(false);
|
||||
dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT);
|
||||
dataSet.setColor(color);
|
||||
dataSet.setValueTextColor(GBApplication.getSecondaryTextColor(context));
|
||||
dataSet.setLineWidth(1.5f);
|
||||
dataSet.setHighlightLineWidth(2f);
|
||||
dataSet.setDrawValues(false);
|
||||
dataSet.setDrawHorizontalHighlightIndicator(false);
|
||||
return dataSet;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
timesInZone.clear();
|
||||
activityPoints.clear();
|
||||
@@ -294,15 +377,6 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
|
||||
return summaryData;
|
||||
}
|
||||
|
||||
final ActivityKind activityKind;
|
||||
if (sport != null) {
|
||||
if (StringUtils.isNullOrEmpty(summary.getName())) {
|
||||
summary.setName(sport.getName());
|
||||
}
|
||||
activityKind = getActivityKind(sport.getSport(), sport.getSubSport());
|
||||
} else {
|
||||
activityKind = getActivityKind(session.getSport(), session.getSubSport());
|
||||
}
|
||||
final ActivityKind.CycleUnit cycleUnit = ActivityKind.getCycleUnit(activityKind);
|
||||
|
||||
final String weightUnit;
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.model.workout
|
||||
|
||||
import com.github.mikephil.charting.data.ChartData
|
||||
import com.github.mikephil.charting.formatter.ValueFormatter
|
||||
|
||||
data class WorkoutChart(
|
||||
data class WorkoutChart @JvmOverloads constructor(
|
||||
val title: String,
|
||||
val chartData: ChartData<*>
|
||||
val chartData: ChartData<*>,
|
||||
var chartYLabelFormatter: ValueFormatter? = null,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user