diff --git a/api/activities.go b/api/activities.go index 5253427..592c932 100644 --- a/api/activities.go +++ b/api/activities.go @@ -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] } diff --git a/cmd/make.go b/cmd/make.go index e27c28e..c8d6ad1 100644 --- a/cmd/make.go +++ b/cmd/make.go @@ -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 } diff --git a/cmd/plot.go b/cmd/plot.go index fe6d84f..2451a33 100644 --- a/cmd/plot.go +++ b/cmd/plot.go @@ -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 } diff --git a/cmd/stats.go b/cmd/stats.go index 1a06a81..28a4463 100644 --- a/cmd/stats.go +++ b/cmd/stats.go @@ -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 } diff --git a/cmd/top.go b/cmd/top.go index 0dedcf1..6ef5fac 100644 --- a/cmd/top.go +++ b/cmd/top.go @@ -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 } diff --git a/pkg/plot/plot.go b/pkg/plot/plot.go index 694e6a3..74dcc43 100644 --- a/pkg/plot/plot.go +++ b/pkg/plot/plot.go @@ -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 diff --git a/pkg/stats/list.go b/pkg/stats/list.go index 11eac04..2bb74e1 100644 --- a/pkg/stats/list.go +++ b/pkg/stats/list.go @@ -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 { @@ -20,10 +20,10 @@ 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 } @@ -31,7 +31,8 @@ func List(db Storage, types, workouts []string, years []int) ([]string, [][]stri 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 } diff --git a/pkg/stats/stats.go b/pkg/stats/stats.go index 8c60a9a..81a1844 100644 --- a/pkg/stats/stats.go +++ b/pkg/stats/stats.go @@ -13,7 +13,7 @@ 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, @@ -21,7 +21,7 @@ func Stats(db Storage, measurement, period string, types []string, month, day in 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 { diff --git a/pkg/stats/top.go b/pkg/stats/top.go index f3bd677..7529d37 100644 --- a/pkg/stats/top.go +++ b/pkg/stats/top.go @@ -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"}, diff --git a/server/list.go b/server/list.go index e5c2a29..82e8544 100644 --- a/server/list.go +++ b/server/list.go @@ -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{}, } } @@ -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)) } } diff --git a/server/plot.go b/server/plot.go index 5e07388..f0fd046 100644 --- a/server/plot.go +++ b/server/plot.go @@ -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{}, } } @@ -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 { @@ -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) } @@ -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), ) } diff --git a/server/server.go b/server/server.go index 7d14c88..ea4c939 100644 --- a/server/server.go +++ b/server/server.go @@ -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) } @@ -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 { @@ -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") @@ -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, @@ -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 @@ -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)) } } diff --git a/server/server_test.go b/server/server_test.go index 13efc8a..83a3d7d 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -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) } diff --git a/server/top.go b/server/top.go index e69a7d4..fa891a9 100644 --- a/server/top.go +++ b/server/top.go @@ -10,22 +10,24 @@ import ( ) type TopFormData struct { - Name string - Types map[string]bool - Years map[int]bool - measurement string - period string - limit int + Name string + Types map[string]bool + WorkoutTypes map[string]bool + Years map[int]bool + measurement string + period string + limit int } func newTopFormData() TopFormData { return TopFormData{ - Name: "top", - Types: map[string]bool{}, - Years: map[int]bool{}, - measurement: "sum(distance)", - period: "week", - limit: 100, + Name: "top", + Types: map[string]bool{}, + WorkoutTypes: map[string]bool{}, + Years: map[int]bool{}, + measurement: "sum(distance)", + period: "week", + limit: 100, } } @@ -47,15 +49,19 @@ func topPost(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 /top", "values", values) tf := &page.Top.Form tf.Years = years td := &page.Top.Data - td.Headers, td.Rows, err = stats.Top(db, tf.measurement, tf.period, selectedTypes(types), tf.limit, selectedYears(years)) + td.Headers, td.Rows, err = stats.Top( + db, tf.measurement, tf.period, selectedTypes(types), selectedWorkoutTypes(workoutTypes), + tf.limit, selectedYears(years), + ) return errors.Join(err, c.Render(200, "top-data", td)) } } diff --git a/server/views/index.html b/server/views/index.html index cb89c9b..6245122 100644 --- a/server/views/index.html +++ b/server/views/index.html @@ -61,6 +61,16 @@ {{ end }} +{{ block "workouttypes" . }} +{{ $name := .Name }} +