{-# LANGUAGE DataKinds #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} module Point ( Pos (..), Point (..), save, fetch, migration, ) where import Control.Exception import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.Trans.Reader (ReaderT) import Data.Aeson import Data.ByteString.Lazy.Char8 qualified as L import Data.Either import Data.Functor ((<&>)) import Data.List as List import Data.List qualified import Data.Map as Map import Data.Maybe (isJust) import Data.Text qualified import Data.Text.Lazy as T import Data.Time import Data.Time.Clock (DiffTime, NominalDiffTime, UTCTime, addUTCTime) import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds) import Data.Time.ISO8601 qualified import Database.Persist import Database.Persist.Class import Database.Persist.Postgresql ( ConnectionString, SqlBackend, createPostgresqlPool, pgConnStr, pgPoolSize, rawExecute, runMigration, runSqlPool, ) import Database.Persist.TH import Debug.Trace (trace, traceShow) import Session qualified import Text.Read (readMaybe) import Text.XML import Text.XML.Cursor as Cursor -- import Track (Point (..), Pos (..)) -- import Track as T data Pos = Pos Double Double (Maybe Double) deriving (Show, Eq) type Power = Maybe Int type Cadence = Maybe Int type HeartRate = Maybe Int data Point = Point { pos :: Pos, time :: UTCTime, cadence :: Cadence, power :: Power, heartRate :: HeartRate } deriving (Show) instance ToJSON Pos where toJSON (Pos lat lon ele) = case ele of Just e -> object ["lat" .= lat, "lon" .= lon, "ele" .= e] Nothing -> object ["lat" .= lat, "lon" .= lon, "ele" .= Null] instance ToJSON Point where toJSON Point {..} = object [ "pos" .= pos, "time" .= utcTimeToPOSIXSeconds time, "cadence" .= cadence, "power" .= power, "heartRate" .= heartRate ] share [mkPersist sqlSettings, mkMigrate "migration"] [persistLowerCase| Trkpt lat Double lon Double ele Double Maybe time UTCTime cadence Int Maybe power Int Maybe heartRate Int Maybe |] fromPoint :: Point -> Trkpt fromPoint p = let Pos lat lon ele = pos p in Trkpt lat lon ele (time p) (cadence p) (power p) (heartRate p) toPoint :: Entity Trkpt -> Point toPoint entity = let tkp = (\(Entity _ tkp) -> tkp) entity pos = Pos (trkptLat tkp) (trkptLon tkp) (trkptEle tkp) in Point pos (trkptTime tkp) (trkptCadence tkp) (trkptPower tkp) (trkptHeartRate tkp) data OverlapExists = OverlapExists String deriving (Show) instance Exception OverlapExists fetch :: (MonadIO m) => UTCTime -> NominalDiffTime -> ReaderT SqlBackend m [Point] fetch start duration = do let finish = addUTCTime duration start trkpts <- selectList [TrkptTime >. start, TrkptTime <. finish] [] return $ List.map toPoint trkpts -- any :: (MonadIO m) => UTCTime -> NominalDiffTime -> ReaderT SqlBackend m Bool any start duration = do let finish = addUTCTime duration start exists <- selectFirst [TrkptTime >. start, TrkptTime <. finish] [] return $ isJust exists startTime :: [Point] -> UTCTime startTime (p : ps) = List.foldr (\a b -> min b (time a)) (time p) ps duration :: [Point] -> NominalDiffTime duration track = case track of [] -> 0 (p : ps) -> let start = startTime track finish = List.foldr (\a b -> max b (time a)) (time p) ps in diffUTCTime finish start save :: (MonadIO m) => [Point] -> ReaderT SqlBackend m (Either OverlapExists [Point]) save track = do let start = startTime track priors <- Point.any start (duration track) if priors then return $ Left (OverlapExists "track overlaps with existing data") else do mapM_ (Database.Persist.Class.insert . fromPoint) track Session.refreshDrafts return $ Right track