Skip to content

Commit 30302c0

Browse files
authored
Merge pull request #93 from MohamedKamal000/migrate-to-oba_gtfs
Migrate to oba gtfs
2 parents c1ba12f + 5f1f091 commit 30302c0

16 files changed

Lines changed: 251 additions & 30 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ module watchdog.onebusaway.org
33
go 1.23.5
44

55
require (
6+
github.com/OneBusAway/go-gtfs v1.1.1
67
github.com/OneBusAway/go-sdk v0.1.0-alpha.13
78
github.com/getsentry/sentry-go v0.33.0
89
github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4
9-
github.com/jamespfennell/gtfs v0.1.24
1010
github.com/julienschmidt/httprouter v1.3.0
1111
github.com/prometheus/client_golang v1.20.5
1212
github.com/prometheus/client_model v0.6.1

go.sum

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/OneBusAway/go-gtfs v1.1.1 h1:JWl0ndXHBED6PAh8v3w0UgSDYWBg2OmHvAJb5RXX3Ss=
2+
github.com/OneBusAway/go-gtfs v1.1.1/go.mod h1:MJqNyFOJs+iE1R6uerTyfBY6g3/sxvTvVdRhDeN1bu8=
13
github.com/OneBusAway/go-sdk v0.1.0-alpha.13 h1:xQdZjREPJTON4XKoQpUf9YTm8KCVsLJyOW9LkldyquY=
24
github.com/OneBusAway/go-sdk v0.1.0-alpha.13/go.mod h1:h1TnOvie6gN5gi0no/0w6nPg1jbidz2D+Osyq72R60Q=
35
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -15,12 +17,6 @@ github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4 h1:vCeHcs8N7MOccOOsOVIy
1517
github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
1618
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
1719
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
18-
github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4 h1:vCeHcs8N7MOccOOsOVIy1xcYu+kBkA4J5urTgigww7c=
19-
github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
20-
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
21-
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
22-
github.com/jamespfennell/gtfs v0.1.24 h1:em/W8Bg88xZwX70r7FoghMiuZDn3m5oj0EfqmikU1Io=
23-
github.com/jamespfennell/gtfs v0.1.24/go.mod h1:nj9QFIc695IUKM5djZXzyPtCIAYxufafb0sQNbiVMOs=
2420
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
2521
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
2622
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=

internal/app/test_helpers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"testing"
88
"time"
99

10-
remoteGtfs "github.com/jamespfennell/gtfs"
10+
remoteGtfs "github.com/OneBusAway/go-gtfs"
1111
"github.com/prometheus/client_golang/prometheus"
1212
"watchdog.onebusaway.org/internal/config"
1313
"watchdog.onebusaway.org/internal/geo"

internal/geo/geo_cluster.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package geo
33
import (
44
"fmt"
55

6+
remoteGtfs "github.com/OneBusAway/go-gtfs"
67
"github.com/golang/geo/s2"
7-
remoteGtfs "github.com/jamespfennell/gtfs"
88
)
99

1010
const s2Level = 13 // S2 cell level with 850–1225 m spatial resolution

internal/geo/geo_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package geo
22

33
import (
4-
remoteGtfs "github.com/jamespfennell/gtfs"
4+
remoteGtfs "github.com/OneBusAway/go-gtfs"
55
)
66

77
// For now geo package only exposes helper functions to be used by other packages.

internal/geo/geo_utils.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88
"math"
99
"sync"
1010

11+
remoteGtfs "github.com/OneBusAway/go-gtfs"
1112
"github.com/golang/geo/s2"
12-
remoteGtfs "github.com/jamespfennell/gtfs"
1313
)
1414

