aboutsummaryrefslogtreecommitdiff
path: root/lib/Server/GTFS_RT.hs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Server/GTFS_RT.hs')
-rw-r--r--lib/Server/GTFS_RT.hs209
1 files changed, 209 insertions, 0 deletions
diff --git a/lib/Server/GTFS_RT.hs b/lib/Server/GTFS_RT.hs
new file mode 100644
index 0000000..e2b23e0
--- /dev/null
+++ b/lib/Server/GTFS_RT.hs
@@ -0,0 +1,209 @@
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedLists #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TupleSections #-}
+{-# LANGUAGE TypeApplications #-}
+
+module Server.GTFS_RT (gtfsRealtimeServer) where
+
+import qualified Data.Sequence as Seq
+import Data.Time.Clock.POSIX (getPOSIXTime)
+import Data.Time.Clock.System (SystemTime (systemSeconds),
+ getSystemTime,
+ utcToSystemTime)
+import GTFS.Realtime.Alert as AL (Alert (..))
+import GTFS.Realtime.Alert.Cause (Cause (CONSTRUCTION))
+import GTFS.Realtime.Alert.Effect (Effect (DETOUR))
+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.TimeRange (TimeRange (TimeRange))
+import GTFS.Realtime.TranslatedString (TranslatedString (TranslatedString))
+import GTFS.Realtime.TranslatedString.Translation (Translation (Translation))
+import GTFS.Realtime.TripDescriptor as TD (TripDescriptor (..))
+import Prelude hiding (id)
+import Text.ProtocolBuffers (Utf8 (Utf8),
+ defaultValue)
+import Text.ProtocolBuffers.WireMessage (zzEncode64)
+
+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 Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.Encoding (encodeUtf8)
+import Data.Time (Day)
+import Data.Time.Calendar (Day, toGregorian)
+import Data.Time.Clock (UTCTime)
+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.Position as POS (Position (..))
+import GTFS.Realtime.VehicleDescriptor as VD (VehicleDescriptor (..))
+import GTFS.Realtime.VehiclePosition as VP (VehiclePosition (..))
+import Persist (Announcement (..),
+ EntityField (TripPingToken),
+ Key (..),
+ RunningTrip (..),
+ TripPing (..),
+ runSql)
+import Servant.API ((:<|>) (..))
+import Servant.Server (Handler (Handler),
+ Server)
+
+
+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 -> Server 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 RunningTrip]) <- selectList [] []
+ pings <- forM running $ \(Entity key entity) -> do
+ selectFirst [TripPingToken ==. key] [] <&> fmap (, entity)
+ dFeedMessage $ Seq.fromList $ fmap mkPosition $ catMaybes pings
+ where mkPosition (Entity (TripPingKey key) TripPing{..}, RunningTrip{..}) =
+ (dFeedEntity (toUtf8 . T.pack . show $ key))
+ { FE.vehicle = Just $ VehiclePosition
+ { trip = Just (dTripDescriptor runningTripTripNumber Nothing)
+ , VP.vehicle = case runningTripTrainset 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 tripPingLat
+ , longitude = double2Float tripPingLong
+ , 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 tripPingTimestamp)
+ , 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
+ }