fitness: add cardio kcal estimation with Minetti/Ainsworth models
Add cardioKcalEstimate.ts implementing tiered calorie estimation for cardio exercises: Minetti gradient-dependent polynomials for GPS run/walk/hike, cycling physics model, MET-based fallbacks from Ainsworth Compendium, and flat-rate estimates. Wire cardio kcal into SessionCard, workout completion screen, history detail, and stats overview API alongside existing strength kcal (Lytle). Move citation info from stats overview to clickable DOI links on workout detail kcal pill.
This commit is contained in:
@@ -6,6 +6,7 @@ import { WorkoutSession } from '$models/WorkoutSession';
|
||||
import { BodyMeasurement } from '$models/BodyMeasurement';
|
||||
import { getExerciseById, getExerciseMetrics } from '$lib/data/exercises';
|
||||
import { estimateWorkoutKcal, estimateCumulativeKcal, type ExerciseData, type Demographics } from '$lib/data/kcalEstimate';
|
||||
import { estimateCardioKcal, estimateCumulativeCardioKcal, type CardioEstimateResult } from '$lib/data/cardioKcalEstimate';
|
||||
import { FitnessGoal } from '$models/FitnessGoal';
|
||||
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
@@ -57,12 +58,14 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
// Lifetime totals: tonnage lifted + cardio km + kcal estimate
|
||||
const allSessions = await WorkoutSession.find(
|
||||
{ createdBy: user.nickname },
|
||||
{ 'exercises.exerciseId': 1, 'exercises.sets': 1 }
|
||||
{ 'exercises.exerciseId': 1, 'exercises.sets': 1, 'exercises.totalDistance': 1 }
|
||||
).lean();
|
||||
|
||||
let totalTonnage = 0;
|
||||
let totalCardioKm = 0;
|
||||
const workoutKcalResults: { kcal: number; see: number }[] = [];
|
||||
const cardioKcalResults: CardioEstimateResult[] = [];
|
||||
const bodyWeightKg = demographics.bodyWeightKg ?? 80;
|
||||
|
||||
for (const s of allSessions) {
|
||||
const strengthExercises: ExerciseData[] = [];
|
||||
@@ -72,18 +75,31 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
const isCardio = metrics.includes('distance');
|
||||
const weightMultiplier = exercise?.bilateral ? 2 : 1;
|
||||
const completedSets: { weight: number; reps: number }[] = [];
|
||||
for (const set of ex.sets) {
|
||||
if (!set.completed) continue;
|
||||
if (isCardio) {
|
||||
if (isCardio) {
|
||||
let dist = (ex as any).totalDistance ?? 0;
|
||||
let dur = 0;
|
||||
for (const set of ex.sets) {
|
||||
if (!set.completed) continue;
|
||||
if (!dist) dist += set.distance ?? 0;
|
||||
dur += set.duration ?? 0;
|
||||
totalCardioKm += set.distance ?? 0;
|
||||
} else {
|
||||
}
|
||||
if (dist > 0 || dur > 0) {
|
||||
cardioKcalResults.push(estimateCardioKcal(ex.exerciseId, bodyWeightKg, {
|
||||
distanceKm: dist || undefined,
|
||||
durationMin: dur || undefined,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
for (const set of ex.sets) {
|
||||
if (!set.completed) continue;
|
||||
const w = (set.weight ?? 0) * weightMultiplier;
|
||||
totalTonnage += w * (set.reps ?? 0);
|
||||
if (set.reps) completedSets.push({ weight: w, reps: set.reps });
|
||||
}
|
||||
}
|
||||
if (completedSets.length > 0) {
|
||||
strengthExercises.push({ exerciseId: ex.exerciseId, sets: completedSets });
|
||||
if (completedSets.length > 0) {
|
||||
strengthExercises.push({ exerciseId: ex.exerciseId, sets: completedSets });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (strengthExercises.length > 0) {
|
||||
@@ -92,7 +108,17 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const kcalEstimate = estimateCumulativeKcal(workoutKcalResults);
|
||||
const strengthKcal = estimateCumulativeKcal(workoutKcalResults);
|
||||
const cardioKcal = estimateCumulativeCardioKcal(cardioKcalResults);
|
||||
const totalKcal = strengthKcal.kcal + cardioKcal.kcal;
|
||||
const sMargin = strengthKcal.kcal - strengthKcal.lower;
|
||||
const cMargin = cardioKcal.kcal - cardioKcal.lower;
|
||||
const combinedMargin = Math.round(Math.sqrt(sMargin ** 2 + cMargin ** 2));
|
||||
const kcalEstimate = {
|
||||
kcal: totalKcal,
|
||||
lower: Math.max(0, totalKcal - combinedMargin),
|
||||
upper: totalKcal + combinedMargin,
|
||||
};
|
||||
|
||||
const weightMeasurements = await BodyMeasurement.find(
|
||||
{ createdBy: user.nickname, weight: { $ne: null } },
|
||||
|
||||
Reference in New Issue
Block a user