packages feed

threadscope-0.2.14.1: GUI/Timeline/Ticks.hs

{-# LANGUAGE CPP #-}
module GUI.Timeline.Ticks (
    renderVRulers,
    XScaleMode(..),
    renderXScaleArea,
    renderXScale,
    renderHRulers,
    renderYScale,
    mu,
    deZero,
  ) where

import Events.HECs
import GUI.Types
import GUI.Timeline.CairoDrawing
import GUI.ViewerColours

import Graphics.Rendering.Cairo
import Control.Monad
import Text.Printf

-- Minor, semi-major and major ticks are drawn and the absolute period of
-- the ticks is determined by the zoom level.
-- There are ten minor ticks to a major tick and a semi-major tick
-- occurs half way through a major tick (overlapping the corresponding
-- minor tick).
-- The timestamp values are in micro-seconds (1e-6) i.e.
-- a timestamp value of 1000000 represents 1s. The position on the drawing
-- canvas is in milliseconds (ms) (1e-3).
-- scaleValue is used to divide a timestamp value to yield a pixel value.
-- NOTE: the code below will crash if the timestampFor100Pixels is 0.
-- The zoom factor should be controlled to ensure that this never happens.

-- | Render vertical rulers (solid translucent lines), matching scale ticks.
renderVRulers :: Double -> Timestamp -> Timestamp -> Int -> XScaleMode
              -> Render()
renderVRulers scaleValue startPos endPos height xScaleMode = do
  let timestampFor100Pixels = truncate (100 * scaleValue)
      snappedTickDuration :: Timestamp
      snappedTickDuration =
        10 ^ max 0 (truncate (logBase 10 (fromIntegral timestampFor100Pixels)
                              :: Double))
      tickWidthInPixels :: Double
      tickWidthInPixels = fromIntegral snappedTickDuration / scaleValue
      firstTick :: Timestamp
      firstTick = snappedTickDuration * (startPos `div` snappedTickDuration)
  setSourceRGBAhex black 0.15
  setLineWidth scaleValue
  case xScaleMode of
    XScaleTime ->
      drawVRulers tickWidthInPixels scaleValue
        (fromIntegral $ firstTick + snappedTickDuration)
        (fromIntegral snappedTickDuration) endPos height
        (1 + fromIntegral (startPos `div` snappedTickDuration))
    XScaleLog _ dx ->
      drawVRulers 1e1000 1 dx dx endPos height 1

-- | Render a single vertical ruler and then recurse.
drawVRulers :: Double -> Double -> Double -> Double
            -> Timestamp -> Int -> Int -> Render ()
drawVRulers tickWidthInPixels scaleValue pos incr endPos height i =
  if floor pos <= endPos then do
    when (atMajorTick || atMidTick || tickWidthInPixels > 70) $ do
      draw_line (veryRoundedPos, 0) (veryRoundedPos, height)
    drawVRulers
      tickWidthInPixels scaleValue (pos + incr) incr endPos height (i + 1)
  else
    return ()
  where
    -- Hack to sync with drawXTicks.
    veryRoundedPos = round $
      scaleValue * fromIntegral (floor (fromIntegral (round pos) / scaleValue))
    atMidTick = i `mod` 5 == 0
    atMajorTick = i `mod` 10 == 0


-- | Render the X scale, based on view parameters and hecs.
renderXScaleArea :: ViewParameters -> HECs -> Render ()
renderXScaleArea ViewParameters{width, scaleValue, hadjValue, xScaleAreaHeight}
                 hecs =
  let lastTx = hecLastEventTime hecs
      off y = y + xScaleAreaHeight - 17
  in renderXScale scaleValue hadjValue lastTx width off XScaleTime


data XScaleMode = XScaleTime | XScaleLog Double Double deriving Eq

-- | Render the X (vertical) scale: render X axis and call ticks rendering.
-- TODO: refactor common parts with renderVRulers, in particular to expose
-- that ruler positions match tick positions.
renderXScale :: Double -> Double -> Timestamp -> Int
             -> (Int -> Int) -> XScaleMode
             -> Render ()
renderXScale scaleValue hadjValue lastTx width off xScaleMode = do
  let scale_width = fromIntegral width * scaleValue
      startPos :: Timestamp
      startPos = floor hadjValue
      startLine :: Timestamp
      startLine = floor $ hadjValue / scaleValue
      endPos :: Timestamp
      endPos = ceiling $ min (hadjValue + scale_width) (fromIntegral lastTx)
      endLine :: Timestamp
      endLine = ceiling $ min (hadjValue + scale_width) (fromIntegral lastTx)
                          / scaleValue
  save
  translate (- fromIntegral startLine) 0
  selectFontFace "sans serif" FontSlantNormal FontWeightNormal
  setFontSize 12
  setSourceRGBAhex black 1.0
-- setLineCap LineCapRound -- TODO: breaks rendering currently (see BrokenX.png)
  setLineWidth 1.0
  draw_line (startLine, off 16) (endLine, off 16)
  let tFor100Pixels = truncate (100 * scaleValue)
      snappedTickDuration :: Timestamp
      snappedTickDuration =
        10 ^ max 0 (truncate (logBase 10 (fromIntegral tFor100Pixels)
                              :: Double))
      tickWidthInPixels :: Double
      tickWidthInPixels = fromIntegral snappedTickDuration / scaleValue
      firstTick :: Timestamp
      firstTick = snappedTickDuration * (startPos `div` snappedTickDuration)
  case xScaleMode of
    XScaleTime ->
      drawXTicks tickWidthInPixels scaleValue (fromIntegral firstTick)
        (fromIntegral snappedTickDuration) endPos off xScaleMode
        (fromIntegral (startPos `div` snappedTickDuration))
    XScaleLog _ segmentWidth ->
      drawXTicks 1e1000 1 0 segmentWidth endPos off xScaleMode 0
  restore

-- | Render a single X scale tick and then recurse.
drawXTicks :: Double -> Double -> Double -> Double -> Timestamp
           -> (Int -> Int) -> XScaleMode -> Int
           -> Render ()
drawXTicks tickWidthInPixels scaleValue pos incr endPos off xScaleMode i =
  if floor pos <= endPos then do
    when (pos /= 0 || xScaleMode == XScaleTime) $
      draw_line (floor $ fromIntegral x1 / scaleValue, off 16)
                (floor $ fromIntegral x1 / scaleValue, off (16 - tickLength))
    when (atMajorTick || atMidTick || tickWidthInPixels > 70) $ do
      tExtent <- textExtents tickTimeText
      let tExtentWidth = textExtentsWidth tExtent
      move_to (floor $ fromIntegral textPosX / scaleValue, textPosY)
      when (floor (pos + incr) <= endPos
            && (tExtentWidth + tExtentWidth / 3 < width || atMajorTick)) $
        showText tickTimeText
    drawXTicks
      tickWidthInPixels scaleValue (pos + incr) incr endPos off xScaleMode (i+1)
  else
    return ()
  where
    atMidTick = xScaleMode == XScaleTime && i `mod` 5 == 0
    atMajorTick = xScaleMode == XScaleTime && i `mod` 10 == 0
    (textPosX, textPosY) =
      if xScaleMode == XScaleTime
      then (x1 + ceiling (scaleValue * 3), off (-3))
      else (x1 + ceiling (scaleValue * 2), tickLength + 13)
    tickLength | atMajorTick = 16
               | atMidTick = 10
               | otherwise = if xScaleMode == XScaleTime then 6 else 8
    posTime = case xScaleMode of
                XScaleTime -> round pos
                XScaleLog minX _ -> round $ 2 ** (minX + pos / incr)
    tickTimeText = showMultiTime posTime
    width = if atMidTick then 5 * tickWidthInPixels
            else tickWidthInPixels
    -- We cheat at pos 0, to avoid half covering the tick by the grey label area.
    lineWidth = scaleValue
    x1 = round $ if pos == 0 && xScaleMode == XScaleTime then lineWidth else pos

-- | Display the micro-second time unit with an appropriate suffix
-- depending on the actual time value.
-- For times < 1e-6 the time is shown in micro-seconds.
-- For times >= 1e-6 and < 0.1 seconds the time is shown in ms
-- For times >= 0.5 seconds the time is shown in seconds
showMultiTime :: Timestamp -> String
showMultiTime pos =
  if pos == 0 then "0s"
  else if pos < 1000 then -- Show time as micro-seconds for times < 1e-6
         reformatMS posf ++ (mu ++ "s")  -- microsecond (1e-6s).
       else if pos < 100000 then -- Show miliseonds for time < 0.1s
              reformatMS (posf / 1000) ++ "ms" -- miliseconds 1e-3
            else -- Show time in seconds
              reformatMS (posf / 1000000) ++ "s"
  where
    posf :: Double
    posf = fromIntegral pos
    reformatMS :: Show a => a -> String
    reformatMS pos = deZero (show pos)

-------------------------------------------------------------------------------

-- | Render horizontal rulers (dashed translucent lines),
-- matching scale ticks (visible in the common @incr@ value and starting at 0).
renderHRulers :: Int -> Timestamp -> Timestamp -> Render ()
renderHRulers hecSparksHeight start end = do
  let dstart = fromIntegral start
      dend = fromIntegral end
      incr = fromIntegral hecSparksHeight / 10
  -- dashed lines across the graphs
  setSourceRGBAhex black 0.15
  setLineWidth 1
  save
  forM_ [0, 5] $ \h -> do
    let y = h * incr
    moveTo dstart y
    lineTo dend y
    stroke
  restore

-- | Render one of the Y (horizontal) scales: render the Y axis
-- and call ticks rendering.
renderYScale :: Int -> Double -> Double -> Double -> Double -> Render ()
renderYScale hecSparksHeight scaleValue maxSpark xoffset yoffset = do
  let -- This is slightly off (by 1% at most), but often avoids decimal dot:
      maxS = if maxSpark < 100
             then maxSpark  -- too small, would be visible on screen
             else fromIntegral (2 * (ceiling maxSpark ` div` 2))
      incr = fromIntegral hecSparksHeight / 10
  save
  newPath
  moveTo (xoffset + 12) yoffset
  lineTo (xoffset + 12) (yoffset + fromIntegral hecSparksHeight)
  setSourceRGBAhex black 1.0
  setLineCap LineCapRound
  setLineWidth 1.0  -- TODO: it's not really 1 pixel, due to the scale
  stroke
  selectFontFace "sans serif" FontSlantNormal FontWeightNormal
  setFontSize 12
  scale scaleValue 1.0
  setLineWidth 0.5  -- TODO: it's not really 0.5 pixels, due to the scale
  drawYTicks maxS 0 incr xoffset yoffset 0
  restore

-- | Render a single Y scale tick and then recurse.
drawYTicks :: Double -> Double -> Double -> Double -> Double -> Int -> Render ()
drawYTicks maxS pos incr xoffset yoffset i =
  if i <= 10 then do
    -- TODO: snap to pixels, currently looks semi-transparent
    moveTo (xoffset + 12) (yoffset + majorTick - pos)
    lineTo (xoffset + 12 - tickLength) (yoffset + majorTick - pos)
    stroke
    when (atMajorTick || atMidTick) $ do
      tExtent <- textExtents tickText
      (fewPixels, yPix) <- deviceToUserDistance 3 4
      moveTo (xoffset - textExtentsWidth tExtent - fewPixels)
             (yoffset + majorTick - pos + yPix)
      when (atMidTick || atMajorTick) $
        showText tickText
    drawYTicks maxS (pos + incr) incr xoffset yoffset (i + 1)
  else
    return ()
  where
    atMidTick = i `mod` 5 == 0
    atMajorTick = i `mod` 10 == 0
    majorTick = 10 * incr
    tickText = reformatV (fromIntegral i * maxS / 10)
    tickLength | atMajorTick = 11
               | atMidTick   = 9
               | otherwise   = 6
    reformatV :: Double -> String
    reformatV v =
      if v < 0.01 && v > 0
      then eps
      else deZero (printf "%.2f" v)

-------------------------------------------------------------------------------

-- | The \'micro\' symbol.
mu :: String
#if MIN_VERSION_cairo(0,12,0) && !MIN_VERSION_cairo(0,12,1)
-- this version of cairo doesn't handle Unicode properly.
-- Thus, we do the encoding by hand:
mu = "\194\181"
#else
-- Haskell cairo bindings 0.12.1 have proper Unicode support
mu = "\x00b5"
#endif

-- | The \'epsilon\' symbol.
eps :: String
#if MIN_VERSION_cairo(0,12,0) && !MIN_VERSION_cairo(0,12,1)
-- this version of cairo doesn't handle Unicode properly.
-- Thus, we do the encoding by hand:
eps = "\206\181"
#else
-- Haskell cairo bindings 0.12.1 have proper Unicode support
eps = "\x03b5"
#endif


-- | Remove all meaningless trailing zeroes.
deZero :: String -> String
deZero s
  | '.' `elem` s =
    reverse . dropWhile (=='.') . dropWhile (=='0') . reverse $ s
  | otherwise = s