Compare commits

...

5 Commits

Author SHA1 Message Date
6ea8a4717d note some changes needed for multiuser support 2024-12-02 00:10:31 +00:00
8d01b16e4f render thresholds for each graph
also add the visible graphs to Model type instead of hardcoding them
2024-12-02 00:09:33 +00:00
d41426f4cf separate the grisly internals from the user-facing README 2024-11-29 18:17:12 +00:00
0cb79fb54d make make run tests 2024-11-29 18:14:35 +00:00
a21b21d240 describe Procfile in README 2024-11-29 18:14:19 +00:00
5 changed files with 147 additions and 100 deletions

81
HACKING.md Normal file
View File

@ -0,0 +1,81 @@
# Development and Design notes
## WIP, Puzzles and TODO
* rename Track to Gpx, it deals only with parsing.
* can we lose this "if isJust lat && isJust lon && isJust ts" wart?
* probably we should store points in a more efficient form than
a singly-linked list
* boring stuff like auth[zn]
* need a web server in haskell that
* [done] accepts file upload and parses the gpx file
* [done] serves the data points in some format elm can digest easily
* [done] need a database of some kind so the data can be saved
* [done] frontend can get data from backend
* [done] for DX, backend can serve the js files needed by frontend
* [ad hoc] we only have yesod-core, may need other parts as well
* [done] detect and refuse uploads which overlap an existing time frame (http 409) so that we can script upload-all-the-tracks.
* could we converge the Point and Trkpt to make sql better?
* [done] move Store into Point
* on timeline, show power, cadence, speed, height, ascent (checkboxes)
* done some
* need speed and ascent
* need checkboxes
* zoom gesture on graphs causes map to adjust
* zooming map causes graphs to adjust
* threshold display: adjust vertical slider to show time spent at
or above a particular intensity. Indicate somehow the length of
each continuous stretch at that intensity
## Multiuser
I am minded to avoid writing any of that user registration/authn stuff
and just use a third-party SSO thing like Authelia to cover password
resets etc. Installation instructions will include "set up a reverse
proxy for this app which sends Remote-User and other Remote-* info as HTTP
headers".
However, we do still need some changes
1) trackpoints are no longer unique by time
2) also sessions
3) we want a `users` table with id and name/fullname/email to tie them back to
4) if there is no userid for the current Remote-User header, make one
## Sessions
The calendar (TBD) displays sessions. a session is a sequence of
measurements describing a ride or a race or a trip. we can extract
potential sessions from the data by looking for series of points not
more than x (10?) minutes apart, but the rider may override
that. Consider: I ride solo to the start point of a group ride, join a
tandem partner to do the group ride, then ride solo home. There is not
necessarily ten minutes between them.
After a new track is uploaded, we look at all the points covered by
draft sessions, and rearrange them to cover the new points. Draft
sessions are then presented to the rider who may approve them as-is -
perhaps involving other data collection as well ("perceived effort" or
"which bike setup was this" or ...) - or chop them up using
information they have but that the computer doesn't
Aside: in theory we don't even need draft sessions and we could have
the rider create sessions from the calendar page. However, that's a
GET and might be slow if it has to figure out what all the sessions
would be every time someone looks at it. So the draft session is just
to precompute that and make the view easier
The summary of a session is for display on the calendar and might
change depending on the nature of the training effort. e.g. for a
long slow ride we show total distance, for interval training we show
time spent in HR zones ...
-

View File

