First we bulk up the NikeRun data:
data NikeRun = NikeRun {
userId :: Int,
runId :: Int,
extendedData :: [Double],
calories :: Double,
startTime :: UTCTime
} deriving (Eq,Show)
We also define some functions to derive more data from NikeRun:
duration :: NikeRun -> Double duration nr = (10.0 * (fromIntegral (length (extendedData nr)))) / 60.0 distance :: NikeRun -> Double distance nr = last (extendedData nr) pace :: NikeRun -> Double pace nr = (duration nr) / (distance nr)
and we modify the xml data processing function parseNikeRun to extract the start time of the run too:
parseNikeRun uId rId = atTag "sportsData" >>>
proc x -> do
cs <- textAtTag "calories" -< x
exds <- textAtTag "extendedData" -< x
sts <- textAtTag "startTime" -< x
returnA -< NikeRun {
userId = uId,
runId = rId,
extendedData = readDoubles exds,
startTime = readTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S%z" sts,
calories = read cs }
We're ready now to render a NikeRun using a string template. We'll follow closely this description:
renderNikeRun :: Int -> Int -> String -> NikeRun -> IO String
renderNikeRun w h templates nr = do
dirs <- directoryGroup templates
let chart = chartNikeRun w h nr
let tpl = fromJust $ getStringTemplate "nikerun" dirs
return $ render $ setAttribute "chart" chart $
setAttribute "calories" (calories nr) $
setAttribute "duration" (renderDouble (duration nr)) $
setAttribute "distance" (renderDouble (distance nr)) $
setAttribute "pace" (renderDouble (pace nr)) $
setAttribute "startTime" (renderTime (startTime nr)) $
setAttribute "userId" (userId nr) $
setAttribute "runId" (runId nr) tpl
where
renderDouble :: Double -> String
renderDouble x = (printf "%.2f" x)::String
renderTime :: UTCTime -> String
renderTime t = formatTime defaultTimeLocale "%x, %r" t
For our convenience we want to provide a function to get the runId of the most recent run. We do it similar to the fetching and xml processing in part 3, this time with a different URL to fetch run summaries and extract runIds:
nikeRunIdsURL :: Int -> String
nikeRunIdsURL userId =
"http://nikeplus.nike.com/nikeplus/v1/" ++
"services/app/get_public_user_data.jsp?id=" ++ (show userId)
retrieveNikeRunIds :: Int -> IO String
retrieveNikeRunIds userId = do
case parseURI (nikeRunIdsURL userId) of
Nothing -> ioError . userError $ "Invalid URL"
Just uri -> get uri
parseNikeRunId = atTag "mostRecentRun" >>>
proc x -> do
runId <- getAttrValue "id" -< x
returnA -< (read runId)::Int
getMostRecentNikeRunId :: Int -> IO Int
getMostRecentNikeRunId userId = do
doc <- retrieveNikeRunIds userId
let xml = parseXML doc
nikeRunIds <- runX (xml >>> parseNikeRunId)
case nikeRunIds of
[] -> ioError . userError $
"Failed to parse most recent nike run id " ++ show userId
id:_ -> return id
That's it. To test this we write our first main module in file nikepub.hs. We will expand that later wih command line argument parsing:
module Main (main) where import Codemanic.NikeRuns main :: IO () main = do let userId = 617307368 runId <- getMostRecentNikeRunId userId nr <- getNikeRun userId runId rendered <- renderNikeRun 600 200 "/Users/uwe/moonlight/haskell/templates" nr putStrLn rendered
We create a templates directory and define the following template file nikerun.st in there:
chart = $chart$ duration = $duration$ min distance = $distance$ km pace = $pace$ min/km calories = $calories$ startTime = $startTime$ userId = $userId$ runId = $runId$
We run nikepub.hs with runhaskell because I haven't started reading the cabal user guide yet:
runhaskell nikepub.hs chart = http://chart.apis.google.com/chart?chxp=0,0,25,50,75,100|1,50|2,0,25,50,75,100|3,50&chxl=0:|6.05|5.64|5.22|4.81|4.40|1:|pace+(min%2fkm)|2:|0.0|15.2|30.3|45.5|60.7|3:|time+(min)&chxt=y,y,x,x&chs=600x200&chd=e:WdWbW-WMWuV-V-WAVUVPTtTYSOQSPeOFNeMZMJMALdLeLeLdMAM3NQNwOUO6PgQfRTRWR-R0RGQXPfPOOQN8M.MvMnMqM0NHOFO.PNQDQKQIP-P2PvN3M7L-KSIhGsF-FlErEvEbGgIPJiMPOOQvQdQRQZO7NzLzKPJpIwI5IvKcMGMVNANVOFL8KSJUHuG7GWHJI.LHOMQ-UyYZaidBedezegeHdrcnbAavZ9YJWwVPUPRwPKNpKjIAEwCUA6AAAhBnElHTJXNOQ2TZUDVZVJSdQRONNHMSLvMQNCOfPOPLP7PnOaMYK4KJJEJAJ8KNLNMHM6NgOhQbQIP0QPPeO7OGOFOCNSNLLsMINZOSQFSjVpXIYlZzavbhbZbIaJZyZhW8VlTZSVP1NQMXKVJ2JDIrKILANROQPzR4SPT4UHUPURULT.SZSERsQuQhPwPyP3QuRAQiRxSVS9S6SxTRSiSWRgSISWRVRnQWPlPGPCQQPrQKQPQ9SXSETWVEWNUlSwSeP7NuLLI8HjGXF9E8FuHLHXIbJjLOL2MhN-M1MrMPLoK5J6KAJxJzKHKzLnMFMLMALnK5JiIbILIOH.I5KWLwMfNGOJOMO4O6OSOTONODNONEOVNSNZM6MaMfLEL7LCKyKjJOKUJoKXL2NjQuRyU3XQY5bAcMdweWe3fefmhekRpKt1zq6N..&cht=lc&chg=25.0,25.0,3,2 duration = 60.67 min distance = 10.92 km pace = 5.56 min/km calories = 1078.0 startTime = 06/03/09, 11:06:55 PM userId = 617307368 runId = 672740334It worked. In the next part we'll look at xml rpc and how to do command line parsing. For now here's the modified NikeRuns module:
{-# LANGUAGE Arrows, NoMonomorphismRestriction, DeriveDataTypeable #-}
module Codemanic.NikeRuns
(
chartNikeRun,
NikeRun,
getNikeRun,
getMostRecentNikeRunId,
renderNikeRun
)
where
import Codemanic.NumericLists
import Graphics.Google.Chart
import Data.Time.Clock
import Data.Time.Format
import System.Locale
import System.Time
import System.Time.Parse
import Text.Printf
import Text.Regex
import Text.XML.HXT.Arrow
import Network.HTTP
import Network.URI
import Text.StringTemplate
import Data.Generics
import Maybe
transformAndSmoothRunData :: [Double] -> [Double]
transformAndSmoothRunData =
flipInRange .
(convolve (gaussianKernel 5)) .
(correlate (movingAverageKernel 6)) .
(map (\x -> (1.0 / (6.0 * x)))) .
(filter (/=0)) .
diff
transformAndAverageRunData :: [Double] -> [Double]
transformAndAverageRunData =
flipInRange .
(correlate (movingAverageKernel 6)) .
(map (\x -> (1.0 / (6.0 * x)))) .
(filter (/=0)) .
diff
scaler :: [Double] -> Double -> (Double -> Double)
scaler xs y = (\x -> (x - minV) * y / d)
where
minV = minInList xs
maxV = maxInList xs
d = maxV - minV
encodeRunData :: [Double] -> ChartData
encodeRunData xs = encodeDataExtended [xs']
where
sc = scaler xs (fromIntegral 4095)
xs' = map (round . sc) xs :: [Int]
sampler :: Int -> Double -> Double -> [Double]
sampler n minV maxV = [(minV + d * (fromIntegral x) / (fromIntegral n)) | x <- [0..n]]
where
d = maxV - minV
yLabels :: Int -> [Double] -> [String]
yLabels n xs = map (\x -> printf "%.2f" x) (reverse (sampler n (minInList xs) (maxInList xs)))
xLabels :: Int -> [Double] -> [String]
xLabels n xs = map (\x -> printf "%.1f" x) (sampler n 0.0 duration)
where
duration = (fromIntegral $ length xs) / 6.0
suffix :: String -> (String -> String)
suffix s = (\x -> x ++ s)
data NikeRun = NikeRun {
userId :: Int,
runId :: Int,
extendedData :: [Double],
calories :: Double,
startTime :: UTCTime
} deriving (Eq,Show)
duration :: NikeRun -> Double
duration nr = (10.0 * (fromIntegral (length (extendedData nr)))) / 60.0
distance :: NikeRun -> Double
distance nr = last (extendedData nr)
pace :: NikeRun -> Double
pace nr = (duration nr) / (distance nr)
chartNikeRun :: Int -> Int -> NikeRun -> String
chartNikeRun w h NikeRun {extendedData = xs, calories = c} =
suffix "&chg=25.0,25.0,3,2" $
chartURL $
setAxisLabelPositions [[0, 25, 50, 75, 100], [50], [0, 25, 50, 75, 100], [50]] $
setAxisLabels [(yLabels 4 ylxs), ["pace (min/km)"], (xLabels 4 xs), ["time (min)"]] $
setAxisTypes [AxisLeft, AxisLeft, AxisBottom, AxisBottom] $
setSize w h $
setData (encodeRunData txs) $
newLineChart
where
ylxs = transformAndAverageRunData xs
txs = transformAndSmoothRunData xs
nikeRunURL :: Int -> Int -> String
nikeRunURL userId runId =
"http://nikeplus.nike.com/nikeplus/v1/services/widget/get_public_run.jsp?userID=" ++ (show userId) ++
"&id=" ++ (show runId)
nikeRunIdsURL :: Int -> String
nikeRunIdsURL userId =
"http://nikeplus.nike.com/nikeplus/v1/services/app/get_public_user_data.jsp?id=" ++ (show userId)
get :: URI -> IO String
get uri = do
eresp <- simpleHTTP (Request uri GET [] "")
case eresp of
Left _ -> ioError . userError $ "Failed to get " ++ show uri
Right res -> return $ rspBody res
retrieveNikeRun :: Int -> Int -> IO String
retrieveNikeRun userId runId = do
case parseURI (nikeRunURL userId runId) of
Nothing -> ioError . userError $ "Invalid URL"
Just uri -> get uri
retrieveNikeRunIds :: Int -> IO String
retrieveNikeRunIds userId = do
case parseURI (nikeRunIdsURL userId) of
Nothing -> ioError . userError $ "Invalid URL"
Just uri -> get uri
readDoubles :: String -> [Double]
readDoubles s = map (\y -> read y::Double) (splitRegex (mkRegex ",") s)
atTag tag = deep (isElem >>> hasName tag)
text = getChildren >>> getText
textAtTag tag = atTag tag >>> text
parseXML doc = readString [(a_validate,v_0)] doc
parseNikeRun uId rId = atTag "sportsData" >>>
proc x -> do
cs <- textAtTag "calories" -< x
exds <- textAtTag "extendedData" -< x
sts <- textAtTag "startTime" -< x
returnA -< NikeRun {
userId = uId,
runId = rId,
extendedData = readDoubles exds,
startTime = readTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S%z" sts,
calories = read cs }
parseNikeRunId = atTag "mostRecentRun" >>>
proc x -> do
runId <- getAttrValue "id" -< x
returnA -< (read runId)::Int
getNikeRun :: Int -> Int -> IO NikeRun
getNikeRun userId runId = do
doc <- retrieveNikeRun userId runId
let xml = parseXML doc
nikeRuns <- runX (xml >>> (parseNikeRun userId runId))
case nikeRuns of
[] -> ioError . userError $ "Failed to parse nike run " ++ show runId
nr:_ -> return nr
getMostRecentNikeRunId :: Int -> IO Int
getMostRecentNikeRunId userId = do
doc <- retrieveNikeRunIds userId
let xml = parseXML doc
nikeRunIds <- runX (xml >>> parseNikeRunId)
case nikeRunIds of
[] -> ioError . userError $ "Failed to parse most recent nike run id " ++ show userId
id:_ -> return id
renderNikeRun :: Int -> Int -> String -> NikeRun -> IO String
renderNikeRun w h templates nr = do
dirs <- directoryGroup templates
let chart = chartNikeRun w h nr
let tpl = fromJust $ getStringTemplate "nikerun" dirs
return $ render $ setAttribute "chart" chart $
setAttribute "calories" (calories nr) $
setAttribute "duration" (renderDouble (duration nr)) $
setAttribute "distance" (renderDouble (distance nr)) $
setAttribute "pace" (renderDouble (pace nr)) $
setAttribute "startTime" (renderTime (startTime nr)) $
setAttribute "userId" (userId nr) $
setAttribute "runId" (runId nr) tpl
where
renderDouble :: Double -> String
renderDouble x = (printf "%.2f" x)::String
renderTime :: UTCTime -> String
renderTime t = formatTime defaultTimeLocale "%x, %r" t
Leave a comment