diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTestData.java b/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTestData.java index d4a8a3894c1..e0a85e23794 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTestData.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTestData.java @@ -19,6 +19,7 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.test.support.ResourceLoader; import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; import org.opentripplanner.transit.service.StopModel; import org.opentripplanner.transit.service.TransitModel; @@ -39,6 +40,7 @@ public final class FlexIntegrationTestData { public static final FlexServiceDate FLEX_DATE = new FlexServiceDate( SERVICE_DATE, SECONDS_SINCE_MIDNIGHT, + RoutingBookingInfo.NOT_SET, new TIntHashSet() ); diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/template/FlexTemplateFactoryTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/template/FlexTemplateFactoryTest.java index 8fe26b2718a..8f9067af774 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/template/FlexTemplateFactoryTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/template/FlexTemplateFactoryTest.java @@ -34,6 +34,7 @@ import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; class FlexTemplateFactoryTest { @@ -61,6 +62,7 @@ class FlexTemplateFactoryTest { private static final FlexServiceDate DATE = new FlexServiceDate( LocalDate.of(2024, Month.MAY, 17), SERVICE_TIME_OFFSET, + RoutingBookingInfo.NOT_SET, new TIntHashSet() ); @@ -101,7 +103,7 @@ void testCreateAccessTemplateForUnscheduledTripWithTwoStopsAndNoBoardRestriction assertEquals(1, template.toStopIndex); assertSame(CALCULATOR, template.calculator); assertSame(STOP_B, template.transferStop); - assertSame(DATE.serviceDate, template.serviceDate); + assertSame(DATE.serviceDate(), template.serviceDate); assertEquals(SERVICE_TIME_OFFSET, template.secondsFromStartOfTime); assertEquals(1, subject.size(), subject::toString); @@ -133,7 +135,7 @@ void testCreateEgressTemplateForUnscheduledTripWithTwoStopsAndNoBoardRestriction assertEquals(1, template.toStopIndex); assertSame(CALCULATOR, template.calculator); assertSame(STOP_A, template.transferStop); - assertSame(DATE.serviceDate, template.serviceDate); + assertSame(DATE.serviceDate(), template.serviceDate); assertEquals(SERVICE_TIME_OFFSET, template.secondsFromStartOfTime); assertEquals(1, subject.size(), subject::toString); diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java index ffaa19e15f4..2883394f837 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java @@ -104,6 +104,7 @@ void calculateDirectFare() { new DefaultTransitService(transitModel), FlexParameters.defaultValues(), OffsetDateTime.parse("2021-11-12T10:15:24-05:00").toInstant(), + null, 1, 1, List.of(from), diff --git a/src/ext/java/org/opentripplanner/ext/flex/FlexAccessEgress.java b/src/ext/java/org/opentripplanner/ext/flex/FlexAccessEgress.java index f7ccd441185..b3eaac5f017 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/FlexAccessEgress.java +++ b/src/ext/java/org/opentripplanner/ext/flex/FlexAccessEgress.java @@ -3,9 +3,8 @@ import static org.opentripplanner.model.StopTime.MISSING_VALUE; import java.util.Objects; -import java.util.Optional; -import javax.annotation.Nullable; import org.opentripplanner.ext.flex.trip.FlexTrip; +import org.opentripplanner.framework.time.TimeUtils; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.street.search.state.State; import org.opentripplanner.transit.model.site.RegularStop; @@ -20,8 +19,7 @@ public final class FlexAccessEgress { private final FlexTrip trip; private final State lastState; private final boolean stopReachedOnBoard; - - @Nullable + private final int requestedBookingTime; private final RoutingBookingInfo routingBookingInfo; public FlexAccessEgress( @@ -31,7 +29,8 @@ public FlexAccessEgress( int toStopIndex, FlexTrip trip, State lastState, - boolean stopReachedOnBoard + boolean stopReachedOnBoard, + int requestedBookingTime ) { this.stop = stop; this.pathDurations = pathDurations; @@ -40,7 +39,8 @@ public FlexAccessEgress( this.trip = Objects.requireNonNull(trip); this.lastState = lastState; this.stopReachedOnBoard = stopReachedOnBoard; - this.routingBookingInfo = createRoutingBookingInfo().orElse(null); + this.routingBookingInfo = createRoutingBookingInfo(); + this.requestedBookingTime = requestedBookingTime; } public RegularStop stop() { @@ -56,9 +56,19 @@ public boolean stopReachedOnBoard() { } public int earliestDepartureTime(int departureTime) { - int requestedDepartureTime = pathDurations.mapToFlexTripDepartureTime(departureTime); + int tripDepartureTime = pathDurations.mapToFlexTripDepartureTime(departureTime); + + int tmp = tripDepartureTime; + tripDepartureTime = + routingBookingInfo.earliestDepartureTime(requestedBookingTime, tripDepartureTime); + + if (tmp != tripDepartureTime) { + System.out.println("departure time ....... : " + TimeUtils.timeToStrLong(tmp)); + System.out.println("min notice dep.time .. : " + TimeUtils.timeToStrLong(tripDepartureTime)); + } + int earliestDepartureTime = trip.earliestDepartureTime( - requestedDepartureTime, + tripDepartureTime, fromStopIndex, toStopIndex, pathDurations.trip() @@ -66,13 +76,23 @@ public int earliestDepartureTime(int departureTime) { if (earliestDepartureTime == MISSING_VALUE) { return MISSING_VALUE; } + /* + if ( + !routingBookingInfo.isThereEnoughTimeToBookForDeparture( + earliestDepartureTime, + requestedBookingTime + ) + ) { + return MISSING_VALUE; + } + */ return pathDurations.mapToRouterDepartureTime(earliestDepartureTime); } public int latestArrivalTime(int arrivalTime) { - int requestedArrivalTime = pathDurations.mapToFlexTripArrivalTime(arrivalTime); + int tripArrivalTime = pathDurations.mapToFlexTripArrivalTime(arrivalTime); int latestArrivalTime = trip.latestArrivalTime( - requestedArrivalTime, + tripArrivalTime, fromStopIndex, toStopIndex, pathDurations.trip() @@ -80,17 +100,17 @@ public int latestArrivalTime(int arrivalTime) { if (latestArrivalTime == MISSING_VALUE) { return MISSING_VALUE; } + if ( + routingBookingInfo.exceedsMinimumBookingNotice( + latestArrivalTime - pathDurations.trip(), + requestedBookingTime + ) + ) { + return MISSING_VALUE; + } return pathDurations.mapToRouterArrivalTime(latestArrivalTime); } - /** - * Return routing booking info for the boarding stop. Empty, if there are not any - * booking restrictions, witch applies to routing. - */ - public Optional routingBookingInfo() { - return Optional.ofNullable(routingBookingInfo); - } - @Override public String toString() { return ToStringBuilder @@ -105,14 +125,10 @@ public String toString() { .toString(); } - private Optional createRoutingBookingInfo() { - var bookingInfo = trip.getPickupBookingInfo(fromStopIndex); - if (bookingInfo == null) { - return Optional.empty(); - } + private RoutingBookingInfo createRoutingBookingInfo() { return RoutingBookingInfo .of() - .withBookingInfo(bookingInfo) + .withBookingInfo(trip.getPickupBookingInfo(fromStopIndex)) .withLegDurationInSeconds(pathDurations.total()) .withTimeOffsetInSeconds(pathDurations.access()) .build(); diff --git a/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java b/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java index 9170eabb8cf..28fefc277ad 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java +++ b/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java @@ -28,6 +28,7 @@ import org.opentripplanner.street.model.vertex.TransitStopVertex; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; import org.opentripplanner.transit.service.TransitService; public class FlexRouter { @@ -47,7 +48,8 @@ public class FlexRouter { /* Request data */ private final ZonedDateTime startOfTime; - private final int departureTime; + private final int requestedTime; + private final int requestedBookingTime; private final List dates; public FlexRouter( @@ -55,6 +57,7 @@ public FlexRouter( TransitService transitService, FlexParameters flexParameters, Instant requestedTime, + Instant requestedBookingTime, int additionalPastSearchDays, int additionalFutureSearchDays, Collection streetAccesses, @@ -89,7 +92,11 @@ public FlexRouter( ZoneId tz = transitService.getTimeZone(); LocalDate searchDate = LocalDate.ofInstant(requestedTime, tz); this.startOfTime = ServiceDateUtils.asStartOfService(searchDate, tz); - this.departureTime = ServiceDateUtils.secondsSinceStartOfTime(startOfTime, requestedTime); + this.requestedTime = ServiceDateUtils.secondsSinceStartOfTime(startOfTime, requestedTime); + this.requestedBookingTime = + requestedBookingTime == null + ? RoutingBookingInfo.NOT_SET + : ServiceDateUtils.secondsSinceStartOfTime(startOfTime, requestedBookingTime); this.dates = createFlexServiceDates( transitService, @@ -108,7 +115,7 @@ public List createFlexOnlyItineraries(boolean arriveBy) { egressFlexPathCalculator, flexParameters.maxTransferDuration() ) - .calculateDirectFlexPaths(streetAccesses, streetEgresses, dates, departureTime, arriveBy); + .calculateDirectFlexPaths(streetAccesses, streetEgresses, dates, requestedTime, arriveBy); var itineraries = new ArrayList(); @@ -161,6 +168,7 @@ private List createFlexServiceDates( new FlexServiceDate( date, ServiceDateUtils.secondsSinceStartOfTime(startOfTime, date), + requestedBookingTime, transitService.getServiceCodesRunningForDate(date) ) ); @@ -198,7 +206,8 @@ public Collection getTransfersToStop(StopLocation stop) { @Override public boolean isDateActive(FlexServiceDate date, FlexTrip trip) { - return date.isFlexTripRunning(trip, transitService); + int serviceCode = transitService.getServiceCodeForId(trip.getTrip().getServiceId()); + return date.isTripServiceRunning(serviceCode); } } } diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java b/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java index 3e83449df16..aba74987439 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java +++ b/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java @@ -47,6 +47,7 @@ abstract class AbstractFlexTemplate { protected final StopLocation transferStop; protected final int secondsFromStartOfTime; public final LocalDate serviceDate; + protected final int requestedBookingTime; protected final FlexPathCalculator calculator; private final Duration maxTransferDuration; @@ -77,8 +78,9 @@ abstract class AbstractFlexTemplate { this.fromStopIndex = boardStopPosition; this.toStopIndex = alightStopPosition; this.transferStop = transferStop; - this.secondsFromStartOfTime = date.secondsFromStartOfTime; - this.serviceDate = date.serviceDate; + this.secondsFromStartOfTime = date.secondsFromStartOfTime(); + this.serviceDate = date.serviceDate(); + this.requestedBookingTime = date.requestedBookingTime(); this.calculator = calculator; this.maxTransferDuration = maxTransferDuration; } @@ -210,7 +212,8 @@ private FlexAccessEgress createFlexAccessEgress( toStopIndex, trip, finalState, - transferEdges.isEmpty() + transferEdges.isEmpty(), + requestedBookingTime ); }) .orElse(null); diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/ClosestTrip.java b/src/ext/java/org/opentripplanner/ext/flex/template/ClosestTrip.java index b0094c976ba..c19685c6297 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/template/ClosestTrip.java +++ b/src/ext/java/org/opentripplanner/ext/flex/template/ClosestTrip.java @@ -9,6 +9,7 @@ import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.framework.lang.IntUtils; import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; /** * The combination of the closest stop, trip and trip active date. @@ -54,7 +55,7 @@ static Collection of( boolean pickup ) { var closestTrips = findAllTripsReachableFromNearbyStop(callbackService, nearbyStops, pickup); - return findActiveDatesForTripAndDecorateResult(callbackService, dates, closestTrips); + return findActiveDatesForTripAndDecorateResult(callbackService, dates, closestTrips, true); } @Override @@ -90,7 +91,8 @@ public FlexServiceDate activeDate() { private static ArrayList findActiveDatesForTripAndDecorateResult( FlexAccessEgressCallbackAdapter callbackService, List dates, - Map, ClosestTrip> map + Map, ClosestTrip> map, + boolean pickup ) { var result = new ArrayList(); // Add active dates @@ -99,6 +101,11 @@ private static ArrayList findActiveDatesForTripAndDecorateResult( var closestTrip = e.getValue(); // Include dates where the service is running for (FlexServiceDate date : dates) { + // Filter away boardings early. This needs to be done for egress as well when the + // board stop is known (not known here). + if (pickup && exceedsLatestBookingTime(trip, date, closestTrip.stopPos())) { + continue; + } if (callbackService.isDateActive(date, trip)) { result.add(closestTrip.withDate(date)); } @@ -111,4 +118,17 @@ private ClosestTrip withDate(FlexServiceDate date) { Objects.requireNonNull(date); return new ClosestTrip(this, date); } + + /** + * Check if the trip can be booked at the given date and boarding stop position. + */ + private static boolean exceedsLatestBookingTime( + FlexTrip trip, + FlexServiceDate date, + int stopPos + ) { + return RoutingBookingInfo + .of(trip.getPickupBookingInfo(stopPos)) + .exceedsLatestBookingTime(date.requestedBookingTime()); + } } diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessTemplate.java b/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessTemplate.java index 4d7577bb4b1..585f063c784 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessTemplate.java +++ b/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessTemplate.java @@ -10,6 +10,7 @@ import org.opentripplanner.ext.flex.edgetype.FlexTripEdge; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; import org.opentripplanner.ext.flex.trip.FlexTrip; +import org.opentripplanner.framework.time.TimeUtils; import org.opentripplanner.model.PathTransfer; import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.street.model.edge.Edge; @@ -18,6 +19,7 @@ import org.opentripplanner.street.search.state.State; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; class FlexAccessTemplate extends AbstractFlexTemplate { @@ -46,7 +48,7 @@ class FlexAccessTemplate extends AbstractFlexTemplate { Optional createDirectGraphPath( NearbyStop egress, boolean arriveBy, - int departureTime + int requestedDepartureTime ) { List egressEdges = egress.edges; @@ -76,7 +78,7 @@ Optional createDirectGraphPath( int timeShift; if (arriveBy) { - int lastStopArrivalTime = flexDurations.mapToFlexTripArrivalTime(departureTime); + int lastStopArrivalTime = flexDurations.mapToFlexTripArrivalTime(requestedDepartureTime); int latestArrivalTime = trip.latestArrivalTime( lastStopArrivalTime, fromStopIndex, @@ -88,10 +90,32 @@ Optional createDirectGraphPath( return Optional.empty(); } + // No need to time-shift latestArrivalTime for meeting the min-booking notice restriction, + // the time is already as-late-as-possible + var bookingInfo = RoutingBookingInfo.of(trip.getPickupBookingInfo(fromStopIndex)); + if (!bookingInfo.isThereEnoughTimeToBookForArrival(latestArrivalTime, requestedBookingTime)) { + return Optional.empty(); + } + // Shift from departing at departureTime to arriving at departureTime timeShift = flexDurations.mapToRouterArrivalTime(latestArrivalTime) - flexDurations.total(); } else { - int firstStopDepartureTime = flexDurations.mapToFlexTripDepartureTime(departureTime); + int firstStopDepartureTime = flexDurations.mapToFlexTripDepartureTime(requestedDepartureTime); + + // Time-shift departure so the minimum-booking-notice restriction is honored. This is not + // necessary in for access/egress since in practice Raptor will do this for us. + // TODO get routing booking info + var bookingInfo = trip.getPickupBookingInfo(fromStopIndex); + if (bookingInfo != null) { + var minNotice = bookingInfo.getMinimumBookingNotice(); + if (minNotice != null && requestedBookingTime != RoutingBookingInfo.NOT_SET) { + int firstBookableDepartureTime = requestedBookingTime + (int) minNotice.toSeconds(); + if (firstBookableDepartureTime > firstStopDepartureTime) { + firstStopDepartureTime = firstBookableDepartureTime; + } + } + } + int earliestDepartureTime = trip.earliestDepartureTime( firstStopDepartureTime, fromStopIndex, @@ -102,7 +126,29 @@ Optional createDirectGraphPath( if (earliestDepartureTime == MISSING_VALUE) { return Optional.empty(); } + + var routingBookingInfo = RoutingBookingInfo.of(bookingInfo); + if ( + !routingBookingInfo.isThereEnoughTimeToBookForDeparture( + earliestDepartureTime, + requestedBookingTime + ) + ) { + return Optional.empty(); + } + timeShift = flexDurations.mapToRouterDepartureTime(earliestDepartureTime); + + System.out.println( + "requestedDepartureTime .. : " + TimeUtils.timeToStrLong(requestedDepartureTime) + ); + System.out.println( + "requestedBookingTime .... : " + TimeUtils.timeToStrLong(requestedBookingTime) + ); + System.out.println( + "EDT ..................... : " + TimeUtils.timeToStrLong(earliestDepartureTime) + ); + System.out.println("BookingInfo ............. : " + bookingInfo); } return Optional.of(new DirectFlexPath(timeShift, finalState)); diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/FlexServiceDate.java b/src/ext/java/org/opentripplanner/ext/flex/template/FlexServiceDate.java index 5019defefcd..ee85cf77b03 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/template/FlexServiceDate.java +++ b/src/ext/java/org/opentripplanner/ext/flex/template/FlexServiceDate.java @@ -2,8 +2,6 @@ import gnu.trove.set.TIntSet; import java.time.LocalDate; -import org.opentripplanner.ext.flex.trip.FlexTrip; -import org.opentripplanner.transit.service.TransitService; /** * This class contains information used in a flex router, and depends on the date the search was @@ -12,33 +10,47 @@ public class FlexServiceDate { /** The local date */ - public final LocalDate serviceDate; + private final LocalDate serviceDate; /** * How many seconds does this date's "midnight" (12 hours before noon) differ from the "midnight" * of the date for the search. */ - public final int secondsFromStartOfTime; + private final int secondsFromStartOfTime; /** Which services are running on the date. */ - public final TIntSet servicesRunning; + private final TIntSet servicesRunning; + + private final int requestedBookingTime; public FlexServiceDate( LocalDate serviceDate, int secondsFromStartOfTime, + int requestedBookingTime, TIntSet servicesRunning ) { this.serviceDate = serviceDate; this.secondsFromStartOfTime = secondsFromStartOfTime; + this.requestedBookingTime = requestedBookingTime; this.servicesRunning = servicesRunning; } - public boolean isFlexTripRunning(FlexTrip flexTrip, TransitService transitService) { - return ( - servicesRunning != null && - servicesRunning.contains( - transitService.getServiceCodeForId(flexTrip.getTrip().getServiceId()) - ) - ); + LocalDate serviceDate() { + return serviceDate; + } + + int secondsFromStartOfTime() { + return secondsFromStartOfTime; + } + + int requestedBookingTime() { + return requestedBookingTime; + } + + /** + * Return true if the given {@code serviceCode} is active and running. + */ + public boolean isTripServiceRunning(int serviceCode) { + return servicesRunning != null && servicesRunning.contains(serviceCode); } } diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/FlexTemplateFactory.java b/src/ext/java/org/opentripplanner/ext/flex/template/FlexTemplateFactory.java index cc807d86f70..fb96f5fd8fd 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/template/FlexTemplateFactory.java +++ b/src/ext/java/org/opentripplanner/ext/flex/template/FlexTemplateFactory.java @@ -9,6 +9,7 @@ import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; /** * The factory is used to create flex trip templates. @@ -74,16 +75,28 @@ private List createEgressTemplates() { var result = new ArrayList(); int end = isBoardingAndAlightingAtSameStopPositionAllowed() ? alightStopPos : alightStopPos - 1; - for (int boardIndex = 0; boardIndex <= end; boardIndex++) { - if (trip.getBoardRule(boardIndex).isRoutable()) { - for (var stop : expandStopsAt(trip, boardIndex)) { - result.add(createEgressTemplate(trip, stop, boardIndex, alightStopPos)); + for (int boardStopPos = 0; boardStopPos <= end; boardStopPos++) { + if (isAllowedToBoardAt(boardStopPos)) { + for (var stop : expandStopsAt(trip, boardStopPos)) { + result.add(createEgressTemplate(trip, stop, boardStopPos, alightStopPos)); } } } return result; } + /** + * Check if stop position is routable and that the latest-booking time criteria is met. + */ + private boolean isAllowedToBoardAt(int boardStopPosition) { + return ( + trip.getBoardRule(boardStopPosition).isRoutable() && + !RoutingBookingInfo + .of(trip.getPickupBookingInfo(boardStopPosition)) + .exceedsLatestBookingTime(date.requestedBookingTime()) + ); + } + /** * With respect to one journey/itinerary this method retuns {@code true} if a passenger can * board and alight at the same stop in the journey pattern. This is not allowed for regular diff --git a/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java b/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java index 09da0c87112..11df0381f4e 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java +++ b/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java @@ -10,7 +10,6 @@ import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.Nonnull; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; import org.opentripplanner.ext.flex.flexpathcalculator.TimePenaltyCalculator; 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 416e0159944..6a1404c3039 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 @@ -1,9 +1,7 @@ package org.opentripplanner.routing.algorithm.raptoradapter.router; -import static org.opentripplanner.framework.time.TimeUtils.toTransitTimeSeconds; import static org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressType.ACCESS; import static org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressType.EGRESS; -import static org.opentripplanner.routing.algorithm.raptoradapter.transit.BookingRestrictionAccessEgress.decorateAccessEgressBookingRestriction; import java.time.Duration; import java.time.Instant; @@ -277,16 +275,6 @@ private Collection fetchAccessEgresses(AccessEgre results.addAll(AccessEgressMapper.mapFlexAccessEgresses(flexAccessList, type.isEgress())); } - if (request.bookingTime() != null) { - int requestedBookingTime = toTransitTimeSeconds(transitSearchTimeZero, request.bookingTime()); - return results - .stream() - .map(accessEgress -> - decorateAccessEgressBookingRestriction(accessEgress, requestedBookingTime) - ) - .toList(); - } - return results; } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java index 35c627828c6..ff60138d77d 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/DirectFlexRouter.java @@ -60,6 +60,7 @@ public static List route( serverContext.transitService(), serverContext.flexParameters(), request.dateTime(), + request.bookingTime(), additionalSearchDays.additionalSearchDaysInPast(), additionalSearchDays.additionalSearchDaysInFuture(), accessStops, diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java index 347bbc33d79..5023e595678 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/FlexAccessEgressRouter.java @@ -64,6 +64,7 @@ public static Collection routeAccessEgress( transitService, config, request.dateTime(), + request.bookingTime(), searchDays.additionalSearchDaysInPast(), searchDays.additionalSearchDaysInFuture(), accessStops, diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/BookingRestrictionAccessEgress.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/BookingRestrictionAccessEgress.java deleted file mode 100644 index 71c864b4ecb..00000000000 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/BookingRestrictionAccessEgress.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.opentripplanner.routing.algorithm.raptoradapter.transit; - -import static org.opentripplanner.raptor.api.model.RaptorConstants.TIME_NOT_SET; - -import javax.annotation.Nullable; -import org.opentripplanner.framework.model.TimeAndCost; -import org.opentripplanner.street.search.state.State; -import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; - -public class BookingRestrictionAccessEgress implements RoutingAccessEgress { - - private final RoutingAccessEgress delegate; - - /** - * The requested time the passenger will book the trip. Normally, this is when the search is - * performed plus a small grace period to allow the user to complete the booking. - */ - private final int requestedBookingTime; - - private final RoutingBookingInfo bookingInfo; - - public BookingRestrictionAccessEgress( - RoutingAccessEgress delegate, - RoutingBookingInfo bookingInfo, - int requestedBookingTime - ) { - this.delegate = delegate; - this.requestedBookingTime = requestedBookingTime; - this.bookingInfo = bookingInfo; - } - - public static RoutingAccessEgress decorateAccessEgressBookingRestriction( - RoutingAccessEgress accessEgress, - int requestedBookingTime - ) { - var bookingInfo = accessEgress.routingBookingInfo(); - return bookingInfo.isPresent() - ? new BookingRestrictionAccessEgress(accessEgress, bookingInfo.get(), requestedBookingTime) - : accessEgress; - } - - @Override - public int stop() { - return delegate.stop(); - } - - @Override - public int c1() { - return delegate.c1(); - } - - @Override - public int durationInSeconds() { - return delegate.durationInSeconds(); - } - - @Override - public int earliestDepartureTime(int requestedDepartureTime) { - int edt = delegate.earliestDepartureTime(requestedDepartureTime); - if (edt == TIME_NOT_SET) { - return TIME_NOT_SET; - } - if (bookingInfo.isThereEnoughTimeToBookForDeparture(edt, requestedBookingTime)) { - return edt; - } - return TIME_NOT_SET; - } - - @Override - public int latestArrivalTime(int requestedArrivalTime) { - var lat = delegate.latestArrivalTime(requestedArrivalTime); - if (lat == TIME_NOT_SET) { - return TIME_NOT_SET; - } - if (bookingInfo.isThereEnoughTimeToBookForArrival(lat, requestedBookingTime)) { - return lat; - } - return TIME_NOT_SET; - } - - @Override - public boolean hasOpeningHours() { - return delegate.hasOpeningHours(); - } - - @Override - @Nullable - public String openingHoursToString() { - return delegate.openingHoursToString(); - } - - @Override - public int numberOfRides() { - return delegate.numberOfRides(); - } - - @Override - public boolean hasRides() { - return delegate.hasRides(); - } - - @Override - public boolean stopReachedOnBoard() { - return delegate.stopReachedOnBoard(); - } - - @Override - public boolean stopReachedByWalking() { - return delegate.stopReachedByWalking(); - } - - @Override - public boolean isFree() { - return delegate.isFree(); - } - - @Override - public String defaultToString() { - return delegate.defaultToString(); - } - - @Override - public String asString(boolean includeStop, boolean includeCost, @Nullable String summary) { - return delegate.asString(includeStop, includeCost, summary); - } - - @Override - public RoutingAccessEgress withPenalty(TimeAndCost penalty) { - return new BookingRestrictionAccessEgress( - delegate.withPenalty(penalty), - bookingInfo, - requestedBookingTime - ); - } - - @Override - public State getLastState() { - return delegate.getLastState(); - } - - @Override - public boolean isWalkOnly() { - return delegate.isWalkOnly(); - } - - @Override - public boolean hasPenalty() { - return delegate.hasPenalty(); - } - - @Override - public TimeAndCost penalty() { - return delegate.penalty(); - } -} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/FlexAccessEgressAdapter.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/FlexAccessEgressAdapter.java index fae11497c84..04f51e76681 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/FlexAccessEgressAdapter.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/FlexAccessEgressAdapter.java @@ -1,11 +1,9 @@ package org.opentripplanner.routing.algorithm.raptoradapter.transit; -import java.util.Optional; import org.opentripplanner.ext.flex.FlexAccessEgress; import org.opentripplanner.framework.model.TimeAndCost; import org.opentripplanner.model.StopTime; import org.opentripplanner.raptor.api.model.RaptorConstants; -import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; /** * This class is used to adapt the FlexAccessEgress into a time-dependent multi-leg DefaultAccessEgress. @@ -65,11 +63,6 @@ public RoutingAccessEgress withPenalty(TimeAndCost penalty) { return new FlexAccessEgressAdapter(this, penalty); } - @Override - public Optional routingBookingInfo() { - return flexAccessEgress.routingBookingInfo(); - } - private static int mapToRaptorTime(int flexTime) { return flexTime == StopTime.MISSING_VALUE ? RaptorConstants.TIME_NOT_SET : flexTime; } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/RoutingAccessEgress.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/RoutingAccessEgress.java index debf5ac17ed..d22ec0f71f9 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/RoutingAccessEgress.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/RoutingAccessEgress.java @@ -1,10 +1,8 @@ package org.opentripplanner.routing.algorithm.raptoradapter.transit; -import java.util.Optional; import org.opentripplanner.framework.model.TimeAndCost; import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.street.search.state.State; -import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; /** * Encapsulate information about an access or egress path. This interface extends @@ -32,11 +30,4 @@ public interface RoutingAccessEgress extends RaptorAccessEgress { boolean hasPenalty(); TimeAndCost penalty(); - - /** - * Booking info enforced by the router. By default nothing is returned. - */ - default Optional routingBookingInfo() { - return Optional.empty(); - } } diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/booking/RoutingBookingInfo.java b/src/main/java/org/opentripplanner/transit/model/timetable/booking/RoutingBookingInfo.java index e3e0c8feb93..ebeba5fe29f 100644 --- a/src/main/java/org/opentripplanner/transit/model/timetable/booking/RoutingBookingInfo.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/booking/RoutingBookingInfo.java @@ -1,7 +1,7 @@ package org.opentripplanner.transit.model.timetable.booking; import java.util.Objects; -import java.util.Optional; +import javax.annotation.Nullable; import org.opentripplanner.framework.lang.IntUtils; import org.opentripplanner.framework.tostring.ToStringBuilder; @@ -17,7 +17,9 @@ */ public final class RoutingBookingInfo { - private static final int NOT_SET = -1_999_999; + public static final int NOT_SET = -1_999_999; + private static final int ZERO = 0; + private static final RoutingBookingInfo UNRESTRICTED = new RoutingBookingInfo(); private final int latestBookingTime; private final int minimumBookingNotice; @@ -43,10 +45,36 @@ private RoutingBookingInfo( IntUtils.requireNotNegative(timeOffsetInSeconds, "timeOffsetInSeconds"); } + private RoutingBookingInfo() { + this.latestBookingTime = NOT_SET; + this.minimumBookingNotice = NOT_SET; + this.legDurationInSeconds = ZERO; + this.timeOffsetInSeconds = ZERO; + } + + /** See {@link #isUnrestricted()} */ + public static RoutingBookingInfo unrestricted() { + return UNRESTRICTED; + } + + public static RoutingBookingInfo of(@Nullable BookingInfo bookingInfo) { + return bookingInfo == null ? unrestricted() : of().withBookingInfo(bookingInfo).build(); + } + public static RoutingBookingInfo.Builder of() { return new Builder(); } + /** + * Return {@code true} if there are no booking restrictions. Note! there can be other + * booking-related information associated with the trip. + */ + public boolean isUnrestricted() { + return ( + (this == UNRESTRICTED) || (latestBookingTime == NOT_SET && minimumBookingNotice == NOT_SET) + ); + } + /** * Check if requested board-time can be booked according to the booking info rules. See * {@link BookingInfo}. @@ -54,6 +82,9 @@ public static RoutingBookingInfo.Builder of() { * If not the case, the RaptorConstants.TIME_NOT_SET is returned. */ public boolean isThereEnoughTimeToBookForDeparture(int departureTime, int requestedBookingTime) { + if (isUnrestricted(requestedBookingTime)) { + return true; + } return isThereEnoughTimeToBook(departureTime + timeOffsetInSeconds, requestedBookingTime); } @@ -64,28 +95,24 @@ public boolean isThereEnoughTimeToBookForDeparture(int departureTime, int reques * If not the case, the RaptorConstants.TIME_NOT_SET is returned. */ public boolean isThereEnoughTimeToBookForArrival(int arrivalTime, int requestedBookingTime) { + if (isUnrestricted(requestedBookingTime)) { + return true; + } return isThereEnoughTimeToBook( arrivalTime - legDurationInSeconds + timeOffsetInSeconds, requestedBookingTime ); } - /** - * Check if requested board-time can be booked according to the booking info rules. See - * {@link BookingInfo}. - *

- * If not the case, the RaptorConstants.TIME_NOT_SET is returned. - */ - private boolean isThereEnoughTimeToBook(int time, int requestedBookingTime) { - // This can be optimized/simplified; it can be done before the search start since it - // only depends on the latestBookingTime and requestedBookingTime, not the departure time. - if (exceedsLatestBookingTime(requestedBookingTime)) { - return false; - } - if (exceedsMinimumBookingNotice(time, requestedBookingTime)) { - return false; + public int earliestDepartureTime(int requestedDepartureTime, int departureTime) { + if (requestedDepartureTime == NOT_SET || minimumBookingNotice == NOT_SET) { + return departureTime; } - return true; + return Math.max(requestedDepartureTime + minimumBookingNotice, departureTime); + } + + private boolean isUnrestricted(int requestedBookingTime) { + return requestedBookingTime == NOT_SET || isUnrestricted(); } @Override @@ -114,20 +141,50 @@ public String toString() { .of(RoutingBookingInfo.class) .addServiceTime("latestBookingTime", latestBookingTime, NOT_SET) .addDurationSec("minimumBookingNotice", minimumBookingNotice, NOT_SET) + .addDurationSec("timeOffsetInSeconds", timeOffsetInSeconds, ZERO) + .addDurationSec("legDurationInSeconds", legDurationInSeconds, ZERO) .toString(); } - private boolean exceedsLatestBookingTime(int requestedEarliestBookingTime) { - return exist(latestBookingTime) && requestedEarliestBookingTime > latestBookingTime; + /** + * Check if requested board-time can be booked according to the booking info rules. See + * {@link BookingInfo}. + *

+ * If not the case, the RaptorConstants.TIME_NOT_SET is returned. + */ + private boolean isThereEnoughTimeToBook(int time, int requestedBookingTime) { + // This can be optimized/simplified; it can be done before the search start since it + // only depends on the latestBookingTime and requestedBookingTime, not the departure time. + if (exceedsLatestBookingTime(requestedBookingTime)) { + return false; + } + if (exceedsMinimumBookingNotice(time, requestedBookingTime)) { + return false; + } + return true; + } + + public boolean exceedsLatestBookingTime(int requestedBookingTime) { + return ( + exist(requestedBookingTime) && + exist(latestBookingTime) && + exceedsLatestBookingTime(requestedBookingTime, latestBookingTime) + ); + } + + private static boolean exceedsLatestBookingTime(int requestedBookingTime, int latestBookingTime) { + return requestedBookingTime > latestBookingTime; } /** * Check if the given time is after (or eq to) the earliest time allowed according to the minimum * booking notice. */ - private boolean exceedsMinimumBookingNotice(int departureTime, int requestedBookingTime) { + public boolean exceedsMinimumBookingNotice(int departureTime, int requestedBookingTime) { return ( - exist(minimumBookingNotice) && (departureTime - minimumBookingNotice < requestedBookingTime) + exist(requestedBookingTime) && + exist(minimumBookingNotice) && + (departureTime - minimumBookingNotice < requestedBookingTime) ); } @@ -137,21 +194,34 @@ private static boolean exist(int value) { public static class Builder { - private int latestBookingTime = NOT_SET; - private int minimumBookingNotice = NOT_SET; + private int latestBookingTime; + private int minimumBookingNotice; private int legDurationInSeconds = 0; private int timeOffsetInSeconds = 0; + public Builder() { + setUnrestricted(); + } + /** * Convenience method to add booking info to builder. */ - public Builder withBookingInfo(BookingInfo bookingInfo) { - if (bookingInfo.getLatestBookingTime() != null) { - withLatestBookingTime(bookingInfo.getLatestBookingTime().relativeTimeSeconds()); - } - if (bookingInfo.getMinimumBookingNotice() != null) { - withMinimumBookingNotice((int) bookingInfo.getMinimumBookingNotice().toSeconds()); + public Builder withBookingInfo(@Nullable BookingInfo bookingInfo) { + // Clear booking + if (bookingInfo == null) { + setUnrestricted(); + return this; } + withLatestBookingTime( + bookingInfo.getLatestBookingTime() == null + ? NOT_SET + : bookingInfo.getLatestBookingTime().relativeTimeSeconds() + ); + withMinimumBookingNotice( + bookingInfo.getMinimumBookingNotice() == null + ? NOT_SET + : (int) bookingInfo.getMinimumBookingNotice().toSeconds() + ); return this; } @@ -185,19 +255,21 @@ public Builder withTimeOffsetInSeconds(int timeOffsetInSeconds) { return this; } - public Optional build() { + public RoutingBookingInfo build() { if (latestBookingTime == NOT_SET && minimumBookingNotice == NOT_SET) { - return Optional.empty(); + return RoutingBookingInfo.unrestricted(); } - - return Optional.of( - new RoutingBookingInfo( - latestBookingTime, - minimumBookingNotice, - legDurationInSeconds, - timeOffsetInSeconds - ) + return new RoutingBookingInfo( + latestBookingTime, + minimumBookingNotice, + legDurationInSeconds, + timeOffsetInSeconds ); } + + private void setUnrestricted() { + latestBookingTime = NOT_SET; + minimumBookingNotice = NOT_SET; + } } } diff --git a/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java b/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java index 6f228bdd420..5da127de2ba 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapperTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.opentripplanner._support.time.ZoneIds; import org.opentripplanner.ext.flex.FlexAccessEgress; +import org.opentripplanner.ext.flex.FlexPathDurations; import org.opentripplanner.framework.model.Cost; import org.opentripplanner.framework.model.TimeAndCost; import org.opentripplanner.framework.time.TimeUtils; @@ -57,6 +58,7 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; import org.opentripplanner.transit.service.DefaultTransitService; import org.opentripplanner.transit.service.TransitModel; @@ -149,7 +151,16 @@ void createItineraryWithOnBoardFlexAccess() { ); State state = TestStateBuilder.ofWalking().streetEdge().streetEdge().build(); - FlexAccessEgress flexAccessEgress = new FlexAccessEgress(S1, null, 0, 1, flexTrip, state, true); + FlexAccessEgress flexAccessEgress = new FlexAccessEgress( + S1, + new FlexPathDurations(0, (int) state.getElapsedTimeSeconds(), 0, 0), + 0, + 1, + flexTrip, + state, + true, + RoutingBookingInfo.NOT_SET + ); RaptorAccessEgress access = new FlexAccessEgressAdapter(flexAccessEgress, false); Transfer transfer = new Transfer(S2.getIndex(), 0); RaptorTransfer raptorTransfer = new DefaultRaptorTransfer(S1.getIndex(), 0, 0, transfer); diff --git a/src/test/java/org/opentripplanner/transit/model/timetable/booking/RoutingBookingInfoTest.java b/src/test/java/org/opentripplanner/transit/model/timetable/booking/RoutingBookingInfoTest.java index 8b575d76517..e188220e906 100644 --- a/src/test/java/org/opentripplanner/transit/model/timetable/booking/RoutingBookingInfoTest.java +++ b/src/test/java/org/opentripplanner/transit/model/timetable/booking/RoutingBookingInfoTest.java @@ -52,8 +52,7 @@ void isThereEnoughTimeToBookWithMinBookingTimeBeforeDeparture( var subject = RoutingBookingInfo .of() .withMinimumBookingNotice(MINIMUM_BOOKING_NOTICE_20m) - .build() - .orElseThrow(); + .build(); // Since we have not set a duration or offset, departure and arrival is the same assertEquals( @@ -89,8 +88,7 @@ void isThereEnoughTimeToBookWithMinBookingTimeBeforeArrival( ) .withLegDurationInSeconds(duration) .withTimeOffsetInSeconds(offset) - .build() - .orElseThrow(); + .build(); assertEquals( expected.minBookingNotice, @@ -125,8 +123,7 @@ void isThereEnoughTimeToBookWithLatestBookingTime( ) .withLegDurationInSeconds(duration) .withTimeOffsetInSeconds(offset) - .build() - .orElseThrow(); + .build(); assertEquals( expected.latestBookingTime, @@ -159,8 +156,7 @@ void isThereEnoughTimeToBookUsingBoth( .withLatestBookingTime(LATEST_BOOKING_TIME_13_00.toSecondOfDay()) .withLegDurationInSeconds(duration) .withTimeOffsetInSeconds(offset) - .build() - .orElseThrow(); + .build(); assertEquals( expected == Expected.BOTH, @@ -185,8 +181,7 @@ void testToString() { .of() .withMinimumBookingNotice(MINIMUM_BOOKING_NOTICE_20m) .withLatestBookingTime(LATEST_BOOKING_TIME_13_00.toSecondOfDay()) - .build() - .orElseThrow(); + .build(); assertEquals( "RoutingBookingInfo{latestBookingTime: 13:00, minimumBookingNotice: 20m}",