Compare commits
No commits in common. "2c49318823670c9392e123d3698eeea15c2807c1" and "ab3a5aaf9f4f0dcbfb748f018809f0a8f3ddf63b" have entirely different histories.
2c49318823
...
ab3a5aaf9f
36
README.md
36
README.md
@ -104,7 +104,7 @@ _Do not look below this line_
|
|||||||
sessions are then presented to the rider who may approve them
|
sessions are then presented to the rider who may approve them
|
||||||
as-is - perhaps involving other data collection as well ("perceived
|
as-is - perhaps involving other data collection as well ("perceived
|
||||||
effort" or "which bike setup was this" or ...) - or chop them up
|
effort" or "which bike setup was this" or ...) - or chop them up
|
||||||
using information they have but that the computer doesn't
|
using information thy have but the computer doesn't
|
||||||
|
|
||||||
in theory we don't even need draft sessions and we could have the
|
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
|
rider create sessions from the calendar page. However, that's a GET
|
||||||
@ -119,10 +119,38 @@ we show time spent in HR zones ...
|
|||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
start and end marks can be drag targets but we also need to know where
|
time axis wants to show ticks which are at least (portalWidth/4)
|
||||||
they are when they're not being dragged
|
pixels apart, and choose the smallest of the following time intervals
|
||||||
|
which are greater than that width
|
||||||
|
|
||||||
selectedRange start, duration
|
1 second
|
||||||
|
5 seconds
|
||||||
|
15 seconds
|
||||||
|
30 seconds
|
||||||
|
60 seconds
|
||||||
|
5 minutes
|
||||||
|
15 minutes
|
||||||
|
1 hour
|
||||||
|
3 hours
|
||||||
|
6 hours
|
||||||
|
24 hours
|
||||||
|
|
||||||
|
if the full width is 240 seconds, show a tick every 60 second
|
||||||
|
if the full width is 20 seconds, show a tick every 5 seconds
|
||||||
|
|
||||||
|
if the width grows past 20 seconds, the distance between points shrinks
|
||||||
|
and therefore we put ticks less often
|
||||||
|
|
||||||
|
if width <= 4 * 5, 5 second tick
|
||||||
|
if width <= 4 * 15, 15 second tick
|
||||||
|
etc ...
|
||||||
|
|
||||||
|
to convert time to pixels, multiply by portalWidth / duration
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
portalWidth / duration
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ in haskellEnv.overrideAttrs(o: {
|
|||||||
elm-format
|
elm-format
|
||||||
elm-optimize-level-2
|
elm-optimize-level-2
|
||||||
elm-review
|
elm-review
|
||||||
elm-analyse
|
|
||||||
elm-test
|
elm-test
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
|
4
elm.json
4
elm.json
@ -17,7 +17,9 @@
|
|||||||
"elm/url": "1.0.0",
|
"elm/url": "1.0.0",
|
||||||
"elm-community/list-extra": "8.7.0",
|
"elm-community/list-extra": "8.7.0",
|
||||||
"elm-explorations/test": "2.2.0",
|
"elm-explorations/test": "2.2.0",
|
||||||
"mpizenberg/elm-pointer-events": "5.0.0"
|
"mpizenberg/elm-pointer-events": "5.0.0",
|
||||||
|
"rtfeldman/elm-iso8601-date-strings": "1.1.4",
|
||||||
|
"ymtszw/elm-xml-decode": "3.2.2"
|
||||||
},
|
},
|
||||||
"indirect": {
|
"indirect": {
|
||||||
"elm/bytes": "1.0.8",
|
"elm/bytes": "1.0.8",
|
||||||
|
@ -4,12 +4,19 @@
|
|||||||
module Lib exposing(looseLabels)
|
module Lib exposing(looseLabels)
|
||||||
|
|
||||||
|
|
||||||
|
maybe default val =
|
||||||
|
case val of
|
||||||
|
Just v -> v
|
||||||
|
Nothing -> default
|
||||||
|
|
||||||
-- https://github.com/cenfun/nice-ticks/blob/master/docs/Nice-Numbers-for-Graph-Labels.pdf
|
-- https://github.com/cenfun/nice-ticks/blob/master/docs/Nice-Numbers-for-Graph-Labels.pdf
|
||||||
|
|
||||||
|
log10 x = logBase 10 x
|
||||||
|
expt b x = b^(toFloat x)
|
||||||
|
|
||||||
niceNumber x round =
|
niceNumber x round =
|
||||||
let expt b ex = b^ toFloat ex
|
let exp = floor (log10 x)
|
||||||
exp = floor (logBase 10 x)
|
f = x / (expt 10.0 exp)
|
||||||
f = x / expt 10.0 exp
|
|
||||||
nfRound = if f < 1.5
|
nfRound = if f < 1.5
|
||||||
then 1
|
then 1
|
||||||
else if f < 3
|
else if f < 3
|
||||||
|
@ -2,7 +2,7 @@ module Main exposing (view)
|
|||||||
|
|
||||||
import Browser
|
import Browser
|
||||||
import Browser.Navigation as Nav
|
import Browser.Navigation as Nav
|
||||||
import Html exposing (Html, button, div, span, text, img)
|
import Html exposing (Html, button, div, span, text, img, pre)
|
||||||
import Html.Attributes as H exposing (src, style, width, height)
|
import Html.Attributes as H exposing (src, style, width, height)
|
||||||
import Html.Events exposing (onClick, on)
|
import Html.Events exposing (onClick, on)
|
||||||
import Html.Events.Extra.Pointer as Pointer
|
import Html.Events.Extra.Pointer as Pointer
|
||||||
@ -12,20 +12,24 @@ import List.Extra exposing(find)
|
|||||||
import Json.Decode as D
|
import Json.Decode as D
|
||||||
import Http
|
import Http
|
||||||
import Point exposing(Point, Pos ,decoder)
|
import Point exposing(Point, Pos ,decoder)
|
||||||
import Svg exposing (Svg, svg, rect, g, polyline, line)
|
import Svg exposing (Svg, svg, rect, circle, g, polyline, line)
|
||||||
import Svg.Attributes as S exposing
|
import Svg.Attributes as S exposing
|
||||||
( viewBox
|
( viewBox
|
||||||
, preserveAspectRatio
|
, preserveAspectRatio
|
||||||
, transform
|
, transform
|
||||||
, x, y
|
, x, y
|
||||||
, x1, y1 , x2, y2
|
, x1, y1 , x2, y2
|
||||||
|
, r, rx, ry
|
||||||
|
, cx, cy
|
||||||
, fill
|
, fill
|
||||||
|
, points
|
||||||
, stroke, strokeWidth, strokeOpacity)
|
, stroke, strokeWidth, strokeOpacity)
|
||||||
import Time
|
import Time exposing(Posix)
|
||||||
import Url.Parser exposing (Parser, (<?>), int, map, s, string)
|
import Url.Parser exposing (Parser, (</>), (<?>), int, map, oneOf, s, string)
|
||||||
import Url.Parser.Query as Query
|
import Url.Parser.Query as Query
|
||||||
import Url exposing (Url)
|
import Url exposing (Url)
|
||||||
|
|
||||||
|
|
||||||
type Route = Timeline (Maybe Int) (Maybe Int)
|
type Route = Timeline (Maybe Int) (Maybe Int)
|
||||||
|
|
||||||
routeParser : Parser (Route -> a) a
|
routeParser : Parser (Route -> a) a
|
||||||
@ -41,10 +45,12 @@ main =
|
|||||||
{ init = init
|
{ init = init
|
||||||
, update = update
|
, update = update
|
||||||
, subscriptions = subscriptions
|
, subscriptions = subscriptions
|
||||||
, onUrlRequest = \ _ -> NewUrlRequest
|
, onUrlRequest = (\ ur -> NewUrlRequest)
|
||||||
, onUrlChange = \ _ -> UrlChanged
|
, onUrlChange = (\ u -> UrlChanged)
|
||||||
, view = view }
|
, view = view }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- MATHS
|
-- MATHS
|
||||||
|
|
||||||
-- Coordinates in a Mercator projection
|
-- Coordinates in a Mercator projection
|
||||||
@ -66,6 +72,9 @@ incZoom (FineZoomLevel z) delta =
|
|||||||
|
|
||||||
type alias TileNumber = { x: Int, y: Int }
|
type alias TileNumber = { x: Int, y: Int }
|
||||||
|
|
||||||
|
type alias Lat = Float
|
||||||
|
type alias Lng = Float
|
||||||
|
|
||||||
-- project lat/long to co-ordinates based on pseudocode at
|
-- project lat/long to co-ordinates based on pseudocode at
|
||||||
-- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Zoom_levels
|
-- https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Zoom_levels
|
||||||
|
|
||||||
@ -117,7 +126,7 @@ boundingTiles centre z width height =
|
|||||||
|
|
||||||
-- MODEL
|
-- MODEL
|
||||||
|
|
||||||
type DragTarget = Map | Graph | StartMark | EndMark | NoTarget
|
type DragTarget = Map | Graph | StartMark | EndMark
|
||||||
|
|
||||||
type Drag
|
type Drag
|
||||||
= None
|
= None
|
||||||
@ -128,7 +137,7 @@ dragTo : Drag -> (Int, Int) -> Drag
|
|||||||
dragTo d dest =
|
dragTo d dest =
|
||||||
case d of
|
case d of
|
||||||
None -> None
|
None -> None
|
||||||
Dragging target from _ -> Dragging target from dest
|
Dragging target from to -> Dragging target from dest
|
||||||
|
|
||||||
dragDelta target d =
|
dragDelta target d =
|
||||||
case d of
|
case d of
|
||||||
@ -146,21 +155,20 @@ type alias Model =
|
|||||||
{ centre: Coord
|
{ centre: Coord
|
||||||
, zoom: FineZoomLevel
|
, zoom: FineZoomLevel
|
||||||
, drag: Drag
|
, drag: Drag
|
||||||
, startTime : Float
|
, startTime : Int
|
||||||
, duration : Float
|
, duration : Int
|
||||||
, markedTime : (Float, Float)
|
|
||||||
, track: TrackState }
|
, track: TrackState }
|
||||||
|
|
||||||
init : () -> Url -> Nav.Key -> (Model, Cmd Msg)
|
init : () -> Url -> Nav.Key -> (Model, Cmd Msg)
|
||||||
init _ url navKey =
|
init _ url navKey =
|
||||||
let (start, duration) =
|
let (start, duration) =
|
||||||
case Url.Parser.parse routeParser url of
|
case Url.Parser.parse routeParser url of
|
||||||
Just (Timeline (Just s) (Just d)) -> (toFloat s, toFloat d)
|
Just (Timeline (Just s) (Just d)) -> (s, d)
|
||||||
_ -> (10,10)
|
_ -> (10,10)
|
||||||
in
|
in
|
||||||
((Model
|
((Model
|
||||||
(toCoord (Pos 0 0 Nothing))
|
(toCoord (Pos 0 0 Nothing))
|
||||||
(FineZoomLevel (1*8)) None 0 0 (0,0) Loading),
|
(FineZoomLevel (1*8)) None 0 0 Empty),
|
||||||
(fetchTrack start duration))
|
(fetchTrack start duration))
|
||||||
|
|
||||||
-- SUBSCRIPTIONS
|
-- SUBSCRIPTIONS
|
||||||
@ -171,9 +179,9 @@ subscriptions model = Sub.none
|
|||||||
|
|
||||||
fetchTrack start duration = Http.get
|
fetchTrack start duration = Http.get
|
||||||
{ url = ("http://localhost:3000/points?start=" ++
|
{ url = ("http://localhost:3000/points?start=" ++
|
||||||
String.fromInt (floor start) ++
|
String.fromInt start ++
|
||||||
"&duration=" ++
|
"&duration=" ++
|
||||||
String.fromInt (ceiling duration))
|
String.fromInt duration)
|
||||||
, expect = Http.expectJson Loaded (D.list Point.decoder)
|
, expect = Http.expectJson Loaded (D.list Point.decoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,33 +227,14 @@ updateModel msg model =
|
|||||||
drag = None,
|
drag = None,
|
||||||
startTime =
|
startTime =
|
||||||
let (delta, _) = subTuple start end
|
let (delta, _) = subTuple start end
|
||||||
in model.startTime + toFloat delta * model.duration / portalWidth
|
in model.startTime + delta * model.duration // portalWidth
|
||||||
}
|
}
|
||||||
Dragging StartMark start end ->
|
|
||||||
{ model |
|
|
||||||
drag = None,
|
|
||||||
markedTime =
|
|
||||||
let delta = Tuple.first (subTuple start end)
|
|
||||||
deltat = toFloat delta * model.duration / portalWidth
|
|
||||||
(s, d) = model.markedTime
|
|
||||||
in (s - deltat, d + deltat)
|
|
||||||
}
|
|
||||||
Dragging EndMark start end ->
|
|
||||||
{ model |
|
|
||||||
drag = None,
|
|
||||||
markedTime =
|
|
||||||
let delta = Tuple.first (subTuple start end)
|
|
||||||
deltat = toFloat delta * model.duration / portalWidth
|
|
||||||
(s, d) = model.markedTime
|
|
||||||
in (s, d - deltat)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ -> model
|
_ -> model
|
||||||
TimeScale factor ->
|
TimeScale factor ->
|
||||||
let fudge = factor
|
let fudge = factor
|
||||||
len = model.duration - fudge
|
len = model.duration - floor(fudge)
|
||||||
in { model |
|
in { model |
|
||||||
startTime = model.startTime + fudge / 2
|
startTime = model.startTime + floor(fudge / 2)
|
||||||
, duration = len
|
, duration = len
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,9 +248,8 @@ updateModel msg model =
|
|||||||
| track = Present trk
|
| track = Present trk
|
||||||
, centre = toCoord (Point.centre trk)
|
, centre = toCoord (Point.centre trk)
|
||||||
, zoom = FineZoomLevel ( 13 * 8)
|
, zoom = FineZoomLevel ( 13 * 8)
|
||||||
, startTime = start
|
, startTime = floor start
|
||||||
, duration = duration
|
, duration = ceiling duration
|
||||||
, markedTime = (start + 300, duration - 900)
|
|
||||||
}
|
}
|
||||||
Err (Http.BadBody e) -> { model | track = Debug.log e (Failure "e") }
|
Err (Http.BadBody e) -> { model | track = Debug.log e (Failure "e") }
|
||||||
Err e -> { model | track = Debug.log "unknown error" (Failure "e") }
|
Err e -> { model | track = Debug.log "unknown error" (Failure "e") }
|
||||||
@ -420,41 +408,10 @@ measureView title colour fn points =
|
|||||||
, xtick 5
|
, xtick 5
|
||||||
]
|
]
|
||||||
|
|
||||||
type alias TargetedPointerEvent =
|
timeClickDecoder =
|
||||||
{ pointerEvent : Pointer.Event
|
D.map Dribble (D.at ["target", "id"] D.string)
|
||||||
, targetId : String
|
|
||||||
}
|
|
||||||
|
|
||||||
targetedEventDecoder =
|
timeAxis points =
|
||||||
D.map2 TargetedPointerEvent
|
|
||||||
Pointer.eventDecoder
|
|
||||||
(D.at ["target", "id"] D.string)
|
|
||||||
|
|
||||||
targetFor : String -> DragTarget
|
|
||||||
targetFor s =
|
|
||||||
case s of
|
|
||||||
"left-marker" -> StartMark
|
|
||||||
"right-marker" -> EndMark
|
|
||||||
_ -> NoTarget
|
|
||||||
|
|
||||||
onDownWithTarget tag =
|
|
||||||
let
|
|
||||||
decoder =
|
|
||||||
targetedEventDecoder
|
|
||||||
|> D.map tag
|
|
||||||
|> D.map options
|
|
||||||
|
|
||||||
options message =
|
|
||||||
{ message = message
|
|
||||||
, stopPropagation = True
|
|
||||||
, preventDefault = True
|
|
||||||
}
|
|
||||||
in
|
|
||||||
Html.Events.custom "pointerdown" decoder
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
timeAxis model points =
|
|
||||||
let graphHeight = 30
|
let graphHeight = 30
|
||||||
startTime = Maybe.withDefault 0 (Point.startTime points)
|
startTime = Maybe.withDefault 0 (Point.startTime points)
|
||||||
maxX = Point.duration points
|
maxX = Point.duration points
|
||||||
@ -510,23 +467,15 @@ timeAxis model points =
|
|||||||
, H.id "right-marker"
|
, H.id "right-marker"
|
||||||
, strokeWidth "3"
|
, strokeWidth "3"
|
||||||
] []
|
] []
|
||||||
markStartPix = case model.markedTime of
|
|
||||||
(s, d) ->
|
|
||||||
floor ((s - startTime) * portalWidth/maxX)
|
|
||||||
markEndPix = case model.markedTime of
|
|
||||||
(s, d) ->
|
|
||||||
ceiling ((s - startTime + d) * portalWidth/maxX)
|
|
||||||
epos e = Tuple.mapBoth floor floor e.pointer.clientPos
|
|
||||||
|
|
||||||
in
|
in
|
||||||
svg
|
svg
|
||||||
[ width portalWidth
|
[ width portalWidth
|
||||||
, height (graphHeight + 20)
|
, height (graphHeight + 20)
|
||||||
, onDownWithTarget (\e -> DragStart (targetFor e.targetId) (epos e.pointerEvent))
|
, on "pointerdown" timeClickDecoder
|
||||||
, viewBox ("0 -10 " ++ (String.fromInt portalWidth) ++
|
, viewBox ("0 -10 " ++ (String.fromInt portalWidth) ++
|
||||||
" " ++ (String.fromInt (graphHeight + 10)))
|
" " ++ (String.fromInt (graphHeight + 10)))
|
||||||
]
|
]
|
||||||
(bg::(markStart markStartPix)::(markEnd markEndPix)::xticks)
|
(bg::(markStart 22)::(markEnd 422)::xticks)
|
||||||
|
|
||||||
|
|
||||||
cadenceView : List Point -> Svg Msg
|
cadenceView : List Point -> Svg Msg
|
||||||
@ -580,9 +529,9 @@ ifTrack model content =
|
|||||||
case model.track of
|
case model.track of
|
||||||
Present t ->
|
Present t ->
|
||||||
let (dt, _) = dragDelta Graph model.drag
|
let (dt, _) = dragDelta Graph model.drag
|
||||||
dpix = toFloat dt * model.duration / portalWidth
|
dpix = dt * model.duration // portalWidth
|
||||||
start = model.startTime + dpix
|
start = toFloat (model.startTime + dpix)
|
||||||
points = Point.subseq t start model.duration |>
|
points = Point.subseq t start (toFloat model.duration) |>
|
||||||
Point.downsample 300
|
Point.downsample 300
|
||||||
in content points
|
in content points
|
||||||
Failure f -> Debug.log f (div [] [ Html.text "failure", Html.text f])
|
Failure f -> Debug.log f (div [] [ Html.text "failure", Html.text f])
|
||||||
@ -668,10 +617,10 @@ viewDiv model =
|
|||||||
[ div [] [ ifTrack model cadenceView ]
|
[ div [] [ ifTrack model cadenceView ]
|
||||||
, div [] [ ifTrack model powerView ]
|
, div [] [ ifTrack model powerView ]
|
||||||
, div [] [ ifTrack model eleView ]
|
, div [] [ ifTrack model eleView ]
|
||||||
, div [] [ ifTrack model (timeAxis model) ]
|
, div [] [ ifTrack model timeAxis ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
view : Model -> Browser.Document Msg
|
view : Model -> Browser.Document Msg
|
||||||
view model =
|
view model =
|
||||||
Browser.Document "Souplesse" [ (viewDiv model) ]
|
Browser.Document "Souplesse elm" [ (viewDiv model) ]
|
||||||
|
@ -38,9 +38,10 @@ last x xs =
|
|||||||
[] -> x
|
[] -> x
|
||||||
(x_::xs_) -> last x_ xs_
|
(x_::xs_) -> last x_ xs_
|
||||||
|
|
||||||
|
tracef x = Debug.log (String.fromFloat x) x
|
||||||
|
|
||||||
-- divide the points into n equal time buckets and return the first
|
-- divide the points into n equal time buckets and return the first
|
||||||
-- point in each
|
-- point in each
|
||||||
downsample : Int -> List Point -> List Point
|
|
||||||
downsample n points =
|
downsample n points =
|
||||||
let
|
let
|
||||||
nextpoint step prev points_ =
|
nextpoint step prev points_ =
|
||||||
@ -57,16 +58,14 @@ downsample n points =
|
|||||||
in nextpoint step first rest
|
in nextpoint step first rest
|
||||||
[] -> []
|
[] -> []
|
||||||
|
|
||||||
duration : List Point -> Float
|
|
||||||
duration points =
|
duration points =
|
||||||
case points of
|
case points of
|
||||||
(p::ps) -> (last p ps).time - p.time
|
(p::ps) -> (last p ps).time - p.time
|
||||||
_ -> 0
|
_ -> 0
|
||||||
|
|
||||||
startTime : List Point -> Maybe Float
|
|
||||||
startTime points =
|
startTime points =
|
||||||
case points of
|
case points of
|
||||||
(p::_) -> Just p.time
|
(p::ps) -> Just p.time
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
type Bound = Bound Pos Pos | NoBound
|
type Bound = Bound Pos Pos | NoBound
|
||||||
@ -82,11 +81,9 @@ extendBound pos b =
|
|||||||
NoBound ->
|
NoBound ->
|
||||||
Bound pos pos
|
Bound pos pos
|
||||||
|
|
||||||
bounds : List Point -> Bound
|
|
||||||
bounds points =
|
bounds points =
|
||||||
List.foldr extendBound NoBound (List.map .pos points)
|
List.foldr extendBound NoBound (List.map .pos points)
|
||||||
|
|
||||||
centre : List Point -> Pos
|
|
||||||
centre points =
|
centre points =
|
||||||
case bounds points of
|
case bounds points of
|
||||||
Bound min max -> Pos
|
Bound min max -> Pos
|
||||||
@ -95,7 +92,6 @@ centre points =
|
|||||||
Nothing
|
Nothing
|
||||||
NoBound -> Pos 0 0 Nothing
|
NoBound -> Pos 0 0 Nothing
|
||||||
|
|
||||||
subseq : List Point -> Float -> Float -> List Point
|
|
||||||
subseq points start dur =
|
subseq points start dur =
|
||||||
case points of
|
case points of
|
||||||
[] -> []
|
[] -> []
|
||||||
|
Loading…
Reference in New Issue
Block a user