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];
|
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 */
|
/** Fixed MET values for activities without speed data */
|
||||||
const FIXED_METS: Record<string, number> = {
|
const FIXED_METS: Record<string, number> = {
|
||||||
'swimming': 5.8, // Compendium 18310, moderate effort
|
'swimming': 5.8, // Compendium 18310, moderate effort
|
||||||
@@ -270,6 +289,18 @@ const FIXED_METS: Record<string, number> = {
|
|||||||
'cycling-indoor': 6.8, // Compendium 02014, moderate
|
'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 ─────────────────────────────────────
|
// ── Main cardio estimation interface ─────────────────────────────────────
|
||||||
|
|
||||||
export interface CardioEstimateResult {
|
export interface CardioEstimateResult {
|
||||||
@@ -305,6 +336,8 @@ export function estimateCardioKcal(
|
|||||||
const isRunning = exerciseId === 'running';
|
const isRunning = exerciseId === 'running';
|
||||||
const isWalking = exerciseId === 'walking' || exerciseId === 'hiking';
|
const isWalking = exerciseId === 'walking' || exerciseId === 'hiking';
|
||||||
const isCycling = exerciseId === 'cycling-outdoor' || exerciseId === 'cycling-indoor';
|
const isCycling = exerciseId === 'cycling-outdoor' || exerciseId === 'cycling-indoor';
|
||||||
|
const isSwimming = exerciseId === 'swimming';
|
||||||
|
const isRowing = exerciseId === 'rowing-machine' || exerciseId === 'rowing-outdoor';
|
||||||
const isRunOrWalk = isRunning || isWalking;
|
const isRunOrWalk = isRunning || isWalking;
|
||||||
|
|
||||||
// 1. GPS-based estimation
|
// 1. GPS-based estimation
|
||||||
@@ -324,22 +357,23 @@ export function estimateCardioKcal(
|
|||||||
if (distanceKm && distanceKm > 0 && durationMin && durationMin > 0) {
|
if (distanceKm && distanceKm > 0 && durationMin && durationMin > 0) {
|
||||||
const speedKmh = distanceKm / (durationMin / 60);
|
const speedKmh = distanceKm / (durationMin / 60);
|
||||||
|
|
||||||
if (isRunning) {
|
/** @type {[number, number][] | null} */
|
||||||
const met = interpolateMet(RUNNING_METS, speedKmh);
|
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;
|
const kcal = met * bodyWeightKg * (durationMin / 60) * 1.05;
|
||||||
return withUncertainty(kcal, 0.20, 'met-speed');
|
return withUncertainty(kcal, 0.20, 'met-speed');
|
||||||
}
|
}
|
||||||
if (isWalking) {
|
if (isWalking) {
|
||||||
// Walking: ~3.5 METs at 5 km/h, scales roughly with speed
|
|
||||||
const met = Math.max(2.0, 0.7 * speedKmh);
|
const met = Math.max(2.0, 0.7 * speedKmh);
|
||||||
const kcal = met * bodyWeightKg * (durationMin / 60) * 1.05;
|
const kcal = met * bodyWeightKg * (durationMin / 60) * 1.05;
|
||||||
return withUncertainty(kcal, 0.20, 'met-speed');
|
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
|
// 3. Duration only → fixed MET
|
||||||
@@ -367,13 +401,8 @@ export function estimateCardioKcal(
|
|||||||
|
|
||||||
// 4. Distance only → flat-rate kcal/kg/km
|
// 4. Distance only → flat-rate kcal/kg/km
|
||||||
if (distanceKm && distanceKm > 0) {
|
if (distanceKm && distanceKm > 0) {
|
||||||
let kcalPerKgPerKm: number;
|
const rate = FLAT_RATE[exerciseId] ?? 0.8;
|
||||||
if (isRunning) kcalPerKgPerKm = 1.0; // Léger & Mercier
|
const kcal = rate * bodyWeightKg * distanceKm;
|
||||||
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;
|
|
||||||
return withUncertainty(kcal, 0.30, 'flat-rate');
|
return withUncertainty(kcal, 0.30, 'flat-rate');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user