170 lines
4.1 KiB
Go
170 lines
4.1 KiB
Go
package export
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
|
|
"rideaware/internal/workout"
|
|
)
|
|
|
|
// ZWO XML structures for marshalling (mirrors the parser types in workout/zwo_parser.go)
|
|
|
|
type zwoWorkoutFile struct {
|
|
XMLName xml.Name `xml:"workout_file"`
|
|
Author string `xml:"author"`
|
|
Name string `xml:"name"`
|
|
Description string `xml:"description"`
|
|
SportType string `xml:"sportType"`
|
|
Workout zwoWorkout `xml:"workout"`
|
|
}
|
|
|
|
type zwoWorkout struct {
|
|
Steps []interface{}
|
|
}
|
|
|
|
type zwoWarmup struct {
|
|
XMLName xml.Name `xml:"Warmup"`
|
|
Duration int `xml:"Duration,attr"`
|
|
PowerLow float64 `xml:"PowerLow,attr"`
|
|
PowerHigh float64 `xml:"PowerHigh,attr"`
|
|
Cadence int `xml:"Cadence,attr,omitempty"`
|
|
}
|
|
|
|
type zwoSteadyState struct {
|
|
XMLName xml.Name `xml:"SteadyState"`
|
|
Duration int `xml:"Duration,attr"`
|
|
Power float64 `xml:"Power,attr"`
|
|
Cadence int `xml:"Cadence,attr,omitempty"`
|
|
}
|
|
|
|
type zwoCooldown struct {
|
|
XMLName xml.Name `xml:"Cooldown"`
|
|
Duration int `xml:"Duration,attr"`
|
|
PowerLow float64 `xml:"PowerLow,attr"`
|
|
PowerHigh float64 `xml:"PowerHigh,attr"`
|
|
Cadence int `xml:"Cadence,attr,omitempty"`
|
|
}
|
|
|
|
type zwoInterval struct {
|
|
XMLName xml.Name `xml:"IntervalsT"`
|
|
Duration int `xml:"Duration,attr"`
|
|
PowerLow float64 `xml:"PowerLow,attr"`
|
|
PowerHigh float64 `xml:"PowerHigh,attr"`
|
|
Cadence int `xml:"Cadence,attr,omitempty"`
|
|
}
|
|
|
|
type zwoRamp struct {
|
|
XMLName xml.Name `xml:"Ramp"`
|
|
Duration int `xml:"Duration,attr"`
|
|
PowerLow float64 `xml:"PowerLow,attr"`
|
|
PowerHigh float64 `xml:"PowerHigh,attr"`
|
|
Cadence int `xml:"Cadence,attr,omitempty"`
|
|
}
|
|
|
|
type zwoFreeRide struct {
|
|
XMLName xml.Name `xml:"FreeRide"`
|
|
Duration int `xml:"Duration,attr"`
|
|
Cadence int `xml:"Cadence,attr,omitempty"`
|
|
}
|
|
|
|
// MarshalXML implements custom XML marshalling for zwoWorkout to preserve segment ordering.
|
|
func (w zwoWorkout) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
|
start.Name = xml.Name{Local: "workout"}
|
|
if err := e.EncodeToken(start); err != nil {
|
|
return err
|
|
}
|
|
for _, step := range w.Steps {
|
|
if err := e.Encode(step); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return e.EncodeToken(start.End())
|
|
}
|
|
|
|
// GenerateZWO creates a .zwo XML file from a workout's structured segments.
|
|
// Power values are stored as %FTP fractions (0.0-2.0) and pass through directly.
|
|
func GenerateZWO(w *workout.Workout) ([]byte, error) {
|
|
if len(w.WorkoutData.Segments) == 0 {
|
|
return nil, fmt.Errorf("workout has no segments")
|
|
}
|
|
|
|
name := w.WorkoutData.Name
|
|
if name == "" {
|
|
name = w.Title
|
|
}
|
|
|
|
author := w.WorkoutData.Author
|
|
if author == "" {
|
|
author = "RideAware"
|
|
}
|
|
|
|
zwo := zwoWorkoutFile{
|
|
Author: author,
|
|
Name: name,
|
|
Description: w.Description,
|
|
SportType: "bike",
|
|
}
|
|
|
|
steps := make([]interface{}, 0, len(w.WorkoutData.Segments))
|
|
for _, seg := range w.WorkoutData.Segments {
|
|
step := segmentToZWOStep(seg)
|
|
if step != nil {
|
|
steps = append(steps, step)
|
|
}
|
|
}
|
|
zwo.Workout = zwoWorkout{Steps: steps}
|
|
|
|
output, err := xml.MarshalIndent(zwo, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate ZWO XML: %w", err)
|
|
}
|
|
|
|
return append([]byte(xml.Header), output...), nil
|
|
}
|
|
|
|
func segmentToZWOStep(seg workout.WorkoutSegment) interface{} {
|
|
switch seg.Type {
|
|
case "warmup":
|
|
return zwoWarmup{
|
|
Duration: seg.Duration,
|
|
PowerLow: seg.PowerLow,
|
|
PowerHigh: seg.PowerHigh,
|
|
Cadence: seg.Cadence,
|
|
}
|
|
case "steadystate":
|
|
return zwoSteadyState{
|
|
Duration: seg.Duration,
|
|
Power: seg.Power,
|
|
Cadence: seg.Cadence,
|
|
}
|
|
case "cooldown":
|
|
return zwoCooldown{
|
|
Duration: seg.Duration,
|
|
PowerLow: seg.PowerLow,
|
|
PowerHigh: seg.PowerHigh,
|
|
Cadence: seg.Cadence,
|
|
}
|
|
case "interval":
|
|
return zwoInterval{
|
|
Duration: seg.Duration,
|
|
PowerLow: seg.PowerLow,
|
|
PowerHigh: seg.PowerHigh,
|
|
Cadence: seg.Cadence,
|
|
}
|
|
case "ramp":
|
|
return zwoRamp{
|
|
Duration: seg.Duration,
|
|
PowerLow: seg.PowerLow,
|
|
PowerHigh: seg.PowerHigh,
|
|
Cadence: seg.Cadence,
|
|
}
|
|
case "freeride":
|
|
return zwoFreeRide{
|
|
Duration: seg.Duration,
|
|
Cadence: seg.Cadence,
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|