Compare commits
5 Commits
0b1aa29f41
...
6ea8a4717d
Author | SHA1 | Date | |
---|---|---|---|
6ea8a4717d | |||
8d01b16e4f | |||
d41426f4cf | |||
0cb79fb54d | |||
a21b21d240 |
81
HACKING.md
Normal file
81
HACKING.md
Normal 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 ...
|
||||
|
||||
-
|
1
Makefile
1
Makefile
@ -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))
|
||||
|
93
README.md
93
README.md
@ -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
|
||||
|
||||
|
@ -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) ]
|
||||
]
|
||||
]
|
||||
|
@ -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 =
|
||||
|
Loading…
Reference in New Issue
Block a user