Files
rideaware-api/internal/export/zwo_generator.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
}
}