Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions internal/restapi/block_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,19 @@
return
}

blockEntry := transformBlockToEntry(block, utils.FormCombinedID(agencyID, blockID), agencyID)
stopIDs := make([]string, len(block))
for i, row := range block {
stopIDs[i] = row.StopID
}
stopAgencies, err := api.loadStopAgencyIDs(ctx, stopIDs)
if err != nil {
api.serverErrorResponse(w, r, err)
return
}

blockEntry := transformBlockToEntry(block, utils.FormCombinedID(agencyID, blockID), agencyID, stopAgencies)

references, err := api.getReferences(ctx, agencyID, block)
references, err := api.getReferences(ctx, agencyID, stopAgencies, block)
if err != nil {
api.serverErrorResponse(w, r, err)
return
Expand All @@ -72,7 +82,7 @@
api.sendResponse(w, r, response)
}

func transformBlockToEntry(block []gtfsdb.GetBlockDetailsRow, blockID, agencyID string) models.BlockEntry {
func transformBlockToEntry(block []gtfsdb.GetBlockDetailsRow, blockID, agencyID string, stopAgencies stopAgencyIDs) models.BlockEntry {

Check failure on line 85 in internal/restapi/block_handler.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=OneBusAway_maglev&issues=AaFvAQFiZptvl_vNJwUQ&open=AaFvAQFiZptvl_vNJwUQ&pullRequest=1794
serviceGroups := make(map[string][]gtfsdb.GetBlockDetailsRow)

for _, row := range block {
Expand Down Expand Up @@ -137,7 +147,7 @@
StopTime: models.StopTime{
ArrivalTime: models.NewModelDuration(time.Duration(stop.ArrivalTime)),
DepartureTime: models.NewModelDuration(time.Duration(stop.DepartureTime)),
StopID: utils.FormCombinedID(agencyID, stop.StopID),
StopID: stopAgencies.combinedStopID(stop.StopID),
},
DropOffType: int(stop.DropOffType.Int64),
PickupType: int(stop.PickupType.Int64),
Expand Down Expand Up @@ -173,7 +183,7 @@
}
}

func (api *RestAPI) getReferences(ctx context.Context, agencyID string, block []gtfsdb.GetBlockDetailsRow) (models.ReferencesModel, error) {
func (api *RestAPI) getReferences(ctx context.Context, agencyID string, stopAgencies stopAgencyIDs, block []gtfsdb.GetBlockDetailsRow) (models.ReferencesModel, error) {
routeIDs := make(map[string]struct{})
stopIDs := make(map[string]struct{})
tripIDs := make(map[string]struct{})
Expand Down Expand Up @@ -227,7 +237,7 @@
stops := make([]models.Stop, 0)
for _, stop := range batchedStops {
stops = append(stops, models.Stop{
ID: utils.FormCombinedID(agencyID, stop.ID),
ID: stopAgencies.combinedStopID(stop.ID),
Name: stop.Name.String,
Code: stop.Code.String,
Lat: stop.Lat,
Expand Down
39 changes: 39 additions & 0 deletions internal/restapi/entity_agency_prefix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package restapi

import (
"net/http"
"strconv"
"testing"
"time"

"github.com/stretchr/testify/assert"
"maglev.onebusaway.org/internal/clock"
internalgtfs "maglev.onebusaway.org/internal/gtfs"
)

func agencyPrefixFiles() map[string]string {
Expand Down Expand Up @@ -96,3 +99,39 @@ func TestStopReferences_UseTheStopsOwnAgency(t *testing.T) {
})
}
}

// Trip status, trip stop times and block stop times name each stop by its own
// agency, not the agency of the trip that visits it.
func TestTripStopIDs_UseTheStopsOwnAgency(t *testing.T) {
now := time.Date(2025, 1, 1, 10, 15, 0, 0, time.UTC)
midTrip := clock.NewMockClock(now)
api := createTestApiWithGTFSFixture(t, midTrip, "agency-prefix.zip", agencyPrefixFiles())
serviceDate := strconv.FormatInt(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(), 10)
api.GtfsManager.MockAddVehicleWithOptions("bus", "other-trip", "other-route", internalgtfs.MockVehicleOptions{Timestamp: &now})
t.Cleanup(api.GtfsManager.MockResetRealTimeData)

_, details := serveApiAndRetrieveEndpoint(t, api,
"/api/where/trip-details/other_other-trip.json?key=TEST&includeStatus=true&serviceDate="+serviceDate)
entry := details.Data.(map[string]any)["entry"].(map[string]any)
status := entry["status"].(map[string]any)
assert.Equal(t, "own_stop-1", status["closestStop"])
assert.Equal(t, "own_stop-2", status["nextStop"])
assert.Equal(t, []string{"own_stop-1", "own_stop-2"}, stopIDsOf(entry["schedule"].(map[string]any)["stopTimes"]))

_, block := serveApiAndRetrieveEndpoint(t, api, "/api/where/block/other_other-block.json?key=TEST")
configuration := block.Data.(map[string]any)["entry"].(map[string]any)["configurations"].([]any)[0].(map[string]any)
trip := configuration["trips"].([]any)[0].(map[string]any)
var blockStopIDs []string
for _, blockStopTime := range trip["blockStopTimes"].([]any) {
blockStopIDs = append(blockStopIDs, blockStopTime.(map[string]any)["stopTime"].(map[string]any)["stopId"].(string))
}
assert.Equal(t, []string{"own_stop-1", "own_stop-2"}, blockStopIDs)
}

func stopIDsOf(stopTimes any) []string {
var stopIDs []string
for _, stopTime := range stopTimes.([]any) {
stopIDs = append(stopIDs, stopTime.(map[string]any)["stopId"].(string))
}
return stopIDs
}
5 changes: 5 additions & 0 deletions internal/restapi/id_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ func (api *RestAPI) extractAndValidateAgencyCodeID(w http.ResponseWriter, r *htt
// agencies' routes serve it.
type stopAgencyIDs map[string]string

// combinedStopID returns the stop's public ID, or "" for a stop with no agency.
func (s stopAgencyIDs) combinedStopID(stopID string) string {
return utils.FormCombinedID(s[stopID], stopID)
}

// loadStopAgencyIDs reads the agency in each stop's public ID. It is the one way to
// learn a stop's agency; the routes serving a stop don't decide it.
func (api *RestAPI) loadStopAgencyIDs(ctx context.Context, stopIDs []string) (stopAgencyIDs, error) {
Expand Down
37 changes: 11 additions & 26 deletions internal/restapi/trips_for_location_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -843,14 +843,11 @@ func (api *RestAPI) buildTripsForLocationEntries(
}
}

stopCoords := make(map[string]struct{ lat, lon float64 })
var stops tripStops
if len(allStopIDs) > 0 {
stopsRaw, err := api.GtfsManager.GtfsDB.Queries.GetStopsByIDs(ctx, allStopIDs)
if err == nil {
for _, s := range stopsRaw {
stopCoords[s.ID] = struct{ lat, lon float64 }{lat: s.Lat, lon: s.Lon}
}
} else {
var err error
stops, err = api.loadTripStops(ctx, allStopIDs)
if err != nil {
reqLogger.Warn("failed to bulk fetch stops", "error", err, "stop_count", len(allStopIDs))
}
}
Expand Down Expand Up @@ -912,7 +909,7 @@ func (api *RestAPI) buildTripsForLocationEntries(
currentLocation: agencyLocation,
stopTimes: stopTimesMap[tripID],
shapePoints: shapePoints,
stopCoords: stopCoords,
stops: stops,
blockTrips: blockTrips,
}, frequency)
}
Expand Down Expand Up @@ -1055,29 +1052,17 @@ func (api *RestAPI) buildScheduleForTrip(
func buildStopTimesList(api *RestAPI, ctx context.Context, stopTimes []gtfsdb.StopTime, shapePoints []gtfs.ShapePoint, agencyID string) []models.StopTime {

reqLogger := logging.ForComponent(ctx, "http_server")
// Batch-fetch all stop coordinates at once
stopIDs := make([]string, len(stopTimes))
for i, st := range stopTimes {
stopIDs[i] = st.StopID
}

stops, err := api.GtfsManager.GtfsDB.Queries.GetStopsByIDs(ctx, stopIDs)

// Create a map for quick stop coordinate lookup
stopCoords := make(map[string]struct{ lat, lon float64 })
stopIDs := stopIDsForStopTimes(stopTimes)
stops, err := api.loadTripStops(ctx, stopIDs)
if err != nil {
// Log the error but continue - distances will be 0 for all stops
reqLogger.Warn("Failed to batch-fetch stop coordinates for distance calculation",
reqLogger.Warn("Failed to batch-fetch stops for the stop-time list",
"error", err,
"agency_id", agencyID,
"stop_count", len(stopIDs))
} else {
for _, stop := range stops {
stopCoords[stop.ID] = struct{ lat, lon float64 }{lat: stop.Lat, lon: stop.Lon}
}
}

return api.calculateBatchStopDistances(stopTimes, shapePoints, stopCoords, agencyID)
return api.calculateBatchStopDistances(stopTimes, shapePoints, stops)

}

Expand Down Expand Up @@ -1340,7 +1325,7 @@ type scheduleData struct {
currentLocation *time.Location
stopTimes []gtfsdb.StopTime
shapePoints []gtfs.ShapePoint
stopCoords map[string]struct{ lat, lon float64 }
stops tripStops
blockTrips []gtfsdb.GetTripsByBlockIDsRow
}

Expand All @@ -1352,7 +1337,7 @@ func (api *RestAPI) buildScheduleFromMemory(data scheduleData, frequency *models
nextTripID, previousTripID := api.calculateNextPrevFromMemory(data.trip, data.blockTrips, data.agencyID)

// Calculate Distances using in-memory coords
stopTimesList := api.calculateBatchStopDistances(data.stopTimes, data.shapePoints, data.stopCoords, data.agencyID)
stopTimesList := api.calculateBatchStopDistances(data.stopTimes, data.shapePoints, data.stops)

return &models.TripsSchedule{
Frequency: frequency,
Expand Down
25 changes: 4 additions & 21 deletions internal/restapi/trips_for_route_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,15 +319,15 @@ func TestTripsForRouteHandler_CrossAgencyInterlinedBlock(t *testing.T) {
assert.Equal(t, time.Date(2025, 6, 12, 0, 0, 0, 0, laLoc).UnixMilli(), entry.Status.ServiceDate.UnixMilli())
})

t.Run("references contain combined stop IDs for queried and cross agencies", func(t *testing.T) {
// Each stop has one ID, under the feed's first agency, even on the other
// agency's interlined trip.
t.Run("references contain one combined ID per stop", func(t *testing.T) {
refStops := model.Data.References.Stops
require.Len(t, refStops, 4, fmt.Sprintf("expected 4 stop references, got %d", len(refStops)))
require.Len(t, refStops, 2, fmt.Sprintf("expected 2 stop references, got %d", len(refStops)))

expectedStopIDs := map[string]bool{
utils.FormCombinedID(tripsForRouteAgencyID, tripsForRouteStop1ID): true,
utils.FormCombinedID(tripsForRouteAgencyID, tripsForRouteStop2ID): true,
utils.FormCombinedID(tfrAgencyB, tripsForRouteStop1ID): true,
utils.FormCombinedID(tfrAgencyB, tripsForRouteStop2ID): true,
}
recvdStopsByID := make(map[string]models.Stop)

Expand All @@ -349,23 +349,6 @@ func TestTripsForRouteHandler_CrossAgencyInterlinedBlock(t *testing.T) {
assert.Contains(t, recvdStopsByID, stopID,
"status stop %q must resolve in references.stops", stopID)
}

// combined stopIDs for both agencies on the same bare stop ID should have the same
// stop-specific data
for _, bareID := range []string{tripsForRouteStop1ID, tripsForRouteStop2ID} {
scheduledTripStopID := utils.FormCombinedID(tripsForRouteAgencyID, bareID)
activeTripStopID := utils.FormCombinedID(tfrAgencyB, bareID)
scheduledTripStop := recvdStopsByID[scheduledTripStopID]
activeTripStop := recvdStopsByID[activeTripStopID]

// clear the agency-specific values and assert the stop-specific
// values are equal
scheduledTripStop.ID, scheduledTripStop.Parent = "", ""
activeTripStop.ID, activeTripStop.Parent = "", ""

assert.Equal(t, scheduledTripStop, activeTripStop,
"shared stop %q should have the same data for both agency-qualified IDs", bareID)
}
})
}

Expand Down
Loading
Loading