Skip to content

Commit

Permalink
[server] Introduce WorkoutTypes selection and Strava links to Web UI
Browse files Browse the repository at this point in the history
  • Loading branch information
jylitalo committed Jun 26, 2024
1 parent 8873a4e commit 5254a32
Show file tree
Hide file tree
Showing 20 changed files with 168 additions and 72 deletions.
8 changes: 7 additions & 1 deletion api/activities.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,13 @@ type ActivitySummary struct {
}

func (as *ActivitySummary) WorkoutType() string {
options := []string{"", "Race", "Long Run", "Workout"}
options := []string{
"Default",
"Run Race", "Long Run", "Run Workout",
"Unknown (4)", "Unknown (5)", "Unknown (6)", "Unknown (7)", "Unknown (8)", "Unknown (9)",
"Default", // for Ride
"Bicycle Race", "Ride Workout",
}
if as.WorkoutTypeId < len(options) {
return options[as.WorkoutTypeId]
}
Expand Down
1 change: 1 addition & 0 deletions cmd/make.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
type Storage interface {
Query(fields []string, cond storage.Conditions, order *storage.Order) (*sql.Rows, error)
QueryTypes(cond storage.Conditions) ([]string, error)
QueryWorkoutTypes(cond storage.Conditions) ([]string, error)
QueryYears(cond storage.Conditions) ([]int, error)
Close() error
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/plot.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func plotCmd(types []string) *cobra.Command {
return err
}
defer db.Close()
err = plot.Plot(db, types, measurement, month, day, nil, output)
err = plot.Plot(db, types, nil, measurement, month, day, nil, output)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func statsCmd(types []string) *cobra.Command {
return err
}
defer db.Close()
years, results, totals, err := stats.Stats(db, measurement, period, types, month, day, nil)
years, results, totals, err := stats.Stats(db, measurement, period, types, nil, month, day, nil)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/top.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func topCmd(types []string) *cobra.Command {
return err
}
defer db.Close()
headers, results, err := stats.Top(db, measurement, period, types, limit, nil)
headers, results, err := stats.Top(db, measurement, period, types, nil, limit, nil)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/plot/plot.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ type Storage interface {
QueryYears(cond storage.Conditions) ([]int, error)
}

func Plot(db Storage, types []string, measurement string, month, day int, years []int, filename string) error {
func Plot(db Storage, types, workoutTypes []string, measurement string, month, day int, years []int, filename string) error {
tz, _ := time.LoadLocation("Europe/Helsinki")
cond := storage.Conditions{Types: types, Month: month, Day: day, Years: years}
cond := storage.Conditions{Types: types, WorkoutTypes: workoutTypes, Month: month, Day: day, Years: years}
years, err := db.QueryYears(cond)
if err != nil {
return err
Expand Down
13 changes: 7 additions & 6 deletions pkg/stats/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
func List(db Storage, types, workouts []string, years []int) ([]string, [][]string, error) {
o := []string{"year", "month", "day"}
rows, err := db.Query(
[]string{"year", "month", "day", "name", "distance", "elevation", "movingtime"},
storage.Conditions{Workouts: workouts, Types: types, Years: years},
[]string{"year", "month", "day", "name", "distance", "elevation", "movingtime", "type", "workouttype", "stravaid"},
storage.Conditions{WorkoutTypes: workouts, Types: types, Years: years},
&storage.Order{GroupBy: o, OrderBy: o},
)
if err != nil {
Expand All @@ -20,18 +20,19 @@ func List(db Storage, types, workouts []string, years []int) ([]string, [][]stri
defer rows.Close()
results := [][]string{}
for rows.Next() {
var year, month, day, movingTime int
var year, month, day, movingTime, stravaID int
var distance, elevation float64
var name string
err = rows.Scan(&year, &month, &day, &name, &distance, &elevation, &movingTime)
var name, typeName, workoutType string
err = rows.Scan(&year, &month, &day, &name, &distance, &elevation, &movingTime, &typeName, &workoutType, &stravaID)
if err != nil {
return nil, nil, err
}
results = append(results, []string{
fmt.Sprintf("%2d.%2d.%d", day, month, year), name,
fmt.Sprintf("%.0f", math.Round(distance/1000)), fmt.Sprintf("%.0f", elevation),
fmt.Sprintf("%2d:%02d:%02d", movingTime/3600, movingTime/60%60, movingTime%60),
typeName, workoutType, fmt.Sprintf("https://strava.com/activities/%d", stravaID),
})
}
return []string{"Date", "Name", "Distance (km)", "Elevation (m)", "Time"}, results, nil
return []string{"Date", "Name", "Distance (km)", "Elevation (m)", "Time", "Type", "Workout Type", "Link"}, results, nil
}
4 changes: 2 additions & 2 deletions pkg/stats/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ type Storage interface {
QueryYears(cond storage.Conditions) ([]int, error)
}

func Stats(db Storage, measurement, period string, types []string, month, day int, years []int) ([]int, [][]string, []string, error) {
func Stats(db Storage, measurement, period string, types, workoutTypes []string, month, day int, years []int) ([]int, [][]string, []string, error) {
inYear := map[string]int{
"month": 12,
"week": 53,
}
if _, ok := inYear[period]; !ok {
return nil, nil, nil, fmt.Errorf("unknown period: %s", period)
}
cond := storage.Conditions{Types: types, Month: month, Day: day, Years: years}
cond := storage.Conditions{Types: types, WorkoutTypes: workoutTypes, Month: month, Day: day, Years: years}
results := make([][]string, inYear[period])
years, err := db.QueryYears(cond)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/stats/top.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (
"github.com/jylitalo/mystats/storage"
)

func Top(db Storage, measurement, period string, types []string, limit int, years []int) ([]string, [][]string, error) {
func Top(db Storage, measurement, period string, types, workoutTypes []string, limit int, years []int) ([]string, [][]string, error) {
results := [][]string{}
rows, err := db.Query(
[]string{measurement + " as total", "year", period},
storage.Conditions{Types: types, Years: years},
storage.Conditions{Types: types, WorkoutTypes: workoutTypes, Years: years},
&storage.Order{
GroupBy: []string{"year", period},
OrderBy: []string{"total desc", "year desc", period + " desc"},
Expand Down
23 changes: 13 additions & 10 deletions server/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ import (
)

type ListFormData struct {
Name string
Workouts []string
Types map[string]bool
Years map[int]bool
Name string
Types map[string]bool
WorkoutTypes map[string]bool
Years map[int]bool
}

func newListFormData() ListFormData {
return ListFormData{
Name: "list",
Workouts: []string{},
Types: map[string]bool{},
Years: map[int]bool{},
Name: "list",
Types: map[string]bool{},
WorkoutTypes: map[string]bool{},
Years: map[int]bool{},
}
}

Expand All @@ -43,13 +43,16 @@ func listPost(page *Page, db Storage) func(c echo.Context) error {

values, errV := c.FormParams()
types, errT := typeValues(values)
workoutTypes, errW := workoutTypeValues(values)
years, errY := yearValues(values)
if err = errors.Join(errV, errT, errY); err != nil {
if err = errors.Join(errV, errT, errW, errY); err != nil {
log.Fatal(err)
}
slog.Info("POST /list", "values", values)
page.List.Form.Years = years
page.List.Data.Headers, page.List.Data.Rows, err = stats.List(db, selectedTypes(types), page.List.Form.Workouts, selectedYears(years))
page.List.Data.Headers, page.List.Data.Rows, err = stats.List(
db, selectedTypes(types), selectedWorkoutTypes(workoutTypes), selectedYears(years),
)
return errors.Join(err, c.Render(200, "list-data", page.List.Data))
}
}
38 changes: 21 additions & 17 deletions server/plot.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,23 @@ import (
)

type PlotFormData struct {
Name string
EndMonth int
EndDay int
Types map[string]bool
Years map[int]bool
Name string
EndMonth int
EndDay int
Types map[string]bool
WorkoutTypes map[string]bool
Years map[int]bool
}

func newPlotFormData() PlotFormData {
t := time.Now()
return PlotFormData{
Name: "plot",
EndMonth: int(t.Month()),
EndDay: t.Day(),
Types: map[string]bool{},
Years: map[int]bool{},
Name: "plot",
EndMonth: int(t.Month()),
EndDay: t.Day(),
Types: map[string]bool{},
WorkoutTypes: map[string]bool{},
Years: map[int]bool{},
}
}

Expand All @@ -39,8 +41,8 @@ type PlotData struct {
Stats [][]string
Totals []string
Filename string
plot func(db plot.Storage, types []string, measurement string, month, day int, years []int, filename string) error
stats func(db stats.Storage, measurement, period string, types []string, month, day int, years []int) ([]int, [][]string, []string, error)
plot func(db plot.Storage, types, workoutTypes []string, measurement string, month, day int, years []int, filename string) error
stats func(db stats.Storage, measurement, period string, types, workoutTypes []string, month, day int, years []int) ([]int, [][]string, []string, error)
}

func newPlotData() PlotData {
Expand All @@ -63,20 +65,21 @@ func newPlotPage() *PlotPage {
}
}

func (p *PlotPage) render(db Storage, types map[string]bool, month, day int, years map[int]bool) error {
func (p *PlotPage) render(db Storage, types, workoutTypes map[string]bool, month, day int, years map[int]bool) error {
p.Form.EndMonth = month
p.Form.EndDay = day
p.Form.Years = years
checkedTypes := selectedTypes(types)
checkedWorkoutTypes := selectedWorkoutTypes(workoutTypes)
checkedYears := selectedYears(years)
d := &p.Data
d.Filename = "cache/plot-" + uuid.NewString() + ".png"
err := d.plot(db, checkedTypes, "distance", month, day, checkedYears, "server/"+d.Filename)
err := d.plot(db, checkedTypes, checkedWorkoutTypes, "distance", month, day, checkedYears, "server/"+d.Filename)
if err != nil {
slog.Error("failed to plot", "err", err)
return err
}
d.Years, d.Stats, d.Totals, err = d.stats(db, d.Measurement, "month", checkedTypes, month, day, checkedYears)
d.Years, d.Stats, d.Totals, err = d.stats(db, d.Measurement, "month", checkedTypes, checkedWorkoutTypes, month, day, checkedYears)
if err != nil {
slog.Error("failed to calculate stats", "err", err)
}
Expand All @@ -89,13 +92,14 @@ func plotPost(page *Page, db Storage) func(c echo.Context) error {
day, errD := strconv.Atoi(c.FormValue("EndDay"))
values, errV := c.FormParams()
types, errT := typeValues(values)
workoutTypes, errW := workoutTypeValues(values)
years, errY := yearValues(values)
if err := errors.Join(errM, errD, errV, errT, errY); err != nil {
if err := errors.Join(errM, errD, errV, errT, errW, errY); err != nil {
log.Fatal(err)
}
slog.Info("POST /plot", "values", values)
return errors.Join(
page.Plot.render(db, types, month, day, years),
page.Plot.render(db, types, workoutTypes, month, day, years),
c.Render(200, "plot-data", page.Plot.Data),
)
}
Expand Down
41 changes: 37 additions & 4 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
type Storage interface {
Query(fields []string, cond storage.Conditions, order *storage.Order) (*sql.Rows, error)
QueryTypes(cond storage.Conditions) ([]string, error)
QueryWorkoutTypes(cond storage.Conditions) ([]string, error)
QueryYears(cond storage.Conditions) ([]int, error)
}

Expand Down Expand Up @@ -101,6 +102,16 @@ func selectedTypes(types map[string]bool) []string {
return checked
}

func selectedWorkoutTypes(types map[string]bool) []string {
checked := []string{}
for k, v := range types {
if v {
checked = append(checked, k)
}
}
return checked
}

func selectedYears(years map[int]bool) []int {
checked := []int{}
for k, v := range years {
Expand All @@ -125,6 +136,20 @@ func typeValues(values url.Values) (map[string]bool, error) {
return types, nil
}

func workoutTypeValues(values url.Values) (map[string]bool, error) {
if values == nil {
return nil, errors.New("no workoutType values given")
}
types := map[string]bool{}
for k, v := range values {
if strings.HasPrefix(k, "wt_") {
tv := strings.ReplaceAll(k[3:], "_", " ")
types[tv] = (len(tv) > 0 && v[0] == "on")
}
}
return types, nil
}

func yearValues(values url.Values) (map[int]bool, error) {
if values == nil {
return nil, errors.New("no year values given")
Expand All @@ -151,8 +176,9 @@ func Start(db Storage, selectedTypes []string, port int) error {

page := newPage()
types, errT := db.QueryTypes(storage.Conditions{})
workoutTypes, errW := db.QueryWorkoutTypes(storage.Conditions{})
years, errY := db.QueryYears(storage.Conditions{})
if err := errors.Join(errT, errY); err != nil {
if err := errors.Join(errT, errW, errY); err != nil {
return err
}
// it is faster to first mark everything false and afterwards change selected one to true,
Expand All @@ -167,6 +193,11 @@ func Start(db Storage, selectedTypes []string, port int) error {
page.Plot.Form.Types[t] = true
page.Top.Form.Types[t] = true
}
for _, t := range workoutTypes {
page.List.Form.WorkoutTypes[t] = true
page.Plot.Form.WorkoutTypes[t] = true
page.Top.Form.WorkoutTypes[t] = true
}
for _, y := range years {
page.List.Form.Years[y] = true
page.Plot.Form.Years[y] = true
Expand All @@ -186,12 +217,14 @@ func indexGet(page *Page, db Storage) func(c echo.Context) error {
return func(c echo.Context) error {
var errL, errT error
pf := &page.Plot.Form
errP := page.Plot.render(db, pf.Types, pf.WorkoutTypes, pf.EndMonth, pf.EndDay, pf.Years)
types := selectedTypes(pf.Types)
errP := page.Plot.render(db, pf.Types, pf.EndMonth, pf.EndDay, pf.Years)
page.List.Data.Headers, page.List.Data.Rows, errL = stats.List(db, types, page.List.Form.Workouts, nil)
workoutTypes := selectedWorkoutTypes(pf.WorkoutTypes)
years := selectedYears(pf.Years)
page.List.Data.Headers, page.List.Data.Rows, errL = stats.List(db, types, workoutTypes, years)
tf := &page.Top.Form
td := &page.Top.Data
td.Headers, td.Rows, errT = stats.Top(db, tf.measurement, tf.period, types, tf.limit, nil)
td.Headers, td.Rows, errT = stats.Top(db, tf.measurement, tf.period, types, workoutTypes, tf.limit, years)
return errors.Join(errP, errL, errT, c.Render(200, "index", page))
}
}
10 changes: 7 additions & 3 deletions server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,23 @@ func (t *testDB) QueryTypes(cond storage.Conditions) ([]string, error) {
return nil, nil
}

func (t *testDB) QueryWorkoutTypes(cond storage.Conditions) ([]string, error) {
return nil, nil
}

func (t *testDB) QueryYears(cond storage.Conditions) ([]int, error) {
return nil, nil
}

func TestTemplateRender(t *testing.T) {
p := newPage()
p.Plot.Data.plot = func(db plot.Storage, types []string, measurement string, month, day int, years []int, filename string) error {
p.Plot.Data.plot = func(db plot.Storage, types, workoutTypes []string, measurement string, month, day int, years []int, filename string) error {
return nil
}
p.Plot.Data.stats = func(db stats.Storage, measurement, period string, types []string, month, day int, years []int) ([]int, [][]string, []string, error) {
p.Plot.Data.stats = func(db stats.Storage, measurement, period string, types, workoutTypes []string, month, day int, years []int) ([]int, [][]string, []string, error) {
return nil, nil, nil, nil
}
err := p.Plot.render(&testDB{}, map[string]bool{"Run": true}, 6, 12, map[int]bool{2024: true})
err := p.Plot.render(&testDB{}, map[string]bool{"Run": true}, nil, 6, 12, map[int]bool{2024: true})
if err != nil {
t.Error(err)
}
Expand Down
Loading

0 comments on commit 5254a32

Please sign in to comment.