fitness: add speed-based MET tables for swimming and rowing kcal estimation
All checks were successful
CI / update (push) Successful in 2m9s
All checks were successful
CI / update (push) Successful in 2m9s
Add SWIMMING_METS and ROWING_METS lookup tables from Ainsworth Compendium for speed-based calorie estimation when distance+duration are available. Add per-exercise flat-rate fallbacks (FLAT_RATE map) instead of hardcoded if/else chains.
This commit is contained in:
@@ -259,6 +259,25 @@ function interpolateMet(table: [number, number][], speedKmh: number): number {
|
||||
return table[table.length - 1][1];
|
||||
}
|
||||
|
||||
/** Swimming METs by speed (km/h). From Compendium codes 18xxx. */
|
||||
const SWIMMING_METS: [number, number][] = [
|
||||
[1.5, 4.8], // light effort, treading
|
||||
[2.0, 5.8], // moderate, leisure
|
||||
[2.5, 8.3], // moderate-vigorous, laps
|
||||
[3.0, 9.8], // vigorous, laps
|
||||
[3.5, 10.3], // fast, competitive training
|
||||
];
|
||||
|
||||
/** Rowing METs by speed (km/h). From Compendium codes 18xxx / 15xxx. */
|
||||
const ROWING_METS: [number, number][] = [
|
||||
[6.0, 3.5], // very light
|
||||
[8.0, 4.8], // light
|
||||
[10.0, 5.8], // moderate
|
||||
[12.0, 7.0], // vigorous
|
||||
[14.0, 8.5], // very vigorous
|
||||
[16.0, 12.0], // racing
|
||||
];
|
||||
|
||||
/** Fixed MET values for activities without speed data */
|
||||
const FIXED_METS: Record<string, number> = {
|
||||
'swimming': 5.8, // Compendium 18310, moderate effort
|
||||
@@ -270,6 +289,18 @@ const FIXED_METS: Record<string, number> = {
|
||||
'cycling-indoor': 6.8, // Compendium 02014, moderate
|
||||
};
|
||||
|
||||
/** Flat-rate kcal/kg/km for distance-only fallback */
|
||||
const FLAT_RATE: Record<string, number> = {
|
||||
'running': 1.0, // Léger & Mercier
|
||||
'walking': 0.7,
|
||||
'hiking': 0.7,
|
||||
'cycling-outdoor': 0.3,
|
||||
'cycling-indoor': 0.3,
|
||||
'swimming': 1.6, // ~4× rolling resistance of running in water
|
||||
'rowing-machine': 0.6,
|
||||
'rowing-outdoor': 0.6,
|
||||
};
|
||||
|
||||
// ── Main cardio estimation interface ─────────────────────────────────────
|
||||
|
||||
export interface CardioEstimateResult {
|
||||
@@ -305,6 +336,8 @@ export function estimateCardioKcal(
|
||||
const isRunning = exerciseId === 'running';
|
||||
const isWalking = exerciseId === 'walking' || exerciseId === 'hiking';
|
||||
const isCycling = exerciseId === 'cycling-outdoor' || exerciseId === 'cycling-indoor';
|
||||
const isSwimming = exerciseId === 'swimming';
|
||||
const isRowing = exerciseId === 'rowing-machine' || exerciseId === 'rowing-outdoor';
|
||||
const isRunOrWalk = isRunning || isWalking;
|
||||
|
||||
// 1. GPS-based estimation
|
||||
@@ -324,22 +357,23 @@ export function estimateCardioKcal(
|
||||
if (distanceKm && distanceKm > 0 && durationMin && durationMin > 0) {
|
||||
const speedKmh = distanceKm / (durationMin / 60);
|
||||
|
||||
if (isRunning) {
|
||||
const met = interpolateMet(RUNNING_METS, speedKmh);
|
||||
/** @type {[number, number][] | null} */
|
||||
let metTable: [number, number][] | null = null;
|
||||
if (isRunning) metTable = RUNNING_METS;
|
||||
else if (isCycling) metTable = CYCLING_METS;
|
||||
else if (isSwimming) metTable = SWIMMING_METS;
|
||||
else if (isRowing) metTable = ROWING_METS;
|
||||
|
||||
if (metTable) {
|
||||
const met = interpolateMet(metTable, speedKmh);
|
||||
const kcal = met * bodyWeightKg * (durationMin / 60) * 1.05;
|
||||
return withUncertainty(kcal, 0.20, 'met-speed');
|
||||
}
|
||||
if (isWalking) {
|
||||
// Walking: ~3.5 METs at 5 km/h, scales roughly with speed
|
||||
const met = Math.max(2.0, 0.7 * speedKmh);
|
||||
const kcal = met * bodyWeightKg * (durationMin / 60) * 1.05;
|
||||
return withUncertainty(kcal, 0.20, 'met-speed');
|
||||
}
|
||||
if (isCycling) {
|
||||
const met = interpolateMet(CYCLING_METS, speedKmh);
|
||||
const kcal = met * bodyWeightKg * (durationMin / 60) * 1.05;
|
||||
return withUncertainty(kcal, 0.20, 'met-speed');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Duration only → fixed MET
|
||||
@@ -367,13 +401,8 @@ export function estimateCardioKcal(
|
||||
|
||||
// 4. Distance only → flat-rate kcal/kg/km
|
||||
if (distanceKm && distanceKm > 0) {
|
||||
let kcalPerKgPerKm: number;
|
||||
if (isRunning) kcalPerKgPerKm = 1.0; // Léger & Mercier
|
||||
else if (isWalking) kcalPerKgPerKm = 0.7; // walking on flat
|
||||
else if (isCycling) kcalPerKgPerKm = 0.3; // rough cycling estimate
|
||||
else kcalPerKgPerKm = 0.8; // generic cardio
|
||||
|
||||
const kcal = kcalPerKgPerKm * bodyWeightKg * distanceKm;
|
||||
const rate = FLAT_RATE[exerciseId] ?? 0.8;
|
||||
const kcal = rate * bodyWeightKg * distanceKm;
|
||||
return withUncertainty(kcal, 0.30, 'flat-rate');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user