mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Add distinct VO2 Max ranges per age / gender
This commit is contained in:
+8
-19
@@ -41,6 +41,8 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
|
||||
@@ -58,6 +60,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.Vo2MaxSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample;
|
||||
|
||||
|
||||
@@ -180,25 +183,26 @@ public class VO2MaxFragment extends AbstractChartFragment<VO2MaxFragment.VO2MaxD
|
||||
});
|
||||
final int[] colors = AbstractDashboardVO2MaxWidget.getColors();
|
||||
final float[] segments = AbstractDashboardVO2MaxWidget.getSegments();
|
||||
float[] vo2MaxRanges = AbstractDashboardVO2MaxWidget.getVO2MaxRanges();
|
||||
final ActivityUser activityUser = new ActivityUser();
|
||||
final int age = activityUser.getAgeAt(LocalDate.ofInstant(getEndDate().toInstant(), ZoneId.systemDefault()));
|
||||
final List<ILineDataSet> lineDataSets = new ArrayList<>();
|
||||
if (supportsVO2MultiSport(device)) {
|
||||
// Running
|
||||
VO2MaxRecord latestRunningRecord = vo2MaxData.getLatestValue(Vo2MaxSample.Type.RUNNING);
|
||||
float runningVO2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, latestRunningRecord != null ? latestRunningRecord.value : 0);
|
||||
float runningVO2MaxValue = VO2MaxRanges.INSTANCE.calculateVO2MaxPercentile(latestRunningRecord != null ? latestRunningRecord.value : 0, age, activityUser.getGender());
|
||||
vo2MaxRunningValue.setText(String.valueOf(latestRunningRecord != null ? Math.round(latestRunningRecord.value) : "-"));
|
||||
gaugeDrawer.drawSegmentedGauge(vo2MaxRunningGauge, colors, segments, runningVO2MaxValue, false, true);
|
||||
lineDataSets.add(createDataSet(runningEntries, getResources().getColor(R.color.vo2max_running_char_line_color), getString(R.string.vo2max_running)));
|
||||
|
||||
// Cycling
|
||||
VO2MaxRecord latestCyclingRecord = vo2MaxData.getLatestValue(Vo2MaxSample.Type.CYCLING);
|
||||
float cyclingVO2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, latestCyclingRecord != null ? latestCyclingRecord.value : 0);
|
||||
float cyclingVO2MaxValue = VO2MaxRanges.INSTANCE.calculateVO2MaxPercentile(latestCyclingRecord != null ? latestCyclingRecord.value : 0, age, activityUser.getGender());
|
||||
gaugeDrawer.drawSegmentedGauge(vo2MaxCyclingGauge, colors, segments, cyclingVO2MaxValue, false, true);
|
||||
vo2MaxCyclingValue.setText(String.valueOf(latestCyclingRecord != null ? Math.round(latestCyclingRecord.value) : "-"));
|
||||
lineDataSets.add(createDataSet(cyclingEntries, getResources().getColor(R.color.vo2max_cycling_char_line_color), getString(R.string.vo2max_cycling)));
|
||||
} else {
|
||||
VO2MaxRecord latestRecord = vo2MaxData.getLatestValue(Vo2MaxSample.Type.ANY);
|
||||
float vO2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, latestRecord != null ? latestRecord.value : 0);
|
||||
float vO2MaxValue = VO2MaxRanges.INSTANCE.calculateVO2MaxPercentile(latestRecord != null ? latestRecord.value : 0, age, activityUser.getGender());
|
||||
gaugeDrawer.drawSegmentedGauge(vo2MaxGauge, colors, segments, vO2MaxValue, false, true);
|
||||
vo2MaxValue.setText(String.valueOf(latestRecord != null ? Math.round(latestRecord.value) : "-"));
|
||||
lineDataSets.add(createDataSet(allEntries, getResources().getColor(R.color.vo2max_running_char_line_color), getString(R.string.menuitem_vo2_max)));
|
||||
@@ -220,21 +224,6 @@ public class VO2MaxFragment extends AbstractChartFragment<VO2MaxFragment.VO2MaxD
|
||||
};
|
||||
}
|
||||
|
||||
private float calculateVO2maxGaugeValue(float[] vo2MaxRanges, float vo2MaxValue) {
|
||||
float value = -1;
|
||||
for (int i = 0; i < vo2MaxRanges.length; i++) {
|
||||
if (vo2MaxValue - vo2MaxRanges[i] > 0) {
|
||||
float rangeValue = i - 1 >= 0 ? vo2MaxRanges[i-1] : 60F;
|
||||
float rangeDiff = rangeValue - vo2MaxRanges[i];
|
||||
float valueDiff = vo2MaxValue - vo2MaxRanges[i];
|
||||
float multiplayer = valueDiff / rangeDiff;
|
||||
value = (4 - i) * 0.2F + 0.2F * (multiplayer > 1 ? 1 : multiplayer) ;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected LineDataSet createDataSet(final List<Entry> values, int color, String label) {
|
||||
final LineDataSet lineDataSet = new LineDataSet(values, label);
|
||||
lineDataSet.setColor(color);
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser
|
||||
|
||||
object VO2MaxRanges {
|
||||
// Percentile boundaries: superior (95-100), excellent (80-95), good (60-80), fair (40-60), poor (0-40)
|
||||
val PERCENTILES = floatArrayOf(0.95f, 0.80f, 0.60f, 0.40f, 0.0f)
|
||||
|
||||
fun calculateVO2MaxPercentile(vo2MaxValue: Float, age: Int, gender: Int): Float {
|
||||
val ranges = getVO2MaxRanges(age, gender)
|
||||
|
||||
// Determine which band the value falls into (0=superior, 1=excellent, 2=good, 3=fair, 4=poor)
|
||||
val bandIndex = when {
|
||||
vo2MaxValue >= ranges[0] -> 0
|
||||
vo2MaxValue >= ranges[1] -> 1
|
||||
vo2MaxValue >= ranges[2] -> 2
|
||||
vo2MaxValue >= ranges[3] -> 3
|
||||
else -> 4
|
||||
}
|
||||
|
||||
// Get the upper and lower bounds for this band
|
||||
val upperBound = if (bandIndex == 0) {
|
||||
// Superior band. Highest recorded VO2 Max
|
||||
97.5f
|
||||
} else {
|
||||
ranges[bandIndex - 1]
|
||||
}
|
||||
val lowerBound = ranges[bandIndex]
|
||||
|
||||
// Scale the percentile within the band
|
||||
val positionInBand = if (upperBound > lowerBound) {
|
||||
((vo2MaxValue - lowerBound) / (upperBound - lowerBound)).coerceIn(0f, 1f)
|
||||
} else {
|
||||
0.5f
|
||||
}
|
||||
|
||||
return PERCENTILES[bandIndex] + (positionInBand * (PERCENTILES[bandIndex - 1] - PERCENTILES[bandIndex]))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a 5-element float array with VO2 max lower bounds (inclusive) for each level. The levels are ordered from highest to
|
||||
* lowest: superior, excellent, good, fair, poor.
|
||||
*/
|
||||
fun getVO2MaxRanges(age: Int, gender: Int): FloatArray {
|
||||
return when (gender) {
|
||||
ActivityUser.GENDER_MALE -> when {
|
||||
age <= 29 -> floatArrayOf(55.4f, 51.1f, 45.4f, 41.7f, 0f)
|
||||
age in 30..39 -> floatArrayOf(54f, 48.3f, 44f, 40.5f, 0f)
|
||||
age in 40..49 -> floatArrayOf(52.5f, 46.4f, 42.4f, 38.5f, 0f)
|
||||
age in 50..59 -> floatArrayOf(48.9f, 43.4f, 39.2f, 35.6f, 0f)
|
||||
age in 60..69 -> floatArrayOf(45.7f, 39.5f, 35.5f, 32.3f, 0f)
|
||||
else -> floatArrayOf(42.1f, 36.7f, 32.3f, 29.4f, 0f)
|
||||
}
|
||||
|
||||
ActivityUser.GENDER_FEMALE -> when {
|
||||
age <= 29 -> floatArrayOf(49.6f, 43.9f, 39.5f, 36.1f, 0f)
|
||||
age in 30..39 -> floatArrayOf(47.4f, 42.4f, 37.8f, 34.4f, 0f)
|
||||
age in 40..49 -> floatArrayOf(45.3f, 39.7f, 36.3f, 33f, 0f)
|
||||
age in 50..59 -> floatArrayOf(41.1f, 36.7f, 33f, 30.1f, 0f)
|
||||
age in 60..69 -> floatArrayOf(37.8f, 33f, 30f, 27.5f, 0f)
|
||||
else -> floatArrayOf(36.7f, 30.9f, 28.1f, 25.9f, 0f)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Average them (?)
|
||||
val rangesMale = getVO2MaxRanges(age, ActivityUser.GENDER_MALE)
|
||||
val rangesFemale = getVO2MaxRanges(age, ActivityUser.GENDER_FEMALE)
|
||||
floatArrayOf(
|
||||
(rangesMale[0] + rangesFemale[0]) / 2f,
|
||||
(rangesMale[1] + rangesFemale[1]) / 2f,
|
||||
(rangesMale[2] + rangesFemale[2]) / 2f,
|
||||
(rangesMale[3] + rangesFemale[3]) / 2f,
|
||||
(rangesMale[4] + rangesFemale[4]) / 2f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-30
@@ -7,14 +7,19 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.VO2MaxRanges;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.Vo2MaxSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample;
|
||||
|
||||
public abstract class AbstractDashboardVO2MaxWidget extends AbstractGaugeWidget implements DashboardVO2MaxWidgetInterface {
|
||||
@@ -62,22 +67,13 @@ public abstract class AbstractDashboardVO2MaxWidget extends AbstractGaugeWidget
|
||||
}
|
||||
|
||||
public static float[] getSegments() {
|
||||
// Should match the percentiles in VO2MaxRanges
|
||||
return new float[] {
|
||||
0.40F,
|
||||
0.20F,
|
||||
0.20F,
|
||||
0.20F,
|
||||
0.20F,
|
||||
0.20F,
|
||||
};
|
||||
}
|
||||
|
||||
public static float[] getVO2MaxRanges() {
|
||||
return new float[] {
|
||||
55.4F,
|
||||
51.1F,
|
||||
45.4F,
|
||||
41.7F,
|
||||
0.0F,
|
||||
0.15F,
|
||||
0.05F,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,8 +87,9 @@ public abstract class AbstractDashboardVO2MaxWidget extends AbstractGaugeWidget
|
||||
|
||||
final int[] colors = getColors();
|
||||
final float[] segments = getSegments();
|
||||
final float[] vo2MaxRanges = getVO2MaxRanges();
|
||||
float vo2MaxValue = calculateVO2maxGaugeValue(vo2MaxRanges, vo2MaxData.value != -1 ? vo2MaxData.value : 0);
|
||||
final ActivityUser activityUser = new ActivityUser();
|
||||
final int age = activityUser.getAgeAt(LocalDate.ofInstant(Instant.ofEpochSecond(dashboardData.timeTo), ZoneId.systemDefault()));
|
||||
float vo2MaxValue = VO2MaxRanges.INSTANCE.calculateVO2MaxPercentile(vo2MaxData.value != -1 ? vo2MaxData.value : 0, age, activityUser.getGender());
|
||||
setText(String.valueOf(vo2MaxData.value != -1 ? Math.round(vo2MaxData.value) : "-"));
|
||||
drawSegmentedGauge(
|
||||
colors,
|
||||
@@ -103,21 +100,6 @@ public abstract class AbstractDashboardVO2MaxWidget extends AbstractGaugeWidget
|
||||
);
|
||||
}
|
||||
|
||||
private float calculateVO2maxGaugeValue(float[] vo2MaxRanges, float vo2MaxValue) {
|
||||
float value = -1;
|
||||
for (int i = 0; i < vo2MaxRanges.length; i++) {
|
||||
if (vo2MaxValue - vo2MaxRanges[i] > 0) {
|
||||
float rangeValue = i - 1 >= 0 ? vo2MaxRanges[i-1] : 60F;
|
||||
float rangeDiff = rangeValue - vo2MaxRanges[i];
|
||||
float valueDiff = vo2MaxValue - vo2MaxRanges[i];
|
||||
float multiplayer = valueDiff / rangeDiff;
|
||||
value = (4 - i) * 0.2F + 0.2F * (multiplayer > 1 ? 1 : multiplayer) ;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static class VO2MaxData implements Serializable {
|
||||
private float value = -1;
|
||||
}
|
||||
|
||||
@@ -147,7 +147,11 @@ public class ActivityUser {
|
||||
}
|
||||
|
||||
public int getAge() {
|
||||
return Period.between(getDateOfBirth(), LocalDate.now()).getYears();
|
||||
return getAgeAt(LocalDate.now());
|
||||
}
|
||||
|
||||
public int getAgeAt(final LocalDate when) {
|
||||
return Period.between(getDateOfBirth(), when).getYears();
|
||||
}
|
||||
|
||||
private void fetchPreferences() {
|
||||
|
||||
Reference in New Issue
Block a user