{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TupleSections #-} module Server.GTFS_RT (gtfsRealtimeServer) where import Prelude hiding (id) import API (GtfsRealtimeAPI) import Control.Monad (forM) import Control.Monad.IO.Class (MonadIO (..)) import Data.ByteString.Lazy (fromStrict) import Data.Functor ((<&>)) import Data.Maybe (catMaybes) import Data.Pool (Pool) import Data.Sequence (Seq) import qualified Data.Sequence as Seq import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Data.Time.Calendar (Day, toGregorian) import Data.Time.Clock (UTCTime) import Data.Time.Clock.POSIX (getPOSIXTime) import Data.Time.Clock.System (SystemTime (systemSeconds), getSystemTime, utcToSystemTime) import qualified Data.UUID as UUID import Data.Word (Word64) import Database.Persist (Entity (Entity), PersistQueryRead (selectFirst), selectList, (==.)) import Database.Persist.Postgresql (SqlBackend) import GHC.Float (double2Float) import GTFS (GTFS, TripID) import GTFS.Realtime.Alert as AL (Alert (..)) import GTFS.Realtime.Alert.SeverityLevel (SeverityLevel (WARNING)) import GTFS.Realtime.EntitySelector as ES (EntitySelector (..)) import GTFS.Realtime.FeedEntity as FE (FeedEntity (..)) import GTFS.Realtime.FeedHeader (FeedHeader (FeedHeader)) import GTFS.Realtime.FeedHeader.Incrementality (Incrementality (FULL_DATASET)) import GTFS.Realtime.FeedMessage as FM (FeedMessage (..)) import GTFS.Realtime.Position as POS (Position (..)) import GTFS.Realtime.TimeRange (TimeRange (TimeRange)) import GTFS.Realtime.TranslatedString (TranslatedString (TranslatedString)) import GTFS.Realtime.TranslatedString.Translation (Translation (Translation)) import GTFS.Realtime.TripDescriptor as TD (TripDescriptor (..)) import GTFS.Realtime.VehicleDescriptor as VD (VehicleDescriptor (..)) import GTFS.Realtime.VehiclePosition as VP (VehiclePosition (..)) import Persist (Announcement (..), EntityField (..), Key (..), Running (..), TrainPing (..), runSql) import Servant.API ((:<|>) (..)) import Text.ProtocolBuffers (Utf8 (Utf8), defaultValue) import Server.Util (Service) uuidUtf8 :: UUID.UUID -> Utf8 uuidUtf8 = Utf8 . fromStrict . UUID.toASCIIBytes toUtf8 :: Text -> Utf8 toUtf8 = Utf8 . fromStrict . encodeUtf8 -- | formats a day in the "stupid" format used by gtfs realtime toStupidDate :: Day -> Utf8 toStupidDate date = toUtf8 $ pad 4 year <> pad 2 month <> pad 2 day where (year, month, day) = toGregorian date pad len num = T.pack $ if ndigits < len then replicate (len - ndigits) '0' <> show num else show num where ndigits = length (show num) -- | basically unix timestamps, raw (because why not i guess) toStupidTime :: UTCTime -> Word64 toStupidTime = fromIntegral . systemSeconds . utcToSystemTime gtfsRealtimeServer :: GTFS -> Pool SqlBackend -> Service GtfsRealtimeAPI gtfsRealtimeServer gtfs dbpool = handleServiceAlerts :<|> handleTripUpdates :<|> handleVehiclePositions where handleServiceAlerts = runSql dbpool $ do -- TODO filter: only select current & future days announcements <- selectList [] [] dFeedMessage $ Seq.fromList $ fmap mkAlert announcements where mkAlert (Entity (AnnouncementKey uuid) Announcement{..}) = (dFeedEntity (uuidUtf8 uuid)) { alert = Just $ Alert { active_period = [TimeRange Nothing Nothing defaultValue] -- TODO: is this time range reasonable, needed, etc.? , informed_entity = [dEntitySelector { ES.trip = Just (dTripDescriptor announcementTrip (Just announcementDay)) } ] , cause = Nothing , effect = Nothing , url = fmap (lang "de" . toUtf8) announcementUrl , header_text = Just $ lang "de" (toUtf8 announcementHeader) , description_text = Just $ lang "de" (toUtf8 announcementMessage) , tts_header_text = Nothing , tts_description_text = Nothing , severity_level = Nothing , image = Nothing , image_alternative_text = Nothing , AL.ext'field = defaultValue } } handleTripUpdates = runSql dbpool $ do error "unimplemented!" -- TODO: how to propagate delay values to next stops? pure undefined handleVehiclePositions = runSql dbpool $ do (running :: [Entity Running]) <- selectList [] [] pings <- forM running $ \(Entity key entity) -> do selectFirst [TrainPingToken ==. key] [] <&> fmap (, entity) dFeedMessage $ Seq.fromList $ mkPosition <$> catMaybes pings where mkPosition (Entity (TrainPingKey key) TrainPing{..}, Running{..}) = (dFeedEntity (toUtf8 . T.pack . show $ key)) { FE.vehicle = Just $ VehiclePosition { trip = Just (dTripDescriptor runningTrip Nothing) , VP.vehicle = case runningVehicle of Nothing -> Nothing Just trainset -> Just $ VehicleDescriptor { VD.id = Nothing , VD.label = Just (toUtf8 trainset) , VD.license_plate = Nothing , VD.ext'field = defaultValue } , position = Just $ Position { latitude = double2Float trainPingLat , longitude = double2Float trainPingLong , odometer = Nothing , speed = Nothing , bearing = Nothing , POS.ext'field = defaultValue } -- TODO: at least one of these should probably be given , current_stop_sequence = Nothing , stop_id = Nothing , current_status = Nothing , timestamp = Just (toStupidTime trainPingTimestamp) , congestion_level = Nothing , occupancy_status = Nothing , occupancy_percentage = Nothing , multi_carriage_details = [] , VP.ext'field = defaultValue } } lang :: Utf8 -> Utf8 -> TranslatedString lang code msg = TranslatedString [Translation msg (Just code) defaultValue] defaultValue -- | a default FeedMessage, issued at the current system time -- TODO: do we ever need incremental updates? -- TODO: maybe instead use last update time? dFeedMessage :: MonadIO m => Seq FeedEntity -> m FeedMessage dFeedMessage entities = do now <- liftIO getSystemTime <&> systemSeconds pure $ FeedMessage { header = FeedHeader "2.0" (Just FULL_DATASET) (Just $ fromIntegral now) defaultValue , entity = entities , FM.ext'field = defaultValue } -- | a dummy FeedEntity (use record updates to add meaningful values to this) dFeedEntity :: Utf8 -> FeedEntity dFeedEntity id = FeedEntity { id , is_deleted = Nothing , trip_update = Nothing , vehicle = Nothing , alert = Nothing , shape = Nothing , FE.ext'field = defaultValue } dEntitySelector :: EntitySelector dEntitySelector = EntitySelector { agency_id = Nothing , route_id = Nothing , route_type = Nothing , trip = Nothing , stop_id = Nothing , direction_id = Nothing , ES.ext'field = defaultValue } dTripDescriptor :: TripID -> Maybe Day -> TripDescriptor dTripDescriptor tripID day = TripDescriptor { trip_id = Just (toUtf8 tripID) , route_id = Nothing , direction_id = Nothing , start_time = Nothing , start_date = fmap toStupidDate day , schedule_relationship = Nothing , TD.ext'field = defaultValue }