diff --git a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java index 79e767b1fea..f4241fcbc61 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraph.java @@ -25,6 +25,7 @@ import org.opentripplanner.framework.concurrent.OtpRequestThreadFactory; import org.opentripplanner.framework.lang.ObjectUtils; import org.opentripplanner.standalone.api.OtpServerRequestContext; +import org.opentripplanner.transit.model.framework.EntityNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,7 +73,7 @@ Response executeGraphQL( return ExecutionResultMapper.timeoutResponse(); } catch (ResponseTooLargeException rtle) { return ExecutionResultMapper.tooLargeResponse(rtle.getMessage()); - } catch (CoercingParseValueException | UnknownOperationException e) { + } catch (EntityNotFoundException | CoercingParseValueException | UnknownOperationException e) { return ExecutionResultMapper.badRequestResponse(e.getMessage()); } catch (Exception systemError) { LOG.error(systemError.getMessage(), systemError); diff --git a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java index b5a934722ea..449ee81021d 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/TransmodelGraphQLSchema.java @@ -58,6 +58,8 @@ import org.opentripplanner.apis.transmodel.model.framework.ServerInfoType; import org.opentripplanner.apis.transmodel.model.framework.StreetModeDurationInputType; import org.opentripplanner.apis.transmodel.model.framework.SystemNoticeType; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelDirectives; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.model.framework.ValidityPeriodType; import org.opentripplanner.apis.transmodel.model.network.DestinationDisplayType; import org.opentripplanner.apis.transmodel.model.network.GroupOfLinesType; @@ -74,10 +76,10 @@ import org.opentripplanner.apis.transmodel.model.plan.TripPatternType; import org.opentripplanner.apis.transmodel.model.plan.TripQuery; import org.opentripplanner.apis.transmodel.model.plan.TripType; -import org.opentripplanner.apis.transmodel.model.plan.ViaLocationInputType; -import org.opentripplanner.apis.transmodel.model.plan.ViaSegmentInputType; -import org.opentripplanner.apis.transmodel.model.plan.ViaTripQuery; -import org.opentripplanner.apis.transmodel.model.plan.ViaTripType; +import org.opentripplanner.apis.transmodel.model.plan.legacyvia.ViaLocationInputType; +import org.opentripplanner.apis.transmodel.model.plan.legacyvia.ViaSegmentInputType; +import org.opentripplanner.apis.transmodel.model.plan.legacyvia.ViaTripQuery; +import org.opentripplanner.apis.transmodel.model.plan.legacyvia.ViaTripType; import org.opentripplanner.apis.transmodel.model.siri.et.EstimatedCallType; import org.opentripplanner.apis.transmodel.model.siri.sx.AffectsType; import org.opentripplanner.apis.transmodel.model.siri.sx.PtSituationElementType; @@ -141,46 +143,21 @@ public static GraphQLSchema create(RouteRequest defaultRequest, GqlUtil gqlUtil) return new TransmodelGraphQLSchema(defaultRequest, gqlUtil).create(); } - // private BookingArrangement getBookingArrangementForTripTimeShort(TripTimeShort tripTimeShort) { - // Trip trip = index.tripForId.get(tripTimeShort.tripId); - // if (trip == null) { - // return null; - // } - // TripPattern tripPattern = index.patternForTrip.get(trip); - // if (tripPattern == null || tripPattern.stopPattern == null) { - // return null; - // } - // return tripPattern.stopPattern.bookingArrangements[tripTimeShort.stopIndex]; - // } - @SuppressWarnings("unchecked") private GraphQLSchema create() { - /* - multilingualStringType, validityPeriodType, infoLinkType, bookingArrangementType, systemNoticeType, - linkGeometryType, serverInfoType, authorityType, operatorType, noticeType - - - - */ - // Framework GraphQLOutputType multilingualStringType = MultilingualStringType.create(); GraphQLObjectType validityPeriodType = ValidityPeriodType.create(gqlUtil); GraphQLObjectType infoLinkType = InfoLinkType.create(); - GraphQLOutputType bookingArrangementType = BookingArrangementType.create(gqlUtil); + GraphQLOutputType bookingArrangementType = BookingArrangementType.create(); GraphQLOutputType systemNoticeType = SystemNoticeType.create(); GraphQLOutputType linkGeometryType = PointsOnLinkType.create(); GraphQLOutputType serverInfoType = ServerInfoType.create(); GraphQLOutputType authorityType = AuthorityType.create( LineType.REF, - PtSituationElementType.REF, - gqlUtil - ); - GraphQLOutputType operatorType = OperatorType.create( - LineType.REF, - ServiceJourneyType.REF, - gqlUtil + PtSituationElementType.REF ); + GraphQLOutputType operatorType = OperatorType.create(LineType.REF, ServiceJourneyType.REF); GraphQLOutputType brandingType = BrandingType.create(); GraphQLOutputType noticeType = NoticeType.create(); GraphQLOutputType rentalVehicleTypeType = RentalVehicleTypeType.create(); @@ -245,8 +222,7 @@ private GraphQLSchema create() { stopPlaceType, lineType, ServiceJourneyType.REF, - DatedServiceJourneyType.REF, - gqlUtil + DatedServiceJourneyType.REF ); // Timetable @@ -270,8 +246,7 @@ private GraphQLSchema create() { lineType, ServiceJourneyType.REF, stopToStopGeometryType, - ptSituationElementType, - gqlUtil + ptSituationElementType ); GraphQLOutputType estimatedCallType = EstimatedCallType.create( bookingArrangementType, @@ -294,16 +269,14 @@ private GraphQLSchema create() { ptSituationElementType, journeyPatternType, estimatedCallType, - TimetabledPassingTimeType.REF, - gqlUtil + TimetabledPassingTimeType.REF ); GraphQLOutputType datedServiceJourneyType = DatedServiceJourneyType.create( serviceJourneyType, journeyPatternType, estimatedCallType, - quayType, - gqlUtil + quayType ); GraphQLOutputType timetabledPassingTime = TimetabledPassingTimeType.create( @@ -311,8 +284,7 @@ private GraphQLSchema create() { noticeType, quayType, destinationDisplayType, - serviceJourneyType, - gqlUtil + serviceJourneyType ); GraphQLObjectType tripPatternTimePenaltyType = TripPatternTimePenaltyType.create(); @@ -358,7 +330,7 @@ private GraphQLSchema create() { ); GraphQLInputObjectType durationPerStreetModeInput = StreetModeDurationInputType.create(gqlUtil); - GraphQLInputObjectType penaltyForStreetMode = PenaltyForStreetModeType.create(gqlUtil); + GraphQLInputObjectType penaltyForStreetMode = PenaltyForStreetModeType.create(); GraphQLFieldDefinition tripQuery = TripQuery.create( routing, @@ -369,7 +341,7 @@ private GraphQLSchema create() { ); GraphQLOutputType viaTripType = ViaTripType.create(tripPatternType, routingErrorType); - GraphQLInputObjectType viaLocationInputType = ViaLocationInputType.create(gqlUtil); + GraphQLInputObjectType viaLocationInputType = ViaLocationInputType.create(); GraphQLInputObjectType viaSegmentInputType = ViaSegmentInputType.create(); GraphQLFieldDefinition viaTripQuery = ViaTripQuery.create( @@ -435,7 +407,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("stopPlace") .description("Get a single stopPlace based on its id)") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(stopPlaceType) .argument( GraphQLArgument @@ -457,7 +429,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("stopPlaces") .description("Get all stopPlaces") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(stopPlaceType))) .argument( GraphQLArgument @@ -492,7 +464,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("stopPlacesByBbox") .description("Get all stop places within the specified bounding box") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(stopPlaceType))) .argument( GraphQLArgument @@ -573,7 +545,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("quay") .description("Get a single quay based on its id)") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(quayType) .argument( GraphQLArgument @@ -594,7 +566,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("quays") .description("Get all quays") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(quayType))) .argument( GraphQLArgument @@ -639,7 +611,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("quaysByBbox") .description("Get all quays within the specified bounding box") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(quayType))) .argument( GraphQLArgument @@ -722,7 +694,7 @@ private GraphQLSchema create() { "limits for the input parameters, but the query will timeout and return if the parameters " + "are too high." ) - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type( relay.connectionType( "quayAtDistance", @@ -808,7 +780,7 @@ private GraphQLSchema create() { .description( "Get all places (quays, stop places, car parks etc. with coordinates) within the specified radius from a location. The returned type has two fields place and distance. The search is done by walking so the distance is according to the network of walkables." ) - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type( relay.connectionType( "placeAtDistance", @@ -1008,7 +980,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("authority") .description("Get an authority by ID") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(authorityType) .argument( GraphQLArgument @@ -1029,7 +1001,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("authorities") .description("Get all authorities") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(authorityType))) .dataFetcher(environment -> { return new ArrayList<>(GqlUtil.getTransitService(environment).getAgencies()); @@ -1041,7 +1013,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("operator") .description("Get a operator by ID") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(operatorType) .argument( GraphQLArgument @@ -1062,7 +1034,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("operators") .description("Get all operators") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(operatorType))) .dataFetcher(environment -> { return new ArrayList<>(GqlUtil.getTransitService(environment).getAllOperators()); @@ -1074,7 +1046,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("line") .description("Get a single line based on its id") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(lineType) .argument( GraphQLArgument @@ -1099,7 +1071,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("lines") .description("Get all lines") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(lineType))) .argument( GraphQLArgument @@ -1252,7 +1224,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("serviceJourney") .description("Get a single service journey based on its id") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(serviceJourneyType) .argument( GraphQLArgument @@ -1273,7 +1245,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("serviceJourneys") .description("Get all service journeys") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(serviceJourneyType))) .argument( GraphQLArgument @@ -1296,7 +1268,7 @@ private GraphQLSchema create() { .newArgument() .name("activeDates") .description("Set of ids of active dates to fetch serviceJourneys for.") - .type(new GraphQLList(gqlUtil.dateScalar)) + .type(new GraphQLList(TransmodelScalars.DATE_SCALAR)) .build() ) .argument( @@ -1350,7 +1322,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("bikeRentalStations") .description("Get all bike rental stations") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .argument( GraphQLArgument .newArgument() @@ -1379,7 +1351,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("bikeRentalStation") .description("Get all bike rental stations") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(bikeRentalStationType) .argument( GraphQLArgument @@ -1406,7 +1378,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("bikeRentalStationsByBbox") .description("Get all bike rental stations within the specified bounding box.") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(bikeRentalStationType))) .argument( GraphQLArgument.newArgument().name("minimumLatitude").type(Scalars.GraphQLFloat).build() @@ -1445,7 +1417,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("bikePark") .description("Get a single bike park based on its id") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(bikeParkType) .argument( GraphQLArgument @@ -1470,7 +1442,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("bikeParks") .description("Get all bike parks") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(bikeParkType))) .dataFetcher(environment -> GqlUtil @@ -1485,7 +1457,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("routingParameters") .description("Get default routing parameters.") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(this.routing.graphQLType) .dataFetcher(environment -> routing.request) .build() @@ -1495,7 +1467,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("situations") .description("Get all active situations.") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ptSituationElementType)))) .argument( GraphQLArgument @@ -1571,7 +1543,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("situation") .description("Get a single situation based on its situationNumber") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(ptSituationElementType) .argument( GraphQLArgument @@ -1597,7 +1569,7 @@ private GraphQLSchema create() { .newFieldDefinition() .name("leg") .description("Refetch a single leg based on its id") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(LegType.REF) .argument( GraphQLArgument @@ -1626,13 +1598,13 @@ private GraphQLSchema create() { .description( "Get OTP deployment information. This is only useful for developers of OTP itself not regular API users." ) - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(serverInfoType)) .dataFetcher(e -> projectInfo()) .build() ) .field(DatedServiceJourneyQuery.createGetById(datedServiceJourneyType)) - .field(DatedServiceJourneyQuery.createQuery(datedServiceJourneyType, gqlUtil)) + .field(DatedServiceJourneyQuery.createQuery(datedServiceJourneyType)) .build(); return GraphQLSchema @@ -1641,7 +1613,7 @@ private GraphQLSchema create() { .additionalType(placeInterface) .additionalType(timetabledPassingTime) .additionalType(Relay.pageInfoType) - .additionalDirective(gqlUtil.timingData) + .additionalDirective(TransmodelDirectives.TIMING_DATA) .build(); } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/mapping/GeometryMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/mapping/GeometryMapper.java index e2bf687e3b8..664da59d522 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/mapping/GeometryMapper.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/mapping/GeometryMapper.java @@ -2,7 +2,7 @@ import java.util.ArrayList; import java.util.List; -import org.opentripplanner.apis.transmodel.model.util.EncodedPolylineBeanWithStops; +import org.opentripplanner.apis.transmodel.model.framework.EncodedPolylineBeanWithStops; import org.opentripplanner.framework.geometry.EncodedPolyline; import org.opentripplanner.transit.model.network.TripPattern; diff --git a/src/main/java/org/opentripplanner/apis/transmodel/mapping/PassThroughLocationMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/mapping/PassThroughLocationMapper.java deleted file mode 100644 index 951467e8727..00000000000 --- a/src/main/java/org/opentripplanner/apis/transmodel/mapping/PassThroughLocationMapper.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.opentripplanner.apis.transmodel.mapping; - -import static java.util.stream.Collectors.collectingAndThen; -import static java.util.stream.Collectors.toList; - -import java.util.List; -import java.util.Map; -import java.util.Objects; -import org.opentripplanner.routing.api.request.PassThroughPoint; -import org.opentripplanner.transit.service.TransitService; - -class PassThroughLocationMapper { - - static List toLocations( - final TransitService transitService, - final List> passThroughPoints - ) { - return passThroughPoints - .stream() - .map(p -> handlePoint(transitService, p)) - .filter(Objects::nonNull) - .collect(toList()); - // TODO Propagate an error if a stopplace is unknown and fails lookup. - } - - private static PassThroughPoint handlePoint( - final TransitService transitService, - Map map - ) { - final List stops = (List) map.get("placeIds"); - final String name = (String) map.get("name"); - if (stops == null) { - return null; - } - - return stops - .stream() - .map(TransitIdMapper::mapIDToDomain) - .flatMap(id -> { - var stopLocations = transitService.getStopOrChildStops(id); - if (stopLocations.isEmpty()) { - throw new RuntimeException("No match for %s.".formatted(id)); - } - return stopLocations.stream(); - }) - .collect(collectingAndThen(toList(), sls -> new PassThroughPoint(sls, name))); - } -} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapper.java index 72cd5fc6260..a0438ef00d6 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapper.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapper.java @@ -11,12 +11,12 @@ import java.util.Locale; import java.util.Map; import org.opentripplanner.apis.transmodel.TransmodelRequestContext; +import org.opentripplanner.apis.transmodel.model.plan.TripQuery; import org.opentripplanner.apis.transmodel.support.DataFetcherDecorator; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.transit.model.framework.FeedScopedId; -import org.opentripplanner.transit.service.TransitService; public class TripRequestMapper { @@ -40,11 +40,16 @@ public static RouteRequest createRequest(DataFetchingEnvironment environment) { "to", (Map v) -> request.setTo(GenericLocationMapper.toGenericLocation(v)) ); - final TransitService transitService = context.getTransitService(); callWith.argument( "passThroughPoints", (List> v) -> { - request.setPassThroughPoints(PassThroughLocationMapper.toLocations(transitService, v)); + request.setViaLocations(TripViaLocationMapper.toLegacyPassThroughLocations(v)); + } + ); + callWith.argument( + TripQuery.FIELD_VIA, + (List> v) -> { + request.setViaLocations(TripViaLocationMapper.mapToViaLocations(v)); } ); diff --git a/src/main/java/org/opentripplanner/apis/transmodel/mapping/TripViaLocationMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/mapping/TripViaLocationMapper.java new file mode 100644 index 00000000000..ec4846bf514 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/mapping/TripViaLocationMapper.java @@ -0,0 +1,83 @@ +package org.opentripplanner.apis.transmodel.mapping; + +import static java.util.stream.Collectors.toList; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.opentripplanner.apis.transmodel.model.plan.TripQuery; +import org.opentripplanner.apis.transmodel.model.plan.ViaLocationInputType; +import org.opentripplanner.apis.transmodel.support.OneOfInputValidator; +import org.opentripplanner.routing.api.request.via.PassThroughViaLocation; +import org.opentripplanner.routing.api.request.via.ViaLocation; +import org.opentripplanner.routing.api.request.via.VisitViaLocation; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +class TripViaLocationMapper { + + static List mapToViaLocations(final List> via) { + return via.stream().map(TripViaLocationMapper::mapViaLocation).collect(toList()); + } + + /** + * @deprecated Legacy passThrough, use via instead + */ + @Deprecated + static List toLegacyPassThroughLocations( + final List> passThroughPoints + ) { + return passThroughPoints + .stream() + .map(TripViaLocationMapper::mapLegacyPassThroughViaLocation) + .collect(toList()); + } + + private static ViaLocation mapViaLocation(Map inputMap) { + var fieldName = OneOfInputValidator.validateOneOf( + inputMap, + TripQuery.FIELD_VIA, + ViaLocationInputType.FIELD_VISIT, + ViaLocationInputType.FIELD_PASS_THROUGH + ); + + Map value = (Map) inputMap.get(fieldName); + + return switch (fieldName) { + case ViaLocationInputType.FIELD_VISIT -> mapVisitViaLocation(value); + case ViaLocationInputType.FIELD_PASS_THROUGH -> mapPassThroughViaLocation(value); + default -> throw new IllegalArgumentException("Unknown field: " + fieldName); + }; + } + + private static VisitViaLocation mapVisitViaLocation(Map inputMap) { + var label = (String) inputMap.get(ViaLocationInputType.FIELD_LABEL); + var minimumWaitTime = (Duration) inputMap.get(ViaLocationInputType.FIELD_MINIMUM_WAIT_TIME); + var stopLocationIds = mapStopLocationIds(inputMap); + return new VisitViaLocation(label, minimumWaitTime, stopLocationIds, List.of()); + } + + private static PassThroughViaLocation mapPassThroughViaLocation(Map inputMap) { + var label = (String) inputMap.get(ViaLocationInputType.FIELD_LABEL); + var stopLocationIds = mapStopLocationIds(inputMap); + return new PassThroughViaLocation(label, stopLocationIds); + } + + private static List mapStopLocationIds(Map map) { + var c = (Collection) map.get(ViaLocationInputType.FIELD_STOP_LOCATION_IDS); + return c.stream().map(TransitIdMapper::mapIDToDomain).toList(); + } + + /** + * @deprecated Legacy passThrough, use via instead + */ + @Deprecated + private static ViaLocation mapLegacyPassThroughViaLocation(Map inputMap) { + final String name = (String) inputMap.get("name"); + final List stopLocationIds = + ((List) inputMap.get("placeIds")).stream() + .map(TransitIdMapper::mapIDToDomain) + .toList(); + return new PassThroughViaLocation(name, stopLocationIds); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/mapping/ViaLocationMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/mapping/ViaLocationDeprecatedMapper.java similarity index 77% rename from src/main/java/org/opentripplanner/apis/transmodel/mapping/ViaLocationMapper.java rename to src/main/java/org/opentripplanner/apis/transmodel/mapping/ViaLocationDeprecatedMapper.java index af39338fb98..9ef77dfc847 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/mapping/ViaLocationMapper.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/mapping/ViaLocationDeprecatedMapper.java @@ -5,16 +5,17 @@ import java.time.Duration; import java.util.List; import java.util.Map; -import org.opentripplanner.routing.api.request.ViaLocation; +import org.opentripplanner.routing.api.request.ViaLocationDeprecated; import org.opentripplanner.routing.api.response.RoutingError; import org.opentripplanner.routing.api.response.RoutingErrorCode; import org.opentripplanner.routing.error.RoutingValidationException; -class ViaLocationMapper { +@Deprecated +class ViaLocationDeprecatedMapper { - static ViaLocation mapViaLocation(Map viaLocation) { + static ViaLocationDeprecated mapViaLocation(Map viaLocation) { try { - return new ViaLocation( + return new ViaLocationDeprecated( GenericLocationMapper.toGenericLocation(viaLocation), false, (Duration) viaLocation.get("minSlack"), diff --git a/src/main/java/org/opentripplanner/apis/transmodel/mapping/ViaRequestMapper.java b/src/main/java/org/opentripplanner/apis/transmodel/mapping/ViaRequestMapper.java index 0781fbe34a6..082b96c1add 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/mapping/ViaRequestMapper.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/mapping/ViaRequestMapper.java @@ -9,7 +9,7 @@ import org.opentripplanner.framework.graphql.GraphQLUtils; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.RouteViaRequest; -import org.opentripplanner.routing.api.request.ViaLocation; +import org.opentripplanner.routing.api.request.ViaLocationDeprecated; import org.opentripplanner.routing.api.request.request.JourneyRequest; import org.opentripplanner.standalone.api.OtpServerRequestContext; @@ -27,7 +27,10 @@ public static RouteViaRequest createRouteViaRequest(DataFetchingEnvironment envi RouteRequest request = serverContext.defaultRouteRequest(); List> viaInput = environment.getArgument("via"); - List vias = viaInput.stream().map(ViaLocationMapper::mapViaLocation).toList(); + List vias = viaInput + .stream() + .map(ViaLocationDeprecatedMapper::mapViaLocation) + .toList(); List requests; if (environment.containsArgument("segments")) { diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/framework/AuthorityType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/AuthorityType.java index 27d20c8e423..baae947e345 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/framework/AuthorityType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/AuthorityType.java @@ -17,8 +17,7 @@ public class AuthorityType { public static GraphQLObjectType create( GraphQLOutputType lineType, - GraphQLOutputType ptSituationElementType, - GqlUtil gqlUtil + GraphQLOutputType ptSituationElementType ) { return GraphQLObjectType .newObject() @@ -65,7 +64,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("lines") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(lineType))) .dataFetcher(environment -> getTransitService(environment) @@ -80,7 +79,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("situations") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description("Get all situations active for the authority.") .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ptSituationElementType)))) .dataFetcher(environment -> diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/util/EncodedPolylineBeanWithStops.java b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/EncodedPolylineBeanWithStops.java similarity index 80% rename from src/main/java/org/opentripplanner/apis/transmodel/model/util/EncodedPolylineBeanWithStops.java rename to src/main/java/org/opentripplanner/apis/transmodel/model/framework/EncodedPolylineBeanWithStops.java index 92262a17381..23508bf2ef7 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/util/EncodedPolylineBeanWithStops.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/EncodedPolylineBeanWithStops.java @@ -1,4 +1,4 @@ -package org.opentripplanner.apis.transmodel.model.util; +package org.opentripplanner.apis.transmodel.model.framework; import org.opentripplanner.framework.geometry.EncodedPolyline; import org.opentripplanner.transit.model.site.StopLocation; diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/framework/OperatorType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/OperatorType.java index d45319b9d9e..8f55bea52ab 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/framework/OperatorType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/OperatorType.java @@ -14,8 +14,7 @@ public class OperatorType { public static GraphQLObjectType create( GraphQLOutputType lineType, - GraphQLOutputType serviceJourneyType, - GqlUtil gqlUtil + GraphQLOutputType serviceJourneyType ) { return GraphQLObjectType .newObject() @@ -48,7 +47,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("lines") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(lineType))) .dataFetcher(environment -> GqlUtil @@ -64,7 +63,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("serviceJourney") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(serviceJourneyType))) .dataFetcher(environment -> GqlUtil diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/framework/PenaltyForStreetModeType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/PenaltyForStreetModeType.java index 6d945ae912f..b899ada3599 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/framework/PenaltyForStreetModeType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/PenaltyForStreetModeType.java @@ -47,7 +47,7 @@ public class PenaltyForStreetModeType { private static final String FIELD_TIME_PENALTY = "timePenalty"; private static final String FIELD_COST_FACTOR = "costFactor"; - public static GraphQLInputObjectType create(GqlUtil gqlUtil) { + public static GraphQLInputObjectType create() { return GraphQLInputObjectType .newInputObject() .name("PenaltyForStreetMode") @@ -70,7 +70,7 @@ public static GraphQLInputObjectType create(GqlUtil gqlUtil) { GraphQLInputObjectField .newInputObjectField() .name(FIELD_TIME_PENALTY) - .type(new GraphQLNonNull(gqlUtil.doubleFunctionScalar)) + .type(new GraphQLNonNull(TransmodelScalars.DOUBLE_FUNCTION_SCALAR)) .description( """ Penalty applied to the time for the given list of modes. diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/framework/StreetModeDurationInputType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/StreetModeDurationInputType.java index bf274688617..182885fdeef 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/framework/StreetModeDurationInputType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/StreetModeDurationInputType.java @@ -42,7 +42,7 @@ public static GraphQLInputObjectType create(GqlUtil gqlUtil) { GraphQLInputObjectField .newInputObjectField() .name(FIELD_DURATION) - .type(new GraphQLNonNull(gqlUtil.durationScalar)) + .type(new GraphQLNonNull(TransmodelScalars.DURATION_SCALAR)) ) .build(); } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/framework/TransmodelDirectives.java b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/TransmodelDirectives.java new file mode 100644 index 00000000000..98ee99a8a64 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/TransmodelDirectives.java @@ -0,0 +1,14 @@ +package org.opentripplanner.apis.transmodel.model.framework; + +import graphql.introspection.Introspection; +import graphql.schema.GraphQLDirective; + +public class TransmodelDirectives { + + public static final GraphQLDirective TIMING_DATA = GraphQLDirective + .newDirective() + .name("timingData") + .description("Add timing data to prometheus, if Actuator API is enabled") + .validLocation(Introspection.DirectiveLocation.FIELD_DEFINITION) + .build(); +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/framework/TransmodelScalars.java b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/TransmodelScalars.java new file mode 100644 index 00000000000..404fbd4a5ba --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/framework/TransmodelScalars.java @@ -0,0 +1,30 @@ +package org.opentripplanner.apis.transmodel.model.framework; + +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLScalarType; +import org.opentripplanner.apis.transmodel.model.scalars.DoubleFunctionFactory; +import org.opentripplanner.apis.transmodel.model.scalars.LocalTimeScalarFactory; +import org.opentripplanner.apis.transmodel.model.scalars.TimeScalarFactory; +import org.opentripplanner.framework.graphql.scalar.DateScalarFactory; +import org.opentripplanner.framework.graphql.scalar.DurationScalarFactory; + +/** + * This class contains all Transmodel custom scalars, except the + * {@link org.opentripplanner.apis.transmodel.support.GqlUtil#dateTimeScalar}. + */ +public class TransmodelScalars { + + public static final GraphQLScalarType DATE_SCALAR; + public static final GraphQLScalarType DOUBLE_FUNCTION_SCALAR; + public static final GraphQLScalarType LOCAL_TIME_SCALAR; + public static final GraphQLObjectType TIME_SCALAR; + public static final GraphQLScalarType DURATION_SCALAR; + + static { + DATE_SCALAR = DateScalarFactory.createTransmodelDateScalar(); + DOUBLE_FUNCTION_SCALAR = DoubleFunctionFactory.createDoubleFunctionScalar(); + LOCAL_TIME_SCALAR = LocalTimeScalarFactory.createLocalTimeScalar(); + TIME_SCALAR = TimeScalarFactory.createSecondsSinceMidnightAsTimeObject(); + DURATION_SCALAR = DurationScalarFactory.createDurationScalar(); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/network/JourneyPatternType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/network/JourneyPatternType.java index 0743b02a3c4..4ed3871ff8c 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/network/JourneyPatternType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/network/JourneyPatternType.java @@ -16,6 +16,8 @@ import org.locationtech.jts.geom.LineString; import org.opentripplanner.apis.transmodel.mapping.GeometryMapper; import org.opentripplanner.apis.transmodel.model.EnumTypes; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelDirectives; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.framework.geometry.EncodedPolyline; import org.opentripplanner.transit.model.network.TripPattern; @@ -33,8 +35,7 @@ public static GraphQLObjectType create( GraphQLOutputType lineType, GraphQLOutputType serviceJourneyType, GraphQLOutputType stopToStopGeometryType, - GraphQLNamedOutputType ptSituationElementType, - GqlUtil gqlUtil + GraphQLNamedOutputType ptSituationElementType ) { return GraphQLObjectType .newObject() @@ -68,7 +69,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("serviceJourneys") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(serviceJourneyType)))) .dataFetcher(e -> ((TripPattern) e.getSource()).scheduledTripsAsStream().collect(Collectors.toList()) @@ -79,9 +80,11 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("serviceJourneysForDate") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description("List of service journeys for the journey pattern for a given date") - .argument(GraphQLArgument.newArgument().name("date").type(gqlUtil.dateScalar).build()) + .argument( + GraphQLArgument.newArgument().name("date").type(TransmodelScalars.DATE_SCALAR).build() + ) .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(serviceJourneyType)))) .dataFetcher(environment -> { TIntSet services = GqlUtil diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/network/StopToStopGeometryType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/network/StopToStopGeometryType.java index 800eda04cb2..98a965e50be 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/network/StopToStopGeometryType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/network/StopToStopGeometryType.java @@ -3,7 +3,7 @@ import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLOutputType; -import org.opentripplanner.apis.transmodel.model.util.EncodedPolylineBeanWithStops; +import org.opentripplanner.apis.transmodel.model.framework.EncodedPolylineBeanWithStops; public class StopToStopGeometryType { diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/FilterInputType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/FilterInputType.java index ce792ca3790..1e868eadb86 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/FilterInputType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/FilterInputType.java @@ -7,7 +7,7 @@ public class FilterInputType { - static final GraphQLInputObjectType INPUT_TYPE = GraphQLInputObjectType + public static final GraphQLInputObjectType INPUT_TYPE = GraphQLInputObjectType .newInputObject() .name("TripFilterInput") .description( diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ItineraryFiltersInputType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ItineraryFiltersInputType.java index b5d82af5138..7f0d5b10215 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ItineraryFiltersInputType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ItineraryFiltersInputType.java @@ -8,6 +8,7 @@ import java.util.Map; import java.util.function.Consumer; import org.opentripplanner.apis.transmodel.model.EnumTypes; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.model.scalars.DoubleFunction; import org.opentripplanner.apis.transmodel.support.DataFetcherDecorator; import org.opentripplanner.apis.transmodel.support.GqlUtil; @@ -26,7 +27,7 @@ public class ItineraryFiltersInputType { private static final String GROUP_SIMILARITY_KEEP_N_ITINERARIES = "groupSimilarityKeepNumOfItineraries"; - public static GraphQLInputObjectType create(GqlUtil gqlUtil, ItineraryFilterPreferences dft) { + public static GraphQLInputObjectType create(ItineraryFilterPreferences dft) { return GraphQLInputObjectType .newInputObject() .name("ItineraryFilters") @@ -59,7 +60,7 @@ public static GraphQLInputObjectType create(GqlUtil gqlUtil, ItineraryFilterPref GraphQLInputObjectField .newInputObjectField() .name("costLimitFunction") - .type(new GraphQLNonNull(gqlUtil.doubleFunctionScalar)) + .type(new GraphQLNonNull(TransmodelScalars.DOUBLE_FUNCTION_SCALAR)) .build() ) .field( diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/LegType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/LegType.java index 867d08e3933..ec7b736f7a6 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/LegType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/LegType.java @@ -22,6 +22,8 @@ import org.opentripplanner.apis.transmodel.model.EnumTypes; import org.opentripplanner.apis.transmodel.model.TransmodelTransportSubmode; import org.opentripplanner.apis.transmodel.model.TripTimeOnDateHelper; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelDirectives; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.framework.geometry.EncodedPolyline; import org.opentripplanner.model.plan.Leg; @@ -268,7 +270,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("fromEstimatedCall") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description("EstimatedCall for the quay where the leg originates.") .type(estimatedCallType) .dataFetcher(env -> TripTimeOnDateHelper.getTripTimeOnDateForFromPlace(env.getSource())) @@ -278,7 +280,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("toEstimatedCall") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description("EstimatedCall for the quay where the leg ends.") .type(estimatedCallType) .dataFetcher(env -> TripTimeOnDateHelper.getTripTimeOnDateForToPlace(env.getSource())) @@ -318,7 +320,7 @@ public static GraphQLObjectType create( .description( "For transit legs, the service date of the trip. For non-transit legs, null." ) - .type(gqlUtil.dateScalar) + .type(TransmodelScalars.DATE_SCALAR) .dataFetcher(environment -> Optional.of((Leg) environment.getSource()).map(Leg::getServiceDate).orElse(null) ) @@ -352,7 +354,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("intermediateEstimatedCalls") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description( "For ride legs, estimated calls for quays between the Place where the leg originates and the Place where the leg ends. For non-ride legs, empty list." ) @@ -366,7 +368,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("serviceJourneyEstimatedCalls") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description( "For ride legs, all estimated calls for the service journey. For non-ride legs, empty list." ) diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java index b67b26b90b6..24acaecda4a 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/TripQuery.java @@ -18,6 +18,7 @@ import org.opentripplanner.apis.transmodel.model.framework.LocationInputType; import org.opentripplanner.apis.transmodel.model.framework.PassThroughPointInputType; import org.opentripplanner.apis.transmodel.model.framework.PenaltyForStreetModeType; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelDirectives; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.routing.api.request.preference.RoutingPreferences; import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; @@ -27,6 +28,12 @@ public class TripQuery { public static final String ACCESS_EGRESS_PENALTY = "accessEgressPenalty"; public static final String MAX_ACCESS_EGRESS_DURATION_FOR_MODE = "maxAccessEgressDurationForMode"; public static final String MAX_DIRECT_DURATION_FOR_MODE = "maxDirectDurationForMode"; + public static final String FIELD_VIA = "via"; + public static final String DOC_VIA = + """ + The list of via locations the journey is required to visit. All locations are + visited in the order they are listed. + """; public static GraphQLFieldDefinition create( DefaultRouteRequestType routing, @@ -45,7 +52,7 @@ public static GraphQLFieldDefinition create( "trip patterns describing suggested alternatives for the trip." ) .type(new GraphQLNonNull(tripType)) - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .argument( GraphQLArgument .newArgument() @@ -173,10 +180,19 @@ Normally this is when the search is performed (now), plus a small grace period t GraphQLArgument .newArgument() .name("passThroughPoints") + .deprecate("Use via instead") .description("The list of points the journey is required to pass through.") .type(new GraphQLList(new GraphQLNonNull(PassThroughPointInputType.INPUT_TYPE))) .build() ) + .argument( + GraphQLArgument + .newArgument() + .name(FIELD_VIA) + .description(DOC_VIA) + .type(new GraphQLList(new GraphQLNonNull(ViaLocationInputType.VIA_LOCATION_INPUT))) + .build() + ) .argument( GraphQLArgument .newArgument() @@ -572,7 +588,7 @@ Normally this is when the search is performed (now), plus a small grace period t "Configure the itinerary-filter-chain. NOTE! THESE PARAMETERS ARE USED " + "FOR SERVER-SIDE TUNING AND IS AVAILABLE HERE FOR TESTING ONLY." ) - .type(ItineraryFiltersInputType.create(gqlUtil, preferences.itineraryFilter())) + .type(ItineraryFiltersInputType.create(preferences.itineraryFilter())) .build() ) .argument( diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaLocationInputType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaLocationInputType.java index 6ce02240817..efe4585aa96 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaLocationInputType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaLocationInputType.java @@ -1,78 +1,114 @@ package org.opentripplanner.apis.transmodel.model.plan; +import static graphql.Directives.OneOfDirective; + import graphql.Scalars; -import graphql.schema.GraphQLInputObjectField; +import graphql.language.StringValue; import graphql.schema.GraphQLInputObjectType; -import org.opentripplanner.apis.transmodel.model.framework.CoordinateInputType; -import org.opentripplanner.apis.transmodel.support.GqlUtil; -import org.opentripplanner.routing.api.request.ViaLocation; +import graphql.schema.GraphQLList; +import java.time.Duration; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; public class ViaLocationInputType { - public static GraphQLInputObjectType create(GqlUtil gqlUtil) { - return GraphQLInputObjectType - .newInputObject() - .name("ViaLocationInput") - .description( - "Input format for specifying a location through either a place reference (id), " + - "coordinates or both. If both place and coordinates are provided the place ref will be " + - "used if found, coordinates will only be used if place is not known. The location also " + - "contain information about the minimum and maximum time the user is willing to stay at " + - "the via location." - ) - .field( - GraphQLInputObjectField - .newInputObjectField() - .name("name") - .description( - "The name of the location. This is pass-through information" + - " and is not used in routing." - ) - .type(Scalars.GraphQLString) - .build() - ) - .field( - GraphQLInputObjectField - .newInputObjectField() - .name("place") - .description( - "The id of an element in the OTP model. Currently supports" + - " Quay, StopPlace, multimodal StopPlace, and GroupOfStopPlaces." - ) - .type(Scalars.GraphQLString) - .build() - ) - .field( - GraphQLInputObjectField - .newInputObjectField() - .name("coordinates") - .description( - "Coordinates for the location. This can be used alone or as" + - " fallback if the place id is not found." - ) - .type(CoordinateInputType.INPUT_TYPE) - .build() - ) - .field( - GraphQLInputObjectField - .newInputObjectField() - .name("minSlack") - .defaultValue(ViaLocation.DEFAULT_MIN_SLACK) - .description( - "The minimum time the user wants to stay in the via location before continuing his journey" - ) - .type(gqlUtil.durationScalar) - ) - .field( - GraphQLInputObjectField - .newInputObjectField() - .name("maxSlack") - .defaultValue(ViaLocation.DEFAULT_MAX_SLACK) - .description( - "The maximum time the user wants to stay in the via location before continuing his journey" - ) - .type(gqlUtil.durationScalar) - ) - .build(); - } + /* type constants */ + + private static final String INPUT_VIA_LOCATION = "PlanViaLocationInput"; + private static final String INPUT_VISIT_VIA_LOCATION = "PlanVisitViaLocationInput"; + private static final String INPUT_PASS_THROUGH_VIA_LOCATION = "PlanPassThroughViaLocationInput"; + + private static final String DOC_VISIT_VIA_LOCATION = + """ + A visit-via-location is a physical visit to one of the stops or coordinates listed. An + on-board visit does not count, the traveler must alight or board at the given stop for + it to to be accepted. To visit a coordinate, the traveler must walk(bike or drive) to + the closest point in the street network from a stop and back to another stop to join + the transit network. + + NOTE! Coordinates are NOT supported jet. + """; + private static final String DOC_PASS_THROUGH_VIA_LOCATION = + """ + One of the listed stop locations must be visited on-board a transit vehicle or the journey must + alight or board at the location. + """; + private static final String DOC_VIA_LOCATION = + """ + A via-location is used to specifying a location as an intermediate place the router must + route through. The via-location must be either a pass-through-location or a + visit-via-location. An on-board "visit" is only allowed for pass-through-via-locations, while + the visit-via-location can visit a stop-location or a coordinate and specify a + minimum-wait-time. + """; + + /* field constants */ + + public static final String FIELD_LABEL = "label"; + public static final String FIELD_MINIMUM_WAIT_TIME = "minimumWaitTime"; + public static final String FIELD_STOP_LOCATION_IDS = "stopLocationIds"; + + // TODO : Add coordinates + //private static final String FIELD_COORDINATES = "coordinates"; + public static final String FIELD_VISIT = "visit"; + public static final String FIELD_PASS_THROUGH = "passThrough"; + + private static final String DOC_LABEL = + "The label/name of the location. This is pass-through " + + "information and is not used in routing."; + private static final String DOC_MINIMUM_WAIT_TIME = + """ + The minimum wait time is used to force the trip to stay the given duration at the + via-location before the trip is continued. + """; + private static final String DOC_STOP_LOCATION_IDS = + """ + A list of stop locations. A stop location can be a quay, a stop place, a multimodal + stop place or a group of stop places. It is enough to visit ONE of the locations + listed. + """; + + static final GraphQLInputObjectType VISIT_VIA_LOCATION_INPUT = GraphQLInputObjectType + .newInputObject() + .name(INPUT_VISIT_VIA_LOCATION) + .description(DOC_VISIT_VIA_LOCATION) + .field(b -> b.name(FIELD_LABEL).description(DOC_LABEL).type(Scalars.GraphQLString)) + .field(b -> + b + .name(FIELD_MINIMUM_WAIT_TIME) + .description(DOC_MINIMUM_WAIT_TIME) + .type(TransmodelScalars.DURATION_SCALAR) + .defaultValueLiteral(StringValue.of(Duration.ZERO.toString())) + ) + .field(b -> + b + .name(FIELD_STOP_LOCATION_IDS) + .description(DOC_STOP_LOCATION_IDS) + .type(GraphQLList.list(Scalars.GraphQLString)) + ) + /* + TODO: Add support for coordinates + */ + .build(); + + static final GraphQLInputObjectType PASS_THROUGH_VIA_LOCATION_INPUT = GraphQLInputObjectType + .newInputObject() + .name(INPUT_PASS_THROUGH_VIA_LOCATION) + .description(DOC_PASS_THROUGH_VIA_LOCATION) + .field(b -> b.name(FIELD_LABEL).description(DOC_LABEL).type(Scalars.GraphQLString)) + .field(b -> + b + .name(FIELD_STOP_LOCATION_IDS) + .description(DOC_STOP_LOCATION_IDS) + .type(GraphQLList.list(Scalars.GraphQLString)) + ) + .build(); + + public static final GraphQLInputObjectType VIA_LOCATION_INPUT = GraphQLInputObjectType + .newInputObject() + .name(INPUT_VIA_LOCATION) + .description(DOC_VIA_LOCATION) + .withDirective(OneOfDirective) + .field(b -> b.name(FIELD_VISIT).type(VISIT_VIA_LOCATION_INPUT)) + .field(b -> b.name(FIELD_PASS_THROUGH).type(PASS_THROUGH_VIA_LOCATION_INPUT)) + .build(); } diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/legacyvia/ViaLocationInputType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/legacyvia/ViaLocationInputType.java new file mode 100644 index 00000000000..1f26d12e7f7 --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/legacyvia/ViaLocationInputType.java @@ -0,0 +1,78 @@ +package org.opentripplanner.apis.transmodel.model.plan.legacyvia; + +import graphql.Scalars; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import org.opentripplanner.apis.transmodel.model.framework.CoordinateInputType; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; +import org.opentripplanner.routing.api.request.ViaLocationDeprecated; + +public class ViaLocationInputType { + + public static GraphQLInputObjectType create() { + return GraphQLInputObjectType + .newInputObject() + .name("ViaLocationInput") + .description( + "Input format for specifying a location through either a place reference (id), " + + "coordinates or both. If both place and coordinates are provided the place ref will be " + + "used if found, coordinates will only be used if place is not known. The location also " + + "contain information about the minimum and maximum time the user is willing to stay at " + + "the via location." + ) + .field( + GraphQLInputObjectField + .newInputObjectField() + .name("name") + .description( + "The name of the location. This is pass-through information" + + " and is not used in routing." + ) + .type(Scalars.GraphQLString) + .build() + ) + .field( + GraphQLInputObjectField + .newInputObjectField() + .name("place") + .description( + "The id of an element in the OTP model. Currently supports" + + " Quay, StopPlace, multimodal StopPlace, and GroupOfStopPlaces." + ) + .type(Scalars.GraphQLString) + .build() + ) + .field( + GraphQLInputObjectField + .newInputObjectField() + .name("coordinates") + .description( + "Coordinates for the location. This can be used alone or as" + + " fallback if the place id is not found." + ) + .type(CoordinateInputType.INPUT_TYPE) + .build() + ) + .field( + GraphQLInputObjectField + .newInputObjectField() + .name("minSlack") + .defaultValue(ViaLocationDeprecated.DEFAULT_MIN_SLACK) + .description( + "The minimum time the user wants to stay in the via location before continuing his journey" + ) + .type(TransmodelScalars.DURATION_SCALAR) + ) + .field( + GraphQLInputObjectField + .newInputObjectField() + .name("maxSlack") + .defaultValue(ViaLocationDeprecated.DEFAULT_MAX_SLACK) + .description( + "The maximum time the user wants to stay in the via location before continuing his journey" + ) + .type(TransmodelScalars.DURATION_SCALAR) + ) + .build(); + } +} diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaSegmentInputType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/legacyvia/ViaSegmentInputType.java similarity index 95% rename from src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaSegmentInputType.java rename to src/main/java/org/opentripplanner/apis/transmodel/model/plan/legacyvia/ViaSegmentInputType.java index 0d591a1357a..a2ada7487c3 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaSegmentInputType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/legacyvia/ViaSegmentInputType.java @@ -1,10 +1,11 @@ -package org.opentripplanner.apis.transmodel.model.plan; +package org.opentripplanner.apis.transmodel.model.plan.legacyvia; import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLList; import graphql.schema.GraphQLNonNull; import org.opentripplanner.apis.transmodel.model.EnumTypes; +import org.opentripplanner.apis.transmodel.model.plan.FilterInputType; public class ViaSegmentInputType { diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaTripQuery.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/legacyvia/ViaTripQuery.java similarity index 94% rename from src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaTripQuery.java rename to src/main/java/org/opentripplanner/apis/transmodel/model/plan/legacyvia/ViaTripQuery.java index 8641dbc60c2..e0616354a99 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaTripQuery.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/legacyvia/ViaTripQuery.java @@ -1,4 +1,4 @@ -package org.opentripplanner.apis.transmodel.model.plan; +package org.opentripplanner.apis.transmodel.model.plan.legacyvia; import graphql.Scalars; import graphql.schema.GraphQLArgument; @@ -11,6 +11,8 @@ import org.opentripplanner.apis.transmodel.model.DefaultRouteRequestType; import org.opentripplanner.apis.transmodel.model.EnumTypes; import org.opentripplanner.apis.transmodel.model.framework.LocationInputType; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelDirectives; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.support.GqlUtil; public class ViaTripQuery { @@ -28,8 +30,9 @@ public static GraphQLFieldDefinition create( .description( "Via trip search. Find trip patterns traveling via one or more intermediate (via) locations." ) + .deprecate("The the regular plan query with via stop instead.") .type(new GraphQLNonNull(viaTripType)) - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .argument( GraphQLArgument .newArgument() @@ -72,7 +75,7 @@ public static GraphQLFieldDefinition create( "The search-window used is returned to the response metadata as `searchWindowUsed` for " + "debugging purposes." ) - .type(new GraphQLNonNull(gqlUtil.durationScalar)) + .type(new GraphQLNonNull(TransmodelScalars.DURATION_SCALAR)) .build() ) .argument( diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaTripType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/legacyvia/ViaTripType.java similarity index 98% rename from src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaTripType.java rename to src/main/java/org/opentripplanner/apis/transmodel/model/plan/legacyvia/ViaTripType.java index 8b55222f3b8..740664c02f0 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/plan/ViaTripType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/plan/legacyvia/ViaTripType.java @@ -1,4 +1,4 @@ -package org.opentripplanner.apis.transmodel.model.plan; +package org.opentripplanner.apis.transmodel.model.plan.legacyvia; import graphql.Scalars; import graphql.schema.DataFetchingEnvironment; diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/siri/et/EstimatedCallType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/siri/et/EstimatedCallType.java index 3587b3dc0f6..612a0faa4b0 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/siri/et/EstimatedCallType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/siri/et/EstimatedCallType.java @@ -17,6 +17,8 @@ import java.util.Set; import org.opentripplanner.apis.transmodel.mapping.OccupancyStatusMapper; import org.opentripplanner.apis.transmodel.model.EnumTypes; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelDirectives; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.model.TripTimeOnDate; import org.opentripplanner.routing.alertpatch.StopCondition; @@ -271,7 +273,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("date") - .type(new GraphQLNonNull(gqlUtil.dateScalar)) + .type(new GraphQLNonNull(TransmodelScalars.DATE_SCALAR)) .description("The date the estimated call is valid for.") .dataFetcher(environment -> ((TripTimeOnDate) environment.getSource()).getServiceDay()) .build() @@ -326,7 +328,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("situations") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ptSituationElementType)))) .description("Get all relevant situations for this EstimatedCall.") .dataFetcher(environment -> diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/siri/sx/AffectsType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/siri/sx/AffectsType.java index 4aded08e3e8..58bf9d3f958 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/siri/sx/AffectsType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/siri/sx/AffectsType.java @@ -8,6 +8,7 @@ import graphql.schema.GraphQLOutputType; import graphql.schema.GraphQLUnionType; import org.opentripplanner.apis.transmodel.model.EnumTypes; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.model.stop.StopPlaceType; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.routing.alertpatch.EntitySelector; @@ -23,8 +24,7 @@ public static GraphQLOutputType create( GraphQLOutputType stopPlaceType, GraphQLOutputType lineType, GraphQLOutputType serviceJourneyType, - GraphQLOutputType datedServiceJourneyType, - GqlUtil gqlUtil + GraphQLOutputType datedServiceJourneyType ) { GraphQLObjectType affectedStopPlace = GraphQLObjectType .newObject() @@ -96,7 +96,7 @@ public static GraphQLOutputType create( GraphQLFieldDefinition .newFieldDefinition() .name("operatingDay") - .type(gqlUtil.dateScalar) + .type(TransmodelScalars.DATE_SCALAR) .dataFetcher(environment -> environment.getSource().serviceDate()) .build() ) @@ -204,7 +204,7 @@ public static GraphQLOutputType create( GraphQLFieldDefinition .newFieldDefinition() .name("operatingDay") - .type(gqlUtil.dateScalar) + .type(TransmodelScalars.DATE_SCALAR) .dataFetcher(environment -> environment.getSource().serviceDate() ) diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/stop/QuayType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/stop/QuayType.java index e146b537c1c..ae01a9013f6 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/stop/QuayType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/stop/QuayType.java @@ -19,6 +19,7 @@ import java.util.Optional; import org.locationtech.jts.geom.Geometry; import org.opentripplanner.apis.transmodel.model.EnumTypes; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelDirectives; import org.opentripplanner.apis.transmodel.model.plan.JourneyWhiteListed; import org.opentripplanner.apis.transmodel.model.scalars.GeoJSONCoordinatesScalar; import org.opentripplanner.apis.transmodel.support.GqlUtil; @@ -169,7 +170,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("lines") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description("List of lines servicing this quay") .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(lineType)))) .dataFetcher(env -> @@ -187,7 +188,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("journeyPatterns") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description("List of journey patterns servicing this quay") .type(new GraphQLNonNull(new GraphQLList(journeyPatternType))) .dataFetcher(env -> @@ -199,7 +200,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("estimatedCalls") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description("List of visits to this quay as part of vehicle journeys.") .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(estimatedCallType)))) .argument( diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/stop/StopPlaceType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/stop/StopPlaceType.java index 53621ee9b5a..57a1a81bcde 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/stop/StopPlaceType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/stop/StopPlaceType.java @@ -29,6 +29,7 @@ import org.opentripplanner.apis.transmodel.mapping.TransitIdMapper; import org.opentripplanner.apis.transmodel.model.EnumTypes; import org.opentripplanner.apis.transmodel.model.TransmodelTransportSubmode; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelDirectives; import org.opentripplanner.apis.transmodel.model.plan.JourneyWhiteListed; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.framework.graphql.GraphQLUtils; @@ -227,7 +228,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("quays") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description("Returns all quays that are children of this stop place") .type(new GraphQLList(quayType)) .argument( @@ -285,7 +286,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("estimatedCalls") - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description("List of visits to this stop place as part of vehicle journeys.") .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(estimatedCallType)))) .argument( diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/BookingArrangementType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/BookingArrangementType.java index de0f86b1166..727dc37d99b 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/BookingArrangementType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/BookingArrangementType.java @@ -7,14 +7,14 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLOutputType; import org.opentripplanner.apis.transmodel.model.EnumTypes; -import org.opentripplanner.apis.transmodel.support.GqlUtil; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.transit.model.organization.ContactInfo; import org.opentripplanner.transit.model.timetable.booking.BookingInfo; import org.opentripplanner.transit.model.timetable.booking.BookingTime; public class BookingArrangementType { - public static GraphQLObjectType create(GqlUtil gqlUtil) { + public static GraphQLObjectType create() { GraphQLOutputType contactType = GraphQLObjectType .newObject() .name("Contact") @@ -82,7 +82,7 @@ public static GraphQLObjectType create(GqlUtil gqlUtil) { .newFieldDefinition() .name("latestBookingTime") .description("Latest time the service can be booked. ISO 8601 timestamp") - .type(gqlUtil.localTimeScalar) + .type(TransmodelScalars.LOCAL_TIME_SCALAR) .dataFetcher(environment -> { final BookingTime latestBookingTime = (bookingInfo(environment)).getLatestBookingTime(); return latestBookingTime == null ? null : latestBookingTime.getTime(); diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyQuery.java b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyQuery.java index e3fbf90a35d..aef659901e4 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyQuery.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyQuery.java @@ -12,6 +12,7 @@ import java.util.List; import org.opentripplanner.apis.transmodel.mapping.TransitIdMapper; import org.opentripplanner.apis.transmodel.model.EnumTypes; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.transit.api.request.TripOnServiceDateRequest; import org.opentripplanner.transit.api.request.TripOnServiceDateRequestBuilder; @@ -38,10 +39,7 @@ public static GraphQLFieldDefinition createGetById(GraphQLOutputType datedServic .build(); } - public static GraphQLFieldDefinition createQuery( - GraphQLOutputType datedServiceJourneyType, - GqlUtil gqlUtil - ) { + public static GraphQLFieldDefinition createQuery(GraphQLOutputType datedServiceJourneyType) { return GraphQLFieldDefinition .newFieldDefinition() .name("datedServiceJourneys") @@ -69,7 +67,9 @@ public static GraphQLFieldDefinition createQuery( GraphQLArgument .newArgument() .name("operatingDays") - .type(new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(gqlUtil.dateScalar)))) + .type( + new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(TransmodelScalars.DATE_SCALAR))) + ) ) .argument( GraphQLArgument diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyType.java index faeccf930c6..7fcce88c4ee 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/DatedServiceJourneyType.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.Optional; import org.opentripplanner.apis.transmodel.model.EnumTypes; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelDirectives; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.routing.TripTimeOnDateHelper; import org.opentripplanner.transit.model.network.TripPattern; @@ -33,8 +35,7 @@ public static GraphQLObjectType create( GraphQLOutputType serviceJourneyType, GraphQLOutputType journeyPatternType, GraphQLType estimatedCallType, - GraphQLType quayType, - GqlUtil gqlUtil + GraphQLType quayType ) { return GraphQLObjectType .newObject() @@ -48,7 +49,7 @@ public static GraphQLObjectType create( .description( "The date this service runs. The date used is based on the service date as opposed to calendar date." ) - .type(gqlUtil.dateScalar) + .type(TransmodelScalars.DATE_SCALAR) .dataFetcher(environment -> Optional .of(tripOnServiceDate(environment)) @@ -142,7 +143,7 @@ public static GraphQLObjectType create( .newFieldDefinition() .name("estimatedCalls") .type(new GraphQLList(estimatedCallType)) - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description( "Returns scheduled passingTimes for this dated service journey, " + "updated with real-time-updates (if available). " diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/ServiceJourneyType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/ServiceJourneyType.java index c0c96b1e393..e61d0a12edc 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/ServiceJourneyType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/ServiceJourneyType.java @@ -17,6 +17,8 @@ import org.locationtech.jts.geom.LineString; import org.opentripplanner.apis.transmodel.model.EnumTypes; import org.opentripplanner.apis.transmodel.model.TransmodelTransportSubmode; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelDirectives; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.framework.geometry.EncodedPolyline; import org.opentripplanner.model.TripTimeOnDate; @@ -41,8 +43,7 @@ public static GraphQLObjectType create( GraphQLOutputType ptSituationElementType, GraphQLOutputType journeyPatternType, GraphQLOutputType estimatedCallType, - GraphQLOutputType timetabledPassingTimeType, - GqlUtil gqlUtil + GraphQLOutputType timetabledPassingTimeType ) { return GraphQLObjectType .newObject() @@ -61,8 +62,8 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("activeDates") - .withDirective(gqlUtil.timingData) - .type(new GraphQLNonNull(new GraphQLList(gqlUtil.dateScalar))) + .withDirective(TransmodelDirectives.TIMING_DATA) + .type(new GraphQLNonNull(new GraphQLList(TransmodelScalars.DATE_SCALAR))) .dataFetcher(environment -> GqlUtil .getTransitService(environment) @@ -235,7 +236,7 @@ public static GraphQLObjectType create( .newFieldDefinition() .name("passingTimes") .type(new GraphQLNonNull(new GraphQLList(timetabledPassingTimeType))) - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description( "Returns scheduled passing times only - without real-time-updates, for realtime-data use 'estimatedCalls'" ) @@ -254,7 +255,7 @@ public static GraphQLObjectType create( .newFieldDefinition() .name("estimatedCalls") .type(new GraphQLList(estimatedCallType)) - .withDirective(gqlUtil.timingData) + .withDirective(TransmodelDirectives.TIMING_DATA) .description( "Returns scheduled passingTimes for this ServiceJourney for a given date, updated with real-time-updates (if available). " + "NB! This takes a date as argument (default=today) and returns estimatedCalls for that date and should only be used if the date is " + @@ -264,7 +265,7 @@ public static GraphQLObjectType create( GraphQLArgument .newArgument() .name("date") - .type(gqlUtil.dateScalar) + .type(TransmodelScalars.DATE_SCALAR) .description("Date to get estimated calls for. Defaults to today.") .build() ) diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/TimetabledPassingTimeType.java b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/TimetabledPassingTimeType.java index 9370ed74d93..0f4a40f0750 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/TimetabledPassingTimeType.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/timetable/TimetabledPassingTimeType.java @@ -8,6 +8,7 @@ import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLOutputType; import graphql.schema.GraphQLTypeReference; +import org.opentripplanner.apis.transmodel.model.framework.TransmodelScalars; import org.opentripplanner.apis.transmodel.support.GqlUtil; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.framework.application.OTPFeature; @@ -25,8 +26,7 @@ public static GraphQLObjectType create( GraphQLOutputType noticeType, GraphQLOutputType quayType, GraphQLOutputType destinationDisplayType, - GraphQLOutputType serviceJourneyType, - GqlUtil gqlUtil + GraphQLOutputType serviceJourneyType ) { return GraphQLObjectType .newObject() @@ -44,7 +44,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("arrival") - .type(gqlUtil.timeScalar) + .type(TransmodelScalars.TIME_SCALAR) .description("Scheduled time of arrival at quay") .dataFetcher(environment -> missingValueToNull(((TripTimeOnDate) environment.getSource()).getScheduledArrival()) @@ -55,7 +55,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("departure") - .type(gqlUtil.timeScalar) + .type(TransmodelScalars.TIME_SCALAR) .description("Scheduled time of departure from quay") .dataFetcher(environment -> missingValueToNull(((TripTimeOnDate) environment.getSource()).getScheduledDeparture()) @@ -111,7 +111,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("earliestDepartureTime") - .type(gqlUtil.timeScalar) + .type(TransmodelScalars.TIME_SCALAR) .description( "Earliest possible departure time for a service journey with a service window." ) @@ -131,7 +131,7 @@ public static GraphQLObjectType create( GraphQLFieldDefinition .newFieldDefinition() .name("latestArrivalTime") - .type(gqlUtil.timeScalar) + .type(TransmodelScalars.TIME_SCALAR) .description( "Latest possible (planned) arrival time for a service journey with a service window." ) diff --git a/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java b/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java index 16083085500..78e22e3a6af 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/support/GqlUtil.java @@ -1,14 +1,11 @@ package org.opentripplanner.apis.transmodel.support; import graphql.Scalars; -import graphql.introspection.Introspection.DirectiveLocation; import graphql.schema.DataFetchingEnvironment; -import graphql.schema.GraphQLDirective; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLList; import graphql.schema.GraphQLNonNull; -import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLScalarType; import java.time.ZoneId; import java.util.List; @@ -16,12 +13,7 @@ import org.opentripplanner.apis.transmodel.TransmodelRequestContext; import org.opentripplanner.apis.transmodel.mapping.TransitIdMapper; import org.opentripplanner.apis.transmodel.model.scalars.DateTimeScalarFactory; -import org.opentripplanner.apis.transmodel.model.scalars.DoubleFunctionFactory; -import org.opentripplanner.apis.transmodel.model.scalars.LocalTimeScalarFactory; -import org.opentripplanner.apis.transmodel.model.scalars.TimeScalarFactory; import org.opentripplanner.framework.graphql.GraphQLUtils; -import org.opentripplanner.framework.graphql.scalar.DateScalarFactory; -import org.opentripplanner.framework.graphql.scalar.DurationScalarFactory; import org.opentripplanner.routing.graphfinder.GraphFinder; import org.opentripplanner.routing.vehicle_parking.VehicleParkingService; import org.opentripplanner.service.vehiclerental.VehicleRentalService; @@ -34,29 +26,11 @@ public class GqlUtil { public final GraphQLScalarType dateTimeScalar; - public final GraphQLScalarType dateScalar; - public final GraphQLScalarType doubleFunctionScalar; - public final GraphQLScalarType localTimeScalar; - public final GraphQLObjectType timeScalar; - public final GraphQLScalarType durationScalar; - public final GraphQLDirective timingData; /** private to prevent util class from instantiation */ public GqlUtil(ZoneId timeZone) { this.dateTimeScalar = DateTimeScalarFactory.createMillisecondsSinceEpochAsDateTimeStringScalar(timeZone); - this.dateScalar = DateScalarFactory.createTransmodelDateScalar(); - this.doubleFunctionScalar = DoubleFunctionFactory.createDoubleFunctionScalar(); - this.localTimeScalar = LocalTimeScalarFactory.createLocalTimeScalar(); - this.timeScalar = TimeScalarFactory.createSecondsSinceMidnightAsTimeObject(); - this.durationScalar = DurationScalarFactory.createDurationScalar(); - this.timingData = - GraphQLDirective - .newDirective() - .name("timingData") - .description("Add timing data to prometheus, if Actuator API is enabled") - .validLocation(DirectiveLocation.FIELD_DEFINITION) - .build(); } public static TransitService getTransitService(DataFetchingEnvironment environment) { diff --git a/src/main/java/org/opentripplanner/apis/transmodel/support/OneOfInputValidator.java b/src/main/java/org/opentripplanner/apis/transmodel/support/OneOfInputValidator.java new file mode 100644 index 00000000000..47fca16630e --- /dev/null +++ b/src/main/java/org/opentripplanner/apis/transmodel/support/OneOfInputValidator.java @@ -0,0 +1,60 @@ +package org.opentripplanner.apis.transmodel.support; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * Validate @oneOf directive, this validation is NOT done by the Java GraphQL library at the + * moment(v22.1). Remove this when enforced by the library. The {@code @oneOf} is an experimental + * feature in this version of the library. + *

+ * See {@link graphql.Directives#OneOfDirective} + */ +public class OneOfInputValidator { + + /** + * Validate that the {@code parent} {@code map} only has one entry. + * + * @return the field with a value set. + */ + public static String validateOneOf( + Map map, + String parent, + String... definedFields + ) { + var fieldsInInput = Arrays + .stream(definedFields) + .map(k -> map.containsKey(k) ? k : null) + .filter(Objects::nonNull) + .toList(); + + if (fieldsInInput.isEmpty()) { + throw new IllegalArgumentException( + "No entries in '%s @oneOf'. One of '%s' must be set.".formatted( + parent, + String.join("', '", definedFields) + ) + ); + } + if (fieldsInInput.size() > 1) { + throw new IllegalArgumentException( + "Only one entry in '%s @oneOf' is allowed. Set: '%s'".formatted( + parent, + String.join("', '", fieldsInInput) + ) + ); + } + + var field = fieldsInInput.getFirst(); + if (map.get(field) instanceof Collection c) { + if (c.isEmpty()) { + throw new IllegalArgumentException( + "'%s' can not be empty in '%s @oneOf'.".formatted(field, parent) + ); + } + } + return field; + } +} diff --git a/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java b/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java index 63f1df1aad5..969f63f145e 100644 --- a/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java +++ b/src/main/java/org/opentripplanner/framework/collection/CollectionUtils.java @@ -50,6 +50,19 @@ public static boolean isEmpty(@Nullable Collection c) { return c == null || c.isEmpty(); } + /** + * A null-safe version of isEmpty() for a collection. + *

+ * If the collection is {@code null} then {@code true} is returned. + *

+ * If the collection is empty then {@code true} is returned. + *

+ * Otherwise {@code false} is returned. + */ + public static boolean isEmpty(@Nullable Map m) { + return m == null || m.isEmpty(); + } + /** * Look up the given key in a Map, return null if the key is null. * This prevents a NullPointerException if the underlying implementation of the map does not diff --git a/src/main/java/org/opentripplanner/framework/time/DurationUtils.java b/src/main/java/org/opentripplanner/framework/time/DurationUtils.java index 9d16ed7d269..db7facb9e21 100644 --- a/src/main/java/org/opentripplanner/framework/time/DurationUtils.java +++ b/src/main/java/org/opentripplanner/framework/time/DurationUtils.java @@ -186,11 +186,38 @@ public static Duration requireNonNegative(Duration value) { return value; } + /** + * Checks that duration is in positive and less than the given {@code maxLimit}(exclusive). + * + * @param subject used to identify name of the problematic value when throwing an exception. + */ + public static Duration requireNonNegative(Duration value, Duration maxLimit, String subject) { + Objects.requireNonNull(value); + if (value.isNegative()) { + throw new IllegalArgumentException( + "Duration %s can't be negative: %s".formatted(subject, value) + ); + } + if (value.compareTo(maxLimit) >= 0) { + throw new IllegalArgumentException( + "Duration %s can't be longer or equals too %s: %s".formatted( + subject, + durationToStr(maxLimit), + value + ) + ); + } + return value; + } + /** * Checks that duration is not negative and not over 2 days. * * @param subject used to identify name of the problematic value when throwing an exception. + * @deprecated Use {@link #requireNonNegative(Duration, Duration, String)} - This method is + * not generic, it has a hardcoded limit. */ + @Deprecated public static Duration requireNonNegativeLong(Duration value, String subject) { Objects.requireNonNull(value); if (value.isNegative()) { @@ -210,7 +237,10 @@ public static Duration requireNonNegativeLong(Duration value, String subject) { * Checks that duration is not negative and not over 2 hours. * * @param subject used to identify name of the problematic value when throwing an exception. + * @deprecated Use {@link #requireNonNegative(Duration, Duration, String)} - This method is + * not generic, it has a hardcoded limit. */ + @Deprecated public static Duration requireNonNegativeMedium(Duration value, String subject) { Objects.requireNonNull(value); if (value.isNegative()) { @@ -230,7 +260,10 @@ public static Duration requireNonNegativeMedium(Duration value, String subject) * Checks that duration is not negative and not over 30 minutes. * * @param subject used to identify name of the problematic value when throwing an exception. + * @deprecated Use {@link #requireNonNegative(Duration, Duration, String)} - This method is + * not generic, it has a hardcoded limit. */ + @Deprecated public static Duration requireNonNegativeShort(Duration value, String subject) { Objects.requireNonNull(value); if (value.isNegative()) { diff --git a/src/main/java/org/opentripplanner/raptor/RaptorService.java b/src/main/java/org/opentripplanner/raptor/RaptorService.java index d8e7fcd3dcd..70156cbbfbe 100644 --- a/src/main/java/org/opentripplanner/raptor/RaptorService.java +++ b/src/main/java/org/opentripplanner/raptor/RaptorService.java @@ -68,8 +68,8 @@ private RaptorResponse routeUsingStdWorker( RaptorTransitDataProvider transitData, RaptorRequest request ) { - var worker = config.createStdWorker(transitData, request); - var result = worker.route(); + var rangeRaptorRouter = config.createRangeRaptorWithStdWorker(transitData, request); + var result = rangeRaptorRouter.route(); var arrivals = new DefaultStopArrivals(result); return new RaptorResponse<>(result.extractPaths(), arrivals, request, false); } diff --git a/src/main/java/org/opentripplanner/raptor/api/model/AbstractAccessEgressDecorator.java b/src/main/java/org/opentripplanner/raptor/api/model/AbstractAccessEgressDecorator.java index ddb266e0884..eb1a388f91c 100644 --- a/src/main/java/org/opentripplanner/raptor/api/model/AbstractAccessEgressDecorator.java +++ b/src/main/java/org/opentripplanner/raptor/api/model/AbstractAccessEgressDecorator.java @@ -16,6 +16,18 @@ public AbstractAccessEgressDecorator(RaptorAccessEgress delegate) { this.delegate = delegate; } + public static RaptorAccessEgress accessEgressWithExtraSlack( + RaptorAccessEgress delegate, + int slack + ) { + return new AbstractAccessEgressDecorator(delegate) { + @Override + public int durationInSeconds() { + return super.durationInSeconds() + slack; + } + }; + } + protected RaptorAccessEgress delegate() { return delegate; } diff --git a/src/main/java/org/opentripplanner/raptor/api/path/RaptorStopNameResolver.java b/src/main/java/org/opentripplanner/raptor/api/model/RaptorStopNameResolver.java similarity index 94% rename from src/main/java/org/opentripplanner/raptor/api/path/RaptorStopNameResolver.java rename to src/main/java/org/opentripplanner/raptor/api/model/RaptorStopNameResolver.java index 1825c7965fe..a973e1732be 100644 --- a/src/main/java/org/opentripplanner/raptor/api/path/RaptorStopNameResolver.java +++ b/src/main/java/org/opentripplanner/raptor/api/model/RaptorStopNameResolver.java @@ -1,4 +1,4 @@ -package org.opentripplanner.raptor.api.path; +package org.opentripplanner.raptor.api.model; import javax.annotation.Nullable; diff --git a/src/main/java/org/opentripplanner/raptor/api/model/RaptorTransfer.java b/src/main/java/org/opentripplanner/raptor/api/model/RaptorTransfer.java index bab4b9ab166..856d6828b30 100644 --- a/src/main/java/org/opentripplanner/raptor/api/model/RaptorTransfer.java +++ b/src/main/java/org/opentripplanner/raptor/api/model/RaptorTransfer.java @@ -14,8 +14,7 @@ public interface RaptorTransfer { int stop(); /** - * The generalized cost of this transfer in centi-seconds. The value is used to compare with - * riding transit, and will be one component of a full itinerary. + * The generalized cost of this transfer in centi-seconds. *

* This method is called many times, so care needs to be taken that the value is stored, not * calculated for each invocation. diff --git a/src/main/java/org/opentripplanner/raptor/api/path/PathStringBuilder.java b/src/main/java/org/opentripplanner/raptor/api/path/PathStringBuilder.java index b96d1a96f14..da56474ae02 100644 --- a/src/main/java/org/opentripplanner/raptor/api/path/PathStringBuilder.java +++ b/src/main/java/org/opentripplanner/raptor/api/path/PathStringBuilder.java @@ -7,6 +7,7 @@ import org.opentripplanner.framework.time.TimeUtils; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorValueFormatter; import org.opentripplanner.raptor.spi.RaptorCostCalculator; diff --git a/src/main/java/org/opentripplanner/raptor/api/path/RaptorPath.java b/src/main/java/org/opentripplanner/raptor/api/path/RaptorPath.java index b92d30643ec..78d90f1d9f4 100644 --- a/src/main/java/org/opentripplanner/raptor/api/path/RaptorPath.java +++ b/src/main/java/org/opentripplanner/raptor/api/path/RaptorPath.java @@ -4,6 +4,7 @@ import java.util.stream.Stream; import javax.annotation.Nullable; import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.model.RelaxFunction; diff --git a/src/main/java/org/opentripplanner/raptor/api/request/PassThroughPoint.java b/src/main/java/org/opentripplanner/raptor/api/request/PassThroughPoint.java index b17fab84347..187436907fe 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/PassThroughPoint.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/PassThroughPoint.java @@ -10,7 +10,10 @@ /** * A collection of stop indexes used to define a pass through-point. + * + * @deprecated This will be replaced by ViaLocation */ +@Deprecated public class PassThroughPoint { private final String name; diff --git a/src/main/java/org/opentripplanner/raptor/api/request/RaptorViaConnection.java b/src/main/java/org/opentripplanner/raptor/api/request/RaptorViaConnection.java new file mode 100644 index 00000000000..4017d8feee0 --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/api/request/RaptorViaConnection.java @@ -0,0 +1,133 @@ +package org.opentripplanner.raptor.api.request; + +import java.util.Objects; +import javax.annotation.Nullable; +import org.opentripplanner.framework.time.DurationUtils; +import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; +import org.opentripplanner.raptor.api.model.RaptorTransfer; + +/** + * A via-connection is used to connect two stops during the raptor routing. The Raptor + * implementation uses these connections to force the path through at least one connection per + * via-location. This is not an alternative to transfers. Raptor supports several use-cases through + * via-connections: + * + *

Route via a pass-through-stop

+ * Raptor will allow a path to go through a pass-through-stop. The stop can be visited on-board + * transit, or at the alight- or board-stop. The from-stop and to-stop is the same, and the + * minimum-wait-time must be zero. + * + *

Route via a single stop with a minimum-wait-time

+ * Raptor will allow a path to go through a single stop, if the from-stop and to-stop is the same. + * + *

Route via a coordinate

+ * + * To route through a coordinate you need to find all nearby stops, then calculate the "walk" + * durations and produce the set of connection. If you want to spend a min-wait-time at the + * coordinate, this time must be added to the {@code durationInSeconds}. The calculation of + * {@code c1} need to include the walk time, but not the wait time (assuming all connections + * have the same minimum wait time). + */ +public final class RaptorViaConnection { + + private final int fromStop; + private final int durationInSeconds; + + @Nullable + private final RaptorTransfer transfer; + + RaptorViaConnection(RaptorViaLocation parent, int fromStop, @Nullable RaptorTransfer transfer) { + this.fromStop = fromStop; + this.transfer = transfer; + this.durationInSeconds = + parent.minimumWaitTime() + + (transfer == null ? RaptorConstants.ZERO : transfer.durationInSeconds()); + } + + /** + * Stop index where the connection starts. + */ + public int fromStop() { + return fromStop; + } + + @Nullable + public RaptorTransfer transfer() { + return transfer; + } + + /** + * Stop index where the connection ends. This can be the same as the {@code fromStop}. + */ + public int toStop() { + return isSameStop() ? fromStop : transfer.stop(); + } + + /** + * The time duration to walk or travel from the {@code fromStop} to the {@code toStop}. + */ + public int durationInSeconds() { + return durationInSeconds; + } + + /** + * The generalized cost of this via-connection in centi-seconds. + *

+ * This method is called many times, so care needs to be taken that the value is stored, not + * calculated for each invocation. + */ + public int c1() { + return isSameStop() ? RaptorConstants.ZERO : transfer.c1(); + } + + public boolean isSameStop() { + return transfer == null; + } + + /** + * This method is used to check that all connections are unique/provide an optimal path. + * The method returns {@code true} if this instance is better or equals to the given other + * stop with respect to being pareto-optimal. + */ + boolean isBetterOrEqual(RaptorViaConnection other) { + if (fromStop != other.fromStop || toStop() != other.toStop()) { + return false; + } + return durationInSeconds() <= other.durationInSeconds() && c1() <= other.c1(); + } + + /** + * Only from and to stop is part of the equals/hashCode, duplicate connection between to stops + * are not allowed. + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RaptorViaConnection that = (RaptorViaConnection) o; + return fromStop == that.fromStop && Objects.equals(transfer, that.transfer); + } + + @Override + public int hashCode() { + return Objects.hash(fromStop, transfer); + } + + @Override + public String toString() { + return toString(Integer::toString); + } + + public String toString(RaptorStopNameResolver stopNameResolver) { + var buf = new StringBuilder(stopNameResolver.apply(fromStop)); + if (transfer != null) { + buf.append("~").append(stopNameResolver.apply(toStop())); + } + int d = durationInSeconds(); + if (d > RaptorConstants.ZERO) { + buf.append(" ").append(DurationUtils.durationToStr(d)); + } + return buf.toString(); + } +} diff --git a/src/main/java/org/opentripplanner/raptor/api/request/RaptorViaLocation.java b/src/main/java/org/opentripplanner/raptor/api/request/RaptorViaLocation.java new file mode 100644 index 00000000000..3a8df33b37f --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/api/request/RaptorViaLocation.java @@ -0,0 +1,184 @@ +package org.opentripplanner.raptor.api.request; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nullable; +import org.opentripplanner.framework.lang.IntUtils; +import org.opentripplanner.framework.time.DurationUtils; +import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; +import org.opentripplanner.raptor.api.model.RaptorTransfer; + +public final class RaptorViaLocation { + + private static final int MAX_WAIT_TIME_LIMIT = (int) Duration.ofHours(24).toSeconds(); + + private final String label; + private final boolean allowPassThrough; + private final int minimumWaitTime; + private final List connections; + + private RaptorViaLocation( + String label, + boolean allowPassThrough, + Duration minimumWaitTime, + List connections + ) { + this.label = label; + this.allowPassThrough = allowPassThrough; + this.minimumWaitTime = + IntUtils.requireInRange( + (int) minimumWaitTime.toSeconds(), + RaptorConstants.ZERO, + MAX_WAIT_TIME_LIMIT, + "minimumWaitTime" + ); + this.connections = validateConnections(connections); + + if (allowPassThrough && this.minimumWaitTime > RaptorConstants.ZERO) { + throw new IllegalArgumentException("Pass-through and min-wait-time is not allowed."); + } + } + + /** + * Force the path through a set of stops, either on-board or as an alight or board stop. + */ + public static Builder allowPassThrough(@Nullable String label) { + return new Builder(label, true, Duration.ZERO); + } + + /** + * Force the path through one of the listed connections. To visit a stop, the path must board or + * alight transit at the given stop, on-board visits do not count, see + * {@link #allowPassThrough(String)}. + */ + public static Builder via(@Nullable String label) { + return new Builder(label, false, Duration.ZERO); + } + + /** + * Force the path through one of the listed connections, and wait the given minimum-wait-time + * before continuing. To visit a stop, the path must board or alight transit at the given stop, + * on-board visits do not count, see {@link #allowPassThrough(String)}. + */ + public static Builder via(@Nullable String label, Duration minimumWaitTime) { + return new Builder(label, false, minimumWaitTime); + } + + @Nullable + public String label() { + return label; + } + + public boolean allowPassThrough() { + return allowPassThrough; + } + + public int minimumWaitTime() { + return minimumWaitTime; + } + + public List connections() { + return connections; + } + + public String toString() { + return toString(Integer::toString); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RaptorViaLocation that = (RaptorViaLocation) o; + return ( + allowPassThrough == that.allowPassThrough && + minimumWaitTime == that.minimumWaitTime && + Objects.equals(label, that.label) && + Objects.equals(connections, that.connections) + ); + } + + @Override + public int hashCode() { + return Objects.hash(label, allowPassThrough, minimumWaitTime, connections); + } + + public String toString(RaptorStopNameResolver stopNameResolver) { + var buf = new StringBuilder("Via{"); + if (label != null) { + buf.append("label: ").append(label).append(", "); + } + if (allowPassThrough) { + buf.append("allowPassThrough, "); + } + if (minimumWaitTime > RaptorConstants.ZERO) { + buf.append("minWaitTime: ").append(DurationUtils.durationToStr(minimumWaitTime)).append(", "); + } + buf + .append("connections: ") + .append(connections.stream().map(it -> it.toString(stopNameResolver)).toList()); + return buf.append("}").toString(); + } + + private List validateConnections(List connections) { + if (connections.isEmpty()) { + throw new IllegalArgumentException("At least one connection is required."); + } + var list = connections + .stream() + .map(it -> new RaptorViaConnection(this, it.fromStop, it.transfer)) + .toList(); + + // Compare all pairs to check for duplicates and none optimal connections + for (int i = 0; i < list.size(); ++i) { + var a = list.get(i); + for (int j = i + 1; j < list.size(); ++j) { + var b = list.get(j); + if (a.isBetterOrEqual(b) || b.isBetterOrEqual(a)) { + throw new IllegalArgumentException( + "All connection need to be pareto-optimal: (" + a + ") <-> (" + b + ")" + ); + } + } + } + return list; + } + + public static final class Builder { + + private final String label; + private final boolean allowPassThrough; + private final Duration minimumWaitTime; + private final List connections = new ArrayList<>(); + + public Builder(String label, boolean allowPassThrough, Duration minimumWaitTime) { + this.label = label; + this.allowPassThrough = allowPassThrough; + this.minimumWaitTime = minimumWaitTime; + } + + public Builder addViaStop(int stop) { + this.connections.add(new StopAndTransfer(stop, null)); + return this; + } + + public Builder addViaTransfer(int fromStop, RaptorTransfer transfer) { + this.connections.add(new StopAndTransfer(fromStop, transfer)); + return this; + } + + public RaptorViaLocation build() { + return new RaptorViaLocation(label, allowPassThrough, minimumWaitTime, connections); + } + } + + /** + * Use internally to store connection data, before creating the connection objects. If is + * needed to create the bidirectional relationship between {@link RaptorViaLocation} and + * {@link RaptorViaConnection}. + */ + private record StopAndTransfer(int fromStop, @Nullable RaptorTransfer transfer) {} +} diff --git a/src/main/java/org/opentripplanner/raptor/api/request/SearchParams.java b/src/main/java/org/opentripplanner/raptor/api/request/SearchParams.java index 9bad7cf7222..bd32c025eda 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/SearchParams.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/SearchParams.java @@ -16,6 +16,12 @@ */ public class SearchParams { + /** + * The maximum number of via-locations is used as a check to avoid exploiting the + * search performance. Consider restricting this further in the upstream services. + */ + private static final int MAX_VIA_POINTS = 10; + private final int earliestDepartureTime; private final int latestArrivalTime; private final int searchWindowInSeconds; @@ -26,6 +32,7 @@ public class SearchParams { private final boolean constrainedTransfers; private final Collection accessPaths; private final Collection egressPaths; + private final List viaLocations; /** * Default values are defined in the default constructor. @@ -41,6 +48,7 @@ private SearchParams() { constrainedTransfers = false; accessPaths = List.of(); egressPaths = List.of(); + viaLocations = List.of(); } SearchParams(SearchParamsBuilder builder) { @@ -54,6 +62,7 @@ private SearchParams() { this.constrainedTransfers = builder.constrainedTransfers(); this.accessPaths = List.copyOf(builder.accessPaths()); this.egressPaths = List.copyOf(builder.egressPaths()); + this.viaLocations = List.copyOf(builder.viaLocations()); } /** @@ -195,6 +204,22 @@ public Collection egressPaths() { return egressPaths; } + /** + * List of all possible via locations. All + * *

+ * * NOTE! The {@link RaptorTransfer#stop()} is the stop where the egress path start, NOT the + * * destination - think of it as a reversed path. + * *

+ * * Required, at least one egress path must exist. + */ + public List viaLocations() { + return viaLocations; + } + + public boolean hasViaLocations() { + return !viaLocations.isEmpty(); + } + /** * Get the maximum duration of any access or egress path in seconds. */ @@ -214,7 +239,8 @@ public int hashCode() { preferLateArrival, numberOfAdditionalTransfers, accessPaths, - egressPaths + egressPaths, + viaLocations ); } @@ -234,7 +260,8 @@ public boolean equals(Object o) { preferLateArrival == that.preferLateArrival && numberOfAdditionalTransfers == that.numberOfAdditionalTransfers && accessPaths.equals(that.accessPaths) && - egressPaths.equals(that.egressPaths) + egressPaths.equals(that.egressPaths) && + viaLocations.equals(viaLocations) ); } @@ -254,6 +281,7 @@ public String toString() { ) .addCollection("accessPaths", accessPaths, 5, RaptorAccessEgress::defaultToString) .addCollection("egressPaths", egressPaths, 5, RaptorAccessEgress::defaultToString) + .addCollection("via", viaLocations, 5) .toString(); } @@ -278,5 +306,9 @@ void verify() { !(preferLateArrival && timetable), "The 'departAsLateAsPossible' is not allowed together with 'timetableEnabled'." ); + assertProperty( + viaLocations.size() <= MAX_VIA_POINTS, + "The 'viaLocations' exceeds the maximum number of via-locations (" + MAX_VIA_POINTS + ")." + ); } } diff --git a/src/main/java/org/opentripplanner/raptor/api/request/SearchParamsBuilder.java b/src/main/java/org/opentripplanner/raptor/api/request/SearchParamsBuilder.java index 5774a92d6e1..7db4ce5d0f7 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/SearchParamsBuilder.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/SearchParamsBuilder.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.List; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.raptor.api.model.RaptorConstants; @@ -18,9 +19,7 @@ public class SearchParamsBuilder { private final RaptorRequestBuilder parent; - private final Collection accessPaths = new ArrayList<>(); - private final Collection egressPaths = new ArrayList<>(); - // Search + private int earliestDepartureTime; private int latestArrivalTime; private int searchWindowInSeconds; @@ -29,6 +28,9 @@ public class SearchParamsBuilder { private int maxNumberOfTransfers; private boolean timetable; private boolean constrainedTransfers; + private final Collection accessPaths = new ArrayList<>(); + private final Collection egressPaths = new ArrayList<>(); + private final List viaLocations = new ArrayList<>(); public SearchParamsBuilder(RaptorRequestBuilder parent, SearchParams defaults) { this.parent = parent; @@ -42,6 +44,7 @@ public SearchParamsBuilder(RaptorRequestBuilder parent, SearchParams defaults this.constrainedTransfers = defaults.constrainedTransfers(); this.accessPaths.addAll(defaults.accessPaths()); this.egressPaths.addAll(defaults.egressPaths()); + this.viaLocations.addAll(defaults.viaLocations()); } public int earliestDepartureTime() { @@ -72,9 +75,9 @@ public SearchParamsBuilder searchWindowInSeconds(int searchWindowInSeconds) { } public SearchParamsBuilder searchWindow(Duration searchWindow) { - this.searchWindowInSeconds = - searchWindow == null ? RaptorConstants.NOT_SET : (int) searchWindow.toSeconds(); - return this; + return searchWindowInSeconds( + searchWindow == null ? RaptorConstants.NOT_SET : (int) searchWindow.toSeconds() + ); } /** @@ -160,6 +163,20 @@ public SearchParamsBuilder addEgressPaths(RaptorAccessEgress... egressPaths) return addEgressPaths(Arrays.asList(egressPaths)); } + public Collection viaLocations() { + return viaLocations; + } + + public SearchParamsBuilder addViaLocation(RaptorViaLocation location) { + viaLocations.add(location); + return this; + } + + public SearchParamsBuilder addViaLocations(Collection locations) { + viaLocations.addAll(locations); + return this; + } + public RaptorRequest build() { return parent.build(); } @@ -180,6 +197,7 @@ public String toString() { .addNum("numberOfAdditionalTransfers", numberOfAdditionalTransfers) .addCollection("accessPaths", accessPaths, 5) .addCollection("egressPaths", egressPaths, 5) + .addCollection("via", viaLocations, 10) .toString(); } } diff --git a/src/main/java/org/opentripplanner/raptor/configure/RaptorConfig.java b/src/main/java/org/opentripplanner/raptor/configure/RaptorConfig.java index f1477ecc9f3..cc488448304 100644 --- a/src/main/java/org/opentripplanner/raptor/configure/RaptorConfig.java +++ b/src/main/java/org/opentripplanner/raptor/configure/RaptorConfig.java @@ -2,20 +2,23 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.function.IntPredicate; import javax.annotation.Nullable; import org.opentripplanner.framework.concurrent.OtpRequestThreadFactory; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.request.RaptorRequest; import org.opentripplanner.raptor.api.request.RaptorTuningParameters; import org.opentripplanner.raptor.rangeraptor.DefaultRangeRaptorWorker; +import org.opentripplanner.raptor.rangeraptor.RangeRaptor; +import org.opentripplanner.raptor.rangeraptor.RangeRaptorWorkerComposite; import org.opentripplanner.raptor.rangeraptor.context.SearchContext; +import org.opentripplanner.raptor.rangeraptor.context.SearchContextViaLeg; import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; import org.opentripplanner.raptor.rangeraptor.internalapi.PassThroughPointsService; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.RangeRaptorWorker; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerState; import org.opentripplanner.raptor.rangeraptor.internalapi.RoutingStrategy; +import org.opentripplanner.raptor.rangeraptor.multicriteria.McStopArrivals; import org.opentripplanner.raptor.rangeraptor.multicriteria.configure.McRangeRaptorConfig; import org.opentripplanner.raptor.rangeraptor.standard.configure.StdRangeRaptorConfig; import org.opentripplanner.raptor.rangeraptor.transit.RaptorSearchWindowCalculator; @@ -49,44 +52,62 @@ public static RaptorConfig defaultConfigForTes public SearchContext context(RaptorTransitDataProvider transit, RaptorRequest request) { // The passThroughPointsService is needed to create the context, so we initialize it here. this.passThroughPointsService = createPassThroughPointsService(request); - return new SearchContext<>(request, tuningParameters, transit, acceptC2AtDestination()); + var acceptC2AtDestination = passThroughPointsService.isNoop() + ? null + : passThroughPointsService.acceptC2AtDestination(); + return SearchContext.of(request, tuningParameters, transit, acceptC2AtDestination).build(); } - public RaptorWorker createStdWorker( + public RangeRaptor createRangeRaptorWithStdWorker( RaptorTransitDataProvider transitData, RaptorRequest request ) { var context = context(transitData, request); var stdConfig = new StdRangeRaptorConfig<>(context); - return createWorker(context, stdConfig.state(), stdConfig.strategy()); + return createRangeRaptor( + context, + createWorker(context.legs().getFirst(), stdConfig.state(), stdConfig.strategy()) + ); } - public RaptorWorker createMcWorker( + public RangeRaptor createRangeRaptorWithMcWorker( RaptorTransitDataProvider transitData, RaptorRequest request, Heuristics heuristics ) { - final SearchContext context = context(transitData, request); - return new McRangeRaptorConfig<>(context, passThroughPointsService) - .createWorker( - heuristics, - (state, routingStrategy) -> createWorker(context, state, routingStrategy) - ); + var context = context(transitData, request); + RangeRaptorWorker worker = null; + McStopArrivals nextStopArrivals = null; + + if (request.searchParams().hasViaLocations()) { + for (SearchContextViaLeg cxLeg : context.legs().reversed()) { + var c = new McRangeRaptorConfig<>(cxLeg, passThroughPointsService) + .connectWithNextLegArrivals(nextStopArrivals); + var w = createWorker(cxLeg, c.state(), c.strategy()); + worker = RangeRaptorWorkerComposite.of(w, worker); + nextStopArrivals = c.stopArrivals(); + } + } else { + // The first leg is the only leg + var leg = context.legs().getFirst(); + var c = new McRangeRaptorConfig<>(leg, passThroughPointsService).withHeuristics(heuristics); + worker = createWorker(leg, c.state(), c.strategy()); + } + + return createRangeRaptor(context, worker); } - public RaptorWorker createHeuristicSearch( + public RangeRaptor createRangeRaptorWithHeuristicSearch( RaptorTransitDataProvider transitData, RaptorRequest request ) { - var context = context(transitData, request); - var stdConfig = new StdRangeRaptorConfig<>(context); - return createWorker(context, stdConfig.state(), stdConfig.strategy()); + return createRangeRaptorWithStdWorker(transitData, request); } public Heuristics createHeuristic( RaptorTransitDataProvider transitData, RaptorRequest request, - RaptorWorkerResult results + RaptorRouterResult results ) { var context = context(transitData, request); return new StdRangeRaptorConfig<>(context).createHeuristics(results); @@ -116,29 +137,35 @@ private static PassThroughPointsService createPassThroughPointsService(RaptorReq return McRangeRaptorConfig.passThroughPointsService(request.multiCriteria()); } - private RaptorWorker createWorker( - SearchContext ctx, + private RangeRaptorWorker createWorker( + SearchContextViaLeg ctxLeg, RaptorWorkerState workerState, RoutingStrategy routingStrategy ) { + var ctx = ctxLeg.parent(); return new DefaultRangeRaptorWorker<>( workerState, routingStrategy, ctx.transit(), ctx.slackProvider(), - ctx.accessPaths(), - ctx.roundProvider(), + ctxLeg.accessPaths(), ctx.calculator(), - ctx.createLifeCyclePublisher(), + ctx.lifeCycle(), ctx.performanceTimers(), ctx.useConstrainedTransfers() ); } - private IntPredicate acceptC2AtDestination() { - return passThroughPointsService.isNoop() - ? null - : passThroughPointsService.acceptC2AtDestination(); + private RangeRaptor createRangeRaptor(SearchContext ctx, RangeRaptorWorker worker) { + return new RangeRaptor<>( + worker, + ctx.transit(), + ctx.legs().getFirst().accessPaths(), + ctx.roundTracker(), + ctx.calculator(), + ctx.createLifeCyclePublisher(), + ctx.performanceTimers() + ); } @Nullable diff --git a/src/main/java/org/opentripplanner/raptor/path/Path.java b/src/main/java/org/opentripplanner/raptor/path/Path.java index ebade8b2690..99f226df0ef 100644 --- a/src/main/java/org/opentripplanner/raptor/path/Path.java +++ b/src/main/java/org/opentripplanner/raptor/path/Path.java @@ -8,6 +8,7 @@ import java.util.stream.Stream; import javax.annotation.Nullable; import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTransferConstraint; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.AccessPathLeg; @@ -15,7 +16,6 @@ import org.opentripplanner.raptor.api.path.PathLeg; import org.opentripplanner.raptor.api.path.PathStringBuilder; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.api.path.TransitPathLeg; /** diff --git a/src/main/java/org/opentripplanner/raptor/path/PathBuilder.java b/src/main/java/org/opentripplanner/raptor/path/PathBuilder.java index 7612cc0b3ba..3d0d5e706f6 100644 --- a/src/main/java/org/opentripplanner/raptor/path/PathBuilder.java +++ b/src/main/java/org/opentripplanner/raptor/path/PathBuilder.java @@ -6,12 +6,12 @@ import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.raptor.api.model.RaptorConstants; import org.opentripplanner.raptor.api.model.RaptorConstrainedTransfer; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTransfer; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.AccessPathLeg; import org.opentripplanner.raptor.api.path.PathStringBuilder; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.spi.BoardAndAlightTime; import org.opentripplanner.raptor.spi.RaptorCostCalculator; import org.opentripplanner.raptor.spi.RaptorPathConstrainedTransferSearch; diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/DefaultRangeRaptorWorker.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/DefaultRangeRaptorWorker.java index 0f2ceb9d6e4..7db9e4aac85 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/DefaultRangeRaptorWorker.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/DefaultRangeRaptorWorker.java @@ -1,21 +1,21 @@ package org.opentripplanner.raptor.rangeraptor; +import static org.opentripplanner.raptor.rangeraptor.transit.AccessPaths.calculateMaxNumberOfRides; + import java.util.Collection; -import org.opentripplanner.framework.application.OTPRequestTimeoutException; +import javax.annotation.Nullable; import org.opentripplanner.raptor.api.debug.RaptorTimers; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.raptor.api.model.RaptorConstants; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.RangeRaptorWorker; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerState; -import org.opentripplanner.raptor.rangeraptor.internalapi.RoundProvider; import org.opentripplanner.raptor.rangeraptor.internalapi.RoutingStrategy; import org.opentripplanner.raptor.rangeraptor.internalapi.SlackProvider; -import org.opentripplanner.raptor.rangeraptor.lifecycle.LifeCycleEventPublisher; +import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; import org.opentripplanner.raptor.rangeraptor.transit.AccessPaths; import org.opentripplanner.raptor.rangeraptor.transit.RaptorTransitCalculator; -import org.opentripplanner.raptor.rangeraptor.transit.RoundTracker; import org.opentripplanner.raptor.spi.IntIterator; import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; @@ -50,7 +50,7 @@ */ @SuppressWarnings("Duplicates") public final class DefaultRangeRaptorWorker - implements RaptorWorker { + implements RangeRaptorWorker { private final RoutingStrategy transitWorker; @@ -65,12 +65,6 @@ public final class DefaultRangeRaptorWorker */ private final RaptorWorkerState state; - /** - * The round tracker keep track for the current Raptor round, and abort the search if the round - * max limit is reached. - */ - private final RoundTracker roundTracker; - private final RaptorTransitDataProvider transitData; private final SlackProvider slackProvider; @@ -79,25 +73,25 @@ public final class DefaultRangeRaptorWorker private final RaptorTimers timers; + @Nullable private final AccessPaths accessPaths; - private final LifeCycleEventPublisher lifeCycle; - private final int minNumberOfRounds; private final boolean enableTransferConstraints; private int iterationDepartureTime; + private int round; + public DefaultRangeRaptorWorker( RaptorWorkerState state, RoutingStrategy transitWorker, RaptorTransitDataProvider transitData, SlackProvider slackProvider, - AccessPaths accessPaths, - RoundProvider roundProvider, + @Nullable AccessPaths accessPaths, RaptorTransitCalculator calculator, - LifeCycleEventPublisher lifeCyclePublisher, + WorkerLifeCycle lifeCycle, RaptorTimers timers, boolean enableTransferConstraints ) { @@ -108,92 +102,31 @@ public DefaultRangeRaptorWorker( this.calculator = calculator; this.timers = timers; this.accessPaths = accessPaths; - this.minNumberOfRounds = accessPaths.calculateMaxNumberOfRides(); + this.minNumberOfRounds = calculateMaxNumberOfRides(accessPaths); this.enableTransferConstraints = enableTransferConstraints; - // We do a cast here to avoid exposing the round tracker and the life cycle publisher to - // "everyone" by providing access to it in the context. - this.roundTracker = (RoundTracker) roundProvider; - this.lifeCycle = lifeCyclePublisher; + lifeCycle.onSetupIteration(time -> this.iterationDepartureTime = time); + lifeCycle.onPrepareForNextRound(round -> this.round = round); } - /** - * For each iteration (minute), calculate the minimum travel time to each transit stop in - * seconds. - *

- * Run the scheduled search, round 0 is the street search. - */ @Override - public RaptorWorkerResult route() { - timers.route(() -> { - lifeCycle.notifyRouteSearchStart(calculator.searchForward()); - transitData.setup(); - - // The main outer loop iterates backward over all minutes in the departure times window. - // Ergo, we re-use the arrival times found in searches that have already occurred that - // depart later, because the arrival time given departure at time t is upper-bounded by - // the arrival time given departure at minute t + 1. - final IntIterator it = calculator.rangeRaptorMinutes(); - while (it.hasNext()) { - setupIteration(it.next()); - runRaptorForMinute(); - } - - // Iterate over virtual departure times - this is needed to allow access with a time-penalty - // which falls outside the search-window due to the added time-penalty. - if (!calculator.oneIterationOnly()) { - final IntIterator as = accessPaths.iterateOverPathsWithPenalty(iterationDepartureTime); - while (as.hasNext()) { - setupIteration(as.next()); - runRaptorForMinute(); - } - } - }); + public RaptorRouterResult results() { return state.results(); } - /** - * Perform one minute of a RAPTOR search. - */ - private void runRaptorForMinute() { - findAccessOnStreetForRound(); - - while (hasMoreRounds()) { - lifeCycle.prepareForNextRound(roundTracker.nextRound()); - - // NB since we have transfer limiting not bothering to cut off search when there are no - // more transfers as that will be rare and complicates the code - findTransitForRound(); - - findAccessOnBoardForRound(); - - findTransfersForRound(); - - lifeCycle.roundComplete(state.isDestinationReachedInCurrentRound()); - - findAccessOnStreetForRound(); - } - - // This state is repeatedly modified as the outer loop progresses over departure minutes. - // We have to be careful here, the next iteration will modify the state, so we need to make - // protective copies of any information we want to retain. - lifeCycle.iterationComplete(); - } - /** * Check if the RangeRaptor should continue with a new round. */ - private boolean hasMoreRounds() { - if (round() < minNumberOfRounds) { - return true; - } - return state.isNewRoundAvailable() && roundTracker.hasMoreRounds(); + @Override + public boolean hasMoreRounds() { + return state.isNewRoundAvailable(); } /** * Perform a scheduled search */ - private void findTransitForRound() { + @Override + public void findTransitForRound() { timers.findTransitForRound(() -> { IntIterator stops = state.stopsTouchedPreviousRound(); IntIterator routeIndexIterator = transitData.routeIndexIterator(stops); @@ -248,11 +181,11 @@ private void findTransitForRound() { } } } - lifeCycle.transitsForRoundComplete(); }); } - private void findTransfersForRound() { + @Override + public void findTransfersForRound() { timers.findTransfersForRound(() -> { IntIterator it = state.stopsTouchedByTransitCurrentRound(); @@ -262,30 +195,22 @@ private void findTransfersForRound() { // loop transfers are already included by virtue of those stops having been reached state.transferToStops(fromStop, calculator.getTransfers(transitData, fromStop)); } - - lifeCycle.transfersForRoundComplete(); }); } - private int round() { - return roundTracker.round(); - } - - private void findAccessOnStreetForRound() { - addAccessPaths(accessPaths.arrivedOnStreetByNumOfRides(round())); + @Override + public boolean isDestinationReachedInCurrentRound() { + return state.isDestinationReachedInCurrentRound(); } - private void findAccessOnBoardForRound() { - addAccessPaths(accessPaths.arrivedOnBoardByNumOfRides(round())); + @Override + public void findAccessOnStreetForRound() { + addAccessPaths(accessPaths.arrivedOnStreetByNumOfRides(round)); } - /** - * Run the raptor search for this particular iteration departure time - */ - private void setupIteration(int iterationDepartureTime) { - OTPRequestTimeoutException.checkForTimeout(); - this.iterationDepartureTime = iterationDepartureTime; - lifeCycle.setupIteration(this.iterationDepartureTime); + @Override + public void findAccessOnBoardForRound() { + addAccessPaths(accessPaths.arrivedOnBoardByNumOfRides(round)); } /** diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/RangeRaptor.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/RangeRaptor.java new file mode 100644 index 00000000000..873767b92a5 --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/RangeRaptor.java @@ -0,0 +1,171 @@ +package org.opentripplanner.raptor.rangeraptor; + +import org.opentripplanner.framework.application.OTPRequestTimeoutException; +import org.opentripplanner.raptor.api.debug.RaptorTimers; +import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.rangeraptor.internalapi.RangeRaptorWorker; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouter; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; +import org.opentripplanner.raptor.rangeraptor.lifecycle.LifeCycleEventPublisher; +import org.opentripplanner.raptor.rangeraptor.transit.AccessPaths; +import org.opentripplanner.raptor.rangeraptor.transit.RaptorTransitCalculator; +import org.opentripplanner.raptor.rangeraptor.transit.RoundTracker; +import org.opentripplanner.raptor.spi.IntIterator; +import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; + +/** + * The algorithm used herein is described in + *

+ * Conway, Matthew Wigginton, Andrew Byrd, and Marco van der Linden. “Evidence-Based Transit and + * Land Use Sketch Planning Using Interactive Accessibility Methods on Combined Schedule and + * Headway-Based Networks.” Transportation Research Record 2653 (2017). doi:10.3141/2653-06. + *

+ * + * Delling, Daniel, Thomas Pajor, and Renato Werneck. “Round-Based Public Transit Routing”, + * January 1, 2012. + * . + *

+ * This version supports the following features: + *

    + *
  • Raptor (R) + *
  • Range Raptor (RR) + *
  • Multi-criteria pareto optimal Range Raptor (McRR) + *
  • Reverse search in combination with R and RR + *
+ * This version does NOT support the following features: + *
    + *
  • Frequency routes, supported by the original code using Monte Carlo methods + * (generating randomized schedules) + *
+ *

+ * This class originated as a rewrite of Conveyals RAPTOR code: https://github.com/conveyal/r5. + * + * @param The TripSchedule type defined by the user of the raptor API. + */ +@SuppressWarnings("Duplicates") +public final class RangeRaptor implements RaptorRouter { + + private final RangeRaptorWorker worker; + + /** + * The round tracker keep track for the current Raptor round, and abort the search if the round + * max limit is reached. + */ + private final RoundTracker roundTracker; + + private final RaptorTransitDataProvider transitData; + + private final RaptorTransitCalculator calculator; + + private final RaptorTimers timers; + + private final AccessPaths accessPaths; + + private final LifeCycleEventPublisher lifeCycle; + + private final int minNumberOfRounds; + + public RangeRaptor( + RangeRaptorWorker worker, + RaptorTransitDataProvider transitData, + AccessPaths accessPaths, + RoundTracker roundTracker, + RaptorTransitCalculator calculator, + LifeCycleEventPublisher lifeCyclePublisher, + RaptorTimers timers + ) { + this.worker = worker; + this.transitData = transitData; + this.calculator = calculator; + this.timers = timers; + this.accessPaths = accessPaths; + this.minNumberOfRounds = AccessPaths.calculateMaxNumberOfRides(accessPaths); + this.roundTracker = roundTracker; + this.lifeCycle = lifeCyclePublisher; + } + + public RaptorRouterResult route() { + timers.route(() -> { + int iterationDepartureTime = RaptorConstants.TIME_NOT_SET; + lifeCycle.notifyRouteSearchStart(calculator.searchForward()); + transitData.setup(); + + // The main outer loop iterates backward over all minutes in the departure times window. + // Ergo, we re-use the arrival times found in searches that have already occurred that + // depart later, because the arrival time given departure at time t is upper-bounded by + // the arrival time given departure at minute t + 1. + final IntIterator it = calculator.rangeRaptorMinutes(); + while (it.hasNext()) { + iterationDepartureTime = it.next(); + runRaptorForMinute(iterationDepartureTime); + } + + // Iterate over virtual departure times - this is needed to allow access with a time-penalty + // which falls outside the search-window due to the added time-penalty. + if (!calculator.oneIterationOnly()) { + final IntIterator as = accessPaths.iterateOverPathsWithPenalty(iterationDepartureTime); + while (as.hasNext()) { + iterationDepartureTime = as.next(); + runRaptorForMinute(iterationDepartureTime); + } + } + }); + return worker.results(); + } + + /** + * Perform one minute of a RAPTOR search. + */ + private void runRaptorForMinute(int iterationDepartureTime) { + setupIteration(iterationDepartureTime); + worker.findAccessOnStreetForRound(); + + while (hasMoreRounds()) { + lifeCycle.prepareForNextRound(roundTracker.nextRound()); + + // NB since we have transfer limiting not bothering to cut off search when there are no + // more transfers as that will be rare and complicates the code + worker.findTransitForRound(); + lifeCycle.transitsForRoundComplete(); + + worker.findAccessOnBoardForRound(); + + worker.findTransfersForRound(); + lifeCycle.transfersForRoundComplete(); + + lifeCycle.roundComplete(worker.isDestinationReachedInCurrentRound()); + + worker.findAccessOnStreetForRound(); + } + + // This state is repeatedly modified as the outer loop progresses over departure minutes. + // We have to be careful here, the next iteration will modify the state, so we need to make + // protective copies of any information we want to retain. + lifeCycle.iterationComplete(); + } + + /** + * Check if the RangeRaptor should continue with a new round. + */ + private boolean hasMoreRounds() { + if (round() < minNumberOfRounds) { + return true; + } + return worker.hasMoreRounds() && roundTracker.hasMoreRounds(); + } + + private int round() { + return roundTracker.round(); + } + + /** + * Run the raptor search for this particular iteration departure time + */ + private void setupIteration(int iterationDepartureTime) { + OTPRequestTimeoutException.checkForTimeout(); + roundTracker.setupIteration(); + lifeCycle.prepareForNextRound(round()); + lifeCycle.setupIteration(iterationDepartureTime); + } +} diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/RangeRaptorWorkerComposite.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/RangeRaptorWorkerComposite.java new file mode 100644 index 00000000000..8d20525da6b --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/RangeRaptorWorkerComposite.java @@ -0,0 +1,85 @@ +package org.opentripplanner.raptor.rangeraptor; + +import java.util.Collection; +import java.util.List; +import javax.annotation.Nullable; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.rangeraptor.internalapi.RangeRaptorWorker; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; +import org.opentripplanner.raptor.util.composite.CompositeUtil; + +/** + * Iterate over two RR workers. The head should process the access and the tail should produce the + * result. Paths from the head needs to propagate to the tail - this is NOT part of the + * responsibilities for this class. + */ +public class RangeRaptorWorkerComposite + implements RangeRaptorWorker { + + private final List> children; + + private RangeRaptorWorkerComposite(Collection> children) { + this.children = List.copyOf(children); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static RangeRaptorWorker of( + @Nullable RangeRaptorWorker head, + @Nullable RangeRaptorWorker tail + ) { + return CompositeUtil.of( + RangeRaptorWorkerComposite::new, + it -> it instanceof RangeRaptorWorkerComposite, + it -> ((RangeRaptorWorkerComposite) it).children, + head, + tail + ); + } + + @Override + public RaptorRouterResult results() { + return tail().results(); + } + + @Override + public boolean hasMoreRounds() { + return children.stream().anyMatch(RangeRaptorWorker::hasMoreRounds); + } + + @Override + public void findTransitForRound() { + for (RangeRaptorWorker child : children) { + child.findTransitForRound(); + } + } + + @Override + public void findTransfersForRound() { + for (RangeRaptorWorker child : children) { + child.findTransfersForRound(); + } + } + + @Override + public boolean isDestinationReachedInCurrentRound() { + return tail().isDestinationReachedInCurrentRound(); + } + + @Override + public void findAccessOnStreetForRound() { + head().findAccessOnStreetForRound(); + } + + @Override + public void findAccessOnBoardForRound() { + head().findAccessOnBoardForRound(); + } + + private RangeRaptorWorker head() { + return children.getFirst(); + } + + private RangeRaptorWorker tail() { + return children.getLast(); + } +} diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContext.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContext.java index 518d02ae3ad..c0bf8b9d594 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContext.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContext.java @@ -4,6 +4,7 @@ import static org.opentripplanner.raptor.rangeraptor.internalapi.ParetoSetTime.USE_DEPARTURE_TIME; import static org.opentripplanner.raptor.rangeraptor.internalapi.ParetoSetTime.USE_TIMETABLE; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -13,10 +14,10 @@ import javax.annotation.Nullable; import org.opentripplanner.raptor.api.debug.RaptorTimers; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTripPattern; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.model.SearchDirection; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.api.request.DebugRequest; import org.opentripplanner.raptor.api.request.MultiCriteriaRequest; import org.opentripplanner.raptor.api.request.RaptorProfile; @@ -25,7 +26,6 @@ import org.opentripplanner.raptor.api.request.SearchParams; import org.opentripplanner.raptor.rangeraptor.debug.DebugHandlerFactory; import org.opentripplanner.raptor.rangeraptor.internalapi.ParetoSetTime; -import org.opentripplanner.raptor.rangeraptor.internalapi.RoundProvider; import org.opentripplanner.raptor.rangeraptor.internalapi.SlackProvider; import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; import org.opentripplanner.raptor.rangeraptor.lifecycle.LifeCycleEventPublisher; @@ -38,13 +38,14 @@ import org.opentripplanner.raptor.rangeraptor.transit.ReverseRaptorTransitCalculator; import org.opentripplanner.raptor.rangeraptor.transit.RoundTracker; import org.opentripplanner.raptor.rangeraptor.transit.SlackProviderAdapter; +import org.opentripplanner.raptor.rangeraptor.transit.ViaConnections; import org.opentripplanner.raptor.spi.RaptorCostCalculator; import org.opentripplanner.raptor.spi.RaptorSlackProvider; import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; /** - * The search context is used to hold search scoped instances and to pass these to who ever need - * them. + * The search context is used to hold search scoped instances and to pass these to whom ever need + * them. It is one search-context pr RangeRaptor * * @param The TripSchedule type defined by the user of the raptor API. */ @@ -64,31 +65,29 @@ public class SearchContext { private final RaptorTuningParameters tuningParameters; private final RoundTracker roundTracker; private final DebugHandlerFactory debugFactory; - private final EgressPaths egressPaths; - private final AccessPaths accessPaths; private final LifeCycleSubscriptions lifeCycleSubscriptions = new LifeCycleSubscriptions(); @Nullable private final IntPredicate acceptC2AtDestination; + private final List> legs; + /** Lazy initialized */ private RaptorCostCalculator costCalculator = null; - /** - * @param acceptC2AtDestination Currently only the pass-through has a constraint on the c2 value - * for accepting it at the destination, if not this is {@code null}. - */ public SearchContext( RaptorRequest request, RaptorTuningParameters tuningParameters, RaptorTransitDataProvider transit, + AccessPaths accessPaths, + List viaConnections, + @Nullable EgressPaths egressPaths, @Nullable IntPredicate acceptC2AtDestination ) { this.request = request; this.tuningParameters = tuningParameters; this.transit = transit; - this.accessPaths = accessPaths(tuningParameters.iterationDepartureStepInSeconds(), request); - this.egressPaths = egressPaths(request); + this.calculator = createCalculator(request, tuningParameters); this.roundTracker = new RoundTracker( @@ -98,18 +97,24 @@ public SearchContext( ); this.debugFactory = new DebugHandlerFactory<>(debugRequest(request), lifeCycle()); this.acceptC2AtDestination = acceptC2AtDestination; + this.legs = initLegs(accessPaths, viaConnections, egressPaths); } - public AccessPaths accessPaths() { - return accessPaths; - } - - public EgressPaths egressPaths() { - return egressPaths; + /** + * @param acceptC2AtDestination Currently only the pass-through has a constraint on the c2 value + * for accepting it at the destination, if not this is {@code null}. + */ + public static SearchContextBuilder of( + RaptorRequest request, + RaptorTuningParameters tuningParameters, + RaptorTransitDataProvider transit, + @Nullable IntPredicate acceptC2AtDestination + ) { + return new SearchContextBuilder<>(request, tuningParameters, transit, acceptC2AtDestination); } - public int[] egressStops() { - return egressPaths().stops(); + public List> legs() { + return legs; } public SearchParams searchParams() { @@ -193,7 +198,7 @@ public int nRounds() { return tuningParameters.maxNumberOfTransfers() + 1; } - public RoundProvider roundProvider() { + public RoundTracker roundTracker() { return roundTracker; } @@ -224,10 +229,9 @@ public RaptorStopNameResolver stopNameResolver() { public TimeBasedBoardingSupport createTimeBasedBoardingSupport() { return new TimeBasedBoardingSupport<>( - accessPaths().hasTimeDependentAccess(), + legs.getFirst().accessPaths().hasTimeDependentAccess(), slackProvider(), calculator(), - roundProvider(), lifeCycle() ); } @@ -314,18 +318,29 @@ private static ToIntFunction createBoardSlackProvider( : p -> slackProvider.alightSlack(p.slackIndex()); } - private static AccessPaths accessPaths(int iterationStep, RaptorRequest request) { - boolean forward = request.searchDirection().isForward(); - var params = request.searchParams(); - var paths = forward ? params.accessPaths() : params.egressPaths(); - return AccessPaths.create(iterationStep, paths, request.profile(), request.searchDirection()); - } + private List> initLegs( + AccessPaths accessPaths, + List viaConnections, + EgressPaths egressPaths + ) { + if (viaConnections.isEmpty()) { + return List.of(new SearchContextViaLeg<>(this, accessPaths, null, egressPaths)); + } + var accessEmpty = accessPaths.copyEmpty(); + var list = new ArrayList>(); + for (ViaConnections c : viaConnections) { + list.add( + new SearchContextViaLeg<>( + this, + c == viaConnections.getFirst() ? accessPaths : accessEmpty, + c, + null + ) + ); + } + list.add(new SearchContextViaLeg<>(this, accessEmpty, null, egressPaths)); - private static EgressPaths egressPaths(RaptorRequest request) { - boolean forward = request.searchDirection().isForward(); - var params = request.searchParams(); - var paths = forward ? params.egressPaths() : params.accessPaths(); - return EgressPaths.create(paths, request.profile()); + return List.copyOf(list); } static ParetoSetTime paretoSetTimeConfig( diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContextBuilder.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContextBuilder.java new file mode 100644 index 00000000000..592688ad52d --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContextBuilder.java @@ -0,0 +1,82 @@ +package org.opentripplanner.raptor.rangeraptor.context; + +import java.util.List; +import java.util.function.IntPredicate; +import javax.annotation.Nullable; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.request.RaptorRequest; +import org.opentripplanner.raptor.api.request.RaptorTuningParameters; +import org.opentripplanner.raptor.api.request.RaptorViaLocation; +import org.opentripplanner.raptor.rangeraptor.transit.AccessPaths; +import org.opentripplanner.raptor.rangeraptor.transit.EgressPaths; +import org.opentripplanner.raptor.rangeraptor.transit.ViaConnections; +import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; + +public class SearchContextBuilder { + + private final RaptorRequest request; + private final RaptorTuningParameters tuningParameters; + private final RaptorTransitDataProvider transit; + + @Nullable + private final IntPredicate acceptC2AtDestination; + + public SearchContextBuilder( + RaptorRequest request, + RaptorTuningParameters tuningParameters, + RaptorTransitDataProvider transit, + @Nullable IntPredicate acceptC2AtDestination + ) { + this.request = request; + this.tuningParameters = tuningParameters; + this.transit = transit; + this.acceptC2AtDestination = acceptC2AtDestination; + } + + public SearchContext build() { + return createContext(accessPaths(), viaConnections(), egressPaths()); + } + + private SearchContext createContext( + AccessPaths accessPaths, + List viaConnections, + @Nullable EgressPaths egressPaths + ) { + return new SearchContext<>( + request, + tuningParameters, + transit, + accessPaths, + viaConnections, + egressPaths, + acceptC2AtDestination + ); + } + + private AccessPaths accessPaths() { + int iterationStep = tuningParameters.iterationDepartureStepInSeconds(); + boolean forward = request.searchDirection().isForward(); + var params = request.searchParams(); + var paths = forward ? params.accessPaths() : params.egressPaths(); + return AccessPaths.create(iterationStep, paths, request.profile(), request.searchDirection()); + } + + private List viaConnections() { + return request.searchParams().hasViaLocations() + ? request + .searchParams() + .viaLocations() + .stream() + .map(RaptorViaLocation::connections) + .map(ViaConnections::new) + .toList() + : List.of(); + } + + private EgressPaths egressPaths() { + boolean forward = request.searchDirection().isForward(); + var params = request.searchParams(); + var paths = forward ? params.egressPaths() : params.accessPaths(); + return EgressPaths.create(paths, request.profile()); + } +} diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContextViaLeg.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContextViaLeg.java new file mode 100644 index 00000000000..364b7e70d28 --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/context/SearchContextViaLeg.java @@ -0,0 +1,64 @@ +package org.opentripplanner.raptor.rangeraptor.context; + +import javax.annotation.Nullable; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.rangeraptor.transit.AccessPaths; +import org.opentripplanner.raptor.rangeraptor.transit.EgressPaths; +import org.opentripplanner.raptor.rangeraptor.transit.ViaConnections; + +/** + * A search can be split into one or more legs. The {@code parent} search context will have a list + * of legs. The first leg will have a list of {@code accessPaths} and the {@link ViaConnections} + * for transferring to the next leg. The last leg will have {@code egressPaths}. + */ +public class SearchContextViaLeg { + + private final SearchContext parent; + private final AccessPaths accessPaths; + private final ViaConnections viaConnections; + private final EgressPaths egressPaths; + + public SearchContextViaLeg( + SearchContext parent, + AccessPaths accessPaths, + ViaConnections viaConnections, + EgressPaths egressPaths + ) { + this.parent = parent; + this.accessPaths = accessPaths; + this.viaConnections = viaConnections; + this.egressPaths = egressPaths; + } + + /** + * The parent search context this leg is part of. + */ + public SearchContext parent() { + return parent; + } + + /** + * The set of access paths to be used to board this leg. This method returns an empty + * set of access-paths if the leg is not the first leg. Hence, it is null-safe. + */ + public AccessPaths accessPaths() { + return accessPaths; + } + + /** + * The via connections for the via-location this leg ends with. This is {@code null} if this + * leg is the last leg. + */ + @Nullable + public ViaConnections viaConnections() { + return viaConnections; + } + + /** + * The egress path for search, if and only if this is the last leg. + */ + @Nullable + public EgressPaths egressPaths() { + return egressPaths; + } +} diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RangeRaptorWorker.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RangeRaptorWorker.java new file mode 100644 index 00000000000..858c0e7124b --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RangeRaptorWorker.java @@ -0,0 +1,47 @@ +package org.opentripplanner.raptor.rangeraptor.internalapi; + +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; + +/** + * The worker performs the travel search. There are multiple implementations, even some that do not + * return paths. + * + * @param The TripSchedule type defined by the user of the raptor API. + */ +public interface RangeRaptorWorker { + /** + * Fetch the result after the search is performed. + */ + RaptorRouterResult results(); + + /** + * Check if the RangeRaptor should continue with a new round. + */ + boolean hasMoreRounds(); + + /** + * Perform a transit search for the current round. + */ + void findTransitForRound(); + + /** + * Apply transfers for the current round. + */ + void findTransfersForRound(); + + /** + * Return {@code true} if the destination is reached in the current round. + */ + boolean isDestinationReachedInCurrentRound(); + + /** + * Apply access for the current round, including round zero - before the first transit. + * This is applied in each round because the access may include transit (FLEX). + */ + void findAccessOnStreetForRound(); + + /** + * Apply access for the current round, when the access arrives to the stop on-board (FLEX). + */ + void findAccessOnBoardForRound(); +} diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorRouter.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorRouter.java new file mode 100644 index 00000000000..3b5fb711b06 --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorRouter.java @@ -0,0 +1,17 @@ +package org.opentripplanner.raptor.rangeraptor.internalapi; + +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; + +/** + * Interface for Raptor Router. Allow instrumentation/wrapping the router. This is not + * currently used in the main branch of OTP, but it is used in Entur fork to extend the + * router functionality. + */ +public interface RaptorRouter { + /** + * Perform the routing request and return the result. A range-raptor request will + * iterate over the minutes in the search-window, while a plain raptor search will + * just do one iteration. + */ + RaptorRouterResult route(); +} diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorWorkerResult.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorRouterResult.java similarity index 87% rename from src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorWorkerResult.java rename to src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorRouterResult.java index 59aeb48e6ab..df073b796ec 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorWorkerResult.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorRouterResult.java @@ -5,9 +5,9 @@ import org.opentripplanner.raptor.api.path.RaptorPath; /** - * This is the result of the {@link RaptorWorker#route()} call. + * This is the result of a RangeRaptor route call. */ -public interface RaptorWorkerResult { +public interface RaptorRouterResult { /** * Return all paths found. */ diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorWorker.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorWorker.java deleted file mode 100644 index fc5786b2407..00000000000 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorWorker.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.opentripplanner.raptor.rangeraptor.internalapi; - -import org.opentripplanner.raptor.api.model.RaptorTripSchedule; - -/** - * The worker performs the travel search. There are multiple implementations, even some that do not - * return paths. - * - * @param The TripSchedule type defined by the user of the raptor API. - */ -public interface RaptorWorker { - /** - * Perform the routing request. - */ - RaptorWorkerResult route(); -} diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorWorkerState.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorWorkerState.java index a71b8765adb..2add95f9a79 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorWorkerState.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RaptorWorkerState.java @@ -3,11 +3,11 @@ import java.util.Iterator; import org.opentripplanner.raptor.api.model.RaptorTransfer; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; -import org.opentripplanner.raptor.rangeraptor.DefaultRangeRaptorWorker; +import org.opentripplanner.raptor.rangeraptor.RangeRaptor; import org.opentripplanner.raptor.spi.IntIterator; /** - * The contract the state must implement for the {@link DefaultRangeRaptorWorker} to do its job. This + * The contract the state must implement for the {@link RangeRaptor} to do its job. This * allows us to mix workers and states to implement different versions of the algorithm like * Standard, Standard-reversed and multi-criteria and use this with different states keeping only * the information needed by the use-case. Some example use-cases are calculating heuristics, @@ -45,5 +45,5 @@ public interface RaptorWorkerState { */ void transferToStops(int fromStop, Iterator transfers); - RaptorWorkerResult results(); + RaptorRouterResult results(); } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RoundProvider.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RoundProvider.java deleted file mode 100644 index 21bd86f8133..00000000000 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RoundProvider.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.opentripplanner.raptor.rangeraptor.internalapi; - -import org.opentripplanner.raptor.rangeraptor.transit.RoundTracker; - -/** - * Keep track of current Raptor round. The provider is injected where needed instead of passing the - * current round down the call stack. This is faster than passing the round on the stack because the - * round is access so frequently thet in most cases it is cached in the CPU registry - at least - * tests indicate this. - *

- * - * @see RoundTracker - */ -public interface RoundProvider { - /** - * The current Raptor round. - */ - int round(); - - /** - * Return true if this round is the first round, calculating the first transit path. Access is - * calculated in round zero (0). - */ - boolean isFirstRound(); -} diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RoutingStrategy.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RoutingStrategy.java index 63be40e9e8a..df9577aaeff 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RoutingStrategy.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/internalapi/RoutingStrategy.java @@ -2,13 +2,13 @@ import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; -import org.opentripplanner.raptor.rangeraptor.DefaultRangeRaptorWorker; +import org.opentripplanner.raptor.rangeraptor.RangeRaptor; import org.opentripplanner.raptor.spi.RaptorConstrainedBoardingSearch; import org.opentripplanner.raptor.spi.RaptorRoute; import org.opentripplanner.raptor.spi.RaptorTimeTable; /** - * Provides alternative implementations of some logic within the {@link DefaultRangeRaptorWorker}. + * Provides alternative implementations of some logic within the {@link RangeRaptor}. * * @param The TripSchedule type defined by the user of the raptor API. */ diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McRangeRaptorWorkerState.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McRangeRaptorWorkerState.java index eccb009aeaa..1dcada83419 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McRangeRaptorWorkerState.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McRangeRaptorWorkerState.java @@ -6,7 +6,7 @@ import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.raptor.api.model.RaptorTransfer; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerState; import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; import org.opentripplanner.raptor.rangeraptor.multicriteria.arrivals.McStopArrival; @@ -110,9 +110,9 @@ public void transferToStops(int fromStop, Iterator tra } @Override - public RaptorWorkerResult results() { + public RaptorRouterResult results() { arrivals.debugStateInfo(); - return new McRaptorWorkerResult(arrivals, paths); + return new McRaptorRouterResult(arrivals, paths); } Iterable> listStopArrivalsPreviousRound(int stop) { diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McRaptorWorkerResult.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McRaptorRouterResult.java similarity index 91% rename from src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McRaptorWorkerResult.java rename to src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McRaptorRouterResult.java index a664c89e0bd..2339c2a8df6 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McRaptorWorkerResult.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McRaptorRouterResult.java @@ -3,16 +3,16 @@ import java.util.Collection; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; import org.opentripplanner.raptor.rangeraptor.internalapi.SingleCriteriaStopArrivals; import org.opentripplanner.raptor.rangeraptor.path.DestinationArrivalPaths; -public class McRaptorWorkerResult implements RaptorWorkerResult { +public class McRaptorRouterResult implements RaptorRouterResult { private final McStopArrivals stopArrivals; private final DestinationArrivalPaths paths; - public McRaptorWorkerResult(McStopArrivals arrivals, DestinationArrivalPaths paths) { + public McRaptorRouterResult(McStopArrivals arrivals, DestinationArrivalPaths paths) { stopArrivals = arrivals; this.paths = paths; } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McStopArrivals.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McStopArrivals.java index 4090890cc82..7bea413584a 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McStopArrivals.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/McStopArrivals.java @@ -4,17 +4,25 @@ import java.util.BitSet; import java.util.Collections; +import java.util.Objects; import java.util.function.Function; import java.util.stream.Stream; +import javax.annotation.Nullable; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.request.RaptorViaConnection; +import org.opentripplanner.raptor.api.view.ArrivalView; import org.opentripplanner.raptor.rangeraptor.debug.DebugHandlerFactory; import org.opentripplanner.raptor.rangeraptor.multicriteria.arrivals.ArrivalParetoSetComparatorFactory; import org.opentripplanner.raptor.rangeraptor.multicriteria.arrivals.McStopArrival; +import org.opentripplanner.raptor.rangeraptor.multicriteria.arrivals.McStopArrivalFactory; import org.opentripplanner.raptor.rangeraptor.path.DestinationArrivalPaths; import org.opentripplanner.raptor.rangeraptor.transit.AccessPaths; import org.opentripplanner.raptor.rangeraptor.transit.EgressPaths; +import org.opentripplanner.raptor.rangeraptor.transit.ViaConnections; import org.opentripplanner.raptor.spi.IntIterator; import org.opentripplanner.raptor.util.BitSetIterator; +import org.opentripplanner.raptor.util.paretoset.ParetoComparator; +import org.opentripplanner.raptor.util.paretoset.ParetoSetEventListener; /** * This class serve as a wrapper for all stop arrival pareto set, one set for each stop. It also @@ -27,10 +35,12 @@ public final class McStopArrivals { private final StopArrivalParetoSet[] arrivals; private final BitSet touchedStops; + private final McStopArrivals next; - private final ArrivalParetoSetComparatorFactory> comparatorFactory; + //private final ArrivalParetoSetComparatorFactory> comparatorFactory; private final DebugHandlerFactory debugHandlerFactory; private final DebugStopArrivalsStatistics debugStats; + private final ParetoComparator> comparator; /** * Set the time at a transit index iff it is optimal. This sets both the best time and the @@ -38,33 +48,46 @@ public final class McStopArrivals { */ public McStopArrivals( int nStops, - EgressPaths egressPaths, AccessPaths accessPaths, + @Nullable EgressPaths egressPaths, + ViaConnections viaConnections, DestinationArrivalPaths paths, + McStopArrivals next, + McStopArrivalFactory stopArrivalFactory, ArrivalParetoSetComparatorFactory> comparatorFactory, DebugHandlerFactory debugHandlerFactory ) { - this.comparatorFactory = comparatorFactory; + // Assert only-one-of next or egressPaths is set + if (next == null) { + Objects.requireNonNull(egressPaths); + } else if (egressPaths != null) { + throw new IllegalArgumentException( + "Can not delegate to next and at the same have egress paths." + ); + } + //noinspection unchecked this.arrivals = (StopArrivalParetoSet[]) new StopArrivalParetoSet[nStops]; this.touchedStops = new BitSet(nStops); + this.next = next; + this.comparator = comparatorFactory.compareArrivalTimeRoundCostAndOnBoardArrival(); this.debugHandlerFactory = debugHandlerFactory; this.debugStats = new DebugStopArrivalsStatistics(debugHandlerFactory.debugLogger()); - initAccessArrivals(accessPaths); - glueTogetherEgressStopWithDestinationArrivals(egressPaths, paths); + initViaConnections(viaConnections, stopArrivalFactory); + initEgressStopAndGlueItToDestinationArrivals(egressPaths, paths); } - public boolean reached(int stopIndex) { + boolean reached(int stopIndex) { return arrivals[stopIndex] != null && !arrivals[stopIndex].isEmpty(); } /** Slow! do not use during routing! */ - public int bestArrivalTime(int stopIndex) { + int bestArrivalTime(int stopIndex) { return minInt(arrivals[stopIndex].stream(), McStopArrival::arrivalTime); } - public boolean reachedByTransit(int stopIndex) { + boolean reachedByTransit(int stopIndex) { return ( arrivals[stopIndex] != null && arrivals[stopIndex].stream().anyMatch(a -> a.arrivedBy(TRANSIT)) @@ -72,12 +95,12 @@ public boolean reachedByTransit(int stopIndex) { } /** Slow! do not use during routing! */ - public int bestTransitArrivalTime(int stopIndex) { + int bestTransitArrivalTime(int stopIndex) { return transitStopArrivalsMinInt(stopIndex, McStopArrival::arrivalTime); } /** Slow! do not use during routing! */ - public int smallestNumberOfTransfers(int stopIndex) { + int smallestNumberOfTransfers(int stopIndex) { return transitStopArrivalsMinInt(stopIndex, McStopArrival::numberOfTransfers); } @@ -91,6 +114,7 @@ IntIterator stopsTouchedIterator() { void addStopArrival(McStopArrival arrival) { boolean added = findOrCreateSet(arrival.stop()).add(arrival); + if (added) { touchedStops.set(arrival.stop()); } @@ -100,22 +124,16 @@ void debugStateInfo() { debugStats.debugStatInfo(arrivals); } - public boolean hasArrivalsAfterMarker(int stop) { - StopArrivalParetoSet it = arrivals[stop]; - if (it == null) { - return false; - } - return it.hasElementsAfterMarker(); + boolean hasArrivalsAfterMarker(int stop) { + var it = arrivals[stop]; + return it != null && it.hasElementsAfterMarker(); } /** List all transits arrived this round. */ Iterable> listArrivalsAfterMarker(final int stop) { - StopArrivalParetoSet it = arrivals[stop]; - if (it == null) { - // Avoid creating new objects in a tight loop - return Collections::emptyIterator; - } - return it.elementsAfterMarker(); + var it = arrivals[stop]; + // Avoid creating new objects in a tight loop + return it == null ? Collections::emptyIterator : it.elementsAfterMarker(); } void clearTouchedStopsAndSetStopMarkers() { @@ -131,47 +149,69 @@ void clearTouchedStopsAndSetStopMarkers() { private StopArrivalParetoSet findOrCreateSet(final int stop) { if (arrivals[stop] == null) { arrivals[stop] = - StopArrivalParetoSet.createStopArrivalSet( - comparatorFactory.compareArrivalTimeRoundAndCost(), - debugHandlerFactory.paretoSetStopArrivalListener(stop) - ); + StopArrivalParetoSet + .of(comparator) + .withDebugListener(debugHandlerFactory.paretoSetStopArrivalListener(stop)) + .build(); } return arrivals[stop]; } - private void initAccessArrivals(AccessPaths accessPaths) { - int maxNRides = accessPaths.calculateMaxNumberOfRides(); - for (int nRides = 0; nRides <= maxNRides; ++nRides) { - for (var access : accessPaths.arrivedOnBoardByNumOfRides(nRides)) { - int stop = access.stop(); - arrivals[stop] = - StopArrivalParetoSet.createStopArrivalSet( - comparatorFactory.compareArrivalTimeRoundCostAndOnBoardArrival(), - debugHandlerFactory.paretoSetStopArrivalListener(stop) - ); - } + private void initViaConnections( + @Nullable ViaConnections viaConnections, + McStopArrivalFactory stopArrivalFactory + ) { + if (viaConnections == null) { + return; } + viaConnections + .byFromStop() + .forEachEntry((stop, list) -> { + this.arrivals[stop] = + StopArrivalParetoSet + .of(comparator) + .withDebugListener(debugHandlerFactory.paretoSetStopArrivalListener(stop)) + .withNextSearchListener( + new ParetoSetEventListener<>() { + @Override + public void notifyElementAccepted(ArrivalView newElement) { + for (RaptorViaConnection c : list) { + var e = (McStopArrival) newElement; + var n = stopArrivalFactory.createViaStopArrival(e, c); + if (n != null) { + next.addStopArrival(n); + } + } + } + } + ) + .build(); + return true; + }); } /** * This method creates a ParetoSet for the given egress stop. When arrivals are added to the stop, * the "glue" make sure new destination arrivals are added to the destination arrivals. */ - private void glueTogetherEgressStopWithDestinationArrivals( + private void initEgressStopAndGlueItToDestinationArrivals( EgressPaths egressPaths, DestinationArrivalPaths paths ) { + if (egressPaths == null) { + return; + } + egressPaths .byStop() .forEachEntry((stop, list) -> { // The factory is creating the actual "glue" this.arrivals[stop] = - StopArrivalParetoSet.createEgressStopArrivalSet( - comparatorFactory.compareArrivalTimeRoundCostAndOnBoardArrival(), - list, - paths, - debugHandlerFactory.paretoSetStopArrivalListener(stop) - ); + StopArrivalParetoSet + .of(comparator) + .withDebugListener(debugHandlerFactory.paretoSetStopArrivalListener(stop)) + .withEgressListener(list, paths) + .build(); return true; }); } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/StopArrivalParetoSet.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/StopArrivalParetoSet.java index ca653be6fc9..5492cdbbda4 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/StopArrivalParetoSet.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/StopArrivalParetoSet.java @@ -1,7 +1,6 @@ package org.opentripplanner.raptor.rangeraptor.multicriteria; import java.util.List; -import javax.annotation.Nullable; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.view.ArrivalView; @@ -30,36 +29,55 @@ private StopArrivalParetoSet( super(comparator, listener); } - /** - * Create a stop arrivals pareto set and attach an optional {@code paretoSetEventListener} - * (debug handler). - */ - static StopArrivalParetoSet createStopArrivalSet( - ParetoComparator> comparator, - @Nullable ParetoSetEventListener> paretoSetEventListener + public static Builder of( + ParetoComparator> comparator ) { - return new StopArrivalParetoSet<>(comparator, paretoSetEventListener); + return new Builder<>(comparator); } - /** - * Create a new StopArrivalParetoSet and attach a debugger if it exist. Also attach a {@link - * CalculateTransferToDestination} listener which will create new destination arrivals for each - * accepted egress stop arrival. - */ - static StopArrivalParetoSet createEgressStopArrivalSet( - ParetoComparator> comparator, - List egressPaths, - DestinationArrivalPaths destinationArrivals, - @Nullable ParetoSetEventListener> paretoSetEventListener - ) { - ParetoSetEventListener> listener; + static class Builder { - listener = new CalculateTransferToDestination<>(egressPaths, destinationArrivals); + private ParetoSetEventListener> debugListener = null; + private ParetoSetEventListener> egressListener = null; + private ParetoSetEventListener> nextSearchListener = null; + private final ParetoComparator> comparator; - if (paretoSetEventListener != null) { - listener = new ParetoSetEventListenerComposite<>(paretoSetEventListener, listener); + Builder(ParetoComparator> comparator) { + this.comparator = comparator; } - return new StopArrivalParetoSet<>(comparator, listener); + /** + * Attach an optional {@code paretoSetEventListener} debug handler. + */ + Builder withDebugListener(ParetoSetEventListener> debugListener) { + this.debugListener = debugListener; + return this; + } + + /** + * Attach a {@link CalculateTransferToDestination} listener which will create new destination + * arrivals for each accepted egress stop arrival. + */ + Builder withEgressListener( + List egressPaths, + DestinationArrivalPaths destinationArrivals + ) { + this.egressListener = new CalculateTransferToDestination<>(egressPaths, destinationArrivals); + return this; + } + + Builder withNextSearchListener(ParetoSetEventListener> nextSearchListener) { + this.nextSearchListener = nextSearchListener; + return this; + } + + StopArrivalParetoSet build() { + // The order of the listeners is important, we want the debug event for reaching a + // stop to appear before the path is logged (in case both debuggers are enabled). + return new StopArrivalParetoSet<>( + comparator, + ParetoSetEventListenerComposite.of(debugListener, nextSearchListener, egressListener) + ); + } } } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/McStopArrival.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/McStopArrival.java index 895e6aa17e0..e4775c61508 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/McStopArrival.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/McStopArrival.java @@ -107,6 +107,12 @@ public McStopArrival timeShiftNewArrivalTime(int newArrivalTime) { throw new UnsupportedOperationException("No accessEgress for transfer stop arrival"); } + /** + * Add the given amount of slack to the arrival-time. This is used to add extraordinary + * wait-time to an arrival - for example, in via-search where a minimum-wait-time can be set. + */ + public abstract McStopArrival addSlackToArrivalTime(int slack); + @Override public final int hashCode() { throw new IllegalStateException("Avoid using hashCode() and equals() for this class."); diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/McStopArrivalFactory.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/McStopArrivalFactory.java index 485410c8797..7825057c376 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/McStopArrivalFactory.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/McStopArrivalFactory.java @@ -3,6 +3,7 @@ import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.raptor.api.model.RaptorTransfer; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.request.RaptorViaConnection; import org.opentripplanner.raptor.api.view.PatternRideView; public interface McStopArrivalFactory { @@ -20,4 +21,27 @@ McStopArrival createTransferStopArrival( RaptorTransfer transfer, int arrivalTime ); + + default McStopArrival createViaStopArrival( + McStopArrival previous, + RaptorViaConnection viaConnection + ) { + if (viaConnection.isSameStop()) { + if (viaConnection.durationInSeconds() == 0) { + return previous; + } else { + return previous.addSlackToArrivalTime(viaConnection.durationInSeconds()); + } + } else { + if (previous.arrivedOnBoard()) { + return createTransferStopArrival( + previous, + viaConnection.transfer(), + previous.arrivalTime() + viaConnection.durationInSeconds() + ); + } else { + return null; + } + } + } } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c1/AccessStopArrival.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c1/AccessStopArrival.java index e3787d41f78..cab1cde3c73 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c1/AccessStopArrival.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c1/AccessStopArrival.java @@ -1,5 +1,6 @@ package org.opentripplanner.raptor.rangeraptor.multicriteria.arrivals.c1; +import static org.opentripplanner.raptor.api.model.AbstractAccessEgressDecorator.accessEgressWithExtraSlack; import static org.opentripplanner.raptor.api.model.PathLegType.ACCESS; import org.opentripplanner.raptor.api.model.PathLegType; @@ -16,6 +17,7 @@ */ final class AccessStopArrival extends McStopArrival { + private final int departureTime; private final RaptorAccessEgress access; AccessStopArrival(int departureTime, RaptorAccessEgress access) { @@ -26,6 +28,7 @@ final class AccessStopArrival extends McStopArriva access.c1(), access.numberOfRides() ); + this.departureTime = departureTime; this.access = access; } @@ -44,6 +47,11 @@ public AccessPathView accessPath() { return () -> access; } + @Override + public boolean arrivedOnBoard() { + return access.stopReachedOnBoard(); + } + @Override public McStopArrival timeShiftNewArrivalTime(int newRequestedArrivalTime) { int newArrivalTime = access.latestArrivalTime(newRequestedArrivalTime); @@ -62,7 +70,7 @@ public McStopArrival timeShiftNewArrivalTime(int newRequestedArrivalTime) { } @Override - public boolean arrivedOnBoard() { - return access.stopReachedOnBoard(); + public McStopArrival addSlackToArrivalTime(int slack) { + return new AccessStopArrival<>(departureTime, accessEgressWithExtraSlack(access, slack)); } } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c1/TransferStopArrival.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c1/TransferStopArrival.java index 05c165a158b..bca25dbf61c 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c1/TransferStopArrival.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c1/TransferStopArrival.java @@ -55,4 +55,9 @@ public RaptorTransfer transfer() { public boolean arrivedOnBoard() { return false; } + + @Override + public McStopArrival addSlackToArrivalTime(int slack) { + return new TransferStopArrival<>(previous(), transfer, arrivalTime() + slack); + } } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c1/TransitStopArrival.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c1/TransitStopArrival.java index a30581512a7..ee572175b81 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c1/TransitStopArrival.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c1/TransitStopArrival.java @@ -69,4 +69,9 @@ public TransitPathView transitPath() { public boolean arrivedOnBoard() { return true; } + + @Override + public McStopArrival addSlackToArrivalTime(int slack) { + return new TransitStopArrival<>(previous(), stop(), arrivalTime() + slack, c1(), trip); + } } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c2/AccessStopArrivalC2.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c2/AccessStopArrivalC2.java index ed4d9df1415..ecc54110feb 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c2/AccessStopArrivalC2.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c2/AccessStopArrivalC2.java @@ -1,5 +1,6 @@ package org.opentripplanner.raptor.rangeraptor.multicriteria.arrivals.c2; +import static org.opentripplanner.raptor.api.model.AbstractAccessEgressDecorator.accessEgressWithExtraSlack; import static org.opentripplanner.raptor.api.model.PathLegType.ACCESS; import org.opentripplanner.raptor.api.model.PathLegType; @@ -7,6 +8,7 @@ import org.opentripplanner.raptor.api.model.RaptorConstants; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.view.AccessPathView; +import org.opentripplanner.raptor.rangeraptor.multicriteria.arrivals.McStopArrival; import org.opentripplanner.raptor.spi.RaptorCostCalculator; /** @@ -16,6 +18,7 @@ */ final class AccessStopArrivalC2 extends AbstractStopArrivalC2 { + private final int departureTime; private final RaptorAccessEgress access; AccessStopArrivalC2(int departureTime, RaptorAccessEgress access) { @@ -27,6 +30,7 @@ final class AccessStopArrivalC2 extends AbstractSt access.c1(), RaptorCostCalculator.ZERO_COST ); + this.departureTime = departureTime; this.access = access; } @@ -57,6 +61,11 @@ public AbstractStopArrivalC2 timeShiftNewArrivalTime(int newRequestedArrivalT return new AccessStopArrivalC2<>(newDepartureTime, access); } + @Override + public McStopArrival addSlackToArrivalTime(int slack) { + return new AccessStopArrivalC2<>(departureTime, accessEgressWithExtraSlack(access, slack)); + } + @Override public boolean arrivedOnBoard() { return access.stopReachedOnBoard(); diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c2/TransferStopArrivalC2.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c2/TransferStopArrivalC2.java index 42ee55ff784..888346b30fe 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c2/TransferStopArrivalC2.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c2/TransferStopArrivalC2.java @@ -46,4 +46,9 @@ public RaptorTransfer transfer() { public boolean arrivedOnBoard() { return false; } + + @Override + public McStopArrival addSlackToArrivalTime(int slack) { + return new TransferStopArrivalC2<>(previous(), transfer, arrivalTime() + slack); + } } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c2/TransitStopArrivalC2.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c2/TransitStopArrivalC2.java index c047f484609..e08dde5103a 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c2/TransitStopArrivalC2.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/c2/TransitStopArrivalC2.java @@ -58,4 +58,9 @@ public TransitPathView transitPath() { public boolean arrivedOnBoard() { return true; } + + @Override + public McStopArrival addSlackToArrivalTime(int slack) { + return new TransitStopArrivalC2<>(previous(), stop(), arrivalTime() + slack, c1(), c2(), trip); + } } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/configure/McRangeRaptorConfig.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/configure/McRangeRaptorConfig.java index 3673e78ee47..e65d180a9d8 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/configure/McRangeRaptorConfig.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/configure/McRangeRaptorConfig.java @@ -1,17 +1,17 @@ package org.opentripplanner.raptor.rangeraptor.multicriteria.configure; import java.util.Objects; -import java.util.function.BiFunction; import javax.annotation.Nullable; import org.opentripplanner.raptor.api.model.DominanceFunction; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.request.MultiCriteriaRequest; import org.opentripplanner.raptor.api.request.RaptorTransitGroupPriorityCalculator; +import org.opentripplanner.raptor.rangeraptor.RangeRaptor; import org.opentripplanner.raptor.rangeraptor.context.SearchContext; +import org.opentripplanner.raptor.rangeraptor.context.SearchContextViaLeg; import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; import org.opentripplanner.raptor.rangeraptor.internalapi.ParetoSetCost; import org.opentripplanner.raptor.rangeraptor.internalapi.PassThroughPointsService; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker; import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerState; import org.opentripplanner.raptor.rangeraptor.internalapi.RoutingStrategy; import org.opentripplanner.raptor.rangeraptor.multicriteria.McRangeRaptorWorkerState; @@ -42,18 +42,22 @@ */ public class McRangeRaptorConfig { - private final SearchContext context; + private final SearchContextViaLeg contextLeg; private final PathConfig pathConfig; + private final PassThroughPointsService passThroughPointsService; private DestinationArrivalPaths paths; - private PassThroughPointsService passThroughPointsService; + private McRangeRaptorWorkerState state; + private Heuristics heuristics; + private McStopArrivals arrivals; + private McStopArrivals nextLegArrivals = null; public McRangeRaptorConfig( - SearchContext context, + SearchContextViaLeg contextLeg, PassThroughPointsService passThroughPointsService ) { - this.context = Objects.requireNonNull(context); + this.contextLeg = Objects.requireNonNull(contextLeg); this.passThroughPointsService = Objects.requireNonNull(passThroughPointsService); - this.pathConfig = new PathConfig<>(context); + this.pathConfig = new PathConfig<>(this.contextLeg.parent()); } /** @@ -70,12 +74,53 @@ public static PassThroughPointsService passThroughPointsService( /** * Create new multi-criteria worker with optional heuristics. */ - public RaptorWorker createWorker( - Heuristics heuristics, - BiFunction, RoutingStrategy, RaptorWorker> createWorker + public McRangeRaptorConfig withHeuristics(Heuristics heuristics) { + this.heuristics = heuristics; + return this; + } + + /** + * Set the next leg state. This is used to connect the state created by this config with the + * next leg. If this is the last leg, the next leg should be {@code null}. Calling this method + * is optional - if not called. + */ + public McRangeRaptorConfig connectWithNextLegArrivals( + @Nullable McStopArrivals nextLegArrivals ) { - McRangeRaptorWorkerState state = createState(heuristics); - return createWorker.apply(state, createTransitWorkerStrategy(state)); + this.nextLegArrivals = nextLegArrivals; + return this; + } + + /** + * Create new multi-criteria worker with optional heuristics. + */ + public RoutingStrategy strategy() { + return createTransitWorkerStrategy(createState(heuristics)); + } + + public RaptorWorkerState state() { + return createState(heuristics); + } + + /** + * This is used in the config to chain more than one search together. + */ + public McStopArrivals stopArrivals() { + if (arrivals == null) { + this.arrivals = + new McStopArrivals<>( + context().nStops(), + contextLeg.accessPaths(), + contextLeg.egressPaths(), + contextLeg.viaConnections(), + createDestinationArrivalPaths(), + nextLegArrivals, + createStopArrivalFactory(), + createFactoryParetoComparator(), + context().debugFactory() + ); + } + return arrivals; } /* private factory methods */ @@ -101,51 +146,49 @@ private > RoutingStrategy createTransitWorkerStrateg ) { return new MultiCriteriaRoutingStrategy<>( state, - context.createTimeBasedBoardingSupport(), + context().createTimeBasedBoardingSupport(), factory, passThroughPointsService, - context.costCalculator(), - context.slackProvider(), + context().costCalculator(), + context().slackProvider(), createPatternRideParetoSet(patternRideComparator) ); } private McRangeRaptorWorkerState createState(Heuristics heuristics) { - return new McRangeRaptorWorkerState<>( - createStopArrivals(), - createDestinationArrivalPaths(), - createHeuristicsProvider(heuristics), - createStopArrivalFactory(), - context.costCalculator(), - context.calculator(), - context.lifeCycle() - ); + if (state == null) { + state = + new McRangeRaptorWorkerState<>( + stopArrivals(), + createDestinationArrivalPaths(), + createHeuristicsProvider(heuristics), + createStopArrivalFactory(), + context().costCalculator(), + context().calculator(), + context().lifeCycle() + ); + } + return state; } private McStopArrivalFactory createStopArrivalFactory() { return includeC2() ? new StopArrivalFactoryC2<>() : new StopArrivalFactoryC1<>(); } - private McStopArrivals createStopArrivals() { - return new McStopArrivals<>( - context.nStops(), - context.egressPaths(), - context.accessPaths(), - createDestinationArrivalPaths(), - createFactoryParetoComparator(), - context.debugFactory() - ); + private SearchContext context() { + return contextLeg.parent(); } private HeuristicsProvider createHeuristicsProvider(Heuristics heuristics) { if (heuristics == null) { return new HeuristicsProvider<>(); } else { + var ctx = contextLeg.parent(); return new HeuristicsProvider<>( heuristics, - context.roundProvider(), createDestinationArrivalPaths(), - context.debugFactory() + ctx.lifeCycle(), + ctx.debugFactory() ); } } @@ -153,7 +196,7 @@ private HeuristicsProvider createHeuristicsProvider(Heuristics heuristics) { private > ParetoSet createPatternRideParetoSet( ParetoComparator comparator ) { - return new ParetoSet<>(comparator, context.debugFactory().paretoSetPatternRideListener()); + return new ParetoSet<>(comparator, context().debugFactory().paretoSetPatternRideListener()); } private DestinationArrivalPaths createDestinationArrivalPaths() { @@ -169,7 +212,7 @@ private ArrivalParetoSetComparatorFactory> createFactoryParetoC } private MultiCriteriaRequest mcRequest() { - return context.multiCriteria(); + return context().multiCriteria(); } /** @@ -220,7 +263,7 @@ private ParetoSetCost resolveCostConfig() { if (isPassThrough()) { return ParetoSetCost.USE_C1_AND_C2; } - if (context.multiCriteria().relaxCostAtDestination() != null) { + if (context().multiCriteria().relaxCostAtDestination() != null) { return ParetoSetCost.USE_C1_RELAX_DESTINATION; } return ParetoSetCost.USE_C1; diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/heuristic/HeuristicsProvider.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/heuristic/HeuristicsProvider.java index c7b1c43c991..fc4d11c33a4 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/heuristic/HeuristicsProvider.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/multicriteria/heuristic/HeuristicsProvider.java @@ -1,11 +1,12 @@ package org.opentripplanner.raptor.rangeraptor.multicriteria.heuristic; +import java.util.Objects; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.request.Optimization; import org.opentripplanner.raptor.rangeraptor.debug.DebugHandlerFactory; import org.opentripplanner.raptor.rangeraptor.internalapi.HeuristicAtStop; import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; -import org.opentripplanner.raptor.rangeraptor.internalapi.RoundProvider; +import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; import org.opentripplanner.raptor.rangeraptor.multicriteria.arrivals.McStopArrival; import org.opentripplanner.raptor.rangeraptor.path.DestinationArrivalPaths; @@ -18,26 +19,32 @@ public final class HeuristicsProvider { private final Heuristics heuristics; - private final RoundProvider roundProvider; private final DestinationArrivalPaths paths; private final HeuristicAtStop[] stops; private final DebugHandlerFactory debugHandlerFactory; + private int round; + public HeuristicsProvider() { - this(null, null, null, null); + this.heuristics = null; + this.paths = null; + this.stops = null; + this.debugHandlerFactory = null; } public HeuristicsProvider( Heuristics heuristics, - RoundProvider roundProvider, DestinationArrivalPaths paths, + WorkerLifeCycle lifeCycle, DebugHandlerFactory debugHandlerFactory ) { - this.heuristics = heuristics; - this.roundProvider = roundProvider; - this.paths = paths; - this.stops = heuristics == null ? null : new HeuristicAtStop[heuristics.size()]; - this.debugHandlerFactory = debugHandlerFactory; + this.heuristics = Objects.requireNonNull(heuristics); + this.paths = Objects.requireNonNull(paths); + this.stops = new HeuristicAtStop[heuristics.size()]; + this.debugHandlerFactory = Objects.requireNonNull(debugHandlerFactory); + + // Use life-cycle events to inject the range-raptor round + lifeCycle.onPrepareForNextRound(r -> this.round = r); } /** @@ -89,7 +96,7 @@ private boolean qualify(int stop, int arrivalTime, int travelDuration, int cost) return false; } int minArrivalTime = arrivalTime + h.minTravelDuration(); - int minNumberOfTransfers = roundProvider.round() - 1 + h.minNumTransfers(); + int minNumberOfTransfers = round - 1 + h.minNumTransfers(); int minTravelDuration = travelDuration + h.minTravelDuration(); int minCost = cost + h.minCost(); int departureTime = minArrivalTime - minTravelDuration; diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/path/DestinationArrivalPaths.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/path/DestinationArrivalPaths.java index f5e67d593ca..78dc7da4967 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/path/DestinationArrivalPaths.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/path/DestinationArrivalPaths.java @@ -9,9 +9,9 @@ import org.opentripplanner.framework.logging.Throttle; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.api.view.ArrivalView; import org.opentripplanner.raptor.path.Path; import org.opentripplanner.raptor.rangeraptor.debug.DebugHandlerFactory; diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/path/ForwardPathMapper.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/path/ForwardPathMapper.java index 8e9b77f9cb8..6f48aacf652 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/path/ForwardPathMapper.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/path/ForwardPathMapper.java @@ -1,8 +1,8 @@ package org.opentripplanner.raptor.rangeraptor.path; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.api.view.ArrivalView; import org.opentripplanner.raptor.path.PathBuilder; import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/path/ReversePathMapper.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/path/ReversePathMapper.java index fde483b4cd8..94b68e10985 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/path/ReversePathMapper.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/path/ReversePathMapper.java @@ -1,8 +1,8 @@ package org.opentripplanner.raptor.rangeraptor.path; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.path.PathBuilder; import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; import org.opentripplanner.raptor.rangeraptor.transit.TripTimesSearch; diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/path/configure/PathConfig.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/path/configure/PathConfig.java index 673c83e6b78..43d504ced7d 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/path/configure/PathConfig.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/path/configure/PathConfig.java @@ -4,11 +4,11 @@ import org.opentripplanner.raptor.api.model.DominanceFunction; import org.opentripplanner.raptor.api.model.GeneralizedCostRelaxFunction; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.model.RelaxFunction; import org.opentripplanner.raptor.api.model.SearchDirection; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.api.request.RaptorProfile; import org.opentripplanner.raptor.rangeraptor.context.SearchContext; import org.opentripplanner.raptor.rangeraptor.internalapi.ParetoSetCost; diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/StdRangeRaptorWorkerState.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/StdRangeRaptorWorkerState.java index e3aba5c36f9..bcb2ca0f798 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/StdRangeRaptorWorkerState.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/StdRangeRaptorWorkerState.java @@ -5,7 +5,7 @@ import org.opentripplanner.raptor.api.model.RaptorTransfer; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.model.TransitArrival; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; import org.opentripplanner.raptor.rangeraptor.standard.besttimes.BestTimes; import org.opentripplanner.raptor.rangeraptor.standard.internalapi.ArrivedAtDestinationCheck; import org.opentripplanner.raptor.rangeraptor.standard.internalapi.BestNumberOfTransfers; @@ -209,8 +209,8 @@ private void transferToStop(int arrivalTimeTransit, int fromStop, RaptorTransfer } @Override - public RaptorWorkerResult results() { - return new StdRaptorWorkerResult<>( + public RaptorRouterResult results() { + return new StdRaptorRouterResult<>( bestTimes, stopArrivalsState::extractPaths, bestNumberOfTransfers::extractBestNumberOfTransfers diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/StdRaptorWorkerResult.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/StdRaptorRouterResult.java similarity index 92% rename from src/main/java/org/opentripplanner/raptor/rangeraptor/standard/StdRaptorWorkerResult.java rename to src/main/java/org/opentripplanner/raptor/rangeraptor/standard/StdRaptorRouterResult.java index 7a6812c9c95..1612234b185 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/StdRaptorWorkerResult.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/StdRaptorRouterResult.java @@ -4,14 +4,14 @@ import java.util.function.Supplier; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; import org.opentripplanner.raptor.rangeraptor.internalapi.SingleCriteriaStopArrivals; import org.opentripplanner.raptor.rangeraptor.standard.besttimes.BestTimes; /** * Result for Standard Range Raptor route call. */ -public class StdRaptorWorkerResult implements RaptorWorkerResult { +public class StdRaptorRouterResult implements RaptorRouterResult { private final BestTimes bestTimes; private final Supplier>> pathSupplier; @@ -23,7 +23,7 @@ public class StdRaptorWorkerResult implements Rapt */ private Collection> paths = null; - public StdRaptorWorkerResult( + public StdRaptorRouterResult( BestTimes bestTimes, Supplier>> pathSupplier, Supplier bestNumberOfTransfersSupplier diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/besttimes/SimpleBestNumberOfTransfers.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/besttimes/SimpleBestNumberOfTransfers.java index 1fb703565f6..22b0f44a579 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/besttimes/SimpleBestNumberOfTransfers.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/besttimes/SimpleBestNumberOfTransfers.java @@ -1,8 +1,8 @@ package org.opentripplanner.raptor.rangeraptor.standard.besttimes; import org.opentripplanner.framework.lang.IntUtils; -import org.opentripplanner.raptor.rangeraptor.internalapi.RoundProvider; import org.opentripplanner.raptor.rangeraptor.internalapi.SingleCriteriaStopArrivals; +import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; import org.opentripplanner.raptor.rangeraptor.standard.internalapi.BestNumberOfTransfers; import org.opentripplanner.raptor.rangeraptor.support.IntArraySingleCriteriaArrivals; @@ -13,11 +13,12 @@ public class SimpleBestNumberOfTransfers implements BestNumberOfTransfers { private final int[] bestNumOfTransfers; - private final RoundProvider roundProvider; + private int round; - public SimpleBestNumberOfTransfers(int nStops, RoundProvider roundProvider) { + public SimpleBestNumberOfTransfers(int nStops, WorkerLifeCycle lifeCycle) { this.bestNumOfTransfers = IntUtils.intArray(nStops, unreachedMinNumberOfTransfers()); - this.roundProvider = roundProvider; + + lifeCycle.onPrepareForNextRound(r -> this.round = r); } @Override @@ -29,7 +30,7 @@ public int calculateMinNumberOfTransfers(int stop) { * Call this method to notify that the given stop is reached in the current round of Raptor. */ void arriveAtStop(int stop) { - final int numOfTransfers = roundProvider.round() - 1; + final int numOfTransfers = round - 1; if (numOfTransfers < bestNumOfTransfers[stop]) { bestNumOfTransfers[stop] = numOfTransfers; } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/configure/StdRangeRaptorConfig.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/configure/StdRangeRaptorConfig.java index 6e0c3ee5afd..5bd0b7c5077 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/configure/StdRangeRaptorConfig.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/configure/StdRangeRaptorConfig.java @@ -9,7 +9,7 @@ import org.opentripplanner.raptor.rangeraptor.context.SearchContext; import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; import org.opentripplanner.raptor.rangeraptor.internalapi.ParetoSetCost; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerState; import org.opentripplanner.raptor.rangeraptor.internalapi.RoutingStrategy; import org.opentripplanner.raptor.rangeraptor.path.DestinationArrivalPaths; @@ -32,6 +32,7 @@ import org.opentripplanner.raptor.rangeraptor.standard.stoparrivals.StdStopArrivalsState; import org.opentripplanner.raptor.rangeraptor.standard.stoparrivals.path.EgressArrivalToPathAdapter; import org.opentripplanner.raptor.rangeraptor.standard.stoparrivals.view.StopsCursor; +import org.opentripplanner.raptor.rangeraptor.transit.EgressPaths; /** * The responsibility of this class is to wire different standard range raptor worker configurations @@ -68,11 +69,11 @@ public RoutingStrategy strategy() { return strategy; } - public Heuristics createHeuristics(RaptorWorkerResult results) { + public Heuristics createHeuristics(RaptorRouterResult results) { return oneOf( new HeuristicsAdapter( ctx.nStops(), - ctx.egressPaths(), + egressPaths(), ctx.calculator(), ctx.costCalculator(), results.extractBestOverallArrivals(), @@ -163,7 +164,7 @@ private StopArrivalsState stdStopArrivalsState() { private StopArrivalsState wrapStopArrivalsStateWithDebugger(StopArrivalsState state) { if (ctx.debugFactory().isDebugStopArrival()) { return new DebugStopArrivalsState<>( - ctx.roundProvider(), + ctx.lifeCycle(), ctx.debugFactory(), stopsCursor(), state @@ -180,7 +181,7 @@ private DestinationArrivalPaths destinationArrivalPaths() { // adapter notify the destination on each new egress stop arrival. var pathsAdapter = createEgressArrivalToPathAdapter(destinationArrivalPaths); - resolveStopArrivals().setupEgressStopStates(ctx.egressPaths(), pathsAdapter); + resolveStopArrivals().setupEgressStopStates(egressPaths(), pathsAdapter); return destinationArrivalPaths; } @@ -219,7 +220,7 @@ private StdStopArrivals resolveStopArrivals() { this.stopArrivals = withBestNumberOfTransfers( oneOf( - new StdStopArrivals(ctx.nRounds(), ctx.nStops(), ctx.roundProvider()), + new StdStopArrivals(ctx.nRounds(), ctx.nStops(), ctx.lifeCycle()), StdStopArrivals.class ) ); @@ -232,7 +233,7 @@ private StdStopArrivals resolveStopArrivals() { */ private SimpleBestNumberOfTransfers createSimpleBestNumberOfTransfers() { return withBestNumberOfTransfers( - new SimpleBestNumberOfTransfers(ctx.nStops(), ctx.roundProvider()) + new SimpleBestNumberOfTransfers(ctx.nStops(), ctx.lifeCycle()) ); } @@ -249,7 +250,7 @@ private UnknownPathFactory unknownPathFactory() { resolveBestNumberOfTransfers(), ctx.calculator(), ctx.slackProvider().transferSlack(), - ctx.egressPaths(), + egressPaths(), MIN_TRAVEL_DURATION.is(ctx.profile()), paretoComparator(ctx.paretoSetTimeConfig(), ParetoSetCost.NONE, null, null), ctx.lifeCycle() @@ -259,11 +260,15 @@ private UnknownPathFactory unknownPathFactory() { private SimpleArrivedAtDestinationCheck createSimpleArrivedAtDestinationCheck() { return new SimpleArrivedAtDestinationCheck( resolveBestTimes(), - ctx.egressPaths().egressesWitchStartByWalking(), - ctx.egressPaths().egressesWitchStartByARide() + egressPaths().egressesWitchStartByWalking(), + egressPaths().egressesWitchStartByARide() ); } + private EgressPaths egressPaths() { + return ctx.legs().getLast().egressPaths(); + } + private S withBestNumberOfTransfers(S value) { this.bestNumberOfTransfers = oneOf(value, BestNumberOfTransfers.class); return value; diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/debug/DebugStopArrivalsState.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/debug/DebugStopArrivalsState.java index 6249764340f..2e0f3277a2a 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/debug/DebugStopArrivalsState.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/debug/DebugStopArrivalsState.java @@ -7,7 +7,7 @@ import org.opentripplanner.raptor.api.model.TransitArrival; import org.opentripplanner.raptor.api.path.RaptorPath; import org.opentripplanner.raptor.rangeraptor.debug.DebugHandlerFactory; -import org.opentripplanner.raptor.rangeraptor.internalapi.RoundProvider; +import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; import org.opentripplanner.raptor.rangeraptor.standard.internalapi.StopArrivalsState; import org.opentripplanner.raptor.rangeraptor.standard.stoparrivals.view.StopsCursor; @@ -28,12 +28,12 @@ public final class DebugStopArrivalsState * Create a Standard range raptor state for the given context */ public DebugStopArrivalsState( - RoundProvider roundProvider, + WorkerLifeCycle lifeCycle, DebugHandlerFactory dFactory, StopsCursor stopsCursor, StopArrivalsState delegate ) { - this.debug = new StateDebugger<>(stopsCursor, roundProvider, dFactory); + this.debug = new StateDebugger<>(stopsCursor, lifeCycle, dFactory); this.delegate = delegate; } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/debug/StateDebugger.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/debug/StateDebugger.java index 374b4aaf17e..1aee775b920 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/debug/StateDebugger.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/debug/StateDebugger.java @@ -6,7 +6,7 @@ import org.opentripplanner.raptor.api.view.ArrivalView; import org.opentripplanner.raptor.rangeraptor.debug.DebugHandlerFactory; import org.opentripplanner.raptor.rangeraptor.internalapi.DebugHandler; -import org.opentripplanner.raptor.rangeraptor.internalapi.RoundProvider; +import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; import org.opentripplanner.raptor.rangeraptor.standard.stoparrivals.view.StopsCursor; /** @@ -17,28 +17,25 @@ class StateDebugger { private final StopsCursor cursor; - private final RoundProvider roundProvider; private final DebugHandler> debugHandlerStopArrivals; + private int round; - StateDebugger( - StopsCursor cursor, - RoundProvider roundProvider, - DebugHandlerFactory dFactory - ) { + StateDebugger(StopsCursor cursor, WorkerLifeCycle lifeCycle, DebugHandlerFactory dFactory) { this.cursor = cursor; - this.roundProvider = roundProvider; this.debugHandlerStopArrivals = dFactory.debugStopArrival(); + + lifeCycle.onPrepareForNextRound(r -> this.round = r); } void acceptAccessPath(int stop, RaptorAccessEgress access) { if (isDebug(stop)) { - debugHandlerStopArrivals.accept(cursor.access(round(), stop, access)); + debugHandlerStopArrivals.accept(cursor.access(round, stop, access)); } } void rejectAccessPath(RaptorAccessEgress accessPath, int arrivalTime) { if (isDebug(accessPath.stop())) { - reject(cursor.fictiveAccess(round(), accessPath, arrivalTime)); + reject(cursor.fictiveAccess(round, accessPath, arrivalTime)); } } @@ -64,13 +61,13 @@ void dropOldStateAndAcceptNewOnStreetArrival(int stop, Runnable body) { void rejectTransit(int alightStop, int alightTime, T trip, int boardStop, int boardTime) { if (isDebug(alightStop)) { - reject(cursor.fictiveTransit(round(), alightStop, alightTime, trip, boardStop, boardTime)); + reject(cursor.fictiveTransit(round, alightStop, alightTime, trip, boardStop, boardTime)); } } void rejectTransfer(int fromStop, RaptorTransfer transfer, int toStop, int arrivalTime) { if (isDebug(transfer.stop())) { - reject(cursor.fictiveTransfer(round(), fromStop, transfer, toStop, arrivalTime)); + reject(cursor.fictiveTransfer(round, fromStop, transfer, toStop, arrivalTime)); } } @@ -81,7 +78,7 @@ private boolean isDebug(int stop) { } private void accept(int stop, boolean stopReachedOnBoard) { - debugHandlerStopArrivals.accept(cursor.stop(round(), stop, stopReachedOnBoard)); + debugHandlerStopArrivals.accept(cursor.stop(round, stop, stopReachedOnBoard)); } /** @@ -94,8 +91,6 @@ private void accept(int stop, boolean stopReachedOnBoard) { * handler about arrivals that are about to be dropped. */ private void drop(int stop, boolean onBoard, boolean newBestOverall) { - final int round = round(); - // if new arrival arrived on-board, if (onBoard) { // and an existing on-board arrival exist @@ -122,8 +117,4 @@ private void reject(ArrivalView arrival) { private void dropExistingArrival(int round, int stop, boolean onBoard) { debugHandlerStopArrivals.drop(cursor.stop(round, stop, onBoard), null, null); } - - private int round() { - return roundProvider.round(); - } } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/stoparrivals/StdStopArrivals.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/stoparrivals/StdStopArrivals.java index b3b32890ec7..a6dee126211 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/stoparrivals/StdStopArrivals.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/stoparrivals/StdStopArrivals.java @@ -4,8 +4,8 @@ import org.opentripplanner.raptor.api.model.RaptorTransfer; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.model.TransitArrival; -import org.opentripplanner.raptor.rangeraptor.internalapi.RoundProvider; import org.opentripplanner.raptor.rangeraptor.internalapi.SingleCriteriaStopArrivals; +import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; import org.opentripplanner.raptor.rangeraptor.standard.internalapi.BestNumberOfTransfers; import org.opentripplanner.raptor.rangeraptor.standard.internalapi.DestinationArrivalListener; import org.opentripplanner.raptor.rangeraptor.support.IntArraySingleCriteriaArrivals; @@ -18,12 +18,12 @@ public final class StdStopArrivals implements Best /** Arrivals by round and stop - [round][stop] */ private final StopArrivalState[][] arrivals; - private final RoundProvider roundProvider; + private int round; - public StdStopArrivals(int nRounds, int nStops, RoundProvider roundProvider) { - this.roundProvider = roundProvider; + public StdStopArrivals(int nRounds, int nStops, WorkerLifeCycle lifeCycle) { //noinspection unchecked this.arrivals = (StopArrivalState[][]) new StopArrivalState[nRounds][nStops]; + lifeCycle.onPrepareForNextRound(r -> this.round = r); } /** @@ -71,12 +71,12 @@ public SingleCriteriaStopArrivals extractBestNumberOfTransfers() { void setAccessTime(int time, RaptorAccessEgress access, boolean bestTime) { final int stop = access.stop(); - var existingArrival = getOrCreateStopIndex(round(), stop); + var existingArrival = getOrCreateStopIndex(round, stop); if (existingArrival instanceof AccessStopArrivalState) { ((AccessStopArrivalState) existingArrival).setAccessTime(time, access, bestTime); } else { - arrivals[round()][stop] = + arrivals[round][stop] = new AccessStopArrivalState<>( time, access, @@ -92,13 +92,13 @@ void setAccessTime(int time, RaptorAccessEgress access, boolean bestTime) { */ void transferToStop(int fromStop, RaptorTransfer transfer, int arrivalTime) { int stop = transfer.stop(); - var state = getOrCreateStopIndex(round(), stop); + var state = getOrCreateStopIndex(round, stop); state.transferToStop(fromStop, arrivalTime, transfer); } void transitToStop(int stop, int time, int boardStop, int boardTime, T trip, boolean bestTime) { - var state = getOrCreateStopIndex(round(), stop); + var state = getOrCreateStopIndex(round, stop); state.arriveByTransit(time, boardStop, boardTime, trip); @@ -108,13 +108,13 @@ void transitToStop(int stop, int time, int boardStop, int boardTime, T trip, boo } int bestTimePreviousRound(int stop) { - return get(round() - 1, stop).time(); + return get(round - 1, stop).time(); } /* private methods */ TransitArrival previousTransit(int boardStopIndex) { - final int prevRound = round() - 1; + final int prevRound = round - 1; int stopIndex = boardStopIndex; StopArrivalState state = get(prevRound, boardStopIndex); @@ -129,10 +129,6 @@ TransitArrival previousTransit(int boardStopIndex) { : null; } - private int round() { - return roundProvider.round(); - } - private StopArrivalState getOrCreateStopIndex(final int round, final int stop) { if (arrivals[round][stop] == null) { arrivals[round][stop] = StopArrivalState.create(); diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/support/TimeBasedBoardingSupport.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/support/TimeBasedBoardingSupport.java index 40df7000461..9ecd2bbf100 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/support/TimeBasedBoardingSupport.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/support/TimeBasedBoardingSupport.java @@ -1,10 +1,10 @@ package org.opentripplanner.raptor.rangeraptor.support; +import static org.opentripplanner.raptor.rangeraptor.transit.RoundTracker.isFirstRound; import static org.opentripplanner.raptor.spi.RaptorTripScheduleSearch.UNBOUNDED_TRIP_INDEX; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.model.TransitArrival; -import org.opentripplanner.raptor.rangeraptor.internalapi.RoundProvider; import org.opentripplanner.raptor.rangeraptor.internalapi.RoutingStrategy; import org.opentripplanner.raptor.rangeraptor.internalapi.SlackProvider; import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; @@ -23,25 +23,24 @@ public final class TimeBasedBoardingSupport { private final SlackProvider slackProvider; private final RaptorTransitCalculator calculator; - private final RoundProvider roundProvider; private final boolean hasTimeDependentAccess; private boolean inFirstIteration = true; private RaptorTimeTable timeTable; private RaptorTripScheduleSearch tripSearch; + private int round; public TimeBasedBoardingSupport( boolean hasTimeDependentAccess, SlackProvider slackProvider, RaptorTransitCalculator calculator, - RoundProvider roundProvider, WorkerLifeCycle subscriptions ) { this.hasTimeDependentAccess = hasTimeDependentAccess; this.slackProvider = slackProvider; this.calculator = calculator; - this.roundProvider = roundProvider; subscriptions.onIterationComplete(() -> inFirstIteration = false); + subscriptions.onPrepareForNextRound(r -> this.round = r); } public void prepareForTransitWith(RaptorTimeTable timeTable) { @@ -124,7 +123,7 @@ private int earliestBoardTime(int prevArrivalTime, int boardSlack) { * Create a trip search using {@link TripScheduleBoardSearch}. */ private RaptorTripScheduleSearch createTripSearch(RaptorTimeTable timeTable) { - if (!inFirstIteration && roundProvider.isFirstRound() && !hasTimeDependentAccess) { + if (!inFirstIteration && isFirstRound(round) && !hasTimeDependentAccess) { // For the first round of every iteration(except the first) we restrict the first // departure to happen within the time-window of the iteration. Another way to put this, // is to say that we allow for the access path to be time-shifted to a later departure, diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/AccessPaths.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/AccessPaths.java index 66b7227584d..2da4832760f 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/AccessPaths.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/AccessPaths.java @@ -5,6 +5,7 @@ import static org.opentripplanner.raptor.rangeraptor.transit.AccessEgressFunctions.removeNonOptimalPathsForStandardRaptor; import gnu.trove.map.TIntObjectMap; +import gnu.trove.map.hash.TIntObjectHashMap; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -38,17 +39,14 @@ private AccessPaths( int iterationStep, IntUnaryOperator iterationOp, TIntObjectMap> arrivedOnStreetByNumOfRides, - TIntObjectMap> arrivedOnBoardByNumOfRides + TIntObjectMap> arrivedOnBoardByNumOfRides, + int maxTimePenalty ) { this.iterationStep = iterationStep; this.iterationOp = iterationOp; this.arrivedOnStreetByNumOfRides = arrivedOnStreetByNumOfRides; this.arrivedOnBoardByNumOfRides = arrivedOnBoardByNumOfRides; - this.maxTimePenalty = - Math.max( - maxTimePenalty(arrivedOnBoardByNumOfRides), - maxTimePenalty(arrivedOnStreetByNumOfRides) - ); + this.maxTimePenalty = maxTimePenalty; } /** @@ -74,12 +72,28 @@ public static AccessPaths create( } paths = decorateWithTimePenaltyLogic(paths); + var arrivedOnBoardByNumOfRides = groupByRound(paths, RaptorAccessEgress::stopReachedByWalking); + var arrivedOnStreetByNumOfRides = groupByRound(paths, RaptorAccessEgress::stopReachedOnBoard); return new AccessPaths( iterationStep, iterationOp(searchDirection), - groupByRound(paths, RaptorAccessEgress::stopReachedByWalking), - groupByRound(paths, RaptorAccessEgress::stopReachedOnBoard) + arrivedOnBoardByNumOfRides, + arrivedOnStreetByNumOfRides, + Math.max( + maxTimePenalty(arrivedOnBoardByNumOfRides), + maxTimePenalty(arrivedOnStreetByNumOfRides) + ) + ); + } + + public AccessPaths copyEmpty() { + return new AccessPaths( + iterationStep, + iterationOp, + new TIntObjectHashMap<>(), + new TIntObjectHashMap<>(), + maxTimePenalty ); } @@ -103,11 +117,13 @@ public List arrivedOnBoardByNumOfRides(int round) { return filterOnTimePenaltyLimitIfExist(arrivedOnBoardByNumOfRides.get(round)); } - public int calculateMaxNumberOfRides() { - return Math.max( - Arrays.stream(arrivedOnStreetByNumOfRides.keys()).max().orElse(0), - Arrays.stream(arrivedOnBoardByNumOfRides.keys()).max().orElse(0) - ); + public static int calculateMaxNumberOfRides(AccessPaths paths) { + return paths == null + ? 0 + : Math.max( + Arrays.stream(paths.arrivedOnStreetByNumOfRides.keys()).max().orElse(0), + Arrays.stream(paths.arrivedOnBoardByNumOfRides.keys()).max().orElse(0) + ); } /** @@ -141,7 +157,17 @@ public int next() { }; } - private int maxTimePenalty(TIntObjectMap> col) { + /** Raptor uses this information to optimize boarding of the first trip */ + public boolean hasTimeDependentAccess() { + return ( + hasTimeDependentAccess(arrivedOnBoardByNumOfRides) || + hasTimeDependentAccess(arrivedOnStreetByNumOfRides) + ); + } + + /* private methods */ + + private static int maxTimePenalty(TIntObjectMap> col) { return col .valueCollection() .stream() @@ -161,14 +187,6 @@ private static List decorateWithTimePenaltyLogic( return paths.stream().map(it -> it.hasTimePenalty() ? new AccessWithPenalty(it) : it).toList(); } - /** Raptor uses this information to optimize boarding of the first trip */ - public boolean hasTimeDependentAccess() { - return ( - hasTimeDependentAccess(arrivedOnBoardByNumOfRides) || - hasTimeDependentAccess(arrivedOnStreetByNumOfRides) - ); - } - private boolean hasTimePenalty() { return maxTimePenalty != RaptorConstants.TIME_NOT_SET; } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/RoundTracker.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/RoundTracker.java index 05159a19a1e..4a185db0b43 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/RoundTracker.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/RoundTracker.java @@ -1,15 +1,17 @@ package org.opentripplanner.raptor.rangeraptor.transit; -import org.opentripplanner.raptor.rangeraptor.internalapi.RoundProvider; import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; /** * Round tracker to keep track of round index and when to stop exploring new rounds. *

- * In round 0 the access paths with one leg are added. In round 1 the first transit and transfers is - * added, ... + * In round zero(0), the access paths with one leg are added. In round one(1) the first transit and + * transfers is added, ... */ -public class RoundTracker implements RoundProvider { +public class RoundTracker { + + private static final int ROUND_ZERO = 0; + private static final int FIRST_ROUND = 1; /** * The extra number of rounds/transfers we accept compared to the trip with the fewest number of @@ -20,7 +22,7 @@ public class RoundTracker implements RoundProvider { /** * The current round in progress (round index). */ - private int round = 0; + private int round = ROUND_ZERO; /** * The round upper limit for when to abort the search. @@ -28,7 +30,7 @@ public class RoundTracker implements RoundProvider { * This is default set to the maximum number of rounds limit, but as soon as the destination is * reach the {@link #numberOfAdditionalTransfers} is used to update the limit. *

- * The limit is inclusive, indicating the the last round to process. + * The limit is inclusive, indicating the last round to process. */ private int roundMaxLimit; @@ -61,14 +63,14 @@ public int round() { * Return true if this round is the fist round, calculating the first transit path. Access is * calculated in round zero (0). */ - public boolean isFirstRound() { - return round == 1; + public static boolean isFirstRound(int round) { + return round == FIRST_ROUND; } /** * Before each iteration, initialize the round to 0. */ - private void setupIteration() { + public void setupIteration() { round = 0; } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/ViaConnections.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/ViaConnections.java new file mode 100644 index 00000000000..053c1da4354 --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/ViaConnections.java @@ -0,0 +1,26 @@ +package org.opentripplanner.raptor.rangeraptor.transit; + +import static java.util.stream.Collectors.groupingBy; + +import gnu.trove.map.TIntObjectMap; +import gnu.trove.map.hash.TIntObjectHashMap; +import java.util.Collection; +import java.util.List; +import org.opentripplanner.raptor.api.request.RaptorViaConnection; + +public class ViaConnections { + + private final TIntObjectMap> byFromStop; + + public ViaConnections(Collection viaConnections) { + this.byFromStop = new TIntObjectHashMap<>(); + viaConnections + .stream() + .collect(groupingBy(RaptorViaConnection::fromStop)) + .forEach(byFromStop::put); + } + + public TIntObjectMap> byFromStop() { + return byFromStop; + } +} diff --git a/src/main/java/org/opentripplanner/raptor/service/DefaultStopArrivals.java b/src/main/java/org/opentripplanner/raptor/service/DefaultStopArrivals.java index 2d3e3795cd9..244dca1c81c 100644 --- a/src/main/java/org/opentripplanner/raptor/service/DefaultStopArrivals.java +++ b/src/main/java/org/opentripplanner/raptor/service/DefaultStopArrivals.java @@ -1,7 +1,7 @@ package org.opentripplanner.raptor.service; import org.opentripplanner.raptor.api.response.StopArrivals; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; import org.opentripplanner.raptor.rangeraptor.internalapi.SingleCriteriaStopArrivals; /** @@ -13,9 +13,9 @@ public class DefaultStopArrivals implements StopArrivals { private SingleCriteriaStopArrivals bestTransitArrivalTime = null; private SingleCriteriaStopArrivals bestNumberOfTransfers = null; - private final RaptorWorkerResult results; + private final RaptorRouterResult results; - public DefaultStopArrivals(RaptorWorkerResult results) { + public DefaultStopArrivals(RaptorRouterResult results) { this.results = results; } diff --git a/src/main/java/org/opentripplanner/raptor/service/HeuristicSearchTask.java b/src/main/java/org/opentripplanner/raptor/service/HeuristicSearchTask.java index 6a8bc003dd5..78bb9a646bd 100644 --- a/src/main/java/org/opentripplanner/raptor/service/HeuristicSearchTask.java +++ b/src/main/java/org/opentripplanner/raptor/service/HeuristicSearchTask.java @@ -8,17 +8,17 @@ import org.opentripplanner.raptor.api.model.SearchDirection; import org.opentripplanner.raptor.api.request.RaptorRequest; import org.opentripplanner.raptor.configure.RaptorConfig; +import org.opentripplanner.raptor.rangeraptor.RangeRaptor; import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorkerResult; +import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorRouterResult; import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Thin wrapper around a {@link RaptorWorker} to allow for some small additional features. This - * is mostly to extracted some "glue" out of the {@link RangeRaptorDynamicSearch} to make that - * simpler and let it focus on the main bossiness logic. + * This is a thin wrapper around the {@link RangeRaptor} to allow for some small additional + * features. This is mostly to extract some "glue" out of the {@link RangeRaptorDynamicSearch} to + * make that simpler and let it focus on the main bossiness logic. *

* This class is not meant for reuse, create one task for each potential heuristic search. The task * must be {@link #enable()}d before it is {@link #run()}. @@ -33,10 +33,10 @@ public class HeuristicSearchTask { private final RaptorTransitDataProvider transitData; private boolean run = false; - private RaptorWorker search = null; + private RangeRaptor search = null; private RaptorRequest originalRequest; private RaptorRequest heuristicRequest; - private RaptorWorkerResult result = null; + private RaptorRouterResult result = null; public HeuristicSearchTask( RaptorRequest request, @@ -144,7 +144,7 @@ private void createHeuristicSearchIfNotExist(RaptorRequest request) { ); heuristicRequest = builder.build(); - search = config.createHeuristicSearch(transitData, heuristicRequest); + search = config.createRangeRaptorWithHeuristicSearch(transitData, heuristicRequest); } } } diff --git a/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java b/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java index fb96b7a8724..5353804f414 100644 --- a/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java +++ b/src/main/java/org/opentripplanner/raptor/service/RangeRaptorDynamicSearch.java @@ -19,8 +19,8 @@ import org.opentripplanner.raptor.api.request.SearchParamsBuilder; import org.opentripplanner.raptor.api.response.RaptorResponse; import org.opentripplanner.raptor.configure.RaptorConfig; +import org.opentripplanner.raptor.rangeraptor.RangeRaptor; import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; -import org.opentripplanner.raptor.rangeraptor.internalapi.RaptorWorker; import org.opentripplanner.raptor.rangeraptor.transit.RaptorSearchWindowCalculator; import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; import org.slf4j.Logger; @@ -43,7 +43,7 @@ public class RangeRaptorDynamicSearch { private final RaptorConfig config; private final RaptorTransitDataProvider transitData; private final RaptorRequest originalRequest; - private final RaptorSearchWindowCalculator dynamicSearchParamsCalculator; + private final RaptorSearchWindowCalculator dynamicSearchWindowCalculator; private final HeuristicSearchTask fwdHeuristics; private final HeuristicSearchTask revHeuristics; @@ -56,7 +56,7 @@ public RangeRaptorDynamicSearch( this.config = config; this.transitData = transitData; this.originalRequest = originalRequest; - this.dynamicSearchParamsCalculator = + this.dynamicSearchWindowCalculator = config.searchWindowCalculator().withSearchParams(originalRequest.searchParams()); this.fwdHeuristics = new HeuristicSearchTask<>(FORWARD, "Forward", config, transitData); @@ -67,19 +67,18 @@ public RaptorResponse route() { try { enableHeuristicSearchBasedOnOptimizationsAndSearchParameters(); - // Run heuristics, if no destination is reached + // Run the heuristics if no destination is reached runHeuristics(); // Set search-window and other dynamic calculated parameters - RaptorRequest dynamicRequest = originalRequest; - dynamicRequest = requestWithDynamicSearchParams(dynamicRequest); + var dynamicRequest = requestWithDynamicSearchParams(originalRequest); return createAndRunDynamicRRWorker(dynamicRequest); } catch (DestinationNotReachedException e) { return new RaptorResponse<>( Collections.emptyList(), null, - // If a trip exist(forward heuristics succeed), but is outside the calculated + // If a trip exists(forward heuristics succeed), but is outside the calculated // search-window, then set the search-window params as if the request was // performed. This enables the client to page to the next window requestWithDynamicSearchParams(originalRequest), @@ -130,17 +129,18 @@ private void runHeuristics() { private RaptorResponse createAndRunDynamicRRWorker(RaptorRequest request) { LOG.debug("Main request: {}", request); - RaptorWorker raptorWorker; + RangeRaptor rangeRaptorRouter; // Create worker if (request.profile().is(MULTI_CRITERIA)) { - raptorWorker = config.createMcWorker(transitData, request, getDestinationHeuristics()); + rangeRaptorRouter = + config.createRangeRaptorWithMcWorker(transitData, request, getDestinationHeuristics()); } else { - raptorWorker = config.createStdWorker(transitData, request); + rangeRaptorRouter = config.createRangeRaptorWithStdWorker(transitData, request); } // Route - var result = raptorWorker.route(); + var result = rangeRaptorRouter.route(); // create and return response return new RaptorResponse<>( @@ -274,10 +274,10 @@ private RaptorRequest requestWithDynamicSearchParams(RaptorRequest request SearchParamsBuilder builder = request.mutate().searchParams(); if (!request.searchParams().isEarliestDepartureTimeSet()) { - builder.earliestDepartureTime(dynamicSearchParamsCalculator.getEarliestDepartureTime()); + builder.earliestDepartureTime(dynamicSearchWindowCalculator.getEarliestDepartureTime()); } if (!request.searchParams().isSearchWindowSet()) { - builder.searchWindowInSeconds(dynamicSearchParamsCalculator.getSearchWindowSeconds()); + builder.searchWindowInSeconds(dynamicSearchWindowCalculator.getSearchWindowSeconds()); } // We do not set the latest-arrival-time, because we do not want to limit the forward // multi-criteria search, it does not have much effect on the performance - we only risk @@ -287,7 +287,7 @@ private RaptorRequest requestWithDynamicSearchParams(RaptorRequest request private void calculateDynamicSearchParametersFromHeuristics(@Nullable Heuristics heuristics) { if (heuristics != null) { - dynamicSearchParamsCalculator + dynamicSearchWindowCalculator .withHeuristics( heuristics.bestOverallJourneyTravelDuration(), heuristics.minWaitTimeForJourneysReachingDestination() diff --git a/src/main/java/org/opentripplanner/raptor/service/ViaRangeRaptorDynamicSearch.java b/src/main/java/org/opentripplanner/raptor/service/ViaRangeRaptorDynamicSearch.java new file mode 100644 index 00000000000..aedbb5cbeef --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/service/ViaRangeRaptorDynamicSearch.java @@ -0,0 +1,298 @@ +package org.opentripplanner.raptor.service; + +import static org.opentripplanner.raptor.api.model.SearchDirection.FORWARD; +import static org.opentripplanner.raptor.api.model.SearchDirection.REVERSE; +import static org.opentripplanner.raptor.api.request.RaptorProfile.MULTI_CRITERIA; +import static org.opentripplanner.raptor.service.HeuristicToRunResolver.resolveHeuristicToRunBasedOnOptimizationsAndSearchParameters; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.opentripplanner.framework.application.OTPRequestTimeoutException; +import org.opentripplanner.raptor.RaptorService; +import org.opentripplanner.raptor.api.model.RaptorTripSchedule; +import org.opentripplanner.raptor.api.request.RaptorRequest; +import org.opentripplanner.raptor.api.request.SearchParams; +import org.opentripplanner.raptor.api.request.SearchParamsBuilder; +import org.opentripplanner.raptor.api.response.RaptorResponse; +import org.opentripplanner.raptor.configure.RaptorConfig; +import org.opentripplanner.raptor.rangeraptor.RangeRaptor; +import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; +import org.opentripplanner.raptor.rangeraptor.transit.RaptorSearchWindowCalculator; +import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This search helps the {@link RaptorService} to configure + * heuristics and set dynamic search parameters like EDT, LAT and raptor-search-window. + *

+ * If possible the forward and reverse heuristics will be run in parallel. + *

+ * Depending on which optimization is enabled and which search parameters are set a forward and/or a + * reverse "single-iteration" raptor search is performed and heuristics are collected. This is used + * to configure the "main" multi-iteration RangeRaptor search. + */ +public class ViaRangeRaptorDynamicSearch { + + private static final Logger LOG = LoggerFactory.getLogger(ViaRangeRaptorDynamicSearch.class); + + private final RaptorConfig config; + private final RaptorTransitDataProvider transitData; + private final RaptorRequest originalRequest; + private final RaptorSearchWindowCalculator dynamicSearchWindowCalculator; + + private final HeuristicSearchTask fwdHeuristics; + private final HeuristicSearchTask revHeuristics; + + public ViaRangeRaptorDynamicSearch( + RaptorConfig config, + RaptorTransitDataProvider transitData, + RaptorRequest originalRequest + ) { + this.config = config; + this.transitData = transitData; + this.originalRequest = originalRequest; + this.dynamicSearchWindowCalculator = + config.searchWindowCalculator().withSearchParams(originalRequest.searchParams()); + + this.fwdHeuristics = new HeuristicSearchTask<>(FORWARD, "Forward", config, transitData); + this.revHeuristics = new HeuristicSearchTask<>(REVERSE, "Reverse", config, transitData); + } + + public RaptorResponse route() { + try { + enableHeuristicSearchBasedOnOptimizationsAndSearchParameters(); + + // Run heuristics, if no destination is reached + runHeuristics(); + + // Set search-window and other dynamic calculated parameters + var dynamicRequest = requestWithDynamicSearchParams(originalRequest); + + return createAndRunDynamicRRWorker(dynamicRequest); + } catch (DestinationNotReachedException e) { + return new RaptorResponse<>( + Collections.emptyList(), + null, + // If a trip exist(forward heuristics succeed), but is outside the calculated + // search-window, then set the search-window params as if the request was + // performed. This enables the client to page to the next window + requestWithDynamicSearchParams(originalRequest), + false + ); + } + } + + /** + * Only exposed for testing purposes + */ + @Nullable + public Heuristics getDestinationHeuristics() { + if (!originalRequest.useDestinationPruning()) { + return null; + } + LOG.debug("RangeRaptor - Destination pruning enabled."); + return revHeuristics.result(); + } + + /** + * Create and prepare heuristic search (both FORWARD and REVERSE) based on optimizations and input + * search parameters. This is done for Standard and Multi-criteria profiles only. + */ + private void enableHeuristicSearchBasedOnOptimizationsAndSearchParameters() { + // We delegate this to a static method to be able to write unit test on this logic + resolveHeuristicToRunBasedOnOptimizationsAndSearchParameters( + originalRequest, + fwdHeuristics::enable, + revHeuristics::enable + ); + } + + /** + * Run standard "singe-iteration" raptor search to calculate heuristics - this should be really + * fast to run compared with a (multi-criteria) range-raptor search. + * + * @throws DestinationNotReachedException if destination is not reached. + */ + private void runHeuristics() { + if (isItPossibleToRunHeuristicsInParallel()) { + runHeuristicsInParallel(); + } else { + runHeuristicsSequentially(); + } + fwdHeuristics.debugCompareResult(revHeuristics); + } + + private RaptorResponse createAndRunDynamicRRWorker(RaptorRequest request) { + LOG.debug("Main request: {}", request); + RangeRaptor rangeRaptorRouter; + + // Create worker + if (request.profile().is(MULTI_CRITERIA)) { + rangeRaptorRouter = + config.createRangeRaptorWithMcWorker(transitData, request, getDestinationHeuristics()); + } else { + rangeRaptorRouter = config.createRangeRaptorWithStdWorker(transitData, request); + } + + // Route + var result = rangeRaptorRouter.route(); + + // create and return response + return new RaptorResponse<>( + result.extractPaths(), + new DefaultStopArrivals(result), + request, + // This method is not run unless the heuristic reached the destination + true + ); + } + + private boolean isItPossibleToRunHeuristicsInParallel() { + SearchParams s = originalRequest.searchParams(); + return ( + config.isMultiThreaded() && + originalRequest.runInParallel() && + s.isEarliestDepartureTimeSet() && + s.isLatestArrivalTimeSet() && + fwdHeuristics.isEnabled() && + revHeuristics.isEnabled() + ); + } + + /** + * @throws DestinationNotReachedException if destination is not reached + */ + private void runHeuristicsInParallel() { + fwdHeuristics.withRequest(originalRequest); + revHeuristics.withRequest(originalRequest); + Future asyncResult = null; + try { + asyncResult = config.threadPool().submit(fwdHeuristics::run); + revHeuristics.run(); + asyncResult.get(); + LOG.debug( + "Route using RangeRaptor - " + "REVERSE and FORWARD heuristic search performed in parallel." + ); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + // propagate interruption to the running task. + asyncResult.cancel(true); + throw new OTPRequestTimeoutException(); + } catch (ExecutionException e) { + if (e.getCause() instanceof DestinationNotReachedException) { + throw new DestinationNotReachedException(); + } + LOG.error(e.getMessage() + ". Request: " + originalRequest, e); + throw new IllegalStateException( + "Failed to run FORWARD/REVERSE heuristic search in parallel. Details: " + e.getMessage() + ); + } + } + + /** + * @throws DestinationNotReachedException if destination is not reached + */ + private void runHeuristicsSequentially() { + List> tasks = listTasksInOrder(); + + if (tasks.isEmpty()) { + return; + } + + // Run the first heuristic search + Heuristics result = runHeuristicSearchTask(tasks.get(0)); + calculateDynamicSearchParametersFromHeuristics(result); + + if (tasks.size() == 1) { + return; + } + + // Run the second heuristic search + runHeuristicSearchTask(tasks.get(1)); + } + + private Heuristics runHeuristicSearchTask(HeuristicSearchTask task) { + RaptorRequest request = task.getDirection().isForward() + ? requestForForwardHeurSearchWithDynamicSearchParams() + : requestForReverseHeurSearchWithDynamicSearchParams(); + + task.withRequest(request).run(); + + return task.result(); + } + + /** + * If the earliest-departure-time(EDT) is set, the task order should be: + *

    + *
  1. {@code FORWARD}
  2. + *
  3. {@code REVERSE}
  4. + *
+ * If not EDT is set, the latest-arrival-time is set, and the order should be the opposite, + * with {@code REVERSE} first + */ + private List> listTasksInOrder() { + boolean performForwardFirst = originalRequest.searchParams().isEarliestDepartureTimeSet(); + + List> list = performForwardFirst + ? List.of(fwdHeuristics, revHeuristics) + : List.of(revHeuristics, fwdHeuristics); + + return list.stream().filter(HeuristicSearchTask::isEnabled).collect(Collectors.toList()); + } + + private RaptorRequest requestForForwardHeurSearchWithDynamicSearchParams() { + if (originalRequest.searchParams().isEarliestDepartureTimeSet()) { + return originalRequest; + } + return originalRequest + .mutate() + .searchParams() + .earliestDepartureTime(transitData.getValidTransitDataStartTime()) + .build(); + } + + private RaptorRequest requestForReverseHeurSearchWithDynamicSearchParams() { + if (originalRequest.searchParams().isLatestArrivalTimeSet()) { + return originalRequest; + } + return originalRequest + .mutate() + .searchParams() + .latestArrivalTime( + transitData.getValidTransitDataEndTime() + + originalRequest.searchParams().accessEgressMaxDurationSeconds() + ) + .build(); + } + + private RaptorRequest requestWithDynamicSearchParams(RaptorRequest request) { + SearchParamsBuilder builder = request.mutate().searchParams(); + + if (!request.searchParams().isEarliestDepartureTimeSet()) { + builder.earliestDepartureTime(dynamicSearchWindowCalculator.getEarliestDepartureTime()); + } + if (!request.searchParams().isSearchWindowSet()) { + builder.searchWindowInSeconds(dynamicSearchWindowCalculator.getSearchWindowSeconds()); + } + // We do not set the latest-arrival-time, because we do not want to limit the forward + // multi-criteria search, it does not have much effect on the performance - we only risk + // loosing optimal results. + return builder.build(); + } + + private void calculateDynamicSearchParametersFromHeuristics(@Nullable Heuristics heuristics) { + if (heuristics != null) { + dynamicSearchWindowCalculator + .withHeuristics( + heuristics.bestOverallJourneyTravelDuration(), + heuristics.minWaitTimeForJourneysReachingDestination() + ) + .calculate(); + } + } +} diff --git a/src/main/java/org/opentripplanner/raptor/spi/RaptorTransitDataProvider.java b/src/main/java/org/opentripplanner/raptor/spi/RaptorTransitDataProvider.java index b4accdbcc2b..f0f103070a7 100644 --- a/src/main/java/org/opentripplanner/raptor/spi/RaptorTransitDataProvider.java +++ b/src/main/java/org/opentripplanner/raptor/spi/RaptorTransitDataProvider.java @@ -2,11 +2,11 @@ import java.util.Iterator; import javax.annotation.Nonnull; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTransfer; import org.opentripplanner.raptor.api.model.RaptorTransferConstraint; import org.opentripplanner.raptor.api.model.RaptorTripPattern; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.api.request.RaptorRequest; /** diff --git a/src/main/java/org/opentripplanner/raptor/spi/UnknownPath.java b/src/main/java/org/opentripplanner/raptor/spi/UnknownPath.java index 24eae14f997..91de7373650 100644 --- a/src/main/java/org/opentripplanner/raptor/spi/UnknownPath.java +++ b/src/main/java/org/opentripplanner/raptor/spi/UnknownPath.java @@ -3,13 +3,13 @@ import java.util.List; import java.util.stream.Stream; import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.AccessPathLeg; import org.opentripplanner.raptor.api.path.EgressPathLeg; import org.opentripplanner.raptor.api.path.PathLeg; import org.opentripplanner.raptor.api.path.PathStringBuilder; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.api.path.TransitPathLeg; /** diff --git a/src/main/java/org/opentripplanner/raptor/util/composite/CompositeUtil.java b/src/main/java/org/opentripplanner/raptor/util/composite/CompositeUtil.java new file mode 100644 index 00000000000..5e0851c7f6b --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/util/composite/CompositeUtil.java @@ -0,0 +1,54 @@ +package org.opentripplanner.raptor.util.composite; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +public class CompositeUtil { + + /** + * Take a list of children and return a composite instance. Input children witch is {@code null} + * is skipped. If no none {@code null} children are provided, {@code null} is retuned + * thrown. If just one listener is passed in the listener it-self is returned (without any wrapper). + * + * @param The base type which the composite inherit from. + * @param makeComposite Factory method to create a new composite. + * @param isComposite see the {@code listChildren} parameter. + * @param listChildren is a function used together with the {@code isComposite} test to extract + * all children out of a composite. This is used to produce one flat list of + * concrete children, without any composite instances in it. The order is + * kept; the composite children are inserted in the new list of children in + * the same place as the composite instance appeared. + * @return {@code null} if all children are {@code null}, the child it-self if only one child + * exist, and a new composite instance if more than one child exist. + */ + @Nullable + @SafeVarargs + public static T of( + Function, T> makeComposite, + Predicate isComposite, + Function> listChildren, + T... children + ) { + Objects.requireNonNull(children); + + var list = Arrays + .stream(children) + .filter(Objects::nonNull) + .flatMap(it -> isComposite.test(it) ? listChildren.apply(it).stream() : Stream.of(it)) + .toList(); + + if (list.isEmpty()) { + return null; + } + if (list.size() == 1) { + return list.getFirst(); + } + return makeComposite.apply(list); + } +} diff --git a/src/main/java/org/opentripplanner/raptor/util/paretoset/ParetoSet.java b/src/main/java/org/opentripplanner/raptor/util/paretoset/ParetoSet.java index 39656d433e9..536378e3722 100644 --- a/src/main/java/org/opentripplanner/raptor/util/paretoset/ParetoSet.java +++ b/src/main/java/org/opentripplanner/raptor/util/paretoset/ParetoSet.java @@ -211,6 +211,14 @@ protected void notifyElementMoved(int fromIndex, int toIndex) { // Noop } + protected ParetoComparator getComparator() { + return comparator; + } + + protected ParetoSetEventListener getEventListener() { + return eventListener; + } + /** * Return an iterable instance. This is made to be as FAST AS POSSIBLE, sacrificing thread-safety * and modifiable protection. diff --git a/src/main/java/org/opentripplanner/raptor/util/paretoset/ParetoSetEventListenerComposite.java b/src/main/java/org/opentripplanner/raptor/util/paretoset/ParetoSetEventListenerComposite.java index 01d68af4392..46dd929869c 100644 --- a/src/main/java/org/opentripplanner/raptor/util/paretoset/ParetoSetEventListenerComposite.java +++ b/src/main/java/org/opentripplanner/raptor/util/paretoset/ParetoSetEventListenerComposite.java @@ -1,9 +1,9 @@ package org.opentripplanner.raptor.util.paretoset; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.List; +import javax.annotation.Nullable; +import org.opentripplanner.raptor.util.composite.CompositeUtil; /** * The {@link ParetoSet} do only support ONE listener, this class uses the composite pattern to @@ -14,17 +14,29 @@ */ public class ParetoSetEventListenerComposite implements ParetoSetEventListener { - private final List> listeners = new ArrayList<>(); + private final List> listeners; + /** + * Take a list of listeners and return a composite listener. Input listeners witch is {@code null} + * is skipped. If no listeners are provided, all listeners are {@code null}, then + * {@code null} is returned. If just one listener is passed in the listener it-self is returned + * (without any wrapper). If more than one listener exists, a composite instance is returned. + */ + @Nullable @SafeVarargs - public ParetoSetEventListenerComposite(ParetoSetEventListener... listeners) { - this(Arrays.asList(listeners)); + public static ParetoSetEventListener of(ParetoSetEventListener... listeners) { + return CompositeUtil.of( + ParetoSetEventListenerComposite::new, + it -> it instanceof ParetoSetEventListenerComposite, + it -> ((ParetoSetEventListenerComposite) it).listeners, + listeners + ); } private ParetoSetEventListenerComposite( Collection> listeners ) { - this.listeners.addAll(listeners); + this.listeners = List.copyOf(listeners); } @Override @@ -47,4 +59,9 @@ public void notifyElementRejected(T element, T rejectedByElement) { it.notifyElementRejected(element, rejectedByElement); } } + + @Override + public String toString() { + return "ParetoSetEventListenerComposite{" + "listeners=" + listeners + '}'; + } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java b/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java index 7ac3dd1caf6..1e2dd3f2d4e 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/RoutingWorker.java @@ -230,10 +230,18 @@ private Duration searchWindowUsed() { : Duration.ofSeconds(raptorSearchParamsUsed.searchWindowInSeconds()); } - private Void routeDirectStreet( + private List routeDirectStreet( List itineraries, Collection routingErrors ) { + // TODO: Add support for via search to the direct-street search and remove this. + // The direct search is used to prune away silly transit results and it + // would be nice to also support via as a feature in the direct-street + // search. + if (request.isViaSearch()) { + return null; + } + debugTimingAggregator.startedDirectStreetRouter(); try { itineraries.addAll(DirectStreetRouter.route(serverContext, request)); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/FilterTransitWhenDirectModeIsEmpty.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/FilterTransitWhenDirectModeIsEmpty.java index 01a8c9ab112..09431f0be19 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/FilterTransitWhenDirectModeIsEmpty.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/FilterTransitWhenDirectModeIsEmpty.java @@ -55,7 +55,7 @@ public class FilterTransitWhenDirectModeIsEmpty { private final StreetMode originalDirectMode; public FilterTransitWhenDirectModeIsEmpty(RequestModes modes) { - this.originalDirectMode = modes.directMode; + this(modes.directMode); } public FilterTransitWhenDirectModeIsEmpty(StreetMode originalDirectMode) { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java index 37bb7270902..0654a73700d 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/TransitRouter.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.stream.IntStream; import org.opentripplanner.ext.ridehailing.RideHailingAccessShifter; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.model.plan.Itinerary; @@ -41,7 +42,10 @@ import org.opentripplanner.routing.framework.DebugTimingAggregator; import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.street.search.TemporaryVerticesContainer; +import org.opentripplanner.transit.model.framework.EntityNotFoundException; +import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.network.grouppriority.TransitGroupPriorityService; +import org.opentripplanner.transit.model.site.StopLocation; public class TransitRouter { @@ -133,7 +137,8 @@ private TransitRouterResult route() { serverContext.raptorConfig().isMultiThreaded(), accessEgresses.getAccesses(), accessEgresses.getEgresses(), - serverContext.meterRegistry() + serverContext.meterRegistry(), + this::listStopIndexes ); // Route transit @@ -368,4 +373,18 @@ private TemporaryVerticesContainer createTemporaryVerticesContainer( request.journey().egress().mode() ); } + + private IntStream listStopIndexes(FeedScopedId stopLocationId) { + Collection stops = serverContext + .transitService() + .getStopOrChildStops(stopLocationId); + + if (stops.isEmpty()) { + throw new EntityNotFoundException( + "Stop, station, multimodal station or group of stations", + stopLocationId + ); + } + return stops.stream().mapToInt(StopLocation::getIndex); + } } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/LookupStopIndexCallback.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/LookupStopIndexCallback.java new file mode 100644 index 00000000000..1ffd3584777 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/LookupStopIndexCallback.java @@ -0,0 +1,35 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers; + +import java.util.Collection; +import java.util.stream.IntStream; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +/** + * The raptor mapper does not have access to the transit layer, so it need help to + * lookup stop-location indexes (Stop index used by Raptor). There is a one-to-one + * mapping between stops and stop-index, but a station, multimodal-station or group-of-stations + * will most likely contain more than one stop. + */ +@FunctionalInterface +public interface LookupStopIndexCallback { + /** + * The implementation of this method should list all stop indexes part of the entity referenced + * by the given id. + * @return a stream of stop indexes. We return a stream here because we need to merge this with + * the indexes of other stops. + */ + IntStream lookupStopLocationIndexes(FeedScopedId stopLocationId); + + /** + * Take a set of stop location ids and convert them into a sorted distinct list of + * stop indexes. + */ + default int[] lookupStopLocationIndexes(Collection stopLocationIds) { + return stopLocationIds + .stream() + .flatMapToInt(this::lookupStopLocationIndexes) + .sorted() + .distinct() + .toArray(); + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java index 948b132e408..dc602b2071f 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapper.java @@ -7,6 +7,7 @@ import java.time.ZonedDateTime; import java.util.Collection; import java.util.List; +import java.util.function.Predicate; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.raptor.api.model.GeneralizedCostRelaxFunction; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; @@ -18,14 +19,15 @@ import org.opentripplanner.raptor.api.request.PassThroughPoint; import org.opentripplanner.raptor.api.request.RaptorRequest; import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; +import org.opentripplanner.raptor.api.request.RaptorViaLocation; import org.opentripplanner.raptor.rangeraptor.SystemErrDebugLogger; import org.opentripplanner.routing.algorithm.raptoradapter.router.performance.PerformanceTimersForRaptor; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; import org.opentripplanner.routing.api.request.DebugEventType; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.framework.CostLinearFunction; +import org.opentripplanner.routing.api.request.via.ViaLocation; import org.opentripplanner.transit.model.network.grouppriority.DefaultTransitGroupPriorityCalculator; -import org.opentripplanner.transit.model.site.StopLocation; public class RaptorRequestMapper { @@ -35,6 +37,7 @@ public class RaptorRequestMapper { private final long transitSearchTimeZeroEpocSecond; private final boolean isMultiThreadedEnbled; private final MeterRegistry meterRegistry; + private final LookupStopIndexCallback lookUpStopIndex; private RaptorRequestMapper( RouteRequest request, @@ -42,7 +45,8 @@ private RaptorRequestMapper( Collection accessPaths, Collection egressPaths, long transitSearchTimeZeroEpocSecond, - MeterRegistry meterRegistry + MeterRegistry meterRegistry, + LookupStopIndexCallback lookUpStopIndex ) { this.request = request; this.isMultiThreadedEnbled = isMultiThreaded; @@ -50,6 +54,7 @@ private RaptorRequestMapper( this.egressPaths = egressPaths; this.transitSearchTimeZeroEpocSecond = transitSearchTimeZeroEpocSecond; this.meterRegistry = meterRegistry; + this.lookUpStopIndex = lookUpStopIndex; } public static RaptorRequest mapRequest( @@ -58,7 +63,8 @@ public static RaptorRequest mapRequest( boolean isMultiThreaded, Collection accessPaths, Collection egressPaths, - MeterRegistry meterRegistry + MeterRegistry meterRegistry, + LookupStopIndexCallback lookUpStopIndex ) { return new RaptorRequestMapper( request, @@ -66,7 +72,8 @@ public static RaptorRequest mapRequest( accessPaths, egressPaths, transitSearchTimeZero.toEpochSecond(), - meterRegistry + meterRegistry, + lookUpStopIndex ) .doMap(); } @@ -77,6 +84,13 @@ private RaptorRequest doMap() { var preferences = request.preferences(); + // TODO Fix the Raptor search so pass-through and via search can be used together. + if (hasViaLocationsAndPassThroughLocations()) { + throw new IllegalArgumentException( + "A mix of via-locations and pass-through is not allowed in this versionP." + ); + } + if (request.pageCursor() == null) { int time = relativeTime(request.dateTime()); @@ -115,7 +129,9 @@ private RaptorRequest doMap() { var r = pt.raptor(); // Note! If a pass-through-point exists, then the transit-group-priority feature is disabled - if (!request.getPassThroughPoints().isEmpty()) { + + // TODO - We need handle via locations that are not pass-through-points here + if (hasPassThroughOnly()) { mcBuilder.withPassThroughPoints(mapPassThroughPoints()); r.relaxGeneralizedCostAtDestination().ifPresent(mcBuilder::withRelaxCostAtDestination); } else if (!pt.relaxTransitGroupPriority().isNormal()) { @@ -144,6 +160,10 @@ private RaptorRequest doMap() { .addAccessPaths(accessPaths) .addEgressPaths(egressPaths); + if (hasViaLocationsOnly()) { + builder.searchParams().addViaLocations(mapViaLocations()); + } + var raptorDebugging = request.journey().transit().raptorDebugging(); if (raptorDebugging.isEnabled()) { @@ -175,19 +195,57 @@ private RaptorRequest doMap() { ) ); } - return builder.build(); } + private boolean hasPassThroughOnly() { + return request.getViaLocations().stream().allMatch(ViaLocation::isPassThroughLocation); + } + + private boolean hasViaLocationsOnly() { + return request.getViaLocations().stream().noneMatch(ViaLocation::isPassThroughLocation); + } + + private boolean hasViaLocationsAndPassThroughLocations() { + var c = request.getViaLocations(); + return ( + request.isViaSearch() && + c.stream().anyMatch(ViaLocation::isPassThroughLocation) && + c.stream().anyMatch(Predicate.not(ViaLocation::isPassThroughLocation)) + ); + } + + private List mapViaLocations() { + return request.getViaLocations().stream().map(this::mapViaLocation).toList(); + } + + private RaptorViaLocation mapViaLocation(ViaLocation input) { + if (input.isPassThroughLocation()) { + var builder = RaptorViaLocation.allowPassThrough(input.label()); + for (int stopIndex : lookUpStopIndex.lookupStopLocationIndexes(input.stopLocationIds())) { + builder.addViaStop(stopIndex); + } + return builder.build(); + } + // Visit Via location + else { + var builder = RaptorViaLocation.via(input.label(), input.minimumWaitTime()); + for (int stopIndex : lookUpStopIndex.lookupStopLocationIndexes(input.stopLocationIds())) { + builder.addViaStop(stopIndex); + } + return builder.build(); + } + } + private List mapPassThroughPoints() { - return request - .getPassThroughPoints() - .stream() - .map(p -> { - final int[] stops = p.stopLocations().stream().mapToInt(StopLocation::getIndex).toArray(); - return new PassThroughPoint(p.name(), stops); - }) - .toList(); + return request.getViaLocations().stream().map(this::mapPassThroughPoints).toList(); + } + + private PassThroughPoint mapPassThroughPoints(ViaLocation location) { + return new PassThroughPoint( + location.label(), + lookUpStopIndex.lookupStopLocationIndexes(location.stopLocationIds()) + ); } static RelaxFunction mapRelaxCost(CostLinearFunction relax) { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java index 5b9d81fa6c3..c58e18385bf 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/RaptorRoutingRequestTransitData.java @@ -10,8 +10,8 @@ import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.model.transfer.TransferService; import org.opentripplanner.raptor.api.model.RaptorConstrainedTransfer; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTransfer; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.spi.IntIterator; import org.opentripplanner.raptor.spi.RaptorConstrainedBoardingSearch; import org.opentripplanner.raptor.spi.RaptorCostCalculator; diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/api/OptimizedPath.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/api/OptimizedPath.java index ad4df23a42c..f2a36e1678c 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/api/OptimizedPath.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/api/OptimizedPath.java @@ -3,12 +3,12 @@ import java.util.function.Supplier; import org.opentripplanner.model.transfer.TransferConstraint; import org.opentripplanner.raptor.api.model.RaptorConstrainedTransfer; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.AccessPathLeg; import org.opentripplanner.raptor.api.path.PathLeg; import org.opentripplanner.raptor.api.path.PathStringBuilder; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.path.Path; /** diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/configure/TransferOptimizationServiceConfigurator.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/configure/TransferOptimizationServiceConfigurator.java index cb5f3d7fdab..214e79216e5 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/configure/TransferOptimizationServiceConfigurator.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/configure/TransferOptimizationServiceConfigurator.java @@ -3,8 +3,8 @@ import java.util.function.IntFunction; import javax.annotation.Nullable; import org.opentripplanner.model.transfer.TransferService; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.api.request.MultiCriteriaRequest; import org.opentripplanner.raptor.spi.RaptorCostCalculator; import org.opentripplanner.raptor.spi.RaptorTransitDataProvider; diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTail.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTail.java index 8d18c461101..b36a7d6ebfd 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTail.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/model/OptimizedPathTail.java @@ -4,10 +4,10 @@ import org.opentripplanner.framework.tostring.ValueObjectToStringBuilder; import org.opentripplanner.model.transfer.TransferConstraint; import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTransfer; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.model.RaptorValueFormatter; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.api.path.TransitPathLeg; import org.opentripplanner.raptor.path.PathBuilder; import org.opentripplanner.raptor.path.PathBuilderLeg; diff --git a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java index ca8f953ced5..ebfbf6d76b9 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/transferoptimization/services/OptimizePathDomainService.java @@ -8,9 +8,9 @@ import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.api.path.TransferPathLeg; import org.opentripplanner.raptor.api.path.TransitPathLeg; import org.opentripplanner.raptor.spi.RaptorCostCalculator; diff --git a/src/main/java/org/opentripplanner/routing/algorithm/via/ViaRoutingWorker.java b/src/main/java/org/opentripplanner/routing/algorithm/via/ViaRoutingWorker.java index 94dff22547b..a4ff6753d5a 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/via/ViaRoutingWorker.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/via/ViaRoutingWorker.java @@ -12,7 +12,7 @@ import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.RouteViaRequest; -import org.opentripplanner.routing.api.request.ViaLocation; +import org.opentripplanner.routing.api.request.ViaLocationDeprecated; import org.opentripplanner.routing.api.response.InputField; import org.opentripplanner.routing.api.response.RoutingError; import org.opentripplanner.routing.api.response.RoutingErrorCode; @@ -121,7 +121,7 @@ private ViaRoutingResponse combineRoutingResponse(List routingR private List filterTransits( Itinerary i, List itineraries, - ViaLocation viaLocation + ViaLocationDeprecated viaLocation ) { return itineraries.stream().filter(withinSlackTest(i, viaLocation)).toList(); } @@ -129,7 +129,7 @@ private List filterTransits( /** * Only allow departures within min/max slack time. */ - private Predicate withinSlackTest(Itinerary i, ViaLocation v) { + private Predicate withinSlackTest(Itinerary i, ViaLocationDeprecated v) { var earliestDeparturetime = i.endTime().plus(v.minSlack()); var latestDeparturetime = i.endTime().plus(v.maxSlack()); diff --git a/src/main/java/org/opentripplanner/routing/api/RoutingService.java b/src/main/java/org/opentripplanner/routing/api/RoutingService.java index f840ac27524..fcea16e3991 100644 --- a/src/main/java/org/opentripplanner/routing/api/RoutingService.java +++ b/src/main/java/org/opentripplanner/routing/api/RoutingService.java @@ -8,5 +8,10 @@ public interface RoutingService { RoutingResponse route(RouteRequest request); + /** + * @deprecated We will replace the complex via-search with a simpler version part of the + * existing trip search. + */ + @Deprecated ViaRoutingResponse route(RouteViaRequest request); } diff --git a/src/main/java/org/opentripplanner/routing/api/request/PassThroughPoint.java b/src/main/java/org/opentripplanner/routing/api/request/PassThroughPoint.java deleted file mode 100644 index fa63c2e5668..00000000000 --- a/src/main/java/org/opentripplanner/routing/api/request/PassThroughPoint.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.opentripplanner.routing.api.request; - -import java.util.List; -import javax.annotation.Nullable; -import org.opentripplanner.transit.model.site.StopLocation; - -/** - * Defines one pass-through point which the journey must pass through. - */ -public record PassThroughPoint(List stopLocations, @Nullable String name) { - /** - * Get the one or multiple stops of the pass-through point, of which only one is required to be - * passed through. - */ - @Override - public List stopLocations() { - return stopLocations; - } - - /** - * Get an optional name of the pass-through point for debugging and logging. - */ - @Override - @Nullable - public String name() { - return name; - } -} diff --git a/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java b/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java index 76e5dcc558a..328ecb73e52 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java +++ b/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java @@ -21,6 +21,7 @@ import org.opentripplanner.model.plan.paging.cursor.PageCursor; import org.opentripplanner.routing.api.request.preference.RoutingPreferences; import org.opentripplanner.routing.api.request.request.JourneyRequest; +import org.opentripplanner.routing.api.request.via.ViaLocation; import org.opentripplanner.routing.api.response.InputField; import org.opentripplanner.routing.api.response.RoutingError; import org.opentripplanner.routing.api.response.RoutingErrorCode; @@ -56,7 +57,7 @@ public class RouteRequest implements Cloneable, Serializable { private GenericLocation to; - private List passThroughPoints = Collections.emptyList(); + private List via = Collections.emptyList(); private Instant dateTime = Instant.now(); @@ -278,12 +279,19 @@ public void setTo(GenericLocation to) { this.to = to; } - public List getPassThroughPoints() { - return passThroughPoints; + /** + * Return {@code true} if at least one via location is set! + */ + public boolean isViaSearch() { + return !via.isEmpty(); + } + + public List getViaLocations() { + return via; } - public void setPassThroughPoints(final List passThroughPoints) { - this.passThroughPoints = passThroughPoints; + public void setViaLocations(final List via) { + this.via = via; } /** @@ -291,15 +299,15 @@ public void setPassThroughPoints(final List passThroughPoints) * latest-departure-time(LDT). In case of a reverse search it will be the time from earliest to * latest arrival time (LAT - EAT). *

- * All optimal travels that depart within the search window is guaranteed to be found. + * All optimal travels that depart within the search window are guaranteed to be found. *

* This is sometimes referred to as the Range Raptor Search Window - but could be used in a none * Transit search as well; Hence this is named search-window and not raptor-search-window. Do not * confuse this with the travel-window, which is the time between EDT to LAT. *

* Use {@code null} to unset, and {@link Duration#ZERO} to do one Raptor iteration. The value is - * dynamically assigned a suitable value, if not set. In a small to medium size operation you may - * use a fixed value, like 60 minutes. If you have a mixture of high frequency cities routes and + * dynamically assigned a suitable value, if not set. In a small-to-medium size operation, you may + * use a fixed value, like 60 minutes. If you have a mixture of high-frequency city routes and * infrequent long distant journeys, the best option is normally to use the dynamic auto * assignment. *

diff --git a/src/main/java/org/opentripplanner/routing/api/request/RouteViaRequest.java b/src/main/java/org/opentripplanner/routing/api/request/RouteViaRequest.java index 97e492ffd98..7d91558cf77 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/RouteViaRequest.java +++ b/src/main/java/org/opentripplanner/routing/api/request/RouteViaRequest.java @@ -14,7 +14,11 @@ /** * Trip planning request with a list of via points. + * + * @deprecated We will replace the complex via-search with a simpler version part of the + * existing trip search. */ +@Deprecated public class RouteViaRequest implements Serializable { private final GenericLocation from; @@ -27,7 +31,10 @@ public class RouteViaRequest implements Serializable { private final Locale locale; private final Integer numItineraries; - private RouteViaRequest(List viaLocations, List viaJourneys) { + private RouteViaRequest( + List viaLocations, + List viaJourneys + ) { if (viaLocations == null || viaLocations.isEmpty()) { throw new IllegalArgumentException("viaLocations must not be empty"); } @@ -67,7 +74,10 @@ private RouteViaRequest(Builder builder) { this.numItineraries = builder.numItineraries; } - public static Builder of(List viaLocations, List viaJourneys) { + public static Builder of( + List viaLocations, + List viaJourneys + ) { return new Builder(new RouteViaRequest(viaLocations, viaJourneys)); } @@ -230,8 +240,8 @@ public Builder withNumItineraries(Integer numItineraries) { } /** - * ViaSegments contains the {@link JourneyRequest} to the next {@link ViaLocation}. The last + * ViaSegments contains the {@link JourneyRequest} to the next {@link ViaLocationDeprecated}. The last * segment has null viaLocation, as `to` is the destination of that segment. */ - public record ViaSegment(JourneyRequest journeyRequest, ViaLocation viaLocation) {} + public record ViaSegment(JourneyRequest journeyRequest, ViaLocationDeprecated viaLocation) {} } diff --git a/src/main/java/org/opentripplanner/routing/api/request/ViaLocation.java b/src/main/java/org/opentripplanner/routing/api/request/ViaLocationDeprecated.java similarity index 82% rename from src/main/java/org/opentripplanner/routing/api/request/ViaLocation.java rename to src/main/java/org/opentripplanner/routing/api/request/ViaLocationDeprecated.java index 9701095a382..7d864dddb16 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/ViaLocation.java +++ b/src/main/java/org/opentripplanner/routing/api/request/ViaLocationDeprecated.java @@ -11,8 +11,12 @@ * @param passThroughPoint Does the via point represent a pass through * @param minSlack Minimum time that is allowed to wait for interchange. * @param maxSlack Maximum time to wait for next departure. + * + * @deprecated We will replace the complex via-search with a simpler version part of the + * existing trip search. */ -public record ViaLocation( +@Deprecated +public record ViaLocationDeprecated( GenericLocation point, boolean passThroughPoint, Duration minSlack, @@ -21,7 +25,7 @@ public record ViaLocation( public static final Duration DEFAULT_MAX_SLACK = Duration.ofHours(1); public static final Duration DEFAULT_MIN_SLACK = Duration.ofMinutes(5); - public ViaLocation { + public ViaLocationDeprecated { Objects.requireNonNull(minSlack); Objects.requireNonNull(maxSlack); Objects.requireNonNull(point); diff --git a/src/main/java/org/opentripplanner/routing/api/request/via/AbstractViaLocation.java b/src/main/java/org/opentripplanner/routing/api/request/via/AbstractViaLocation.java new file mode 100644 index 00000000000..2f694df53cb --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/api/request/via/AbstractViaLocation.java @@ -0,0 +1,44 @@ +package org.opentripplanner.routing.api.request.via; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nullable; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +public abstract class AbstractViaLocation implements ViaLocation { + + private final String label; + private final List stopLocationIds; + + public AbstractViaLocation(String label, Collection stopLocationIds) { + this.label = label; + this.stopLocationIds = List.copyOf(stopLocationIds); + } + + @Nullable + @Override + public String label() { + return label; + } + + @Override + public List stopLocationIds() { + return stopLocationIds; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbstractViaLocation that = (AbstractViaLocation) o; + return ( + Objects.equals(label, that.label) && Objects.equals(stopLocationIds, that.stopLocationIds) + ); + } + + @Override + public int hashCode() { + return Objects.hash(label, stopLocationIds); + } +} diff --git a/src/main/java/org/opentripplanner/routing/api/request/via/PassThroughViaLocation.java b/src/main/java/org/opentripplanner/routing/api/request/via/PassThroughViaLocation.java new file mode 100644 index 00000000000..1116031cec6 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/api/request/via/PassThroughViaLocation.java @@ -0,0 +1,37 @@ +package org.opentripplanner.routing.api.request.via; + +import java.util.Collection; +import javax.annotation.Nullable; +import org.opentripplanner.framework.tostring.ToStringBuilder; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +/** + * One of the listed stop locations or one of its children must be visited. An on-board + * intermediate stop visit is ok, as well as boarding or alighting at one of the stops. + */ +public class PassThroughViaLocation extends AbstractViaLocation { + + @SuppressWarnings("DataFlowIssue") + public PassThroughViaLocation(@Nullable String label, Collection stopLocationIds) { + super(label, stopLocationIds); + if (stopLocationIds.isEmpty()) { + throw new IllegalArgumentException( + "A pass through via location must have at least one stop location. Label: " + label + ); + } + } + + @Override + public boolean isPassThroughLocation() { + return true; + } + + @Override + public String toString() { + return ToStringBuilder + .of(PassThroughViaLocation.class) + .addObj("label", label()) + .addCol("stopLocationIds", stopLocationIds()) + .toString(); + } +} diff --git a/src/main/java/org/opentripplanner/routing/api/request/via/ViaLocation.java b/src/main/java/org/opentripplanner/routing/api/request/via/ViaLocation.java new file mode 100644 index 00000000000..c2108886abc --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/api/request/via/ViaLocation.java @@ -0,0 +1,56 @@ +package org.opentripplanner.routing.api.request.via; + +import java.time.Duration; +import java.util.List; +import javax.annotation.Nullable; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +/** + * Defines a via location which the journey must route through. At least one stop location or + * coordinate must exist. When routing, the via-location is visited if at least one of the stops + * or coordinates is visited, before the journey continues. There is no need to visit any other + * stop location or coordinate. + *

+ * The stop locations and coordinates are distinct locations. In earlier versions of OTP the + * coordinates were used as a fallback for when a stop was not found. But in this version, a + * {@link org.opentripplanner.transit.model.framework.EntityNotFoundException} is thrown if + * one of the stops does not exist. The search does NOT try to be smart and recover from an + * entity no found exception. + */ +public interface ViaLocation { + /** + * Get an optional name/label of for debugging and logging. Not used in business logic. + */ + @Nullable + String label(); + + /** + * The minimum wait time is used to force the trip to stay the given duration at the via location + * before the trip is continued. This cannot be used together with allow-pass-through, since a + * pass-through stop is visited on-board. + */ + default Duration minimumWaitTime() { + return Duration.ZERO; + } + + /** + * Returns {@code true} if this location is a pass-through-point. Only stops can be visited and + * the {@code minimumWaitTime} must be zero. + */ + boolean isPassThroughLocation(); + + /** + * A list of stop witch can be used as via location together with the {@code coordinates}.A stop + * location can be a stop, a station, a multimodal station or a group of stations. + */ + List stopLocationIds(); + + /** + * Get the one or multiple stop locations. This is optional to implement, an empty + * list is returned if this is not available. + */ + default List coordinates() { + return List.of(); + } +} diff --git a/src/main/java/org/opentripplanner/routing/api/request/via/VisitViaLocation.java b/src/main/java/org/opentripplanner/routing/api/request/via/VisitViaLocation.java new file mode 100644 index 00000000000..3bd62a32a0b --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/api/request/via/VisitViaLocation.java @@ -0,0 +1,102 @@ +package org.opentripplanner.routing.api.request.via; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nullable; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.framework.time.DurationUtils; +import org.opentripplanner.framework.tostring.ToStringBuilder; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +/** + * A visit-via-location is a physical visit to one of the stops or coordinates listed. An on-board + * visit does not count , the traveler must alight or board at the given stop for it to to be + * accepted. To visit a coordinate, the traveler must walk(bike or drive) to the closest point in + * the street network from a stop and back to another stop to join the transit network. + *

+ * TODO: NOTE! Coordinates are NOT supported jet. + */ +public class VisitViaLocation extends AbstractViaLocation { + + private static final Duration MINIMUM_WAIT_TIME_MAX_LIMIT = Duration.ofHours(24); + + private final Duration minimumWaitTime; + private final List coordinates; + + public VisitViaLocation( + @Nullable String label, + @Nullable Duration minimumWaitTime, + List stopLocationIds, + List coordinates + ) { + super(label, stopLocationIds); + this.minimumWaitTime = + DurationUtils.requireNonNegative( + minimumWaitTime == null ? Duration.ZERO : minimumWaitTime, + MINIMUM_WAIT_TIME_MAX_LIMIT, + "minimumWaitTime" + ); + this.coordinates = List.copyOf(coordinates); + + if (stopLocationIds().isEmpty() && coordinates().isEmpty()) { + throw new IllegalArgumentException( + "A via location must have at least one stop location or a coordinate. Label: " + label + ); + } + } + + /** + * The minimum wait time is used to force the trip to stay the given duration at the via location + * before the trip is continued. This cannot be used together with allow-pass-through, since a + * pass-through stop is visited on-board. + */ + @Override + public Duration minimumWaitTime() { + return minimumWaitTime; + } + + @Override + public boolean isPassThroughLocation() { + return false; + } + + @Override + public List coordinates() { + return coordinates; + } + + @Override + public String toString() { + return ToStringBuilder + .of(VisitViaLocation.class) + .addObj("label", label()) + .addDuration("minimumWaitTime", minimumWaitTime, Duration.ZERO) + .addCol("stopLocationIds", stopLocationIds()) + .addObj("coordinates", coordinates) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + VisitViaLocation that = (VisitViaLocation) o; + return ( + Objects.equals(minimumWaitTime, that.minimumWaitTime) && + Objects.equals(coordinates, that.coordinates) + ); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), minimumWaitTime, coordinates); + } +} diff --git a/src/main/java/org/opentripplanner/transit/model/framework/EntityNotFoundException.java b/src/main/java/org/opentripplanner/transit/model/framework/EntityNotFoundException.java new file mode 100644 index 00000000000..362dad1b80b --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/framework/EntityNotFoundException.java @@ -0,0 +1,34 @@ +package org.opentripplanner.transit.model.framework; + +/** + * This exception is used by the main OTP business logic to signal that one of the + * ids passed in is not found. This exception should be handled in a generic way in each + * API. + *

+ * When an entity is not found, it indicates that there is a system integration error. This + * should not be used if the user type in the id, then the client should validate the id + * before it is passed into OTP. + */ +public class EntityNotFoundException extends RuntimeException { + + private final String entityName; + private final FeedScopedId id; + + /** + * Use this if the id can be of more than one type, or you would like to provide an + * alternative name. + */ + public EntityNotFoundException(String entityName, FeedScopedId id) { + this.entityName = entityName; + this.id = id; + } + + public EntityNotFoundException(Class entityType, FeedScopedId id) { + this(entityType.getSimpleName(), id); + } + + @Override + public String getMessage() { + return entityName + " entity not found: " + id; + } +} diff --git a/src/main/java/org/opentripplanner/transit/service/TransitService.java b/src/main/java/org/opentripplanner/transit/service/TransitService.java index 1e7c4ff8397..e624d13c6f8 100644 --- a/src/main/java/org/opentripplanner/transit/service/TransitService.java +++ b/src/main/java/org/opentripplanner/transit/service/TransitService.java @@ -140,6 +140,12 @@ public interface TransitService { StopLocation getStopLocation(FeedScopedId parseId); + /** + * Return all stops associated with the given id. If a Station, a MultiModalStation, or a + * GroupOfStations matches the id, then all child stops are returned. If the id matches a regular + * stops, area stop or stop group, then a list with one item is returned. + * An empty list is if nothing is found. + */ Collection getStopOrChildStops(FeedScopedId id); Collection listStopLocationGroups(); diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index b53741fa828..d5eb5adeea7 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -834,7 +834,7 @@ type QueryType { "Use the cursor to go to the next \"page\" of itineraries. Copy the cursor from the last response and keep the original request as is. This will enable you to search for itineraries in the next or previous time-window." pageCursor: String, "The list of points the journey is required to pass through." - passThroughPoints: [PassThroughPoint!], + passThroughPoints: [PassThroughPoint!] @deprecated(reason : "Use via instead"), """ Relax generalized-cost when comparing trips with a different set of transit-group-priorities. The groups are set server side for service-journey and @@ -900,6 +900,11 @@ type QueryType { triangleFactors: TriangleFactors, "Whether or not bike rental availability information will be used to plan bike rental trips." useBikeRentalAvailabilityInformation: Boolean = false, + """ + The list of via locations the journey is required to visit. All locations are + visited in the order they are listed. + """ + via: [PlanViaLocationInput!], "Wait cost is multiplied by this value. Setting this to a value lower than 1 indicates that waiting is better than staying on a vehicle. This should never be set higher than walkReluctance, since that would lead to walking down a line to avoid waiting." waitReluctance: Float = 1.0, "Walk cost is multiplied by this value. This is the main parameter to use for limiting walking." @@ -943,7 +948,7 @@ type QueryType { via: [ViaLocationInput!]!, "Whether the trip must be wheelchair accessible. Supported for the street part to the search, not implemented for the transit yet." wheelchairAccessible: Boolean = false - ): ViaTrip! @timingData + ): ViaTrip! @deprecated(reason : "The the regular plan query with via stop instead.") @timingData } type RentalVehicle implements PlaceInterface { @@ -2103,6 +2108,58 @@ input PenaltyForStreetMode { timePenalty: DoubleFunction! } +""" +One of the listed stop locations must be visited on-board a transit vehicle or the journey must +alight or board at the location. +""" +input PlanPassThroughViaLocationInput { + "The label/name of the location. This is pass-through information and is not used in routing." + label: String + """ + A list of stop locations. A stop location can be a quay, a stop place, a multimodal + stop place or a group of stop places. It is enough to visit ONE of the locations + listed. + """ + stopLocationIds: [String] +} + +""" +A via-location is used to specifying a location as an intermediate place the router must +route through. The via-location must be either a pass-through-location or a +visit-via-location. An on-board "visit" is only allowed for pass-through-via-locations, while +the visit-via-location can visit a stop-location or a coordinate and specify a +minimum-wait-time. +""" +input PlanViaLocationInput @oneOf { + passThrough: PlanPassThroughViaLocationInput + visit: PlanVisitViaLocationInput +} + +""" +A visit-via-location is a physical visit to one of the stops or coordinates listed. An +on-board visit does not count, the traveler must alight or board at the given stop for +it to to be accepted. To visit a coordinate, the traveler must walk(bike or drive) to +the closest point in the street network from a stop and back to another stop to join +the transit network. + +NOTE! Coordinates are NOT supported jet. +""" +input PlanVisitViaLocationInput { + "The label/name of the location. This is pass-through information and is not used in routing." + label: String + """ + The minimum wait time is used to force the trip to stay the given duration at the + via-location before the trip is continued. + """ + minimumWaitTime: Duration = "PT0S" + """ + A list of stop locations. A stop location can be a quay, a stop place, a multimodal + stop place or a group of stop places. It is enough to visit ONE of the locations + listed. + """ + stopLocationIds: [String] +} + """ A relax-cost is used to increase the limit when comparing one cost to another cost. This is used to include more results into the result. A `ratio=2.0` means a path(itinerary) diff --git a/src/test/java/org/opentripplanner/_support/asserts/AssertEqualsAndHashCode.java b/src/test/java/org/opentripplanner/_support/asserts/AssertEqualsAndHashCode.java new file mode 100644 index 00000000000..7e4fbe91676 --- /dev/null +++ b/src/test/java/org/opentripplanner/_support/asserts/AssertEqualsAndHashCode.java @@ -0,0 +1,33 @@ +package org.opentripplanner._support.asserts; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class AssertEqualsAndHashCode { + + private final Object subject; + + @SuppressWarnings("EqualsWithItself") + public AssertEqualsAndHashCode(Object subject) { + this.subject = subject; + assertEquals(subject, subject); + } + + public static AssertEqualsAndHashCode verify(Object subject) { + return new AssertEqualsAndHashCode(subject); + } + + public AssertEqualsAndHashCode sameAs(Object same) { + assertEquals(subject, same); + assertEquals(subject.hashCode(), same.hashCode()); + return this; + } + + public AssertEqualsAndHashCode differentFrom(Object... others) { + for (Object other : others) { + assertNotEquals(subject, other); + assertNotEquals(subject.hashCode(), other.hashCode()); + } + return this; + } +} diff --git a/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java index ab7e2b62b7c..38a411ac1cf 100644 --- a/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java @@ -39,11 +39,11 @@ import org.opentripplanner.model.plan.PlanTestConstants; import org.opentripplanner.model.plan.ScheduledTransitLeg; import org.opentripplanner.raptor.configure.RaptorConfig; -import org.opentripplanner.routing.api.request.PassThroughPoint; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.preference.StreetPreferences; import org.opentripplanner.routing.api.request.preference.TimeSlopeSafetyTriangle; +import org.opentripplanner.routing.api.request.via.ViaLocation; import org.opentripplanner.routing.core.VehicleRoutingOptimizeType; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.service.realtimevehicles.internal.DefaultRealtimeVehicleService; @@ -311,39 +311,29 @@ public void testBikeTriangleFactorsHasNoEffect(VehicleRoutingOptimizeType bot) { } @Test - void testPassThroughPoints() { + void testViaLocations() { TransitIdMapper.clearFixedFeedId(); - final List PTP1 = List.of(stop1, stop2, stop3).stream().map(STOP_TO_ID).toList(); - final List PTP2 = List.of(stop2, stop3, stop1).stream().map(STOP_TO_ID).toList(); + final List PTP1 = Stream.of(stop1, stop2, stop3).map(STOP_TO_ID).toList(); + final List PTP2 = Stream.of(stop3, stop2).map(STOP_TO_ID).toList(); final Map arguments = Map.of( "passThroughPoints", List.of(Map.of("name", "PTP1", "placeIds", PTP1), Map.of("placeIds", PTP2, "name", "PTP2")) ); - final List points = TripRequestMapper + final List viaLocations = TripRequestMapper .createRequest(executionContext(arguments)) - .getPassThroughPoints(); - assertEquals(PTP1, points.get(0).stopLocations().stream().map(STOP_TO_ID).toList()); - assertEquals("PTP1", points.get(0).name()); - assertEquals(PTP2, points.get(1).stopLocations().stream().map(STOP_TO_ID).toList()); - assertEquals("PTP2", points.get(1).name()); - } - - @Test - void testPassThroughPointsNoMatch() { - TransitIdMapper.clearFixedFeedId(); - - final Map arguments = Map.of( - "passThroughPoints", - List.of(Map.of("placeIds", List.of("F:XX:NonExisting"))) + .getViaLocations(); + assertEquals( + "PassThroughViaLocation{label: PTP1, stopLocationIds: [F:ST:stop1, F:ST:stop2, F:ST:stop3]}", + viaLocations.get(0).toString() ); - - final RuntimeException ex = assertThrows( - RuntimeException.class, - () -> TripRequestMapper.createRequest(executionContext(arguments)) + assertEquals("PTP1", viaLocations.get(0).label()); + assertEquals( + "PassThroughViaLocation{label: PTP2, stopLocationIds: [F:ST:stop3, F:ST:stop2]}", + viaLocations.get(1).toString() ); - assertEquals("No match for F:XX:NonExisting.", ex.getMessage()); + assertEquals("PTP2", viaLocations.get(1).label()); } @Test diff --git a/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripViaLocationMapperTest.java b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripViaLocationMapperTest.java new file mode 100644 index 00000000000..49304ee1e05 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripViaLocationMapperTest.java @@ -0,0 +1,141 @@ +package org.opentripplanner.apis.transmodel.mapping; + +import static java.util.Map.entry; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.apis.transmodel.model.plan.ViaLocationInputType.FIELD_LABEL; +import static org.opentripplanner.apis.transmodel.model.plan.ViaLocationInputType.FIELD_MINIMUM_WAIT_TIME; +import static org.opentripplanner.apis.transmodel.model.plan.ViaLocationInputType.FIELD_PASS_THROUGH; +import static org.opentripplanner.apis.transmodel.model.plan.ViaLocationInputType.FIELD_STOP_LOCATION_IDS; +import static org.opentripplanner.apis.transmodel.model.plan.ViaLocationInputType.FIELD_VISIT; + +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TripViaLocationMapperTest { + + public static final String LABEL = "TestLabel"; + public static final Duration MIN_WAIT_TIME = Duration.ofMinutes(5); + public static final List LIST_IDS_INPUT = List.of("F:ID1", "F:ID2"); + public static final String EXPECTED_IDS_AS_STRING = "[F:ID1, F:ID2]"; + + @BeforeEach + void setup() { + TransitIdMapper.clearFixedFeedId(); + } + + @Test + void testMapToVisitViaLocations() { + Map input = Map.ofEntries( + entry(FIELD_VISIT, visitInput(LABEL, MIN_WAIT_TIME, LIST_IDS_INPUT)) + ); + var result = TripViaLocationMapper.mapToViaLocations(List.of(input)); + + var via = result.getFirst(); + + assertEquals(LABEL, via.label()); + assertEquals(MIN_WAIT_TIME, via.minimumWaitTime()); + assertEquals(EXPECTED_IDS_AS_STRING, via.stopLocationIds().toString()); + assertFalse(via.isPassThroughLocation()); + assertEquals( + "[VisitViaLocation{label: TestLabel, minimumWaitTime: 5m, stopLocationIds: [F:ID1, F:ID2], coordinates: []}]", + result.toString() + ); + } + + @Test + void testMapToVisitViaLocationsWithBareMinimum() { + Map input = Map.of( + FIELD_VISIT, + Map.of(FIELD_STOP_LOCATION_IDS, List.of("F:1")) + ); + var result = TripViaLocationMapper.mapToViaLocations(List.of(input)); + + var via = result.getFirst(); + + assertNull(via.label()); + assertEquals(Duration.ZERO, via.minimumWaitTime()); + assertEquals("[F:1]", via.stopLocationIds().toString()); + assertFalse(via.isPassThroughLocation()); + } + + @Test + void tetMapToPassThrough() { + Map input = Map.of(FIELD_PASS_THROUGH, passThroughInput(LABEL, LIST_IDS_INPUT)); + var result = TripViaLocationMapper.mapToViaLocations(List.of(input)); + var via = result.getFirst(); + + assertEquals(LABEL, via.label()); + assertEquals(EXPECTED_IDS_AS_STRING, via.stopLocationIds().toString()); + assertTrue(via.isPassThroughLocation()); + assertEquals( + "PassThroughViaLocation{label: TestLabel, stopLocationIds: [F:ID1, F:ID2]}", + via.toString() + ); + } + + @Test + void tetMapToPassThroughWithBareMinimum() { + Map input = Map.of( + FIELD_PASS_THROUGH, + Map.of(FIELD_STOP_LOCATION_IDS, List.of("F:1")) + ); + var result = TripViaLocationMapper.mapToViaLocations(List.of(input)); + var via = result.getFirst(); + + assertNull(via.label()); + assertEquals("[F:1]", via.stopLocationIds().toString()); + assertTrue(via.isPassThroughLocation()); + } + + @Test + void testOneOf() { + Map input = Map.ofEntries( + entry(FIELD_VISIT, visitInput("A", Duration.ofMinutes(1), List.of("F:99"))), + entry(FIELD_PASS_THROUGH, passThroughInput(LABEL, LIST_IDS_INPUT)) + ); + var ex = assertThrows( + IllegalArgumentException.class, + () -> TripViaLocationMapper.mapToViaLocations(List.of(input)) + ); + assertEquals( + "Only one entry in 'via @oneOf' is allowed. Set: 'visit', 'passThrough'", + ex.getMessage() + ); + + ex = + assertThrows( + IllegalArgumentException.class, + () -> TripViaLocationMapper.mapToViaLocations(List.of(Map.of())) + ); + assertEquals( + "No entries in 'via @oneOf'. One of 'visit', 'passThrough' must be set.", + ex.getMessage() + ); + } + + private Map visitInput(String label, Duration minWaitTime, List ids) { + var map = new HashMap(); + if (label != null) { + map.put(FIELD_LABEL, label); + } + if (minWaitTime != null) { + map.put(FIELD_MINIMUM_WAIT_TIME, minWaitTime); + } + if (ids != null) { + map.put(FIELD_STOP_LOCATION_IDS, ids); + } + return map; + } + + private Map passThroughInput(String label, List ids) { + return visitInput(label, null, ids); + } +} diff --git a/src/test/java/org/opentripplanner/apis/transmodel/support/OneOfInputValidatorTest.java b/src/test/java/org/opentripplanner/apis/transmodel/support/OneOfInputValidatorTest.java new file mode 100644 index 00000000000..e641a1cc855 --- /dev/null +++ b/src/test/java/org/opentripplanner/apis/transmodel/support/OneOfInputValidatorTest.java @@ -0,0 +1,53 @@ +package org.opentripplanner.apis.transmodel.support; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class OneOfInputValidatorTest { + + @Test + void testValidateOneReturnsTheFieldName() { + assertEquals( + "two", + OneOfInputValidator.validateOneOf(Map.of("two", "X"), "parent", "one", "two") + ); + } + + @Test + void testValidateOneOfWithEmptySetOfArguments() { + var ex = assertThrows( + IllegalArgumentException.class, + () -> OneOfInputValidator.validateOneOf(Map.of(), "parent", "one", "two") + ); + assertEquals( + "No entries in 'parent @oneOf'. One of 'one', 'two' must be set.", + ex.getMessage() + ); + } + + @Test + void testValidateOneOfWithTooManyArguments() { + var ex = assertThrows( + IllegalArgumentException.class, + () -> + OneOfInputValidator.validateOneOf(Map.of("one", "X", "two", "Y"), "parent", "one", "two") + ); + assertEquals( + "Only one entry in 'parent @oneOf' is allowed. Set: 'one', 'two'", + ex.getMessage() + ); + } + + @Test + void testValidateOneOfWithEmptyCollection() { + var ex = assertThrows( + IllegalArgumentException.class, + () -> OneOfInputValidator.validateOneOf(Map.of("one", List.of()), "parent", "one", "two") + ); + assertEquals("'one' can not be empty in 'parent @oneOf'.", ex.getMessage()); + } +} diff --git a/src/test/java/org/opentripplanner/framework/collection/CollectionUtilsTest.java b/src/test/java/org/opentripplanner/framework/collection/CollectionUtilsTest.java index e2eaab520bc..5b89811e9fe 100644 --- a/src/test/java/org/opentripplanner/framework/collection/CollectionUtilsTest.java +++ b/src/test/java/org/opentripplanner/framework/collection/CollectionUtilsTest.java @@ -7,9 +7,11 @@ import com.google.type.Month; import java.time.Duration; import java.util.ArrayList; +import java.util.Collection; import java.util.EnumSet; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Test; @@ -49,4 +51,18 @@ void testToString() { Set set = new HashSet<>(list); assertEquals("[, APRIL, JUNE, PT3H]", CollectionUtils.toString(set, NULL_STRING)); } + + @Test + void testIsEmptyMap() { + assertTrue(CollectionUtils.isEmpty((Map) null)); + assertTrue(CollectionUtils.isEmpty(Map.of())); + assertFalse(CollectionUtils.isEmpty(Map.of(1, 1))); + } + + @Test + void testIsEmptyCollection() { + assertTrue(CollectionUtils.isEmpty((Collection) null)); + assertTrue(CollectionUtils.isEmpty(List.of())); + assertFalse(CollectionUtils.isEmpty(Set.of(1))); + } } diff --git a/src/test/java/org/opentripplanner/framework/time/DurationUtilsTest.java b/src/test/java/org/opentripplanner/framework/time/DurationUtilsTest.java index 97b8afc52c0..a045e693b29 100644 --- a/src/test/java/org/opentripplanner/framework/time/DurationUtilsTest.java +++ b/src/test/java/org/opentripplanner/framework/time/DurationUtilsTest.java @@ -23,6 +23,8 @@ public class DurationUtilsTest { + private final Duration NEG_1s = Duration.ofSeconds(-1); + private final Duration D1s = Duration.ofSeconds(1); private final Duration D3d = Duration.ofDays(3); private final Duration D2h = Duration.ofHours(2); private final Duration D5m = Duration.ofMinutes(5); @@ -127,6 +129,28 @@ public void testRequireNonNegative() { assertThrows(IllegalArgumentException.class, () -> requireNonNegative(Duration.ofSeconds(-1))); } + @Test + public void testRequireNonNegativeAndMaxLimit() { + // Firs make sure legal values are accepted + requireNonNegative(Duration.ZERO, D2h, "test"); + requireNonNegative(D2h.minus(D1s), D2h, "test"); + + // null is not supported + assertThrows(NullPointerException.class, () -> requireNonNegative(null, D2h, "test")); + + // Test max limit + var ex = assertThrows( + IllegalArgumentException.class, + () -> requireNonNegative(D2h, D2h, "test") + ); + assertEquals("Duration test can't be longer or equals too 2h: PT2H", ex.getMessage()); + + // Test non-negative + ex = + assertThrows(IllegalArgumentException.class, () -> requireNonNegative(NEG_1s, D2h, "test")); + assertEquals("Duration test can't be negative: PT-1S", ex.getMessage()); + } + @Test public void testRequireNonNegativeLong() { assertThrows(NullPointerException.class, () -> requireNonNegativeLong(null, "test")); diff --git a/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java b/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java index 39022da42ca..657e060c30b 100644 --- a/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java +++ b/src/test/java/org/opentripplanner/raptor/RaptorArchitectureTest.java @@ -21,7 +21,12 @@ public class RaptorArchitectureTest { private static final Package API_PATH = API.subPackage("path"); private static final Package RAPTOR_UTIL = RAPTOR.subPackage("util"); private static final Package RAPTOR_UTIL_PARETO_SET = RAPTOR_UTIL.subPackage("paretoset"); - private static final Module RAPTOR_UTILS = Module.of(RAPTOR_UTIL, RAPTOR_UTIL_PARETO_SET); + private static final Package RAPTOR_UTIL_COMPOSITE = RAPTOR_UTIL.subPackage("composite"); + private static final Module RAPTOR_UTILS = Module.of( + RAPTOR_UTIL, + RAPTOR_UTIL_PARETO_SET, + RAPTOR_UTIL_COMPOSITE + ); private static final Package RAPTOR_SPI = RAPTOR.subPackage("spi"); private static final Package RAPTOR_PATH = RAPTOR.subPackage("path"); private static final Package CONFIGURE = RAPTOR.subPackage("configure"); @@ -78,7 +83,8 @@ void enforcePackageDependenciesRaptorSPI() { @Test void enforcePackageDependenciesUtil() { RAPTOR_UTIL.dependsOn(FRAMEWORK_UTILS, RAPTOR_SPI).verify(); - RAPTOR_UTIL_PARETO_SET.verify(); + RAPTOR_UTIL_PARETO_SET.dependsOn(RAPTOR_UTIL_COMPOSITE).verify(); + RAPTOR_UTIL_COMPOSITE.verify(); } @Test @@ -200,7 +206,8 @@ void enforcePackageDependenciesInRaptorService() { RAPTOR_UTIL, CONFIGURE, RR_INTERNAL_API, - RR_TRANSIT + RR_TRANSIT, + RANGE_RAPTOR ) .verify(); } diff --git a/src/test/java/org/opentripplanner/raptor/_data/api/TestPathBuilder.java b/src/test/java/org/opentripplanner/raptor/_data/api/TestPathBuilder.java index 007018ca6c4..3c59c8543dd 100644 --- a/src/test/java/org/opentripplanner/raptor/_data/api/TestPathBuilder.java +++ b/src/test/java/org/opentripplanner/raptor/_data/api/TestPathBuilder.java @@ -9,8 +9,8 @@ import org.opentripplanner.raptor._data.transit.TestTripPattern; import org.opentripplanner.raptor._data.transit.TestTripSchedule; import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.path.PathBuilder; import org.opentripplanner.raptor.spi.DefaultSlackProvider; import org.opentripplanner.raptor.spi.RaptorCostCalculator; diff --git a/src/test/java/org/opentripplanner/raptor/_data/api/TestRaptorPath.java b/src/test/java/org/opentripplanner/raptor/_data/api/TestRaptorPath.java index dab47a6d18b..b1cd2c4d411 100644 --- a/src/test/java/org/opentripplanner/raptor/_data/api/TestRaptorPath.java +++ b/src/test/java/org/opentripplanner/raptor/_data/api/TestRaptorPath.java @@ -4,12 +4,12 @@ import java.util.stream.Stream; import javax.annotation.Nullable; import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTripSchedule; import org.opentripplanner.raptor.api.path.AccessPathLeg; import org.opentripplanner.raptor.api.path.EgressPathLeg; import org.opentripplanner.raptor.api.path.PathLeg; import org.opentripplanner.raptor.api.path.RaptorPath; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.api.path.TransitPathLeg; /** diff --git a/src/test/java/org/opentripplanner/raptor/_data/transit/TestTransitData.java b/src/test/java/org/opentripplanner/raptor/_data/transit/TestTransitData.java index 97ad09e3bfd..5f1617075c4 100644 --- a/src/test/java/org/opentripplanner/raptor/_data/transit/TestTransitData.java +++ b/src/test/java/org/opentripplanner/raptor/_data/transit/TestTransitData.java @@ -16,9 +16,9 @@ import org.opentripplanner.model.transfer.TransferConstraint; import org.opentripplanner.raptor._data.RaptorTestConstants; import org.opentripplanner.raptor.api.model.RaptorConstrainedTransfer; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.model.RaptorTransfer; import org.opentripplanner.raptor.api.model.RaptorTripPattern; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; import org.opentripplanner.raptor.rangeraptor.SystemErrDebugLogger; import org.opentripplanner.raptor.spi.DefaultSlackProvider; diff --git a/src/test/java/org/opentripplanner/raptor/api/request/ViaLocationTest.java b/src/test/java/org/opentripplanner/raptor/api/request/ViaLocationTest.java new file mode 100644 index 00000000000..2aca0845ff8 --- /dev/null +++ b/src/test/java/org/opentripplanner/raptor/api/request/ViaLocationTest.java @@ -0,0 +1,251 @@ +package org.opentripplanner.raptor.api.request; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.raptor._data.transit.TestTransfer; +import org.opentripplanner.raptor.api.model.RaptorConstants; +import org.opentripplanner.raptor.api.model.RaptorTransfer; + +class ViaLocationTest { + + private static final int STOP_A = 12; + private static final int STOP_B = 13; + private static final int STOP_C = 14; + private static final Duration WAIT_TIME = Duration.ofMinutes(3); + private static final int WAIT_TIME_SEC = (int) WAIT_TIME.toSeconds(); + private static final int C1 = 200; + private static final int TX_DURATION = 35; + private static final RaptorTransfer TX = new TestTransfer(STOP_B, TX_DURATION, C1); + + @Test + void passThroughStop() { + var subject = RaptorViaLocation.allowPassThrough("PassThrough A").addViaStop(STOP_C).build(); + + assertEquals("PassThrough A", subject.label()); + assertTrue(subject.allowPassThrough()); + assertEquals(RaptorConstants.ZERO, subject.minimumWaitTime()); + assertEquals( + "Via{label: PassThrough A, allowPassThrough, connections: [C]}", + subject.toString(ViaLocationTest::stopName) + ); + assertEquals( + "Via{label: PassThrough A, allowPassThrough, connections: [14]}", + subject.toString() + ); + + assertEquals(1, subject.connections().size()); + + var c = subject.connections().getFirst(); + assertEquals(STOP_C, c.fromStop()); + assertEquals(STOP_C, c.toStop()); + assertTrue(c.isSameStop()); + Assertions.assertEquals(RaptorConstants.ZERO, c.durationInSeconds()); + assertEquals(RaptorConstants.ZERO, c.c1()); + } + + @Test + void viaSingleStop() { + var subject = RaptorViaLocation.via("Tx A").addViaStop(STOP_B).build(); + + assertEquals("Tx A", subject.label()); + assertFalse(subject.allowPassThrough()); + assertEquals(RaptorConstants.ZERO, subject.minimumWaitTime()); + assertEquals("Via{label: Tx A, connections: [B]}", subject.toString(ViaLocationTest::stopName)); + assertEquals("Via{label: Tx A, connections: [13]}", subject.toString()); + assertEquals(1, subject.connections().size()); + + var connection = subject.connections().getFirst(); + assertEquals(STOP_B, connection.fromStop()); + assertEquals(STOP_B, connection.toStop()); + assertTrue(connection.isSameStop()); + Assertions.assertEquals(RaptorConstants.ZERO, connection.durationInSeconds()); + assertEquals(RaptorConstants.ZERO, connection.c1()); + } + + @Test + void testCombinationOfPassThroughAndTransfer() { + var subject = RaptorViaLocation + .allowPassThrough("PassThrough A") + .addViaStop(STOP_C) + .addViaTransfer(STOP_A, TX) + .build(); + + assertEquals("PassThrough A", subject.label()); + assertTrue(subject.allowPassThrough()); + assertEquals(RaptorConstants.ZERO, subject.minimumWaitTime()); + assertEquals( + "Via{label: PassThrough A, allowPassThrough, connections: [C, A~B 35s]}", + subject.toString(ViaLocationTest::stopName) + ); + assertEquals(2, subject.connections().size()); + + var c = subject.connections().getFirst(); + assertEquals(STOP_C, c.fromStop()); + assertEquals(STOP_C, c.toStop()); + assertTrue(c.isSameStop()); + Assertions.assertEquals(RaptorConstants.ZERO, c.durationInSeconds()); + assertEquals(RaptorConstants.ZERO, c.c1()); + + c = subject.connections().getLast(); + assertEquals(STOP_A, c.fromStop()); + assertEquals(STOP_B, c.toStop()); + assertFalse(c.isSameStop()); + Assertions.assertEquals(TX_DURATION, c.durationInSeconds()); + assertEquals(C1, c.c1()); + } + + @Test + void viaStopAorCWithWaitTime() { + var subject = RaptorViaLocation + .via("Plaza", WAIT_TIME) + .addViaStop(STOP_C) + .addViaTransfer(STOP_A, TX) + .build(); + + assertEquals("Plaza", subject.label()); + assertFalse(subject.allowPassThrough()); + assertEquals(WAIT_TIME_SEC, subject.minimumWaitTime()); + assertEquals( + "Via{label: Plaza, minWaitTime: 3m, connections: [C 3m, A~B 3m35s]}", + subject.toString(ViaLocationTest::stopName) + ); + assertEquals(2, subject.connections().size()); + + var connection = subject.connections().getFirst(); + assertEquals(STOP_C, connection.fromStop()); + assertEquals(STOP_C, connection.toStop()); + assertTrue(connection.isSameStop()); + Assertions.assertEquals(WAIT_TIME_SEC, connection.durationInSeconds()); + assertEquals(RaptorConstants.ZERO, connection.c1()); + + connection = subject.connections().getLast(); + assertEquals(STOP_A, connection.fromStop()); + assertEquals(STOP_B, connection.toStop()); + assertFalse(connection.isSameStop()); + Assertions.assertEquals(WAIT_TIME_SEC + TX.durationInSeconds(), connection.durationInSeconds()); + assertEquals(C1, connection.c1()); + } + + static List isBetterThanTestCases() { + // Subject is: STOP_A, STOP_B, MIN_DURATION, C1 + return List.of( + Arguments.of(STOP_A, STOP_B, TX_DURATION, C1, true, "Same"), + Arguments.of(STOP_C, STOP_B, TX_DURATION, C1, false, "toStop differ"), + Arguments.of(STOP_A, STOP_C, TX_DURATION, C1, false, "fromStop differ"), + Arguments.of(STOP_A, STOP_B, TX_DURATION + 1, C1, true, "Wait time is better"), + Arguments.of(STOP_A, STOP_B, TX_DURATION - 1, C1, false, "Wait time is worse"), + Arguments.of(STOP_A, STOP_B, TX_DURATION, C1 + 1, true, "C1 is better"), + Arguments.of(STOP_A, STOP_B, TX_DURATION, C1 - 1, false, "C1 is worse") + ); + } + + @ParameterizedTest + @MethodSource("isBetterThanTestCases") + void isBetterThan( + int fromStop, + int toStop, + int minWaitTime, + int c1, + boolean expected, + String description + ) { + var subject = RaptorViaLocation + .via("Subject") + .addViaTransfer(STOP_A, new TestTransfer(STOP_B, TX_DURATION, C1)) + .build() + .connections() + .getFirst(); + + var candidate = RaptorViaLocation + .via("Candidate") + .addViaTransfer(fromStop, new TestTransfer(toStop, minWaitTime, c1)) + .build() + .connections() + .getFirst(); + + assertEquals(subject.isBetterOrEqual(candidate), expected, description); + } + + @Test + void throwsExceptionIfConnectionsIsNotParetoOptimal() { + var e = assertThrows( + IllegalArgumentException.class, + () -> + RaptorViaLocation + .via("S") + .addViaTransfer(STOP_A, new TestTransfer(STOP_B, TX_DURATION, C1)) + .addViaTransfer(STOP_A, new TestTransfer(STOP_B, TX_DURATION, C1)) + .build() + ); + assertEquals( + "All connection need to be pareto-optimal: (12~13 35s) <-> (12~13 35s)", + e.getMessage() + ); + } + + @Test + void testEqualsAndHashCode() { + var subject = RaptorViaLocation.via(null).addViaTransfer(STOP_A, TX).build(); + var same = RaptorViaLocation.via(null).addViaTransfer(STOP_A, TX).build(); + // Slightly less wait-time and slightly larger cost(c1) + var other = RaptorViaLocation + .via(null, Duration.ofSeconds(1)) + .addViaTransfer(STOP_A, TX) + .build(); + + assertEquals(subject, same); + assertNotEquals(subject, other); + assertNotEquals(subject, "Does not match another type"); + + assertEquals(subject.hashCode(), same.hashCode()); + assertNotEquals(subject.hashCode(), other.hashCode()); + } + + @Test + void testToString() { + var subject = RaptorViaLocation.via("A|B").addViaStop(STOP_A).addViaStop(STOP_B).build(); + assertEquals("Via{label: A|B, connections: [12, 13]}", subject.toString()); + assertEquals( + "Via{label: A|B, connections: [A, B]}", + subject.toString(ViaLocationTest::stopName) + ); + + subject = RaptorViaLocation.via(null, WAIT_TIME).addViaStop(STOP_B).build(); + assertEquals("Via{minWaitTime: 3m, connections: [13 3m]}", subject.toString()); + assertEquals( + "Via{minWaitTime: 3m, connections: [B 3m]}", + subject.toString(ViaLocationTest::stopName) + ); + + subject = RaptorViaLocation.via(null).addViaTransfer(STOP_A, TX).build(); + assertEquals("Via{connections: [12~13 35s]}", subject.toString()); + assertEquals("Via{connections: [A~B 35s]}", subject.toString(ViaLocationTest::stopName)); + + subject = RaptorViaLocation.allowPassThrough(null).addViaStop(STOP_C).build(); + assertEquals("Via{allowPassThrough, connections: [14]}", subject.toString()); + assertEquals( + "Via{allowPassThrough, connections: [C]}", + subject.toString(ViaLocationTest::stopName) + ); + } + + private static String stopName(int i) { + return switch (i) { + case 12 -> "A"; + case 13 -> "B"; + case 14 -> "C"; + default -> throw new IllegalArgumentException("Unknown stop: " + i); + }; + } +} diff --git a/src/test/java/org/opentripplanner/raptor/moduletests/J02_ViaSearchTest.java b/src/test/java/org/opentripplanner/raptor/moduletests/J02_ViaSearchTest.java new file mode 100644 index 00000000000..581cd117cb1 --- /dev/null +++ b/src/test/java/org/opentripplanner/raptor/moduletests/J02_ViaSearchTest.java @@ -0,0 +1,376 @@ +package org.opentripplanner.raptor.moduletests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.model.plan.PlanTestConstants.D2m; +import static org.opentripplanner.raptor._data.RaptorTestConstants.D1m; +import static org.opentripplanner.raptor._data.RaptorTestConstants.D30s; +import static org.opentripplanner.raptor._data.RaptorTestConstants.STOP_A; +import static org.opentripplanner.raptor._data.RaptorTestConstants.STOP_B; +import static org.opentripplanner.raptor._data.RaptorTestConstants.STOP_C; +import static org.opentripplanner.raptor._data.RaptorTestConstants.STOP_D; +import static org.opentripplanner.raptor._data.RaptorTestConstants.STOP_E; +import static org.opentripplanner.raptor._data.RaptorTestConstants.STOP_F; +import static org.opentripplanner.raptor._data.RaptorTestConstants.T00_00; +import static org.opentripplanner.raptor._data.RaptorTestConstants.T01_00; +import static org.opentripplanner.raptor._data.api.PathUtils.pathsToString; +import static org.opentripplanner.raptor._data.transit.TestAccessEgress.walk; +import static org.opentripplanner.raptor._data.transit.TestRoute.route; +import static org.opentripplanner.raptor._data.transit.TestTransfer.transfer; +import static org.opentripplanner.raptor._data.transit.TestTripSchedule.schedule; +import static org.opentripplanner.raptor.api.request.RaptorViaLocation.via; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.opentripplanner.raptor.RaptorService; +import org.opentripplanner.raptor._data.api.PathUtils; +import org.opentripplanner.raptor._data.transit.TestTransitData; +import org.opentripplanner.raptor._data.transit.TestTripSchedule; +import org.opentripplanner.raptor.api.request.RaptorProfile; +import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; +import org.opentripplanner.raptor.api.request.RaptorViaLocation; +import org.opentripplanner.raptor.configure.RaptorConfig; + +/** + * FEATURE UNDER TEST + * + * Raptor should be able to handle route request with one or more via locations. + * If a stop is specified as via location in the request, then all the results returned + * from raptor should include the stop. The stop should be a alight, board or intermediate + * stop of one of the trips in the path. + * + * It should be possible to specify more than one connection. The result should include the + * via locations in the order as they were specified in the request. Only alternatives that pass + * through all via locations should be included in the result. + * + * To support stations and other collections of stops, Raptor should also support multiple via + * connections in one via location. + */ +class J02_ViaSearchTest { + + static final List VIA_LOCATION_STOP_B = List.of(viaLocation("B", STOP_B)); + static final List VIA_LOCATION_STOP_C = List.of(viaLocation("C", STOP_C)); + static final List VIA_LOCATION_STOP_A_OR_B = List.of( + viaLocation("B&C", STOP_A, STOP_B) + ); + + static final List VIA_LOCATION_STOP_B_THEN_D = List.of( + viaLocation("B", STOP_B), + viaLocation("D", STOP_D) + ); + static final List VIA_LOCATION_STOP_C_THEN_B = List.of( + viaLocation("B", STOP_C), + viaLocation("D", STOP_B) + ); + + private final RaptorService raptorService = new RaptorService<>( + RaptorConfig.defaultConfigForTest() + ); + + private RaptorRequestBuilder prepareRequest() { + var builder = new RaptorRequestBuilder(); + + builder + .profile(RaptorProfile.MULTI_CRITERIA) + // TODO: 2023-07-24 Currently heuristics does not work with pass-through so we + // have to turn them off. Make sure to re-enable optimization later when it's fixed + .clearOptimizations(); + + builder + .searchParams() + .earliestDepartureTime(T00_00) + .latestArrivalTime(T01_00) + .searchWindow(Duration.ofMinutes(10)) + .timetable(true); + + return builder; + } + + @Test + @DisplayName( + "Basic via search with just one route. You should be forced to get off the " + + "first trip and wait for the next one at the specified via stop." + ) + void viaSearchAlightingAtViaStop() { + var data = new TestTransitData(); + + data.withRoutes( + route("R1", STOP_A, STOP_B, STOP_C, STOP_D) + .withTimetable(schedule("0:02 0:10 0:20 0:30"), schedule("0:12 0:20 0:30 0:40")) + ); + + var requestBuilder = prepareRequest(); + + requestBuilder + .searchParams() + .addAccessPaths(walk(STOP_A, D30s)) + .addViaLocation(via("C").addViaStop(STOP_C).build()) + .addEgressPaths(walk(STOP_D, D30s)); + + var result = raptorService.route(requestBuilder.build(), data); + + // Verify that we alight the first trip at stop C and board the second trip + assertEquals( + "Walk 30s ~ A ~ BUS R1 0:02 0:20 ~ C ~ BUS R1 0:30 0:40 ~ D ~ Walk 30s [0:01:30 0:40:30 39m Tₓ1 C₁3_600]", + pathsToString(result) + ); + } + + @Test + @DisplayName( + "Basic via search with just two routes. You should be forced to get off the first route, " + + "then transfer and BOARD the second trip at the specified via stop. This test that via works " + + "at the boarding stop. We will add better options for the transfer to see that the given via " + + "stop is used over the alternatives." + ) + void viaSearchArrivingByTransferAtViaStop() { + var data = new TestTransitData(); + + data + .withRoutes( + route("R1", STOP_A, STOP_B, STOP_D, STOP_E).withTimetable(schedule("0:02 0:10 0:20 0:30")), + route("R2", STOP_C, STOP_D, STOP_E).withTimetable(schedule("0:25 0:30 0:40")) + ) + // Walk 1 minute to transfer from D to C - this is the only way to visit stop C + .withTransfer(STOP_D, transfer(STOP_C, D1m)); + + var requestBuilder = prepareRequest(); + + requestBuilder + .searchParams() + .addAccessPaths(walk(STOP_A, D30s)) + .addViaLocation(via("C").addViaStop(STOP_C).build()) + .addEgressPaths(walk(STOP_E, D30s)); + + var result = raptorService.route(requestBuilder.build(), data); + + // Verify that we alight the first trip at stop C and board the second trip + assertEquals( + "Walk 30s ~ A ~ BUS R1 0:02 0:20 ~ D ~ Walk 1m ~ C ~ BUS R2 0:25 0:40 ~ E ~ Walk 30s " + + "[0:01:30 0:40:30 39m Tₓ1 C₁3_660]", + pathsToString(result) + ); + } + + @Test + @DisplayName( + "Via stop as the first stop in the journey - only the access will be used for the first " + + "part, no transit. Access arrival should be copied over to 'next' worker." + ) + void accessWalkToViaStopWithoutTransit() { + var data = new TestTransitData(); + + data.withRoutes( + route("R1", STOP_A, STOP_B, STOP_C, STOP_D) + .withTimetable( + schedule("0:02 0:05 0:10 0:15"), + // We add another trip to allow riding trip one - via B - then ride trip two, this + // is not a pareto-optimal solution and should only appear if there is something wrong. + schedule("0:12 0:15 0:20 0:25") + ) + ); + + var requestBuilder = prepareRequest(); + + // We will add access to A, B, and C, but since the B stop is the via point we expect that to + // be used + requestBuilder + .searchParams() + .addViaLocations(VIA_LOCATION_STOP_B) + // We allow access to A, B, and C - if the via search works as expected, only access to B + // should be used - access to A would require an extra transfer; C has no valid paths. + .addAccessPaths(walk(STOP_A, D30s)) + .addAccessPaths(walk(STOP_B, D30s)) + .addAccessPaths(walk(STOP_C, D30s)) + .addEgressPaths(walk(STOP_D, D30s)); + + // Verify that the journey start by walking to the via stop, the uses one trip to the destination. + // A combination of trip one and two with a transfer is not expected. + assertEquals( + PathUtils.join( + "Walk 30s ~ B ~ BUS R1 0:05 0:15 ~ D ~ Walk 30s [0:04:30 0:15:30 11m Tₓ0 C₁1_320]", + "Walk 30s ~ B ~ BUS R1 0:15 0:25 ~ D ~ Walk 30s [0:14:30 0:25:30 11m Tₓ0 C₁1_320]" + ), + pathsToString(raptorService.route(requestBuilder.build(), data)) + ); + } + + @Test + @DisplayName( + "Via stop as the last stop in the journey - only the egress will be used for the last " + + "part, no transit. The transit arrival at the via stop should be copied over to the " + + "next worker and then this should be used to add the egress - without any transfers or" + + "more transit." + ) + void transitToViaStopThenTakeEgressWalkToDestination() { + var data = new TestTransitData(); + + data.withRoutes( + route("R1", STOP_A, STOP_B, STOP_C, STOP_D) + .withTimetable( + schedule("0:02 0:05 0:10 0:20"), + // We add another trip to check that we do not transfer to the other trip at some point. + schedule("0:12 0:15 0:20 0:25") + ) + ); + + var requestBuilder = prepareRequest(); + + // We will add access to A, B, and C, but since the B stop is the via point we expect that to + // be used + requestBuilder + .searchParams() + .addAccessPaths(walk(STOP_A, D30s)) + .addViaLocations(VIA_LOCATION_STOP_C) + // We allow egress from B, C, and D - if the via search works as expected, only egress from C + // should be used - egress from B has not visited via stop C, and egress from stop D would + // require a transfer at stop C to visit the via stop - this is not an optimal path. + .addEgressPaths(walk(STOP_B, D30s)) + .addEgressPaths(walk(STOP_C, D30s)) + .addEgressPaths(walk(STOP_D, D30s)); + + assertEquals( + PathUtils.join( + "Walk 30s ~ A ~ BUS R1 0:02 0:10 ~ C ~ Walk 30s [0:01:30 0:10:30 9m Tₓ0 C₁1_200]", + "Walk 30s ~ A ~ BUS R1 0:12 0:20 ~ C ~ Walk 30s [0:11:30 0:20:30 9m Tₓ0 C₁1_200]" + ), + pathsToString(raptorService.route(requestBuilder.build(), data)) + ); + } + + @Test + @DisplayName("Multiple via points") + void multipleViaPoints() { + var data = new TestTransitData(); + + // Create two routes. + // The first one includes one via stop point. + // The second one includes the second via point. + // Both arrive at the desired destination, so normally there should not be any transfers. + data.withRoutes( + route("R2", STOP_A, STOP_B, STOP_C, STOP_D, STOP_E, STOP_F) + .withTimetable( + schedule("0:02 0:05 0:10 0:15 0:20 0:25"), + schedule("0:12 0:15 0:20 0:25 0:30 0:35"), + schedule("0:22 0:25 0:30 0:35 0:40 0:45") + ) + ); + + data.mcCostParamsBuilder().transferCost(100); + + var requestBuilder = prepareRequest(); + + requestBuilder + .searchParams() + .addAccessPaths(walk(STOP_A, D30s)) + .addViaLocations(VIA_LOCATION_STOP_B_THEN_D) + .addEgressPaths(walk(STOP_F, D30s)); + + // Verify that both via points are included + assertEquals( + "Walk 30s ~ A " + + "~ BUS R2 0:02 0:05 ~ B " + + "~ BUS R2 0:15 0:25 ~ D " + + "~ BUS R2 0:35 0:45 ~ F " + + "~ Walk 30s " + + "[0:01:30 0:45:30 44m Tₓ2 C₁4_700]", + pathsToString(raptorService.route(requestBuilder.build(), data)) + ); + } + + @Test + @DisplayName("Multiple via points works with circular lines, visit stop C than stop B") + void viaSearchWithCircularLine() { + var data = new TestTransitData(); + + data.withRoute( + route("R1", STOP_A, STOP_B, STOP_C, STOP_B, STOP_C, STOP_B, STOP_C, STOP_B, STOP_D) + .withTimetable(schedule("0:05 0:10 0:15 0:20 0:25 0:30 0:35 0:40 0:45")) + ); + + var requestBuilder = prepareRequest(); + + requestBuilder + .searchParams() + .addAccessPaths(walk(STOP_A, D30s)) + .addViaLocations(VIA_LOCATION_STOP_C_THEN_B) + .addEgressPaths(walk(STOP_D, D30s)); + + assertEquals( + "Walk 30s ~ A " + + "~ BUS R1 0:05 0:15 ~ C " + + "~ BUS R1 0:25 0:30 ~ B " + + "~ BUS R1 0:40 0:45 ~ D " + + "~ Walk 30s " + + "[0:04:30 0:45:30 41m Tₓ2 C₁4_320]", + pathsToString(raptorService.route(requestBuilder.build(), data)) + ); + } + + @Test + @DisplayName("Multiple stops in the same via location") + void testViaSearchWithManyStopsInTheViaLocation() { + var data = new TestTransitData(); + + data.withRoutes( + route("R1", STOP_A, STOP_C).withTimetable(schedule("0:04 0:15")), + route("R2", STOP_B, STOP_C).withTimetable(schedule("0:05 0:14")) + ); + + var requestBuilder = prepareRequest(); + + requestBuilder + .searchParams() + .addAccessPaths(walk(STOP_A, D30s)) + .addAccessPaths(walk(STOP_B, D2m)) + .addViaLocations(VIA_LOCATION_STOP_A_OR_B) + .addEgressPaths(walk(STOP_C, D30s)); + + // Both routes are pareto optimal. + // Route 2 is faster, but it contains more walking + // Verify that both routes are included as a valid result + assertEquals( + PathUtils.join( + "Walk 2m ~ B ~ BUS R2 0:05 0:14 ~ C ~ Walk 30s [0:03 0:14:30 11m30s Tₓ0 C₁1_440]", + "Walk 30s ~ A ~ BUS R1 0:04 0:15 ~ C ~ Walk 30s [0:03:30 0:15:30 12m Tₓ0 C₁1_380]" + ), + pathsToString(raptorService.route(requestBuilder.build(), data)) + ); + } + + @Test + @DisplayName("Test minimum wait time") + void testMinWaitTime() { + var data = new TestTransitData(); + data.withRoutes( + route("R1", STOP_A, STOP_B).withTimetable(schedule("0:02:00 0:04:00")), + route("R2", STOP_B, STOP_C) + .withTimetable(schedule("0:05:44 0:10"), schedule("0:05:45 0:11"), schedule("0:05:46 0:12")) + ); + + var requestBuilder = prepareRequest(); + var minWaitTime = Duration.ofSeconds(45); + + requestBuilder + .searchParams() + .addAccessPaths(walk(STOP_A, D30s)) + .addViaLocations(List.of(RaptorViaLocation.via("B", minWaitTime).addViaStop(STOP_B).build())) + .addEgressPaths(walk(STOP_C, D30s)); + + // We expect to bard the second trip at 0:05:45, since the minWaitTime is 45s and the + // transfer slack is 60s. + assertEquals( + "Walk 30s ~ A ~ BUS R1 0:02 0:04 ~ B ~ BUS R2 0:05:45 0:11 ~ C ~ Walk 30s " + + "[0:01:30 0:11:30 10m Tₓ1 C₁1_860]", + pathsToString(raptorService.route(requestBuilder.build(), data)) + ); + } + + private static RaptorViaLocation viaLocation(String label, int... stops) { + var builder = RaptorViaLocation.via(label); + Arrays.stream(stops).forEach(builder::addViaStop); + return builder.build(); + } +} diff --git a/src/test/java/org/opentripplanner/raptor/rangeraptor/multicriteria/StopArrivalStateParetoSetTest.java b/src/test/java/org/opentripplanner/raptor/rangeraptor/multicriteria/StopArrivalStateParetoSetTest.java index e9c39ba6bbf..60fa14e9385 100644 --- a/src/test/java/org/opentripplanner/raptor/rangeraptor/multicriteria/StopArrivalStateParetoSetTest.java +++ b/src/test/java/org/opentripplanner/raptor/rangeraptor/multicriteria/StopArrivalStateParetoSetTest.java @@ -1,8 +1,6 @@ package org.opentripplanner.raptor.rangeraptor.multicriteria; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.opentripplanner.raptor.rangeraptor.multicriteria.StopArrivalParetoSet.createEgressStopArrivalSet; -import static org.opentripplanner.raptor.rangeraptor.multicriteria.StopArrivalParetoSet.createStopArrivalSet; import java.util.Arrays; import java.util.List; @@ -20,6 +18,7 @@ import org.opentripplanner.raptor.rangeraptor.multicriteria.arrivals.McStopArrival; import org.opentripplanner.raptor.rangeraptor.multicriteria.arrivals.c1.StopArrivalFactoryC1; import org.opentripplanner.raptor.rangeraptor.multicriteria.ride.c1.PatternRideC1; +import org.opentripplanner.raptor.util.paretoset.ParetoComparator; public class StopArrivalStateParetoSetTest { @@ -33,9 +32,9 @@ public class StopArrivalStateParetoSetTest { .schedule("10:00 10:30") .build(); - // In this test each stop is used to identify the pareto vector - it is just one - // ParetoSet "subject" with multiple "stops" in it. The stop have no effect on - // the Pareto functionality. + // In this test, each stop is used to identify the pareto vector - it is just one + // ParetoSet "subject" with multiple "stops" in it. The stop has no effect on + // the Pareto functionality - the stop is not a criteria in the pareto-function. private static final int STOP_1 = 1; private static final int STOP_2 = 2; private static final int STOP_3 = 3; @@ -72,19 +71,12 @@ public class StopArrivalStateParetoSetTest { ); private static Stream testCases() { + ParetoComparator> comparator = COMPARATOR_FACTORY.compareArrivalTimeRoundAndCost(); return Stream.of( - Arguments.of( - "Stop Arrival - regular", - createStopArrivalSet(COMPARATOR_FACTORY.compareArrivalTimeRoundAndCost(), null) - ), + Arguments.of("Stop Arrival - regular", StopArrivalParetoSet.of(comparator).build()), Arguments.of( "Stop Arrival - w/egress", - createEgressStopArrivalSet( - COMPARATOR_FACTORY.compareArrivalTimeRoundCostAndOnBoardArrival(), - List.of(), - null, - null - ) + StopArrivalParetoSet.of(comparator).withEgressListener(List.of(), null).build() ) ); } @@ -169,7 +161,8 @@ public void testRoundAndTimeDominance( */ @Test public void testTransitAndTransferDoesNotAffectDominance() { - var subject = createStopArrivalSet(COMPARATOR_FACTORY.compareArrivalTimeRoundAndCost(), null); + ParetoComparator> comparator = COMPARATOR_FACTORY.compareArrivalTimeRoundAndCost(); + var subject = StopArrivalParetoSet.of(comparator).build(); subject.add(newAccessStopState(STOP_1, 20, ANY)); subject.add(newTransitStopState(ROUND_1, STOP_2, 10, ANY)); subject.add(newTransferStopState(ROUND_1, STOP_4, 8, ANY)); @@ -184,12 +177,10 @@ public void testTransitAndTransferDoesNotAffectDominance() { */ @Test public void testTransitAndTransferDoesAffectDominanceForStopArrivalsWithEgress() { - var subject = createEgressStopArrivalSet( - COMPARATOR_FACTORY.compareArrivalTimeRoundCostAndOnBoardArrival(), - List.of(), - null, - null - ); + var subject = StopArrivalParetoSet + .of(COMPARATOR_FACTORY.compareArrivalTimeRoundCostAndOnBoardArrival()) + .withEgressListener(List.of(), null) + .build(); subject.add(newAccessStopState(STOP_1, 20, ANY)); subject.add(newTransitStopState(ROUND_1, STOP_2, 10, ANY)); subject.add(newTransferStopState(ROUND_1, STOP_4, 8, ANY)); diff --git a/src/test/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/ArrivalParetoSetComparatorFactoryTest.java b/src/test/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/ArrivalParetoSetComparatorFactoryTest.java index 9b6311b58bc..5fd19fc07f5 100644 --- a/src/test/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/ArrivalParetoSetComparatorFactoryTest.java +++ b/src/test/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/ArrivalParetoSetComparatorFactoryTest.java @@ -305,5 +305,10 @@ public PathLegType arrivedBy() { public boolean arrivedOnBoard() { return arrivedOnBoard; } + + @Override + public McStopArrival addSlackToArrivalTime(int slack) { + throw new UnsupportedOperationException(); + } } } diff --git a/src/test/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/McStopArrivalTest.java b/src/test/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/McStopArrivalTest.java index 64593a91b2a..9cbb35ca0a6 100644 --- a/src/test/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/McStopArrivalTest.java +++ b/src/test/java/org/opentripplanner/raptor/rangeraptor/multicriteria/arrivals/McStopArrivalTest.java @@ -149,5 +149,10 @@ public PathLegType arrivedBy() { public boolean arrivedOnBoard() { return arrivedOnBoard; } + + @Override + public McStopArrival addSlackToArrivalTime(int slack) { + throw new UnsupportedOperationException(); + } } } diff --git a/src/test/java/org/opentripplanner/raptor/rangeraptor/transit/AccessPathsTest.java b/src/test/java/org/opentripplanner/raptor/rangeraptor/transit/AccessPathsTest.java index 8611a046a5b..505fa67706e 100644 --- a/src/test/java/org/opentripplanner/raptor/rangeraptor/transit/AccessPathsTest.java +++ b/src/test/java/org/opentripplanner/raptor/rangeraptor/transit/AccessPathsTest.java @@ -78,8 +78,9 @@ void arrivedOnBoardByNumOfRides() { @Test void calculateMaxNumberOfRides() { - assertEquals(3, create(STANDARD).calculateMaxNumberOfRides()); - assertEquals(3, create(MULTI_CRITERIA).calculateMaxNumberOfRides()); + assertEquals(0, AccessPaths.calculateMaxNumberOfRides(null)); + assertEquals(3, AccessPaths.calculateMaxNumberOfRides(create(STANDARD))); + assertEquals(3, AccessPaths.calculateMaxNumberOfRides(create(MULTI_CRITERIA))); } @Test diff --git a/src/test/java/org/opentripplanner/raptor/util/PathStringBuilderTest.java b/src/test/java/org/opentripplanner/raptor/util/PathStringBuilderTest.java index 771efe6ab03..1179712e2fd 100644 --- a/src/test/java/org/opentripplanner/raptor/util/PathStringBuilderTest.java +++ b/src/test/java/org/opentripplanner/raptor/util/PathStringBuilderTest.java @@ -5,8 +5,8 @@ import static org.opentripplanner.raptor._data.transit.TestAccessEgress.free; import org.junit.jupiter.api.Test; +import org.opentripplanner.raptor.api.model.RaptorStopNameResolver; import org.opentripplanner.raptor.api.path.PathStringBuilder; -import org.opentripplanner.raptor.api.path.RaptorStopNameResolver; public class PathStringBuilderTest { diff --git a/src/test/java/org/opentripplanner/raptor/util/composite/CompositeUtilTest.java b/src/test/java/org/opentripplanner/raptor/util/composite/CompositeUtilTest.java new file mode 100644 index 00000000000..6de1f8b842d --- /dev/null +++ b/src/test/java/org/opentripplanner/raptor/util/composite/CompositeUtilTest.java @@ -0,0 +1,76 @@ +package org.opentripplanner.raptor.util.composite; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class CompositeUtilTest { + + @Test + void testOf() { + TNamed EMPTY = null; + assertNull(composite(EMPTY)); + assertNull(composite(EMPTY, EMPTY)); + + assertEquals("A", composite(tnamed("A")).name()); + assertEquals("A", composite(EMPTY, tnamed("A"), EMPTY).name()); + assertEquals("(A:B)", composite(tnamed("A"), tnamed("B")).name()); + // Nested composites are flattened into one composite + assertEquals( + "(A:B:C)", + composite(composite(tnamed("A")), composite(tnamed("B")), composite(tnamed("C"))).name() + ); + } + + TNamed composite(TNamed... children) { + return TNamedComposite.of(children); + } + + TNamed tnamed(String name) { + return new DefaultTNamed(name); + } + + interface TNamed { + String name(); + } + + static final class DefaultTNamed implements TNamed { + + private final String name; + + public DefaultTNamed(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } + } + + static final class TNamedComposite implements TNamed { + + private final List children; + + private TNamedComposite(List children) { + this.children = children; + } + + static TNamed of(TNamed... children) { + return CompositeUtil.of( + TNamedComposite::new, + TNamedComposite.class::isInstance, + it -> ((TNamedComposite) it).children, + children + ); + } + + @Override + public String name() { + return "(" + children.stream().map(TNamed::name).collect(Collectors.joining(":")) + ")"; + } + } +} diff --git a/src/test/java/org/opentripplanner/raptor/util/paretoset/ParetoSetEventListenerCompositeTest.java b/src/test/java/org/opentripplanner/raptor/util/paretoset/ParetoSetEventListenerCompositeTest.java new file mode 100644 index 00000000000..3fe05f80afd --- /dev/null +++ b/src/test/java/org/opentripplanner/raptor/util/paretoset/ParetoSetEventListenerCompositeTest.java @@ -0,0 +1,63 @@ +package org.opentripplanner.raptor.util.paretoset; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +class ParetoSetEventListenerCompositeTest { + + public static final String EMPTY = ""; + private final TestParetoSetEventListener l1 = new TestParetoSetEventListener<>(); + private final TestParetoSetEventListener l2 = new TestParetoSetEventListener<>(); + private final ParetoSetEventListener subject = ParetoSetEventListenerComposite.of(l1, l2); + + @Test + void notifyElementAccepted() { + assertNotNull(subject); + subject.notifyElementAccepted("A"); + assertState("A", EMPTY, EMPTY); + subject.notifyElementAccepted("B"); + assertState("A B", EMPTY, EMPTY); + } + + @Test + void notifyElementDropped() { + assertNotNull(subject); + subject.notifyElementDropped("A", "x"); + assertState(EMPTY, "A", EMPTY); + subject.notifyElementDropped("C", "y"); + assertState(EMPTY, "A C", EMPTY); + } + + @Test + void notifyElementRejected() { + assertNotNull(subject); + subject.notifyElementRejected("A", "x"); + assertState(EMPTY, EMPTY, "A"); + subject.notifyElementRejected("C", "y"); + assertState(EMPTY, EMPTY, "A C"); + } + + @Test + void verifyTheListenerStructureIsFlattenOut() { + assertNotNull(subject); + assertEquals( + subject.toString(), + "ParetoSetEventListenerComposite{listeners=[" + + "TestParetoSetEventListener, TestParetoSetEventListener" + + "]}" + ); + } + + private void assertState(String accepted, String dropped, String rejected) { + assertEquals(l1.acceptedAsString(), accepted); + assertEquals(l2.acceptedAsString(), accepted); + + assertEquals(l1.droppedAsString(), dropped); + assertEquals(l2.droppedAsString(), dropped); + + assertEquals(l1.rejectedAsString(), rejected); + assertEquals(l2.rejectedAsString(), rejected); + } +} diff --git a/src/test/java/org/opentripplanner/raptor/util/paretoset/ParetoSetEventListenerTest.java b/src/test/java/org/opentripplanner/raptor/util/paretoset/ParetoSetEventListenerTest.java index 1843d2c18ba..0f23144efc6 100644 --- a/src/test/java/org/opentripplanner/raptor/util/paretoset/ParetoSetEventListenerTest.java +++ b/src/test/java/org/opentripplanner/raptor/util/paretoset/ParetoSetEventListenerTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; @@ -10,22 +9,20 @@ public class ParetoSetEventListenerTest { - private final List accepted = new ArrayList<>(); - private final List rejected = new ArrayList<>(); - private final List dropped = new ArrayList<>(); - // Given a set and function + private final TestParetoSetEventListener listener = new TestParetoSetEventListener(); + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") - private final ParetoSet subject = new ParetoSet<>( + private final ParetoSet subject = new ParetoSet<>( (l, r) -> l.v1 < r.v1 || l.v2 < r.v2, - eventListener() + listener ); @BeforeEach public void setup() { subject.clear(); - clearResult(); + listener.clear(); } @Test @@ -43,7 +40,7 @@ public void testAccept() { public void testReject() { // Add a initial value subject.add(vector(5, 1)); - clearResult(); + listener.clear(); // Add another value -> expect rejected subject.add(vector(6, 2)); @@ -56,7 +53,7 @@ public void testDropped() { subject.add(vector(2, 5)); subject.add(vector(4, 4)); subject.add(vector(5, 3)); - clearResult(); + listener.clear(); // Add another value -> expect rejected subject.add(vector(1, 5)); @@ -66,14 +63,8 @@ public void testDropped() { assertAcceptedRejectedAndDropped("[1, 0]", "", "[4, 4] [5, 3] [1, 5]"); } - private Vector vector(int u, int v) { - return new Vector("", u, v); - } - - private void clearResult() { - accepted.clear(); - rejected.clear(); - dropped.clear(); + private TestVector vector(int u, int v) { + return new TestVector("", u, v); } private void assertAcceptedRejectedAndDropped( @@ -81,32 +72,17 @@ private void assertAcceptedRejectedAndDropped( String expRejected, String expDropped ) { - assertEquals(expAccepted, toString(accepted)); - assertEquals(expRejected, toString(rejected)); - assertEquals(expDropped, toString(dropped)); - clearResult(); + assertEquals(expAccepted, listener.acceptedAsString()); + assertEquals(expRejected, listener.rejectedAsString()); + assertEquals(expDropped, listener.droppedAsString()); + listener.clear(); } - private String toString(List list) { - return list.stream().map(Vector::toString).collect(Collectors.joining(" ")); + private String toString(List list) { + return list.stream().map(TestVector::toString).collect(Collectors.joining(" ")); } - private ParetoSetEventListener eventListener() { - return new ParetoSetEventListener<>() { - @Override - public void notifyElementAccepted(Vector newElement) { - accepted.add(newElement); - } - - @Override - public void notifyElementDropped(Vector element, Vector droppedByElement) { - dropped.add(element); - } - - @Override - public void notifyElementRejected(Vector element, Vector rejectedByElement) { - rejected.add(element); - } - }; + private TestParetoSetEventListener eventListener() { + return new TestParetoSetEventListener<>(); } } diff --git a/src/test/java/org/opentripplanner/raptor/util/paretoset/ParetoSetTest.java b/src/test/java/org/opentripplanner/raptor/util/paretoset/ParetoSetTest.java index 21f998b4757..1c410c13e1c 100644 --- a/src/test/java/org/opentripplanner/raptor/util/paretoset/ParetoSetTest.java +++ b/src/test/java/org/opentripplanner/raptor/util/paretoset/ParetoSetTest.java @@ -16,29 +16,29 @@ public class ParetoSetTest { - private static final ParetoComparator DIFFERENT = (l, r) -> l.v1 != r.v1; - private static final ParetoComparator LESS_THEN = (l, r) -> l.v1 < r.v1; - private static final ParetoComparator LESS_LESS_THEN = (l, r) -> + private static final ParetoComparator DIFFERENT = (l, r) -> l.v1 != r.v1; + private static final ParetoComparator LESS_THEN = (l, r) -> l.v1 < r.v1; + private static final ParetoComparator LESS_LESS_THEN = (l, r) -> l.v1 < r.v1 || l.v2 < r.v2; - private static final ParetoComparator LESS_DIFFERENT_THEN = (l, r) -> + private static final ParetoComparator LESS_DIFFERENT_THEN = (l, r) -> l.v1 < r.v1 || l.v2 != r.v2; // Used to stored dropped vectors (callback from set) - private final List dropped = new ArrayList<>(); + private final List dropped = new ArrayList<>(); - private final ParetoSetEventListener listener = new ParetoSetEventListener<>() { + private final ParetoSetEventListener listener = new ParetoSetEventListener<>() { @Override - public void notifyElementAccepted(Vector newElement) { + public void notifyElementAccepted(TestVector newElement) { /* NOOP */ } @Override - public void notifyElementDropped(Vector element, Vector droppedByElement) { + public void notifyElementDropped(TestVector element, TestVector droppedByElement) { dropped.add(element); } @Override - public void notifyElementRejected(Vector element, Vector rejectedByElement) { + public void notifyElementRejected(TestVector element, TestVector rejectedByElement) { /* NOOP */ } }; @@ -46,7 +46,7 @@ public void notifyElementRejected(Vector element, Vector rejectedByElement) { @Test public void initiallyEmpty() { // Given a empty set - ParetoSet set = new ParetoSet<>(LESS_THEN); + ParetoSet set = new ParetoSet<>(LESS_THEN); assertEquals("{}", set.toString(), "The initial set should be empty."); assertTrue(set.isEmpty(), "The initial set should be empty."); @@ -54,14 +54,14 @@ public void initiallyEmpty() { @Test public void addVector() { - ParetoSet set = new ParetoSet<>((l, r) -> + ParetoSet set = new ParetoSet<>((l, r) -> l.v1 < r.v1 || // less than l.v2 != r.v2 || // different dominates l.v3 + 2 < r.v3 // at least 2 less than ); // When one element is added - addOk(set, new Vector("V0", 5, 5, 5)); + addOk(set, new TestVector("V0", 5, 5, 5)); // Then the element should be the only element in the set assertEquals("{V0[5, 5, 5]}", set.toString()); @@ -73,8 +73,8 @@ public void addVector() { @Test public void removeAVectorIsNotAllowed() { // Given a set with a vector - ParetoSet set = new ParetoSet<>(LESS_THEN); - Vector vector = new Vector("V0", 5); + ParetoSet set = new ParetoSet<>(LESS_THEN); + TestVector vector = new TestVector("V0", 5); addOk(set, vector); // When vector is removed, expect an exception @@ -84,23 +84,23 @@ public void removeAVectorIsNotAllowed() { @Test public void testLessThen() { // Given a set with one element: [5] - ParetoSet set = new ParetoSet<>(LESS_THEN); - set.add(new Vector("V0", 5)); + ParetoSet set = new ParetoSet<>(LESS_THEN); + set.add(new TestVector("V0", 5)); // When adding the same value - addRejected(set, new Vector("Not", 5)); + addRejected(set, new TestVector("Not", 5)); // Then expect no change in the set assertEquals("{V0[5]}", set.toString()); // When adding a greater value - addRejected(set, new Vector("Not", 6)); + addRejected(set, new TestVector("Not", 6)); // Then expect no change in the set assertEquals("{V0[5]}", set.toString()); // When adding the a lesser value - addOk(set, new Vector("V1", 4)); + addOk(set, new TestVector("V1", 4)); // Then the lesser value should replace the bigger one assertEquals("{V1[4]}", set.toString()); @@ -109,23 +109,23 @@ public void testLessThen() { @Test public void testDifferent() { // Given a set with one element: [5] - ParetoSet set = new ParetoSet<>(DIFFERENT); - set.add(new Vector("V0", 5)); + ParetoSet set = new ParetoSet<>(DIFFERENT); + set.add(new TestVector("V0", 5)); // When adding the same value - addRejected(set, new Vector("NOT ADDED", 5)); + addRejected(set, new TestVector("NOT ADDED", 5)); // Then expect no change in the set assertEquals("{V0[5]}", set.toString()); // When adding the a different value - addOk(set, new Vector("D1", 6)); + addOk(set, new TestVector("D1", 6)); // Then both values should be included assertEquals("{V0[5], D1[6]}", set.toString()); // When adding the several more different values - addOk(set, new Vector("D2", 3)); - addOk(set, new Vector("D3", 4)); - addOk(set, new Vector("D4", 8)); + addOk(set, new TestVector("D2", 3)); + addOk(set, new TestVector("D3", 4)); + addOk(set, new TestVector("D4", 8)); // Then all values should be included assertEquals("{V0[5], D1[6], D2[3], D3[4], D4[8]}", set.toString()); } @@ -134,8 +134,8 @@ public void testDifferent() { public void testTwoCriteriaWithLessThen() { // Given a set with one element with 2 criteria: [5, 5] // and a function where at least one value is less then to make it into the set - ParetoSet set = new ParetoSet<>(LESS_LESS_THEN); - Vector v0 = new Vector("V0", 5, 5); + ParetoSet set = new ParetoSet<>(LESS_LESS_THEN); + TestVector v0 = new TestVector("V0", 5, 5); // Cases that does NOT make it into the set testNotAdded(set, v0, vector(6, 5), "Add a new vector where 1st value disqualifies it"); @@ -154,8 +154,8 @@ public void testTwoCriteriaWithLessThen() { @Test public void testTwoCriteria_lessThen_and_different() { // Given a set with one element with 2 criteria: [5, 5] - ParetoSet set = new ParetoSet<>(LESS_DIFFERENT_THEN); - Vector v0 = new Vector("V0", 5, 5); + ParetoSet set = new ParetoSet<>(LESS_DIFFERENT_THEN); + TestVector v0 = new TestVector("V0", 5, 5); // Cases that does NOT make it into the set testNotAdded(set, v0, vector(6, 5), "1st value disqualifies it"); @@ -173,8 +173,8 @@ public void testTwoCriteria_lessThen_and_different() { @Test public void testTwoCriteria_lessThen_and_lessThenValue() { // Given a set with one element with 2 criteria: [5, 5] - ParetoSet set = new ParetoSet<>((l, r) -> l.v1 < r.v1 || l.v2 < r.v2 + 1); - Vector v0 = new Vector("V0", 5, 5); + ParetoSet set = new ParetoSet<>((l, r) -> l.v1 < r.v1 || l.v2 < r.v2 + 1); + TestVector v0 = new TestVector("V0", 5, 5); // Cases that does NOT make it into the set testNotAdded(set, v0, vector(6, 6), "1st value is to big"); @@ -194,25 +194,25 @@ public void testTwoCriteria_lessThen_and_lessThenValue() { @Test public void testOneVectorDominatesMany() { // Given a set and function - ParetoSet set = new ParetoSet<>(LESS_LESS_THEN); + ParetoSet set = new ParetoSet<>(LESS_LESS_THEN); // Add some values - all pareto optimal - set.add(new Vector("V0", 5, 1)); - set.add(new Vector("V1", 3, 3)); - set.add(new Vector("V2", 0, 7)); - set.add(new Vector("V3", 1, 5)); + set.add(new TestVector("V0", 5, 1)); + set.add(new TestVector("V1", 3, 3)); + set.add(new TestVector("V2", 0, 7)); + set.add(new TestVector("V3", 1, 5)); // Assert all vectors is there assertEquals("{V0[5, 1], V1[3, 3], V2[0, 7], V3[1, 5]}", set.toString()); // Add a vector which dominates all vectors in set, except [0, 7] - set.add(new Vector("V", 1, 1)); + set.add(new TestVector("V", 1, 1)); // Expect just 2 vectors assertEquals("{V2[0, 7], V[1, 1]}", set.toString()); // Add a vector which dominates all vectors in set - set.add(new Vector("X", 0, 1)); + set.add(new TestVector("X", 0, 1)); // Expect just 1 vector - the last assertEquals("{X[0, 1]}", set.toString()); @@ -221,19 +221,19 @@ public void testOneVectorDominatesMany() { @Test public void testRelaxedCriteriaAcceptingTheTwoSmallestValues() { // Given a set and function - ParetoSet set = new ParetoSet<>((l, r) -> l.v1 < r.v1 || l.v2 < r.v2 + 2); + ParetoSet set = new ParetoSet<>((l, r) -> l.v1 < r.v1 || l.v2 < r.v2 + 2); // Add some values - set.add(new Vector("V0", 5, 5)); - set.add(new Vector("V1", 4, 4)); - set.add(new Vector("V2", 5, 4)); - set.add(new Vector("V3", 5, 3)); - set.add(new Vector("V4", 5, 2)); - set.add(new Vector("V5", 5, 1)); - set.add(new Vector("V6", 5, 2)); - set.add(new Vector("V7", 5, 3)); - set.add(new Vector("V8", 5, 4)); - set.add(new Vector("V9", 5, 5)); + set.add(new TestVector("V0", 5, 5)); + set.add(new TestVector("V1", 4, 4)); + set.add(new TestVector("V2", 5, 4)); + set.add(new TestVector("V3", 5, 3)); + set.add(new TestVector("V4", 5, 2)); + set.add(new TestVector("V5", 5, 1)); + set.add(new TestVector("V6", 5, 2)); + set.add(new TestVector("V7", 5, 3)); + set.add(new TestVector("V8", 5, 4)); + set.add(new TestVector("V9", 5, 5)); // Expect all vectors with v1=4 or v2 in [1,2] assertEquals("{V1[4, 4], V4[5, 2], V5[5, 1], V6[5, 2]}", set.toString()); @@ -242,16 +242,16 @@ public void testRelaxedCriteriaAcceptingTheTwoSmallestValues() { @Test public void testRelaxedCriteriaAcceptingTenPercentExtra() { // Given a set and function - ParetoSet set = new ParetoSet<>((l, r) -> + ParetoSet set = new ParetoSet<>((l, r) -> l.v1 < r.v1 || l.v2 <= IntUtils.round(r.v2 * 1.1) ); // Add some values - set.add(new Vector("a", 1, 110)); - set.add(new Vector("a", 1, 111)); - set.add(new Vector("d", 1, 100)); - set.add(new Vector("g", 1, 111)); - set.add(new Vector("g", 1, 110)); + set.add(new TestVector("a", 1, 110)); + set.add(new TestVector("a", 1, 111)); + set.add(new TestVector("d", 1, 100)); + set.add(new TestVector("g", 1, 111)); + set.add(new TestVector("g", 1, 110)); assertEquals("{a[1, 110], d[1, 100], g[1, 110]}", set.toString()); } @@ -260,10 +260,10 @@ public void testRelaxedCriteriaAcceptingTenPercentExtra() { public void testFourCriteria() { // Given a set with one element with 2 criteria: [5, 5] // and the pareto function is: <, !=, >, <+2 - ParetoSet set = new ParetoSet<>((l, r) -> + ParetoSet set = new ParetoSet<>((l, r) -> l.v1 < r.v1 || l.v2 != r.v2 || l.v3 > r.v3 || l.v4 < r.v4 + 2 ); - Vector v0 = new Vector("V0", 5, 5, 5, 5); + TestVector v0 = new TestVector("V0", 5, 5, 5, 5); // Cases that does NOT make it into the set testNotAdded(set, v0, vector(5, 5, 5, 7), "same as v0"); @@ -299,7 +299,7 @@ public void testFourCriteria() { @Test public void testAutoScalingOfParetoSet() { // Given a set with 2 criteria - ParetoSet set = new ParetoSet<>(LESS_LESS_THEN); + ParetoSet set = new ParetoSet<>(LESS_LESS_THEN); // The initial size is set to 16. // Add 100 mutually dominant values @@ -319,13 +319,13 @@ public void testAutoScalingOfParetoSet() { @Test public void testAddingMultipleElements() { // Given a set with 2 criteria: LT and LT - ParetoSet set = new ParetoSet<>(LESS_LESS_THEN); - Vector v55 = new Vector("v55", 5, 5); - Vector v53 = new Vector("v53", 5, 3); - Vector v44 = new Vector("v44", 4, 4); - Vector v35 = new Vector("v35", 3, 5); - Vector v25 = new Vector("v25", 2, 5); - Vector v22 = new Vector("v22", 2, 2); + ParetoSet set = new ParetoSet<>(LESS_LESS_THEN); + TestVector v55 = new TestVector("v55", 5, 5); + TestVector v53 = new TestVector("v53", 5, 3); + TestVector v44 = new TestVector("v44", 4, 4); + TestVector v35 = new TestVector("v35", 3, 5); + TestVector v25 = new TestVector("v25", 2, 5); + TestVector v22 = new TestVector("v22", 2, 2); // A dominant vector should replace more than one other vector test(set, "v25", v25, v35); @@ -355,7 +355,7 @@ public void testAddingMultipleElements() { @Test public void elementsAreNotDroppedWhenParetoOptimalElementsAreAdded() { // Given a set with 2 criteria: LT and LT - ParetoSet set = new ParetoSet<>(LESS_LESS_THEN, listener); + ParetoSet set = new ParetoSet<>(LESS_LESS_THEN, listener); // Before any elements are added the list of dropped elements should be empty assertTrue(dropped.isEmpty()); @@ -372,7 +372,7 @@ public void elementsAreNotDroppedWhenParetoOptimalElementsAreAdded() { @Test public void firstElementIsDroppedWhenANewDominatingElementIsAdded() { // Given a set with 2 criteria: LT and LT and a vector [7, 3] - ParetoSet set = new ParetoSet<>(LESS_LESS_THEN, listener); + ParetoSet set = new ParetoSet<>(LESS_LESS_THEN, listener); set.add(vector(7, 3)); assertTrue(dropped.isEmpty()); @@ -397,7 +397,7 @@ public void firstElementIsDroppedWhenANewDominatingElementIsAdded() { @Test public void lastElementIsDroppedWhenANewDominatingElementIsAdded() { // Given a set with 2 criteria: LT and LT and a vector [7, 3] - ParetoSet set = new ParetoSet<>(LESS_LESS_THEN, listener); + ParetoSet set = new ParetoSet<>(LESS_LESS_THEN, listener); set.add(vector(5, 5)); set.add(vector(7, 3)); assertTrue(dropped.isEmpty()); @@ -411,7 +411,7 @@ public void lastElementIsDroppedWhenANewDominatingElementIsAdded() { * Test that both #add and #qualify return the same value - true. The set should contain the * vector, but that is left to the caller to verify. */ - private static void addOk(ParetoSet set, Vector v) { + private static void addOk(ParetoSet set, TestVector v) { assertTrue(set.qualify(v)); assertTrue(set.add(v)); } @@ -420,58 +420,68 @@ private static void addOk(ParetoSet set, Vector v) { * Test that both #add and #qualify return the same value - false. The set should not contain the * vector, but that is left to the caller to verify. */ - private static void addRejected(ParetoSet set, Vector v) { + private static void addRejected(ParetoSet set, TestVector v) { assertFalse(set.qualify(v)); assertFalse(set.add(v)); } - private static String names(Iterable set) { + private static String names(Iterable set) { return StreamSupport .stream(set.spliterator(), false) .map(it -> it == null ? "null" : it.name) .collect(Collectors.joining(" ")); } - private static Vector vector(int a, int b) { - return new Vector("Test", a, b); + private static TestVector vector(int a, int b) { + return new TestVector("Test", a, b); } - private static Vector vector(int a, int b, int c, int d) { - return new Vector("Test", a, b, c, d); + private static TestVector vector(int a, int b, int c, int d) { + return new TestVector("Test", a, b, c, d); } private static void testNotAdded( - ParetoSet set, - Vector v0, - Vector v1, + ParetoSet set, + TestVector v0, + TestVector v1, String description ) { test(set, v0, v1, description, v0); } - private static void testReplace(ParetoSet set, Vector v0, Vector v1, String description) { + private static void testReplace( + ParetoSet set, + TestVector v0, + TestVector v1, + String description + ) { test(set, v0, v1, description, v1); } - private static void keepBoth(ParetoSet set, Vector v0, Vector v1, String description) { + private static void keepBoth( + ParetoSet set, + TestVector v0, + TestVector v1, + String description + ) { test(set, v0, v1, description, v0, v1); } private static void test( - ParetoSet set, - Vector v0, - Vector v1, + ParetoSet set, + TestVector v0, + TestVector v1, String description, - Vector... expected + TestVector... expected ) { new TestCase(v0, v1, description, expected).run(set); } - private void test(ParetoSet set, String expected, Vector... vectorsToAdd) { + private void test(ParetoSet set, String expected, TestVector... vectorsToAdd) { set.clear(); - for (Vector v : vectorsToAdd) { + for (TestVector v : vectorsToAdd) { // Copy vector to avoid any identity pitfalls - Vector vector = new Vector(v); + TestVector vector = new TestVector(v); boolean qualify = set.qualify(vector); assertEquals(qualify, set.add(vector), "Qualify and add should return the same value."); } @@ -480,12 +490,12 @@ private void test(ParetoSet set, String expected, Vector... vectorsToAdd static class TestCase { - final Vector v0; - final Vector v1; + final TestVector v0; + final TestVector v1; final String expected; final String description; - TestCase(Vector v0, Vector v1, String description, Vector... expected) { + TestCase(TestVector v0, TestVector v1, String description, TestVector... expected) { this.v0 = v0; this.v1 = v1; this.expected = @@ -495,7 +505,7 @@ static class TestCase { this.description = description; } - void run(ParetoSet set) { + void run(ParetoSet set) { set.clear(); set.add(v0); diff --git a/src/test/java/org/opentripplanner/raptor/util/paretoset/TestParetoSetEventListener.java b/src/test/java/org/opentripplanner/raptor/util/paretoset/TestParetoSetEventListener.java new file mode 100644 index 00000000000..7e4bb539dc5 --- /dev/null +++ b/src/test/java/org/opentripplanner/raptor/util/paretoset/TestParetoSetEventListener.java @@ -0,0 +1,58 @@ +package org.opentripplanner.raptor.util.paretoset; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * An event listener witch keeps the result for each event type. Used in tests in this package + * only! + */ +class TestParetoSetEventListener implements ParetoSetEventListener { + + private final List accepted = new ArrayList<>(); + private final List rejected = new ArrayList<>(); + private final List dropped = new ArrayList<>(); + + @Override + public void notifyElementAccepted(T newElement) { + accepted.add(newElement); + } + + @Override + public void notifyElementDropped(T element, T droppedByElement) { + dropped.add(element); + } + + @Override + public void notifyElementRejected(T element, T rejectedByElement) { + rejected.add(element); + } + + @Override + public String toString() { + return "TestParetoSetEventListener"; + } + + public String acceptedAsString() { + return toString(accepted); + } + + public String rejectedAsString() { + return toString(rejected); + } + + public String droppedAsString() { + return toString(dropped); + } + + void clear() { + accepted.clear(); + rejected.clear(); + dropped.clear(); + } + + private String toString(List list) { + return list.stream().map(Object::toString).collect(Collectors.joining(" ")); + } +} diff --git a/src/test/java/org/opentripplanner/raptor/util/paretoset/Vector.java b/src/test/java/org/opentripplanner/raptor/util/paretoset/TestVector.java similarity index 71% rename from src/test/java/org/opentripplanner/raptor/util/paretoset/Vector.java rename to src/test/java/org/opentripplanner/raptor/util/paretoset/TestVector.java index fa696b9af17..7f9998f5485 100644 --- a/src/test/java/org/opentripplanner/raptor/util/paretoset/Vector.java +++ b/src/test/java/org/opentripplanner/raptor/util/paretoset/TestVector.java @@ -2,29 +2,33 @@ import java.util.Objects; -class Vector { +/** + * Create a value object type to test the ParetoSet "generic" type criteria. We create a new type + * to isolate the tests in this package from any other randomly chosen type. + */ +class TestVector { private static final int NOT_SET = -999; final String name; final int v1, v2, v3, v4; - Vector(Vector o) { + TestVector(TestVector o) { this(o.name, o.v1, o.v2, o.v3, o.v4); } - Vector(String name, int v1) { + TestVector(String name, int v1) { this(name, v1, NOT_SET, NOT_SET, NOT_SET); } - Vector(String name, int v1, int v2) { + TestVector(String name, int v1, int v2) { this(name, v1, v2, NOT_SET, NOT_SET); } - Vector(String name, int v1, int v2, int v3) { + TestVector(String name, int v1, int v2, int v3) { this(name, v1, v2, v3, NOT_SET); } - Vector(String name, int v1, int v2, int v3, int v4) { + TestVector(String name, int v1, int v2, int v3, int v4) { this.name = name; this.v1 = v1; this.v2 = v2; @@ -45,7 +49,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - Vector v = (Vector) o; + TestVector v = (TestVector) o; return v1 == v.v1 && v2 == v.v2 && v3 == v.v3 && v4 == v.v4 && name.equals(v.name); } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/LookupStopIndexCallbackTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/LookupStopIndexCallbackTest.java new file mode 100644 index 00000000000..fb39db51cd5 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/LookupStopIndexCallbackTest.java @@ -0,0 +1,53 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model.framework.EntityNotFoundException; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +class LookupStopIndexCallbackTest { + + private static final FeedScopedId ID_1 = FeedScopedId.ofNullable("F", "1"); + private static final FeedScopedId ID_2 = FeedScopedId.ofNullable("F", "2"); + private static final FeedScopedId ID_3 = FeedScopedId.ofNullable("F", "3"); + + private final LookupStopIndexCallback subject = new TestLookupStopIndexCallback( + Map.of(ID_1, new int[] { 1, 7, 13 }, ID_2, new int[] { 2, 7, 15 }) + ); + + /** + * This mostly verifies that the test is set up correctly, the code tested is the dummy inside + * the test. + */ + void lookupStopLocationIndexesSingleIdInput() { + assertArrayEquals(new int[] { 1, 7, 13 }, subject.lookupStopLocationIndexes(ID_1).toArray()); + var ex = Assertions.assertThrows( + EntityNotFoundException.class, + () -> subject.lookupStopLocationIndexes(ID_3).toArray() + ); + assertEquals("StopLocation does not exist for id F:3", ex.getMessage()); + } + + @Test + void lookupStopLocationIndexesCollectionInput() { + assertArrayEquals(new int[] {}, subject.lookupStopLocationIndexes(List.of())); + assertArrayEquals(new int[] { 1, 7, 13 }, subject.lookupStopLocationIndexes(List.of(ID_1))); + assertArrayEquals( + new int[] { 1, 2, 7, 13, 15 }, + subject.lookupStopLocationIndexes(List.of(ID_1, ID_2)) + ); + + // Should throw exception? + var ex = assertThrows( + EntityNotFoundException.class, + () -> subject.lookupStopLocationIndexes(List.of(ID_1, ID_3)) + ); + assertEquals("StopLocation entity not found: F:3", ex.getMessage()); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java index 47542782884..87e90e61b2e 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/RaptorRequestMapperTest.java @@ -6,6 +6,8 @@ import java.time.Duration; import java.time.ZonedDateTime; import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -14,10 +16,12 @@ import org.opentripplanner.raptor._data.transit.TestTripSchedule; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.raptor.api.request.RaptorRequest; -import org.opentripplanner.routing.api.request.PassThroughPoint; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.framework.CostLinearFunction; +import org.opentripplanner.routing.api.request.via.PassThroughViaLocation; +import org.opentripplanner.routing.api.request.via.VisitViaLocation; import org.opentripplanner.transit.model._data.TransitModelForTest; +import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.site.StopLocation; class RaptorRequestMapperTest { @@ -26,12 +30,13 @@ class RaptorRequestMapperTest { private static final StopLocation STOP_A = TEST_MODEL.stop("Stop:A").build(); private static final List ACCESS = List.of(TestAccessEgress.walk(12, 45)); private static final List EGRESS = List.of(TestAccessEgress.walk(144, 54)); - private static final Duration D0s = Duration.ofSeconds(0); private static final CostLinearFunction R1 = CostLinearFunction.of("50 + 1.0x"); private static final CostLinearFunction R2 = CostLinearFunction.of("0 + 1.5x"); private static final CostLinearFunction R3 = CostLinearFunction.of("30 + 2.0x"); + private static final Map STOPS_MAP = Map.of(STOP_A.getId(), STOP_A); + static List testCasesRelaxedCost() { return List.of( Arguments.of(CostLinearFunction.NORMAL, 0, 0), @@ -52,11 +57,29 @@ void mapRelaxCost(CostLinearFunction input, int cost, int expected) { assertEquals(expected, calcCost.relax(cost)); } + @Test + void testViaLocation() { + var req = new RouteRequest(); + var minWaitTime = Duration.ofMinutes(13); + + req.setViaLocations( + List.of(new VisitViaLocation("Via A", minWaitTime, List.of(STOP_A.getId()), List.of())) + ); + + var result = map(req); + + assertTrue(result.searchParams().hasViaLocations()); + assertEquals( + "[Via{label: Via A, minWaitTime: 13m, connections: [0 13m]}]", + result.searchParams().viaLocations().toString() + ); + } + @Test void testPassThroughPoints() { var req = new RouteRequest(); - req.setPassThroughPoints(List.of(new PassThroughPoint(List.of(STOP_A), "Via A"))); + req.setViaLocations(List.of(new PassThroughViaLocation("Via A", List.of(STOP_A.getId())))); var result = map(req); @@ -72,7 +95,7 @@ void testPassThroughPointsTurnTransitGroupPriorityOff() { var req = new RouteRequest(); // Set pass-through and relax transit-group-priority - req.setPassThroughPoints(List.of(new PassThroughPoint(List.of(STOP_A), "Via A"))); + req.setViaLocations(List.of(new PassThroughViaLocation("Via A", List.of(STOP_A.getId())))); req.withPreferences(p -> p.withTransit(t -> t.withRelaxTransitGroupPriority(CostLinearFunction.of("30m + 1.2t"))) ); @@ -90,7 +113,8 @@ private static RaptorRequest map(RouteRequest request) { false, ACCESS, EGRESS, - null + null, + id -> IntStream.of(STOPS_MAP.get(id).getIndex()) ); } } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TestLookupStopIndexCallback.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TestLookupStopIndexCallback.java new file mode 100644 index 00000000000..f1898d69f52 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/TestLookupStopIndexCallback.java @@ -0,0 +1,25 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers; + +import java.util.Map; +import java.util.stream.IntStream; +import org.opentripplanner.transit.model.framework.EntityNotFoundException; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.site.StopLocation; + +public class TestLookupStopIndexCallback implements LookupStopIndexCallback { + + private final Map index; + + public TestLookupStopIndexCallback(Map index) { + this.index = index; + } + + @Override + public IntStream lookupStopLocationIndexes(FeedScopedId stopLocationId) { + int[] values = index.get(stopLocationId); + if (values == null) { + throw new EntityNotFoundException(StopLocation.class, stopLocationId); + } + return IntStream.of(values); + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/via/ViaRoutingWorkerTest.java b/src/test/java/org/opentripplanner/routing/algorithm/via/ViaRoutingWorkerTest.java index 94c8c7b2f71..4425aa10ccd 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/via/ViaRoutingWorkerTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/via/ViaRoutingWorkerTest.java @@ -17,7 +17,7 @@ import org.opentripplanner.model.plan.TripPlan; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.RouteViaRequest; -import org.opentripplanner.routing.api.request.ViaLocation; +import org.opentripplanner.routing.api.request.ViaLocationDeprecated; import org.opentripplanner.routing.api.request.request.JourneyRequest; import org.opentripplanner.routing.api.response.RoutingResponse; import org.opentripplanner.routing.api.response.ViaRoutingResponseConnection; @@ -136,7 +136,7 @@ public RouteViaRequest createRouteViaRequest() { int minSlack = 10; int maxSlack = 45; var viaLocations = List.of( - new ViaLocation( + new ViaLocationDeprecated( location(viaC), false, Duration.ofMinutes(minSlack), diff --git a/src/test/java/org/opentripplanner/routing/api/request/via/PassThroughViaLocationTest.java b/src/test/java/org/opentripplanner/routing/api/request/via/PassThroughViaLocationTest.java new file mode 100644 index 00000000000..9ed431912ac --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/api/request/via/PassThroughViaLocationTest.java @@ -0,0 +1,64 @@ +package org.opentripplanner.routing.api.request.via; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Duration; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner._support.asserts.AssertEqualsAndHashCode; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +class PassThroughViaLocationTest { + + private static final FeedScopedId ID = FeedScopedId.ofNullable("F", "1"); + + private static final String LABEL = "AName"; + + @SuppressWarnings("DataFlowIssue") + private static final ViaLocation subject = new PassThroughViaLocation(LABEL, List.of(ID)); + + @Test + void allowAsPassThroughPoint() { + assertTrue(subject.isPassThroughLocation()); + } + + @Test + void minimumWaitTime() { + assertEquals(Duration.ZERO, subject.minimumWaitTime()); + } + + @Test + void label() { + assertEquals(LABEL, subject.label()); + } + + @Test + void stopLocationIds() { + assertEquals("[F:1]", subject.stopLocationIds().toString()); + } + + @Test + void coordinates() { + assertEquals("[]", subject.coordinates().toString()); + } + + @Test + void testToString() { + assertEquals( + "PassThroughViaLocation{label: AName, stopLocationIds: [F:1]}", + subject.toString() + ); + } + + @Test + void testEqAndHashCode() { + AssertEqualsAndHashCode + .verify(subject) + .sameAs(new PassThroughViaLocation(subject.label(), subject.stopLocationIds())) + .differentFrom( + new PassThroughViaLocation("Other", subject.stopLocationIds()), + new PassThroughViaLocation(subject.label(), List.of(new FeedScopedId("F", "2"))) + ); + } +} diff --git a/src/test/java/org/opentripplanner/routing/api/request/via/VisitViaLocationTest.java b/src/test/java/org/opentripplanner/routing/api/request/via/VisitViaLocationTest.java new file mode 100644 index 00000000000..e36ed42551e --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/api/request/via/VisitViaLocationTest.java @@ -0,0 +1,77 @@ +package org.opentripplanner.routing.api.request.via; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.time.Duration; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner._support.asserts.AssertEqualsAndHashCode; +import org.opentripplanner.framework.geometry.WgsCoordinate; +import org.opentripplanner.transit.model.framework.FeedScopedId; + +class VisitViaLocationTest { + + private static final FeedScopedId ID = FeedScopedId.ofNullable("F", "1"); + private static final String LABEL = "AName"; + private static final Duration MINIMUM_WAIT_TIME = Duration.ofMinutes(5); + + @SuppressWarnings("DataFlowIssue") + private static final ViaLocation subject = new VisitViaLocation( + LABEL, + MINIMUM_WAIT_TIME, + List.of(ID), + List.of(WgsCoordinate.GREENWICH) + ); + + @Test + void allowAsPassThroughPoint() { + assertFalse(subject.isPassThroughLocation()); + } + + @Test + void minimumWaitTime() { + assertEquals(MINIMUM_WAIT_TIME, subject.minimumWaitTime()); + } + + @Test + void label() { + assertEquals(LABEL, subject.label()); + } + + @Test + void stopLocationIds() { + assertEquals("[F:1]", subject.stopLocationIds().toString()); + } + + @Test + void coordinates() { + assertEquals("[" + WgsCoordinate.GREENWICH + "]", subject.coordinates().toString()); + } + + @Test + void testToString() { + assertEquals( + "VisitViaLocation{label: AName, minimumWaitTime: 5m, stopLocationIds: [F:1], coordinates: [(51.48, 0.0)]}", + subject.toString() + ); + } + + @Test + void testEqAndHashCode() { + var l = subject.label(); + var mwt = subject.minimumWaitTime(); + var ids = subject.stopLocationIds(); + var cs = subject.coordinates(); + + AssertEqualsAndHashCode + .verify(subject) + .sameAs(new VisitViaLocation(l, mwt, ids, cs)) + .differentFrom( + new VisitViaLocation("other", mwt, ids, cs), + new VisitViaLocation(l, Duration.ZERO, ids, cs), + new VisitViaLocation(l, mwt, List.of(), cs), + new VisitViaLocation(l, mwt, ids, List.of()) + ); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/framework/EntityNotFoundExceptionTest.java b/src/test/java/org/opentripplanner/transit/model/framework/EntityNotFoundExceptionTest.java new file mode 100644 index 00000000000..f33eb389a37 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/framework/EntityNotFoundExceptionTest.java @@ -0,0 +1,22 @@ +package org.opentripplanner.transit.model.framework; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class EntityNotFoundExceptionTest { + + private static final FeedScopedId ID = FeedScopedId.ofNullable("F", "1"); + + @Test + void getMessage() { + assertEquals( + "Integer entity not found: F:1", + new EntityNotFoundException(Integer.class, ID).getMessage() + ); + assertEquals( + "Stop or Station entity not found: F:1", + new EntityNotFoundException("Stop or Station", ID).getMessage() + ); + } +}