1515
// BoundingBox defines the geographic boundaries of a rectangular area.
@@ -41,8 +41,8 @@ func computeBoundingBox(stops []remoteGtfs.Stop) (BoundingBox, error) {
4141

4242
for _, stop := range stops {
4343
if stop.Latitude != nil && stop.Longitude != nil {
44-
lat := float64(*stop.Latitude)
45-
lon := float64(*stop.Longitude)
44+
lat := *stop.Latitude
45+
lon := *stop.Longitude
4646
if lat < minLat {
4747
minLat = lat
4848
}

internal/gtfs/gtfs_bundles.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import (
1111
"sync"
1212
"time"
1313

14+
remoteGtfs "github.com/OneBusAway/go-gtfs"
1415
"github.com/getsentry/sentry-go"
15-
remoteGtfs "github.com/jamespfennell/gtfs"
1616
"watchdog.onebusaway.org/internal/config"
1717
"watchdog.onebusaway.org/internal/geo"
1818
"watchdog.onebusaway.org/internal/models"

internal/gtfs/gtfs_bundles_test.go

Lines changed: 152 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"testing"
1111
"time"
1212

13-
remoteGtfs "github.com/jamespfennell/gtfs"
13+
remoteGtfs "github.com/OneBusAway/go-gtfs"
1414
"watchdog.onebusaway.org/internal/geo"
1515
"watchdog.onebusaway.org/internal/models"
1616
)
@@ -56,7 +56,6 @@ func TestDownloadGTFSBundle(t *testing.T) {
5656
if staticBundle == nil {
5757
t.Fatal("static data retrieved from the store is nil; expected non-nil value")
5858
}
59-
6059
data := readFixture(t, "gtfs.zip")
6160
expectedStaticData, err := remoteGtfs.ParseStatic(data, remoteGtfs.ParseStaticOptions{})
6261
if err != nil {
@@ -109,6 +108,151 @@ func TestDownloadGTFSBundle(t *testing.T) {
109108

110109
}
111110

111+
func TestAgencyParsing(t *testing.T) {
112+
data := readFixture(t, "gtfs.zip")
113+
staticBundle, err := remoteGtfs.ParseStatic(data, remoteGtfs.ParseStaticOptions{})
114+
if err != nil {
115+
t.Fatalf("failed to parse expected GTFS static data from fixture: %v", err)
116+
}
117+
118+
expectedStaticAgencies := []remoteGtfs.Agency{
119+
{
120+
Id: "40",
121+
Name: "Sound Transit",
122+
Url: "https://www.soundtransit.org",
123+
Timezone: "America/Los_Angeles",
124+
Language: "en",
125+
Phone: "1-888-889-6368",
126+
FareUrl: "https://www.soundtransit.org/ride-with-us/how-to-pay/fares",
127+
Email: "main@soundtransit.org",
128+
},
129+
}
130+
131+
if staticBundle.Agencies == nil {
132+
t.Fatal("stored static data has nil Agencies slice; expected it to be populated")
133+
}
134+
if len(staticBundle.Agencies) == 0 {
135+
t.Fatal("stored Agencies slice is empty; static data likely not parsed correctly")
136+
}
137+
138+
if len(expectedStaticAgencies) != len(staticBundle.Agencies) {
139+
t.Fatalf("expected %d agencies, got %d", len(expectedStaticAgencies), len(staticBundle.Agencies))
140+
}
141+
142+
expectedAgencies := make(map[string]remoteGtfs.Agency)
143+
144+
for _, agency := range expectedStaticAgencies {
145+
expectedAgencies[agency.Id] = remoteGtfs.Agency{
146+
Id: agency.Id,
147+
Name: agency.Name,
148+
Timezone: agency.Timezone,
149+
Url: agency.Url,
150+
FareUrl: agency.FareUrl,
151+
Language: agency.Language,
152+
Phone: agency.Phone,
153+
Email: agency.Email,
154+
}
155+
}
156+
157+
for _, agency := range staticBundle.Agencies {
158+
expectedAgency, ok := expectedAgencies[agency.Id]
159+
if !ok {
160+
t.Fatalf("unexpected agency ID %s", agency.Id)
161+
}
162+
163+
if expectedAgency != agency {
164+
t.Errorf(
165+
"agency mismatch for ID %s expected: %+v got %+v",
166+
agency.Id,
167+
expectedAgency,
168+
agency,
169+
)
170+
}
171+
}
172+
}
173+
174+
func TestStopsParsing(t *testing.T) {
175+
server := models.ObaServer{ID: 1, Name: "test"}
176+
177+
data := readFixture(t, "gtfs.zip")
178+
staticBundle, err := remoteGtfs.ParseStatic(data, remoteGtfs.ParseStaticOptions{})
179+
if err != nil {
180+
t.Fatal("failed to parse gtfs static data")
181+
}
182+
staticData := models.NewStaticData(staticBundle)
183+
staticStore := NewStaticStore()
184+
staticStore.Set(server.ID, staticData)
185+
stopIDs := []string{"11060", "1108"} // Make sure these exist in your test GTFS
186+
stopsData := map[string]struct {
187+
stopName string
188+
lat float64
189+
long float64
190+
}{
191+
"11060": {stopName: "Broadway & E Denny Way", lat: 47.618425, long: -122.320940},
192+
"1108": {stopName: "Westlake", lat: 47.611450, long: -122.337532},
193+
}
194+
195+
stops, err := getStopLocationsByIDs(server.ID, stopIDs, staticStore)
196+
if err != nil {
197+
t.Fatalf("unexpected error: %v", err)
198+
}
199+
if len(stops) == 0 {
200+
t.Fatalf("expected some matched stops, got 0")
201+
}
202+
203+
for _, stop := range stops {
204+
expected, ok := stopsData[stop.Id]
205+
if !ok {
206+
t.Fatalf("unexpected stop ID returned: %s", stop.Id)
207+
}
208+
if stop.Latitude == nil || stop.Longitude == nil {
209+
t.Fatalf("stop %s missing coordinates", stop.Id)
210+
}
211+
212+
if stop.Name != expected.stopName {
213+
t.Errorf("stop %s name mismatch: expected %s, got %s",
214+
stop.Id, expected.stopName, stop.Name)
215+
}
216+
217+
const epsilon = 1e-5
218+
if diff := *stop.Latitude - expected.lat; diff > epsilon || diff < -epsilon {
219+
t.Errorf("stop %s latitude mismatch: expected %f, got %f",
220+
stop.Id, expected.lat, *stop.Latitude)
221+
}
222+
if diff := *stop.Longitude - expected.long; diff > epsilon || diff < -epsilon {
223+
t.Errorf("stop %s longitude mismatch: expected %f, got %f",
224+
stop.Id, expected.long, *stop.Longitude)
225+
}
226+
}
227+
}
228+
229+
func TestGetEarliestAndLatestServiceDates(t *testing.T) {
230+
server := models.ObaServer{ID: 1, Name: "test"}
231+
data := readFixture(t, "gtfs.zip")
232+
staticBundle, err := remoteGtfs.ParseStatic(data, remoteGtfs.ParseStaticOptions{})
233+
if err != nil {
234+
t.Fatal("failed to parse gtfs static data")
235+
}
236+
staticData := models.NewStaticData(staticBundle)
237+
staticStore := NewStaticStore()
238+
staticStore.Set(server.ID, staticData)
239+
loc, _ := time.LoadLocation("America/Los_Angeles")
240+
// make sure these already exist in the test data
241+
expectedEarliestEndDate := time.Date(2024, 11, 22, 0, 0, 0, 0, loc)
242+
expectedLatestEndDate := time.Date(2025, 03, 28, 0, 0, 0, 0, loc)
243+
actualEarliest, actualLatest, err := getEarliestAndLatestServiceDates(staticData)
244+
if err != nil {
245+
t.Fatalf("unexpected error: %v", err)
246+
}
247+
248+
if !expectedEarliestEndDate.Equal(actualEarliest) {
249+
t.Fatalf("earliest date mismatch: expected %s, got %s", expectedEarliestEndDate.Format("2006-01-02"), actualEarliest.Format("2006-01-02"))
250+
}
251+
if !expectedLatestEndDate.Equal(actualLatest) {
252+
t.Fatalf("latest date mismatch: expected %s, got %s", expectedLatestEndDate.Format("2006-01-02"), actualLatest.Format("2006-01-02"))
253+
}
254+
}
255+
112256
func TestGetStopLocationsByIDs(t *testing.T) {
113257
server := models.ObaServer{ID: 1, Name: "test"}
114258

@@ -121,7 +265,7 @@ func TestGetStopLocationsByIDs(t *testing.T) {
121265
staticStore := NewStaticStore()
122266
staticStore.Set(server.ID, staticData)
123267

124-
t.Run("Valid stop IDs", func(t *testing.T) {
268+
t.Run("Valid stops IDs", func(t *testing.T) {
125269
stopIDs := []string{"11060", "1108"} // Make sure these exist in your test GTFS
126270
stops, err := getStopLocationsByIDs(server.ID, stopIDs, staticStore)
127271
if err != nil {
@@ -187,19 +331,21 @@ func TestFetchAndStoreGTFSRTFeed(t *testing.T) {
187331
t.Fatalf("Expected %d vehicles, got %d", len(expectedRtData.Vehicles), len(realtimeData.Vehicles))
188332
}
189333

190-
expectedMap := make(map[string]struct{})
334+
expectedMap := make(map[string]remoteGtfs.Vehicle)
191335
for _, vehicle := range expectedRtData.Vehicles {
192336
if vehicle.ID != nil {
193-
expectedMap[vehicle.ID.ID] = struct{}{}
337+
expectedMap[vehicle.ID.ID] = vehicle
194338
}
195339
}
196340
countExpectedNilIDs := len(expectedRtData.Vehicles) - len(expectedMap)
197341
countNilIDs := 0
198342
for _, vehicle := range realtimeData.Vehicles {
199343
if vehicle.ID != nil {
200-
if _, exists := expectedMap[vehicle.ID.ID]; !exists {
344+
expectedVehicle, exists := expectedMap[vehicle.ID.ID]
345+
if !exists {
201346
t.Errorf("Unexpected vehicle ID %s found in GTFS-RT data", vehicle.ID.ID)
202347
}
348+
assertVehicle(t, &vehicle, &expectedVehicle)
203349
} else {
204350
countNilIDs++
205351
}

internal/gtfs/gtfs_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"net/http"
77
"time"
88

9-
remoteGtfs "github.com/jamespfennell/gtfs"
9+
remoteGtfs "github.com/OneBusAway/go-gtfs"
1010
"watchdog.onebusaway.org/internal/geo"
1111
"watchdog.onebusaway.org/internal/models"
1212
)

internal/gtfs/test_helpers.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import (
66
"os"
77
"path/filepath"
88
"testing"
9+
"time"
10+
11+
remoteGtfs "github.com/OneBusAway/go-gtfs"
912
)
1013

1114
func setupGtfsServer(t *testing.T, fixturePath string) *httptest.Server {
@@ -55,3 +58,81 @@ func readFixture(t *testing.T, fixturePath string) []byte {
5558

5659
return data
5760
}
61+
62+
func assertPtr[T comparable](t *testing.T, expected *T, actual *T, field string, equal func(a, b T) bool) {
63+
t.Helper()
64+
65+
if expected == nil {
66+
if actual != nil {
67+
t.Errorf("%s mismatch:\nexpected: nil\ngot %v", field, *actual)
68+
}
69+
return
70+
}
71+
72+
if actual == nil {
73+
t.Errorf("%s mismatch:\nexpected: %v\ngot nil", field, *expected)
74+
return
75+
}
76+
if !equal(*expected, *actual) {
77+
t.Errorf("%s mismatch:\nexpected %v, got %v", field, *expected, *actual)
78+
}
79+
}
80+
81+
func assertVehicle(t *testing.T, actual *remoteGtfs.Vehicle, expected *remoteGtfs.Vehicle) {
82+
t.Helper()
83+
if expected == nil {
84+
t.Fatal("expected vehicle must not be nil")
85+
}
86+
if actual == nil {
87+
t.Errorf("vehicle mismatch:\nexpected: %v\ngot nil", expected)
88+
return
89+
}
90+
91+
if expected.ID == nil {
92+
if actual.ID != nil {
93+
t.Errorf("vehicle ID mismatch:\nexpected: nil\ngot %v", actual.ID)
94+
}
95+
} else {
96+
if actual.ID == nil {
97+
t.Errorf("vehicle ID mismatch:\nexpected: %v\ngot nil", expected.ID)
98+
} else if expected.ID.ID != actual.ID.ID {
99+
t.Errorf("vehicle ID mismatch:\nexpected: %s\ngot %s", expected.ID.ID, actual.ID.ID)
100+
}
101+
}
102+
103+
assertPtr(t, expected.StopID, actual.StopID, "vehicle StopID", func(a, b string) bool {
104+
return a == b
105+
})
106+
assertPtr(t, expected.CurrentStopSequence, actual.CurrentStopSequence, "vehicle StopSequence", func(a, b uint32) bool {
107+
return a == b
108+
})
109+
assertPtr(t, expected.Timestamp, actual.Timestamp, "vehicle Timestamp", func(a, b time.Time) bool {
110+
return a.Equal(b)
111+
})
112+
113+
if expected.Position == nil {
114+
if actual.Position != nil {
115+
t.Errorf("vehicle Position mismatch:\nexpected: nil\ngot %v", actual.Position)
116+
}
117+
} else {
118+
if actual.Position == nil {
119+
t.Errorf("vehicle Position mismatch:\nexpected: %v\ngot nil", expected.Position)
120+
} else {
121+
const eps = 1e-5
122+
123+
floatEq := func(a, b float32) bool {
124+
diff := a - b
125+
return diff <= eps && diff >= -eps
126+
}
127+
128+
assertPtr(t, expected.Position.Latitude, actual.Position.Latitude, "vehicle latitude", floatEq)
129+
assertPtr(t, expected.Position.Longitude, actual.Position.Longitude, "vehicle longitude", floatEq)
130+
assertPtr(t, expected.Position.Speed, actual.Position.Speed, "vehicle speed", floatEq)
131+
}
132+
}
133+
134+
if expected.IsEntityInMessage != actual.IsEntityInMessage {
135+
t.Errorf("IsEntityInMessage mismatch:\nexpected: %v\ngot %v",
136+
expected.IsEntityInMessage, actual.IsEntityInMessage)
137+
}
138+
}

0 commit comments

Comments
 (0)