incyclist-services 1.7.77 → 1.7.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WORKOUT_ZONE_COLORS = void 0;
4
4
  exports.getStepDuration = getStepDuration;
5
5
  exports.getStepPower = getStepPower;
6
+ exports.getStepTargetText = getStepTargetText;
7
+ exports.getFlattenedSteps = getFlattenedSteps;
6
8
  exports.getWorkoutGraphSeries = getWorkoutGraphSeries;
7
9
  const model_1 = require("../model");
8
10
  exports.WORKOUT_ZONE_COLORS = [
@@ -95,6 +97,36 @@ function getStepPower(step, ftp) {
95
97
  const stepText = formatPowerText(low, high, unit, rangePrefix);
96
98
  return appendConvertedSuffix(stepText, low, high, unit, ftp);
97
99
  }
100
+ function resolvePowerWatts(power, ftp) {
101
+ if (!power)
102
+ return {};
103
+ const toWatts = (v) => {
104
+ if (v === undefined)
105
+ return undefined;
106
+ if (power.type === 'watt')
107
+ return Math.round(v);
108
+ return ftp === undefined ? undefined : Math.round(v / 100 * ftp);
109
+ };
110
+ return { min: toWatts(power.min), max: toWatts(power.max) };
111
+ }
112
+ function formatRange(low, high, unit, rangePrefix = '') {
113
+ if (low !== undefined && high !== undefined && Math.round(low) !== Math.round(high))
114
+ return `${rangePrefix}${Math.round(low)}-${Math.round(high)}${unit}`;
115
+ const v = high ?? low;
116
+ return v === undefined ? '' : `${Math.round(v)}${unit}`;
117
+ }
118
+ function getStepTargetText(step, ftp, powerOverrideWatts) {
119
+ const { min: minPower, max: maxPower } = powerOverrideWatts ?? resolvePowerWatts(step.power, ftp);
120
+ const ascending = step.steady || step.cooldown === false;
121
+ const lowPower = ascending ? minPower : maxPower;
122
+ const highPower = ascending ? maxPower : minPower;
123
+ const rangePrefix = step.steady ? '' : 'Ramp ';
124
+ const powerText = formatRange(lowPower, highPower, 'W', rangePrefix);
125
+ const hrText = formatRange(step.hrm?.min, step.hrm?.max, 'HR');
126
+ const cadenceText = formatRange(step.cadence?.min, step.cadence?.max, ' rpm');
127
+ const parts = [powerText, hrText, cadenceText].filter(Boolean);
128
+ return parts.length ? parts.join(' at ') : 'free';
129
+ }
98
130
  function coerceToWorkout(workout) {
99
131
  if (workout instanceof model_1.Workout)
100
132
  return workout;
@@ -139,15 +171,27 @@ function buildRampBars(wo, step, stepStart, ftp, absValues) {
139
171
  }
140
172
  return bars;
141
173
  }
142
- function getWorkoutGraphSeries(workout, opts) {
143
- const { ftp, absValues = false } = opts ?? {};
174
+ function getFlattenedSteps(workout) {
144
175
  const wo = coerceToWorkout(workout);
145
176
  if (!wo)
146
177
  return [];
147
- const bars = [];
178
+ const result = [];
148
179
  let stepStart = 0;
149
180
  for (let limits = wo.getLimits(0, true); limits !== undefined; limits = wo.getLimits(stepStart + 0.01, true)) {
150
181
  const step = limits.step;
182
+ const duration = step.getDuration();
183
+ result.push({ step, start: stepStart, end: stepStart + duration, duration });
184
+ stepStart += duration;
185
+ }
186
+ return result;
187
+ }
188
+ function getWorkoutGraphSeries(workout, opts) {
189
+ const { ftp, absValues = false } = opts ?? {};
190
+ const wo = coerceToWorkout(workout);
191
+ if (!wo)
192
+ return [];
193
+ const bars = [];
194
+ getFlattenedSteps(wo).forEach(({ step, start: stepStart }) => {
151
195
  if (step.steady) {
152
196
  const bar = buildSteadyBar(step, stepStart, ftp, absValues);
153
197
  if (bar)
@@ -156,7 +200,6 @@ function getWorkoutGraphSeries(workout, opts) {
156
200
  else {
157
201
  bars.push(...buildRampBars(wo, step, stepStart, ftp, absValues));
158
202
  }
159
- stepStart += step.getDuration();
160
- }
203
+ });
161
204
  return bars;
162
205
  }
@@ -383,48 +383,46 @@ let WorkoutRidePageService = (() => {
383
383
  };
384
384
  }
385
385
  buildUpcomingSteps(current, ftp) {
386
+ const empty = { previous: null, current: null, upcoming: [], hasMore: false };
386
387
  if (!current)
387
- return { current: null, upcoming: [] };
388
+ return empty;
388
389
  const limits = this.getWorkoutRide().getCurrentLimits();
389
390
  if (!limits)
390
- return { current: null, upcoming: [] };
391
+ return empty;
391
392
  const currentStep = {
392
- label: this.formatCurrentStepLabel(limits),
393
+ label: (0, graph_1.getStepTargetText)(limits.step ?? {}, ftp, { min: limits.minPower, max: limits.maxPower }),
393
394
  targetPower: limits.targetPower ?? null,
394
395
  duration: limits.duration,
395
396
  remaining: limits.remaining,
396
397
  isCurrent: true
397
398
  };
398
399
  const elapsedTime = this.getElapsedActivityTime();
399
- const steps = current.steps ?? [];
400
- const currentIndex = steps.findIndex(s => s.start <= elapsedTime && s.end > elapsedTime);
401
- const upcoming = currentIndex === -1
402
- ? []
403
- : steps.slice(currentIndex + 1, currentIndex + 1 + UPCOMING_STEPS_COUNT).map(step => ({
404
- label: (0, graph_1.getStepPower)(step, ftp) || 'free',
405
- targetPower: this.getStepAbsolutePower(step, ftp),
406
- duration: step.duration,
407
- remaining: null,
408
- isCurrent: false
409
- }));
410
- return { current: currentStep, upcoming };
400
+ const flattened = (0, graph_1.getFlattenedSteps)(current);
401
+ const currentIndex = flattened.findIndex(s => s.start <= elapsedTime && s.end > elapsedTime);
402
+ const toDisplay = (entry) => ({
403
+ label: (0, graph_1.getStepTargetText)(entry.step, ftp),
404
+ targetPower: this.getStepAbsolutePower(entry.step, ftp),
405
+ duration: entry.duration,
406
+ remaining: null,
407
+ isCurrent: false
408
+ });
409
+ if (currentIndex === -1)
410
+ return { previous: null, current: currentStep, upcoming: [], hasMore: false };
411
+ const previous = currentIndex > 0 ? toDisplay(flattened[currentIndex - 1]) : null;
412
+ const upcomingEntries = flattened.slice(currentIndex + 1, currentIndex + 1 + UPCOMING_STEPS_COUNT);
413
+ const upcoming = upcomingEntries.map(toDisplay);
414
+ const hasMore = currentIndex + 1 + UPCOMING_STEPS_COUNT < flattened.length;
415
+ return { previous, current: currentStep, upcoming, hasMore };
411
416
  }
412
417
  buildDashboardLine(wo) {
413
418
  const limits = this.getWorkoutRide().getCurrentLimits();
414
- return {
415
- targetPower: limits?.targetPower ?? null,
416
- targetDuration: limits?.duration ?? 0,
417
- remaining: limits?.remaining ?? 0,
418
- text: wo.title ?? '',
419
- mode: wo.mode ?? null
420
- };
421
- }
422
- formatCurrentStepLabel(limits) {
423
- if (limits.targetPower === undefined || limits.targetPower === null)
424
- return 'free';
425
- if (limits.minPower !== undefined && limits.maxPower !== undefined && limits.minPower !== limits.maxPower)
426
- return `${Math.round(limits.minPower)}-${Math.round(limits.maxPower)}W`;
427
- return `${Math.round(limits.targetPower)}W`;
419
+ const title = wo.title ?? '';
420
+ if (!limits)
421
+ return { text: title, mode: wo.mode ?? null };
422
+ const target = (0, graph_1.getStepTargetText)(limits.step ?? {}, wo.ftp, { min: limits.minPower, max: limits.maxPower });
423
+ const duration = (0, graph_1.getStepDuration)({ duration: limits.duration });
424
+ const text = title ? `${target} for ${duration} - ${title}` : `${target} for ${duration}`;
425
+ return { text, mode: wo.mode ?? null };
428
426
  }
429
427
  getStepAbsolutePower(step, ftp) {
430
428
  const p = step.power;
@@ -461,8 +459,8 @@ let WorkoutRidePageService = (() => {
461
459
  startGateProps: null,
462
460
  title: '',
463
461
  graph: { bars: [], ftp: 0, ftpLine: 0, domain: { x: [0, 0], y: [0, 0] } },
464
- steps: { current: null, upcoming: [] },
465
- dashboard: { targetPower: null, targetDuration: 0, remaining: 0, text: '', mode: null }
462
+ steps: { previous: null, current: null, upcoming: [], hasMore: false },
463
+ dashboard: { text: '', mode: null }
466
464
  };
467
465
  }
468
466
  updatePageDisplay() {
@@ -498,7 +498,7 @@ let WorkoutRide = (() => {
498
498
  request.maxCadence = limits?.cadence?.max ? Math.round(limits.cadence.max) : undefined;
499
499
  request.minHrm = limits.hrm?.min ? Math.round(limits.hrm.min) : undefined;
500
500
  request.maxHrm = limits.hrm?.max ? Math.round(limits.hrm.max) : undefined;
501
- this.currentLimits = { ...request, duration: limits.duration, remaining: limits.remaining };
501
+ this.currentLimits = { ...request, duration: limits.duration, remaining: limits.remaining, step: limits.step };
502
502
  }
503
503
  getZoomParameters(time) {
504
504
  let start, stop;
@@ -89,6 +89,36 @@ export function getStepPower(step, ftp) {
89
89
  const stepText = formatPowerText(low, high, unit, rangePrefix);
90
90
  return appendConvertedSuffix(stepText, low, high, unit, ftp);
91
91
  }
92
+ function resolvePowerWatts(power, ftp) {
93
+ if (!power)
94
+ return {};
95
+ const toWatts = (v) => {
96
+ if (v === undefined)
97
+ return undefined;
98
+ if (power.type === 'watt')
99
+ return Math.round(v);
100
+ return ftp === undefined ? undefined : Math.round(v / 100 * ftp);
101
+ };
102
+ return { min: toWatts(power.min), max: toWatts(power.max) };
103
+ }
104
+ function formatRange(low, high, unit, rangePrefix = '') {
105
+ if (low !== undefined && high !== undefined && Math.round(low) !== Math.round(high))
106
+ return `${rangePrefix}${Math.round(low)}-${Math.round(high)}${unit}`;
107
+ const v = high ?? low;
108
+ return v === undefined ? '' : `${Math.round(v)}${unit}`;
109
+ }
110
+ export function getStepTargetText(step, ftp, powerOverrideWatts) {
111
+ const { min: minPower, max: maxPower } = powerOverrideWatts ?? resolvePowerWatts(step.power, ftp);
112
+ const ascending = step.steady || step.cooldown === false;
113
+ const lowPower = ascending ? minPower : maxPower;
114
+ const highPower = ascending ? maxPower : minPower;
115
+ const rangePrefix = step.steady ? '' : 'Ramp ';
116
+ const powerText = formatRange(lowPower, highPower, 'W', rangePrefix);
117
+ const hrText = formatRange(step.hrm?.min, step.hrm?.max, 'HR');
118
+ const cadenceText = formatRange(step.cadence?.min, step.cadence?.max, ' rpm');
119
+ const parts = [powerText, hrText, cadenceText].filter(Boolean);
120
+ return parts.length ? parts.join(' at ') : 'free';
121
+ }
92
122
  function coerceToWorkout(workout) {
93
123
  if (workout instanceof Workout)
94
124
  return workout;
@@ -133,15 +163,27 @@ function buildRampBars(wo, step, stepStart, ftp, absValues) {
133
163
  }
134
164
  return bars;
135
165
  }
136
- export function getWorkoutGraphSeries(workout, opts) {
137
- const { ftp, absValues = false } = opts ?? {};
166
+ export function getFlattenedSteps(workout) {
138
167
  const wo = coerceToWorkout(workout);
139
168
  if (!wo)
140
169
  return [];
141
- const bars = [];
170
+ const result = [];
142
171
  let stepStart = 0;
143
172
  for (let limits = wo.getLimits(0, true); limits !== undefined; limits = wo.getLimits(stepStart + 0.01, true)) {
144
173
  const step = limits.step;
174
+ const duration = step.getDuration();
175
+ result.push({ step, start: stepStart, end: stepStart + duration, duration });
176
+ stepStart += duration;
177
+ }
178
+ return result;
179
+ }
180
+ export function getWorkoutGraphSeries(workout, opts) {
181
+ const { ftp, absValues = false } = opts ?? {};
182
+ const wo = coerceToWorkout(workout);
183
+ if (!wo)
184
+ return [];
185
+ const bars = [];
186
+ getFlattenedSteps(wo).forEach(({ step, start: stepStart }) => {
145
187
  if (step.steady) {
146
188
  const bar = buildSteadyBar(step, stepStart, ftp, absValues);
147
189
  if (bar)
@@ -150,7 +192,6 @@ export function getWorkoutGraphSeries(workout, opts) {
150
192
  else {
151
193
  bars.push(...buildRampBars(wo, step, stepStart, ftp, absValues));
152
194
  }
153
- stepStart += step.getDuration();
154
- }
195
+ });
155
196
  return bars;
156
197
  }
@@ -39,7 +39,7 @@ import { IncyclistPageService } from "../../../base/pages";
39
39
  import { useRideDisplay } from "../../../ride/display";
40
40
  import { useActivityRide } from "../../../activities";
41
41
  import { useWorkoutRide } from "../service";
42
- import { getStepPower, getWorkoutGraphSeries } from "../../base/graph";
42
+ import { getFlattenedSteps, getStepDuration, getStepTargetText, getWorkoutGraphSeries } from "../../base/graph";
43
43
  const BACKGROUND_PAUSE_TIMEOUT_MS = 300000;
44
44
  const UPCOMING_STEPS_COUNT = 3;
45
45
  const DEFAULT_LOAD_INCREMENT = 1;
@@ -380,48 +380,46 @@ let WorkoutRidePageService = (() => {
380
380
  };
381
381
  }
382
382
  buildUpcomingSteps(current, ftp) {
383
+ const empty = { previous: null, current: null, upcoming: [], hasMore: false };
383
384
  if (!current)
384
- return { current: null, upcoming: [] };
385
+ return empty;
385
386
  const limits = this.getWorkoutRide().getCurrentLimits();
386
387
  if (!limits)
387
- return { current: null, upcoming: [] };
388
+ return empty;
388
389
  const currentStep = {
389
- label: this.formatCurrentStepLabel(limits),
390
+ label: getStepTargetText(limits.step ?? {}, ftp, { min: limits.minPower, max: limits.maxPower }),
390
391
  targetPower: limits.targetPower ?? null,
391
392
  duration: limits.duration,
392
393
  remaining: limits.remaining,
393
394
  isCurrent: true
394
395
  };
395
396
  const elapsedTime = this.getElapsedActivityTime();
396
- const steps = current.steps ?? [];
397
- const currentIndex = steps.findIndex(s => s.start <= elapsedTime && s.end > elapsedTime);
398
- const upcoming = currentIndex === -1
399
- ? []
400
- : steps.slice(currentIndex + 1, currentIndex + 1 + UPCOMING_STEPS_COUNT).map(step => ({
401
- label: getStepPower(step, ftp) || 'free',
402
- targetPower: this.getStepAbsolutePower(step, ftp),
403
- duration: step.duration,
404
- remaining: null,
405
- isCurrent: false
406
- }));
407
- return { current: currentStep, upcoming };
397
+ const flattened = getFlattenedSteps(current);
398
+ const currentIndex = flattened.findIndex(s => s.start <= elapsedTime && s.end > elapsedTime);
399
+ const toDisplay = (entry) => ({
400
+ label: getStepTargetText(entry.step, ftp),
401
+ targetPower: this.getStepAbsolutePower(entry.step, ftp),
402
+ duration: entry.duration,
403
+ remaining: null,
404
+ isCurrent: false
405
+ });
406
+ if (currentIndex === -1)
407
+ return { previous: null, current: currentStep, upcoming: [], hasMore: false };
408
+ const previous = currentIndex > 0 ? toDisplay(flattened[currentIndex - 1]) : null;
409
+ const upcomingEntries = flattened.slice(currentIndex + 1, currentIndex + 1 + UPCOMING_STEPS_COUNT);
410
+ const upcoming = upcomingEntries.map(toDisplay);
411
+ const hasMore = currentIndex + 1 + UPCOMING_STEPS_COUNT < flattened.length;
412
+ return { previous, current: currentStep, upcoming, hasMore };
408
413
  }
409
414
  buildDashboardLine(wo) {
410
415
  const limits = this.getWorkoutRide().getCurrentLimits();
411
- return {
412
- targetPower: limits?.targetPower ?? null,
413
- targetDuration: limits?.duration ?? 0,
414
- remaining: limits?.remaining ?? 0,
415
- text: wo.title ?? '',
416
- mode: wo.mode ?? null
417
- };
418
- }
419
- formatCurrentStepLabel(limits) {
420
- if (limits.targetPower === undefined || limits.targetPower === null)
421
- return 'free';
422
- if (limits.minPower !== undefined && limits.maxPower !== undefined && limits.minPower !== limits.maxPower)
423
- return `${Math.round(limits.minPower)}-${Math.round(limits.maxPower)}W`;
424
- return `${Math.round(limits.targetPower)}W`;
416
+ const title = wo.title ?? '';
417
+ if (!limits)
418
+ return { text: title, mode: wo.mode ?? null };
419
+ const target = getStepTargetText(limits.step ?? {}, wo.ftp, { min: limits.minPower, max: limits.maxPower });
420
+ const duration = getStepDuration({ duration: limits.duration });
421
+ const text = title ? `${target} for ${duration} - ${title}` : `${target} for ${duration}`;
422
+ return { text, mode: wo.mode ?? null };
425
423
  }
426
424
  getStepAbsolutePower(step, ftp) {
427
425
  const p = step.power;
@@ -458,8 +456,8 @@ let WorkoutRidePageService = (() => {
458
456
  startGateProps: null,
459
457
  title: '',
460
458
  graph: { bars: [], ftp: 0, ftpLine: 0, domain: { x: [0, 0], y: [0, 0] } },
461
- steps: { current: null, upcoming: [] },
462
- dashboard: { targetPower: null, targetDuration: 0, remaining: 0, text: '', mode: null }
459
+ steps: { previous: null, current: null, upcoming: [], hasMore: false },
460
+ dashboard: { text: '', mode: null }
463
461
  };
464
462
  }
465
463
  updatePageDisplay() {
@@ -495,7 +495,7 @@ let WorkoutRide = (() => {
495
495
  request.maxCadence = limits?.cadence?.max ? Math.round(limits.cadence.max) : undefined;
496
496
  request.minHrm = limits.hrm?.min ? Math.round(limits.hrm.min) : undefined;
497
497
  request.maxHrm = limits.hrm?.max ? Math.round(limits.hrm.max) : undefined;
498
- this.currentLimits = { ...request, duration: limits.duration, remaining: limits.remaining };
498
+ this.currentLimits = { ...request, duration: limits.duration, remaining: limits.remaining, step: limits.step };
499
499
  }
500
500
  getZoomParameters(time) {
501
501
  let start, stop;
@@ -1,7 +1,18 @@
1
- import { Workout } from "../model";
1
+ import { Step, Workout } from "../model";
2
2
  import { StepDefinition } from "../model/types";
3
3
  import { WorkoutGraphPlanBar, WorkoutGraphSeriesOptions } from "./types";
4
4
  export declare const WORKOUT_ZONE_COLORS: string[];
5
5
  export declare function getStepDuration(step: StepDefinition): string;
6
6
  export declare function getStepPower(step: StepDefinition, ftp?: number): string;
7
+ export declare function getStepTargetText(step: StepDefinition, ftp?: number, powerOverrideWatts?: {
8
+ min?: number;
9
+ max?: number;
10
+ }): string;
11
+ export interface FlattenedStep {
12
+ step: Step;
13
+ start: number;
14
+ end: number;
15
+ duration: number;
16
+ }
17
+ export declare function getFlattenedSteps(workout: Workout): FlattenedStep[];
7
18
  export declare function getWorkoutGraphSeries(workout: Workout, opts?: WorkoutGraphSeriesOptions): WorkoutGraphPlanBar[];
@@ -1,7 +1,7 @@
1
1
  import { IncyclistPageService } from "../../../base/pages";
2
2
  import type { IObserver } from "../../../base/typedefs";
3
3
  import type { CurrentRideState, RideType } from "../../../types";
4
- import type { ActiveWorkoutLimit, WorkoutDisplayProperties } from "../types";
4
+ import type { WorkoutDisplayProperties } from "../types";
5
5
  import type { Workout } from "../../base/model";
6
6
  import type { StepDefinition } from "../../base/model/types";
7
7
  import type { IWorkoutRidePageService, WorkoutDashboardLine, WorkoutGraphActuals, WorkoutGraphPlan, WorkoutRideMenuProps, WorkoutRidePageDisplayProps, WorkoutUpcomingSteps } from "./types";
@@ -51,7 +51,6 @@ export declare class WorkoutRidePageService extends IncyclistPageService impleme
51
51
  protected buildGraphPlan(current: Workout | undefined, ftp: number): WorkoutGraphPlan;
52
52
  protected buildUpcomingSteps(current: Workout | undefined, ftp: number): WorkoutUpcomingSteps;
53
53
  protected buildDashboardLine(wo: WorkoutDisplayProperties): WorkoutDashboardLine;
54
- protected formatCurrentStepLabel(limits: ActiveWorkoutLimit): string;
55
54
  protected getStepAbsolutePower(step: StepDefinition, ftp?: number): number | null;
56
55
  protected getStepFlags(): {
57
56
  canStepBack: boolean;
@@ -28,13 +28,12 @@ export interface WorkoutStepDisplay {
28
28
  isCurrent: boolean;
29
29
  }
30
30
  export interface WorkoutUpcomingSteps {
31
+ previous: WorkoutStepDisplay | null;
31
32
  current: WorkoutStepDisplay | null;
32
33
  upcoming: WorkoutStepDisplay[];
34
+ hasMore: boolean;
33
35
  }
34
36
  export interface WorkoutDashboardLine {
35
- targetPower: number | null;
36
- targetDuration: number;
37
- remaining: number;
38
37
  text: string;
39
38
  mode: string | null;
40
39
  }
@@ -1,4 +1,5 @@
1
1
  import { Workout } from "../base/model";
2
+ import { StepDefinition } from "../base/model/types";
2
3
  export interface WorkoutRequest {
3
4
  time: number;
4
5
  minPower?: number;
@@ -12,6 +13,7 @@ export interface WorkoutRequest {
12
13
  export interface ActiveWorkoutLimit extends WorkoutRequest {
13
14
  duration: number;
14
15
  remaining: number;
16
+ step?: StepDefinition;
15
17
  }
16
18
  export interface WorkoutDisplayProperties {
17
19
  workout?: Workout;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incyclist-services",
3
- "version": "1.7.77",
3
+ "version": "1.7.78",
4
4
  "peerDependencies": {
5
5
  "gd-eventlog": "^0.1.27"
6
6
  },