diff --git a/docs/RouterConfiguration.md b/docs/RouterConfiguration.md index 1e77d3513a2..42f6237ee34 100644 --- a/docs/RouterConfiguration.md +++ b/docs/RouterConfiguration.md @@ -730,7 +730,11 @@ HTTP headers to add to the request. Any header key, value can be inserted. "frequency" : "1m", "headers" : { "Header-Name" : "Header-Value" - } + }, + "fuzzyTripMatching" : false, + "features" : [ + "position" + ] }, { "type" : "websocket-gtfs-rt-updater" diff --git a/docs/UpdaterConfig.md b/docs/UpdaterConfig.md index cd3ee3f7035..be9f9579c17 100644 --- a/docs/UpdaterConfig.md +++ b/docs/UpdaterConfig.md @@ -223,17 +223,27 @@ The information is downloaded in a single HTTP request and polled regularly. -| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | -|----------------------------|:---------------:|----------------------------------------------------------------------------|:----------:|---------------|:-----:| -| type = "vehicle-positions" | `enum` | The type of the updater. | *Required* | | 1.5 | -| feedId | `string` | Feed ID to which the update should be applied. | *Required* | | 2.2 | -| frequency | `duration` | How often the positions should be updated. | *Optional* | `"PT1M"` | 2.2 | -| url | `uri` | The URL of GTFS-RT protobuf HTTP resource to download the positions from. | *Required* | | 2.2 | -| [headers](#u__6__headers) | `map of string` | HTTP headers to add to the request. Any header key, value can be inserted. | *Optional* | | 2.3 | +| Config Parameter | Type | Summary | Req./Opt. | Default Value | Since | +|-----------------------------|:---------------:|----------------------------------------------------------------------------|:----------:|---------------|:-----:| +| type = "vehicle-positions" | `enum` | The type of the updater. | *Required* | | 1.5 | +| feedId | `string` | Feed ID to which the update should be applied. | *Required* | | 2.2 | +| frequency | `duration` | How often the positions should be updated. | *Optional* | `"PT1M"` | 2.2 | +| fuzzyTripMatching | `boolean` | Whether to match trips fuzzily. | *Optional* | `false` | 2.5 | +| url | `uri` | The URL of GTFS-RT protobuf HTTP resource to download the positions from. | *Required* | | 2.2 | +| [features](#u__6__features) | `enum set` | Which features of GTFS RT vehicle positions should be loaded into OTP. | *Optional* | | 2.5 | +| [headers](#u__6__headers) | `map of string` | HTTP headers to add to the request. Any header key, value can be inserted. | *Optional* | | 2.3 | ##### Parameter details +

features

+ +**Since version:** `2.5` ∙ **Type:** `enum set` ∙ **Cardinality:** `Optional` +**Path:** /updaters/[6] +**Enum values:** `position` | `stop-position` | `occupancy` + +Which features of GTFS RT vehicle positions should be loaded into OTP. +

headers

**Since version:** `2.3` ∙ **Type:** `map of string` ∙ **Cardinality:** `Optional` @@ -256,7 +266,11 @@ HTTP headers to add to the request. Any header key, value can be inserted. "frequency" : "1m", "headers" : { "Header-Name" : "Header-Value" - } + }, + "fuzzyTripMatching" : false, + "features" : [ + "position" + ] } ] } diff --git a/src/ext-test/java/org/opentripplanner/ext/transmodelapi/mapping/TripRequestMapperTest.java b/src/ext-test/java/org/opentripplanner/ext/transmodelapi/mapping/TripRequestMapperTest.java index 1549a1feab9..0160b58140a 100644 --- a/src/ext-test/java/org/opentripplanner/ext/transmodelapi/mapping/TripRequestMapperTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/transmodelapi/mapping/TripRequestMapperTest.java @@ -40,7 +40,7 @@ import org.opentripplanner.routing.api.request.preference.TimeSlopeSafetyTriangle; import org.opentripplanner.routing.core.BicycleOptimizeType; import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.service.vehiclepositions.internal.DefaultVehiclePositionService; +import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; import org.opentripplanner.service.worldenvelope.internal.DefaultWorldEnvelopeRepository; import org.opentripplanner.service.worldenvelope.internal.DefaultWorldEnvelopeService; @@ -123,7 +123,7 @@ public class TripRequestMapperTest implements PlanTestConstants { Metrics.globalRegistry, RouterConfig.DEFAULT.vectorTileLayers(), new DefaultWorldEnvelopeService(new DefaultWorldEnvelopeRepository()), - new DefaultVehiclePositionService(), + new DefaultRealtimeVehicleService(transitService), new DefaultVehicleRentalService(), RouterConfig.DEFAULT.flexConfig(), List.of(), diff --git a/src/ext/graphql/transmodelapi/schema.graphql b/src/ext/graphql/transmodelapi/schema.graphql index 28ca9cd129b..b2bf6fb52c6 100644 --- a/src/ext/graphql/transmodelapi/schema.graphql +++ b/src/ext/graphql/transmodelapi/schema.graphql @@ -1524,18 +1524,50 @@ enum MultiModalMode { parent } +"OccupancyStatus to be exposed in the API. The values are based on GTFS-RT" enum OccupancyStatus { - "The vehicle or carriage has a few seats available." + """ + The vehicle or carriage can currently accommodate only standing passengers and has limited + space for them. There isn't a big difference between this and `full` so it's possible to + handle them as the same value, if one wants to limit the number of different values. + SIRI nordic profile: merge into `standingRoomOnly`. + """ + crushedStandingRoomOnly + """ + The vehicle is considered empty by most measures, and has few or no passengers onboard, but is + still accepting passengers. There isn't a big difference between this and `manySeatsAvailable` + so it's possible to handle them as the same value, if one wants to limit the number of different + values. + SIRI nordic profile: merge these into `manySeatsAvailable`. + """ + empty + """ + The vehicle or carriage has a few seats available. + SIRI nordic profile: less than ~50% of seats available. + """ fewSeatsAvailable - "The vehicle or carriage is considered full by most measures, but may still be allowing passengers to board." + """ + The vehicle or carriage is considered full by most measures, but may still be allowing + passengers to board. + """ full - "The vehicle or carriage has a large number of seats available." + """ + The vehicle or carriage has a large number of seats available. + SIRI nordic profile: more than ~50% of seats available. + """ manySeatsAvailable "The vehicle or carriage doesn't have any occupancy data available." noData - "The vehicle or carriage has no seats or standing room available." + """ + The vehicle or carriage has no seats or standing room available. + SIRI nordic profile: if vehicle/carriage is not in use / unavailable, or passengers are only + allowed to alight due to e.g. crowding. + """ notAcceptingPassengers - "The vehicle or carriage only has standing room available." + """ + The vehicle or carriage only has standing room available. + SIRI nordic profile: less than ~10% of seats available. + """ standingRoomOnly } diff --git a/src/ext/java/org/opentripplanner/ext/siri/mapper/OccupancyMapper.java b/src/ext/java/org/opentripplanner/ext/siri/mapper/OccupancyMapper.java index d745ea3d473..61b7a15798a 100644 --- a/src/ext/java/org/opentripplanner/ext/siri/mapper/OccupancyMapper.java +++ b/src/ext/java/org/opentripplanner/ext/siri/mapper/OccupancyMapper.java @@ -10,7 +10,7 @@ public class OccupancyMapper { public static OccupancyStatus mapOccupancyStatus(OccupancyEnumeration occupancy) { if (occupancy == null) { - return OccupancyStatus.NO_DATA; + return OccupancyStatus.NO_DATA_AVAILABLE; } return switch (occupancy) { case SEATS_AVAILABLE -> OccupancyStatus.MANY_SEATS_AVAILABLE; diff --git a/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/OccupancyStatusMapper.java b/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/OccupancyStatusMapper.java new file mode 100644 index 00000000000..978f8d46c2c --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/transmodelapi/mapping/OccupancyStatusMapper.java @@ -0,0 +1,27 @@ +package org.opentripplanner.ext.transmodelapi.mapping; + +import org.opentripplanner.transit.model.timetable.OccupancyStatus; + +/** + * Transmodel API supports a subset of {@link OccupancyStatus} and this mapper can be used to map + * any value to a value supported by the transmodel API. + */ +public class OccupancyStatusMapper { + + /** + * @return {@link OccupancyStatus} supported by the Transmodel API that is the closes match to the + * original. + */ + public static OccupancyStatus mapStatus(OccupancyStatus occupancyStatus) { + return switch (occupancyStatus) { + case NO_DATA_AVAILABLE -> OccupancyStatus.NO_DATA_AVAILABLE; + case EMPTY -> OccupancyStatus.EMPTY; + case MANY_SEATS_AVAILABLE -> OccupancyStatus.MANY_SEATS_AVAILABLE; + case FEW_SEATS_AVAILABLE -> OccupancyStatus.FEW_SEATS_AVAILABLE; + case STANDING_ROOM_ONLY -> OccupancyStatus.STANDING_ROOM_ONLY; + case CRUSHED_STANDING_ROOM_ONLY -> occupancyStatus.CRUSHED_STANDING_ROOM_ONLY; + case FULL -> OccupancyStatus.FULL; + case NOT_ACCEPTING_PASSENGERS -> OccupancyStatus.NOT_ACCEPTING_PASSENGERS; + }; + } +} diff --git a/src/ext/java/org/opentripplanner/ext/transmodelapi/model/EnumTypes.java b/src/ext/java/org/opentripplanner/ext/transmodelapi/model/EnumTypes.java index ff7a8002504..2b6942d7b8a 100644 --- a/src/ext/java/org/opentripplanner/ext/transmodelapi/model/EnumTypes.java +++ b/src/ext/java/org/opentripplanner/ext/transmodelapi/model/EnumTypes.java @@ -190,40 +190,19 @@ public class EnumTypes { .value("all", "all", "Both multiModal parents and their mono modal child stop places.") .build(); - public static final GraphQLEnumType OCCUPANCY_STATUS = GraphQLEnumType - .newEnum() - .name("OccupancyStatus") - .value( - "noData", - OccupancyStatus.NO_DATA, - "The vehicle or carriage doesn't have any occupancy data available." - ) - .value( - "manySeatsAvailable", - OccupancyStatus.MANY_SEATS_AVAILABLE, - "The vehicle or carriage has a large number of seats available." - ) - .value( - "fewSeatsAvailable", - OccupancyStatus.SEATS_AVAILABLE, - "The vehicle or carriage has a few seats available." - ) - .value( - "standingRoomOnly", - OccupancyStatus.STANDING_ROOM_ONLY, - "The vehicle or carriage only has standing room available." - ) - .value( - "full", - OccupancyStatus.FULL, - "The vehicle or carriage is considered full by most measures, but may still be allowing passengers to board." - ) - .value( - "notAcceptingPassengers", - OccupancyStatus.NOT_ACCEPTING_PASSENGERS, - "The vehicle or carriage has no seats or standing room available." + public static final GraphQLEnumType OCCUPANCY_STATUS = createFromDocumentedEnum( + "OccupancyStatus", + List.of( + map("noData", OccupancyStatus.NO_DATA_AVAILABLE), + map("empty", OccupancyStatus.EMPTY), + map("manySeatsAvailable", OccupancyStatus.MANY_SEATS_AVAILABLE), + map("fewSeatsAvailable", OccupancyStatus.FEW_SEATS_AVAILABLE), + map("standingRoomOnly", OccupancyStatus.STANDING_ROOM_ONLY), + map("crushedStandingRoomOnly", OccupancyStatus.CRUSHED_STANDING_ROOM_ONLY), + map("full", OccupancyStatus.FULL), + map("notAcceptingPassengers", OccupancyStatus.NOT_ACCEPTING_PASSENGERS) ) - .build(); + ); public static final GraphQLEnumType PURCHASE_WHEN = GraphQLEnumType .newEnum() diff --git a/src/ext/java/org/opentripplanner/ext/transmodelapi/model/siri/et/EstimatedCallType.java b/src/ext/java/org/opentripplanner/ext/transmodelapi/model/siri/et/EstimatedCallType.java index 3b7156854d9..7a39aee6cca 100644 --- a/src/ext/java/org/opentripplanner/ext/transmodelapi/model/siri/et/EstimatedCallType.java +++ b/src/ext/java/org/opentripplanner/ext/transmodelapi/model/siri/et/EstimatedCallType.java @@ -15,6 +15,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.Set; +import org.opentripplanner.ext.transmodelapi.mapping.OccupancyStatusMapper; import org.opentripplanner.ext.transmodelapi.model.EnumTypes; import org.opentripplanner.ext.transmodelapi.support.GqlUtil; import org.opentripplanner.model.TripTimeOnDate; @@ -202,7 +203,9 @@ public static GraphQLObjectType create( .name("occupancyStatus") .type(new GraphQLNonNull(EnumTypes.OCCUPANCY_STATUS)) .dataFetcher(environment -> - ((TripTimeOnDate) environment.getSource()).getOccupancyStatus() + OccupancyStatusMapper.mapStatus( + ((TripTimeOnDate) environment.getSource()).getOccupancyStatus() + ) ) .build() ) diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GraphQLRequestContext.java b/src/main/java/org/opentripplanner/apis/gtfs/GraphQLRequestContext.java index 82424276930..d7229b84b1f 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GraphQLRequestContext.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GraphQLRequestContext.java @@ -6,7 +6,7 @@ import org.opentripplanner.routing.fares.FareService; import org.opentripplanner.routing.graphfinder.GraphFinder; import org.opentripplanner.routing.vehicle_parking.VehicleParkingService; -import org.opentripplanner.service.vehiclepositions.VehiclePositionService; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.transit.service.TransitService; @@ -17,7 +17,7 @@ public record GraphQLRequestContext( FareService fareService, VehicleParkingService vehicleParkingService, VehicleRentalService vehicleRentalService, - VehiclePositionService vehiclePositionService, + RealtimeVehicleService realtimeVehicleService, GraphFinder graphFinder, RouteRequest defaultRouteRequest ) { @@ -28,7 +28,7 @@ public static GraphQLRequestContext ofServerContext(OtpServerRequestContext cont context.graph().getFareService(), context.graph().getVehicleParkingService(), context.vehicleRentalService(), - context.vehiclePositionService(), + context.realtimeVehicleService(), context.graphFinder(), context.defaultRouteRequest() ); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java index d7b936a6198..9c6b50dbb4c 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/GtfsGraphQLIndex.java @@ -71,6 +71,7 @@ import org.opentripplanner.apis.gtfs.datafetchers.TicketTypeImpl; import org.opentripplanner.apis.gtfs.datafetchers.TranslatedStringImpl; import org.opentripplanner.apis.gtfs.datafetchers.TripImpl; +import org.opentripplanner.apis.gtfs.datafetchers.TripOccupancyImpl; import org.opentripplanner.apis.gtfs.datafetchers.UnknownImpl; import org.opentripplanner.apis.gtfs.datafetchers.VehicleParkingImpl; import org.opentripplanner.apis.gtfs.datafetchers.VehiclePositionImpl; @@ -170,6 +171,7 @@ protected static GraphQLSchema buildSchema() { .type(typeWiring.build(CurrencyImpl.class)) .type(typeWiring.build(FareProductUseImpl.class)) .type(typeWiring.build(DefaultFareProductImpl.class)) + .type(typeWiring.build(TripOccupancyImpl.class)) .build(); SchemaGenerator schemaGenerator = new SchemaGenerator(); return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PatternImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PatternImpl.java index ad16e8718d2..35777dc9836 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PatternImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/PatternImpl.java @@ -22,8 +22,8 @@ import org.opentripplanner.routing.alertpatch.EntitySelector; import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.routing.services.TransitAlertService; -import org.opentripplanner.service.vehiclepositions.VehiclePositionService; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.TripPattern; @@ -227,9 +227,9 @@ public DataFetcher> tripsForDate() { } @Override - public DataFetcher> vehiclePositions() { + public DataFetcher> vehiclePositions() { return environment -> - getVehiclePositionsService(environment).getVehiclePositions(this.getSource(environment)); + getRealtimeVehiclesService(environment).getRealtimeVehicles(this.getSource(environment)); } private Agency getAgency(DataFetchingEnvironment environment) { @@ -252,8 +252,8 @@ private List getTrips(DataFetchingEnvironment environment) { return getSource(environment).scheduledTripsAsStream().collect(Collectors.toList()); } - private VehiclePositionService getVehiclePositionsService(DataFetchingEnvironment environment) { - return environment.getContext().vehiclePositionService(); + private RealtimeVehicleService getRealtimeVehiclesService(DataFetchingEnvironment environment) { + return environment.getContext().realtimeVehicleService(); } private TransitService getTransitService(DataFetchingEnvironment environment) { diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopRelationshipImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopRelationshipImpl.java index 3625c025148..bccc1212abb 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopRelationshipImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/StopRelationshipImpl.java @@ -4,7 +4,7 @@ import graphql.schema.DataFetchingEnvironment; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition.StopRelationship; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; public class StopRelationshipImpl implements GraphQLDataFetchers.GraphQLStopRelationship { @@ -23,7 +23,7 @@ public DataFetcher stop() { return env -> getSource(env).stop(); } - private StopRelationship getSource(DataFetchingEnvironment environment) { + private RealtimeVehicle.StopRelationship getSource(DataFetchingEnvironment environment) { return environment.getSource(); } } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripImpl.java index 3be84cb37de..78255ce7787 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripImpl.java @@ -22,12 +22,14 @@ import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLBikesAllowed; import org.opentripplanner.apis.gtfs.mapping.BikesAllowedMapper; +import org.opentripplanner.apis.gtfs.model.TripOccupancy; import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.model.Timetable; import org.opentripplanner.model.TripTimeOnDate; import org.opentripplanner.routing.alertpatch.EntitySelector; import org.opentripplanner.routing.alertpatch.TransitAlert; import org.opentripplanner.routing.services.TransitAlertService; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.TripPattern; @@ -368,6 +370,16 @@ public DataFetcher wheelchairAccessible( return environment -> GraphQLUtils.toGraphQL(getSource(environment).getWheelchairBoarding()); } + @Override + public DataFetcher occupancy() { + return environment -> { + Trip trip = getSource(environment); + return new TripOccupancy( + getRealtimeVehiclesService(environment).getVehicleOccupancyStatus(trip) + ); + }; + } + private List getStops(DataFetchingEnvironment environment) { TripPattern tripPattern = getTripPattern(environment); if (tripPattern == null) { @@ -392,6 +404,10 @@ private TransitService getTransitService(DataFetchingEnvironment environment) { return environment.getContext().transitService(); } + private RealtimeVehicleService getRealtimeVehiclesService(DataFetchingEnvironment environment) { + return environment.getContext().realtimeVehicleService(); + } + private Trip getSource(DataFetchingEnvironment environment) { return environment.getSource(); } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripOccupancyImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripOccupancyImpl.java new file mode 100644 index 00000000000..b8b01eda755 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/TripOccupancyImpl.java @@ -0,0 +1,31 @@ +package org.opentripplanner.apis.gtfs.datafetchers; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes; +import org.opentripplanner.apis.gtfs.model.TripOccupancy; + +public class TripOccupancyImpl implements GraphQLDataFetchers.GraphQLTripOccupancy { + + @Override + public DataFetcher occupancyStatus() { + return env -> { + var occupancyStatus = getSource(env).occupancyStatus(); + return switch (occupancyStatus) { + case NO_DATA_AVAILABLE -> GraphQLTypes.GraphQLOccupancyStatus.NO_DATA_AVAILABLE; + case EMPTY -> GraphQLTypes.GraphQLOccupancyStatus.EMPTY; + case MANY_SEATS_AVAILABLE -> GraphQLTypes.GraphQLOccupancyStatus.MANY_SEATS_AVAILABLE; + case FEW_SEATS_AVAILABLE -> GraphQLTypes.GraphQLOccupancyStatus.FEW_SEATS_AVAILABLE; + case STANDING_ROOM_ONLY -> GraphQLTypes.GraphQLOccupancyStatus.STANDING_ROOM_ONLY; + case CRUSHED_STANDING_ROOM_ONLY -> GraphQLTypes.GraphQLOccupancyStatus.CRUSHED_STANDING_ROOM_ONLY; + case FULL -> GraphQLTypes.GraphQLOccupancyStatus.FULL; + case NOT_ACCEPTING_PASSENGERS -> GraphQLTypes.GraphQLOccupancyStatus.NOT_ACCEPTING_PASSENGERS; + }; + }; + } + + private TripOccupancy getSource(DataFetchingEnvironment environment) { + return environment.getSource(); + } +} diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehiclePositionImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehiclePositionImpl.java index 0118eaa4a1a..7c2c0a7172a 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehiclePositionImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/VehiclePositionImpl.java @@ -3,45 +3,47 @@ import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition.StopRelationship; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle.StopRelationship; import org.opentripplanner.transit.model.timetable.Trip; public class VehiclePositionImpl implements GraphQLDataFetchers.GraphQLVehiclePosition { @Override public DataFetcher heading() { - return env -> getSource(env).heading(); + return env -> getSource(env).heading().orElse(null); } @Override public DataFetcher label() { - return env -> getSource(env).label(); + return env -> getSource(env).label().orElse(null); } @Override public DataFetcher lastUpdated() { - return env -> getSource(env).time().getEpochSecond(); + return env -> getSource(env).time().map(time -> time.getEpochSecond()).orElse(null); } @Override public DataFetcher lat() { - return env -> getSource(env).coordinates().latitude(); + return env -> + getSource(env).coordinates().map(coordinates -> coordinates.latitude()).orElse(null); } @Override public DataFetcher lon() { - return env -> getSource(env).coordinates().longitude(); + return env -> + getSource(env).coordinates().map(coordinates -> coordinates.longitude()).orElse(null); } @Override public DataFetcher speed() { - return env -> getSource(env).speed(); + return env -> getSource(env).speed().orElse(null); } @Override public DataFetcher stopRelationship() { - return env -> getSource(env).stop(); + return env -> getSource(env).stop().orElse(null); } @Override @@ -51,10 +53,10 @@ public DataFetcher trip() { @Override public DataFetcher vehicleId() { - return env -> getSource(env).vehicleId().toString(); + return env -> getSource(env).vehicleId().map(vehicleId -> vehicleId.toString()).orElse(null); } - private RealtimeVehiclePosition getSource(DataFetchingEnvironment environment) { + private RealtimeVehicle getSource(DataFetchingEnvironment environment) { return environment.getSource(); } } diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index d0562215f70..8f2013f75af 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -16,11 +16,13 @@ import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLAlertSeverityLevelType; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLBikesAllowed; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLInputField; +import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLOccupancyStatus; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRelativeDirection; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLRoutingErrorCode; import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode; import org.opentripplanner.apis.gtfs.model.RideHailingProvider; import org.opentripplanner.apis.gtfs.model.StopPosition; +import org.opentripplanner.apis.gtfs.model.TripOccupancy; import org.opentripplanner.ext.fares.model.FareRuleSet; import org.opentripplanner.ext.ridehailing.model.RideEstimate; import org.opentripplanner.model.StopTimesInPattern; @@ -44,8 +46,8 @@ import org.opentripplanner.routing.vehicle_parking.VehicleParking; import org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces; import org.opentripplanner.routing.vehicle_parking.VehicleParkingState; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition.StopRelationship; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle.StopRelationship; import org.opentripplanner.service.vehiclerental.model.RentalVehicleType; import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace; import org.opentripplanner.service.vehiclerental.model.VehicleRentalStation; @@ -560,7 +562,7 @@ public interface GraphQLPattern { public DataFetcher> tripsForDate(); - public DataFetcher> vehiclePositions(); + public DataFetcher> vehiclePositions(); } public interface GraphQLPlace { @@ -1033,6 +1035,8 @@ public interface GraphQLTrip { public DataFetcher id(); + public DataFetcher occupancy(); + public DataFetcher pattern(); public DataFetcher route(); @@ -1060,6 +1064,14 @@ public interface GraphQLTrip { public DataFetcher wheelchairAccessible(); } + /** + * Occupancy of a vehicle on a trip. This should include the most recent occupancy information + * available for a trip. Historic data might not be available. + */ + public interface GraphQLTripOccupancy { + public DataFetcher occupancyStatus(); + } + /** This is used for alert entities that we don't explicitly handle or they are missing. */ public interface GraphQLUnknown { public DataFetcher description(); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java index 3593145e274..cba63739dd5 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLTypes.java @@ -747,6 +747,18 @@ public enum GraphQLMode { WALK, } + /** Occupancy status of a vehicle. */ + public enum GraphQLOccupancyStatus { + CRUSHED_STANDING_ROOM_ONLY, + EMPTY, + FEW_SEATS_AVAILABLE, + FULL, + MANY_SEATS_AVAILABLE, + NOT_ACCEPTING_PASSENGERS, + NO_DATA_AVAILABLE, + STANDING_ROOM_ONLY, + } + public static class GraphQLOpeningHoursDatesArgs { private List dates; diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml index 14d00ec1bd4..1f5341558c2 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml @@ -58,6 +58,7 @@ config: Itinerary: org.opentripplanner.model.plan.Itinerary#Itinerary Leg: org.opentripplanner.model.plan.Leg#Leg Mode: String + OccupancyStatus: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLOccupancyStatus#GraphQLOccupancyStatus TransitMode: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLTransitMode#GraphQLTransitMode PageInfo: Object Pattern: org.opentripplanner.transit.model.network.TripPattern#TripPattern @@ -85,14 +86,15 @@ config: TicketType: org.opentripplanner.ext.fares.model.FareRuleSet#FareRuleSet TranslatedString: java.util.Map#Map.Entry Trip: org.opentripplanner.transit.model.timetable.Trip#Trip + TripOccupancy: org.opentripplanner.apis.gtfs.model.TripOccupancy#TripOccupancy Unknown: org.opentripplanner.apis.gtfs.model.UnknownModel#UnknownModel VehicleParking: org.opentripplanner.routing.vehicle_parking.VehicleParking#VehicleParking VehicleParkingSpaces: org.opentripplanner.routing.vehicle_parking.VehicleParkingSpaces#VehicleParkingSpaces VehicleParkingState: org.opentripplanner.routing.vehicle_parking.VehicleParkingState#VehicleParkingState VertexType: String SystemNotice: org.opentripplanner.model.SystemNotice#SystemNotice - VehiclePosition: org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition#RealtimeVehiclePosition - StopRelationship: org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition.StopRelationship#StopRelationship + VehiclePosition: org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle#RealtimeVehicle + StopRelationship: org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle.StopRelationship#StopRelationship WheelchairBoarding: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLWheelchairBoarding FormFactor: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLFormFactor PropulsionType: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLPropulsionType diff --git a/src/main/java/org/opentripplanner/apis/gtfs/model/TripOccupancy.java b/src/main/java/org/opentripplanner/apis/gtfs/model/TripOccupancy.java new file mode 100644 index 00000000000..1d4a61d3bdb --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/gtfs/model/TripOccupancy.java @@ -0,0 +1,8 @@ +package org.opentripplanner.apis.gtfs.model; + +import org.opentripplanner.transit.model.timetable.OccupancyStatus; + +/** + * Record for holding trip occupancy information. + */ +public record TripOccupancy(OccupancyStatus occupancyStatus) {} diff --git a/src/main/java/org/opentripplanner/service/realtimevehicles/RealtimeVehicleRepository.java b/src/main/java/org/opentripplanner/service/realtimevehicles/RealtimeVehicleRepository.java new file mode 100644 index 00000000000..acf558cb2d1 --- /dev/null +++ b/src/main/java/org/opentripplanner/service/realtimevehicles/RealtimeVehicleRepository.java @@ -0,0 +1,29 @@ +package org.opentripplanner.service.realtimevehicles; + +import java.util.List; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; +import org.opentripplanner.transit.model.network.TripPattern; + +public interface RealtimeVehicleRepository { + /** + * For the given pattern set all realtime vehicles. + *

+ * The list is expected to be exhaustive: all existing vehicles will be overridden. + *

+ * This means that if there are two updaters providing vehicles for the same pattern they + * overwrite each other. + */ + void setRealtimeVehicles(TripPattern pattern, List updates); + + /** + * Remove all vehicles for a given pattern. + *

+ * This is useful to clear old vehicles for which there are no more updates and we assume that + * they have stopped their trip. + */ + void clearRealtimeVehicles(TripPattern pattern); + /** + * Get the vehicles for a certain trip. + */ + List getRealtimeVehicles(TripPattern pattern); +} diff --git a/src/main/java/org/opentripplanner/service/realtimevehicles/RealtimeVehicleService.java b/src/main/java/org/opentripplanner/service/realtimevehicles/RealtimeVehicleService.java new file mode 100644 index 00000000000..949b196faeb --- /dev/null +++ b/src/main/java/org/opentripplanner/service/realtimevehicles/RealtimeVehicleService.java @@ -0,0 +1,22 @@ +package org.opentripplanner.service.realtimevehicles; + +import java.util.List; +import javax.annotation.Nonnull; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.OccupancyStatus; +import org.opentripplanner.transit.model.timetable.Trip; + +public interface RealtimeVehicleService { + /** + * Get the realtime vehicles for a certain trip pattern. Service contains all the vehicles that + * exist in input feeds but doesn't store any historical data. + */ + List getRealtimeVehicles(@Nonnull TripPattern pattern); + + /** + * Get the latest occupancy status for a certain trip. Service contains all the vehicles that + * exist in input feeds but doesn't store any historical data. + */ + OccupancyStatus getVehicleOccupancyStatus(@Nonnull Trip trip); +} diff --git a/src/main/java/org/opentripplanner/service/realtimevehicles/configure/RealtimeVehicleRepositoryModule.java b/src/main/java/org/opentripplanner/service/realtimevehicles/configure/RealtimeVehicleRepositoryModule.java new file mode 100644 index 00000000000..c247420c301 --- /dev/null +++ b/src/main/java/org/opentripplanner/service/realtimevehicles/configure/RealtimeVehicleRepositoryModule.java @@ -0,0 +1,16 @@ +package org.opentripplanner.service.realtimevehicles.configure; + +import dagger.Binds; +import dagger.Module; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; +import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; + +/** + * The repository is used during application loading phase, so we need to provide + * a module for the repository as well as the service. + */ +@Module +public interface RealtimeVehicleRepositoryModule { + @Binds + RealtimeVehicleRepository bindRepository(DefaultRealtimeVehicleService repository); +} diff --git a/src/main/java/org/opentripplanner/service/realtimevehicles/configure/RealtimeVehicleServiceModule.java b/src/main/java/org/opentripplanner/service/realtimevehicles/configure/RealtimeVehicleServiceModule.java new file mode 100644 index 00000000000..625d3ee9510 --- /dev/null +++ b/src/main/java/org/opentripplanner/service/realtimevehicles/configure/RealtimeVehicleServiceModule.java @@ -0,0 +1,16 @@ +package org.opentripplanner.service.realtimevehicles.configure; + +import dagger.Binds; +import dagger.Module; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; +import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; + +/** + * The service is used during application serve phase, not loading, so we need to provide + * a module for the service without the repository, which is injected from the loading phase. + */ +@Module +public interface RealtimeVehicleServiceModule { + @Binds + RealtimeVehicleService bindService(DefaultRealtimeVehicleService service); +} diff --git a/src/main/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleService.java b/src/main/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleService.java new file mode 100644 index 00000000000..07fa48fa84f --- /dev/null +++ b/src/main/java/org/opentripplanner/service/realtimevehicles/internal/DefaultRealtimeVehicleService.java @@ -0,0 +1,73 @@ +package org.opentripplanner.service.realtimevehicles.internal; + +import static org.opentripplanner.transit.model.timetable.OccupancyStatus.NO_DATA_AVAILABLE; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.time.Instant; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nonnull; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.OccupancyStatus; +import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.service.TransitService; + +@Singleton +public class DefaultRealtimeVehicleService + implements RealtimeVehicleService, RealtimeVehicleRepository { + + private final Map> vehicles = new ConcurrentHashMap<>(); + + private final TransitService transitService; + + @Inject + public DefaultRealtimeVehicleService(TransitService transitService) { + this.transitService = transitService; + } + + @Override + public void setRealtimeVehicles(TripPattern pattern, List updates) { + vehicles.put(pattern, List.copyOf(updates)); + } + + @Override + public void clearRealtimeVehicles(TripPattern pattern) { + vehicles.remove(pattern); + } + + @Override + public List getRealtimeVehicles(@Nonnull TripPattern pattern) { + // the list is made immutable during insertion, so we can safely return them + return vehicles.getOrDefault(pattern, List.of()); + } + + @Nonnull + @Override + public OccupancyStatus getVehicleOccupancyStatus(@Nonnull Trip trip) { + return getOccupancyStatus(trip.getId(), transitService.getPatternForTrip(trip)); + } + + /** + * Get the latest occupancy status for a certain trip. Service contains all the vehicles that + * exist in input feeds but doesn't store any historical data. This method is an alternative to + * {@link #getVehicleOccupancyStatus(Trip)} and works even when {@link TransitService} is not + * provided to the service. + */ + public OccupancyStatus getOccupancyStatus(FeedScopedId tripId, TripPattern pattern) { + return vehicles + .getOrDefault(pattern, List.of()) + .stream() + .filter(vehicle -> tripId.equals(vehicle.trip().getId())) + .max(Comparator.comparing(vehicle -> vehicle.time().orElse(Instant.MIN))) + .flatMap(RealtimeVehicle::occupancyStatus) + .orElse(NO_DATA_AVAILABLE); + } +} diff --git a/src/main/java/org/opentripplanner/service/realtimevehicles/model/RealtimeVehicle.java b/src/main/java/org/opentripplanner/service/realtimevehicles/model/RealtimeVehicle.java new file mode 100644 index 00000000000..e06ae5ebaa7 --- /dev/null +++ b/src/main/java/org/opentripplanner/service/realtimevehicles/model/RealtimeVehicle.java @@ -0,0 +1,114 @@ +package org.opentripplanner.service.realtimevehicles.model; + +import java.time.Instant; +import java.util.Optional; +import javax.annotation.Nonnull; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.OccupancyStatus; +import org.opentripplanner.transit.model.timetable.Trip; + +/** + * Internal model of a realtime vehicle. + */ +public class RealtimeVehicle { + + private final FeedScopedId vehicleId; + + private final String label; + + private final WgsCoordinate coordinates; + + /** + * Speed in meters per second + */ + private final Double speed; + /** + * Bearing, in degrees, clockwise from North, i.e., 0 is North and 90 is East. This can be the + * compass bearing, or the direction towards the next stop or intermediate location. + */ + private final Double heading; + + /** + * When the realtime vehicle was recorded + */ + private final Instant time; + + /** + * Status of the vehicle, ie. if approaching the next stop or if it is there already. + */ + private final StopRelationship stop; + + private final Trip trip; + + /** + * How full the vehicle is and is it still accepting passengers. + */ + private final OccupancyStatus occupancyStatus; + + RealtimeVehicle(RealtimeVehicleBuilder builder) { + var stopRelationship = Optional + .ofNullable(builder.stop()) + .map(s -> new StopRelationship(s, builder.stopStatus())) + .orElse(null); + this.vehicleId = builder.vehicleId(); + this.label = builder.label(); + this.coordinates = builder.coordinates(); + this.speed = builder.speed(); + this.heading = builder.heading(); + this.time = builder.time(); + this.stop = stopRelationship; + this.trip = builder.trip(); + this.occupancyStatus = builder.occupancyStatus(); + } + + public Optional vehicleId() { + return Optional.ofNullable(vehicleId); + } + + public Optional label() { + return Optional.ofNullable(label); + } + + public Optional coordinates() { + return Optional.ofNullable(coordinates); + } + + public Optional speed() { + return Optional.ofNullable(speed); + } + + public Optional heading() { + return Optional.ofNullable(heading); + } + + public Optional time() { + return Optional.ofNullable(time); + } + + public Optional stop() { + return Optional.ofNullable(stop); + } + + @Nonnull + public Trip trip() { + return trip; + } + + public Optional occupancyStatus() { + return Optional.ofNullable(occupancyStatus); + } + + public static RealtimeVehicleBuilder builder() { + return new RealtimeVehicleBuilder(); + } + + public enum StopStatus { + INCOMING_AT, + STOPPED_AT, + IN_TRANSIT_TO, + } + + public record StopRelationship(StopLocation stop, StopStatus status) {} +} diff --git a/src/main/java/org/opentripplanner/service/realtimevehicles/model/RealtimeVehicleBuilder.java b/src/main/java/org/opentripplanner/service/realtimevehicles/model/RealtimeVehicleBuilder.java new file mode 100644 index 00000000000..cc68919f41d --- /dev/null +++ b/src/main/java/org/opentripplanner/service/realtimevehicles/model/RealtimeVehicleBuilder.java @@ -0,0 +1,117 @@ +package org.opentripplanner.service.realtimevehicles.model; + +import java.time.Instant; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle.StopStatus; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.OccupancyStatus; +import org.opentripplanner.transit.model.timetable.Trip; + +public class RealtimeVehicleBuilder { + + private FeedScopedId vehicleId; + private String label; + private WgsCoordinate coordinates; + private Double speed = null; + private Double heading = null; + private Instant time; + private StopStatus stopStatus = StopStatus.IN_TRANSIT_TO; + private StopLocation stop; + private Trip trip; + private OccupancyStatus occupancyStatus; + + public FeedScopedId vehicleId() { + return vehicleId; + } + + public RealtimeVehicleBuilder withVehicleId(FeedScopedId vehicleId) { + this.vehicleId = vehicleId; + return this; + } + + public String label() { + return label; + } + + public RealtimeVehicleBuilder withLabel(String label) { + this.label = label; + return this; + } + + public WgsCoordinate coordinates() { + return coordinates; + } + + public RealtimeVehicleBuilder withCoordinates(WgsCoordinate c) { + this.coordinates = c; + return this; + } + + public Double speed() { + return speed; + } + + public RealtimeVehicleBuilder withSpeed(double speed) { + this.speed = speed; + return this; + } + + public Double heading() { + return heading; + } + + public RealtimeVehicleBuilder withHeading(double heading) { + this.heading = heading; + return this; + } + + public Instant time() { + return time; + } + + public RealtimeVehicleBuilder withTime(Instant time) { + this.time = time; + return this; + } + + public StopStatus stopStatus() { + return stopStatus; + } + + public RealtimeVehicleBuilder withStopStatus(StopStatus stopStatus) { + this.stopStatus = stopStatus; + return this; + } + + public StopLocation stop() { + return stop; + } + + public RealtimeVehicleBuilder withStop(StopLocation stop) { + this.stop = stop; + return this; + } + + public Trip trip() { + return trip; + } + + public RealtimeVehicleBuilder withTrip(Trip trip) { + this.trip = trip; + return this; + } + + public OccupancyStatus occupancyStatus() { + return occupancyStatus; + } + + public RealtimeVehicleBuilder withOccupancyStatus(OccupancyStatus occupancyStatus) { + this.occupancyStatus = occupancyStatus; + return this; + } + + public RealtimeVehicle build() { + return new RealtimeVehicle(this); + } +} diff --git a/src/main/java/org/opentripplanner/service/vehiclepositions/VehiclePositionRepository.java b/src/main/java/org/opentripplanner/service/vehiclepositions/VehiclePositionRepository.java deleted file mode 100644 index a28e316ac37..00000000000 --- a/src/main/java/org/opentripplanner/service/vehiclepositions/VehiclePositionRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.opentripplanner.service.vehiclepositions; - -import java.util.List; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition; -import org.opentripplanner.transit.model.network.TripPattern; - -public interface VehiclePositionRepository { - /** - * For the given pattern set all realtime vehicle positions. - *

- * The list is expected to be exhaustive: all existing positions will be overridden. - *

- * This means that if there are two updaters providing positions for the same pattern they - * overwrite each other. - */ - void setVehiclePositions(TripPattern pattern, List updates); - - /** - * Remove all vehicle positions for a given pattern. - *

- * This is useful to clear old vehicles for which there are no more updates and we assume that - * they have stopped their trip. - */ - void clearVehiclePositions(TripPattern pattern); - /** - * Get the vehicle positions for a certain trip. - */ - List getVehiclePositions(TripPattern pattern); -} diff --git a/src/main/java/org/opentripplanner/service/vehiclepositions/VehiclePositionService.java b/src/main/java/org/opentripplanner/service/vehiclepositions/VehiclePositionService.java deleted file mode 100644 index b4cbf1176ea..00000000000 --- a/src/main/java/org/opentripplanner/service/vehiclepositions/VehiclePositionService.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.opentripplanner.service.vehiclepositions; - -import java.util.List; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition; -import org.opentripplanner.transit.model.network.TripPattern; - -public interface VehiclePositionService { - /** - * Get the vehicle positions for a certain trip. - */ - List getVehiclePositions(TripPattern pattern); -} diff --git a/src/main/java/org/opentripplanner/service/vehiclepositions/configure/VehiclePositionsRepositoryModule.java b/src/main/java/org/opentripplanner/service/vehiclepositions/configure/VehiclePositionsRepositoryModule.java deleted file mode 100644 index d4820e1fe31..00000000000 --- a/src/main/java/org/opentripplanner/service/vehiclepositions/configure/VehiclePositionsRepositoryModule.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.opentripplanner.service.vehiclepositions.configure; - -import dagger.Binds; -import dagger.Module; -import org.opentripplanner.service.vehiclepositions.VehiclePositionRepository; -import org.opentripplanner.service.vehiclepositions.internal.DefaultVehiclePositionService; - -/** - * The repository is used during application loading phase, so we need to provide - * a module for the repository as well as the service. - */ -@Module -public interface VehiclePositionsRepositoryModule { - @Binds - VehiclePositionRepository bindRepository(DefaultVehiclePositionService repository); -} diff --git a/src/main/java/org/opentripplanner/service/vehiclepositions/configure/VehiclePositionsServiceModule.java b/src/main/java/org/opentripplanner/service/vehiclepositions/configure/VehiclePositionsServiceModule.java deleted file mode 100644 index a8cfcbfbe4c..00000000000 --- a/src/main/java/org/opentripplanner/service/vehiclepositions/configure/VehiclePositionsServiceModule.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.opentripplanner.service.vehiclepositions.configure; - -import dagger.Binds; -import dagger.Module; -import org.opentripplanner.service.vehiclepositions.VehiclePositionService; -import org.opentripplanner.service.vehiclepositions.internal.DefaultVehiclePositionService; - -/** - * The service is used during application serve phase, not loading, so we need to provide - * a module for the service without the repository, which is injected from the loading phase. - */ -@Module -public interface VehiclePositionsServiceModule { - @Binds - VehiclePositionService bindService(DefaultVehiclePositionService service); -} diff --git a/src/main/java/org/opentripplanner/service/vehiclepositions/internal/DefaultVehiclePositionService.java b/src/main/java/org/opentripplanner/service/vehiclepositions/internal/DefaultVehiclePositionService.java deleted file mode 100644 index a9027f044d2..00000000000 --- a/src/main/java/org/opentripplanner/service/vehiclepositions/internal/DefaultVehiclePositionService.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.opentripplanner.service.vehiclepositions.internal; - -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.opentripplanner.service.vehiclepositions.VehiclePositionRepository; -import org.opentripplanner.service.vehiclepositions.VehiclePositionService; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition; -import org.opentripplanner.transit.model.network.TripPattern; - -@Singleton -public class DefaultVehiclePositionService - implements VehiclePositionService, VehiclePositionRepository { - - private final Map> positions = new ConcurrentHashMap<>(); - - @Inject - public DefaultVehiclePositionService() {} - - @Override - public void setVehiclePositions(TripPattern pattern, List updates) { - positions.put(pattern, List.copyOf(updates)); - } - - @Override - public void clearVehiclePositions(TripPattern pattern) { - positions.remove(pattern); - } - - @Override - public List getVehiclePositions(TripPattern pattern) { - // the list is made immutable during insertion, so we can safely return them - return positions.getOrDefault(pattern, List.of()); - } -} diff --git a/src/main/java/org/opentripplanner/service/vehiclepositions/model/RealtimeVehiclePosition.java b/src/main/java/org/opentripplanner/service/vehiclepositions/model/RealtimeVehiclePosition.java deleted file mode 100644 index 56c99c445c1..00000000000 --- a/src/main/java/org/opentripplanner/service/vehiclepositions/model/RealtimeVehiclePosition.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.opentripplanner.service.vehiclepositions.model; - -import java.time.Instant; -import org.opentripplanner.framework.geometry.WgsCoordinate; -import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.site.StopLocation; -import org.opentripplanner.transit.model.timetable.Trip; - -/** - * Internal model of a realtime vehicle position. - */ -public record RealtimeVehiclePosition( - FeedScopedId vehicleId, - String label, - WgsCoordinate coordinates, - /** - * Speed in meters per second - */ - Double speed, - /** - * Bearing, in degrees, clockwise from North, i.e., 0 is North and 90 is East. This can be the - * compass bearing, or the direction towards the next stop or intermediate location. - */ - Double heading, - - /** - * When the realtime position was recorded - */ - Instant time, - - /** - * Status of the vehicle, ie. if approaching the next stop or if it is there already. - */ - StopRelationship stop, - Trip trip -) { - public static RealtimeVehiclePositionBuilder builder() { - return new RealtimeVehiclePositionBuilder(); - } - - public enum StopStatus { - INCOMING_AT, - STOPPED_AT, - IN_TRANSIT_TO, - } - - public record StopRelationship(StopLocation stop, StopStatus status) {} -} diff --git a/src/main/java/org/opentripplanner/service/vehiclepositions/model/RealtimeVehiclePositionBuilder.java b/src/main/java/org/opentripplanner/service/vehiclepositions/model/RealtimeVehiclePositionBuilder.java deleted file mode 100644 index 00f2c3f7b51..00000000000 --- a/src/main/java/org/opentripplanner/service/vehiclepositions/model/RealtimeVehiclePositionBuilder.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.opentripplanner.service.vehiclepositions.model; - -import java.time.Instant; -import java.util.Optional; -import org.opentripplanner.framework.geometry.WgsCoordinate; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition.StopRelationship; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition.StopStatus; -import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.model.site.StopLocation; -import org.opentripplanner.transit.model.timetable.Trip; - -public class RealtimeVehiclePositionBuilder { - - private FeedScopedId vehicleId; - private String label; - private WgsCoordinate coordinates; - private Double speed = null; - private Double heading = null; - private Instant time; - private StopStatus stopStatus = StopStatus.IN_TRANSIT_TO; - private StopLocation stop; - private Trip trip; - - public RealtimeVehiclePositionBuilder setVehicleId(FeedScopedId vehicleId) { - this.vehicleId = vehicleId; - return this; - } - - public RealtimeVehiclePositionBuilder setLabel(String label) { - this.label = label; - return this; - } - - public RealtimeVehiclePositionBuilder setCoordinates(WgsCoordinate c) { - this.coordinates = c; - return this; - } - - public RealtimeVehiclePositionBuilder setSpeed(double speed) { - this.speed = speed; - return this; - } - - public RealtimeVehiclePositionBuilder setHeading(double heading) { - this.heading = heading; - return this; - } - - public RealtimeVehiclePositionBuilder setTime(Instant time) { - this.time = time; - return this; - } - - public RealtimeVehiclePositionBuilder setStopStatus(StopStatus stopStatus) { - this.stopStatus = stopStatus; - return this; - } - - public RealtimeVehiclePositionBuilder setStop(StopLocation stop) { - this.stop = stop; - return this; - } - - public RealtimeVehiclePositionBuilder setTrip(Trip trip) { - this.trip = trip; - return this; - } - - public RealtimeVehiclePosition build() { - var stop = Optional - .ofNullable(this.stop) - .map(s -> new StopRelationship(s, stopStatus)) - .orElse(null); - return new RealtimeVehiclePosition( - vehicleId, - label, - coordinates, - speed, - heading, - time, - stop, - trip - ); - } -} diff --git a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index a7effd16704..5c4b6ad0c1e 100644 --- a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -17,7 +17,7 @@ import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graphfinder.GraphFinder; -import org.opentripplanner.service.vehiclepositions.VehiclePositionService; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; import org.opentripplanner.standalone.config.sandbox.FlexConfig; @@ -82,7 +82,7 @@ public interface OtpServerRequestContext { */ WorldEnvelopeService worldEnvelopeService(); - VehiclePositionService vehiclePositionService(); + RealtimeVehicleService realtimeVehicleService(); VehicleRentalService vehicleRentalService(); diff --git a/src/main/java/org/opentripplanner/standalone/config/framework/json/OtpVersion.java b/src/main/java/org/opentripplanner/standalone/config/framework/json/OtpVersion.java index 9cfb7237c66..6caf9082cff 100644 --- a/src/main/java/org/opentripplanner/standalone/config/framework/json/OtpVersion.java +++ b/src/main/java/org/opentripplanner/standalone/config/framework/json/OtpVersion.java @@ -9,7 +9,8 @@ public enum OtpVersion { V2_1("2.1"), V2_2("2.2"), V2_3("2.3"), - V2_4("2.4"); + V2_4("2.4"), + V2_5("2.5"); private final String text; diff --git a/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java b/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java index 3bc66ce6ea5..0302fce4d3a 100644 --- a/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java +++ b/src/main/java/org/opentripplanner/standalone/config/framework/json/ParameterBuilder.java @@ -4,6 +4,7 @@ import static org.opentripplanner.standalone.config.framework.json.ConfigType.COST_LINEAR_FUNCTION; import static org.opentripplanner.standalone.config.framework.json.ConfigType.DOUBLE; import static org.opentripplanner.standalone.config.framework.json.ConfigType.DURATION; +import static org.opentripplanner.standalone.config.framework.json.ConfigType.ENUM; import static org.opentripplanner.standalone.config.framework.json.ConfigType.FEED_SCOPED_ID; import static org.opentripplanner.standalone.config.framework.json.ConfigType.INTEGER; import static org.opentripplanner.standalone.config.framework.json.ConfigType.LOCALE; @@ -233,7 +234,22 @@ public > Set asEnumSet(Class enumClass) { it -> parseOptionalEnum(it.asText(), enumClass) ); List result = optionalList.stream().filter(Optional::isPresent).map(Optional::get).toList(); - return result.isEmpty() ? EnumSet.noneOf(enumClass) : EnumSet.copyOf(result); + // Set is immutable + return result.isEmpty() ? Set.of() : Set.copyOf(result); + } + + public > Set asEnumSet(Class enumClass, Collection defaultValues) { + List dft = (defaultValues instanceof List) + ? (List) defaultValues + : List.copyOf(defaultValues); + info.withOptional(dft.toString()).withEnumSet(enumClass); + List> optionalList = buildAndListSimpleArrayElements( + List.of(), + it -> parseOptionalEnum(it.asText(), enumClass) + ); + List result = optionalList.stream().filter(Optional::isPresent).map(Optional::get).toList(); + // Set is immutable + return result.isEmpty() ? Set.copyOf(dft) : Set.copyOf(result); } /** diff --git a/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/VehiclePositionsUpdaterConfig.java b/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/VehiclePositionsUpdaterConfig.java index 4090da7991a..502f0ea456e 100644 --- a/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/VehiclePositionsUpdaterConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/routerconfig/updaters/VehiclePositionsUpdaterConfig.java @@ -2,8 +2,13 @@ import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_2; import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_3; +import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_5; +import static org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig.VehiclePositionFeature.OCCUPANCY; +import static org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig.VehiclePositionFeature.POSITION; +import static org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig.VehiclePositionFeature.STOP_POSITION; import java.time.Duration; +import java.util.List; import org.opentripplanner.standalone.config.framework.json.NodeAdapter; import org.opentripplanner.updater.vehicle_position.VehiclePositionsUpdaterParameters; @@ -25,7 +30,31 @@ public static VehiclePositionsUpdaterParameters create(String updaterRef, NodeAd .since(V2_2) .summary("The URL of GTFS-RT protobuf HTTP resource to download the positions from.") .asUri(); + var fuzzyTripMatching = c + .of("fuzzyTripMatching") + .since(V2_5) + .summary("Whether to match trips fuzzily.") + .asBoolean(false); + var features = c + .of("features") + .since(V2_5) + .summary("Which features of GTFS RT vehicle positions should be loaded into OTP.") + .asEnumSet(VehiclePositionFeature.class, List.of(POSITION, STOP_POSITION, OCCUPANCY)); var headers = HttpHeadersConfig.headers(c, V2_3); - return new VehiclePositionsUpdaterParameters(updaterRef, feedId, url, frequency, headers); + return new VehiclePositionsUpdaterParameters( + updaterRef, + feedId, + url, + frequency, + headers, + fuzzyTripMatching, + features + ); + } + + public enum VehiclePositionFeature { + POSITION, + STOP_POSITION, + OCCUPANCY, } } diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java index ee71aa3f542..e60fd159332 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplication.java @@ -18,7 +18,7 @@ import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerMapper; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.TransitLayerUpdater; import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.service.vehiclepositions.VehiclePositionRepository; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; import org.opentripplanner.service.vehiclerental.VehicleRentalRepository; import org.opentripplanner.service.worldenvelope.WorldEnvelopeRepository; import org.opentripplanner.standalone.api.OtpServerRequestContext; @@ -149,7 +149,7 @@ private void setupTransitRoutingServer() { /* Create updater modules from JSON config. */ UpdaterConfigurator.configure( graph(), - vehiclePositionRepository(), + realtimeVehicleRepository(), vehicleRentalRepository(), transitModel(), routerConfig().updaterConfig() @@ -239,8 +239,8 @@ public DataImportIssueSummary dataImportIssueSummary() { return factory.dataImportIssueSummary(); } - public VehiclePositionRepository vehiclePositionRepository() { - return factory.vehiclePositionRepository(); + public RealtimeVehicleRepository realtimeVehicleRepository() { + return factory.realtimeVehicleRepository(); } public VehicleRentalRepository vehicleRentalRepository() { diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java index f882bad3ffd..b776df1ae5d 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationFactory.java @@ -9,10 +9,10 @@ import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.service.vehiclepositions.VehiclePositionRepository; -import org.opentripplanner.service.vehiclepositions.VehiclePositionService; -import org.opentripplanner.service.vehiclepositions.configure.VehiclePositionsRepositoryModule; -import org.opentripplanner.service.vehiclepositions.configure.VehiclePositionsServiceModule; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; +import org.opentripplanner.service.realtimevehicles.configure.RealtimeVehicleRepositoryModule; +import org.opentripplanner.service.realtimevehicles.configure.RealtimeVehicleServiceModule; import org.opentripplanner.service.vehiclerental.VehicleRentalRepository; import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.service.vehiclerental.configure.VehicleRentalRepositoryModule; @@ -38,8 +38,8 @@ ConfigModule.class, TransitModule.class, WorldEnvelopeServiceModule.class, - VehiclePositionsServiceModule.class, - VehiclePositionsRepositoryModule.class, + RealtimeVehicleServiceModule.class, + RealtimeVehicleRepositoryModule.class, VehicleRentalServiceModule.class, VehicleRentalRepositoryModule.class, ConstructApplicationModule.class, @@ -53,8 +53,8 @@ public interface ConstructApplicationFactory { TransitModel transitModel(); WorldEnvelopeRepository worldEnvelopeRepository(); WorldEnvelopeService worldEnvelopeService(); - VehiclePositionRepository vehiclePositionRepository(); - VehiclePositionService vehiclePositionService(); + RealtimeVehicleRepository realtimeVehicleRepository(); + RealtimeVehicleService realtimeVehicleService(); VehicleRentalRepository vehicleRentalRepository(); VehicleRentalService vehicleRentalService(); DataImportIssueSummary dataImportIssueSummary(); diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java index bc677028296..3b43644e24a 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java @@ -10,7 +10,7 @@ import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.service.vehiclepositions.VehiclePositionService; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; import org.opentripplanner.standalone.api.OtpServerRequestContext; @@ -29,7 +29,7 @@ OtpServerRequestContext providesServerContext( Graph graph, TransitService transitService, WorldEnvelopeService worldEnvelopeService, - VehiclePositionService vehiclePositionService, + RealtimeVehicleService realtimeVehicleService, VehicleRentalService vehicleRentalService, List rideHailingServices, @Nullable TraverseVisitor traverseVisitor @@ -43,7 +43,7 @@ OtpServerRequestContext providesServerContext( Metrics.globalRegistry, routerConfig.vectorTileLayers(), worldEnvelopeService, - vehiclePositionService, + realtimeVehicleService, vehicleRentalService, routerConfig.flexConfig(), rideHailingServices, diff --git a/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java index dad75d1f6f6..62b848fd5e9 100644 --- a/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java @@ -16,7 +16,7 @@ import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.service.DefaultRoutingService; -import org.opentripplanner.service.vehiclepositions.VehiclePositionService; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; import org.opentripplanner.standalone.api.HttpRequestScoped; @@ -41,7 +41,7 @@ public class DefaultServerRequestContext implements OtpServerRequestContext { private final FlexConfig flexConfig; private final TraverseVisitor traverseVisitor; private final WorldEnvelopeService worldEnvelopeService; - private final VehiclePositionService vehiclePositionService; + private final RealtimeVehicleService realtimeVehicleService; private final VehicleRentalService vehicleRentalService; /** @@ -57,7 +57,7 @@ private DefaultServerRequestContext( TileRendererManager tileRendererManager, VectorTilesResource.LayersParameters vectorTileLayers, WorldEnvelopeService worldEnvelopeService, - VehiclePositionService vehiclePositionService, + RealtimeVehicleService realtimeVehicleService, VehicleRentalService vehicleRentalService, List rideHailingServices, TraverseVisitor traverseVisitor, @@ -75,7 +75,7 @@ private DefaultServerRequestContext( this.traverseVisitor = traverseVisitor; this.routeRequestDefaults = routeRequestDefaults; this.worldEnvelopeService = worldEnvelopeService; - this.vehiclePositionService = vehiclePositionService; + this.realtimeVehicleService = realtimeVehicleService; this.rideHailingServices = rideHailingServices; } @@ -91,7 +91,7 @@ public static DefaultServerRequestContext create( MeterRegistry meterRegistry, VectorTilesResource.LayersParameters vectorTileLayers, WorldEnvelopeService worldEnvelopeService, - VehiclePositionService vehiclePositionService, + RealtimeVehicleService realtimeVehicleService, VehicleRentalService vehicleRentalService, FlexConfig flexConfig, List rideHailingServices, @@ -107,7 +107,7 @@ public static DefaultServerRequestContext create( new TileRendererManager(graph, routeRequestDefaults.preferences()), vectorTileLayers, worldEnvelopeService, - vehiclePositionService, + realtimeVehicleService, vehicleRentalService, rideHailingServices, traverseVisitor, @@ -158,8 +158,8 @@ public WorldEnvelopeService worldEnvelopeService() { } @Override - public VehiclePositionService vehiclePositionService() { - return vehiclePositionService; + public RealtimeVehicleService realtimeVehicleService() { + return realtimeVehicleService; } @Override diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/OccupancyStatus.java b/src/main/java/org/opentripplanner/transit/model/timetable/OccupancyStatus.java index 8be2f468b46..9c32ca39394 100644 --- a/src/main/java/org/opentripplanner/transit/model/timetable/OccupancyStatus.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/OccupancyStatus.java @@ -1,35 +1,67 @@ package org.opentripplanner.transit.model.timetable; +import org.opentripplanner.framework.doc.DocumentedEnum; + /** * OccupancyStatus to be exposed in the API. The values are based on GTFS-RT - * (transit_realtime.VehiclePosition.OccupancyStatus), but is currently a subset that easily can - * be mapped to the Nordic SIRI-profile (SIRI 2.1) - * - * Descriptions are also based on the SIRI-profile + * (transit_realtime.VehiclePosition.OccupancyStatus) that can be easily be mapped to the Nordic + * SIRI-profile (SIRI 2.1) + *

+ * Descriptions are copied from the GTFS-RT specification with additions of SIRI nordic profile documentation. */ -public enum OccupancyStatus { - /** - * Default. There is no occupancy-data on this departure - */ - NO_DATA, - /** - * More than ~50% of seats available - */ +public enum OccupancyStatus implements DocumentedEnum { + NO_DATA_AVAILABLE, + EMPTY, MANY_SEATS_AVAILABLE, - /** - * Less than ~50% of seats available - */ - SEATS_AVAILABLE, - /** - * Less than ~10% of seats available - */ + FEW_SEATS_AVAILABLE, STANDING_ROOM_ONLY, - /** - * Close to or at full capacity - */ + CRUSHED_STANDING_ROOM_ONLY, FULL, - /** - * If vehicle/carriage is not in use / unavailable, or passengers are only allowed to alight due to e.g. crowding - */ - NOT_ACCEPTING_PASSENGERS, + NOT_ACCEPTING_PASSENGERS; + + @Override + public String typeDescription() { + return "OccupancyStatus to be exposed in the API. The values are based on GTFS-RT"; + } + + @Override + public String enumValueDescription() { + return switch (this) { + case NO_DATA_AVAILABLE -> "The vehicle or carriage doesn't have any occupancy data available."; + case EMPTY -> """ + The vehicle is considered empty by most measures, and has few or no passengers onboard, but is + still accepting passengers. There isn't a big difference between this and `manySeatsAvailable` + so it's possible to handle them as the same value, if one wants to limit the number of different + values. + SIRI nordic profile: merge these into `manySeatsAvailable`. + """; + case MANY_SEATS_AVAILABLE -> """ + The vehicle or carriage has a large number of seats available. + SIRI nordic profile: more than ~50% of seats available. + """; + case FEW_SEATS_AVAILABLE -> """ + The vehicle or carriage has a few seats available. + SIRI nordic profile: less than ~50% of seats available. + """; + case STANDING_ROOM_ONLY -> """ + The vehicle or carriage only has standing room available. + SIRI nordic profile: less than ~10% of seats available. + """; + case CRUSHED_STANDING_ROOM_ONLY -> """ + The vehicle or carriage can currently accommodate only standing passengers and has limited + space for them. There isn't a big difference between this and `full` so it's possible to + handle them as the same value, if one wants to limit the number of different values. + SIRI nordic profile: merge into `standingRoomOnly`. + """; + case FULL -> """ + The vehicle or carriage is considered full by most measures, but may still be allowing + passengers to board. + """; + case NOT_ACCEPTING_PASSENGERS -> """ + The vehicle or carriage has no seats or standing room available. + SIRI nordic profile: if vehicle/carriage is not in use / unavailable, or passengers are only + allowed to alight due to e.g. crowding. + """; + }; + } } diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java b/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java index 8b86a3b37b3..c6b4ea51b0f 100644 --- a/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java @@ -324,7 +324,7 @@ public void setOccupancyStatus(int stop, OccupancyStatus occupancyStatus) { public OccupancyStatus getOccupancyStatus(int stop) { if (this.occupancyStatus == null) { - return OccupancyStatus.NO_DATA; + return OccupancyStatus.NO_DATA_AVAILABLE; } return this.occupancyStatus[stop]; } @@ -680,7 +680,7 @@ private void prepareForRealTimeUpdates() { arrivalTimes[i] += timeShift; departureTimes[i] += timeShift; stopRealTimeStates[i] = StopRealTimeState.DEFAULT; - occupancyStatus[i] = OccupancyStatus.NO_DATA; + occupancyStatus[i] = OccupancyStatus.NO_DATA_AVAILABLE; } // Update the real-time state diff --git a/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java b/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java index ea63561ccb3..037bd080f47 100644 --- a/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java +++ b/src/main/java/org/opentripplanner/updater/configure/UpdaterConfigurator.java @@ -13,7 +13,7 @@ import org.opentripplanner.framework.io.OtpHttpClient; import org.opentripplanner.model.calendar.openinghours.OpeningHoursCalendarService; import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.service.vehiclepositions.VehiclePositionRepository; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; import org.opentripplanner.service.vehiclerental.VehicleRentalRepository; import org.opentripplanner.transit.service.TransitModel; import org.opentripplanner.updater.GraphUpdaterManager; @@ -46,20 +46,20 @@ public class UpdaterConfigurator { private final Graph graph; private final TransitModel transitModel; private final UpdatersParameters updatersParameters; - private final VehiclePositionRepository vehiclePositionRepository; + private final RealtimeVehicleRepository realtimeVehicleRepository; private final VehicleRentalRepository vehicleRentalRepository; private SiriTimetableSnapshotSource siriTimetableSnapshotSource = null; private TimetableSnapshotSource gtfsTimetableSnapshotSource = null; private UpdaterConfigurator( Graph graph, - VehiclePositionRepository vehiclePositionRepository, + RealtimeVehicleRepository realtimeVehicleRepository, VehicleRentalRepository vehicleRentalRepository, TransitModel transitModel, UpdatersParameters updatersParameters ) { this.graph = graph; - this.vehiclePositionRepository = vehiclePositionRepository; + this.realtimeVehicleRepository = realtimeVehicleRepository; this.vehicleRentalRepository = vehicleRentalRepository; this.transitModel = transitModel; this.updatersParameters = updatersParameters; @@ -67,15 +67,15 @@ private UpdaterConfigurator( public static void configure( Graph graph, - VehiclePositionRepository vehiclePositionService, - VehicleRentalRepository vehicleRentalService, + RealtimeVehicleRepository realtimeVehicleRepository, + VehicleRentalRepository vehicleRentalRepository, TransitModel transitModel, UpdatersParameters updatersParameters ) { new UpdaterConfigurator( graph, - vehiclePositionService, - vehicleRentalService, + realtimeVehicleRepository, + vehicleRentalRepository, transitModel, updatersParameters ) @@ -163,7 +163,7 @@ private List createUpdatersFromConfig() { } for (var configItem : updatersParameters.getVehiclePositionsUpdaterParameters()) { updaters.add( - new PollingVehiclePositionUpdater(configItem, vehiclePositionRepository, transitModel) + new PollingVehiclePositionUpdater(configItem, realtimeVehicleRepository, transitModel) ); } for (var configItem : updatersParameters.getSiriETUpdaterParameters()) { diff --git a/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java b/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java index 31d94caeb00..bb2e27c3ce8 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_position/PollingVehiclePositionUpdater.java @@ -5,17 +5,21 @@ import java.util.List; import java.util.Optional; import org.opentripplanner.framework.tostring.ToStringBuilder; -import org.opentripplanner.service.vehiclepositions.VehiclePositionRepository; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.updater.GtfsRealtimeFuzzyTripMatcher; import org.opentripplanner.updater.spi.PollingGraphUpdater; import org.opentripplanner.updater.spi.WriteToGraphCallback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Add vehicle positions to OTP patterns via a GTFS-RT source. + * Map vehicle positions to + * {@link org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle} and add them to OTP + * patterns via a GTFS-RT source. */ public class PollingVehiclePositionUpdater extends PollingGraphUpdater { @@ -26,7 +30,7 @@ public class PollingVehiclePositionUpdater extends PollingGraphUpdater { */ private final GtfsRealtimeHttpVehiclePositionSource vehiclePositionSource; - private final VehiclePositionPatternMatcher vehiclePositionPatternMatcher; + private final RealtimeVehiclePatternMatcher realtimeVehiclePatternMatcher; /** * Parent update manager. Is used to execute graph writer runnables. @@ -35,21 +39,26 @@ public class PollingVehiclePositionUpdater extends PollingGraphUpdater { public PollingVehiclePositionUpdater( VehiclePositionsUpdaterParameters params, - VehiclePositionRepository vehiclePositionService, + RealtimeVehicleRepository realtimeVehicleRepository, TransitModel transitModel ) { super(params); this.vehiclePositionSource = new GtfsRealtimeHttpVehiclePositionSource(params.url(), params.headers()); var index = transitModel.getTransitModelIndex(); - this.vehiclePositionPatternMatcher = - new VehiclePositionPatternMatcher( + var fuzzyTripMatcher = params.fuzzyTripMatching() + ? new GtfsRealtimeFuzzyTripMatcher(new DefaultTransitService(transitModel)) + : null; + this.realtimeVehiclePatternMatcher = + new RealtimeVehiclePatternMatcher( params.feedId(), tripId -> index.getTripForId().get(tripId), trip -> index.getPatternForTrip().get(trip), (trip, date) -> getPatternIncludingRealtime(transitModel, trip, date), - vehiclePositionService, - transitModel.getTimeZone() + realtimeVehicleRepository, + transitModel.getTimeZone(), + fuzzyTripMatcher, + params.vehiclePositionFeatures() ); LOG.info( @@ -75,7 +84,7 @@ public void runPolling() { if (updates != null) { // Handle updating trip positions via graph writer runnable - var runnable = new VehiclePositionUpdaterRunnable(updates, vehiclePositionPatternMatcher); + var runnable = new VehiclePositionUpdaterRunnable(updates, realtimeVehiclePatternMatcher); saveResultOnGraph.execute(runnable); } } diff --git a/src/main/java/org/opentripplanner/updater/vehicle_position/VehiclePositionPatternMatcher.java b/src/main/java/org/opentripplanner/updater/vehicle_position/RealtimeVehiclePatternMatcher.java similarity index 61% rename from src/main/java/org/opentripplanner/updater/vehicle_position/VehiclePositionPatternMatcher.java rename to src/main/java/org/opentripplanner/updater/vehicle_position/RealtimeVehiclePatternMatcher.java index 1fb05c9dcec..fe29ef35b97 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_position/VehiclePositionPatternMatcher.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_position/RealtimeVehiclePatternMatcher.java @@ -1,5 +1,8 @@ package org.opentripplanner.updater.vehicle_position; +import static org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig.VehiclePositionFeature.OCCUPANCY; +import static org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig.VehiclePositionFeature.POSITION; +import static org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig.VehiclePositionFeature.STOP_POSITION; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.INVALID_INPUT_STRUCTURE; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.NO_SERVICE_ON_DATE; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.TRIP_NOT_FOUND; @@ -30,15 +33,18 @@ import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.lang.StringUtils; import org.opentripplanner.framework.time.ServiceDateUtils; -import org.opentripplanner.service.vehiclepositions.VehiclePositionRepository; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition; -import org.opentripplanner.service.vehiclepositions.model.RealtimeVehiclePosition.StopStatus; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleRepository; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle.StopStatus; +import org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.framework.Result; import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.OccupancyStatus; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.updater.GtfsRealtimeFuzzyTripMatcher; import org.opentripplanner.updater.spi.ResultLogger; import org.opentripplanner.updater.spi.UpdateError; import org.opentripplanner.updater.spi.UpdateResult; @@ -50,27 +56,31 @@ * Responsible for converting vehicle positions in memory to exportable ones, and associating each * position with a pattern. */ -public class VehiclePositionPatternMatcher { +public class RealtimeVehiclePatternMatcher { - private static final Logger LOG = LoggerFactory.getLogger(VehiclePositionPatternMatcher.class); + private static final Logger LOG = LoggerFactory.getLogger(RealtimeVehiclePatternMatcher.class); private final String feedId; - private final VehiclePositionRepository repository; + private final RealtimeVehicleRepository repository; private final ZoneId timeZoneId; private final Function getTripForId; private final Function getStaticPattern; private final BiFunction getRealtimePattern; + private final GtfsRealtimeFuzzyTripMatcher fuzzyTripMatcher; + private final Set vehiclePositionFeatures; private Set patternsInPreviousUpdate = Set.of(); - public VehiclePositionPatternMatcher( + public RealtimeVehiclePatternMatcher( String feedId, Function getTripForId, Function getStaticPattern, BiFunction getRealtimePattern, - VehiclePositionRepository repository, - ZoneId timeZoneId + RealtimeVehicleRepository repository, + ZoneId timeZoneId, + GtfsRealtimeFuzzyTripMatcher fuzzyTripMatcher, + Set vehiclePositionFeatures ) { this.feedId = feedId; this.getTripForId = getTripForId; @@ -78,28 +88,30 @@ public VehiclePositionPatternMatcher( this.getRealtimePattern = getRealtimePattern; this.repository = repository; this.timeZoneId = timeZoneId; + this.fuzzyTripMatcher = fuzzyTripMatcher; + this.vehiclePositionFeatures = vehiclePositionFeatures; } /** - * Attempts to match each vehicle position to a pattern, then adds each to a pattern + * Attempts to match each vehicle to a pattern, then adds each to a pattern * * @param vehiclePositions List of vehicle positions to match to patterns */ - public UpdateResult applyVehiclePositionUpdates(List vehiclePositions) { + public UpdateResult applyRealtimeVehicleUpdates(List vehiclePositions) { var matchResults = vehiclePositions .stream() - .map(vehiclePosition -> toRealtimeVehiclePosition(feedId, vehiclePosition)) + .map(vehiclePosition -> toRealtimeVehicle(feedId, vehiclePosition)) .toList(); - // we take the list of positions and out of them create a Map> - // that map makes it very easy to update the positions in the service - // it also enables the bookkeeping about which pattern previously had positions but no longer do + // we take the list of vehicles and out of them create a Map> + // that map makes it very easy to update the vehicles in the service + // it also enables the bookkeeping about which pattern previously had vehicles but no longer do // these need to be removed from the service as we assume that the vehicle has stopped - var positions = matchResults + var vehicles = matchResults .stream() .filter(Result::isSuccess) .map(Result::successValue) - .collect(Collectors.groupingBy(PatternAndVehiclePosition::pattern)) + .collect(Collectors.groupingBy(PatternAndRealtimeVehicle::pattern)) .entrySet() .stream() .collect( @@ -109,18 +121,18 @@ public UpdateResult applyVehiclePositionUpdates(List vehiclePos e .getValue() .stream() - .map(PatternAndVehiclePosition::position) + .map(PatternAndRealtimeVehicle::vehicle) .collect(Collectors.toList()) ) ); - positions.forEach(repository::setVehiclePositions); - Set patternsInCurrentUpdate = positions.keySet(); + vehicles.forEach(repository::setRealtimeVehicles); + Set patternsInCurrentUpdate = vehicles.keySet(); - // if there was a position in the previous update but not in the current one, we assume - // that the pattern has no more vehicle positions. + // if there was a vehicle in the previous update but not in the current one, we assume + // that the pattern has no more vehicles. var toDelete = Sets.difference(patternsInPreviousUpdate, patternsInCurrentUpdate); - toDelete.forEach(repository::clearVehiclePositions); + toDelete.forEach(repository::clearRealtimeVehicles); patternsInPreviousUpdate = patternsInCurrentUpdate; if (!vehiclePositions.isEmpty() && patternsInCurrentUpdate.isEmpty()) { @@ -185,81 +197,87 @@ protected static LocalDate inferServiceDate( } /** - * Converts GtfsRealtime vehicle position to the OTP RealtimeVehiclePosition which can be used by + * Converts GtfsRealtime vehicle position to the OTP RealtimeVehicle which can be used by * the API. * * @param stopIndexOfGtfsSequence A function that takes a GTFS stop_sequence and returns the index * of the stop in the trip. */ - private RealtimeVehiclePosition mapVehiclePosition( + private RealtimeVehicle mapRealtimeVehicle( VehiclePosition vehiclePosition, List stopsOnVehicleTrip, @Nonnull Trip trip, @Nonnull Function stopIndexOfGtfsSequence ) { - var newPosition = RealtimeVehiclePosition.builder(); + var newVehicle = RealtimeVehicle.builder(); - if (vehiclePosition.hasPosition()) { + if (vehiclePositionFeatures.contains(POSITION) && vehiclePosition.hasPosition()) { var position = vehiclePosition.getPosition(); - newPosition.setCoordinates( + newVehicle.withCoordinates( new WgsCoordinate(position.getLatitude(), position.getLongitude()) ); if (position.hasSpeed()) { - newPosition.setSpeed(position.getSpeed()); + newVehicle.withSpeed(position.getSpeed()); } if (position.hasBearing()) { - newPosition.setHeading(position.getBearing()); + newVehicle.withHeading(position.getBearing()); } } if (vehiclePosition.hasVehicle()) { var vehicle = vehiclePosition.getVehicle(); var id = new FeedScopedId(feedId, vehicle.getId()); - newPosition - .setVehicleId(id) - .setLabel(Optional.ofNullable(vehicle.getLabel()).orElse(vehicle.getLicensePlate())); + newVehicle + .withVehicleId(id) + .withLabel(Optional.ofNullable(vehicle.getLabel()).orElse(vehicle.getLicensePlate())); } if (vehiclePosition.hasTimestamp()) { - newPosition.setTime(Instant.ofEpochSecond(vehiclePosition.getTimestamp())); + newVehicle.withTime(Instant.ofEpochSecond(vehiclePosition.getTimestamp())); } - if (vehiclePosition.hasCurrentStatus()) { - newPosition.setStopStatus(toModel(vehiclePosition.getCurrentStatus())); - } + if (vehiclePositionFeatures.contains(STOP_POSITION)) { + if (vehiclePosition.hasCurrentStatus()) { + newVehicle.withStopStatus(stopStatusToModel(vehiclePosition.getCurrentStatus())); + } - // we prefer the to get the current stop from the stop_id - if (vehiclePosition.hasStopId()) { - var matchedStops = stopsOnVehicleTrip - .stream() - .filter(stop -> stop.getId().getId().equals(vehiclePosition.getStopId())) - .toList(); - if (matchedStops.size() == 1) { - newPosition.setStop(matchedStops.get(0)); - } else { - LOG.warn( - "Stop ID {} is not in trip {}. Not setting stopRelationship.", - vehiclePosition.getStopId(), - trip.getId() - ); + // we prefer the to get the current stop from the stop_id + if (vehiclePosition.hasStopId()) { + var matchedStops = stopsOnVehicleTrip + .stream() + .filter(stop -> stop.getId().getId().equals(vehiclePosition.getStopId())) + .toList(); + if (matchedStops.size() == 1) { + newVehicle.withStop(matchedStops.get(0)); + } else { + LOG.warn( + "Stop ID {} is not in trip {}. Not setting stopRelationship.", + vehiclePosition.getStopId(), + trip.getId() + ); + } + } + // but if stop_id isn't there we try current_stop_sequence + else if (vehiclePosition.hasCurrentStopSequence()) { + stopIndexOfGtfsSequence + .apply(vehiclePosition.getCurrentStopSequence()) + .ifPresent(stopIndex -> { + if (validStopIndex(stopIndex, stopsOnVehicleTrip)) { + var stop = stopsOnVehicleTrip.get(stopIndex); + newVehicle.withStop(stop); + } + }); } - } - // but if stop_id isn't there we try current_stop_sequence - else if (vehiclePosition.hasCurrentStopSequence()) { - stopIndexOfGtfsSequence - .apply(vehiclePosition.getCurrentStopSequence()) - .ifPresent(stopIndex -> { - if (validStopIndex(stopIndex, stopsOnVehicleTrip)) { - var stop = stopsOnVehicleTrip.get(stopIndex); - newPosition.setStop(stop); - } - }); } - newPosition.setTrip(trip); + newVehicle.withTrip(trip); - return newPosition.build(); + if (vehiclePositionFeatures.contains(OCCUPANCY) && vehiclePosition.hasOccupancyStatus()) { + newVehicle.withOccupancyStatus(occupancyStatusToModel(vehiclePosition.getOccupancyStatus())); + } + + return newVehicle.build(); } /** @@ -271,7 +289,7 @@ private static boolean validStopIndex(int stopIndex, List stopsOnV private record TemporalDistance(LocalDate date, long distance) {} - private static StopStatus toModel(VehicleStopStatus currentStatus) { + private static StopStatus stopStatusToModel(VehicleStopStatus currentStatus) { return switch (currentStatus) { case IN_TRANSIT_TO -> StopStatus.IN_TRANSIT_TO; case INCOMING_AT -> StopStatus.INCOMING_AT; @@ -279,6 +297,22 @@ private static StopStatus toModel(VehicleStopStatus currentStatus) { }; } + private static OccupancyStatus occupancyStatusToModel( + VehiclePosition.OccupancyStatus occupancyStatus + ) { + return switch (occupancyStatus) { + case NO_DATA_AVAILABLE -> OccupancyStatus.NO_DATA_AVAILABLE; + case EMPTY -> OccupancyStatus.EMPTY; + case MANY_SEATS_AVAILABLE -> OccupancyStatus.MANY_SEATS_AVAILABLE; + case FEW_SEATS_AVAILABLE -> OccupancyStatus.FEW_SEATS_AVAILABLE; + case STANDING_ROOM_ONLY -> OccupancyStatus.STANDING_ROOM_ONLY; + case CRUSHED_STANDING_ROOM_ONLY -> OccupancyStatus.CRUSHED_STANDING_ROOM_ONLY; + case FULL -> OccupancyStatus.FULL; + case NOT_ACCEPTING_PASSENGERS -> OccupancyStatus.NOT_ACCEPTING_PASSENGERS; + case NOT_BOARDABLE -> OccupancyStatus.NOT_ACCEPTING_PASSENGERS; + }; + } + private static String toString(VehiclePosition vehiclePosition) { try { return JsonFormat.printer().omittingInsignificantWhitespace().print(vehiclePosition); @@ -287,7 +321,12 @@ private static String toString(VehiclePosition vehiclePosition) { } } - private Result toRealtimeVehiclePosition( + private VehiclePosition fuzzilySetTrip(VehiclePosition vehiclePosition) { + var trip = fuzzyTripMatcher.match(feedId, vehiclePosition.getTrip()); + return vehiclePosition.toBuilder().setTrip(trip).build(); + } + + private Result toRealtimeVehicle( String feedId, VehiclePosition vehiclePosition ) { @@ -299,7 +338,11 @@ private Result toRealtimeVehiclePosition return Result.failure(UpdateError.noTripId(INVALID_INPUT_STRUCTURE)); } - var tripId = vehiclePosition.getTrip().getTripId(); + var vehiclePositionWithTripId = fuzzyTripMatcher == null + ? vehiclePosition + : fuzzilySetTrip(vehiclePosition); + + var tripId = vehiclePositionWithTripId.getTrip().getTripId(); if (StringUtils.hasNoValue(tripId)) { return Result.failure(UpdateError.noTripId(UpdateError.UpdateErrorType.NO_TRIP_ID)); @@ -317,7 +360,7 @@ private Result toRealtimeVehiclePosition } var serviceDate = Optional - .of(vehiclePosition.getTrip().getStartDate()) + .of(vehiclePositionWithTripId.getTrip().getStartDate()) .map(Strings::emptyToNull) .flatMap(ServiceDateUtils::parseStringToOptional) .orElseGet(() -> inferServiceDate(trip)); @@ -337,15 +380,15 @@ private Result toRealtimeVehiclePosition } // Add position to pattern - var newPosition = mapVehiclePosition( - vehiclePosition, + var newVehicle = mapRealtimeVehicle( + vehiclePositionWithTripId, pattern.getStops(), trip, staticTripTimes::stopIndexOfGtfsSequence ); - return Result.success(new PatternAndVehiclePosition(pattern, newPosition)); + return Result.success(new PatternAndRealtimeVehicle(pattern, newVehicle)); } - record PatternAndVehiclePosition(TripPattern pattern, RealtimeVehiclePosition position) {} + record PatternAndRealtimeVehicle(TripPattern pattern, RealtimeVehicle vehicle) {} } diff --git a/src/main/java/org/opentripplanner/updater/vehicle_position/VehiclePositionUpdaterRunnable.java b/src/main/java/org/opentripplanner/updater/vehicle_position/VehiclePositionUpdaterRunnable.java index 3bb5f349fb2..d1f923696b5 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_position/VehiclePositionUpdaterRunnable.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_position/VehiclePositionUpdaterRunnable.java @@ -9,7 +9,7 @@ public record VehiclePositionUpdaterRunnable( List updates, - VehiclePositionPatternMatcher matcher + RealtimeVehiclePatternMatcher matcher ) implements GraphWriterRunnable { public VehiclePositionUpdaterRunnable { @@ -20,6 +20,6 @@ public record VehiclePositionUpdaterRunnable( @Override public void run(Graph graph, TransitModel transitModel) { // Apply new vehicle positions - matcher.applyVehiclePositionUpdates(updates); + matcher.applyRealtimeVehicleUpdates(updates); } } diff --git a/src/main/java/org/opentripplanner/updater/vehicle_position/VehiclePositionsUpdaterParameters.java b/src/main/java/org/opentripplanner/updater/vehicle_position/VehiclePositionsUpdaterParameters.java index 7d08158903a..14b3d3a04de 100644 --- a/src/main/java/org/opentripplanner/updater/vehicle_position/VehiclePositionsUpdaterParameters.java +++ b/src/main/java/org/opentripplanner/updater/vehicle_position/VehiclePositionsUpdaterParameters.java @@ -3,6 +3,8 @@ import java.net.URI; import java.time.Duration; import java.util.Objects; +import java.util.Set; +import org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig; import org.opentripplanner.updater.spi.HttpHeaders; import org.opentripplanner.updater.spi.PollingGraphUpdaterParameters; @@ -11,7 +13,9 @@ public record VehiclePositionsUpdaterParameters( String feedId, URI url, Duration frequency, - HttpHeaders headers + HttpHeaders headers, + boolean fuzzyTripMatching, + Set vehiclePositionFeatures ) implements PollingGraphUpdaterParameters { public VehiclePositionsUpdaterParameters { diff --git a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls index aaf312244d1..046e581af30 100644 --- a/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls +++ b/src/main/resources/org/opentripplanner/apis/gtfs/schema.graphqls @@ -3394,6 +3394,66 @@ enum AbsoluteDirection { NORTHWEST } +"""Occupancy status of a vehicle.""" +enum OccupancyStatus { + """Default. There is no occupancy-data on this departure.""" + NO_DATA_AVAILABLE + + """ + The vehicle is considered empty by most measures, and has few or no passengers onboard, but is + still accepting passengers. There isn't a big difference between this and MANY_SEATS_AVAILABLE + so it's possible to handle them as the same value, if one wants to limit the number of different + values. + SIRI nordic profile: merge these into `MANY_SEATS_AVAILABLE`. + """ + EMPTY + + """ + The vehicle or carriage has a large number of seats available. The amount of free seats out of + the total seats available to be considered large enough to fall into this category is + determined at the discretion of the producer. There isn't a big difference between this and + EMPTY so it's possible to handle them as the same value, if one wants to limit the number of + different values. + SIRI nordic profile: more than ~50% of seats available. + """ + MANY_SEATS_AVAILABLE + + """ + The vehicle or carriage has a small number of seats available. The amount of free seats out of + the total seats available to be considered small enough to fall into this category is + determined at the discretion of the producer. + SIRI nordic profile: less than ~50% of seats available. + """ + FEW_SEATS_AVAILABLE + + """ + The vehicle or carriage can currently accommodate only standing passengers. + SIRI nordic profile: less than ~10% of seats available. + """ + STANDING_ROOM_ONLY + + """ + The vehicle or carriage can currently accommodate only standing passengers and has limited + space for them. There isn't a big difference between this and FULL so it's possible to handle + them as the same value, if one wants to limit the number of different values. + SIRI nordic profile: merge into `STANDING_ROOM_ONLY`. + """ + CRUSHED_STANDING_ROOM_ONLY + + """ + The vehicle is considered full by most measures, but may still be allowing passengers to + board. + """ + FULL + + """ + The vehicle or carriage is not accepting passengers. + SIRI nordic profile: if vehicle/carriage is not in use / unavailable, or passengers are only allowed + to alight due to e.g. crowding. + """ + NOT_ACCEPTING_PASSENGERS +} + type step { """The distance in meters that this step takes.""" distance: Float @@ -3972,6 +4032,12 @@ type Trip implements Node { """ types: [TripAlertType] ): [Alert] + + """ + The latest realtime occupancy information for the latest occurance of this + trip. + """ + occupancy: TripOccupancy } """Entities, which are relevant for a trip and can contain alerts""" @@ -3998,6 +4064,17 @@ enum TripAlertType { STOPS_ON_TRIP } +""" +Occupancy of a vehicle on a trip. This should include the most recent occupancy information +available for a trip. Historic data might not be available. +""" +type TripOccupancy { + """ + Occupancy information mapped to a limited set of descriptive states. + """ + occupancyStatus: OccupancyStatus +} + """ A system notice is used to tag elements with system information for debugging or other system related purpose. One use-case is to run a routing search with diff --git a/src/test/java/org/opentripplanner/TestServerContext.java b/src/test/java/org/opentripplanner/TestServerContext.java index 969769f93fa..8e68fc7c3a0 100644 --- a/src/test/java/org/opentripplanner/TestServerContext.java +++ b/src/test/java/org/opentripplanner/TestServerContext.java @@ -6,8 +6,8 @@ import java.util.List; import org.opentripplanner.raptor.configure.RaptorConfig; import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.service.vehiclepositions.VehiclePositionService; -import org.opentripplanner.service.vehiclepositions.internal.DefaultVehiclePositionService; +import org.opentripplanner.service.realtimevehicles.RealtimeVehicleService; +import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; @@ -18,6 +18,7 @@ import org.opentripplanner.standalone.server.DefaultServerRequestContext; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; +import org.opentripplanner.transit.service.TransitService; public class TestServerContext { @@ -30,6 +31,7 @@ public static OtpServerRequestContext createServerContext( ) { transitModel.index(); final RouterConfig routerConfig = RouterConfig.DEFAULT; + var transitService = new DefaultTransitService(transitModel); DefaultServerRequestContext context = DefaultServerRequestContext.create( routerConfig.transitTuningConfig(), routerConfig.routingRequestDefaults(), @@ -39,7 +41,7 @@ public static OtpServerRequestContext createServerContext( Metrics.globalRegistry, routerConfig.vectorTileLayers(), createWorldEnvelopeService(), - createVehiclePositionService(), + createRealtimeVehicleService(transitService), createVehicleRentalService(), routerConfig.flexConfig(), List.of(), @@ -54,8 +56,8 @@ public static WorldEnvelopeService createWorldEnvelopeService() { return new DefaultWorldEnvelopeService(new DefaultWorldEnvelopeRepository()); } - public static VehiclePositionService createVehiclePositionService() { - return new DefaultVehiclePositionService(); + public static RealtimeVehicleService createRealtimeVehicleService(TransitService transitService) { + return new DefaultRealtimeVehicleService(transitService); } public static VehicleRentalService createVehicleRentalService() { diff --git a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java index bd3c10f491e..c1e0d0bbccb 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/GraphQLIntegrationTest.java @@ -14,10 +14,12 @@ import static org.opentripplanner.model.plan.PlanTestConstants.T11_30; import static org.opentripplanner.model.plan.PlanTestConstants.T11_50; import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary; +import static org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle.StopStatus.IN_TRANSIT_TO; import static org.opentripplanner.test.support.JsonAssertions.assertEqualJson; import static org.opentripplanner.transit.model._data.TransitModelForTest.id; import static org.opentripplanner.transit.model.basic.TransitMode.BUS; import static org.opentripplanner.transit.model.basic.TransitMode.FERRY; +import static org.opentripplanner.transit.model.timetable.OccupancyStatus.FEW_SEATS_AVAILABLE; import jakarta.ws.rs.core.Response; import java.io.IOException; @@ -68,7 +70,8 @@ import org.opentripplanner.routing.impl.TransitAlertServiceImpl; import org.opentripplanner.routing.services.TransitAlertService; import org.opentripplanner.routing.vehicle_parking.VehicleParking; -import org.opentripplanner.service.vehiclepositions.internal.DefaultVehiclePositionService; +import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; +import org.opentripplanner.service.realtimevehicles.model.RealtimeVehicle; import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; import org.opentripplanner.standalone.config.framework.json.JsonSupport; import org.opentripplanner.test.support.FilePatternSource; @@ -216,6 +219,28 @@ public TransitAlertService getTransitAlertService() { var alerts = ListUtils.combine(List.of(alert), getTransitAlert(entitySelector)); transitService.getTransitAlertService().setAlerts(alerts); + var realtimeVehicleService = new DefaultRealtimeVehicleService(transitService); + var occypancyVehicle = RealtimeVehicle + .builder() + .withTrip(trip) + .withTime(Instant.MAX) + .withVehicleId(id("vehicle-1")) + .withOccupancyStatus(FEW_SEATS_AVAILABLE) + .build(); + var positionVehicle = RealtimeVehicle + .builder() + .withTrip(trip) + .withTime(Instant.MIN) + .withVehicleId(id("vehicle-2")) + .withLabel("vehicle2") + .withCoordinates(new WgsCoordinate(60.0, 80.0)) + .withHeading(80.0) + .withSpeed(10.2) + .withStop(pattern.getStop(0)) + .withStopStatus(IN_TRANSIT_TO) + .build(); + realtimeVehicleService.setRealtimeVehicles(pattern, List.of(occypancyVehicle, positionVehicle)); + context = new GraphQLRequestContext( new TestRoutingService(List.of(i1)), @@ -223,7 +248,7 @@ public TransitAlertService getTransitAlertService() { new DefaultFareService(), graph.getVehicleParkingService(), new DefaultVehicleRentalService(), - new DefaultVehiclePositionService(), + realtimeVehicleService, GraphFinder.getInstance(graph, transitService::findRegularStop), new RouteRequest() ); diff --git a/src/test/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapperTest.java b/src/test/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapperTest.java index c6e69cecf4f..af6352839fb 100644 --- a/src/test/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/gtfs/mapping/RouteRequestMapperTest.java @@ -29,7 +29,7 @@ import org.opentripplanner.routing.core.BicycleOptimizeType; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graphfinder.GraphFinder; -import org.opentripplanner.service.vehiclepositions.internal.DefaultVehiclePositionService; +import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; import org.opentripplanner.test.support.VariableSource; import org.opentripplanner.transit.service.DefaultTransitService; @@ -51,7 +51,7 @@ class RouteRequestMapperTest implements PlanTestConstants { new DefaultFareService(), graph.getVehicleParkingService(), new DefaultVehicleRentalService(), - new DefaultVehiclePositionService(), + new DefaultRealtimeVehicleService(transitService), GraphFinder.getInstance(graph, transitService::findRegularStop), new RouteRequest() ); diff --git a/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java b/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java index 5faffc6a606..519514ab9ed 100644 --- a/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java +++ b/src/test/java/org/opentripplanner/transit/speed_test/SpeedTest.java @@ -22,7 +22,7 @@ import org.opentripplanner.routing.framework.DebugTimingAggregator; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.SerializedGraphObject; -import org.opentripplanner.service.vehiclepositions.internal.DefaultVehiclePositionService; +import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; import org.opentripplanner.service.vehiclerental.internal.DefaultVehicleRentalService; import org.opentripplanner.standalone.OtpStartupInfo; import org.opentripplanner.standalone.api.OtpServerRequestContext; @@ -90,9 +90,11 @@ public SpeedTest( this.testCaseDefinitions = tcIO.readTestCaseDefinitions(); this.expectedResultsByTcId = tcIO.readExpectedResults(); + var transitService = new DefaultTransitService(transitModel); + UpdaterConfigurator.configure( graph, - new DefaultVehiclePositionService(), + new DefaultRealtimeVehicleService(transitService), new DefaultVehicleRentalService(), transitModel, config.updatersConfig @@ -111,7 +113,7 @@ public SpeedTest( timer.getRegistry(), List::of, TestServerContext.createWorldEnvelopeService(), - TestServerContext.createVehiclePositionService(), + TestServerContext.createRealtimeVehicleService(transitService), TestServerContext.createVehicleRentalService(), config.flexConfig, List.of(), diff --git a/src/test/java/org/opentripplanner/updater/vehicle_position/VehiclePositionsMatcherTest.java b/src/test/java/org/opentripplanner/updater/vehicle_position/RealtimeVehicleMatcherTest.java similarity index 66% rename from src/test/java/org/opentripplanner/updater/vehicle_position/VehiclePositionsMatcherTest.java rename to src/test/java/org/opentripplanner/updater/vehicle_position/RealtimeVehicleMatcherTest.java index 4a1a5d287bf..2ddc4860679 100644 --- a/src/test/java/org/opentripplanner/updater/vehicle_position/VehiclePositionsMatcherTest.java +++ b/src/test/java/org/opentripplanner/updater/vehicle_position/RealtimeVehicleMatcherTest.java @@ -2,6 +2,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.opentripplanner.model.plan.PlanTestConstants.T11_00; +import static org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig.VehiclePositionFeature.OCCUPANCY; +import static org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig.VehiclePositionFeature.POSITION; +import static org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig.VehiclePositionFeature.STOP_POSITION; import static org.opentripplanner.transit.model._data.TransitModelForTest.stopTime; import static org.opentripplanner.updater.spi.UpdateError.UpdateErrorType.TRIP_NOT_FOUND_IN_PATTERN; @@ -24,7 +27,8 @@ import org.opentripplanner._support.time.ZoneIds; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.model.StopTime; -import org.opentripplanner.service.vehiclepositions.internal.DefaultVehiclePositionService; +import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; +import org.opentripplanner.standalone.config.routerconfig.updaters.VehiclePositionsUpdaterConfig; import org.opentripplanner.test.support.VariableSource; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.framework.Deduplicator; @@ -32,22 +36,38 @@ import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.network.StopPattern; import org.opentripplanner.transit.model.network.TripPattern; +import org.opentripplanner.transit.model.timetable.OccupancyStatus; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; -public class VehiclePositionsMatcherTest { +public class RealtimeVehicleMatcherTest { - public static final Route ROUTE = TransitModelForTest.route("1").build(); + private static final Route ROUTE = TransitModelForTest.route("1").build(); + private static final Set FEATURES = Set.of( + POSITION, + STOP_POSITION, + OCCUPANCY + ); ZoneId zoneId = ZoneIds.BERLIN; String tripId = "trip1"; FeedScopedId scopedTripId = TransitModelForTest.id(tripId); @Test - public void matchRealtimePositionsToTrip() { + public void matchRealtimeVehiclesToTrip() { var pos = vehiclePosition(tripId); testVehiclePositions(pos); } + @Test + public void testOccupancy() { + var pos = vehiclePosition(tripId); + var posWithOccupancy = pos + .toBuilder() + .setOccupancyStatus(VehiclePosition.OccupancyStatus.FEW_SEATS_AVAILABLE) + .build(); + testVehiclePositionOccupancy(posWithOccupancy); + } + @Test @DisplayName("If the vehicle position has no start_date we need to guess the service day") public void inferStartDate() { @@ -64,7 +84,7 @@ public void inferStartDate() { @Test public void tripNotFoundInPattern() { - var service = new DefaultVehiclePositionService(); + var service = new DefaultRealtimeVehicleService(null); final String secondTripId = "trip2"; @@ -75,17 +95,19 @@ public void tripNotFoundInPattern() { var pattern = tripPattern(trip1, stopTimes); // Map positions to trips in feed - VehiclePositionPatternMatcher matcher = new VehiclePositionPatternMatcher( + RealtimeVehiclePatternMatcher matcher = new RealtimeVehiclePatternMatcher( TransitModelForTest.FEED_ID, ignored -> trip2, ignored -> pattern, (id, time) -> pattern, service, - zoneId + zoneId, + null, + FEATURES ); var positions = List.of(vehiclePosition(secondTripId)); - var result = matcher.applyVehiclePositionUpdates(positions); + var result = matcher.applyRealtimeVehicleUpdates(positions); assertEquals(1, result.failed()); assertEquals(Set.of(TRIP_NOT_FOUND_IN_PATTERN), result.failures().keySet()); @@ -93,7 +115,7 @@ public void tripNotFoundInPattern() { @Test public void sequenceId() { - var service = new DefaultVehiclePositionService(); + var service = new DefaultRealtimeVehicleService(null); var tripId = "trip1"; var scopedTripId = TransitModelForTest.id(tripId); @@ -106,13 +128,15 @@ public void sequenceId() { var patternForTrip = Map.of(trip1, pattern1); // Map positions to trips in feed - VehiclePositionPatternMatcher matcher = new VehiclePositionPatternMatcher( + RealtimeVehiclePatternMatcher matcher = new RealtimeVehiclePatternMatcher( TransitModelForTest.FEED_ID, tripForId::get, patternForTrip::get, (id, time) -> patternForTrip.get(id), service, - zoneId + zoneId, + null, + FEATURES ); var pos = VehiclePosition @@ -124,12 +148,12 @@ public void sequenceId() { var positions = List.of(pos); // Execute the same match-to-pattern step as the runner - matcher.applyVehiclePositionUpdates(positions); + matcher.applyRealtimeVehicleUpdates(positions); // ensure that gtfs-rt was matched to an OTP pattern correctly - assertEquals(1, service.getVehiclePositions(pattern1).size()); - var nextStop = service.getVehiclePositions(pattern1).get(0).stop().stop(); - assertEquals("F:stop-20", nextStop.getId().toString()); + assertEquals(1, service.getRealtimeVehicles(pattern1).size()); + var nextStop = service.getRealtimeVehicles(pattern1).get(0).stop(); + assertEquals("F:stop-20", nextStop.get().stop().getId().toString()); } @Test @@ -146,7 +170,7 @@ void invalidStopSequence() { } private void testVehiclePositions(VehiclePosition pos) { - var service = new DefaultVehiclePositionService(); + var service = new DefaultRealtimeVehicleService(null); var trip = TransitModelForTest.trip(tripId).build(); var stopTimes = List.of(stopTime(trip, 0), stopTime(trip, 1), stopTime(trip, 2)); @@ -156,40 +180,77 @@ private void testVehiclePositions(VehiclePosition pos) { var patternForTrip = Map.of(trip, pattern); // an untouched pattern has no vehicle positions - assertEquals(0, service.getVehiclePositions(pattern).size()); + assertEquals(0, service.getRealtimeVehicles(pattern).size()); // Map positions to trips in feed - VehiclePositionPatternMatcher matcher = new VehiclePositionPatternMatcher( + var matcher = new RealtimeVehiclePatternMatcher( TransitModelForTest.FEED_ID, tripForId::get, patternForTrip::get, (id, time) -> patternForTrip.get(id), service, - zoneId + zoneId, + null, + FEATURES ); var positions = List.of(pos); // Execute the same match-to-pattern step as the runner - matcher.applyVehiclePositionUpdates(positions); + matcher.applyRealtimeVehicleUpdates(positions); // ensure that gtfs-rt was matched to an OTP pattern correctly - var vehiclePositions = service.getVehiclePositions(pattern); - assertEquals(1, vehiclePositions.size()); + var realtimeVehicles = service.getRealtimeVehicles(pattern); + assertEquals(1, realtimeVehicles.size()); - var parsedPos = vehiclePositions.get(0); - assertEquals(tripId, parsedPos.trip().getId().getId()); - assertEquals(new WgsCoordinate(1, 1), parsedPos.coordinates()); - assertEquals(30, parsedPos.heading()); + var parsedVehicle = realtimeVehicles.get(0); + assertEquals(tripId, parsedVehicle.trip().getId().getId()); + assertEquals(new WgsCoordinate(1, 1), parsedVehicle.coordinates().get()); + assertEquals(30, parsedVehicle.heading().get()); // if we have an empty list of updates then clear the positions from the previous update - matcher.applyVehiclePositionUpdates(List.of()); - assertEquals(0, service.getVehiclePositions(pattern).size()); + matcher.applyRealtimeVehicleUpdates(List.of()); + assertEquals(0, service.getRealtimeVehicles(pattern).size()); + } + + private void testVehiclePositionOccupancy(VehiclePosition pos) { + var service = new DefaultRealtimeVehicleService(null); + var trip = TransitModelForTest.trip(tripId).build(); + var stopTimes = List.of(stopTime(trip, 0), stopTime(trip, 1), stopTime(trip, 2)); + + TripPattern pattern = tripPattern(trip, stopTimes); + + var tripForId = Map.of(scopedTripId, trip); + var patternForTrip = Map.of(trip, pattern); + + // an untouched pattern has no vehicle positions + assertEquals(0, service.getRealtimeVehicles(pattern).size()); + + // Map positions to trips in feed + RealtimeVehiclePatternMatcher matcher = new RealtimeVehiclePatternMatcher( + TransitModelForTest.FEED_ID, + tripForId::get, + patternForTrip::get, + (id, time) -> patternForTrip.get(id), + service, + zoneId, + null, + FEATURES + ); + + var positions = List.of(pos); + + // Execute the same match-to-pattern step as the runner + matcher.applyRealtimeVehicleUpdates(positions); + + // Check that occupancy for the trip is as set in original position + var occupancy = service.getOccupancyStatus(trip.getId(), pattern); + assertEquals(OccupancyStatus.FEW_SEATS_AVAILABLE, occupancy); } @Test public void clearOldTrips() { - var service = new DefaultVehiclePositionService(); + var service = new DefaultRealtimeVehicleService(null); var tripId1 = "trip1"; var tripId2 = "trip2"; @@ -210,17 +271,19 @@ public void clearOldTrips() { var patternForTrip = Map.of(trip1, pattern1, trip2, pattern2); - // an untouched pattern has no vehicle positions - assertEquals(0, service.getVehiclePositions(pattern1).size()); + // an untouched pattern has no vehicles + assertEquals(0, service.getRealtimeVehicles(pattern1).size()); // Map positions to trips in feed - VehiclePositionPatternMatcher matcher = new VehiclePositionPatternMatcher( + RealtimeVehiclePatternMatcher matcher = new RealtimeVehiclePatternMatcher( TransitModelForTest.FEED_ID, tripForId::get, patternForTrip::get, (id, time) -> patternForTrip.get(id), service, - zoneId + zoneId, + null, + FEATURES ); var pos1 = vehiclePosition(tripId1); @@ -230,16 +293,16 @@ public void clearOldTrips() { var positions = List.of(pos1, pos2); // Execute the same match-to-pattern step as the runner - matcher.applyVehiclePositionUpdates(positions); + matcher.applyRealtimeVehicleUpdates(positions); // ensure that gtfs-rt was matched to an OTP pattern correctly - assertEquals(1, service.getVehiclePositions(pattern1).size()); - assertEquals(1, service.getVehiclePositions(pattern2).size()); + assertEquals(1, service.getRealtimeVehicles(pattern1).size()); + assertEquals(1, service.getRealtimeVehicles(pattern2).size()); - matcher.applyVehiclePositionUpdates(List.of(pos1)); - assertEquals(1, service.getVehiclePositions(pattern1).size()); + matcher.applyRealtimeVehicleUpdates(List.of(pos1)); + assertEquals(1, service.getRealtimeVehicles(pattern1).size()); // because there are no more updates for pattern2 we remove all positions - assertEquals(0, service.getVehiclePositions(pattern2).size()); + assertEquals(0, service.getRealtimeVehicles(pattern2).size()); } static Stream inferenceTestCases = Stream.of( @@ -261,7 +324,7 @@ void inferServiceDayOfTripAt6(String time, String expectedDate) { var tripTimes = new TripTimes(trip, stopTimes, new Deduplicator()); var instant = OffsetDateTime.parse(time).toInstant(); - var inferredDate = VehiclePositionPatternMatcher.inferServiceDate(tripTimes, zoneId, instant); + var inferredDate = RealtimeVehiclePatternMatcher.inferServiceDate(tripTimes, zoneId, instant); assertEquals(LocalDate.parse(expectedDate), inferredDate); } @@ -280,7 +343,7 @@ void inferServiceDateCloseToMidnight() { // because the trip "crosses" midnight and we are already on the next day, we infer the service date to be // yesterday - var inferredDate = VehiclePositionPatternMatcher.inferServiceDate(tripTimes, zoneId, time); + var inferredDate = RealtimeVehiclePatternMatcher.inferServiceDate(tripTimes, zoneId, time); assertEquals(LocalDate.parse("2022-04-04"), inferredDate); } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/patterns.json b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/patterns.json index 5f2a0e7d746..b23ced4f954 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/expectations/patterns.json +++ b/src/test/resources/org/opentripplanner/apis/gtfs/expectations/patterns.json @@ -47,7 +47,43 @@ "pickupType" : "SCHEDULED", "dropoffType" : "SCHEDULED" } - ] + ], + "occupancy" : { + "occupancyStatus" : "FEW_SEATS_AVAILABLE" + } + } + ], + "vehiclePositions" : [ + { + "vehicleId" : "F:vehicle-1", + "label" : null, + "lat" : null, + "lon" : null, + "stopRelationship" : null, + "speed" : null, + "heading" : null, + "lastUpdated" : 31556889864403199, + "trip" : { + "gtfsId" : "F:123" + } + }, + { + "vehicleId" : "F:vehicle-2", + "label" : "vehicle2", + "lat" : 60.0, + "lon" : 80.0, + "stopRelationship" : { + "status" : "IN_TRANSIT_TO", + "stop" : { + "gtfsId" : "F:Stop_0" + } + }, + "speed" : 10.2, + "heading" : 80.0, + "lastUpdated" : -31557014167219200, + "trip" : { + "gtfsId" : "F:123" + } } ] } diff --git a/src/test/resources/org/opentripplanner/apis/gtfs/queries/patterns.graphql b/src/test/resources/org/opentripplanner/apis/gtfs/queries/patterns.graphql index d4feaf2c8f6..32be856274e 100644 --- a/src/test/resources/org/opentripplanner/apis/gtfs/queries/patterns.graphql +++ b/src/test/resources/org/opentripplanner/apis/gtfs/queries/patterns.graphql @@ -17,6 +17,27 @@ pickupType dropoffType } + occupancy { + occupancyStatus + } + } + vehiclePositions { + vehicleId + label + lat + lon + stopRelationship { + status + stop { + gtfsId + } + } + speed + heading + lastUpdated + trip { + gtfsId + } } } } \ No newline at end of file diff --git a/src/test/resources/standalone/config/router-config.json b/src/test/resources/standalone/config/router-config.json index ddfafff3eea..9cf87635680 100644 --- a/src/test/resources/standalone/config/router-config.json +++ b/src/test/resources/standalone/config/router-config.json @@ -291,7 +291,9 @@ "frequency": "1m", "headers": { "Header-Name": "Header-Value" - } + }, + "fuzzyTripMatching": false, + "features": ["position"] }, // Streaming differential GTFS-RT TripUpdates over websockets {