A4YPCXNG44HRKJZLXU5Q4JEBD73UETQCHVMBHVQR2UDCYZLLAFXAC module Frontend.Util whereimport Preludeimport Data.Argonaut.Core (Json)import Data.Argonaut.Decode (JsonDecodeError, decodeJson, (.:))import Data.Array (filter, length, zip, (..))import Data.DateTime (DateTime, Time(..), adjust, diff, modifyTime, setMillisecond, setMinute, setSecond)import Data.DateTime.Instant (fromDateTime, instant, toDateTime, unInstant)import Data.Either (Either, either, note)import Data.Formatter.DateTime (formatDateTime)import Data.Int (floor, toNumber)import Data.Maybe (Maybe, fromMaybe)import Data.Newtype (unwrap)import Data.Time.Duration (Days(..), Hours(..), Milliseconds(..), Seconds(..))import Data.Traversable (sequence, traverse)import Data.Tuple (Tuple)import Effect.Aff (Aff, error, makeAff)import Graphics.Canvas (CanvasImageSource, tryLoadImage)import Halogen.HTML as HHimport Halogen.HTML.Properties as HPclasses :: forall r i. Array String -> HH.IProp (class :: String | r) iclasses = HP.classes <<< map HH.ClassNameformatDateTime' :: DateTime -> StringformatDateTime' = either identity identity <<< formatDateTime "YYYY-MM-DD HH:mm:ss.SS"datetimeToSeconds :: DateTime -> NumberdatetimeToSeconds = (_ / 1000.0) <<< unwrap <<< unInstant <<< fromDateTimesecondsToDateTime :: Number -> Maybe DateTimesecondsToDateTime = map toDateTime <<< instant <<< Milliseconds <<< (_ * 1000.0)-- TODO: convert stuff to local time-- getTimezoneOffset $ fromDateTime startmidnightBefore :: DateTime -> DateTimemidnightBefore = modifyTime (const $ Time bottom bottom bottom bottom)hourBefore :: DateTime -> DateTimehourBefore = modifyTime (setMinute bottom <<< setSecond bottom <<< setMillisecond bottom)daysInInterval :: DateTime -> DateTime -> Array DateTimedaysInInterval start end = doletmidnight = midnightBefore startduration = diff end midnight :: DaysnDays = floor (unwrap duration)fromMaybe [] <<< sequence $0 .. nDays <#> \n ->adjust (Days (toNumber n)) midnighthoursInInterval :: DateTime -> DateTime -> DateTime -> Array DateTimehoursInInterval current start end = doletmidnight = midnightBefore currenthours = 0 .. 23 <#> Hours <<< toNumberhoursDT = fromMaybe [] <<< sequence $ hours <#> \n -> adjust n midnightfilter (\a -> (unwrap (a `diff` start :: Hours) >= -1.0) && a <= end) hoursDTdecodeTimestamps :: Json -> Either JsonDecodeError (Array Number)decodeTimestamps json = doo <- decodeJson jsoncontents <- o .: "Ok"decodeJson contents >>= traverse decodeJsonformatDuration :: Seconds -> StringformatDuration (Seconds t) = leth = floor $ t / 3600.0m = floor (t / 60.0) - h * 60s = floor t - h * 3600 - m * 60in show h <> "h " <> show m <> "m " <> show s <> "s"loadImage :: String -> Aff CanvasImageSourceloadImage url = domakeAff \cont -> dotryLoadImage url \msource ->note (error $ "Failed to load image from \"" <> url <> "\"") msource # contmemptyenumerate :: forall a. Array a -> Array (Tuple Int a)enumerate a = zip (0 .. (length a - 1)) a
"use strict";var uPlot = require("/uPlot/dist/uPlot.cjs.js");exports.setSizeImpl = uplot => width => height => () => {uplot.setSize({width, height});};exports.setDataImpl = uplot => data => () => {uplot.setData(data);};exports.destroyImpl = uplot => () => {uplot.destroy();uplot = null;};exports.initializeImpl = element => opts => () => {console.log("opts", opts);return new uPlot(opts, [], element);};
module Frontend.UPlot whereimport Preludeimport Data.Maybe (Maybe)import Data.Nullable (Nullable, toNullable)import Effect (Effect)import Web.HTML (HTMLElement)foreign import initializeImpl :: HTMLElement -> Opts -> Effect UPlotforeign import setSizeImpl :: UPlot -> Number -> Number -> Effect Unitforeign import setDataImpl :: UPlot -> Array (Array (Nullable Number)) -> Effect Unitforeign import destroyImpl :: UPlot -> Effect Unitdata UPlot -- opaque uPlot handledata Plot = Plot{ uplot :: UPlot}-- Optstype Series ={ stroke :: String, fill :: String}type Grid ={ show :: Boolean, stroke :: String, width :: Int}defaultGrid :: GriddefaultGrid ={ show: true, stroke: "rgba(0,0,0,0.07)", width: 2}noGrid :: GridnoGrid = defaultGrid { show=false }type Axis ={ show :: Boolean, scale :: String, space :: Int, gap :: Int, size :: Int, labelSize :: Int-- , labelFont :: Font, side :: Int, grid :: Grid-- , ticks :: Ticks-- , font :: Font-- , rotate: Int}defaultXAxis :: AxisdefaultXAxis ={ show: true, scale: "x", space: 50, gap: 5, size: 50, labelSize: 30-- , labelFont, side: 2--, class: "x-vals"--, incrs: timeIncrs--, values: timeVals--, filter: retArg1, grid: defaultGrid-- , ticks-- , font-- , rotate: 0}defaultYAxis :: AxisdefaultYAxis ={ show: true, scale: "y", space: 30, gap: 5, size: 50, labelSize: 30-- , labelFont, side: 3--, class: "x-vals"--, incrs: timeIncrs--, values: timeVals--, filter: retArg1, grid: defaultGrid-- , ticks-- , font-- , rotate: 0}noAxis :: AxisnoAxis = defaultXAxis { show=false }type Opts ={ width :: Number, height :: Number, axes :: Array Axis, scales ::{ x ::{ time :: Boolean, distr :: Int}}, series :: Array Series}defaultSeries :: SeriesdefaultSeries ={ stroke: "blue", fill: "rgba(0,0,255,0.1)"}defaultOpts :: OptsdefaultOpts ={ width: 500.0, height: 300.0, axes: [defaultXAxis, defaultYAxis], scales:{ x:{ time: true, distr: 1}}, series: [defaultSeries, defaultSeries]}initialize :: HTMLElement -> Opts -> Effect Plotinitialize e opts = initializeImpl e opts <#> \uplot -> Plot { uplot: uplot }setSize :: Plot -> Number -> Number -> Effect UnitsetSize (Plot p) w h = setSizeImpl p.uplot w hsetData :: Plot -> Array (Array (Maybe Number)) -> Effect UnitsetData (Plot p) = setDataImpl p.uplot <<< map (map toNullable)destroy :: Plot -> Effect Unitdestroy (Plot p) = destroyImpl p.uplot
module Frontend.Types whereimport Preludeimport Data.Argonaut.Core (Json)import Data.Argonaut.Decode (class DecodeJson, JsonDecodeError(..), decodeJson, (.:))import Data.Array (zipWith, (!!))import Data.DateTime (DateTime)import Data.DateTime.Instant (fromDateTime, unInstant)import Data.Either (Either(..), note)import Data.Generic.Rep (class Generic)import Data.Generic.Rep.Show (genericShow)import Data.Maybe (Maybe)import Data.Newtype (class Newtype, unwrap)import Data.Traversable (for)import Data.Tuple (Tuple(..))import Foreign.Object as FOimport Frontend.Util (secondsToDateTime)dateTimeToSeconds :: DateTime -> NumberdateTimeToSeconds = (_ / 1000.0) <<< unwrap <<< unInstant <<< fromDateTimedata Vec2 = V2 Number Numbernewtype Box = Box { x0 :: Number, y0 :: Number, x1 :: Number, y1 :: Number }newtype Object = Object{ id :: Int, pos :: Vec2, gap :: Int, heading :: Vec2}newtype Instant = Instant{ timestamp :: DateTime, objects :: Array Object}newtype MaybeInstant = MaybeInstant{ timestamp :: DateTime, objects :: Maybe (Array Object)}type Tracklet = Array{ t :: DateTime, p :: Vec2}newtype Tracklets = Tracklets (Array Tracklet)newtype Info = Info{ n_entries :: Int, first_dt :: DateTime, last_dt :: DateTime}type Zone ={ name :: String, box :: Box}newtype Config = Config{ bbox :: Box, grid_px_size :: Number, n_objects :: Int, model_path :: String, min_tracklet_len :: Int, predict_stride :: Number, toss_first :: Int, gpu :: Int, map :: String, zones :: Array Zone, cameras :: Array{ name :: String, width :: Int, height :: Int, calib :: String, image :: String}}derive instance genericVec2 :: Generic Vec2 _instance showBox :: Show Box whereshow (Box {x0, y0, x1, y1}) = "x: " <> show [x0, x1] <> " m, y: " <> show [y0, y1] <> " m"instance showVec2 :: Show Vec2 whereshow = genericShowderive instance genericObject :: Generic Object _derive instance newtypeObject :: Newtype Object _instance showObject :: Show Object whereshow = genericShowderive instance genericInstant :: Generic Instant _derive instance newtypeInstant :: Newtype Instant _instance showInstant :: Show Instant whereshow = genericShowderive instance genericMaybeInstant :: Generic MaybeInstant _derive instance newtypeMaybeInstant :: Newtype MaybeInstant _instance showMaybeInstant :: Show MaybeInstant whereshow = genericShowderive instance genericTracklets :: Generic Tracklets _derive instance newtypeTracklets :: Newtype Tracklets _instance showTracklets :: Show Tracklets whereshow = genericShowderive instance genericInfo :: Generic Info _derive instance newtypeInfo :: Newtype Info _instance showInfo :: Show Info whereshow = genericShowderive instance genericConfig :: Generic Config _derive instance newtypeConfig :: Newtype Config _instance showConfig :: Show Config whereshow = genericShowinstance decodeJsonVec2 :: DecodeJson Vec2 wheredecodeJson json = case decodeJson json ofLeft err -> Left errRight (Tuple a b) -> Right (V2 a b)instance decodeJsonObject :: DecodeJson Object wheredecodeJson json = Object <$> decodeJson jsoninstance decodeBox :: DecodeJson Box wheredecodeJson :: Json -> Either JsonDecodeError BoxdecodeJson json = doo <- decodeJson jsoncase o of[x0, y0, x1, y1] -> pure $ Box { x0, y0, x1, y1 }_ -> Left $ TypeMismatch "Bounding box must have 4 elements."instance decodeJsonInstant :: DecodeJson Instant wheredecodeJson :: Json -> Either JsonDecodeError InstantdecodeJson json = doo <- decodeJson jsonts <- o .: "timestamp"objects <- o .: "objects"timestamp <- note (TypeMismatch "Invalid Timestamp") $ secondsToDateTime tspure $ Instant { timestamp: timestamp, objects }instance decodeJsonMaybeInstant :: DecodeJson MaybeInstant wheredecodeJson :: Json -> Either JsonDecodeError MaybeInstantdecodeJson json = doo <- decodeJson jsonts <- o .: "timestamp"objects <- o .: "objects"timestamp <- note (TypeMismatch "Invalid Timestamp") $ secondsToDateTime tspure $ MaybeInstant { timestamp: timestamp, objects }instance decodeJsonInfo :: DecodeJson Info wheredecodeJson :: Json -> Either JsonDecodeError InfodecodeJson json = doo <- decodeJson jsonn_entries <- o .: "entries"first_ts <- o .: "first_ts"last_ts <- o .: "last_ts"first_dt <- note (TypeMismatch "Invalid Timestamp") $ secondsToDateTime first_tslast_dt <- note (TypeMismatch "Invalid Timestamp") $ secondsToDateTime last_tspure $ Info { n_entries, first_dt, last_dt }instance decodeJsonConfig :: DecodeJson Config wheredecodeJson :: Json -> Either JsonDecodeError ConfigdecodeJson json = doo <- decodeJson jsonbbox <- o .: "bbox"grid_px_size <- o .: "grid_px_size"n_objects <- o .: "n_objects"model_path <- o .: "model_path"min_tracklet_len <- o .: "min_tracklet_len"predict_stride <- o .: "predict_stride"toss_first <- o .: "toss_first"gpu <- o .: "gpu"map <- o .: "map"zones' <- o .: "zones" :: Either JsonDecodeError (Array Json)zones <- for zones' \zone -> doz <- decodeJson zonename <- z .: "name"box <- z .: "box"pure { name, box: Box box }cs <- o .: "cameras" :: Either JsonDecodeError (Array Json)cameras <- for cs \cj -> doc <- decodeJson cjname <- c .: "name"width <- c .: "w"height <- c .: "h"calib <- c .: "calib"image <- c .: "image"pure { name: name, width, height, calib, image }pure $ Config{ bbox, grid_px_size, n_objects, model_path, min_tracklet_len, predict_stride, toss_first, gpu, map, zones, cameras }instance decodeJsonTracklets :: DecodeJson Tracklets wheredecodeJson :: Json -> Either JsonDecodeError TrackletsdecodeJson json = dolet arrayErr = TypeMismatch "Invalid array length."tracklets <- decodeJson json :: Either JsonDecodeError (Array (FO.Object Json))res <- for tracklets \tracklet -> dott <- tracklet .: "t"px <- tracklet .: "x"py <- tracklet .: "y"dt <- note (TypeMismatch "Invalid timestamp.") $ for tt secondsToDateTimeRight $ zipWith (\t p -> {t, p}) dt (zipWith V2 px py)let r = respure $ Tracklets res
module Frontend.Tracklets( TrackletSOA, trackletSOA, unTrackletSOA) whereimport Preludeimport Data.Argonaut.Core (Json)import Data.Argonaut.Decode (class DecodeJson, JsonDecodeError(..), decodeJson, (.:))import Data.Array (length, (!!))import Data.DateTime (DateTime)import Data.Either (Either, note)import Data.Maybe (Maybe(..))import Frontend.Util (secondsToDateTime)type TrackletSOAData ={ id :: Int, t :: Array Number, x :: Array Number, y :: Array Number, t0 :: DateTime}newtype TrackletSOA = TrackletSOA TrackletSOADatatrackletSOA :: TrackletSOAData -> Maybe TrackletSOAtrackletSOA {id, t, x, y, t0}| length t == length x && length x == length y = Just $ TrackletSOA {id, t, x, y, t0}| otherwise = NothingunTrackletSOA :: TrackletSOA -> TrackletSOADataunTrackletSOA (TrackletSOA d) = dinstance showTrackletSOA :: Show TrackletSOA whereshow (TrackletSOA {t0, t, x, y}) = "Tracklet with " <> show (length t) <> " points starting at " <> show t0instance decodeJsonTracklets :: DecodeJson TrackletSOA wheredecodeJson :: Json -> Either JsonDecodeError TrackletSOAdecodeJson json = dotracklet <- decodeJson jsonid <- tracklet .: "id"t <- tracklet .: "t"x <- tracklet .: "x"y <- tracklet .: "y"t0s <- note (TypeMismatch "Empty tracklet.") $ t !! 0t0 <- note (TypeMismatch "Invalid timestamp.") $ secondsToDateTime t0snote (TypeMismatch "Lengths of {t, x, y} arrays are not equal.") $ trackletSOA{id, t: (_ - t0s) <$> t, x, y, t0}
module Frontend.Tracklet.Filter whereimport Preludeimport Data.Array (dropWhile, head, init, last, tail, zipWith)import Data.DateTime (DateTime, diff)import Data.Foldable (sum)import Data.Maybe (Maybe(..), fromMaybe, isJust)import Data.Time.Duration (class Duration, Seconds(..), convertDuration)import Frontend.Types (Box(..), Vec2(..), Tracklet)import Math (sqrt)type TrackletFilter = Tracklet -> Boolean-- Boolean algebra for tracklet filters is automatically implemented.-- (true, false, and, or, not, implies) are for freedistanceAtLeast :: Number -> TrackletFilterdistanceAtLeast n t = (_ >= n) $ sum $ fromMaybe [0.0] $ (zipWith dist <$> init t <*> tail t) wheredist {p: V2 x1 y1} {p: V2 x2 y2} = sqrt $ (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)durationAtLeast :: forall d. Duration d => d -> TrackletFilterdurationAtLeast d t = fromMaybe false doh <- head tl <- last tlet d' = diff l.t h.t :: Secondspure $ d' >= (convertDuration d)insideBox :: Box -> PointFilterinsideBox (Box {x0, y0, x1, y1}) {p: (V2 x y)} =x0 <= x && x <= x1 && y0 <= y && y <= y1first :: PointFilter -> Tracklet -> Booleanfirst = runParser <<< onetype PointFilter = { t :: DateTime, p :: Vec2 } -> Boolean-- LL(0) parsertype Parser = Tracklet -> Maybe TrackletrunParser :: Parser -> Tracklet -> BooleanrunParser p = isJust <<< pany :: PointFilter -> Parserany f = until f `andThen` one funtil :: PointFilter -> Parseruntil f = many (not <$> f)many :: PointFilter -> Parsermany f = Just <<< dropWhile fone :: PointFilter -> Parserone f t = case f <$> head t ofJust true -> tail t_ -> NothingandThen :: Parser -> Parser -> ParserandThen p q t = p t >>= qtype HLine = {x0 :: Number, x1 :: Number, y :: Number}data Direction = Up | Dnduration :: forall d. Duration d => d -> Parserduration d t = doh <- head tlet pred = dropWhile (\x -> (diff x.t h.t :: Seconds) < convertDuration d) tcase pred of[] -> Nothingl -> Just lhLine :: HLine -> Direction -> ParserhLine {x0, x1, y} dir = letrt = Box {x0, x1, y0: y + 0.5, y1: y + 2.0}rb = Box {x0, x1, y0: y - 2.0, y1: y - 0.5}{r1, r2} = case dir ofDn -> {r1: rt, r2: rb}Up -> {r1: rb, r2: rt}in until (insideBox r1) `andThen` one (insideBox r1) `andThen` until (insideBox r2) `andThen` one (insideBox r2)
module Frontend.Route whereimport Prelude hiding ((/))import Data.Generic.Rep (class Generic)import Data.Generic.Rep.Show (genericShow)import Effect.Class (class MonadEffect, liftEffect)import Routing.Duplex (RouteDuplex', print, root)import Routing.Duplex.Generic (noArgs, sum)import Routing.Duplex.Generic.Syntax ((/))import Routing.Hash (setHash)-- All possible routes in the applicationdata Route= Dashboard| Tracking| Filtering| Statistics| Zones| Camerasderive instance genericRoute :: Generic Route _derive instance eqRoute :: Eq Routederive instance ordRoute :: Ord Routeinstance showRoute :: Show Route whereshow = genericShowrouteCodec :: RouteDuplex' RouterouteCodec = root $ sum{ "Dashboard": noArgs, "Tracking": "tracking" / noArgs, "Filtering": "filtering" / noArgs, "Statistics": "statistics" / noArgs, "Zones": "zones" / noArgs, "Cameras": "cameras" / noArgs}navigate :: forall m. MonadEffect m => Route -> m Unitnavigate = liftEffect <<< setHash <<< print routeCodec
module Main whereimport Preludeimport Data.Maybe (Maybe(..))import Effect (Effect)import Effect.Aff (launchAff_)import Halogen (liftEffect)import Halogen as Himport Halogen.Aff as HAimport Halogen.VDom.Driver (runUI)import Routing.Duplex (parse)import Routing.Hash (matchesWith)import Frontend.Component.Index as Indeximport Frontend.Route (routeCodec)main :: Effect Unitmain = HA.runHalogenAff dobody <- HA.awaitBodyhalogenIO <- runUI Index.component unit bodyvoid $ liftEffect $ matchesWith ( parse routeCodec ) \mOld new ->when ( mOld /= Just new ) dolaunchAff_$ halogenIO.query $ H.tell $ Index.Navigate newpure unit
"use strict";exports.imgElementToImageSourceImpl = function(id, Just, Nothing) {return function() {var el = document.getElementById(id);if (el && el instanceof HTMLImageElement) {return Just(el);} else {return Nothing;}};};exports.imageShape = image => {return { width: image.width, height: image.height };}
module Frontend.Draw whereimport Preludeimport CSS.Color as CSSimport Color (Color, hsl, rgb, toHSLA, toHexString)import Control.Safely (for_)import Data.Array ((!!))import Data.Function.Uncurried (Fn3, runFn3)import Data.Int (toNumber)import Data.Maybe (Maybe(..))import Effect (Effect)import Frontend.Types (Box(..), Object(..), Tracklet, Vec2(..))import Graphics.Canvas (CanvasElement, CanvasImageSource, getCanvasHeight, getCanvasWidth, getContext2D)import Graphics.Canvas as Canvasimport Math (atan2, cos, pi, sin)foreign import imgElementToImageSourceImpl :: forall r. Fn3 String (CanvasImageSource -> r) r (Effect r)foreign import imageShape :: CanvasImageSource -> { width :: Int, height :: Int }-- | Get a img element by ID, or `Nothing` if the element does not exist.getImgElementById :: String -> Effect (Maybe CanvasImageSource)getImgElementById elId = runFn3 imgElementToImageSourceImpl elId Just NothingmetersToPixels :: Vec2 -> Box -> Vec2 -> Vec2metersToPixels (V2 canvasW canvasH) (Box {x0, y0, x1, y1}) (V2 x y) = letw = x1 - x0h = y1 - y0px = (x - x0) / w * canvasWpy = (1.0 - (y - y0) / h) * canvasHin V2 px pycanvasShape :: CanvasElement -> Effect Vec2canvasShape elem = dow <- getCanvasWidth elemh <- getCanvasHeight elempure $ V2 w hdrawTracklet :: CanvasElement -> Box -> Number -> Tracklet -> Effect UnitdrawTracklet elem box alpha tracklet = doshape <- canvasShape elemctx <- getContext2D elemletcolor = rgb 100 100 200Canvas.setLineJoin ctx Canvas.BevelJoinCanvas.setLineWidth ctx 5.0Canvas.setGlobalAlpha ctx alphaCanvas.setStrokeStyle ctx (toHexString color)Canvas.beginPath ctxfor_ tracklet \pt -> dolet V2 px py = metersToPixels shape box pt.pCanvas.lineTo ctx px pyCanvas.stroke ctx-- start circlecase tracklet !! 0 ofNothing -> pure unitJust pt -> dolet V2 px py = metersToPixels shape box pt.pCanvas.setGlobalAlpha ctx 0.2Canvas.setFillStyle ctx (toHexString $ rgb 200 100 100)Canvas.beginPath ctxCanvas.arc ctx{ x: px, y: py, radius: 5.0, start: 0.0, end: 6.3}Canvas.fill ctxhashedColor :: Int -> ColorhashedColor h = hsl (toNumber $ h * 15_485_867 `mod` 255) 1.0 0.3colorToCss :: Color -> CSS.ColorcolorToCss col = let{h, s, l, a} = toHSLA colin CSS.hsla h s l adrawObject :: CanvasElement -> Box -> Object -> Effect UnitdrawObject elem box (Object obj) = doctx <- getContext2D elemshape <- canvasShape elemlet V2 hx hy' = obj.headinghy = -hy'angle = atan2 hy hxV2 px py = metersToPixels shape box $ obj.poslet color = toHexString (hashedColor obj.id)alpha = clamp 0.0 1.0 $ toNumber (10 - obj.gap) / 10.0Canvas.setLineWidth ctx 5.0Canvas.setGlobalAlpha ctx alphaCanvas.setStrokeStyle ctx colorCanvas.setFillStyle ctx colorCanvas.beginPath ctxCanvas.arc ctx{ x: px, y: py, radius: 10.0, start: angle + 3.15 / 3.0, end: angle - 3.15 / 3.0}Canvas.moveTo ctx (px + cos (angle - pi / 3.0) * 10.0) (py + sin (angle - pi / 3.0) * 10.0)Canvas.lineTo ctx (px + hx * 20.0) (py + hy * 20.0)Canvas.lineTo ctx (px + cos (angle + pi / 3.0) * 10.0) (py + sin (angle + pi / 3.0) * 10.0)Canvas.stroke ctxCanvas.setGlobalAlpha ctx (alpha / 3.0)Canvas.fill ctx
module Frontend.Dom where-- import Preludeimport Halogen.HTML as HHimport Frontend.Util (classes)card :: forall w i. Array String -> Array (HH.HTML w i) -> Array (HH.HTML w i) -> HH.HTML w icard classes' header body =HH.div [ classes classes' ][ HH.div [ classes ["card", "shadow", "mb-4"] ][ HH.div [ classes ["card-header", "card-header", "py-3", "d-flex", "flex-row", "align-items-center", "justify-content-between"] ][ HH.h6 [ classes ["m-0", "font-weight-bold", "text-primary"] ]header], HH.div [ classes ["card-body"] ]body]]
module Frontend.Component.Zones whereimport Preludeimport Data.Maybe (Maybe(..))import Effect.Aff.Class (class MonadAff)import Frontend.Dom (card)import Frontend.Types (Config(..))import Frontend.Util (classes)import Halogen as Himport Halogen.HTML as HHtype State ={ config :: Config}data Action= Initializetype Input = { config :: Config }component :: forall query output m. MonadAff m => H.Component HH.HTML query Input output mcomponent = H.mkComponent{ initialState: identity, render, eval: H.mkEval H.defaultEval{ initialize = Just Initialize}}render :: forall s m. MonadAff m => State -> H.ComponentHTML Action s mrender { config: Config config } =HH.div [ classes ["row"] ][ card ["col-lg-12"][ HH.text "Zones" ][ HH.span_ [ HH.text $ "ToDo" ]]]
module Frontend.Component.Statistics whereimport Preludeimport Data.Array (elemIndex, filter, head, last, length, sortWith, take, zip, (!!))import Data.DateTime (DateTime)import Data.Either (Either, hush)import Data.Foldable (and, or)import Data.Int (round, toNumber)import Data.JSDate (getTimezoneOffset, now)import Data.List (fromFoldable)import Data.Maybe (Maybe(..), fromMaybe)import Data.Tuple (Tuple(..))import Effect.Aff.Class (class MonadAff, liftAff)import Effect.Class.Console (log)import Frontend.Api (fetchTrackletsSOA)import Frontend.Dom (card)import Frontend.Tracklet.Filter (insideBox)import Frontend.Tracklets (TrackletSOA, unTrackletSOA)import Frontend.Types (Config(..), Info(..), Vec2(..))import Frontend.Util (classes, formatDateTime')import Global (encodeURIComponent)import Halogen (liftEffect)import Halogen as Himport Halogen.HTML as HHimport Halogen.HTML.Events as HEimport Halogen.HTML.Properties as HPimport Record.CSV.Printer (printCSVWithOrder)import Record.CSV.Printer.SList (SLProxy(..))import Record.CSV.Printer.SList (type (:), type (!), SLProxy(..))type Props ={ len :: Int, id :: Int, startTime :: DateTime, duration :: Number, durationInShop :: Number, startZones :: Array Boolean, endZones :: Array Boolean, throughZones :: Array Boolean, throughShop :: Boolean}type Slots =()-- _plot = (SProxy :: SProxy "plot")type State ={ info :: Info, config :: Config, fetching :: Boolean, tracklets :: Maybe (Array TrackletSOA), props :: Maybe (Array Props)}data Action= Initialize| SetInput Input| FetchDatatype Input = { info :: Info, config :: Config }component :: forall query output m. MonadAff m => H.Component HH.HTML query Input output mcomponent = H.mkComponent{ initialState: \{info, config} ->{ info, config, fetching: false, tracklets: Nothing, props: Nothing}, render, eval: H.mkEval H.defaultEval{ handleAction = handleAction, initialize = Just Initialize, receive = Just <<< SetInput}}render :: forall m. MonadAff m => State -> H.ComponentHTML Action Slots mrender state =HH.div [ classes ["row"] ][ HH.div [ classes ["col-lg-12"] ][ card [][ HH.text "Statistics" ][ statistics state]]]type Order= "id": "startTime": "duration": "durationInShop": "startZone": "endZone"! "throughShop"toCsv :: Array String -> Array Props -> StringtoCsv zoneNames props = fromMaybe "Error" $ encodeURIComponent =<< (hush csv) wherecsv = printCSVWithOrder (SLProxy :: SLProxy Order) (fromFoldable (map csvTransform props))csvTransform{ len, id, startTime, duration, durationInShop, startZones, endZones, throughZones, throughShop} ={ id, startTime: formatDateTime' startTime, duration: round duration, durationInShop: round durationInShop, startZone: fromMaybe "-" $ (zoneNames !! _) =<< elemIndex true startZones, endZone: fromMaybe "-" $ (zoneNames !! _) =<< elemIndex true endZones, throughShop}statistics :: forall m. MonadAff m => State -> H.ComponentHTML Action Slots mstatistics {info: Info info, config: Config config, tracklets: mtracklets, fetching, props} = letzoneNames = take 4 (_.name <$> config.zones)in HH.div_[ HH.button[ HE.onClick $ const $ Just FetchData, classes ["btn", "btn-primary", "btn-outline"]] [HH.text "Fetch Data"], if fetchingthen HH.span [ classes ["spinner-border", "spinner-border-sm", "mx-4"] ][ HH.span [ classes ["sr-only"] ] [ HH.text "Loading..." ]]else HH.text "", HH.div [ classes ["m-2"] ] case mtracklets ofJust tracklets ->[ HH.text $ "Number of tracklets: " <> show (length tracklets)]Nothing ->[ HH.text "No data." ], HH.div [ classes ["m-2"] ] case props ofNothing -> []Just props' ->[ HH.a [HP.href $ "data:text/plain;charset=UTF-8," <> toCsv zoneNames props', HP.download "data.csv"] [HH.text "csv"]]]cmpProps :: Config -> TrackletSOA -> PropscmpProps (Config config) tr = let{id, t0, t, x, y} = unTrackletSOA trshopZones = config.zones -- FIXME: checkout is inside the shop.inShop p = and ((\{box} -> not $ insideBox box p) <$> shopZones)in{ len: length t, id, startTime: t0, duration: fromMaybe 0.0 $ last t, durationInShop: letlenInShop = length $ filter identity $ zip x y <#> \(Tuple x' y') ->inShop { p: V2 x' y', t: t0 }in toNumber lenInShop / toNumber (length t) * (fromMaybe 0.0 $ last t), startZones: fromMaybe (config.zones $> false) dox0 <- head xy0 <- head ypure $ config.zones <#> \zone ->insideBox zone.box { p: V2 x0 y0, t: t0 }, endZones: fromMaybe (config.zones $> false) dox0 <- last xy0 <- last ypure $ config.zones <#> \zone ->insideBox zone.box { p: V2 x0 y0, t: t0 }, throughZones: config.zones <#> \zone ->or $ zip x y <#> \(Tuple x' y') ->insideBox zone.box { p: V2 x' y', t: t0 }, throughShop: or $ zip x y <#> \(Tuple x' y') ->inShop { p: V2 x' y', t: t0 }}handleAction :: forall output m. MonadAff m => Action -> H.HalogenM State Action Slots output m UnithandleAction = case _ ofInitialize -> do{info: Info info, config: Config config} <- H.getpure unitSetInput { info, config } -> H.modify_ _ { info = info, config = config }FetchData -> do{info: Info info, config: Config config} <- H.getH.modify_ _ { fetching = true }-- Get datatracklets <- liftAff $ fetchTrackletsSOA info.first_dt info.last_dt 10-- FIXME: Get offset from data, not from nowtzOffset <- liftEffect $ now >>= getTimezoneOffset-- Compute statisticsletprops :: Either String (Array Props)props = sortWith _.id <<< map (cmpProps (Config config)) <$> trackletslog $ show propsH.modify_ _ { fetching = false, tracklets = hush tracklets, props = hush props }
module Frontend.Component.Plot whereimport Preludeimport Data.Foldable (sequence_, traverse_)import Data.Maybe (Maybe(..))import Data.Traversable (for_)import Effect.Aff.Class (class MonadAff)import Effect.Class (class MonadEffect)import Effect.Class.Console (log)import Frontend.UPlot as UPlotimport Halogen (liftEffect)import Halogen as Himport Halogen.HTML as HHimport Halogen.HTML.Properties as HPimport Halogen.Query.EventSource (eventListenerEventSource)import Web.Event.Event (EventType(..))import Web.HTML (window)import Web.HTML.HTMLElement (getBoundingClientRect)import Web.HTML.Window (document, toEventTarget)type State ={ plot :: Maybe UPlot.Plot, opts :: UPlot.Opts}type Data = Array (Array (Maybe Number))data Query a =SetData Data adata Action= Initialize| Finalize| Resize-- | SetOpts UPlot.Optscomponent :: forall o m. MonadAff m => H.Component HH.HTML Query UPlot.Opts o mcomponent =H.mkComponent{ initialState: \opts -> { plot: Nothing, opts }, render, eval: H.mkEval $ H.defaultEval{ handleAction = handleAction, handleQuery = handleQuery, initialize = Just Initialize, finalize = Just Finalize, receive = const Nothing -- Just <<< SetOpts}}render :: forall m. State -> H.ComponentHTML Action () mrender state = HH.div [ HP.ref (H.RefLabel "plot") ] []handleQuery :: forall a o m. MonadEffect m => Query a -> H.HalogenM State Action () o m (Maybe a)handleQuery = case _ ofSetData d a -> doplot' <- H.get <#> _.plotfor_ plot' \plot -> doliftEffect $ UPlot.setData plot dpure (Just a)handleAction ∷ forall o m. MonadAff m => Action → H.HalogenM State Action () o m UnithandleAction = case _ ofInitialize -> dolog "Initializing"div <- H.getHTMLElementRef (H.RefLabel "plot")opts <- H.get <#> _.optsfor_ div \element -> doplot <- liftEffect $ UPlot.initialize element opts-- liftEffect $ setData plot [[1.0, 2.0, 3.0], [10.0, 12.0, 11.0], [20.0, 12.0, 11.0], [30.0, 12.0, 11.0]]H.modify_ _ { plot = Just plot }-- Register resize handlerdocument <- H.liftEffect $ document =<< windowwindow <- liftEffect windowH.subscribe' \sid ->eventListenerEventSource(EventType "resize")(toEventTarget window)\ev -> Just ResizehandleAction Resize-- log $ show elementFinalize -> doplot <- H.get <#> _.plotliftEffect $ traverse_ UPlot.destroy plotResize -> dodiv <- H.getHTMLElementRef (H.RefLabel "plot")state <- H.getsequence_ dodiv' <- divplot <- state.plot :: Maybe UPlot.Plotpure $ dorect <- liftEffect $ getBoundingClientRect div'liftEffect $ UPlot.setSize plot rect.width state.opts.height-- SetOpts opts -> do-- H.modify_ _ { opts = opts }-- log "Setting opts"-- handleAction Initialize
module Frontend.Component.Index whereimport Preludeimport Control.Monad.Rec.Class (forever)import Data.Either (Either(..), hush, isLeft, note)import Data.Maybe (Maybe(..), fromMaybe, isJust)import Data.Monoid (guard)import Data.Symbol (SProxy(..))import Data.Traversable (for, sequence)import Effect.Aff (Milliseconds(..), delay, error, forkAff, killFiber)import Effect.Aff.Class (class MonadAff, liftAff)import Effect.Class (class MonadEffect, liftEffect)import Effect.Class.Console (log)import Frontend.Api (fetchConfig, fetchInfo, fetchInfoImage)import Frontend.Assets (certiconLogo)import Frontend.Assets as Assetsimport Frontend.Component.Cameras as Camerasimport Frontend.Component.Dashboard as Dashboardimport Frontend.Component.Filtering as Filteringimport Frontend.Component.Statistics as Statisticsimport Frontend.Component.Zones as Zonesimport Frontend.Route (Route(..), navigate, routeCodec)import Frontend.Types (Config(..), Info)import Frontend.Util (classes)import Graphics.Canvas (CanvasImageSource)import Halogen (SubscriptionId)import Halogen as Himport Halogen.HTML as HHimport Halogen.HTML.Events as HEimport Halogen.HTML.Properties as HPimport Halogen.Query.EventSource (EventSource)import Halogen.Query.EventSource as EventSourceimport Routing.Duplex (parse, print)import Routing.Hash (getHash)import Web.Event.Event (preventDefault)import Web.UIEvent.MouseEvent (MouseEvent, toEvent)type State ={ info :: Maybe Info, config :: Maybe Config, mapImage :: Maybe CanvasImageSource, route :: Maybe Route, infoUpdater :: Maybe SubscriptionId, sidebar :: Boolean}data Action= Initialize| GoTo Route MouseEvent| InfoTick| AutoUpdate Boolean| ToggleSidebardata Query a = Navigate Route atype Slots =( dashboard :: forall query. H.Slot query Void Unit, tracklets :: forall query. H.Slot query Void Unit, statistics :: forall query. H.Slot query Void Unit, filtering :: forall query. H.Slot query Void Unit, zones :: forall query. H.Slot query Void Unit, cameras :: forall query. H.Slot query Void Unit)-- _tracklets = SProxy :: SProxy "tracklets"component :: forall i o m. MonadAff m => H.Component HH.HTML Query i o mcomponent =H.mkComponent{ initialState: \_ ->{ info: Nothing, config: Nothing, mapImage: Nothing, route: Nothing, infoUpdater: Nothing, sidebar: true}, render, eval: H.mkEval $ H.defaultEval{ handleAction = handleAction, handleQuery = handleQuery, initialize = Just Initialize}}pageLink :: forall w i. Route -> String -> Boolean -> HH.HTML w ipageLink route icon active =HH.li [ classes $ ["nav-item"] <> if active then ["active"] else [] ][ HH.a [ HP.href ("/web/#" <> print routeCodec route), classes ["nav-link"] ][ HH.i [ classes ["fas", "fa-fw", icon] ] [], HH.span_ [ HH.text $ show route ]]]sidebar :: forall m. MonadAff m => Maybe Route -> Info -> H.ComponentHTML Action Slots msidebar route info =HH.ul [ HP.id_ "accordionSidebar", classes ["navbar-nav", "bg-gradient-primary", "sidebar", "sidebar-dark", "accordion"] ][ HH.a[ HP.href "/web/#/", classes ["sidebar-brand", "d-flex", "align-items-center", "justify-content-center"] ][ HH.img [ HP.src Assets.logoWhite, classes ["sidebar-brand-icon"] ], HH.div [ classes ["sidebar-brand-text", "mx-3"]][ HH.text "Retail Analytics" ]], HH.hr [ classes ["sidebar-divider", "my-0"] ], Dashboard # \a -> pageLink a "fa-tachometer-alt" (route == Just a), HH.hr [ classes ["sidebar-divider"] ], HH.div [ classes ["sidebar-heading"] ][ HH.text "Analytics" ], Tracking # \a -> pageLink a "fa-map-marker" (route == Just a), Filtering # \a -> pageLink a "fa-filter" (route == Just a), Statistics # \a -> pageLink a "fa-table" (route == Just a), Zones # \a -> pageLink a "fa-shapes" (route == Just a), Cameras # \a -> pageLink a "fa-video" (route == Just a), HH.hr [ classes ["sidebar-divider"] ], HH.div [ classes ["sidebar-heading"] ][ HH.text "External Links" ], HH.li [ classes ["nav-item"] ][ HH.a [ HP.href "/", classes ["nav-link"] ][ HH.i [ classes ["fas", "fa-fw", "fa-wrench"] ] [], HH.span_ [ HH.text "API Documentation" ]]], HH.li [ classes ["nav-item"] ][ HH.a [ HP.href "https://gitlab.certicon.cz/retail/server/issues", classes ["nav-link"] ][ HH.i [ classes ["fas", "fa-fw", "fa-bug"] ] [], HH.span_ [ HH.text "Issue Tracker" ]]], HH.hr [ classes ["sidebar-divider", "d-none", "d-md-block"] ]-- , HH.div [ classes ["text-center", "d-none", "d-md-inline"] ]-- [ HH.button [ HP.id_ "sidebarToggle", classes ["rounded-circle", "border-0"] ]-- [ ]-- ]]navbar :: forall m. MonadAff m => Info -> Boolean -> H.ComponentHTML Action Slots mnavbar info updating =HH.nav [ classes ["navbar", "navbar-expand", "navbar-light", "bg-white", "topbar", "mb-4", "static-top", "shadow"] ][ HH.button [ HE.onClick (const $ Just ToggleSidebar), HP.id_ "sidebarToggleTop", classes ["btn", "btn-link", "d-md-none", "rounded-circle", "mr-3"] ][ HH.i [ classes ["fa", "fa-bars"] ] []], HH.div [ classes ["btn-group", "btn-group-sm", "mx-3", "my-1"] ][ HH.button[ HE.onClick (const $ Just $ AutoUpdate (not updating) ), classes $ ["btn", "btn-secondary"] <> guard updating ["active"]] [ HH.text $ if updating then "Stop Live" else "Go Live" ]], HH.div [ classes ["navbar-nav", "ml-auto"] ][ HH.img [ HP.src certiconLogo, classes ["certicon-logo", "mr-2"] ]]]content :: forall m. MonadAff m => Maybe Route -> Info -> Config -> CanvasImageSource -> H.ComponentHTML Action Slots mcontent route info config mapImage =HH.div [ classes ["container-fluid", "mt-4"] ][ case route ofJust Dashboard ->HH.slot (SProxy :: SProxy "dashboard") unit Dashboard.component {info, config, mapImage} (const Nothing)Just Tracking -> HH.text "Tracklets: not upgraded to the new API yet"Just Filtering ->HH.slot (SProxy :: SProxy "filtering") unit Filtering.component {info, config, mapImage} (const Nothing)Just Statistics ->HH.slot (SProxy :: SProxy "statistics") unit Statistics.component {info, config} (const Nothing)Just Zones ->HH.slot (SProxy :: SProxy "zones") unit Zones.component {config} (const Nothing)Just Cameras ->HH.slot (SProxy :: SProxy "cameras") unit Cameras.component {config} (const Nothing)Nothing -> HH.div_ [ HH.text "Oh no! That page wasn't found." ]]render :: forall m. MonadAff m => State -> H.ComponentHTML Action Slots mrender state = letvalues = doinfo <- note "Waiting for database information..." state.infoconfig <- note "Waiting for config..." state.configmapImage <- note "Waiting for the map..." state.mapImagepure {info, config, mapImage}in case values ofLeft err -> HH.text errRight {info, config, mapImage} ->HH.div [ HP.id_ "wrapper" ] $guard state.sidebar [ sidebar state.route info ] <>[ HH.div [ HP.id_ "content-wrapper", classes ["d-flex", "flex-column"] ][ HH.div [ HP.id_ "content" ][ navbar info (isJust state.infoUpdater), content state.route info config mapImage]]]handleAction :: forall cs o m. MonadAff m => Action → H.HalogenM State Action cs o m UnithandleAction = case _ ofInitialize -> doinitialRoute <- hush <<< ( parse routeCodec ) <$> H.liftEffect getHashnavigate $ fromMaybe Dashboard initialRouteinfo <- liftAff fetchInfoconfig <- liftAff fetchConfigwhen (isLeft info) $ log $ show infowhen (isLeft config) $ log $ show configmapImage <- join <$> for config \(Config c) ->liftAff $ fetchInfoImage c.mapH.modify_ _ { info = hush info, config = hush config, mapImage = hush mapImage }pure unitGoTo route e -> doliftEffect $ preventDefault ( toEvent e )mRoute <- H.gets _.routewhen ( mRoute /= Just route ) $ navigate routeAutoUpdate true -> doid <- H.subscribe (infoTimer 2000.0)H.modify_ _ { infoUpdater = Just id }AutoUpdate false -> dost <- H.getvoid $ sequence $ H.unsubscribe <$> st.infoUpdaterH.modify_ _ { infoUpdater = Nothing }InfoTick -> dost <- H.getres <- liftAff fetchInfocase res ofLeft err -> log $ "Error requesting info: " <> errRight info ->H.modify_ _ { info = Just info }pure unitToggleSidebar -> doH.modify_ \st -> st { sidebar = not st.sidebar }handleQuery :: forall a o m. MonadEffect m => Query a -> H.HalogenM State Action Slots o m ( Maybe a )handleQuery = case _ of-- This is the case that runs every time the brower's hash route changes.Navigate route a -> domRoute <- H.gets _.routewhen ( mRoute /= Just route ) $H.modify_ _ { route = Just route }pure ( Just a )infoTimer :: forall m. MonadAff m => Number -> EventSource m ActioninfoTimer delayMs = EventSource.affEventSource \emitter -> dofiber <- forkAff $ forever dodelay $ Milliseconds delayMsEventSource.emit emitter InfoTickpure $ EventSource.Finalizer dokillFiber (error "Event source finalized") fiberpure unit
module Frontend.Component.Filtering whereimport Preludeimport Data.Array (length)import Data.Array as Aimport Data.Array.NonEmpty (NonEmptyArray, fromNonEmpty, head, toArray)import Data.DateTime (DateTime, adjust, diff)import Data.Either (Either(..), hush)import Data.Foldable (for_)import Data.HeytingAlgebra (tt, ff)import Data.Int (round, toNumber)import Data.Maybe (Maybe(..), fromMaybe)import Data.Newtype (unwrap)import Data.NonEmpty ((:|))import Data.Number (fromString)import Data.Symbol (SProxy(..))import Data.Time.Duration (class Duration, Hours(..), convertDuration, negateDuration)import Effect (Effect)import Effect.Aff.Class (class MonadAff, liftAff)import Effect.Class.Console (log)import Frontend.Api (fetchInstantInterval, fetchTracklets)import Frontend.Component.Plot as Plotimport Frontend.Dom (card)import Frontend.Draw (drawTracklet, imageShape)import Frontend.Tracklet.Filter (HLine, TrackletFilter)import Frontend.Tracklet.Filter as Fimport Frontend.Types (Box, Config(..), Info(..), MaybeInstant, Tracklet, Tracklets(..), Zone, dateTimeToSeconds)import Frontend.UPlot (Opts, defaultOpts, defaultSeries)import Frontend.Util (classes, datetimeToSeconds, secondsToDateTime)import Graphics.Canvas (CanvasImageSource, drawImage, getCanvasElementById, getContext2D, setGlobalAlpha)import Halogen (liftEffect)import Halogen as Himport Halogen.HTML as HHimport Halogen.HTML.Events as HEimport Halogen.HTML.Properties as HPtype State ={ info :: Info, config :: Config, mapImage :: CanvasImageSource, instants :: Maybe (Array MaybeInstant), tracklets :: Maybe (Array Tracklet), filtered :: Maybe (Array Tracklet), filter :: FilterRecord, seekPos :: DateTime, seekLen :: Hours, fetching :: Boolean, trackletAlpha :: Number}type Slots =( plot :: H.Slot Plot.Query Void Unit)data Action= Initialize| SetInput Input| Seek DateTime| SetSeekLen Hours| FetchData| SetTrackletAlpha Number| SetFilter FilterRecordtype Input = { info :: Info, config :: Config, mapImage :: CanvasImageSource }type FilterRecord = { name :: String, filter :: TrackletFilter }line = {x0: 12.0, x1: 14.0, y: 11.0} :: HLinepreparedFilters :: Array Zone -> NonEmptyArray FilterRecordpreparedFilters zones = letoriginateAtZone {name, box} ={ name: "Tracklets which originate at " <> name, filter: F.first (F.insideBox box)}passThroughZone {name, box} ={ name: "Tracklets which pass through " <> name, filter: F.runParser $ F.any (F.insideBox box)}in fromNonEmpty $ {name: "Everything", filter: tt} :|[ {name: "Nothing", filter: ff}]<> (originateAtZone <$> zones)<> (passThroughZone <$> zones)seekLenChoices :: NonEmptyArray HoursseekLenChoices = Hours <$> fromNonEmpty (1.0 :| [6.0, 12.0, 24.0])_plot = (SProxy :: SProxy "plot")component :: forall query output m. MonadAff m => H.Component HH.HTML query Input output mcomponent = H.mkComponent{ initialState: \{info, config, mapImage} ->{ info, config, mapImage, instants: Nothing, tracklets: Nothing, filtered: Nothing, filter: head $ preparedFilters (unwrap config).zones, seekPos: (unwrap info).first_dt, seekLen: head seekLenChoices, fetching: false, trackletAlpha: 0.8}, render, eval: H.mkEval H.defaultEval{ handleAction = handleAction, initialize = Just Initialize, receive = Just <<< SetInput}}filteringOptions :: forall m. MonadAff m => State -> H.ComponentHTML Action Slots mfilteringOptions {info: Info info, config: Config config, seekPos, seekLen, fetching, trackletAlpha, tracklets, filtered, filter} = letmaxSeek = fromMaybe info.last_dt $ adjust (negateDuration seekLen) info.last_dtin HH.div_[ HH.text $ "Total tracklets: " <> fromMaybe "N/A" (show <<< length <$> tracklets), HH.br_, HH.text $ "Filtered tracklets: " <> fromMaybe "N/A" (show <<< length <$> filtered), HH.hr_, HH.div [ classes ["by-2"] ][ HH.text "Tracklet Alpha:", HH.input[ HP.type_ HP.InputRange, classes ["slider", "custom-range"], HP.min 0.0, HP.max 255.0, HE.onValueInput (map (SetTrackletAlpha <<< (_ / 255.0)) <<< fromString), HP.value $ show $ trackletAlpha * 255.0]], HH.div [ classes ["by-2"] ] $[ HH.text "Show:", HH.br_, HH.div [ classes ["container"] ][ HH.div [ classes ["row"] ](toArray (preparedFilters config.zones) <#> \f -> HH.div [ classes ["col-6"] ][ HH.input[ HP.type_ HP.InputRadio, HP.value "Hello!", HP.checked (f.name == filter.name), HE.onClick $ const $ Just $ SetFilter f, classes ["mx-2"] ], HH.text f.name])]]]seekingOptions :: forall m. MonadAff m => State -> H.ComponentHTML Action Slots mseekingOptions {info: Info info, seekPos, seekLen, fetching, trackletAlpha, tracklets, filtered, filter} = letmaxSeek = fromMaybe info.last_dt $ adjust (negateDuration seekLen) info.last_dtin HH.div_[ HH.div [ classes ["my-2"] ] [ HH.text "Seek:" ], HH.div [ classes ["my-2"] ][ HH.input[ HP.type_ HP.InputRange, classes ["slider", "custom-range"], HP.min $ datetimeToSeconds info.first_dt, HP.max $ datetimeToSeconds maxSeek, HE.onValueInput (map Seek <<< secondsToDateTime <=< fromString), HP.value $ show $ dateTimeToSeconds $ seekPos `min` maxSeek]], HH.div [ classes ["my-2"] ] $ toArray seekLenChoices <#> \d ->HH.button[ HE.onClick $ const $ Just (SetSeekLen d), classes ["btn", if seekLen == d then "btn-primary" else "btn-primary-outline"]] [HH.text $ show (round $ unwrap d) <> " h"], HH.div [ classes ["my-2"] ][ HH.button[ HE.onClick $ const $ Just FetchData, classes ["btn", "btn-primary", "btn-outline"]] [HH.text "Fetch Data"], HH.i [classes $ ["m-2"] <> if fetching then ["fas", "fa-fw", "fa-hourglass"] else []] []], HH.slot _plot unit Plot.component seekPlotOpts (const Nothing)]seekPlotOpts :: OptsseekPlotOpts = defaultOpts{ series = [defaultSeries, {stroke: "white", fill: "rgba(0, 0, 0, 0.1)"}, defaultSeries ]}render :: forall m. MonadAff m => State -> H.ComponentHTML Action Slots mrender state = let{width, height} = imageShape state.mapImageinHH.div [ classes ["row"] ][ HH.div [ classes ["col-lg-7"] ][ card [][ HH.text "Filtering" ][ filteringOptions state], card [][ HH.text "Seeking" ][ seekingOptions state]], card ["col-lg-5"][ HH.text "Tracklets" ][ HH.canvas [ HP.id_ "heatmap-canvas", HP.width width, HP.height height ]]]drawTracklets :: CanvasImageSource -> Box -> Array Tracklet -> Number -> Effect UnitdrawTracklets mapImage box tracklets alpha = domCanvas <- liftEffect $ getCanvasElementById "heatmap-canvas"liftEffect case mCanvas ofNothing -> log "Can't locate canvas."Just canvas -> doctx <- getContext2D canvassetGlobalAlpha ctx 1.0drawImage ctx mapImage 0.0 0.0log $ "Drawing " <> show (length tracklets) <> " tracklets..."for_ tracklets (drawTracklet canvas box alpha)drawInstants :: forall s a output m d. Duration d =>Array MaybeInstant -> DateTime -> d -> H.HalogenM s a Slots output m UnitdrawInstants instants seek window = doletinWindow t = let d = t `diff` seek inHours 0.0 <= d && d < convertDuration windowx = instants <#> (unwrap >>> _.timestamp >>> datetimeToSeconds >>> Just)y = instants <#> (unwrap >>> _.objects >>> map (A.length >>> toNumber))w = instants <#> \i ->if inWindow (unwrap i).timestamp then Just 20.0 else Nothingvoid $ H.query _plot unit $ H.tell (\a -> Plot.SetData [x, w, y] a)handleAction :: forall output m. MonadAff m => Action -> H.HalogenM State Action Slots output m UnithandleAction = case _ ofInitialize -> do{info: Info info, config: Config config, seekPos, seekLen} <- H.get-- Number of tracklets over timeinstantRange <- liftAff $ fetchInstantInterval info.first_dt info.last_dt (Hours 0.2)for_ instantRange \instants -> do -- EitherH.modify_ $ _ { instants = Just instants }drawInstants instants seekPos seekLen-- Tracklet heatmaphandleAction FetchDatapure unitSetInput { info, config } -> H.modify_ _ { info = info, config = config }Seek time -> do{instants, seekPos, seekLen} <- H.getH.modify_ _ { seekPos = time }for_ instants \i -> dodrawInstants i time seekLenpure unitSetSeekLen hours -> do{info: Info info, instants, seekPos} <- H.getlet maxPos = fromMaybe info.last_dt $ adjust (negateDuration hours) info.last_dtnewPos = seekPos `min` maxPosH.modify_ _ { seekLen = hours, seekPos = newPos }for_ instants \i -> dodrawInstants i newPos hoursFetchData -> do{info: Info info, config: Config config, mapImage, seekPos, seekLen, trackletAlpha, filter} <- H.getH.modify_ _ { fetching = true }lett1 = seekPost2 = fromMaybe seekPos $ adjust seekLen seekPostracklets' <- liftAff $ fetchTracklets t1 t2 10case tracklets' ofLeft err -> log $ "Tracklets weren't fetched:" <> show errRight (Tracklets tracklets) -> dolet filtered = A.filter filter.filter trackletsliftEffect $ drawTracklets mapImage config.bbox filtered trackletAlphaH.modify_ _ { filtered = Just filtered }H.modify_ _ { fetching = false, tracklets = unwrap <$> hush tracklets' }SetTrackletAlpha a -> do{ config: Config config, mapImage, filtered } <- H.getH.modify_ _ { trackletAlpha = a }liftEffect $ drawTracklets mapImage config.bbox (fromMaybe [] filtered) aSetFilter f -> do{ config: Config config, mapImage, tracklets, trackletAlpha } <- H.getlet filtered = A.filter f.filter $ fromMaybe [] trackletslog $ "Setting filter to " <> f.nameH.modify_ _ { filter = f, filtered = Just filtered }liftEffect $ drawTracklets mapImage config.bbox filtered trackletAlpha
-- | Take incoming Aff requests, and perform them sequentially.-- | In case a request is received while another is being performed,-- | queue the new request instead. The queued request will be performed once the-- | one before finishes. The request queue contains at most a single request;-- | in case multiple ones are added, only the newest one stays.-- |-- | This ensures optimal semantics in the following sense: At every point in time,-- | the freshest possible result is returned to the caller.module Frontend.Component.Debounce whereimport Preludeimport Data.Maybe (Maybe(..))import Data.Newtype (class Newtype)import Effect.Aff (Aff)import Effect.Aff.Class (class MonadAff, liftAff)import Halogen as Himport Halogen.HTML as HHnewtype Output t = Output tderive instance newtypeOutput :: Newtype (Output t) _data Query t a = Query (Aff t) atype State t = { seeking :: Boolean, seekQueue :: Maybe (Aff t) }debounce :: forall input t m. MonadAff m => H.Component HH.HTML (Query t) input (Output t) mdebounce =H.mkComponent{ initialState: \_ -> { seeking: false, seekQueue: Nothing }, render: const $ HH.text "", eval: H.mkEval $ H.defaultEval { handleQuery = handleQuery }}wherehandleQuery :: forall action a. Query t a -> H.HalogenM (State t) action () (Output t) m (Maybe a)handleQuery = case _ ofQuery computation a -> doH.modify_ _ { seekQueue = Just computation }seeking <- H.gets _.seekingcase seeking oftrue -> pure (Just a)false -> doH.modify_ _ { seeking = true, seekQueue=Nothing }res <- liftAff computationH.modify_ _ { seeking = false }H.raise $ Output resq <- H.gets _.seekQueuecase q ofNothing -> pure (Just a)Just v -> handleQuery (Query v a)
module Frontend.Component.Dashboard whereimport Preludeimport CSS (color)import Color (toHexString)import Data.Array (length)import Data.DateTime (diff)import Data.Foldable (for_)import Data.Formatter.Number (formatOrShowNumber)import Data.Int (floor, toNumber)import Data.Maybe (Maybe(..))import Data.Monoid (guard)import Data.Newtype (unwrap)import Data.Symbol (SProxy(..))import Data.Time.Duration (class Duration, Days(..), Hours(..), Minutes(..), Seconds(..), convertDuration)import Data.Tuple (Tuple(..))import Effect.Aff.Class (class MonadAff, liftAff)import Effect.Class.Console (log)import Frontend.Api (fetchInstantInterval)import Frontend.Component.Plot as Plotimport Frontend.Dom (card)import Frontend.Draw (canvasShape, colorToCss, hashedColor, imageShape, metersToPixels)import Frontend.Route (Route(..), routeCodec)import Frontend.Types (Box(..), Config(..), Info(..), Vec2(..))import Frontend.UPlot (defaultOpts, defaultSeries, defaultXAxis, noAxis)import Frontend.Util (classes, datetimeToSeconds, enumerate, formatDateTime')import Graphics.Canvas (CanvasImageSource)import Graphics.Canvas as Canvasimport Halogen (liftEffect)import Halogen as Himport Halogen.HTML as HHimport Halogen.HTML.CSS as CSSimport Halogen.HTML.Properties as HPimport Routing.Duplex (print)type State ={ info :: Info, config :: Config, mapImage :: CanvasImageSource}type Slots =( plot :: H.Slot Plot.Query Void Unit)data Action= Initialize| SetInput Inputtype Input = { info :: Info, config :: Config, mapImage :: CanvasImageSource }_plot = (SProxy :: SProxy "plot")component :: forall query output m. MonadAff m => H.Component HH.HTML query Input output mcomponent = H.mkComponent{ initialState: \{info, config, mapImage} ->{ info, config, mapImage}, render, eval: H.mkEval H.defaultEval{ handleAction = handleAction, initialize = Just Initialize, receive = Just <<< SetInput}}tableRow :: forall a s m. String -> String -> H.ComponentHTML a s mtableRow label value = HH.tr_[ HH.td_ [ HH.text label ], HH.td_ [ HH.text value ]]formatDuration :: forall d. Duration d => d -> StringformatDuration duration = letDays d = convertDuration durationHours h = convertDuration durationds = show $ floor dhs = show $ floor h - (floor d * 24)in guard (floor d > 0) (ds <> " days ") <> hs <> " hours"basicInformation :: forall a s m. MonadAff m => String -> Info -> Config -> H.ComponentHTML a s mbasicInformation cls (Info info) (Config config) =card [cls][ HH.text "Basic Information" ][ HH.table [ classes ["info-table"] ][ tableRow "Recording Start" $formatDateTime' info.first_dt, tableRow "Recording End" $formatDateTime' info.last_dt, tableRow "Time Span" $formatDuration (info.last_dt `diff` info.first_dt :: Days), tableRow "Number of Data Points" $formatOrShowNumber "0,0" (toNumber info.n_entries), tableRow "Recording Length" $formatDuration (Seconds $ toNumber info.n_entries * config.predict_stride)]]sceneConfiguration :: forall a s m. MonadAff m => String -> Config -> H.ComponentHTML a s msceneConfiguration cls (Config config) =card [cls][ HH.text "Scene Configuration" ][ HH.table [ classes ["info-table"] ][ tableRow "Number of Cameras" $show (length config.cameras), tableRow "Frames per Second" $formatOrShowNumber "0.0" (1.0 / config.predict_stride), tableRow "Maximum Number of Objects" $show config.n_objects, tableRow "Minimum Tracklet Length" $show config.min_tracklet_len <> " frames", tableRow "Warmup Period" $show config.toss_first <> " frames", tableRow "Quantization" $show config.grid_px_size <> " m", tableRow "Bounding Box" $show config.bbox, tableRow "GPU ID" $show config.gpu], HH.div [ classes [] ][ HH.text "Zones:", HH.br_, HH.ul [] $ enumerate config.zones <#> \(Tuple i zone) ->HH.li [CSS.style (color $ colorToCss (hashedColor i))] [HH.text zone.name]], HH.div [ classes ["mt-4"] ][ HH.i_ [ HH.a [ HP.href ("/web/#" <> print routeCodec Cameras) ] [ HH.text "show cameras" ] ], HH.br_, HH.i_[ HH.text "show raw config:", HH.a [ classes ["mx-1"], HP.href "/info/tracker.yaml" ] [ HH.text "yaml" ], HH.text "|", HH.a [ classes ["mx-1"], HP.href "/config" ] [ HH.text "json" ]]]]sceneMap :: forall a s m. MonadAff m => CanvasImageSource -> H.ComponentHTML a s msceneMap mapImage = let{width, height} = imageShape mapImagein card [][ HH.text "Scene Map" ][ HH.canvas [ HP.id_ "map-canvas", HP.width width, HP.height height ]]render :: forall a m. MonadAff m => State -> H.ComponentHTML a Slots mrender state = letplotOpts = defaultOpts{ height = 100.0, axes = [defaultXAxis, noAxis], series = [defaultSeries, defaultSeries { fill="#4e73df" }]}inHH.div [ classes ["row"] ][ HH.div [ classes ["col-lg-8"]][ card [][ HH.text "Recording History" ][ HH.slot _plot unit Plot.component plotOpts (const Nothing)], HH.div [ classes ["row"] ][ basicInformation "col-xl-6" state.info state.config, sceneConfiguration "col-xl-6" state.config]], HH.div [ classes ["col-lg-4"] ][ sceneMap state.mapImage]]handleAction :: forall output m. MonadAff m => Action -> H.HalogenM State Action Slots output m UnithandleAction = case _ ofInitialize -> do{info: Info info, config: Config config, mapImage} <- H.get-- Draw recording timelineinstantRange <- liftAff $ fetchInstantInterval info.first_dt info.last_dt (Minutes 10.0)for_ instantRange \instants -> dolet x = instants <#> (unwrap >>> _.timestamp >>> datetimeToSeconds >>> Just)y = instants <#> (unwrap >>> _.objects >>> map (const 1.0))void $ H.query _plot unit $ H.tell (\a -> Plot.SetData [x, y] a)-- draw the mapmCanvas <- liftEffect $ Canvas.getCanvasElementById "map-canvas"liftEffect case mCanvas ofNothing -> log "Can't locate canvas."Just canvas -> doctx <- Canvas.getContext2D canvasCanvas.setGlobalAlpha ctx 1.0Canvas.drawImage ctx mapImage 0.0 0.0Canvas.setGlobalAlpha ctx 0.1cshape <- canvasShape canvasfor_ (enumerate config.zones) \(Tuple i zone) -> dolet Box {x0, x1, y0, y1} = zone.boxV2 x y = metersToPixels cshape config.bbox (V2 x0 y0)V2 xx yy = metersToPixels cshape config.bbox (V2 x1 y1)Canvas.setFillStyle ctx (toHexString $ hashedColor i)Canvas.beginPath ctxCanvas.rect ctx {x, y, width: xx - x, height: yy - y}Canvas.fill ctxSetInput { info, config } -> doH.modify_ _ { info = info, config = config }handleAction Initialize
module Frontend.Component.Cameras whereimport Preludeimport Data.Maybe (Maybe(..))import Effect.Aff.Class (class MonadAff)import Frontend.Dom (card)import Frontend.Types (Config(..))import Frontend.Util (classes)import Halogen as Himport Halogen.HTML as HHimport Halogen.HTML.Properties as HPtype State ={ config :: Config}data Action= Initializetype Input = { config :: Config }component :: forall query output m. MonadAff m => H.Component HH.HTML query Input output mcomponent = H.mkComponent{ initialState: identity, render, eval: H.mkEval H.defaultEval{ initialize = Just Initialize}}render :: forall s m. MonadAff m => State -> H.ComponentHTML Action s mrender { config: Config config } =HH.div [ classes ["row"] ] $config.cameras <#> \info ->card ["col-lg-6"][ HH.text "Camera View" ][ HH.b_ [ HH.text $ "Name: " <> info.name ], HH.br_, HH.text $ "Resolution: " <> show info.width <> "×" <> show info.height, HH.br_, HH.a [ HP.href $ "/info/" <> info.image ][ HH.img [ classes ["camera-preview"], HP.src $ "/info/" <> info.image ]]]
module Frontend.Cache whereimport Preludeimport Data.Foldable (class Foldable)import Data.Map as Mimport Data.Maybe (Maybe)import Data.Tuple (Tuple(..), snd)-- TODO: limit capacitydata Cache a = Cache{ cache :: M.Map Int (Tuple Int a), i :: Int}empty :: forall a. Cache aempty = Cache { cache: M.empty, i: 0 }insert :: forall a. Int -> a -> Cache a -> Cache ainsert k v (Cache c) = Cache{ cache: M.insert k (Tuple c.i v) c.cache, i: c.i + 1}insertFoldable :: forall a f. Foldable f => Functor f => f (Tuple Int a) -> Cache a -> Cache ainsertFoldable f (Cache c) = Cache{ cache: M.fromFoldable (map (\(Tuple k v) -> Tuple k (Tuple c.i v)) f) `M.union` c.cache, i: c.i + 1}size :: forall a. Cache a -> Intsize (Cache c) = M.size c.cachelookup :: forall v. Int -> Cache v -> Maybe vlookup v (Cache c) = snd <$> M.lookup v c.cache
"use strict";exports.logoWhite = require("../../static/logo-white.svg");exports.certiconLogo = require("../../static/certicon-logo.svg");
module Frontend.Assets whereforeign import logoWhite :: Stringforeign import certiconLogo :: String
module Frontend.Api whereimport Preludeimport Affjax as AXimport Affjax.ResponseFormat as ResponseFormatimport Data.Argonaut.Decode (class DecodeJson, decodeJson, (.:))import Data.Bifunctor (lmap)import Data.DateTime (DateTime)import Data.Either (Either(..))import Data.Newtype (unwrap)import Data.Time.Duration (class Duration, Seconds, convertDuration)import Effect.Aff (Aff)import Effect.Class (liftEffect)import Frontend.Tracklets (TrackletSOA)import Frontend.Types (Config, Info, Instant, MaybeInstant, Tracklets)import Frontend.Util (datetimeToSeconds, loadImage)import Graphics.Canvas (CanvasImageSource)import Web.File.Url (createObjectURL)fetchInfo :: Aff (Either String Info)fetchInfo = getJson "/info"fetchConfig :: Aff (Either String Config)fetchConfig = getJson "/config"fetchInfoImage :: String -> Aff (Either String CanvasImageSource)fetchInfoImage url = doresult <- lmap AX.printError <$> AX.get ResponseFormat.blob ("/info/" <> url)case result ofLeft err -> pure $ Left errRight response -> doblobUrl <- liftEffect $ createObjectURL response.bodyimage <- loadImage blobUrlpure (Right image)fetchTracklets :: DateTime -> DateTime -> Int -> Aff (Either String Tracklets)fetchTracklets from to stride = letfrom' = show $ datetimeToSeconds fromto' = show $ datetimeToSeconds tostride' = show stridein getJson $ "/tracklet_pos?from=" <> from' <> "&to=" <> to' <> "&stride=" <> stride'fetchTrackletsSOA :: DateTime -> DateTime -> Int -> Aff (Either String (Array TrackletSOA))fetchTrackletsSOA from to stride = letfrom' = show $ datetimeToSeconds fromto' = show $ datetimeToSeconds tostride' = show stridein getJson $ "/tracklet_pos?from=" <> from' <> "&to=" <> to' <> "&stride=" <> stride'fetchInstantInterval :: forall d. Duration d => DateTime -> DateTime -> d -> Aff (Either String (Array MaybeInstant))fetchInstantInterval from to stride = letfrom' = show $ datetimeToSeconds fromto' = show $ datetimeToSeconds tostride' = show $ unwrap (convertDuration stride :: Seconds)in getJson $ "/time_range_t?from=" <> from' <> "&to=" <> to' <> "&stride=" <> stride'fetchInstantRange :: DateTime -> DateTime -> Number -> Aff (Either String (Array Instant))fetchInstantRange from to stride = letfrom' = show $ datetimeToSeconds fromto' = show $ datetimeToSeconds tostride' = show stridein getJson $ "/time_range_n?from=" <> from' <> "&to=" <> to' <> "&stride=" <> stride'getJson :: forall a. DecodeJson a => String -> Aff (Either String a)getJson url = doresult <- lmap AX.printError <$> AX.get ResponseFormat.json urlpure $ doresponse <- resulto <- lmap show $ decodeJson response.bodyok <- lmap show $ o .: "Ok"lmap show $ decodeJson ok