Skip to content

Commit

Permalink
realtime: fix arrival-to-departure delay conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
matslina committed Jan 3, 2024
1 parent 2034d97 commit 164b5f6
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 39 deletions.
80 changes: 41 additions & 39 deletions realtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,41 @@ func resolveStopReferences(updates []*parse.StopTimeUpdate, events []*storage.St
}
}

// Computes delay by comparing a static time given as offset, and
// timestamp from realtime.
func delayFromOffsetAndTime(tz *time.Location, eventOffset time.Duration, updateTime time.Time) time.Duration {

// The eventOffset (from static GTFS) gives a time of
// day, as an offset from noon-12h in the local
// timezone. It's possible to apply to multiple dates,
// but most likely it's from the same day, or the day
// before the realtime update. Whichever's closer in
// time is what we pick.

sameNoonLocal := time.Date(updateTime.Year(), updateTime.Month(), updateTime.Day(), 12, 0, 0, 0, tz)
sameNoonUTC := sameNoonLocal.UTC()
prevNoonUTC := sameNoonLocal.AddDate(0, 0, -1).UTC()

sameTime := sameNoonUTC.Add(-12 * time.Hour).Add(eventOffset)
prevTime := prevNoonUTC.Add(-12 * time.Hour).Add(eventOffset)

sameDiff := updateTime.Sub(sameTime)
prevDiff := updateTime.Sub(prevTime)

sameDiffAbs := sameDiff
if sameDiffAbs < 0 {
sameDiffAbs *= -1
}
prevDiffAbs := prevDiff
if prevDiffAbs < 0 {
prevDiffAbs *= -1
}
if sameDiffAbs < prevDiffAbs {
return sameDiff
}
return prevDiff
}

// Construct RealtimeUpdates from StopTimeUpdates and
// StopTimeEvents. Groups them by trip and stop.
func (rt *Realtime) buildRealtimeUpdates(
Expand Down Expand Up @@ -296,41 +331,6 @@ func (rt *Realtime) buildRealtimeUpdates(
})
}

// Computes delay of an update, given the correspnding time
// from static schedule.
updateDelay := func(eventOffset time.Duration, updateTime time.Time) time.Duration {

// The eventOffset (from static GTFS) gives a time of
// day, as an offset from noon-12h in the local
// timezone. It's possible to apply to multiple dates,
// but most likely it's from the same day, or the day
// before the realtime update. Whichever's closer in
// time is what we pick.

sameNoonLocal := time.Date(updateTime.Year(), updateTime.Month(), updateTime.Day(), 12, 0, 0, 0, timezone)
sameNoonUTC := sameNoonLocal.UTC()
prevNoonUTC := sameNoonLocal.AddDate(0, 0, -1).UTC()

sameTime := sameNoonUTC.Add(-12 * time.Hour).Add(eventOffset)
prevTime := prevNoonUTC.Add(-12 * time.Hour).Add(eventOffset)

sameDiff := updateTime.Sub(sameTime)
prevDiff := updateTime.Sub(prevTime)

sameDiffAbs := sameDiff
if sameDiffAbs < 0 {
sameDiffAbs *= -1
}
prevDiffAbs := prevDiff
if prevDiffAbs < 0 {
prevDiffAbs *= -1
}
if sameDiffAbs < prevDiffAbs {
return sameDiff
}
return prevDiff
}