@ -2,6 +2,7 @@ default: frontend/frontend.js dist-newstyle/build/x86_64-linux/ghc-9.6.5/souples
dist-newstyle/build/x86_64-linux/ghc-9.6.5/souplesse-0.1.0.0/x/souplesse/build/souplesse/souplesse: app/*.hs lib/*.hs
cabal build
cabal test --test-show-details=always
FRONTEND=Main.elm Lib.elm Point.elm Pos.elm TileMap.elm Model.elm
frontend/frontend.js: $(patsubst %,frontend/src/%,$(FRONTEND))

View File

@ -49,94 +49,15 @@ ratio, or ... some other weirdness)
Use `nix-shell`. Inside the shell
* use `make` to build frontend (Elm) and backend (Haskell/Yesod)
* use `make` to build frontend (Elm) and backend (Haskell/Yesod) and
run tests for both. Refer to the Makefile (which is gratuitously
x86-64-specific, sorry) for details
* run tests with `cabal test --test-show-details=always`: if you don't
ask for details it won't tell you about incomplete pattern matches
* `Procfile` describes _all_ the things I like to run in different
tabs, including a Docker container for Postgres. Best used with
Overmind: run `overmind start -D` and then `overmind connect` to
fire up a tmux session
* run the app with `cabal run`
----
_Do not look below this line_
## WIP, Puzzles and TODO
* rename Track to Gpx, it deals only with parsing.
* can we lose this "if isJust lat && isJust lon && isJust ts" wart?
* probably we should store points in a more efficient form than
a singly-linked list
* boring stuff like auth[zn]
* need a web server in haskell that
* [done] accepts file upload and parses the gpx file
* [done] serves the data points in some format elm can digest easily
* [done] need a database of some kind so the data can be saved
* [done] frontend can get data from backend
* [done] for DX, backend can serve the js files needed by frontend
* [ad hoc] we only have yesod-core, may need other parts as well
* [done] detect and refuse uploads which overlap an existing time frame (http 409) so that we can script upload-all-the-tracks.
* could we converge the Point and Trkpt to make sql better?
* [done] move Store into Point
* on timeline, show power, cadence, speed, height, ascent (checkboxes)
* done some
* need speed and ascent
* need checkboxes
* zoom gesture on graphs causes map to adjust
* zooming map causes graphs to adjust
* threshold display: adjust vertical slider to show time spent at
or above a particular intensity. Indicate somehow the length of
each continuous stretch at that intensity
* calendar displays sessions. a session is a sequence of measurements
describing a ride or a race or a trip. we can extract potential
sessions from the data by looking for series of points not more than
x (10?) minutes apart, but the rider may override that. Consider: I
ride solo to the start point of a group ride, join a tandem partner
to do the group ride, then ride solo home. There is not necessarily
ten minutes between them.
after a new track is uploaded, we look at all the points covered by
draft sessions, and rearrange them to cover the new points. Draft
sessions are then presented to the rider who may approve them
as-is - perhaps involving other data collection as well ("perceived
effort" or "which bike setup was this" or ...) - or chop them up
using information they have but that the computer doesn't
in theory we don't even need draft sessions and we could have the
rider create sessions from the calendar page. However, that's a GET
and might be slow if it has to figure out what all the sessions would
be every time someone looks at it. So the draft session is just to
precompute that and make the view easier
the summary of a session is for display on the calendar and might
change depending on the nature of the training effort. e.g.
for a long slow ride we show total distance, for interval training
we show time spent in HR zones ...
----
start and end marks can be drag targets but we also need to know where
they are when they're not being dragged
selectedRange start, duration
## Postgres
I run the postgresql devel server using Docker instead of changing my
global NixOS configuration, so that it's self-contained and I can
start and stop it when I want to
```
docker run -p 5432:5432 --name souplesse-postgres -e POSTGRES_USER=souplesse -e POSTGRES_PASSWORD=secret -d postgres
nix-shell -p postgresql --run "psql -h localhost -U souplesse -p 5432"
```
## Sample data

View File

@ -14,7 +14,7 @@ import Json.Decode as D
import Http
import Point exposing (Point, decoder)
import Pos exposing (Pos)
import Svg exposing (Svg, svg, rect, g, polyline, line)
import Svg exposing (Svg, svg, rect, g, polyline, line, mask)
import Svg.Attributes as S exposing
( viewBox
, preserveAspectRatio
@ -226,9 +226,25 @@ selectionRect selection =
, S.height "100%"
] []
thresholdMask id maxX minY threshold maxY =
let r y0 y1 colour =
rect
[
x "0"
, y (String.fromFloat y0)
, width (ceiling maxX)
, height (floor (y1 - y0))
, fill colour
] []
in
mask
[ S.id id ]
[ r minY threshold "black"
, r threshold maxY "white"
]
measureView : String -> Colour -> (Point -> Maybe Float) -> Maybe (Float, Float) -> List Point -> Svg Msg
measureView title colour fn selection points =
measureView : String -> Colour -> (Point -> Maybe Float) -> Float -> Maybe (Float, Float) -> List Point -> Svg Msg
measureView title colour fn threshold selection points =
let graphHeight = 180
startTime = Maybe.withDefault 0 (Point.startTime points)
coords p = case (fn p) of
@ -308,6 +324,16 @@ measureView title colour fn selection points =
, ybar 1
, ybar 2
, ybar 3
, polyline
[ fill colour
, style "opacity" "30%"
, S.mask ("url(#" ++ title ++ "-mask)")
, stroke "none"
, style "vector-effect" "non-scaling-stroke"
, S.points (string ++
(String.fromFloat maxX) ++ ",0, 0,0")
] []
, thresholdMask (title ++ "-mask") maxX minY threshold maxY
, polyline
[ fill "none"
, style "vector-effect" "non-scaling-stroke"
@ -444,14 +470,6 @@ timeAxis model selection points =
]
cadenceView : Maybe (Float, Float) -> List Point -> Svg Msg
cadenceView =
measureView "cadence" "#44ee44" (.cadence >> Maybe.map toFloat)
powerView = measureView "power" "#994444" (.power >> Maybe.map toFloat)
eleView = measureView "elevation" "#4444ee" (.pos >> .ele)
trackView : Int -> Int -> ZoomLevel -> Maybe (Float, Float) -> List Point -> Svg Msg
trackView leftedge topedge zoom selection points =
let plot p =
@ -575,9 +593,14 @@ viewDiv model =
, style "flex-direction" "column"
, style "row-gap" "10px"
]
[ div [] [ ifTrack model cadenceView ]
, div [] [ ifTrack model powerView ]
, div [] [ ifTrack model eleView ]
[ div []
(List.map (\g ->
div []
[ ifTrack model
<| measureView g.id g.colour g.extractor g.threshold
]
)
model.graphs)
, div [] [ ifTrack model (timeAxis model) ]
]
]

View File

@ -17,6 +17,18 @@ type DragState =
| DragLeftMark (Int, Int) Float
| DragRightMark (Int, Int) Float
type GraphToggle = Hidden | Overlay | Solo
type alias TimeGraph =
{ title: String
, id: String
, colour: String
, extractor: Point -> Maybe Float
, toggle : GraphToggle
, threshold : Float
}
type TrackState = Empty | Loading | Failure String | Present (List Point)
type alias Model =
@ -27,6 +39,7 @@ type alias Model =
, duration : Float
, leftMark : Float
, rightMark : Float
, graphs : List TimeGraph
, track: TrackState }
empty = Model
@ -35,6 +48,14 @@ empty = Model
Nothing
0 0
0 0
[
TimeGraph "Cadence" "cadence" "#44ee44" (.cadence >> Maybe.map toFloat)
Solo 90
, TimeGraph "Power" "power" "#994444" (.power >> Maybe.map toFloat)
Solo 100
, TimeGraph "Elevation" "elevation" "#4444ee" (.pos >> .ele)
Solo 0
]
Loading
isMarkActive model =