feat: extend equipment and workout models with service tracking
This commit is contained in:
169
internal/export/zwo_generator.go
Normal file
169
internal/export/zwo_generator.go
Normal file
@@ -0,0 +1,169 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user