Skip to content

Commit 0f9aa8c

Browse files
committed
Fix issue with importing workouts incorrect order
1 parent 8acb7aa commit 0f9aa8c

File tree

2 files changed

+178
-144
lines changed

2 files changed

+178
-144
lines changed

src/services/__tests__/xmlService.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,5 +329,43 @@ describe('xmlService', () => {
329329
cadence: 0,
330330
});
331331
});
332+
333+
334+
it('shoudld parse elements in the correct order', async () => {
335+
const xmlContent = `
336+
<workout_file>
337+
<author></author>
338+
<name></name>
339+
<description>test from zwift 222</description>
340+
<sportType>bike</sportType>
341+
<tags/>
342+
<workout>
343+
<Warmup Duration="600" PowerLow="0.25075471" PowerHigh="0.74886793" pace="0"/>
344+
<SteadyState Duration="300" Power="0.49981132" pace="0"/>
345+
<SteadyState Duration="300" Power="0.65075469" pace="0"/>
346+
<SteadyState Duration="300" Power="0.80924529" pace="0"/>
347+
<Ramp Duration="600" PowerLow="0.25075471" PowerHigh="0.74886793" pace="0"/>
348+
<Ramp Duration="600" PowerLow="0.74886793" PowerHigh="0.25075471" pace="0"/>
349+
<SteadyState Duration="300" Power="0.94886792" pace="0"/>
350+
<SteadyState Duration="300" Power="1.0998113" pace="0"/>
351+
<SteadyState Duration="300" Power="1.2507547" pace="0"/>
352+
<Cooldown Duration="600" PowerLow="0.74886793" PowerHigh="0.25075471" pace="0"/>
353+
</workout>
354+
</workout_file>`;
355+
const file = new File([xmlContent], 'test.zwo', { type: 'text/xml' });
356+
const result = await xmlService.parseWorkoutXml(file, mockUuid);
357+
358+
expect(result.bars).toHaveLength(10);
359+
expect(result.bars[0].type).toBe('trapeze'); // Warmup
360+
expect(result.bars[1].type).toBe('bar'); // SteadyState
361+
expect(result.bars[2].type).toBe('bar'); // SteadyState
362+
expect(result.bars[3].type).toBe('bar'); // SteadyState
363+
expect(result.bars[4].type).toBe('trapeze'); // Ramp
364+
expect(result.bars[5].type).toBe('trapeze'); // Ramp
365+
expect(result.bars[6].type).toBe('bar'); // SteadyState
366+
expect(result.bars[7].type).toBe('bar'); // SteadyState
367+
expect(result.bars[8].type).toBe('bar'); // SteadyState
368+
expect(result.bars[9].type).toBe('trapeze'); // Cooldown
369+
});
332370
});
333371
});

src/services/xmlService.ts

