publishing nike runs, part 4: string templates

| No Comments | No TrackBacks
In this fourth part of the series of entries about publishing nike runs we will use HStringTemplate to compose the actual blog entry from the NikeRun data we built in the third part.

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 = 672740334
It 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

No TrackBacks

TrackBack URL: http://www.codemanic.com/cgi-bin/mt4/mt-tb.cgi/8

Leave a comment

Archives