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
|
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 build
|
||||||
|
cabal test --test-show-details=always
|
||||||
|
|
||||||
FRONTEND=Main.elm Lib.elm Point.elm Pos.elm TileMap.elm Model.elm
|
FRONTEND=Main.elm Lib.elm Point.elm Pos.elm TileMap.elm Model.elm
|
||||||
frontend/frontend.js: $(patsubst %,frontend/src/%,$(FRONTEND))
|
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 `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
|
* `Procfile` describes _all_ the things I like to run in different
|
||||||
ask for details it won't tell you about incomplete pattern matches
|
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
|
## Sample data
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import Json.Decode as D
|
|||||||
import Http
|
import Http
|
||||||
import Point exposing (Point, decoder)
|
import Point exposing (Point, decoder)
|
||||||
import Pos exposing (Pos)
|
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
|
import Svg.Attributes as S exposing
|
||||||
( viewBox
|
( viewBox
|
||||||
, preserveAspectRatio
|
, preserveAspectRatio
|
||||||
@ -226,9 +226,25 @@ selectionRect selection =
|
|||||||
, S.height "100%"
|
, 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 : String -> Colour -> (Point -> Maybe Float) -> Float -> Maybe (Float, Float) -> List Point -> Svg Msg
|
||||||
measureView title colour fn selection points =
|
measureView title colour fn threshold selection points =
|
||||||
let graphHeight = 180
|
let graphHeight = 180
|
||||||
startTime = Maybe.withDefault 0 (Point.startTime points)
|
startTime = Maybe.withDefault 0 (Point.startTime points)
|
||||||
coords p = case (fn p) of
|
coords p = case (fn p) of
|
||||||
@ -308,6 +324,16 @@ measureView title colour fn selection points =
|
|||||||
, ybar 1
|
, ybar 1
|
||||||
, ybar 2
|
, ybar 2
|
||||||
, ybar 3
|
, 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
|
, polyline
|
||||||
[ fill "none"
|
[ fill "none"
|
||||||
, style "vector-effect" "non-scaling-stroke"
|
, 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 : Int -> Int -> ZoomLevel -> Maybe (Float, Float) -> List Point -> Svg Msg
|
||||||
trackView leftedge topedge zoom selection points =
|
trackView leftedge topedge zoom selection points =
|
||||||
let plot p =
|
let plot p =
|
||||||
@ -575,9 +593,14 @@ viewDiv model =
|
|||||||
, style "flex-direction" "column"
|
, style "flex-direction" "column"
|
||||||
, style "row-gap" "10px"
|
, style "row-gap" "10px"
|
||||||
]
|
]
|
||||||
[ div [] [ ifTrack model cadenceView ]
|
[ div []
|
||||||
, div [] [ ifTrack model powerView ]
|
(List.map (\g ->
|
||||||
, div [] [ ifTrack model eleView ]
|
div []
|
||||||
|
[ ifTrack model
|
||||||
|
<| measureView g.id g.colour g.extractor g.threshold
|
||||||
|
]
|
||||||
|
)
|
||||||
|
model.graphs)
|
||||||
, div [] [ ifTrack model (timeAxis model) ]
|
, div [] [ ifTrack model (timeAxis model) ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -17,6 +17,18 @@ type DragState =
|
|||||||
| DragLeftMark (Int, Int) Float
|
| DragLeftMark (Int, Int) Float
|
||||||
| DragRightMark (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 TrackState = Empty | Loading | Failure String | Present (List Point)
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
@ -27,6 +39,7 @@ type alias Model =
|
|||||||
, duration : Float
|
, duration : Float
|
||||||
, leftMark : Float
|
, leftMark : Float
|
||||||
, rightMark : Float
|
, rightMark : Float
|
||||||
|
, graphs : List TimeGraph
|
||||||
, track: TrackState }
|
, track: TrackState }
|
||||||
|
|
||||||
empty = Model
|
empty = Model
|
||||||
@ -35,6 +48,14 @@ empty = Model
|
|||||||
Nothing
|
Nothing
|
||||||
0 0
|
0 0
|
||||||
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
|
Loading
|
||||||
|
|
||||||
isMarkActive model =
|
isMarkActive model =
|
||||||
|
Loading…
Reference in New Issue
Block a user