123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648 |
- // SPDX-FileCopyrightText: Adam Evyčędo
- //
- // SPDX-License-Identifier: AGPL-3.0-or-later
- package traffic
- import (
- "errors"
- "fmt"
- "log"
- "os"
- "path/filepath"
- "sort"
- "strings"
- "time"
- "apiote.xyz/p/gott/v2"
- traffic_errors "apiote.xyz/p/szczanieckiej/traffic/errors"
- "git.sr.ht/~sircmpwn/go-bare"
- "golang.org/x/text/language"
- )
- type DepartureRealtimeNew struct {
- Departure
- Order StopOrder
- Update Update
- Time time.Time
- Alerts []SpecificAlert
- startTime *uint
- }
- func (d DepartureRealtimeNew) IsEmpty() bool {
- return d.Time.IsZero()
- }
- func (d DepartureRealtimeNew) WithUpdate(update Update) DepartureRealtimeNew {
- d.Update = update
- return d
- }
- func (d DepartureRealtimeNew) WithAlerts(alerts []Alert, languages []language.Tag) DepartureRealtimeNew {
- d.Alerts = selectSpecificAlerts(alerts, languages)
- return d
- }
- func (d DepartureRealtimeNew) GetTimeWithDelay() time.Time {
- if d.Update.TimeUTC != "" {
- updateTimeUTC, err := time.Parse("150405", d.Update.Time)
- if err != nil {
- panic("departure update time ‘" + d.Update.Time + "’ not in format 150405")
- }
- updateTime := time.Date(d.Time.Year(), d.Time.Month(), d.Time.Day(), updateTimeUTC.Hour(), updateTimeUTC.Minute(), updateTimeUTC.Second(), 0, time.UTC)
- return updateTime.In(d.Time.Location())
- } else if d.Update.Time != "" {
- updateTime, err := time.Parse("150405", d.Update.Time)
- if err != nil {
- panic("departure update time ‘" + d.Update.Time + "’ not in format 150405")
- }
- updateDateTime := time.Date(d.Time.Year(), d.Time.Month(), d.Time.Day(), updateTime.Hour(), updateTime.Minute(), updateTime.Second(), 0, d.Time.Location())
- return updateDateTime
- } else {
- delay := int(d.Update.Delay)
- return d.Time.Add(time.Duration(delay) * time.Second)
- }
- }
- type DeparturesResult struct {
- traffic *Traffic
- context Context
- date time.Time
- timetableHome string
- calendar []Schedule
- stopOffset uint
- languages []language.Tag
- departuresType DeparturesType
- lineID string
- timezone *time.Location
- datetime time.Time
- minuteB4Datetime time.Time
- scheduleIDs map[string]map[string]struct{}
- stopsFile *os.File
- stop Stop
- tripsFile *os.File
- trips map[string][]Trip
- feedInfo FeedInfo
- enrichMethod func(string, int, string, string, Context) (map[string][]Update, map[string][]Alert, bool, error)
- departures []DepartureRealtimeNew
- }
- func (r DeparturesResult) getTraffic() *Traffic {
- return r.traffic
- }
- func (r DeparturesResult) getContext() Context {
- return r.context
- }
- func (r *DeparturesResult) setTimezone(l *time.Location) {
- r.timezone = l
- }
- func (r *DeparturesResult) setStopsFile(f *os.File) {
- r.stopsFile = f
- }
- func (r DeparturesResult) getTimetableHome() string {
- return r.timetableHome
- }
- func (r DeparturesResult) getStopsFile() *os.File {
- return r.stopsFile
- }
- func (r DeparturesResult) getStopOffset() uint {
- return r.stopOffset
- }
- func (r *DeparturesResult) setStop(s Stop) {
- r.stop = s
- }
- func (r *DeparturesResult) setTripsFile(f *os.File) {
- r.tripsFile = f
- }
- func (r DeparturesResult) getTripsFile() *os.File {
- return r.tripsFile
- }
- func loadTime(r TrafficResult) TrafficResult {
- result := r.(*DeparturesResult)
- now := time.Now()
- datetime := time.Date(result.date.Year(), result.date.Month(),
- result.date.Day(), now.Hour(), now.Minute(), now.Second(), 0, now.Location()).In(result.timezone)
- result.datetime = datetime
- result.minuteB4Datetime = datetime.Add(time.Duration(-1) * time.Minute)
- return result
- }
- func loadSchedules(r TrafficResult) (TrafficResult, error) {
- result := r.(*DeparturesResult)
- schedules := map[string]map[string]struct{}{}
- for _, offset := range []int{0, -1} {
- date := result.date.AddDate(0, 0, offset)
- scheduleIDs, err := findSchedule(result.timetableHome, date, result.calendar)
- if err != nil {
- log.Printf("no schedule for %s: %v\n", date, err)
- } else {
- schedules[date.Format(DateFormat)] = scheduleIDs
- }
- }
- result.scheduleIDs = schedules
- if len(schedules) == 0 {
- return result, fmt.Errorf("no schedules found")
- }
- return result, nil
- }
- func selectTrips(r TrafficResult) (TrafficResult, error) {
- result := r.(*DeparturesResult)
- trips := map[string][]Trip{}
- for _, order := range result.stop.Order {
- _, err := result.tripsFile.Seek(int64(order.TripOffset), 0)
- if err != nil {
- return result, fmt.Errorf("while seeking to StopOrder %v: %w", order, err)
- }
- trip := Trip{}
- err = bare.UnmarshalReader(result.tripsFile, &trip)
- if err != nil {
- return result, fmt.Errorf("while unmarshalling trip at offset %d: %w", order.TripOffset, err)
- }
- for startDate, scheduleIDs := range result.scheduleIDs {
- if _, ok := scheduleIDs[trip.ScheduleID]; ok {
- trips[startDate] = append(trips[startDate], trip)
- }
- }
- }
- result.trips = trips
- result.tripsFile.Close()
- return result, nil
- }
- func getFeedInfo2(r TrafficResult) (TrafficResult, error) {
- result := r.(*DeparturesResult)
- feedInfo, err := getFeedInfo(result.context.DataHome, result.context.FeedID, result.context.Version)
- result.feedInfo = feedInfo
- return result, err
- }
- func selectEnrichMethod(r TrafficResult) TrafficResult {
- result := r.(*DeparturesResult)
- if result.feedInfo.Name != "" {
- _, tripUpdatesRtFeed := result.feedInfo.RealtimeFeeds[TRIP_UPDATES]
- _, vehiclePositionsRtFeed := result.feedInfo.RealtimeFeeds[VEHICLE_POSITIONS]
- if tripUpdatesRtFeed || vehiclePositionsRtFeed {
- result.enrichMethod = getGtfsRealtimeUpdates
- // log.Println("GTFS")
- } else if isLuaUpdatesScript(result.context) {
- result.enrichMethod = getLuaRealtimeUpdates
- // log.Println("Lua")
- } else {
- result.enrichMethod = nil
- // log.Println("none")
- }
- } else {
- result.enrichMethod = nil
- // log.Println("else")
- }
- return result
- }
- func getNoTripsDepartures(r TrafficResult) (TrafficResult, error) {
- result := r.(*DeparturesResult)
- if !isLuaUpdatesScript(result.context) {
- return r, nil
- }
- updates, alerts, areTripsInTimetable, err := result.enrichMethod("", -1, result.stop.Id, result.stop.Code, result.context)
- if areTripsInTimetable {
- return r, nil
- }
- if err != nil {
- return r, fmt.Errorf("while getting departures: %w", err)
- }
- pickups := map[string]Boarding{}
- dropoffs := map[string]Boarding{}
- for _, trips := range result.trips {
- for _, trip := range trips {
- if _, ok := pickups[trip.LineID]; ok {
- continue
- }
- for _, d := range trip.Departures {
- if d.StopSequence == result.stop.Order[trip.Id].Sequence {
- pickups[trip.LineID] = d.Pickup
- dropoffs[trip.LineID] = d.Dropoff
- }
- }
- }
- }
- departures, err := makeDeparturesFromNoTripUpdates(updates[result.stop.Code], alerts, pickups, dropoffs, result.timezone, result.languages)
- if err != nil {
- return r, fmt.Errorf("while creating departures without trip: %w", err)
- }
- result.departures = departures
- return result, nil
- }
- func makeDeparturesFromNoTripUpdates(updates []Update, alerts map[string][]Alert, pickups, dropoffs map[string]Boarding, timezone *time.Location, languages []language.Tag) ([]DepartureRealtimeNew, error) {
- departures := []DepartureRealtimeNew{}
- now := time.Now().In(timezone)
- for _, update := range updates {
- if update.Time == "" {
- log.Printf("update time is empty, update is %+v\n", update)
- continue
- }
- departureTime, err := time.Parse("150405", update.Time)
- if err != nil {
- return departures, fmt.Errorf("while parsing time: %w", err)
- }
- update.Delay = 0
- departureTime = time.Date(now.Year(), now.Month(), now.Day(), departureTime.Hour(), departureTime.Minute(), departureTime.Second(), 0, timezone)
- departures = append(departures, DepartureRealtimeNew{
- Time: departureTime,
- Departure: Departure{
- Pickup: pickups[update.VehicleStatus.LineID],
- Dropoff: dropoffs[update.VehicleStatus.LineID],
- },
- Order: StopOrder{
- uint(departureTime.Unix()),
- 0,
- },
- Update: update,
- Alerts: selectSpecificAlerts(alerts[update.VehicleStatus.TripID], languages),
- })
- }
- return departures, nil
- }
- func getTripsDepartures(r TrafficResult) (TrafficResult, error) {
- result := r.(*DeparturesResult)
- departures := []DepartureRealtimeNew{}
- if len(result.departures) > 0 {
- return r, nil
- }
- var ber BlockingError
- timedOut := false
- for startDate, trips := range result.trips {
- for _, trip := range trips {
- tripStartDate, _ := time.Parse(DateFormat, startDate) // NOTE internally formatted date, will alway succeed
- if len(trip.Headways) == 0 { // NOTE departures based on schedule
- scheduleDeparture, err := getScheduleDeparture(trip, result.stop.Order[trip.Id], tripStartDate, result.timezone, nil, result.context)
- if err != nil {
- log.Printf("no departures found for trip %s, order %v\n", trip.Id, result.stop.Order[trip.Id])
- continue
- }
- if result.enrichMethod != nil && !timedOut {
- departure, err := enrichDeparture(result, scheduleDeparture[0], trip)
- if err != nil {
- if isTimeout(err) || errors.As(err, &ber) || strings.Contains(err.Error(), "connection refused") { // TODO or any other connection problem
- timedOut = true
- log.Printf("blocking error while enriching departure %s -> %s (%v): %v", trip.LineID, trip.Headsign, departure.Time, err)
- } else {
- log.Printf("while enriching departure %s -> %s (%v): %v\n", trip.LineID, trip.Headsign, departure.Time, err)
- }
- departures = append(departures, scheduleDeparture[0])
- } else {
- departures = append(departures, departure)
- }
- } else {
- departures = append(departures, scheduleDeparture[0])
- }
- } else { // departures based on frequencies
- for _, headway := range trip.Headways {
- if headway.Exact { // departures based on frequencies; exact times
- headwayDepartures, err := getScheduleDeparture(trip, result.stop.Order[trip.Id], tripStartDate, result.timezone, &headway, result.context)
- if err != nil {
- log.Printf("no departures found for trip %s, order %v\n", trip.Id, result.stop.Order[trip.Id])
- break
- }
- for _, headwayDeparture := range headwayDepartures {
- if result.enrichMethod != nil && !timedOut {
- departure, err := enrichDeparture(result, headwayDeparture, trip)
- if err != nil {
- if isTimeout(err) || errors.As(err, &ber) || strings.Contains(err.Error(), "connection refused") { // TODO or any other connection problem
- timedOut = true
- log.Printf("blocking error while enriching departure %s -> %s (%v): %v", trip.LineID, trip.Headsign, departure.Time, err)
- } else {
- log.Printf("while enriching departure %s -> %s (%v): %v\n", trip.LineID, trip.Headsign, departure.Time, err)
- }
- departures = append(departures, headwayDeparture)
- } else {
- departures = append(departures, departure)
- }
- } else {
- departures = append(departures, headwayDeparture)
- }
- }
- } else { // departures based on frequencies; inexact times
- headwayDepartures, err := getScheduleDeparture(trip, result.stop.Order[trip.Id], tripStartDate, result.timezone, &headway, result.context)
- if err != nil {
- log.Printf("no departures found for trip %s, order %v\n", trip.Id, result.stop.Order[trip.Id])
- break
- }
- var headwayUpdates []DepartureRealtimeNew
- if !timedOut {
- headwayUpdates, err = getUpdatesDepartures(result, trip, headwayDepartures[0].Pickup, headwayDepartures[0].Dropoff, false, tripStartDate, headwayDepartures[0].Departure.Time)
- if err != nil {
- if isTimeout(err) || errors.As(err, &ber) || strings.Contains(err.Error(), "connection refused") { // TODO or any other connection problem
- timedOut = true
- log.Printf("blocking error while getting updates departures: %v", err)
- } else {
- log.Printf("blocking error while getting updates departures: %v", err)
- }
- }
- }
- headwayUpdatesNumber := len(headwayUpdates)
- if headwayUpdatesNumber == 0 {
- departures = append(departures, headwayDepartures...)
- } else {
- departures = append(departures, headwayUpdates...)
- templateDeparture := headwayUpdates[0]
- for i := uint(0); ; i++ {
- singleDepartureTime := templateDeparture.Time.Add(time.Duration(headway.Interval*i) * -time.Second)
- if singleDepartureTime.Before(headwayDepartures[0].Time) {
- break
- }
- singleDeparture := templateDeparture
- singleDeparture.Update = Update{
- VehicleStatus: VehicleStatus{
- LineID: trip.LineID,
- Headsign: trip.Headsign,
- },
- }
- singleDeparture.Time = singleDepartureTime
- departures = append(departures, singleDeparture)
- }
- templateDeparture = headwayUpdates[headwayUpdatesNumber-1]
- for i := uint(0); ; i++ {
- singleDepartureTime := templateDeparture.Time.Add(time.Duration(headway.Interval*i) * time.Second)
- if singleDepartureTime.After(headwayDepartures[len(headwayDepartures)-1].Time) {
- break
- }
- singleDeparture := templateDeparture
- singleDeparture.Update = Update{
- VehicleStatus: VehicleStatus{
- LineID: trip.LineID,
- Headsign: trip.Headsign,
- },
- }
- singleDeparture.Time = singleDepartureTime
- departures = append(departures, singleDeparture)
- }
- }
- }
- }
- }
- }
- }
- result.departures = departures
- return result, nil
- }
- func getScheduleDeparture(trip Trip, order StopOrder, tripStartDate time.Time, timezone *time.Location, headway *Headway, context Context) ([]DepartureRealtimeNew, error) {
- templateDeparture := DepartureRealtimeNew{}
- found := false
- for _, departure := range trip.Departures {
- if departure.StopSequence == order.Sequence {
- templateDeparture.Departure = departure
- templateDeparture.Order = order
- templateDeparture.Update = Update{
- VehicleStatus: VehicleStatus{
- Headsign: trip.Headsign,
- LineID: trip.LineID,
- },
- }
- templateDeparture.Exact = departure.Exact
- templateDeparture.Time = calculateGtfsTime(departure.Time, 0, tripStartDate,
- timezone)
- found = true
- break
- }
- }
- if !found {
- return []DepartureRealtimeNew{}, traffic_errors.NoStopOrder{
- TripID: trip.Id,
- Order: order.Sequence,
- }
- }
- if headway == nil {
- return []DepartureRealtimeNew{templateDeparture}, nil
- }
- departures := []DepartureRealtimeNew{}
- for i := uint(0); ; i++ {
- if headway.StartTime+(headway.Interval*i) > headway.EndTime {
- break
- }
- singleDeparture := templateDeparture
- singleDeparture.Exact = (headway.Exact && templateDeparture.Exact)
- singleDeparture.Time = singleDeparture.Time.Add(time.Duration(headway.StartTime) * time.Second).Add(time.Duration(headway.Interval*i) * time.Second)
- startTime := templateDeparture.Departure.Time + headway.StartTime + (headway.Interval * i)
- singleDeparture.startTime = &startTime
- departures = append(departures, singleDeparture)
- }
- return departures, nil
- }
- func enrichDeparture(r *DeparturesResult, scheduleDeparture DepartureRealtimeNew, trip Trip) (DepartureRealtimeNew, error) {
- if r.departuresType == DEPARTURES_HYBRID {
- var (
- updates map[string][]Update
- alerts map[string][]Alert
- err error
- )
- updates, alerts, _, err = r.enrichMethod(trip.Id, scheduleDeparture.Order.Sequence, r.stop.Id, r.stop.Code, r.context)
- if err != nil {
- return scheduleDeparture, err
- }
- tripUpdates := updates[trip.Id]
- var validTripUpdate Update
- for _, tripUpdate := range tripUpdates {
- if scheduleDeparture.startTime != nil && tripUpdate.StartTime != nil && *scheduleDeparture.startTime != *tripUpdate.StartTime {
- // TODO if update.relationship is UNSCHEDULED -> add this update as new departure
- continue
- }
- if tripUpdate.StopSequence > uint32(scheduleDeparture.Order.Sequence) {
- break
- }
- validTripUpdate.Time = tripUpdate.Time
- validTripUpdate.Delay = tripUpdate.Delay
- validTripUpdate.StopID = tripUpdate.StopID
- validTripUpdate.StopSequence = tripUpdate.StopSequence
- validTripUpdate.TimetableRelationship = tripUpdate.TimetableRelationship
- validTripUpdate.VehicleStatus = tripUpdate.VehicleStatus
- }
- validTripUpdate.VehicleStatus.LineID = trip.LineID
- validTripUpdate.VehicleStatus.Headsign = trip.Headsign
- departure := scheduleDeparture.WithUpdate(validTripUpdate).WithAlerts(alerts[trip.Id], r.languages)
- return departure, nil
- } else {
- update := Update{}
- update.VehicleStatus.LineID = trip.LineID
- update.VehicleStatus.Headsign = trip.Headsign
- return scheduleDeparture.WithUpdate(update), nil
- }
- }
- func getUpdatesDepartures(r *DeparturesResult, trip Trip, pickup, dropoff Boarding, exact bool, tripStartDate time.Time, departureTime uint) ([]DepartureRealtimeNew, error) {
- departures := []DepartureRealtimeNew{}
- updates, alerts, _, err := r.enrichMethod(trip.Id, r.stop.Order[trip.Id].Sequence, r.stop.Id, r.stop.Code, r.context)
- if err != nil {
- return departures, err
- }
- tripUpdates := updates[trip.Id]
- updatesMap := map[uint]Update{}
- for _, tripUpdate := range tripUpdates {
- if tripUpdate.StopSequence > uint32(r.stop.Order[trip.Id].Sequence) {
- break
- }
- if tripUpdate.StartTime == nil {
- log.Printf("invalid update for frequency-based inexact-time trip: without start time")
- }
- validTripUpdate := updatesMap[*tripUpdate.StartTime]
- validTripUpdate.Time = tripUpdate.Time
- validTripUpdate.Delay = tripUpdate.Delay
- validTripUpdate.StopID = tripUpdate.StopID
- validTripUpdate.StopSequence = tripUpdate.StopSequence
- validTripUpdate.TimetableRelationship = tripUpdate.TimetableRelationship
- validTripUpdate.VehicleStatus = tripUpdate.VehicleStatus
- validTripUpdate.VehicleStatus.LineID = trip.LineID
- validTripUpdate.VehicleStatus.Headsign = trip.Headsign
- updatesMap[*tripUpdate.StartTime] = validTripUpdate
- }
- for _, validTripUpdate := range updatesMap {
- scheduleDeparture := DepartureRealtimeNew{
- Departure: Departure{
- StopSequence: r.stop.Order[trip.Id].Sequence,
- Pickup: pickup,
- Dropoff: dropoff,
- Exact: exact,
- },
- Order: r.stop.Order[trip.Id],
- Update: validTripUpdate,
- Time: calculateGtfsTime(*validTripUpdate.StartTime, 0, tripStartDate, r.timezone).Add(time.Duration(departureTime) * time.Second),
- startTime: validTripUpdate.StartTime,
- }
- departure := scheduleDeparture.WithUpdate(validTripUpdate).WithAlerts(alerts[trip.Id], r.languages)
- departures = append(departures, departure)
- }
- return departures, nil
- }
- func dropTrips(r TrafficResult) TrafficResult {
- result := r.(*DeparturesResult)
- result.trips = map[string][]Trip{}
- return result
- }
- func filterDepartures(r TrafficResult) TrafficResult {
- result := r.(*DeparturesResult)
- departures := []DepartureRealtimeNew{}
- midnight := result.date // TODO should be in client timezone
- for _, departure := range result.departures {
- if (result.departuresType == DEPARTURES_FULL && departure.GetTimeWithDelay().After(midnight)) || (result.departuresType == DEPARTURES_HYBRID && departure.GetTimeWithDelay().After(result.minuteB4Datetime)) {
- departures = append(departures, departure)
- }
- }
- result.departures = departures
- return result
- }
- func filterDeparturesByLine(r TrafficResult) TrafficResult {
- result := r.(*DeparturesResult)
- departures := []DepartureRealtimeNew{}
- if result.lineID != "" {
- for _, departure := range result.departures {
- if departure.Update.VehicleStatus.LineID == result.lineID {
- departures = append(departures, departure)
- }
- }
- result.departures = departures
- }
- return result
- }
- func addAlerts(r TrafficResult) TrafficResult {
- result := r.(*DeparturesResult)
- alertedDepartures := make([]DepartureRealtimeNew, len(result.departures))
- for i, d := range result.departures {
- if len(d.Alerts) == 0 {
- d.Alerts = GetAlerts("", "", int(d.Order.TripOffset), result.context, result.traffic, result.languages)
- }
- alertedDepartures[i] = d
- }
- result.departures = alertedDepartures
- return result
- }
- func sortDepartures(r TrafficResult) TrafficResult {
- result := r.(*DeparturesResult)
- sort.Slice(result.departures, func(i, j int) bool {
- return result.departures[i].GetTimeWithDelay().Before(result.departures[j].GetTimeWithDelay())
- })
- return result
- }
- func GetDepartures(stopCode, lineID string, ctx Context, traffic *Traffic, date time.Time, departuresType DeparturesType, languages []language.Tag) ([]DepartureRealtimeNew, error) {
- codeIndex := traffic.CodeIndexes[ctx.FeedID][ctx.Version]
- calendar := traffic.Calendars[ctx.FeedID][ctx.Version]
- result := &DeparturesResult{
- traffic: traffic,
- context: ctx,
- date: date, // has timezone of feed
- timetableHome: filepath.Join(ctx.DataHome, ctx.FeedID, string(ctx.Version)),
- calendar: calendar,
- stopOffset: codeIndex[stopCode],
- languages: languages,
- departuresType: departuresType,
- lineID: lineID,
- }
- r := gott.R[TrafficResult]{S: result}.
- Bind(loadTimezone).
- Map(loadTime).
- Bind(loadSchedules).
- Bind(openStopsFile).
- Bind(seekStopsFile).
- Bind(unmarshallStop).
- Bind(openTripsFile).
- Bind(selectTrips).
- Bind(getFeedInfo2).
- Map(selectEnrichMethod).
- Bind(getNoTripsDepartures).
- Bind(getTripsDepartures).
- Map(dropTrips).
- Map(filterDepartures).
- Map(filterDeparturesByLine).
- Map(addAlerts).
- Map(sortDepartures).
- Recover(closeFiles)
- return r.S.(*DeparturesResult).departures, r.E
- }
|