@kaiord/zwo 4.0.0
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.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +1202 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
import { createZwiftParsingError, createConsoleLogger, createZwiftValidationError, intensitySchema, targetTypeSchema, targetUnitSchema, durationTypeSchema } from '@kaiord/core';
|
|
2
|
+
import { XMLParser, XMLValidator, XMLBuilder } from 'fast-xml-parser';
|
|
3
|
+
|
|
4
|
+
// src/providers.ts
|
|
5
|
+
var detectIntervalType = (step) => {
|
|
6
|
+
if (step.target.type === targetTypeSchema.enum.open) {
|
|
7
|
+
return "FreeRide";
|
|
8
|
+
}
|
|
9
|
+
if (step.target.type === targetTypeSchema.enum.power) {
|
|
10
|
+
const powerValue = step.target.value;
|
|
11
|
+
if (powerValue.unit === targetUnitSchema.enum.range) {
|
|
12
|
+
if (step.intensity === intensitySchema.enum.warmup) {
|
|
13
|
+
return "Warmup";
|
|
14
|
+
}
|
|
15
|
+
if (step.intensity === intensitySchema.enum.cooldown) {
|
|
16
|
+
return "Cooldown";
|
|
17
|
+
}
|
|
18
|
+
return "Ramp";
|
|
19
|
+
}
|
|
20
|
+
if (powerValue.unit === targetUnitSchema.enum.percent_ftp || powerValue.unit === targetUnitSchema.enum.watts) {
|
|
21
|
+
return "SteadyState";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return "SteadyState";
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/adapters/krd-to-zwift/text-events-encoder.ts
|
|
28
|
+
var encodeTextEvents = (step) => {
|
|
29
|
+
const stepExtensions = step.extensions?.zwift;
|
|
30
|
+
const textEvents = stepExtensions?.textEvents;
|
|
31
|
+
if (!textEvents || textEvents.length === 0) {
|
|
32
|
+
return void 0;
|
|
33
|
+
}
|
|
34
|
+
if (textEvents.length === 1) {
|
|
35
|
+
const event = textEvents[0];
|
|
36
|
+
const encoded = {
|
|
37
|
+
"@_message": event.message
|
|
38
|
+
};
|
|
39
|
+
if (event.timeoffset !== void 0) {
|
|
40
|
+
encoded["@_timeoffset"] = event.timeoffset;
|
|
41
|
+
}
|
|
42
|
+
if (event.distoffset !== void 0) {
|
|
43
|
+
encoded["@_distoffset"] = event.distoffset;
|
|
44
|
+
}
|
|
45
|
+
return encoded;
|
|
46
|
+
}
|
|
47
|
+
return textEvents.map((event) => {
|
|
48
|
+
const encoded = {
|
|
49
|
+
"@_message": event.message
|
|
50
|
+
};
|
|
51
|
+
if (event.timeoffset !== void 0) {
|
|
52
|
+
encoded["@_timeoffset"] = event.timeoffset;
|
|
53
|
+
}
|
|
54
|
+
if (event.distoffset !== void 0) {
|
|
55
|
+
encoded["@_distoffset"] = event.distoffset;
|
|
56
|
+
}
|
|
57
|
+
return encoded;
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/adapters/krd-to-zwift/intervals-t-encoder.ts
|
|
62
|
+
var encodeDurations = (onStep, offStep, intervalsT) => {
|
|
63
|
+
if (onStep.duration.type === "time") {
|
|
64
|
+
intervalsT["@_OnDuration"] = onStep.duration.seconds;
|
|
65
|
+
} else if (onStep.duration.type === "distance") {
|
|
66
|
+
intervalsT["@_OnDuration"] = onStep.duration.meters;
|
|
67
|
+
}
|
|
68
|
+
if (offStep.duration.type === "time") {
|
|
69
|
+
intervalsT["@_OffDuration"] = offStep.duration.seconds;
|
|
70
|
+
} else if (offStep.duration.type === "distance") {
|
|
71
|
+
intervalsT["@_OffDuration"] = offStep.duration.meters;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var encodePowerTargets = (onStep, offStep, intervalsT) => {
|
|
75
|
+
if (onStep.target.type === "power" && onStep.target.value.unit === "percent_ftp") {
|
|
76
|
+
intervalsT["@_OnPower"] = onStep.target.value.value / 100;
|
|
77
|
+
}
|
|
78
|
+
if (offStep.target.type === "power" && offStep.target.value.unit === "percent_ftp") {
|
|
79
|
+
intervalsT["@_OffPower"] = offStep.target.value.value / 100;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var encodeCadenceTargets = (onStep, offStep, intervalsT) => {
|
|
83
|
+
if (onStep.target.type === "cadence" && onStep.target.value.unit === "rpm") {
|
|
84
|
+
intervalsT["@_Cadence"] = onStep.target.value.value;
|
|
85
|
+
} else {
|
|
86
|
+
const onStepExtensions = onStep.extensions?.zwift;
|
|
87
|
+
const cadence = onStepExtensions?.cadence;
|
|
88
|
+
if (cadence !== void 0) {
|
|
89
|
+
intervalsT["@_Cadence"] = cadence;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (offStep.target.type === "cadence" && offStep.target.value.unit === "rpm") {
|
|
93
|
+
intervalsT["@_CadenceResting"] = offStep.target.value.value;
|
|
94
|
+
} else {
|
|
95
|
+
const offStepExtensions = offStep.extensions?.zwift;
|
|
96
|
+
const cadenceResting = offStepExtensions?.cadence;
|
|
97
|
+
if (cadenceResting !== void 0) {
|
|
98
|
+
intervalsT["@_CadenceResting"] = cadenceResting;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var encodeIntervalsT = (repetitionBlock) => {
|
|
103
|
+
const onStep = repetitionBlock.steps[0];
|
|
104
|
+
const offStep = repetitionBlock.steps[1];
|
|
105
|
+
const intervalsT = {
|
|
106
|
+
"@_Repeat": repetitionBlock.repeatCount
|
|
107
|
+
};
|
|
108
|
+
encodeDurations(onStep, offStep, intervalsT);
|
|
109
|
+
encodePowerTargets(onStep, offStep, intervalsT);
|
|
110
|
+
encodeCadenceTargets(onStep, offStep, intervalsT);
|
|
111
|
+
const textEvents = encodeTextEvents(onStep);
|
|
112
|
+
if (textEvents) {
|
|
113
|
+
intervalsT.textevent = textEvents;
|
|
114
|
+
}
|
|
115
|
+
return intervalsT;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/adapters/krd-to-zwift/duration-encoder.ts
|
|
119
|
+
var encodeDuration = (step, interval, logger) => {
|
|
120
|
+
if (step.duration.type === "time") {
|
|
121
|
+
interval["@_Duration"] = step.duration.seconds;
|
|
122
|
+
} else if (step.duration.type === "distance") {
|
|
123
|
+
interval["@_Duration"] = step.duration.meters;
|
|
124
|
+
interval["@_kaiord:originalDurationType"] = "distance";
|
|
125
|
+
interval["@_kaiord:originalDurationMeters"] = step.duration.meters;
|
|
126
|
+
logger?.warn("Lossy conversion: distance duration converted to time", {
|
|
127
|
+
originalMeters: step.duration.meters,
|
|
128
|
+
convertedSeconds: step.duration.meters,
|
|
129
|
+
stepIndex: step.stepIndex
|
|
130
|
+
});
|
|
131
|
+
} else if (step.duration.type === "open") {
|
|
132
|
+
interval["@_Duration"] = 0;
|
|
133
|
+
} else {
|
|
134
|
+
interval["@_Duration"] = 300;
|
|
135
|
+
interval["@_kaiord:originalDurationType"] = step.duration.type;
|
|
136
|
+
if ("bpm" in step.duration) {
|
|
137
|
+
interval["@_kaiord:originalDurationBpm"] = step.duration.bpm;
|
|
138
|
+
} else if ("watts" in step.duration) {
|
|
139
|
+
interval["@_kaiord:originalDurationWatts"] = step.duration.watts;
|
|
140
|
+
}
|
|
141
|
+
logger?.warn("Lossy conversion: unsupported duration type", {
|
|
142
|
+
originalType: step.duration.type,
|
|
143
|
+
fallbackSeconds: 300,
|
|
144
|
+
stepIndex: step.stepIndex
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/adapters/krd-to-zwift/metadata-encoder.ts
|
|
150
|
+
var encodeHrRange = (step, interval, logger) => {
|
|
151
|
+
if (step.target.type === "heart_rate" && step.target.value.unit === "range") {
|
|
152
|
+
interval["@_kaiord:hrTargetLow"] = step.target.value.min;
|
|
153
|
+
interval["@_kaiord:hrTargetHigh"] = step.target.value.max;
|
|
154
|
+
logger?.warn("Lossy conversion: heart rate target not supported by Zwift", {
|
|
155
|
+
hrRange: { low: step.target.value.min, high: step.target.value.max },
|
|
156
|
+
stepIndex: step.stepIndex
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
var encodeHrBpm = (step, interval, logger) => {
|
|
161
|
+
if (step.target.type === "heart_rate" && step.target.value.unit === "bpm") {
|
|
162
|
+
interval["@_kaiord:hrTargetBpm"] = step.target.value.value;
|
|
163
|
+
logger?.warn("Lossy conversion: heart rate target not supported by Zwift", {
|
|
164
|
+
hrBpm: step.target.value.value,
|
|
165
|
+
stepIndex: step.stepIndex
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var encodeHrZone = (step, interval, logger) => {
|
|
170
|
+
if (step.target.type === "heart_rate" && step.target.value.unit === "zone") {
|
|
171
|
+
interval["@_kaiord:hrTargetZone"] = step.target.value.value;
|
|
172
|
+
logger?.warn("Lossy conversion: heart rate target not supported by Zwift", {
|
|
173
|
+
hrZone: step.target.value.value,
|
|
174
|
+
stepIndex: step.stepIndex
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
var encodeHrPercentMax = (step, interval, logger) => {
|
|
179
|
+
if (step.target.type === "heart_rate" && step.target.value.unit === "percent_max") {
|
|
180
|
+
interval["@_kaiord:hrTargetPercentMax"] = step.target.value.value;
|
|
181
|
+
logger?.warn("Lossy conversion: heart rate target not supported by Zwift", {
|
|
182
|
+
hrPercentMax: step.target.value.value,
|
|
183
|
+
stepIndex: step.stepIndex
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var encodeHeartRateTarget = (step, interval, logger) => {
|
|
188
|
+
encodeHrRange(step, interval, logger);
|
|
189
|
+
encodeHrBpm(step, interval, logger);
|
|
190
|
+
encodeHrZone(step, interval, logger);
|
|
191
|
+
encodeHrPercentMax(step, interval, logger);
|
|
192
|
+
};
|
|
193
|
+
var encodeMetadata = (step, interval) => {
|
|
194
|
+
if (step.name) {
|
|
195
|
+
interval["@_kaiord:name"] = step.name;
|
|
196
|
+
}
|
|
197
|
+
if (step.intensity) {
|
|
198
|
+
interval["@_kaiord:intensity"] = step.intensity;
|
|
199
|
+
}
|
|
200
|
+
if (step.equipment) {
|
|
201
|
+
interval["@_kaiord:equipment"] = step.equipment;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
var convertZwiftPowerTarget = (ftpPercentage) => {
|
|
205
|
+
return {
|
|
206
|
+
type: targetTypeSchema.enum.power,
|
|
207
|
+
value: {
|
|
208
|
+
unit: "percent_ftp",
|
|
209
|
+
value: ftpPercentage * 100
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
var convertZwiftPowerRange = (powerLow, powerHigh) => {
|
|
214
|
+
return {
|
|
215
|
+
type: targetTypeSchema.enum.power,
|
|
216
|
+
value: {
|
|
217
|
+
unit: "range",
|
|
218
|
+
min: powerLow * 100,
|
|
219
|
+
max: powerHigh * 100
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
var convertPowerZoneToPercentFtp = (zone) => {
|
|
224
|
+
const zoneMap = {
|
|
225
|
+
1: 55,
|
|
226
|
+
2: 75,
|
|
227
|
+
3: 90,
|
|
228
|
+
4: 105,
|
|
229
|
+
5: 120,
|
|
230
|
+
6: 150,
|
|
231
|
+
7: 200
|
|
232
|
+
};
|
|
233
|
+
return zoneMap[zone] || 100;
|
|
234
|
+
};
|
|
235
|
+
var convertZwiftCadenceTarget = (cadence, isRunning = false) => {
|
|
236
|
+
const rpm = isRunning ? cadence / 2 : cadence;
|
|
237
|
+
return {
|
|
238
|
+
type: targetTypeSchema.enum.cadence,
|
|
239
|
+
value: {
|
|
240
|
+
unit: "rpm",
|
|
241
|
+
value: rpm
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// src/adapters/krd-to-zwift/power-encoder.ts
|
|
247
|
+
var encodeSteadyStatePowerTarget = (step, interval) => {
|
|
248
|
+
if (step.target.type !== "power") return;
|
|
249
|
+
if (step.target.value.unit === "percent_ftp") {
|
|
250
|
+
interval["@_kaiord:powerUnit"] = "percent_ftp";
|
|
251
|
+
interval["@_Power"] = step.target.value.value / 100;
|
|
252
|
+
} else if (step.target.value.unit === "zone") {
|
|
253
|
+
interval["@_kaiord:powerUnit"] = "zone";
|
|
254
|
+
interval["@_kaiord:powerZone"] = step.target.value.value;
|
|
255
|
+
const percentFtp = convertPowerZoneToPercentFtp(step.target.value.value);
|
|
256
|
+
interval["@_Power"] = percentFtp / 100;
|
|
257
|
+
} else if (step.target.value.unit === "watts") {
|
|
258
|
+
interval["@_kaiord:powerUnit"] = "watts";
|
|
259
|
+
interval["@_kaiord:originalWatts"] = step.target.value.value;
|
|
260
|
+
const assumedFtp = 250;
|
|
261
|
+
interval["@_kaiord:assumedFtp"] = assumedFtp;
|
|
262
|
+
const percentFtp = step.target.value.value / assumedFtp * 100;
|
|
263
|
+
interval["@_Power"] = percentFtp / 100;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
var encodeRampPowerTarget = (step, interval, logger) => {
|
|
267
|
+
if (step.target.type !== "power") return;
|
|
268
|
+
if (step.target.value.unit === "range") {
|
|
269
|
+
let powerLow = step.target.value.min;
|
|
270
|
+
let powerHigh = step.target.value.max;
|
|
271
|
+
interval["@_kaiord:powerUnit"] = "watts";
|
|
272
|
+
const assumedFtp = 250;
|
|
273
|
+
const originalLow = powerLow;
|
|
274
|
+
const originalHigh = powerHigh;
|
|
275
|
+
powerLow = powerLow / assumedFtp * 100;
|
|
276
|
+
powerHigh = powerHigh / assumedFtp * 100;
|
|
277
|
+
interval["@_kaiord:originalWattsLow"] = originalLow;
|
|
278
|
+
interval["@_kaiord:originalWattsHigh"] = originalHigh;
|
|
279
|
+
interval["@_kaiord:assumedFtp"] = assumedFtp;
|
|
280
|
+
logger?.warn("Lossy conversion: watts converted to percent FTP", {
|
|
281
|
+
originalWatts: { low: originalLow, high: originalHigh },
|
|
282
|
+
assumedFtp,
|
|
283
|
+
convertedPercentFtp: { low: powerLow, high: powerHigh },
|
|
284
|
+
stepIndex: step.stepIndex
|
|
285
|
+
});
|
|
286
|
+
interval["@_PowerLow"] = powerLow / 100;
|
|
287
|
+
interval["@_PowerHigh"] = powerHigh / 100;
|
|
288
|
+
} else if (step.target.value.unit === "zone") {
|
|
289
|
+
interval["@_kaiord:powerUnit"] = "zone";
|
|
290
|
+
interval["@_kaiord:powerZone"] = step.target.value.value;
|
|
291
|
+
const percentFtp = convertPowerZoneToPercentFtp(step.target.value.value);
|
|
292
|
+
interval["@_PowerLow"] = percentFtp / 100;
|
|
293
|
+
interval["@_PowerHigh"] = percentFtp / 100;
|
|
294
|
+
} else if (step.target.value.unit === "percent_ftp") {
|
|
295
|
+
interval["@_kaiord:powerUnit"] = "percent_ftp";
|
|
296
|
+
interval["@_PowerLow"] = step.target.value.value / 100;
|
|
297
|
+
interval["@_PowerHigh"] = step.target.value.value / 100;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/adapters/krd-to-zwift/target-encoder.ts
|
|
302
|
+
var encodeSteadyStateTargets = (step, interval) => {
|
|
303
|
+
encodeSteadyStatePowerTarget(step, interval);
|
|
304
|
+
if (step.target.type === "pace" && step.target.value.unit === "mps") {
|
|
305
|
+
interval["@_pace"] = 1e3 / step.target.value.value;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
var encodeRampTargets = (step, interval, logger) => {
|
|
309
|
+
encodeRampPowerTarget(step, interval, logger);
|
|
310
|
+
if (step.target.type === "pace" && step.target.value.unit === "range") {
|
|
311
|
+
interval["@_paceLow"] = 1e3 / step.target.value.max;
|
|
312
|
+
interval["@_paceHigh"] = 1e3 / step.target.value.min;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
var encodeFreeRideTargets = (step, interval) => {
|
|
316
|
+
const stepExtensions = step.extensions?.zwift;
|
|
317
|
+
const flatRoad = stepExtensions?.flatRoad || stepExtensions?.FlatRoad;
|
|
318
|
+
if (flatRoad !== void 0) {
|
|
319
|
+
interval["@_FlatRoad"] = flatRoad;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
var encodeTargets = (step, intervalType, interval, logger) => {
|
|
323
|
+
if (intervalType === "SteadyState") {
|
|
324
|
+
encodeSteadyStateTargets(step, interval);
|
|
325
|
+
} else if (intervalType === "Warmup" || intervalType === "Ramp" || intervalType === "Cooldown") {
|
|
326
|
+
encodeRampTargets(step, interval, logger);
|
|
327
|
+
} else if (intervalType === "FreeRide") {
|
|
328
|
+
encodeFreeRideTargets(step, interval);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
var encodeCadence = (step, interval) => {
|
|
332
|
+
if (step.target.type === "cadence" && step.target.value.unit === "rpm") {
|
|
333
|
+
interval["@_Cadence"] = step.target.value.value;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// src/adapters/krd-to-zwift/step-encoder.ts
|
|
338
|
+
var convertStepToInterval = (step, intervalType, logger) => {
|
|
339
|
+
const interval = {};
|
|
340
|
+
encodeDuration(step, interval, logger);
|
|
341
|
+
encodeTargets(step, intervalType, interval, logger);
|
|
342
|
+
encodeCadence(step, interval);
|
|
343
|
+
encodeHeartRateTarget(step, interval, logger);
|
|
344
|
+
encodeMetadata(step, interval);
|
|
345
|
+
const textEvents = encodeTextEvents(step);
|
|
346
|
+
if (textEvents) {
|
|
347
|
+
interval.textevent = textEvents;
|
|
348
|
+
}
|
|
349
|
+
return interval;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// src/adapters/krd-to-zwift/intervals-encoder.ts
|
|
353
|
+
var convertStepsToZwiftIntervals = (steps, logger) => {
|
|
354
|
+
const intervals = {};
|
|
355
|
+
for (const step of steps) {
|
|
356
|
+
if ("repeatCount" in step) {
|
|
357
|
+
const repetitionBlock = step;
|
|
358
|
+
if (repetitionBlock.steps.length === 2) {
|
|
359
|
+
const intervalsT = encodeIntervalsT(repetitionBlock);
|
|
360
|
+
if (!intervals.IntervalsT) {
|
|
361
|
+
intervals.IntervalsT = [];
|
|
362
|
+
}
|
|
363
|
+
intervals.IntervalsT.push(intervalsT);
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
const workoutStep = step;
|
|
367
|
+
const intervalType = detectIntervalType(workoutStep);
|
|
368
|
+
const interval = convertStepToInterval(workoutStep, intervalType, logger);
|
|
369
|
+
if (!intervals[intervalType]) {
|
|
370
|
+
intervals[intervalType] = [];
|
|
371
|
+
}
|
|
372
|
+
intervals[intervalType].push(interval);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return intervals;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// src/adapters/krd-to-zwift/metadata-builder.ts
|
|
379
|
+
var addKrdMetadata = (workoutFile, metadata, fitExtensions) => {
|
|
380
|
+
if (metadata.created) {
|
|
381
|
+
workoutFile["@_kaiord:timeCreated"] = metadata.created;
|
|
382
|
+
}
|
|
383
|
+
if (metadata.manufacturer) {
|
|
384
|
+
workoutFile["@_kaiord:manufacturer"] = metadata.manufacturer;
|
|
385
|
+
}
|
|
386
|
+
if (metadata.product) {
|
|
387
|
+
workoutFile["@_kaiord:product"] = metadata.product;
|
|
388
|
+
}
|
|
389
|
+
if (metadata.serialNumber) {
|
|
390
|
+
workoutFile["@_kaiord:serialNumber"] = metadata.serialNumber;
|
|
391
|
+
}
|
|
392
|
+
if (fitExtensions) {
|
|
393
|
+
if (fitExtensions.type) {
|
|
394
|
+
workoutFile["@_kaiord:fitType"] = fitExtensions.type;
|
|
395
|
+
}
|
|
396
|
+
if (fitExtensions.hrm_fit_single_byte_product_id) {
|
|
397
|
+
workoutFile["@_kaiord:hrmFitProductId"] = fitExtensions.hrm_fit_single_byte_product_id;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// src/adapters/krd-to-zwift/workout-properties.ts
|
|
403
|
+
var addWorkoutProperties = (workoutFile, workoutName, zwiftExtensions) => {
|
|
404
|
+
if (zwiftExtensions.author) {
|
|
405
|
+
workoutFile.author = zwiftExtensions.author;
|
|
406
|
+
}
|
|
407
|
+
if (workoutName) {
|
|
408
|
+
workoutFile.name = workoutName;
|
|
409
|
+
}
|
|
410
|
+
if (zwiftExtensions.description) {
|
|
411
|
+
workoutFile.description = zwiftExtensions.description;
|
|
412
|
+
}
|
|
413
|
+
if (zwiftExtensions.thresholdSecPerKm !== void 0) {
|
|
414
|
+
workoutFile.thresholdSecPerKm = zwiftExtensions.thresholdSecPerKm;
|
|
415
|
+
}
|
|
416
|
+
const tags = zwiftExtensions.tags;
|
|
417
|
+
if (tags && tags.length > 0) {
|
|
418
|
+
workoutFile.tags = {
|
|
419
|
+
tag: tags.map((name) => ({ "@_name": name })).filter((t) => t["@_name"])
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
var mapSportType = (sport) => {
|
|
424
|
+
return sport === "cycling" ? "bike" : sport === "running" ? "run" : "bike";
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/adapters/krd-to-zwift/workout-file-builder.ts
|
|
428
|
+
var buildWorkoutFile = (workoutData, zwiftExtensions, metadata, fitExtensions, logger) => {
|
|
429
|
+
const workoutFile = {};
|
|
430
|
+
addWorkoutProperties(workoutFile, workoutData.name, zwiftExtensions);
|
|
431
|
+
workoutFile.sportType = mapSportType(workoutData.sport);
|
|
432
|
+
const intervals = convertStepsToZwiftIntervals(
|
|
433
|
+
workoutData.steps || [],
|
|
434
|
+
logger
|
|
435
|
+
);
|
|
436
|
+
workoutFile.workout = intervals;
|
|
437
|
+
addKrdMetadata(workoutFile, metadata, fitExtensions);
|
|
438
|
+
return workoutFile;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// src/adapters/krd-to-zwift.converter.ts
|
|
442
|
+
var extractWorkoutData = (krd) => {
|
|
443
|
+
const workout = krd.extensions?.structured_workout;
|
|
444
|
+
if (!workout || typeof workout !== "object") {
|
|
445
|
+
throw createZwiftParsingError("KRD missing workout in extensions");
|
|
446
|
+
}
|
|
447
|
+
return workout;
|
|
448
|
+
};
|
|
449
|
+
var buildXmlString = (workoutFile) => {
|
|
450
|
+
const builder = new XMLBuilder({
|
|
451
|
+
ignoreAttributes: false,
|
|
452
|
+
attributeNamePrefix: "@_",
|
|
453
|
+
format: true,
|
|
454
|
+
indentBy: " "
|
|
455
|
+
});
|
|
456
|
+
const workoutFileWithNamespace = {
|
|
457
|
+
"@_xmlns:kaiord": "http://kaiord.dev/zwift-extensions/1.0",
|
|
458
|
+
...workoutFile
|
|
459
|
+
};
|
|
460
|
+
const xmlObj = {
|
|
461
|
+
"?xml": {
|
|
462
|
+
"@_version": "1.0",
|
|
463
|
+
"@_encoding": "UTF-8"
|
|
464
|
+
},
|
|
465
|
+
workout_file: workoutFileWithNamespace
|
|
466
|
+
};
|
|
467
|
+
return builder.build(xmlObj);
|
|
468
|
+
};
|
|
469
|
+
var convertKRDToZwift = (krd, logger) => {
|
|
470
|
+
logger.debug("Building Zwift workout structure from KRD");
|
|
471
|
+
const workoutData = extractWorkoutData(krd);
|
|
472
|
+
const zwiftExtensions = krd.extensions?.zwift || {};
|
|
473
|
+
const workoutFile = buildWorkoutFile(
|
|
474
|
+
workoutData,
|
|
475
|
+
zwiftExtensions,
|
|
476
|
+
krd.metadata,
|
|
477
|
+
krd.extensions?.fit,
|
|
478
|
+
logger
|
|
479
|
+
);
|
|
480
|
+
const xmlString = buildXmlString(workoutFile);
|
|
481
|
+
logger.debug("Zwift XML structure built successfully");
|
|
482
|
+
return xmlString;
|
|
483
|
+
};
|
|
484
|
+
var validateInputZwiftXml = async (xmlString, validator, logger) => {
|
|
485
|
+
logger.debug("Validating Zwift file against XSD", {
|
|
486
|
+
xmlLength: xmlString.length
|
|
487
|
+
});
|
|
488
|
+
const validationResult = await validator(xmlString);
|
|
489
|
+
if (!validationResult.valid) {
|
|
490
|
+
logger.error("Zwift file does not conform to XSD schema", {
|
|
491
|
+
errors: validationResult.errors
|
|
492
|
+
});
|
|
493
|
+
throw createZwiftValidationError(
|
|
494
|
+
"Zwift file does not conform to XSD schema",
|
|
495
|
+
validationResult.errors
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
var validateGeneratedZwiftXml = async (xmlString, validator, logger) => {
|
|
500
|
+
logger.debug("Validating generated Zwift XML against XSD", {
|
|
501
|
+
xmlLength: xmlString.length
|
|
502
|
+
});
|
|
503
|
+
const validationResult = await validator(xmlString);
|
|
504
|
+
if (!validationResult.valid) {
|
|
505
|
+
logger.error("Generated Zwift XML does not conform to XSD schema", {
|
|
506
|
+
errors: validationResult.errors
|
|
507
|
+
});
|
|
508
|
+
throw createZwiftValidationError(
|
|
509
|
+
"Generated Zwift XML does not conform to XSD schema",
|
|
510
|
+
validationResult.errors
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
var validateZwiftStructure = (zwiftData, logger) => {
|
|
515
|
+
if (!zwiftData || typeof zwiftData !== "object" || !("workout_file" in zwiftData)) {
|
|
516
|
+
const error = createZwiftParsingError(
|
|
517
|
+
"Invalid Zwift format: missing workout_file element"
|
|
518
|
+
);
|
|
519
|
+
logger.error("Invalid Zwift structure", { error });
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// src/adapters/zwift-to-krd/intervals-extractor.ts
|
|
525
|
+
var extractIntervals = (workout) => {
|
|
526
|
+
if (!workout) return [];
|
|
527
|
+
const intervals = [];
|
|
528
|
+
const intervalTypes = [
|
|
529
|
+
"SteadyState",
|
|
530
|
+
"Warmup",
|
|
531
|
+
"Ramp",
|
|
532
|
+
"Cooldown",
|
|
533
|
+
"IntervalsT",
|
|
534
|
+
"FreeRide"
|
|
535
|
+
];
|
|
536
|
+
for (const type of intervalTypes) {
|
|
537
|
+
const data = workout[type];
|
|
538
|
+
if (data) {
|
|
539
|
+
if (Array.isArray(data)) {
|
|
540
|
+
for (const item of data) {
|
|
541
|
+
intervals.push({ type, data: item });
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
intervals.push({ type, data });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return intervals;
|
|
549
|
+
};
|
|
550
|
+
var extractTags = (tags) => {
|
|
551
|
+
if (!tags || !tags.tag) return [];
|
|
552
|
+
const tagArray = Array.isArray(tags.tag) ? tags.tag : [tags.tag];
|
|
553
|
+
return tagArray.map((t) => t["@_name"]);
|
|
554
|
+
};
|
|
555
|
+
var mapZwiftDuration = (data) => {
|
|
556
|
+
if (data["kaiord:originalDurationType"] === "distance") {
|
|
557
|
+
return {
|
|
558
|
+
type: durationTypeSchema.enum.distance,
|
|
559
|
+
meters: data["kaiord:originalDurationMeters"] || data.Duration || 0
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
if (data["kaiord:originalDurationType"] === "heart_rate_less_than") {
|
|
563
|
+
return {
|
|
564
|
+
type: durationTypeSchema.enum.heart_rate_less_than,
|
|
565
|
+
bpm: data["kaiord:originalDurationBpm"] || 0
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
if (data["kaiord:originalDurationType"] === "power_less_than") {
|
|
569
|
+
return {
|
|
570
|
+
type: durationTypeSchema.enum.power_less_than,
|
|
571
|
+
watts: data["kaiord:originalDurationWatts"] || 0
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
if (data.Duration === void 0 || data.Duration <= 0) {
|
|
575
|
+
return { type: durationTypeSchema.enum.open };
|
|
576
|
+
}
|
|
577
|
+
if (data.durationType === "distance") {
|
|
578
|
+
return {
|
|
579
|
+
type: durationTypeSchema.enum.distance,
|
|
580
|
+
meters: data.Duration
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
type: durationTypeSchema.enum.time,
|
|
585
|
+
seconds: data.Duration
|
|
586
|
+
};
|
|
587
|
+
};
|
|
588
|
+
var createOnStep = (data) => {
|
|
589
|
+
const onDurationData = {
|
|
590
|
+
Duration: data.OnDuration,
|
|
591
|
+
durationType: data.durationType
|
|
592
|
+
};
|
|
593
|
+
const onDuration = mapZwiftDuration(onDurationData);
|
|
594
|
+
let onTarget;
|
|
595
|
+
if (data.OnPower !== void 0) {
|
|
596
|
+
onTarget = convertZwiftPowerTarget(data.OnPower);
|
|
597
|
+
} else if (data.Cadence !== void 0) {
|
|
598
|
+
onTarget = convertZwiftCadenceTarget(data.Cadence);
|
|
599
|
+
} else {
|
|
600
|
+
onTarget = { type: targetTypeSchema.enum.open };
|
|
601
|
+
}
|
|
602
|
+
const textEventData = extractTextEvents(data.textevent);
|
|
603
|
+
const onStep = {
|
|
604
|
+
stepIndex: data.stepIndex,
|
|
605
|
+
durationType: onDuration.type,
|
|
606
|
+
duration: onDuration,
|
|
607
|
+
targetType: onTarget.type,
|
|
608
|
+
target: onTarget,
|
|
609
|
+
intensity: intensitySchema.enum.active,
|
|
610
|
+
...textEventData
|
|
611
|
+
};
|
|
612
|
+
if (data.OnPower !== void 0 && data.Cadence !== void 0) {
|
|
613
|
+
onStep.extensions = {
|
|
614
|
+
zwift: {
|
|
615
|
+
cadence: data.Cadence
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
return onStep;
|
|
620
|
+
};
|
|
621
|
+
var createOffStep = (data) => {
|
|
622
|
+
const offDurationData = {
|
|
623
|
+
Duration: data.OffDuration,
|
|
624
|
+
durationType: data.durationType
|
|
625
|
+
};
|
|
626
|
+
const offDuration = mapZwiftDuration(offDurationData);
|
|
627
|
+
let offTarget;
|
|
628
|
+
if (data.OffPower !== void 0) {
|
|
629
|
+
offTarget = convertZwiftPowerTarget(data.OffPower);
|
|
630
|
+
} else if (data.CadenceResting !== void 0) {
|
|
631
|
+
offTarget = convertZwiftCadenceTarget(data.CadenceResting);
|
|
632
|
+
} else {
|
|
633
|
+
offTarget = { type: targetTypeSchema.enum.open };
|
|
634
|
+
}
|
|
635
|
+
const offStep = {
|
|
636
|
+
stepIndex: data.stepIndex + 1,
|
|
637
|
+
durationType: offDuration.type,
|
|
638
|
+
duration: offDuration,
|
|
639
|
+
targetType: offTarget.type,
|
|
640
|
+
target: offTarget,
|
|
641
|
+
intensity: intensitySchema.enum.recovery
|
|
642
|
+
};
|
|
643
|
+
if (data.OffPower !== void 0 && data.CadenceResting !== void 0) {
|
|
644
|
+
offStep.extensions = {
|
|
645
|
+
zwift: {
|
|
646
|
+
cadence: data.CadenceResting
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
return offStep;
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// src/adapters/interval/intervals-t.mapper.ts
|
|
654
|
+
var mapIntervalsTToKrd = (data) => {
|
|
655
|
+
return {
|
|
656
|
+
repeatCount: data.Repeat,
|
|
657
|
+
steps: [createOnStep(data), createOffStep(data)]
|
|
658
|
+
};
|
|
659
|
+
};
|
|
660
|
+
var restoreHrRange = (data) => {
|
|
661
|
+
if (data["kaiord:hrTargetLow"] !== void 0 && data["kaiord:hrTargetHigh"] !== void 0) {
|
|
662
|
+
return {
|
|
663
|
+
type: targetTypeSchema.enum.heart_rate,
|
|
664
|
+
value: {
|
|
665
|
+
unit: "range",
|
|
666
|
+
min: data["kaiord:hrTargetLow"],
|
|
667
|
+
max: data["kaiord:hrTargetHigh"]
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
return null;
|
|
672
|
+
};
|
|
673
|
+
var restoreHrBpm = (data) => {
|
|
674
|
+
if (data["kaiord:hrTargetBpm"] !== void 0) {
|
|
675
|
+
return {
|
|
676
|
+
type: targetTypeSchema.enum.heart_rate,
|
|
677
|
+
value: {
|
|
678
|
+
unit: "bpm",
|
|
679
|
+
value: data["kaiord:hrTargetBpm"]
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
return null;
|
|
684
|
+
};
|
|
685
|
+
var restoreHrZone = (data) => {
|
|
686
|
+
if (data["kaiord:hrTargetZone"] !== void 0) {
|
|
687
|
+
return {
|
|
688
|
+
type: targetTypeSchema.enum.heart_rate,
|
|
689
|
+
value: {
|
|
690
|
+
unit: "zone",
|
|
691
|
+
value: data["kaiord:hrTargetZone"]
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
};
|
|
697
|
+
var restoreHrPercentMax = (data) => {
|
|
698
|
+
if (data["kaiord:hrTargetPercentMax"] !== void 0) {
|
|
699
|
+
return {
|
|
700
|
+
type: targetTypeSchema.enum.heart_rate,
|
|
701
|
+
value: {
|
|
702
|
+
unit: "percent_max",
|
|
703
|
+
value: data["kaiord:hrTargetPercentMax"]
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
return null;
|
|
708
|
+
};
|
|
709
|
+
var restoreHeartRateTarget = (data) => {
|
|
710
|
+
return restoreHrRange(data) || restoreHrBpm(data) || restoreHrZone(data) || restoreHrPercentMax(data) || null;
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
// src/adapters/interval/target-restoration.ts
|
|
714
|
+
var restoreWattsTarget = (data) => {
|
|
715
|
+
if (data["kaiord:powerUnit"] === "watts" && data["kaiord:originalWattsLow"] !== void 0 && data["kaiord:originalWattsHigh"] !== void 0) {
|
|
716
|
+
return {
|
|
717
|
+
type: targetTypeSchema.enum.power,
|
|
718
|
+
value: {
|
|
719
|
+
unit: "range",
|
|
720
|
+
min: data["kaiord:originalWattsLow"],
|
|
721
|
+
max: data["kaiord:originalWattsHigh"]
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
};
|
|
727
|
+
var restoreZoneTarget = (data) => {
|
|
728
|
+
if (data["kaiord:powerUnit"] === "zone" && data["kaiord:powerZone"] !== void 0) {
|
|
729
|
+
return {
|
|
730
|
+
type: targetTypeSchema.enum.power,
|
|
731
|
+
value: {
|
|
732
|
+
unit: "zone",
|
|
733
|
+
value: data["kaiord:powerZone"]
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
return null;
|
|
738
|
+
};
|
|
739
|
+
var restorePowerTarget = (data, powerLow, powerHigh, convertZwiftPowerRange2) => {
|
|
740
|
+
const wattsTarget = restoreWattsTarget(data);
|
|
741
|
+
if (wattsTarget) return wattsTarget;
|
|
742
|
+
const zoneTarget = restoreZoneTarget(data);
|
|
743
|
+
if (zoneTarget) return zoneTarget;
|
|
744
|
+
if (powerLow !== void 0 && powerHigh !== void 0 && convertZwiftPowerRange2) {
|
|
745
|
+
return convertZwiftPowerRange2(powerLow, powerHigh);
|
|
746
|
+
}
|
|
747
|
+
return null;
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// src/adapters/interval/ramp-helpers.ts
|
|
751
|
+
var buildRampDurationData = (data) => ({
|
|
752
|
+
Duration: data.Duration,
|
|
753
|
+
durationType: data.durationType,
|
|
754
|
+
"kaiord:originalDurationType": data["kaiord:originalDurationType"],
|
|
755
|
+
"kaiord:originalDurationMeters": data["kaiord:originalDurationMeters"],
|
|
756
|
+
"kaiord:originalDurationBpm": data["kaiord:originalDurationBpm"],
|
|
757
|
+
"kaiord:originalDurationWatts": data["kaiord:originalDurationWatts"]
|
|
758
|
+
});
|
|
759
|
+
var resolveRampTarget = (data) => {
|
|
760
|
+
const powerTarget = restorePowerTarget(
|
|
761
|
+
data,
|
|
762
|
+
data.PowerLow,
|
|
763
|
+
data.PowerHigh,
|
|
764
|
+
convertZwiftPowerRange
|
|
765
|
+
);
|
|
766
|
+
const hrTarget = restoreHeartRateTarget(data);
|
|
767
|
+
return powerTarget || hrTarget || { type: targetTypeSchema.enum.open };
|
|
768
|
+
};
|
|
769
|
+
var resolveIntensity = (data, defaultIntensity) => {
|
|
770
|
+
return data["kaiord:intensity"] || defaultIntensity;
|
|
771
|
+
};
|
|
772
|
+
var addRampMetadata = (step, data) => {
|
|
773
|
+
if (data["kaiord:name"]) step.name = data["kaiord:name"];
|
|
774
|
+
if (data["kaiord:equipment"]) {
|
|
775
|
+
step.equipment = data["kaiord:equipment"];
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// src/adapters/interval/ramp.mapper.ts
|
|
780
|
+
var mapWarmupToKrd = (data) => {
|
|
781
|
+
return mapRampToKrd(data, intensitySchema.enum.warmup);
|
|
782
|
+
};
|
|
783
|
+
var mapRampToKrd = (data, intensity = intensitySchema.enum.active) => {
|
|
784
|
+
const duration = mapZwiftDuration(buildRampDurationData(data));
|
|
785
|
+
const target = resolveRampTarget(data);
|
|
786
|
+
const textEventData = extractTextEvents(
|
|
787
|
+
data.textevent
|
|
788
|
+
);
|
|
789
|
+
const step = {
|
|
790
|
+
stepIndex: data.stepIndex,
|
|
791
|
+
durationType: duration.type,
|
|
792
|
+
duration,
|
|
793
|
+
targetType: target.type,
|
|
794
|
+
target,
|
|
795
|
+
intensity: resolveIntensity(data, intensity),
|
|
796
|
+
...textEventData
|
|
797
|
+
};
|
|
798
|
+
addRampMetadata(step, data);
|
|
799
|
+
return step;
|
|
800
|
+
};
|
|
801
|
+
var mapCooldownToKrd = (data) => {
|
|
802
|
+
return mapRampToKrd(data, intensitySchema.enum.cooldown);
|
|
803
|
+
};
|
|
804
|
+
var restoreSteadyStateTarget = (data) => {
|
|
805
|
+
if (data["kaiord:powerUnit"] === "watts" && data["kaiord:originalWatts"]) {
|
|
806
|
+
return {
|
|
807
|
+
type: targetTypeSchema.enum.power,
|
|
808
|
+
value: {
|
|
809
|
+
unit: "watts",
|
|
810
|
+
value: data["kaiord:originalWatts"]
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
if (data["kaiord:powerUnit"] === "zone" && data["kaiord:powerZone"]) {
|
|
815
|
+
return {
|
|
816
|
+
type: targetTypeSchema.enum.power,
|
|
817
|
+
value: {
|
|
818
|
+
unit: "zone",
|
|
819
|
+
value: data["kaiord:powerZone"]
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
if (data.Power !== void 0) {
|
|
824
|
+
return convertZwiftPowerTarget(data.Power);
|
|
825
|
+
}
|
|
826
|
+
const hrTarget = restoreHeartRateTarget(data);
|
|
827
|
+
return hrTarget || { type: targetTypeSchema.enum.open };
|
|
828
|
+
};
|
|
829
|
+
var mapSteadyStateToKrd = (data) => {
|
|
830
|
+
const durationData = {
|
|
831
|
+
Duration: data.Duration,
|
|
832
|
+
durationType: data.durationType,
|
|
833
|
+
"kaiord:originalDurationType": data["kaiord:originalDurationType"],
|
|
834
|
+
"kaiord:originalDurationMeters": data["kaiord:originalDurationMeters"],
|
|
835
|
+
"kaiord:originalDurationBpm": data["kaiord:originalDurationBpm"],
|
|
836
|
+
"kaiord:originalDurationWatts": data["kaiord:originalDurationWatts"]
|
|
837
|
+
};
|
|
838
|
+
const duration = mapZwiftDuration(durationData);
|
|
839
|
+
const target = restoreSteadyStateTarget(data);
|
|
840
|
+
const textEventData = extractTextEvents(data.textevent);
|
|
841
|
+
const step = {
|
|
842
|
+
stepIndex: data.stepIndex,
|
|
843
|
+
durationType: duration.type,
|
|
844
|
+
duration,
|
|
845
|
+
targetType: target.type,
|
|
846
|
+
target,
|
|
847
|
+
intensity: data["kaiord:intensity"] || intensitySchema.enum.active,
|
|
848
|
+
...textEventData
|
|
849
|
+
};
|
|
850
|
+
if (data["kaiord:name"]) {
|
|
851
|
+
step.name = data["kaiord:name"];
|
|
852
|
+
}
|
|
853
|
+
if (data["kaiord:equipment"]) {
|
|
854
|
+
step.equipment = data["kaiord:equipment"];
|
|
855
|
+
}
|
|
856
|
+
return step;
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
// src/adapters/interval/index.ts
|
|
860
|
+
var extractTextEvents = (textevent) => {
|
|
861
|
+
if (!textevent) {
|
|
862
|
+
return {};
|
|
863
|
+
}
|
|
864
|
+
const events = Array.isArray(textevent) ? textevent : [textevent];
|
|
865
|
+
if (events.length === 0) {
|
|
866
|
+
return {};
|
|
867
|
+
}
|
|
868
|
+
const primaryMessage = events[0].message;
|
|
869
|
+
const result = {};
|
|
870
|
+
if (primaryMessage) {
|
|
871
|
+
result.notes = primaryMessage;
|
|
872
|
+
}
|
|
873
|
+
if (events.length > 0) {
|
|
874
|
+
result.extensions = {
|
|
875
|
+
zwift: {
|
|
876
|
+
textEvents: events
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
return result;
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
// src/adapters/interval/free-ride.mapper.ts
|
|
884
|
+
var mapFreeRideToKrd = (data) => {
|
|
885
|
+
const durationData = {
|
|
886
|
+
Duration: data.Duration,
|
|
887
|
+
durationType: data.durationType
|
|
888
|
+
};
|
|
889
|
+
const duration = mapZwiftDuration(durationData);
|
|
890
|
+
const textEventData = extractTextEvents(data.textevent);
|
|
891
|
+
const step = {
|
|
892
|
+
stepIndex: data.stepIndex,
|
|
893
|
+
durationType: duration.type,
|
|
894
|
+
duration,
|
|
895
|
+
targetType: targetTypeSchema.enum.open,
|
|
896
|
+
target: { type: targetTypeSchema.enum.open },
|
|
897
|
+
intensity: intensitySchema.enum.active,
|
|
898
|
+
...textEventData
|
|
899
|
+
};
|
|
900
|
+
const flatRoad = data["@_FlatRoad"] ?? data.FlatRoad;
|
|
901
|
+
if (flatRoad !== void 0) {
|
|
902
|
+
step.extensions = {
|
|
903
|
+
...step.extensions,
|
|
904
|
+
zwift: {
|
|
905
|
+
...step.extensions?.zwift || {},
|
|
906
|
+
FlatRoad: flatRoad
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
return step;
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
// src/adapters/zwift-to-krd/intervals-processor.ts
|
|
914
|
+
var normalizeAttributeNames = (data) => {
|
|
915
|
+
const normalized = {};
|
|
916
|
+
for (const [key, value] of Object.entries(data)) {
|
|
917
|
+
const normalizedKey = key.startsWith("@_") ? key.substring(2) : key;
|
|
918
|
+
normalized[normalizedKey] = value;
|
|
919
|
+
}
|
|
920
|
+
return normalized;
|
|
921
|
+
};
|
|
922
|
+
var processSingleStep = (interval, stepIndex, durationType) => {
|
|
923
|
+
const normalizedData = normalizeAttributeNames(interval.data);
|
|
924
|
+
const data = { ...normalizedData, stepIndex, durationType };
|
|
925
|
+
if (interval.type === "SteadyState") {
|
|
926
|
+
return { step: mapSteadyStateToKrd(data), indexIncrement: 1 };
|
|
927
|
+
} else if (interval.type === "Warmup") {
|
|
928
|
+
return { step: mapWarmupToKrd(data), indexIncrement: 1 };
|
|
929
|
+
} else if (interval.type === "Ramp") {
|
|
930
|
+
return { step: mapRampToKrd(data), indexIncrement: 1 };
|
|
931
|
+
} else if (interval.type === "Cooldown") {
|
|
932
|
+
return { step: mapCooldownToKrd(data), indexIncrement: 1 };
|
|
933
|
+
} else if (interval.type === "FreeRide") {
|
|
934
|
+
return { step: mapFreeRideToKrd(data), indexIncrement: 1 };
|
|
935
|
+
}
|
|
936
|
+
throw new Error(`Unknown interval type: ${interval.type}`);
|
|
937
|
+
};
|
|
938
|
+
var processInterval = (interval, stepIndex, durationType) => {
|
|
939
|
+
if (interval.type === "IntervalsT") {
|
|
940
|
+
const normalizedData = normalizeAttributeNames(interval.data);
|
|
941
|
+
const repetitionBlock = mapIntervalsTToKrd({
|
|
942
|
+
...normalizedData,
|
|
943
|
+
stepIndex,
|
|
944
|
+
durationType
|
|
945
|
+
});
|
|
946
|
+
return {
|
|
947
|
+
step: repetitionBlock,
|
|
948
|
+
indexIncrement: repetitionBlock.steps.length
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
return processSingleStep(interval, stepIndex, durationType);
|
|
952
|
+
};
|
|
953
|
+
var processIntervals = (intervals, durationType) => {
|
|
954
|
+
const steps = [];
|
|
955
|
+
let stepIndex = 0;
|
|
956
|
+
for (const interval of intervals) {
|
|
957
|
+
const result = processInterval(interval, stepIndex, durationType);
|
|
958
|
+
steps.push(result.step);
|
|
959
|
+
stepIndex += result.indexIncrement;
|
|
960
|
+
}
|
|
961
|
+
return steps;
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// src/adapters/zwift-to-krd/metadata-extractor.ts
|
|
965
|
+
var extractMetadata = (workoutFile, sport) => ({
|
|
966
|
+
created: workoutFile["@_kaiord:timeCreated"] || (/* @__PURE__ */ new Date()).toISOString(),
|
|
967
|
+
sport,
|
|
968
|
+
manufacturer: workoutFile["@_kaiord:manufacturer"],
|
|
969
|
+
product: workoutFile["@_kaiord:product"],
|
|
970
|
+
serialNumber: workoutFile["@_kaiord:serialNumber"]
|
|
971
|
+
});
|
|
972
|
+
var extractFitExtensions = (workoutFile) => {
|
|
973
|
+
if (workoutFile["@_kaiord:fitType"] || workoutFile["@_kaiord:hrmFitProductId"]) {
|
|
974
|
+
return {
|
|
975
|
+
type: workoutFile["@_kaiord:fitType"],
|
|
976
|
+
hrm_fit_single_byte_product_id: workoutFile["@_kaiord:hrmFitProductId"]
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
return void 0;
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
// src/adapters/zwift-to-krd.converter.ts
|
|
983
|
+
var convertZwiftToKRD = (zwiftData, logger) => {
|
|
984
|
+
logger.debug("Converting Zwift to KRD");
|
|
985
|
+
const workoutFile = zwiftData.workout_file;
|
|
986
|
+
const sport = workoutFile.sportType === "bike" ? "cycling" : workoutFile.sportType === "run" ? "running" : "generic";
|
|
987
|
+
const durationType = workoutFile.durationType === "distance" ? "distance" : "time";
|
|
988
|
+
const intervals = extractIntervals(workoutFile.workout);
|
|
989
|
+
const steps = processIntervals(intervals, durationType);
|
|
990
|
+
const metadata = extractMetadata(workoutFile, sport);
|
|
991
|
+
const fitExtensions = extractFitExtensions(workoutFile);
|
|
992
|
+
const extensions = {
|
|
993
|
+
structured_workout: {
|
|
994
|
+
name: workoutFile.name,
|
|
995
|
+
sport,
|
|
996
|
+
steps
|
|
997
|
+
},
|
|
998
|
+
zwift: {
|
|
999
|
+
author: workoutFile.author,
|
|
1000
|
+
description: workoutFile.description,
|
|
1001
|
+
durationType,
|
|
1002
|
+
thresholdSecPerKm: workoutFile.thresholdSecPerKm,
|
|
1003
|
+
tags: extractTags(workoutFile.tags)
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
if (fitExtensions) {
|
|
1007
|
+
extensions.fit = fitExtensions;
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
version: "1.0",
|
|
1011
|
+
type: "structured_workout",
|
|
1012
|
+
metadata,
|
|
1013
|
+
extensions
|
|
1014
|
+
};
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
// src/adapters/fast-xml-parser.ts
|
|
1018
|
+
var parseZwiftXml = (xmlString, logger) => {
|
|
1019
|
+
logger.debug("Parsing Zwift file");
|
|
1020
|
+
try {
|
|
1021
|
+
const parser = new XMLParser({
|
|
1022
|
+
ignoreAttributes: false,
|
|
1023
|
+
attributeNamePrefix: "@_",
|
|
1024
|
+
parseAttributeValue: true
|
|
1025
|
+
});
|
|
1026
|
+
return parser.parse(xmlString);
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
logger.error("Failed to parse Zwift XML", { error });
|
|
1029
|
+
throw createZwiftParsingError("Failed to parse Zwift file", error);
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
var createFastXmlZwiftReader = (logger, validator) => async (xmlString) => {
|
|
1033
|
+
await validateInputZwiftXml(xmlString, validator, logger);
|
|
1034
|
+
const zwiftData = parseZwiftXml(xmlString, logger);
|
|
1035
|
+
validateZwiftStructure(zwiftData, logger);
|
|
1036
|
+
logger.info("Zwift file parsed successfully");
|
|
1037
|
+
return convertZwiftToKRD(zwiftData, logger);
|
|
1038
|
+
};
|
|
1039
|
+
var createFastXmlZwiftWriter = (logger, validator) => async (krd) => {
|
|
1040
|
+
logger.debug("Converting KRD to Zwift format");
|
|
1041
|
+
let xmlString;
|
|
1042
|
+
try {
|
|
1043
|
+
xmlString = convertKRDToZwift(krd, logger);
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
logger.error("Failed to convert KRD to Zwift", { error });
|
|
1046
|
+
throw createZwiftParsingError("Failed to convert KRD to Zwift", error);
|
|
1047
|
+
}
|
|
1048
|
+
await validateGeneratedZwiftXml(xmlString, validator, logger);
|
|
1049
|
+
logger.info("KRD to Zwift conversion successful");
|
|
1050
|
+
return xmlString;
|
|
1051
|
+
};
|
|
1052
|
+
var validateXmlWellFormedness = (xmlString, logger) => {
|
|
1053
|
+
const xmlValidation = XMLValidator.validate(xmlString, {
|
|
1054
|
+
allowBooleanAttributes: true
|
|
1055
|
+
});
|
|
1056
|
+
if (xmlValidation !== true) {
|
|
1057
|
+
logger.warn("Zwift XML well-formedness validation failed", {
|
|
1058
|
+
error: xmlValidation.err
|
|
1059
|
+
});
|
|
1060
|
+
return {
|
|
1061
|
+
valid: false,
|
|
1062
|
+
errors: [
|
|
1063
|
+
{
|
|
1064
|
+
field: `line ${xmlValidation.err.line}`,
|
|
1065
|
+
message: `XML validation failed: ${xmlValidation.err.msg}`
|
|
1066
|
+
}
|
|
1067
|
+
]
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return null;
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
// src/adapters/well-formedness-validator.ts
|
|
1074
|
+
var createWellFormednessValidator = (logger) => async (xmlString) => {
|
|
1075
|
+
try {
|
|
1076
|
+
logger.debug("Validating Zwift XML well-formedness (browser mode)");
|
|
1077
|
+
const wellFormednessError = validateXmlWellFormedness(xmlString, logger);
|
|
1078
|
+
if (wellFormednessError) {
|
|
1079
|
+
return wellFormednessError;
|
|
1080
|
+
}
|
|
1081
|
+
logger.info(
|
|
1082
|
+
"Zwift XML validated successfully (well-formedness only, XSD validation skipped in browser)"
|
|
1083
|
+
);
|
|
1084
|
+
return { valid: true, errors: [] };
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
logger.error("Zwift well-formedness validation failed", { error });
|
|
1087
|
+
return {
|
|
1088
|
+
valid: false,
|
|
1089
|
+
errors: [
|
|
1090
|
+
{
|
|
1091
|
+
field: "root",
|
|
1092
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
1093
|
+
}
|
|
1094
|
+
]
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
// src/adapters/node-modules-loader.ts
|
|
1100
|
+
var isNode = typeof process !== "undefined" && (process.versions?.node || typeof process.env !== "undefined");
|
|
1101
|
+
var validateXML = null;
|
|
1102
|
+
var XSD_SCHEMA_PATH = null;
|
|
1103
|
+
var loadNodeModules = async () => {
|
|
1104
|
+
if (!isNode) {
|
|
1105
|
+
throw new Error("XSD validation is only available in Node.js environments");
|
|
1106
|
+
}
|
|
1107
|
+
if (validateXML && XSD_SCHEMA_PATH) {
|
|
1108
|
+
return { validateXML, XSD_SCHEMA_PATH };
|
|
1109
|
+
}
|
|
1110
|
+
const { createRequire } = await import('module');
|
|
1111
|
+
const { dirname, join } = await import('path');
|
|
1112
|
+
const { fileURLToPath } = await import('url');
|
|
1113
|
+
const require2 = createRequire(import.meta.url);
|
|
1114
|
+
const { validateXML: validateXMLFn } = require2("xsd-schema-validator");
|
|
1115
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1116
|
+
const __dirname = dirname(__filename);
|
|
1117
|
+
const schemaPath = join(__dirname, "../schema/zwift-workout.xsd");
|
|
1118
|
+
validateXML = validateXMLFn;
|
|
1119
|
+
XSD_SCHEMA_PATH = schemaPath;
|
|
1120
|
+
return { validateXML, XSD_SCHEMA_PATH };
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
// src/adapters/xsd-schema-validator.ts
|
|
1124
|
+
var validateAgainstXsdSchema = async (xmlString, logger) => {
|
|
1125
|
+
const { validateXML: validateXMLFn, XSD_SCHEMA_PATH: schemaPath } = await loadNodeModules();
|
|
1126
|
+
const xsdValidationResult = await validateXMLFn(xmlString, schemaPath);
|
|
1127
|
+
if (!xsdValidationResult.valid) {
|
|
1128
|
+
logger.warn("Zwift XSD validation failed", {
|
|
1129
|
+
messages: xsdValidationResult.messages
|
|
1130
|
+
});
|
|
1131
|
+
return {
|
|
1132
|
+
valid: false,
|
|
1133
|
+
errors: xsdValidationResult.messages.map((msg) => ({
|
|
1134
|
+
field: "schema",
|
|
1135
|
+
message: msg
|
|
1136
|
+
}))
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
return null;
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
// src/adapters/xsd-validator.ts
|
|
1143
|
+
var isBrowser = (() => {
|
|
1144
|
+
try {
|
|
1145
|
+
return typeof globalThis.window !== "undefined";
|
|
1146
|
+
} catch {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
})();
|
|
1150
|
+
var createZwiftValidator = (logger) => {
|
|
1151
|
+
if (isBrowser) {
|
|
1152
|
+
logger.info(
|
|
1153
|
+
"Browser environment detected, using well-formedness validation for Zwift XML (XSD validation not available)"
|
|
1154
|
+
);
|
|
1155
|
+
return createWellFormednessValidator(logger);
|
|
1156
|
+
}
|
|
1157
|
+
return createXsdZwiftValidator(logger);
|
|
1158
|
+
};
|
|
1159
|
+
var createXsdZwiftValidator = (logger) => async (xmlString) => {
|
|
1160
|
+
try {
|
|
1161
|
+
logger.debug("Validating Zwift XML structure");
|
|
1162
|
+
const wellFormednessError = validateXmlWellFormedness(xmlString, logger);
|
|
1163
|
+
if (wellFormednessError) {
|
|
1164
|
+
return wellFormednessError;
|
|
1165
|
+
}
|
|
1166
|
+
logger.debug(
|
|
1167
|
+
"XML well-formedness validated, proceeding with XSD validation"
|
|
1168
|
+
);
|
|
1169
|
+
const xsdError = await validateAgainstXsdSchema(xmlString, logger);
|
|
1170
|
+
if (xsdError) {
|
|
1171
|
+
return xsdError;
|
|
1172
|
+
}
|
|
1173
|
+
logger.info("Zwift XML validated successfully against XSD schema");
|
|
1174
|
+
return { valid: true, errors: [] };
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
logger.error("Zwift validation failed", { error });
|
|
1177
|
+
return {
|
|
1178
|
+
valid: false,
|
|
1179
|
+
errors: [
|
|
1180
|
+
{
|
|
1181
|
+
field: "root",
|
|
1182
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
1183
|
+
}
|
|
1184
|
+
]
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
// src/providers.ts
|
|
1190
|
+
var createZwoProviders = (logger) => {
|
|
1191
|
+
const log = logger || createConsoleLogger();
|
|
1192
|
+
const zwiftValidator = createZwiftValidator(log);
|
|
1193
|
+
return {
|
|
1194
|
+
zwiftReader: createFastXmlZwiftReader(log, zwiftValidator),
|
|
1195
|
+
zwiftWriter: createFastXmlZwiftWriter(log, zwiftValidator),
|
|
1196
|
+
zwiftValidator
|
|
1197
|
+
};
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
export { createFastXmlZwiftReader, createFastXmlZwiftWriter, createXsdZwiftValidator, createZwiftValidator, createZwoProviders };
|
|
1201
|
+
//# sourceMappingURL=index.js.map
|
|
1202
|
+
//# sourceMappingURL=index.js.map
|