Lines changed: 140 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,12 @@ export const xmlService = {
7676
if (index === 0) ramp = 'Warmup';
7777
if (index === bars.length - 1) ramp = 'Cooldown';
7878

79-
if (bar.startPower < bar.endPower) {
80-
segment = Builder.create(ramp)
81-
.att('Duration', durationType === 'time' ? bar.time : bar.length)
82-
.att('PowerLow', bar.startPower)
83-
.att('PowerHigh', bar.endPower)
84-
.att('pace', bar.pace);
85-
bar.cadence !== 0 && segment.att('Cadence', bar.cadence);
86-
} else {
87-
segment = Builder.create(ramp)
88-
.att('Duration', durationType === 'time' ? bar.time : bar.length)
89-
.att('PowerLow', bar.endPower)
90-
.att('PowerHigh', bar.startPower)
91-
.att('pace', bar.pace);
92-
bar.cadence !== 0 && segment.att('Cadence', bar.cadence);
93-
}
79+
segment = Builder.create(ramp)
80+
.att('Duration', durationType === 'time' ? bar.time : bar.length)
81+
.att('PowerLow', bar.startPower)
82+
.att('PowerHigh', bar.endPower)
83+
.att('pace', bar.pace);
84+
bar.cadence !== 0 && segment.att('Cadence', bar.cadence);
9485
} else if (bar.type === 'interval') {
9586
segment = Builder.create('IntervalsT')
9687
.att('Repeat', bar.repeat)
@@ -163,31 +154,37 @@ export const xmlService = {
163154
try {
164155
const content = event.target?.result as string;
165156
const result = JSON.parse(
166-
Converter.xml2json(content, { compact: true, spaces: 4 })
157+
Converter.xml2json(content, { compact: false, spaces: 4 })
167158
);
168159

169-
const workout_file = result.workout_file;
170-
171-
if (!workout_file) {
160+
const workoutFileElement = result.elements?.[0];
161+
if (!workoutFileElement || workoutFileElement.name !== 'workout_file') {
172162
throw new Error('Invalid workout file format: missing workout_file element');
173163
}
174164

175-
const name = workout_file.name?._text || '';
176-
const description = workout_file.description?._text || '';
177-
const author = workout_file.author?._text || '';
178-
const sportType =
179-
(workout_file.sportType?._text as SportType) || 'bike';
180-
const durationType =
181-
(workout_file.durationType?._text as DurationType) || 'time';
165+
// Helper to extract text from elements
166+
const getElementText = (elements: any[], elementName: string): string => {
167+
const element = elements?.find((e: any) => e.name === elementName);
168+
return element?.elements?.[0]?.text || '';
169+
};
170+
171+
// Helper to get elements by name
172+
const getElementsByName = (elements: any[], elementName: string) => {
173+
return elements?.filter((e: any) => e.name === elementName) || [];
174+
};
175+
176+
const name = getElementText(workoutFileElement.elements, 'name');
177+
const description = getElementText(workoutFileElement.elements, 'description');
178+
const author = getElementText(workoutFileElement.elements, 'author');
179+
const sportType = (getElementText(workoutFileElement.elements, 'sportType') || 'bike') as SportType;
180+
const durationType = (getElementText(workoutFileElement.elements, 'durationType') || 'time') as DurationType;
182181

183182
const tags: string[] = [];
184-
if (workout_file.tags?.tag) {
185-
const tagsArray = Array.isArray(workout_file.tags.tag)
186-
? workout_file.tags.tag
187-
: [workout_file.tags.tag];
188-
tagsArray.forEach((tag: any) => {
189-
if (tag._attributes?.name) {
190-
tags.push(tag._attributes.name);
183+
const tagsElement = workoutFileElement.elements?.find((e: any) => e.name === 'tags');
184+
if (tagsElement?.elements) {
185+
tagsElement.elements.forEach((tagElement: any) => {
186+
if (tagElement.name === 'tag' && tagElement.attributes?.name) {
187+
tags.push(tagElement.attributes.name);
191188
}
192189
});
193190
}
@@ -198,129 +195,128 @@ export const xmlService = {
198195
let totalTime = 0;
199196
let totalLength = 0;
200197

201-
const workout = workout_file.workout;
202-
const workoutElements = Object.keys(workout).filter(
203-
(key) => !key.startsWith('_')
204-
);
205-
206-
workoutElements.forEach((elementType) => {
207-
const elements = Array.isArray(workout[elementType])
208-
? workout[elementType]
209-
: [workout[elementType]];
210-
211-
elements.forEach((element: any) => {
212-
const attr = element._attributes;
213-
214-
if (!attr) return;
198+
const workoutElement = workoutFileElement.elements?.find((e: any) => e.name === 'workout');
199+
if (!workoutElement?.elements) {
200+
resolve({
201+
name,
202+
description,
203+
author,
204+
sportType,
205+
durationType,
206+
tags,
207+
bars,
208+
instructions,
209+
});
210+
return;
211+
}
215212

216-
const duration =
217-
durationType === 'time'
218-
? parseFloat(attr.Duration)
219-
: parseFloat(attr.Duration);
220-
const pace = parseFloat(attr.pace) || 0;
213+
// Process elements in the order they appear in the XML
214+
workoutElement.elements.forEach((element: any) => {
215+
const attr = element.attributes;
221216

222-
// Handle text events
223-
if (element.textevent) {
224-
const textEvents = Array.isArray(element.textevent)
225-
? element.textevent
226-
: [element.textevent];
217+
if (!attr) return;
227218

228-
textEvents.forEach((textEvent: any) => {
229-
const textAttr = textEvent._attributes;
230-
instructions.push({
231-
id: uuidv4(),
232-
text: textAttr.message || '',
233-
time:
234-
durationType === 'time'
235-
? totalTime + parseFloat(textAttr.timeoffset || 0)
236-
: 0,
237-
length:
238-
durationType === 'distance'
239-
? totalLength + parseFloat(textAttr.distoffset || 0)
240-
: 0,
241-
});
242-
});
243-
}
219+
const duration = parseFloat(attr.Duration) || 0;
220+
const pace = parseFloat(attr.pace) || 0;
244221

245-
// Create bar based on element type
246-
if (elementType === 'SteadyState') {
247-
bars.push({
248-
id: uuidv4(),
249-
type: 'bar',
250-
time: durationType === 'time' ? duration : 0,
251-
length: durationType === 'distance' ? duration : 0,
252-
power: parseFloat(attr.Power),
253-
cadence: parseFloat(attr.Cadence) || 0,
254-
pace: pace,
255-
incline: attr.Incline ? parseFloat(attr.Incline) * 100 : 0,
256-
});
257-
} else if (
258-
elementType === 'Warmup' ||
259-
elementType === 'Cooldown' ||
260-
elementType === 'Ramp'
261-
) {
262-
bars.push({
222+
// Handle text events
223+
if (element.elements) {
224+
const textEvents = element.elements.filter((e: any) => e.name === 'textevent');
225+
textEvents.forEach((textEvent: any) => {
226+
const textAttr = textEvent.attributes;
227+
instructions.push({
263228
id: uuidv4(),
264-
type: 'trapeze',
265-
time: durationType === 'time' ? duration : 0,
266-
length: durationType === 'distance' ? duration : 0,
267-
startPower: parseFloat(attr.PowerLow),
268-
endPower: parseFloat(attr.PowerHigh),
269-
cadence: parseFloat(attr.Cadence) || 0,
270-
pace: pace,
271-
});
272-
} else if (elementType === 'IntervalsT') {
273-
bars.push({
274-
id: uuidv4(),
275-
type: 'interval',
229+
text: textAttr?.message || '',
276230
time:
277231
durationType === 'time'
278-
? (parseFloat(attr.OnDuration) +
279-
parseFloat(attr.OffDuration)) *
280-
parseInt(attr.Repeat)
232+
? totalTime + parseFloat(textAttr?.timeoffset || 0)
281233
: 0,
282234
length:
283235
durationType === 'distance'
284-
? (parseFloat(attr.OnDuration) +
285-
parseFloat(attr.OffDuration)) *
286-
parseInt(attr.Repeat)
236+
? totalLength + parseFloat(textAttr?.distoffset || 0)
287237
: 0,
288-
repeat: parseInt(attr.Repeat),
289-
onDuration:
290-
durationType === 'time'
291-
? parseFloat(attr.OnDuration)
292-
: undefined,
293-
offDuration:
294-
durationType === 'time'
295-
? parseFloat(attr.OffDuration)
296-
: undefined,
297-
onLength:
298-
durationType === 'distance'
299-
? parseFloat(attr.OnDuration)
300-
: undefined,
301-
offLength:
302-
durationType === 'distance'
303-
? parseFloat(attr.OffDuration)
304-
: undefined,
305-
onPower: parseFloat(attr.OnPower),
306-
offPower: parseFloat(attr.OffPower),
307-
cadence: parseFloat(attr.Cadence) || 0,
308-
restingCadence: parseFloat(attr.CadenceResting) || 0,
309-
pace: pace,
310-
});
311-
} else if (elementType === 'FreeRide') {
312-
bars.push({
313-
id: uuidv4(),
314-
type: 'freeRide',
315-
time: durationType === 'time' ? duration : 0,
316-
length: durationType === 'distance' ? duration : 0,
317-
cadence: parseFloat(attr.Cadence) || 0,
318238
});
319-
}
320-
321-
totalTime += duration;
322-
totalLength += duration;
323-
});
239+
});
240+
}
241+
242+
// Create bar based on element type
243+
if (element.name === 'SteadyState') {
244+
bars.push({
245+
id: uuidv4(),
246+
type: 'bar',
247+
time: durationType === 'time' ? duration : 0,
248+
length: durationType === 'distance' ? duration : 0,
249+
power: parseFloat(attr.Power),
250+
cadence: parseFloat(attr.Cadence) || 0,
251+
pace: pace,
252+
incline: attr.Incline ? parseFloat(attr.Incline) * 100 : 0,
253+
});
254+
} else if (
255+
element.name === 'Warmup' ||
256+
element.name === 'Cooldown' ||
257+
element.name === 'Ramp'
258+
) {
259+
bars.push({
260+
id: uuidv4(),
261+
type: 'trapeze',
262+
time: durationType === 'time' ? duration : 0,
263+
length: durationType === 'distance' ? duration : 0,
264+
startPower: parseFloat(attr.PowerLow),
265+
endPower: parseFloat(attr.PowerHigh),
266+
cadence: parseFloat(attr.Cadence) || 0,
267+
pace: pace,
268+
});
269+
} else if (element.name === 'IntervalsT') {
270+
bars.push({
271+
id: uuidv4(),
272+
type: 'interval',
273+
time:
274+
durationType === 'time'
275+
? (parseFloat(attr.OnDuration) +
276+
parseFloat(attr.OffDuration)) *
277+
parseInt(attr.Repeat)
278+
: 0,
279+
length:
280+
durationType === 'distance'
281+
? (parseFloat(attr.OnDuration) +
282+
parseFloat(attr.OffDuration)) *
283+
parseInt(attr.Repeat)
284+
: 0,
285+
repeat: parseInt(attr.Repeat),
286+
onDuration:
287+
durationType === 'time'
288+
? parseFloat(attr.OnDuration)
289+
: undefined,
290+
offDuration:
291+
durationType === 'time'
292+
? parseFloat(attr.OffDuration)
293+
: undefined,
294+
onLength:
295+
durationType === 'distance'
296+
? parseFloat(attr.OnDuration)
297+
: undefined,
298+
offLength:
299+
durationType === 'distance'
300+
? parseFloat(attr.OffDuration)
301+
: undefined,
302+
onPower: parseFloat(attr.OnPower),
303+
offPower: parseFloat(attr.OffPower),
304+
cadence: parseFloat(attr.Cadence) || 0,
305+
restingCadence: parseFloat(attr.CadenceResting) || 0,
306+
pace: pace,
307+
});
308+
} else if (element.name === 'FreeRide') {
309+
bars.push({
310+
id: uuidv4(),
311+
type: 'freeRide',
312+
time: durationType === 'time' ? duration : 0,
313+
length: durationType === 'distance' ? duration : 0,
314+
cadence: parseFloat(attr.Cadence) || 0,
315+
});
316+
}
317+
318+
totalTime += duration;
319+
totalLength += duration;
324320
});
325321

326322
resolve({

0 commit comments

Comments
 (0)