// Combine static schedule and realtime updates
for tripID, tripUpdates := range updatesByTrip {
events, found := eventsByTrip[tripID]
Expand Down Expand Up @@ -375,7 +375,8 @@ func (rt *Realtime) buildRealtimeUpdates(
// Feeds can use the timestamp to communicate delays
rtUp.ArrivalDelay = u.ArrivalDelay
if !u.ArrivalTime.IsZero() && u.ArrivalDelay == 0 {
rtUp.ArrivalDelay = updateDelay(
rtUp.ArrivalDelay = delayFromOffsetAndTime(
timezone,
events[ei].StopTime.ArrivalTime(),
u.ArrivalTime,
)
Expand All @@ -385,7 +386,8 @@ func (rt *Realtime) buildRealtimeUpdates(
// Same thing here
rtUp.DepartureDelay = u.DepartureDelay
if !u.DepartureTime.IsZero() {
rtUp.DepartureDelay = updateDelay(
rtUp.DepartureDelay = delayFromOffsetAndTime(
timezone,
events[ei].StopTime.DepartureTime(),
u.DepartureTime,
)
Expand All @@ -396,12 +398,12 @@ func (rt *Realtime) buildRealtimeUpdates(
// departure. If the arrival is early,
// interpret it as a return to regular
// schedule.
rtUp.DepartureDelay = max(u.ArrivalDelay, 0)
rtUp.DepartureDelay = max(rtUp.ArrivalDelay, 0)
}
if !u.ArrivalIsSet {
// Lacking Arrival data, assume
// departure delay applies to arrival
rtUp.ArrivalDelay = u.DepartureDelay
rtUp.ArrivalDelay = rtUp.DepartureDelay
}

// Track the min and max delays observed. This
Expand Down
80 changes: 80 additions & 0 deletions realtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2026,3 +2026,83 @@ func TestRealtimeDelayCrossingDSTBoundaryFromPreviousDay(t *testing.T) {
require.Equal(t, tc.ExpectedDepartureTime, departures[0].Time, tc.Name)
}
}

func TestRealtimeArrivalDelayOnly(t *testing.T) {

// Some agencies (e.g. WMATA) prefer passing Arrival Delay
// instead of Departure Delay (or dito with Time.) When we
// encounter a delayed arrival, and no departure information
// is available, we should assume the departure is similarly
// delayed.
feed := buildFeed(t, []TripUpdate{
{
TripID: "t1",
StopUpdates: []StopUpdate{
{ // 55s delay
StopID: "s2",
ArrivalSet: true,
ArrivalDelay: 55,
},
},
},
{
TripID: "t2",
StopUpdates: []StopUpdate{
{ // arrival delay, but departure delay shows it recovers
StopID: "s1",
ArrivalSet: true,
ArrivalDelay: 55,
DepartureSet: true,
DepartureDelay: 0,
},
{ // 25s delay (via time)
StopSequence: 2,
ArrivalSet: true,
ArrivalTime: time.Date(2020, 1, 15, 23, 11, 25, 0, time.UTC),
},
},
},
{
TripID: "t3",
StopUpdates: []StopUpdate{
{ // 35s delay (via time)
StopID: "z1",
ArrivalSet: true,
ArrivalTime: time.Date(2020, 1, 15, 23, 5, 35, 0, time.UTC),
},
{ // recovery via early arrival
StopID: "z2",
ArrivalSet: true,
ArrivalTime: time.Date(2020, 1, 15, 23, 5, 55, 0, time.UTC),
},
},
},
})
static := SimpleStaticFixture(t)
rt, err := gtfs.NewRealtime(context.Background(), static, feed)
require.NoError(t, err)
assert.Equal(t, uint64(time.Date(2020, 1, 15, 23, 0, 0, 0, time.UTC).Unix()), rt.Timestamp)

for _, tc := range []struct {
TripID string
StopID string
Time time.Time
Delay time.Duration
}{
{"t1", "s1", time.Date(2020, 1, 15, 23, 0, 0, 0, time.UTC), 0},
{"t1", "s2", time.Date(2020, 1, 15, 23, 1, 55, 0, time.UTC), 55 * time.Second},
{"t1", "s3", time.Date(2020, 1, 15, 23, 2, 55, 0, time.UTC), 55 * time.Second},
{"t2", "s1", time.Date(2020, 1, 15, 23, 0, 0, 0, time.UTC), 0},
{"t2", "s2", time.Date(2020, 1, 15, 23, 11, 25, 0, time.UTC), 25 * time.Second},
{"t2", "s3", time.Date(2020, 1, 15, 23, 12, 25, 0, time.UTC), 25 * time.Second},
{"t3", "z1", time.Date(2020, 1, 15, 23, 5, 35, 0, time.UTC), 35 * time.Second},
{"t3", "z2", time.Date(2020, 1, 15, 23, 6, 0, 0, time.UTC), 0},
} {
deps, err := rt.Departures(tc.StopID, tc.Time.Add(-time.Minute), 2*time.Minute, -1, "", -1, nil)
require.NoError(t, err)
require.Equal(t, 1, len(deps))
assert.Equal(t, tc.Delay, deps[0].Delay, "trip %s stop %s time %s", tc.TripID, tc.StopID, tc.Time)
assert.Equal(t, tc.Time, deps[0].Time, "trip %s stop %s time %s", tc.TripID, tc.StopID, tc.Time)
}

}
32 changes: 32 additions & 0 deletions whitebox_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gtfs

import (
"fmt"
"testing"
"time"

Expand Down Expand Up @@ -148,3 +149,34 @@ func TestStaticRangePerDate(t *testing.T) {
})
}
}

func TestRealtimeDelayFromOffsetAndTime(t *testing.T) {

// TODO: Would be great to flesh this out.

for _, tc := range []struct {
tz *time.Location
eventOffset string
updateTime string
expectedDelay string
}{
{time.UTC, "23h6m", "2020-01-15 23:05:55 +0000 UTC", "-5s"},
{time.UTC, "23h11m", "2020-01-15 23:11:25 +0000 UTC", "25s"},
} {
offset, err := time.ParseDuration(tc.eventOffset)
require.NoError(t, err)
time_, err := time.Parse("2006-01-02 15:04:05 -0700 MST", tc.updateTime)
require.NoError(t, err)
delay, err := time.ParseDuration(tc.expectedDelay)
require.NoError(t, err)

actual := delayFromOffsetAndTime(tc.tz, offset, time_)
assert.Equal(
t, delay, actual,
fmt.Sprintf(
"%s %s, expect %s, got %s",
tc.eventOffset, tc.updateTime, tc.expectedDelay, actual,
),
)
}
}

0 comments on commit 164b5f6

Please sign in to comment.