142 lines
3.9 KiB
TypeScript
142 lines
3.9 KiB
TypeScript
export interface TelemetryConfig {
|
|
heartRateAge: number
|
|
restingHeartRateBpm: number
|
|
userWeightKg: number
|
|
}
|
|
|
|
export type HeartRateTone = 'blue' | 'purple' | 'green' | 'yellow' | 'orange' | 'red'
|
|
|
|
type HeartRateToneMeta = {
|
|
label: string
|
|
heartRateRangeText: string
|
|
speedRangeText: string
|
|
}
|
|
|
|
const HEART_RATE_TONE_META: Record<HeartRateTone, HeartRateToneMeta> = {
|
|
blue: {
|
|
label: '激活放松',
|
|
heartRateRangeText: '<=39%',
|
|
speedRangeText: '<3.2 km/h',
|
|
},
|
|
purple: {
|
|
label: '动态热身',
|
|
heartRateRangeText: '40~54%',
|
|
speedRangeText: '3.2~4.0 km/h',
|
|
},
|
|
green: {
|
|
label: '脂肪燃烧',
|
|
heartRateRangeText: '55~69%',
|
|
speedRangeText: '4.1~5.5 km/h',
|
|
},
|
|
yellow: {
|
|
label: '糖分消耗',
|
|
heartRateRangeText: '70~79%',
|
|
speedRangeText: '5.6~7.1 km/h',
|
|
},
|
|
orange: {
|
|
label: '心肺训练',
|
|
heartRateRangeText: '80~89%',
|
|
speedRangeText: '7.2~8.8 km/h',
|
|
},
|
|
red: {
|
|
label: '峰值锻炼',
|
|
heartRateRangeText: '>=90%',
|
|
speedRangeText: '>=8.9 km/h',
|
|
},
|
|
}
|
|
|
|
export function clampTelemetryAge(age: number): number {
|
|
if (!Number.isFinite(age)) {
|
|
return 30
|
|
}
|
|
|
|
return Math.max(10, Math.min(85, Math.round(age)))
|
|
}
|
|
|
|
export function estimateRestingHeartRateBpm(age: number): number {
|
|
const safeAge = clampTelemetryAge(age)
|
|
const estimated = 68 + (safeAge - 30) * 0.12
|
|
return Math.max(56, Math.min(76, Math.round(estimated)))
|
|
}
|
|
|
|
export function normalizeRestingHeartRateBpm(restingHeartRateBpm: number, age: number): number {
|
|
if (!Number.isFinite(restingHeartRateBpm) || restingHeartRateBpm <= 0) {
|
|
return estimateRestingHeartRateBpm(age)
|
|
}
|
|
|
|
return Math.max(40, Math.min(95, Math.round(restingHeartRateBpm)))
|
|
}
|
|
|
|
export function normalizeUserWeightKg(userWeightKg: number): number {
|
|
if (!Number.isFinite(userWeightKg) || userWeightKg <= 0) {
|
|
return 65
|
|
}
|
|
|
|
return Math.max(35, Math.min(180, Math.round(userWeightKg)))
|
|
}
|
|
|
|
export const DEFAULT_TELEMETRY_CONFIG: TelemetryConfig = {
|
|
heartRateAge: 30,
|
|
restingHeartRateBpm: estimateRestingHeartRateBpm(30),
|
|
userWeightKg: 65,
|
|
}
|
|
|
|
export function mergeTelemetryConfig(overrides?: Partial<TelemetryConfig> | null): TelemetryConfig {
|
|
const heartRateAge = overrides && overrides.heartRateAge !== undefined
|
|
? clampTelemetryAge(overrides.heartRateAge)
|
|
: DEFAULT_TELEMETRY_CONFIG.heartRateAge
|
|
|
|
const restingHeartRateBpm = overrides && overrides.restingHeartRateBpm !== undefined
|
|
? normalizeRestingHeartRateBpm(overrides.restingHeartRateBpm, heartRateAge)
|
|
: estimateRestingHeartRateBpm(heartRateAge)
|
|
const userWeightKg = overrides && overrides.userWeightKg !== undefined
|
|
? normalizeUserWeightKg(overrides.userWeightKg)
|
|
: DEFAULT_TELEMETRY_CONFIG.userWeightKg
|
|
|
|
return {
|
|
heartRateAge,
|
|
restingHeartRateBpm,
|
|
userWeightKg,
|
|
}
|
|
}
|
|
|
|
export function getHeartRateToneSampleBpm(tone: HeartRateTone, config: TelemetryConfig): number {
|
|
const maxHeartRate = Math.max(120, 220 - config.heartRateAge)
|
|
const restingHeartRate = Math.min(maxHeartRate - 15, config.restingHeartRateBpm)
|
|
const reserve = Math.max(20, maxHeartRate - restingHeartRate)
|
|
|
|
if (tone === 'blue') {
|
|
return Math.round(restingHeartRate + reserve * 0.3)
|
|
}
|
|
|
|
if (tone === 'purple') {
|
|
return Math.round(restingHeartRate + reserve * 0.47)
|
|
}
|
|
|
|
if (tone === 'green') {
|
|
return Math.round(restingHeartRate + reserve * 0.62)
|
|
}
|
|
|
|
if (tone === 'yellow') {
|
|
return Math.round(restingHeartRate + reserve * 0.745)
|
|
}
|
|
|
|
if (tone === 'orange') {
|
|
return Math.round(restingHeartRate + reserve * 0.845)
|
|
}
|
|
|
|
return Math.round(restingHeartRate + reserve * 0.93)
|
|
}
|
|
|
|
export function getHeartRateToneLabel(tone: HeartRateTone): string {
|
|
return HEART_RATE_TONE_META[tone].label
|
|
}
|
|
|
|
export function getHeartRateToneRangeText(tone: HeartRateTone): string {
|
|
return HEART_RATE_TONE_META[tone].heartRateRangeText
|
|
}
|
|
|
|
export function getSpeedToneRangeText(tone: HeartRateTone): string {
|
|
return HEART_RATE_TONE_META[tone].speedRangeText
|
|
}
|