diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/AreaStopsToVerticesMapperTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/AreaStopsToVerticesMapperTest.java index 3b41cf9632d..67eb7e3ed93 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/AreaStopsToVerticesMapperTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/AreaStopsToVerticesMapperTest.java @@ -29,10 +29,10 @@ class AreaStopsToVerticesMapperTest { private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); - private static final AreaStop BERLIN_AREA_STOP = TEST_MODEL.areaStopForTest( - "berlin", - Polygons.BERLIN - ); + private static final AreaStop BERLIN_AREA_STOP = TEST_MODEL + .areaStop("berlin") + .withGeometry(Polygons.BERLIN) + .build(); public static final StopModel STOP_MODEL = TEST_MODEL .stopModelBuilder() .withAreaStop(AreaStopsToVerticesMapperTest.BERLIN_AREA_STOP) diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java index f414572ecf2..c88439a9e3f 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java @@ -17,7 +17,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.opentripplanner.ConstantsForTests; import org.opentripplanner.TestOtpModel; import org.opentripplanner.TestServerContext; import org.opentripplanner.framework.application.OTPFeature; @@ -57,14 +56,18 @@ public class FlexIntegrationTest { @BeforeAll static void setup() { OTPFeature.enableFeatures(Map.of(OTPFeature.FlexRouting, true)); - TestOtpModel model = ConstantsForTests.buildOsmGraph(FlexTest.COBB_OSM); + TestOtpModel model = FlexIntegrationTestData.cobbOsm(); graph = model.graph(); transitModel = model.transitModel(); addGtfsToGraph( graph, transitModel, - List.of(FlexTest.COBB_BUS_30_GTFS, FlexTest.MARTA_BUS_856_GTFS, FlexTest.COBB_FLEX_GTFS) + List.of( + FlexIntegrationTestData.COBB_BUS_30_GTFS, + FlexIntegrationTestData.MARTA_BUS_856_GTFS, + FlexIntegrationTestData.COBB_FLEX_GTFS + ) ); service = TestServerContext.createServerContext(graph, transitModel).routingService(); } diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTestData.java b/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTestData.java new file mode 100644 index 00000000000..e0a85e23794 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTestData.java @@ -0,0 +1,90 @@ +package org.opentripplanner.ext.flex; + +import static graphql.Assert.assertTrue; + +import gnu.trove.set.hash.TIntHashSet; +import java.io.File; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import org.opentripplanner.ConstantsForTests; +import org.opentripplanner.TestOtpModel; +import org.opentripplanner.ext.flex.flexpathcalculator.DirectFlexPathCalculator; +import org.opentripplanner.ext.flex.template.FlexServiceDate; +import org.opentripplanner.framework.application.OTPFeature; +import org.opentripplanner.gtfs.graphbuilder.GtfsBundle; +import org.opentripplanner.gtfs.graphbuilder.GtfsModule; +import org.opentripplanner.model.calendar.ServiceDateInterval; +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; + +public final class FlexIntegrationTestData { + + private static final ResourceLoader RES = ResourceLoader.of(FlexIntegrationTestData.class); + + private static final File ASPEN_GTFS = RES.file("aspen-flex-on-demand.gtfs"); + static final File COBB_BUS_30_GTFS = RES.file("cobblinc-bus-30-only.gtfs.zip"); + static final File COBB_FLEX_GTFS = RES.file("cobblinc-scheduled-deviated-flex.gtfs"); + private static final File COBB_OSM = RES.file("cobb-county.filtered.osm.pbf"); + private static final File LINCOLN_COUNTY_GTFS = RES.file("lincoln-county-flex.gtfs"); + static final File MARTA_BUS_856_GTFS = RES.file("marta-bus-856-only.gtfs.zip"); + + public static final DirectFlexPathCalculator CALCULATOR = new DirectFlexPathCalculator(); + private static final LocalDate SERVICE_DATE = LocalDate.of(2021, 4, 11); + private static final int SECONDS_SINCE_MIDNIGHT = LocalTime.of(10, 0).toSecondOfDay(); + public static final FlexServiceDate FLEX_DATE = new FlexServiceDate( + SERVICE_DATE, + SECONDS_SINCE_MIDNIGHT, + RoutingBookingInfo.NOT_SET, + new TIntHashSet() + ); + + public static TestOtpModel aspenGtfs() { + return buildFlexGraph(ASPEN_GTFS); + } + + public static TestOtpModel cobbFlexGtfs() { + return buildFlexGraph(COBB_FLEX_GTFS); + } + + public static TestOtpModel cobbBus30Gtfs() { + return buildFlexGraph(COBB_BUS_30_GTFS); + } + + public static TestOtpModel martaBus856Gtfs() { + return buildFlexGraph(MARTA_BUS_856_GTFS); + } + + public static TestOtpModel lincolnCountyGtfs() { + return buildFlexGraph(LINCOLN_COUNTY_GTFS); + } + + public static TestOtpModel cobbOsm() { + return ConstantsForTests.buildOsmGraph(COBB_OSM); + } + + private static TestOtpModel buildFlexGraph(File file) { + var deduplicator = new Deduplicator(); + var graph = new Graph(deduplicator); + var transitModel = new TransitModel(new StopModel(), deduplicator); + GtfsBundle gtfsBundle = new GtfsBundle(file); + GtfsModule module = new GtfsModule( + List.of(gtfsBundle), + transitModel, + graph, + new ServiceDateInterval(LocalDate.of(2021, 1, 1), LocalDate.of(2022, 1, 1)) + ); + OTPFeature.enableFeatures(Map.of(OTPFeature.FlexRouting, true)); + module.buildGraph(); + transitModel.index(); + graph.index(transitModel.getStopModel()); + OTPFeature.enableFeatures(Map.of(OTPFeature.FlexRouting, false)); + assertTrue(transitModel.hasFlexTrips()); + return new TestOtpModel(graph, transitModel); + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/FlexStopTimesForTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/FlexStopTimesForTest.java index d76382999a2..8437a62e6da 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/FlexStopTimesForTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/FlexStopTimesForTest.java @@ -12,7 +12,10 @@ public class FlexStopTimesForTest { private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); - private static final StopLocation AREA_STOP = TEST_MODEL.areaStopForTest("area", Polygons.BERLIN); + private static final StopLocation AREA_STOP = TEST_MODEL + .areaStop("area") + .withGeometry(Polygons.BERLIN) + .build(); private static final RegularStop REGULAR_STOP = TEST_MODEL.stop("stop").build(); public static StopTime area(String startTime, String endTime) { diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/FlexTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/FlexTest.java deleted file mode 100644 index 26cedae79ee..00000000000 --- a/src/ext-test/java/org/opentripplanner/ext/flex/FlexTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.opentripplanner.ext.flex; - -import static graphql.Assert.assertTrue; - -import gnu.trove.set.hash.TIntHashSet; -import java.io.File; -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.List; -import java.util.Map; -import org.opentripplanner.TestOtpModel; -import org.opentripplanner.ext.flex.flexpathcalculator.DirectFlexPathCalculator; -import org.opentripplanner.framework.application.OTPFeature; -import org.opentripplanner.gtfs.graphbuilder.GtfsBundle; -import org.opentripplanner.gtfs.graphbuilder.GtfsModule; -import org.opentripplanner.model.calendar.ServiceDateInterval; -import org.opentripplanner.routing.graph.Graph; -import org.opentripplanner.test.support.ResourceLoader; -import org.opentripplanner.transit.model.framework.Deduplicator; -import org.opentripplanner.transit.service.StopModel; -import org.opentripplanner.transit.service.TransitModel; - -public abstract class FlexTest { - - private static final ResourceLoader RES = ResourceLoader.of(FlexTest.class); - - protected static final File ASPEN_GTFS = RES.file("aspen-flex-on-demand.gtfs"); - protected static final File COBB_FLEX_GTFS = RES.file("cobblinc-scheduled-deviated-flex.gtfs"); - protected static final File COBB_BUS_30_GTFS = RES.file("cobblinc-bus-30-only.gtfs.zip"); - protected static final File MARTA_BUS_856_GTFS = RES.file("marta-bus-856-only.gtfs.zip"); - protected static final File LINCOLN_COUNTY_GTFS = RES.file("lincoln-county-flex.gtfs"); - protected static final File COBB_OSM = RES.file("cobb-county.filtered.osm.pbf"); - - protected static final DirectFlexPathCalculator calculator = new DirectFlexPathCalculator(); - protected static final LocalDate serviceDate = LocalDate.of(2021, 4, 11); - protected static final int secondsSinceMidnight = LocalTime.of(10, 0).toSecondOfDay(); - protected static final FlexServiceDate flexDate = new FlexServiceDate( - serviceDate, - secondsSinceMidnight, - new TIntHashSet() - ); - - protected static TestOtpModel buildFlexGraph(File file) { - var deduplicator = new Deduplicator(); - var graph = new Graph(deduplicator); - var transitModel = new TransitModel(new StopModel(), deduplicator); - GtfsBundle gtfsBundle = new GtfsBundle(file); - GtfsModule module = new GtfsModule( - List.of(gtfsBundle), - transitModel, - graph, - new ServiceDateInterval(LocalDate.of(2021, 1, 1), LocalDate.of(2022, 1, 1)) - ); - OTPFeature.enableFeatures(Map.of(OTPFeature.FlexRouting, true)); - module.buildGraph(); - transitModel.index(); - graph.index(transitModel.getStopModel()); - OTPFeature.enableFeatures(Map.of(OTPFeature.FlexRouting, false)); - assertTrue(transitModel.hasFlexTrips()); - return new TestOtpModel(graph, transitModel); - } -} diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/GtfsFlexTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/GtfsFlexTest.java index 78c4a2f3ddc..e116831e2d3 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/GtfsFlexTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/GtfsFlexTest.java @@ -3,7 +3,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeAll; @@ -11,26 +10,24 @@ import org.opentripplanner.TestOtpModel; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.ext.flex.trip.UnscheduledTrip; -import org.opentripplanner.routing.graphfinder.NearbyStop; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.service.TransitModel; /** * This test makes sure that one of the example feeds in the GTFS-Flex repo works. It's the City of - * Aspen Downtown taxi service which is a completely unscheduled trip that takes you door-to-door in + * Aspen Downtown taxi service, which is a completely unscheduled trip that takes you door-to-door in * the city. *

* It only contains a single stop time which in GTFS static would not work but is valid in GTFS * Flex. */ -public class GtfsFlexTest extends FlexTest { +class GtfsFlexTest { private static TransitModel transitModel; @BeforeAll static void setup() { - TestOtpModel model = FlexTest.buildFlexGraph(ASPEN_GTFS); + TestOtpModel model = FlexIntegrationTestData.aspenGtfs(); transitModel = model.transitModel(); } @@ -49,50 +46,8 @@ void parseAspenTaxiAsUnscheduledTrip() { ); } - @Test - void calculateAccessTemplate() { - var trip = getFlexTrip(); - var nearbyStop = getNearbyStop(trip); - - var accesses = trip - .getFlexAccessTemplates(nearbyStop, flexDate, calculator, FlexConfig.DEFAULT) - .toList(); - - assertEquals(1, accesses.size()); - - var access = accesses.get(0); - assertEquals(0, access.fromStopIndex); - assertEquals(0, access.toStopIndex); - } - - @Test - void calculateEgressTemplate() { - var trip = getFlexTrip(); - var nearbyStop = getNearbyStop(trip); - var egresses = trip - .getFlexEgressTemplates(nearbyStop, flexDate, calculator, FlexConfig.DEFAULT) - .toList(); - - assertEquals(1, egresses.size()); - - var egress = egresses.get(0); - assertEquals(0, egress.fromStopIndex); - assertEquals(0, egress.toStopIndex); - } - @Test void shouldGeneratePatternForFlexTripWithSingleStop() { assertFalse(transitModel.getAllTripPatterns().isEmpty()); } - - private static NearbyStop getNearbyStop(FlexTrip trip) { - assertEquals(1, trip.getStops().size()); - var stopLocation = trip.getStops().iterator().next(); - return new NearbyStop(stopLocation, 0, List.of(), null); - } - - private static FlexTrip getFlexTrip() { - var flexTrips = transitModel.getAllFlexTrips(); - return flexTrips.iterator().next(); - } } diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculatorTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculatorTest.java index 70f39af2420..985ca5a9898 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculatorTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculatorTest.java @@ -29,7 +29,7 @@ class ScheduledFlexPathCalculatorTest { @Test void calculateTime() { - var c = (FlexPathCalculator) (fromv, tov, fromStopIndex, toStopIndex) -> + var c = (FlexPathCalculator) (fromv, tov, boardStopPosition, alightStopPosition) -> new FlexPath(10_000, (int) Duration.ofMinutes(10).toSeconds(), () -> LineStrings.SIMPLE); var calc = new ScheduledFlexPathCalculator(c, TRIP); var path = calc.calculateFlexPath(V1, V2, 0, 1); diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculatorTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculatorTest.java index e504f8ac762..15f62038a6f 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculatorTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculatorTest.java @@ -15,7 +15,7 @@ class TimePenaltyCalculatorTest { @Test void calculate() { - FlexPathCalculator delegate = (fromv, tov, fromStopIndex, toStopIndex) -> + FlexPathCalculator delegate = (fromv, tov, boardStopPosition, alightStopPosition) -> new FlexPath(10_000, THIRTY_MINS_IN_SECONDS, () -> LineStrings.SIMPLE); var mod = TimePenalty.of(Duration.ofMinutes(10), 1.5f); @@ -26,7 +26,7 @@ void calculate() { @Test void nullValue() { - FlexPathCalculator delegate = (fromv, tov, fromStopIndex, toStopIndex) -> null; + FlexPathCalculator delegate = (fromv, tov, boardStopPosition, alightStopPosition) -> null; var mod = TimePenalty.of(Duration.ofMinutes(10), 1.5f); var calc = new TimePenaltyCalculator(delegate, mod); var path = calc.calculateFlexPath(StreetModelForTest.V1, StreetModelForTest.V2, 0, 5); diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/template/BoardAlight.java b/src/ext-test/java/org/opentripplanner/ext/flex/template/BoardAlight.java new file mode 100644 index 00000000000..cac09fb24a0 --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/flex/template/BoardAlight.java @@ -0,0 +1,7 @@ +package org.opentripplanner.ext.flex.template; + +enum BoardAlight { + BOARD_ONLY, + ALIGHT_ONLY, + BOARD_AND_ALIGHT, +} 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 new file mode 100644 index 00000000000..8c73d0901ff --- /dev/null +++ b/src/ext-test/java/org/opentripplanner/ext/flex/template/FlexTemplateFactoryTest.java @@ -0,0 +1,374 @@ +package org.opentripplanner.ext.flex.template; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.ext.flex.template.BoardAlight.ALIGHT_ONLY; +import static org.opentripplanner.ext.flex.template.BoardAlight.BOARD_AND_ALIGHT; +import static org.opentripplanner.ext.flex.template.BoardAlight.BOARD_ONLY; +import static org.opentripplanner.framework.time.TimeUtils.time; + +import gnu.trove.set.hash.TIntHashSet; +import java.time.Duration; +import java.time.LocalDate; +import java.time.Month; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; +import org.opentripplanner.ext.flex.flexpathcalculator.ScheduledFlexPathCalculator; +import org.opentripplanner.ext.flex.flexpathcalculator.StreetFlexPathCalculator; +import org.opentripplanner.ext.flex.trip.FlexTrip; +import org.opentripplanner.ext.flex.trip.ScheduledDeviatedTrip; +import org.opentripplanner.ext.flex.trip.UnscheduledTrip; +import org.opentripplanner.framework.i18n.I18NString; +import org.opentripplanner.model.PickDrop; +import org.opentripplanner.model.StopTime; +import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.street.model.vertex.StreetLocation; +import org.opentripplanner.street.search.request.StreetSearchRequest; +import org.opentripplanner.street.search.state.State; +import org.opentripplanner.transit.model._data.TransitModelForTest; +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 { + + private static final TransitModelForTest MODEL = TransitModelForTest.of(); + + /** + * This is pass-through information + */ + private static final Duration MAX_TRANSFER_DURATION = Duration.ofMinutes(10); + + /** + * Any calculator will do. The only thing we will test here is that a new scheduled calculator + * is created for scheduled-flex-trips. + */ + private static final FlexPathCalculator CALCULATOR = new StreetFlexPathCalculator( + false, + Duration.ofHours(3) + ); + + public static final int SERVICE_TIME_OFFSET = 3600 * 2; + + /** + * The date is pass-through information in this test, so one date is enough. + */ + private static final FlexServiceDate DATE = new FlexServiceDate( + LocalDate.of(2024, Month.MAY, 17), + SERVICE_TIME_OFFSET, + RoutingBookingInfo.NOT_SET, + new TIntHashSet() + ); + + // Stop A-D is a mix of regular and area stops - it should not matter for this test + private static final StopLocation STOP_A = MODEL.stop("A").build(); + private static final StopLocation STOP_B = MODEL.areaStop("B").build(); + private static final StopLocation STOP_C = MODEL.areaStop("C").build(); + private static final StopLocation STOP_D = MODEL.stop("D").build(); + private static final RegularStop STOP_G1 = MODEL.stop("G1").build(); + private static final RegularStop STOP_G2 = MODEL.stop("G2").build(); + private static final RegularStop STOP_G3 = MODEL.stop("G3").build(); + private static final RegularStop STOP_G4 = MODEL.stop("G4").build(); + private static final StopLocation GROUP_STOP_12 = MODEL.groupStop("G", STOP_G1, STOP_G2); + private static final StopLocation GROUP_STOP_34 = MODEL.groupStop("G", STOP_G3, STOP_G4); + + private static final Trip TRIP = TransitModelForTest.trip("Trip").build(); + private static final int T_10_00 = time("10:00"); + private static final int T_10_10 = time("10:10"); + private static final int T_10_20 = time("10:20"); + private static final int T_10_30 = time("10:30"); + private static final int T_10_40 = time("10:40"); + + @Test + void testCreateAccessTemplateForUnscheduledTripWithTwoStopsAndNoBoardRestrictions() { + var flexTrip = unscheduledTrip( + "FlexTrip", + stopTime(1, STOP_A, BOARD_AND_ALIGHT, T_10_00), + stopTime(2, STOP_B, BOARD_AND_ALIGHT, T_10_10) + ); + + var factory = FlexTemplateFactory.of(CALCULATOR, MAX_TRANSFER_DURATION); + + // Create template with access boarding at stop A + var subject = factory.createAccessTemplates(closestTrip(flexTrip, STOP_A, 0)); + + var template = subject.get(0); + assertEquals(0, template.boardStopPosition); + assertEquals(1, template.alightStopPosition); + assertSame(CALCULATOR, template.calculator); + assertSame(STOP_B, template.transferStop); + assertSame(DATE.serviceDate(), template.serviceDate); + assertEquals(SERVICE_TIME_OFFSET, template.secondsFromStartOfTime); + assertEquals(1, subject.size(), subject::toString); + + // We are not allowed to board and alight at the same stop so boarding the last stop + // will result in an empty result + subject = factory.createAccessTemplates(closestTrip(flexTrip, STOP_B, 2)); + assertTrue(subject.isEmpty(), subject::toString); + + // Search for a stop not part of the pattern should result in an empty result + subject = factory.createAccessTemplates(closestTrip(flexTrip, STOP_C, 99)); + assertTrue(subject.isEmpty(), subject::toString); + } + + @Test + void testCreateEgressTemplateForUnscheduledTripWithTwoStopsAndNoBoardRestrictions() { + var flexTrip = unscheduledTrip( + "FlexTrip", + stopTime(1, STOP_A, BOARD_AND_ALIGHT, T_10_00), + stopTime(2, STOP_B, BOARD_AND_ALIGHT, T_10_10) + ); + + var factory = FlexTemplateFactory.of(CALCULATOR, MAX_TRANSFER_DURATION); + + // Create template with egress alighting at stop B + var subject = factory.createEgressTemplates(closestTrip(flexTrip, STOP_B, 1)); + + var template = subject.get(0); + assertEquals(0, template.boardStopPosition); + assertEquals(1, template.alightStopPosition); + assertSame(CALCULATOR, template.calculator); + assertSame(STOP_A, template.transferStop); + assertSame(DATE.serviceDate(), template.serviceDate); + assertEquals(SERVICE_TIME_OFFSET, template.secondsFromStartOfTime); + assertEquals(1, subject.size(), subject::toString); + + // We are not allowed to board and alight at the same stop so boarding the last stop + // will result in an empty result + subject = factory.createEgressTemplates(closestTrip(flexTrip, STOP_A, 0)); + assertTrue(subject.isEmpty(), subject::toString); + // TODO This is no longer the responsibility of the template factory, reimplement test + // Search for a stop not part of the pattern should result in an empty result + // subject = factory.createEgressTemplates(closestTrip(flexTrip, STOP_C, 99)); + // assertTrue(subject.isEmpty(), subject::toString); + } + + @Test + void testCreateAccessTemplateForUnscheduledTripWithBoardAndAlightRestrictions() { + var flexTrip = unscheduledTrip( + "FlexTrip", + stopTime(1, STOP_A, BOARD_ONLY, T_10_00), + stopTime(2, STOP_B, ALIGHT_ONLY, T_10_10), + stopTime(3, STOP_C, BOARD_ONLY, T_10_20), + stopTime(4, STOP_D, ALIGHT_ONLY, T_10_30) + ); + + var factory = FlexTemplateFactory.of(CALCULATOR, MAX_TRANSFER_DURATION); + + // Create template with boarding at stop A + var subject = factory.createAccessTemplates(closestTrip(flexTrip, STOP_A, 0)); + + var t1 = subject.get(0); + var t2 = subject.get(1); + + assertEquals(0, t1.boardStopPosition); + assertEquals(0, t2.boardStopPosition); + assertEquals(Set.of(1, 3), Set.of(t1.alightStopPosition, t2.alightStopPosition)); + assertEquals(2, subject.size()); + + // Board at stop C + subject = factory.createAccessTemplates(closestTrip(flexTrip, STOP_C, 2)); + + t1 = subject.get(0); + assertEquals(2, t1.boardStopPosition); + assertEquals(3, t1.alightStopPosition); + assertEquals(1, subject.size()); + // TODO This is no longer the responsibility of the template factory, reimplement test + // We are not allowed to board at stop B, an empty result is expected + // subject = factory.createAccessTemplates(closestTrip(flexTrip, STOP_B, 1)); + // assertTrue(subject.isEmpty(), subject::toString); + + // TODO This is no longer the responsibility of the template factory, reimplement test + // Search for a stop not part of the pattern should result in an empty result + // subject = factory.createAccessTemplates(closestTrip(flexTrip, STOP_D, 3)); + // assertTrue(subject.isEmpty(), subject::toString); + } + + @Test + void testCreateEgressTemplateForUnscheduledTripWithBoardAndAlightRestrictions() { + var flexTrip = unscheduledTrip( + "FlexTrip", + stopTime(1, STOP_A, BOARD_ONLY, T_10_00), + stopTime(2, STOP_B, ALIGHT_ONLY, T_10_10), + stopTime(3, STOP_C, BOARD_ONLY, T_10_20), + stopTime(4, STOP_D, ALIGHT_ONLY, T_10_30) + ); + + var factory = FlexTemplateFactory.of(CALCULATOR, MAX_TRANSFER_DURATION); + + // Create template with boarding at stop A + var subject = factory.createEgressTemplates(closestTrip(flexTrip, STOP_D, 3)); + + var t1 = subject.get(0); + var t2 = subject.get(1); + + assertEquals(Set.of(0, 2), Set.of(t1.boardStopPosition, t2.boardStopPosition)); + assertEquals(3, t1.alightStopPosition); + assertEquals(3, t2.alightStopPosition); + assertEquals(2, subject.size()); + + // Board at stop C + subject = factory.createEgressTemplates(closestTrip(flexTrip, STOP_B, 1)); + + t1 = subject.get(0); + assertEquals(0, t1.boardStopPosition); + assertEquals(1, t1.alightStopPosition); + assertEquals(1, subject.size()); + + // TODO This is no longer the responsibility of the template factory, reimplement test + // We are not allowed to board and alight at the same stop so boarding the last stop + // will result in an empty result + // subject = factory.createEgressTemplates(closestTrip(flexTrip, STOP_C, 2)); + // assertTrue(subject.isEmpty(), subject::toString); + + // Search for a stop not part of the pattern should result in an empty result + subject = factory.createEgressTemplates(closestTrip(flexTrip, STOP_A, 0)); + assertTrue(subject.isEmpty(), subject::toString); + } + + @Test + void testCreateAccessTemplateForUnscheduledTripWithTwoGroupsStops() { + var flexTrip = unscheduledTrip( + "FlexTrip", + stopTime(1, GROUP_STOP_12, BOARD_ONLY, T_10_00), + stopTime(2, GROUP_STOP_34, ALIGHT_ONLY, T_10_20) + ); + + var factory = FlexTemplateFactory.of(CALCULATOR, MAX_TRANSFER_DURATION); + + // Create template with access boarding at stop A + var subject = factory.createAccessTemplates(closestTrip(flexTrip, STOP_G1, 0)); + + var t1 = subject.get(0); + var t2 = subject.get(1); + assertEquals(0, t1.boardStopPosition); + assertEquals(0, t2.boardStopPosition); + assertEquals(1, t1.alightStopPosition); + assertEquals(1, t2.alightStopPosition); + assertEquals(Set.of(STOP_G3, STOP_G4), Set.of(t1.transferStop, t2.transferStop)); + assertEquals(2, subject.size(), subject::toString); + } + + @Test + void testCreateEgressTemplateForUnscheduledTripWithTwoGroupsStops() { + var flexTrip = unscheduledTrip( + "FlexTrip", + stopTime(1, GROUP_STOP_12, BOARD_ONLY, T_10_00), + stopTime(2, GROUP_STOP_34, ALIGHT_ONLY, T_10_20) + ); + + var factory = FlexTemplateFactory.of(CALCULATOR, MAX_TRANSFER_DURATION); + + // Create template with access boarding at stop A + var subject = factory.createEgressTemplates(closestTrip(flexTrip, STOP_G4, 1)); + + var t1 = subject.get(0); + var t2 = subject.get(1); + assertEquals(0, t1.boardStopPosition); + assertEquals(0, t2.boardStopPosition); + assertEquals(1, t1.alightStopPosition); + assertEquals(1, t2.alightStopPosition); + assertEquals(Set.of(STOP_G1, STOP_G2), Set.of(t1.transferStop, t2.transferStop)); + assertEquals(2, subject.size(), subject::toString); + } + + @Test + void testCreateAccessTemplateForScheduledDeviatedTrip() { + var flexTrip = scheduledDeviatedFlexTrip( + "FlexTrip", + stopTime(1, STOP_A, BOARD_ONLY, T_10_00), + stopTime(5, STOP_B, BOARD_AND_ALIGHT, T_10_20), + stopTime(10, STOP_C, ALIGHT_ONLY, T_10_30) + ); + + var factory = FlexTemplateFactory.of(CALCULATOR, MAX_TRANSFER_DURATION); + + // Create template with access boarding at stop A + var subject = factory.createAccessTemplates(closestTrip(flexTrip, STOP_B, 1)); + + var template = subject.get(0); + assertEquals(1, template.boardStopPosition); + assertEquals(2, template.alightStopPosition); + assertEquals(STOP_C, template.transferStop); + assertTrue(template.calculator instanceof ScheduledFlexPathCalculator); + assertEquals(1, subject.size(), subject::toString); + } + + @Test + void testCreateEgressTemplateForScheduledDeviatedTrip() { + var flexTrip = scheduledDeviatedFlexTrip( + "FlexTrip", + stopTime(1, STOP_A, BOARD_ONLY, T_10_00), + stopTime(5, STOP_B, BOARD_AND_ALIGHT, T_10_20), + stopTime(10, STOP_C, ALIGHT_ONLY, T_10_30) + ); + + var factory = FlexTemplateFactory.of(CALCULATOR, MAX_TRANSFER_DURATION); + + // Create template with access boarding at stop A + var subject = factory.createEgressTemplates(closestTrip(flexTrip, STOP_B, 1)); + + var template = subject.get(0); + assertEquals(0, template.boardStopPosition); + assertEquals(1, template.alightStopPosition); + assertEquals(STOP_A, template.transferStop); + assertTrue(template.calculator instanceof ScheduledFlexPathCalculator); + assertEquals(1, subject.size(), subject::toString); + } + + /** + * The nearbyStop is pass-through information, except the stop - which defines the "transfer" + * point. + */ + private static NearbyStop nearbyStop(StopLocation transferPoint) { + var id = "NearbyStop:" + transferPoint.getId().getId(); + return new NearbyStop( + transferPoint, + 0, + List.of(), + new State( + new StreetLocation(id, new Coordinate(0, 0), I18NString.of(id)), + StreetSearchRequest.of().build() + ) + ); + } + + private static ScheduledDeviatedTrip scheduledDeviatedFlexTrip(String id, StopTime... stopTimes) { + return MODEL.scheduledDeviatedTrip(id, stopTimes); + } + + private static UnscheduledTrip unscheduledTrip(String id, StopTime... stopTimes) { + return MODEL.unscheduledTrip(id, Arrays.asList(stopTimes)); + } + + private static ClosestTrip closestTrip(FlexTrip trip, StopLocation stop, int stopPos) { + return new ClosestTrip(nearbyStop(stop), trip, stopPos, DATE); + } + + private static StopTime stopTime( + int seqNr, + StopLocation stop, + BoardAlight boardAlight, + int startTime + ) { + var st = MODEL.stopTime(TRIP, seqNr, stop); + switch (boardAlight) { + case BOARD_ONLY: + st.setDropOffType(PickDrop.NONE); + break; + case ALIGHT_ONLY: + st.setPickupType(PickDrop.NONE); + break; + } + st.setFlexWindowStart(startTime); + // 5-minute window + st.setFlexWindowEnd(startTime + 300); + return st; + } +} diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/trip/FlexTripsMapperTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/trip/FlexTripsMapperTest.java index 0d0376bbd32..fad9e98e254 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/trip/FlexTripsMapperTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/trip/FlexTripsMapperTest.java @@ -24,7 +24,7 @@ void defaultTimePenalty() { var trips = FlexTripsMapper.createFlexTrips(builder, NOOP); assertEquals("[UnscheduledTrip{F:flex-1}]", trips.toString()); var unscheduled = (UnscheduledTrip) trips.getFirst(); - var unchanged = unscheduled.flexPathCalculator(new DirectFlexPathCalculator()); + var unchanged = unscheduled.decorateFlexPathCalculator(new DirectFlexPathCalculator()); assertInstanceOf(DirectFlexPathCalculator.class, unchanged); } 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 143388cac0f..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 @@ -19,8 +19,9 @@ import org.opentripplanner.TestServerContext; import org.opentripplanner._support.time.ZoneIds; import org.opentripplanner.ext.fares.DecorateWithFare; +import org.opentripplanner.ext.flex.FlexIntegrationTestData; +import org.opentripplanner.ext.flex.FlexParameters; import org.opentripplanner.ext.flex.FlexRouter; -import org.opentripplanner.ext.flex.FlexTest; import org.opentripplanner.framework.application.OTPFeature; import org.opentripplanner.framework.geometry.EncodedPolyline; import org.opentripplanner.framework.i18n.I18NString; @@ -37,7 +38,6 @@ import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.standalone.api.OtpServerRequestContext; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.street.model.vertex.StreetLocation; import org.opentripplanner.street.search.request.StreetSearchRequest; import org.opentripplanner.street.search.state.State; @@ -53,7 +53,7 @@ *

* Read about the details at: https://www.cobbcounty.org/transportation/cobblinc/routes-and-schedules/flex */ -public class ScheduledDeviatedTripTest extends FlexTest { +class ScheduledDeviatedTripTest { static Graph graph; static TransitModel transitModel; @@ -91,37 +91,6 @@ void parseCobbCountyAsScheduledDeviatedTrip() { assertEquals(-84.63430143459385, flexZone.getLon(), delta); } - @Test - void calculateAccessTemplate() { - var trip = getFlexTrip(); - var nearbyStop = getNearbyStop(trip); - - var accesses = trip - .getFlexAccessTemplates(nearbyStop, flexDate, calculator, FlexConfig.DEFAULT) - .toList(); - - assertEquals(3, accesses.size()); - - var access = accesses.get(0); - assertEquals(1, access.fromStopIndex); - assertEquals(1, access.toStopIndex); - } - - @Test - void calculateEgressTemplate() { - var trip = getFlexTrip(); - var nearbyStop = getNearbyStop(trip); - var egresses = trip - .getFlexEgressTemplates(nearbyStop, flexDate, calculator, FlexConfig.DEFAULT) - .toList(); - - assertEquals(3, egresses.size()); - - var egress = egresses.get(0); - assertEquals(2, egress.fromStopIndex); - assertEquals(2, egress.toStopIndex); - } - @Test void calculateDirectFare() { OTPFeature.enableFeatures(Map.of(OTPFeature.FlexRouting, true)); @@ -133,9 +102,9 @@ void calculateDirectFare() { var router = new FlexRouter( graph, new DefaultTransitService(transitModel), - FlexConfig.DEFAULT, + FlexParameters.defaultValues(), OffsetDateTime.parse("2021-11-12T10:15:24-05:00").toInstant(), - false, + null, 1, 1, List.of(from), @@ -144,7 +113,11 @@ void calculateDirectFare() { var filter = new DecorateWithFare(graph.getFareService()); - var itineraries = router.createFlexOnlyItineraries().stream().peek(filter::decorate).toList(); + var itineraries = router + .createFlexOnlyItineraries(false) + .stream() + .peek(filter::decorate) + .toList(); var itinerary = itineraries.getFirst(); @@ -220,13 +193,13 @@ void shouldNotInterpolateFlexTimes() { */ @Test void parseContinuousPickup() { - var lincolnGraph = FlexTest.buildFlexGraph(LINCOLN_COUNTY_GTFS); + var lincolnGraph = FlexIntegrationTestData.lincolnCountyGtfs(); assertNotNull(lincolnGraph); } @BeforeAll static void setup() { - TestOtpModel model = FlexTest.buildFlexGraph(COBB_FLEX_GTFS); + TestOtpModel model = FlexIntegrationTestData.cobbFlexGtfs(); graph = model.graph(); transitModel = model.transitModel(); } diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledDrivingDurationTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledDrivingDurationTest.java index a4b245b7de8..f19478af629 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledDrivingDurationTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledDrivingDurationTest.java @@ -17,7 +17,12 @@ class UnscheduledDrivingDurationTest { - static final FlexPathCalculator STATIC_CALCULATOR = (fromv, tov, fromStopIndex, toStopIndex) -> + static final FlexPathCalculator STATIC_CALCULATOR = ( + fromv, + tov, + boardStopPosition, + alightStopPosition + ) -> new FlexPath(10_000, (int) Duration.ofMinutes(10).toSeconds(), () -> LineStrings.SIMPLE); private static final StopTime STOP_TIME = FlexStopTimesForTest.area("10:00", "18:00"); @@ -25,7 +30,7 @@ class UnscheduledDrivingDurationTest { void noPenalty() { var trip = UnscheduledTrip.of(id("1")).withStopTimes(List.of(STOP_TIME)).build(); - var calculator = trip.flexPathCalculator(STATIC_CALCULATOR); + var calculator = trip.decorateFlexPathCalculator(STATIC_CALCULATOR); var path = calculator.calculateFlexPath(V1, V2, 0, 0); assertEquals(600, path.durationSeconds); } @@ -38,7 +43,7 @@ void withPenalty() { .withTimePenalty(TimePenalty.of(Duration.ofMinutes(2), 1.5f)) .build(); - var calculator = trip.flexPathCalculator(STATIC_CALCULATOR); + var calculator = trip.decorateFlexPathCalculator(STATIC_CALCULATOR); var path = calculator.calculateFlexPath(V1, V2, 0, 0); assertEquals(1020, path.durationSeconds); } diff --git a/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java b/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java index 80bda31fabf..8bc9d7c8919 100644 --- a/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java @@ -12,8 +12,6 @@ import static org.opentripplanner.model.StopTime.MISSING_VALUE; import static org.opentripplanner.transit.model._data.TransitModelForTest.id; -import gnu.trove.set.hash.TIntHashSet; -import java.time.LocalDate; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -23,18 +21,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.opentripplanner._support.geometry.Polygons; -import org.opentripplanner.ext.flex.FlexServiceDate; -import org.opentripplanner.ext.flex.flexpathcalculator.DirectFlexPathCalculator; -import org.opentripplanner.ext.flex.template.FlexAccessTemplate; -import org.opentripplanner.ext.flex.template.FlexEgressTemplate; import org.opentripplanner.framework.time.DurationUtils; import org.opentripplanner.framework.time.TimeUtils; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.StopTime; import org.opentripplanner.routing.graphfinder.NearbyStop; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.site.AreaStop; import org.opentripplanner.transit.model.site.RegularStop; @@ -51,10 +43,11 @@ class UnscheduledTripTest { private static final int T15_00 = TimeUtils.hm2time(15, 0); private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); - private static final StopLocation AREA_STOP = TEST_MODEL.areaStopForTest("area", Polygons.BERLIN); private static final RegularStop REGULAR_STOP = TEST_MODEL.stop("stop").build(); + private static final StopLocation AREA_STOP = TEST_MODEL.areaStop("area").build(); + @Nested class IsUnscheduledTrip { @@ -548,9 +541,9 @@ void testMultipleAreasEarliestDepartureTime(TestCase tc) { @Test void boardingAlighting() { - var AREA_STOP1 = TEST_MODEL.areaStopForTest("area-1", Polygons.BERLIN); - var AREA_STOP2 = TEST_MODEL.areaStopForTest("area-2", Polygons.BERLIN); - var AREA_STOP3 = TEST_MODEL.areaStopForTest("area-3", Polygons.BERLIN); + var AREA_STOP1 = TEST_MODEL.areaStop("area-1").build(); + var AREA_STOP2 = TEST_MODEL.areaStop("area-2").build(); + var AREA_STOP3 = TEST_MODEL.areaStop("area-3").build(); var first = area(AREA_STOP1, "10:00", "10:05"); first.setDropOffType(NONE); @@ -564,108 +557,43 @@ void boardingAlighting() { .build() .trip(); - assertTrue(trip.isBoardingPossible(nearbyStop(AREA_STOP1))); - assertFalse(trip.isAlightingPossible(nearbyStop(AREA_STOP1))); + assertTrue(trip.isBoardingPossible(AREA_STOP1)); + assertFalse(trip.isAlightingPossible(AREA_STOP1)); - assertFalse(trip.isBoardingPossible(nearbyStop(AREA_STOP2))); - assertTrue(trip.isAlightingPossible(nearbyStop(AREA_STOP2))); + assertFalse(trip.isBoardingPossible(AREA_STOP2)); + assertTrue(trip.isAlightingPossible(AREA_STOP2)); } - @Nested - class FlexTemplates { - - private static final DirectFlexPathCalculator CALCULATOR = new DirectFlexPathCalculator(); - static final StopTime FIRST = area("10:00", "10:05"); - static final StopTime SECOND = area("10:10", "10:15"); - static final StopTime THIRD = area("10:20", "10:25"); - static final StopTime FOURTH = area("10:30", "10:35"); - private static final FlexServiceDate FLEX_SERVICE_DATE = new FlexServiceDate( - LocalDate.of(2023, 9, 16), - 0, - new TIntHashSet() - ); - private static final NearbyStop NEARBY_STOP = new NearbyStop( - FOURTH.getStop(), - 100, - List.of(), - null - ); - - @Test - void accessTemplates() { - var trip = trip(List.of(FIRST, SECOND, THIRD, FOURTH)); - - var templates = accessTemplates(trip); - - assertEquals(3, templates.size()); - - List - .of(0, 1, 2) - .forEach(index -> { - var template = templates.get(index); - assertEquals(0, template.fromStopIndex); - assertEquals(index + 1, template.toStopIndex); - }); - } - - @Test - void accessTemplatesNoAlighting() { - var second = area("10:10", "10:15"); - second.setDropOffType(NONE); - - var trip = trip(List.of(FIRST, second, THIRD, FOURTH)); - - var templates = accessTemplates(trip); - - assertEquals(2, templates.size()); - List - .of(0, 1) - .forEach(index -> { - var template = templates.get(index); - assertEquals(0, template.fromStopIndex); - assertEquals(index + 2, template.toStopIndex); - }); - } - - @Test - void egressTemplates() { - var trip = trip(List.of(FIRST, SECOND, THIRD, FOURTH)); - - var templates = egressTemplates(trip); - - assertEquals(4, templates.size()); - var template = templates.get(0); - assertEquals(0, template.fromStopIndex); - assertEquals(3, template.toStopIndex); - } + private static String timeToString(int time) { + return TimeUtils.timeToStrCompact(time, MISSING_VALUE, "MISSING_VALUE"); + } - @Nonnull - private static UnscheduledTrip trip(List stopTimes) { - return new TestCase.Builder(FIRST, THIRD).withStopTimes(stopTimes).build().trip(); - } + private static StopTime area(String startTime, String endTime) { + return area(AREA_STOP, endTime, startTime); + } - @Nonnull - private static List accessTemplates(UnscheduledTrip trip) { - return trip - .getFlexAccessTemplates(NEARBY_STOP, FLEX_SERVICE_DATE, CALCULATOR, FlexConfig.DEFAULT) - .toList(); - } + private static StopTime area(StopLocation areaStop, String endTime, String startTime) { + var stopTime = new StopTime(); + stopTime.setStop(areaStop); + stopTime.setFlexWindowStart(TimeUtils.time(startTime)); + stopTime.setFlexWindowEnd(TimeUtils.time(endTime)); + return stopTime; + } - @Nonnull - private static List egressTemplates(UnscheduledTrip trip) { - return trip - .getFlexEgressTemplates(NEARBY_STOP, FLEX_SERVICE_DATE, CALCULATOR, FlexConfig.DEFAULT) - .toList(); - } + private static StopTime regularDeparture(String departureTime) { + return regularStopTime(MISSING_VALUE, TimeUtils.time(departureTime)); } - private static String timeToString(int time) { - return TimeUtils.timeToStrCompact(time, MISSING_VALUE, "MISSING_VALUE"); + private static StopTime regularArrival(String arrivalTime) { + return regularStopTime(TimeUtils.time(arrivalTime), MISSING_VALUE); } - @Nonnull - private static NearbyStop nearbyStop(AreaStop stop) { - return new NearbyStop(stop, 1000, List.of(), null); + private static StopTime regularStopTime(int arrivalTime, int departureTime) { + var stopTime = new StopTime(); + stopTime.setStop(REGULAR_STOP); + stopTime.setArrivalTime(arrivalTime); + stopTime.setDepartureTime(departureTime); + return stopTime; } record TestCase( diff --git a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopPropertyMapperTest.java b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopPropertyMapperTest.java index 2cf0804b630..5387df91bca 100644 --- a/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopPropertyMapperTest.java +++ b/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/areastops/AreaStopPropertyMapperTest.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.Locale; import org.junit.jupiter.api.Test; -import org.opentripplanner._support.geometry.Polygons; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.site.AreaStop; @@ -15,7 +14,7 @@ class AreaStopPropertyMapperTest { private static final TransitModelForTest MODEL = new TransitModelForTest(StopModel.of()); - private static final AreaStop STOP = MODEL.areaStopForTest("123", Polygons.BERLIN); + private static final AreaStop STOP = MODEL.areaStop("123").build(); private static final Route ROUTE_WITH_COLOR = TransitModelForTest .route("123") .withColor("ffffff") diff --git a/src/ext/java/org/opentripplanner/ext/flex/FlexAccessEgress.java b/src/ext/java/org/opentripplanner/ext/flex/FlexAccessEgress.java index 26833f67a6a..f464f1e1907 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/FlexAccessEgress.java +++ b/src/ext/java/org/opentripplanner/ext/flex/FlexAccessEgress.java @@ -2,47 +2,49 @@ import static org.opentripplanner.model.StopTime.MISSING_VALUE; +import java.util.Objects; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.street.search.state.State; import org.opentripplanner.transit.model.site.RegularStop; +import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; public final class FlexAccessEgress { private final RegularStop stop; private final FlexPathDurations pathDurations; - private final int fromStopIndex; - private final int toStopIndex; - private final FlexTrip trip; + private final int boardStopPosition; + private final int alightStopPosition; + private final FlexTrip trip; private final State lastState; private final boolean stopReachedOnBoard; + private final RoutingBookingInfo routingBookingInfo; public FlexAccessEgress( RegularStop stop, FlexPathDurations pathDurations, - int fromStopIndex, - int toStopIndex, - FlexTrip trip, + int boardStopPosition, + int alightStopPosition, + FlexTrip trip, State lastState, - boolean stopReachedOnBoard + boolean stopReachedOnBoard, + int requestedBookingTime ) { this.stop = stop; this.pathDurations = pathDurations; - this.fromStopIndex = fromStopIndex; - this.toStopIndex = toStopIndex; - this.trip = trip; + this.boardStopPosition = boardStopPosition; + this.alightStopPosition = alightStopPosition; + this.trip = Objects.requireNonNull(trip); this.lastState = lastState; this.stopReachedOnBoard = stopReachedOnBoard; + this.routingBookingInfo = + RoutingBookingInfo.of(requestedBookingTime, trip.getPickupBookingInfo(boardStopPosition)); } public RegularStop stop() { return stop; } - public FlexTrip trip() { - return trip; - } - public State lastState() { return lastState; } @@ -52,11 +54,15 @@ public boolean stopReachedOnBoard() { } public int earliestDepartureTime(int departureTime) { - int requestedDepartureTime = pathDurations.mapToFlexTripDepartureTime(departureTime); + int tripDepartureTime = pathDurations.mapToFlexTripDepartureTime(departureTime); + + // Apply minimum-booking-notice + tripDepartureTime = routingBookingInfo.earliestDepartureTime(tripDepartureTime); + int earliestDepartureTime = trip.earliestDepartureTime( - requestedDepartureTime, - fromStopIndex, - toStopIndex, + tripDepartureTime, + boardStopPosition, + alightStopPosition, pathDurations.trip() ); if (earliestDepartureTime == MISSING_VALUE) { @@ -66,16 +72,19 @@ public int earliestDepartureTime(int departureTime) { } public int latestArrivalTime(int arrivalTime) { - int requestedArrivalTime = pathDurations.mapToFlexTripArrivalTime(arrivalTime); + int tripArrivalTime = pathDurations.mapToFlexTripArrivalTime(arrivalTime); int latestArrivalTime = trip.latestArrivalTime( - requestedArrivalTime, - fromStopIndex, - toStopIndex, + tripArrivalTime, + boardStopPosition, + alightStopPosition, pathDurations.trip() ); if (latestArrivalTime == MISSING_VALUE) { return MISSING_VALUE; } + if (routingBookingInfo.exceedsMinimumBookingNotice(latestArrivalTime - pathDurations.trip())) { + return MISSING_VALUE; + } return pathDurations.mapToRouterArrivalTime(latestArrivalTime); } @@ -83,11 +92,11 @@ public int latestArrivalTime(int arrivalTime) { public String toString() { return ToStringBuilder .of(FlexAccessEgress.class) - .addNum("fromStopIndex", fromStopIndex) - .addNum("toStopIndex", toStopIndex) + .addNum("boardStopPosition", boardStopPosition) + .addNum("alightStopPosition", alightStopPosition) .addObj("durations", pathDurations) .addObj("stop", stop) - .addObj("trip", trip) + .addObj("trip", trip.getId()) .addObj("lastState", lastState) .addBoolIfTrue("stopReachedOnBoard", stopReachedOnBoard) .toString(); diff --git a/src/ext/java/org/opentripplanner/ext/flex/FlexParameters.java b/src/ext/java/org/opentripplanner/ext/flex/FlexParameters.java new file mode 100644 index 00000000000..564991a94a0 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/flex/FlexParameters.java @@ -0,0 +1,57 @@ +package org.opentripplanner.ext.flex; + +import java.time.Duration; + +/** + * Define parameters used to configure flex. For further documentation on these parameters, look + * at the {@link org.opentripplanner.standalone.config.sandbox.FlexConfig} class which implements + * this interface. The flex package does not use all parameters defined here. Some parameters are + * passed into the street search(AStar) as part of a flex use-case. We keep them here for + * completeness and simplicity (just one interface). + */ +public interface FlexParameters { + /** + * See {@link org.opentripplanner.standalone.config.sandbox.FlexConfig} + */ + Duration maxTransferDuration(); + /** + * See {@link org.opentripplanner.standalone.config.sandbox.FlexConfig} + */ + Duration maxFlexTripDuration(); + /** + * See {@link org.opentripplanner.standalone.config.sandbox.FlexConfig} + */ + Duration maxAccessWalkDuration(); + /** + * See {@link org.opentripplanner.standalone.config.sandbox.FlexConfig} + */ + Duration maxEgressWalkDuration(); + + /** + * This defines the default values. This will be used by the OTP configuration and by tests, + * avoid using this directly. + */ + static FlexParameters defaultValues() { + return new FlexParameters() { + @Override + public Duration maxTransferDuration() { + return Duration.ofMinutes(5); + } + + @Override + public Duration maxFlexTripDuration() { + return Duration.ofMinutes(45); + } + + @Override + public Duration maxAccessWalkDuration() { + return Duration.ofMinutes(45); + } + + @Override + public Duration maxEgressWalkDuration() { + return Duration.ofMinutes(45); + } + }; + } +} diff --git a/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java b/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java index c84cb8823a7..84098db9dc9 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java +++ b/src/ext/java/org/opentripplanner/ext/flex/FlexRouter.java @@ -1,33 +1,35 @@ package org.opentripplanner.ext.flex; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; -import java.util.Comparator; import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.opentripplanner.astar.model.GraphPath; import org.opentripplanner.ext.flex.flexpathcalculator.DirectFlexPathCalculator; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; import org.opentripplanner.ext.flex.flexpathcalculator.StreetFlexPathCalculator; -import org.opentripplanner.ext.flex.template.FlexAccessTemplate; -import org.opentripplanner.ext.flex.template.FlexEgressTemplate; +import org.opentripplanner.ext.flex.template.DirectFlexPath; +import org.opentripplanner.ext.flex.template.FlexAccessEgressCallbackAdapter; +import org.opentripplanner.ext.flex.template.FlexAccessFactory; +import org.opentripplanner.ext.flex.template.FlexDirectPathFactory; +import org.opentripplanner.ext.flex.template.FlexEgressFactory; +import org.opentripplanner.ext.flex.template.FlexServiceDate; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.framework.time.ServiceDateUtils; +import org.opentripplanner.model.PathTransfer; import org.opentripplanner.model.plan.Itinerary; import org.opentripplanner.routing.algorithm.mapping.GraphPathToItineraryMapper; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graphfinder.NearbyStop; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; +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 { @@ -36,31 +38,27 @@ public class FlexRouter { private final Graph graph; private final TransitService transitService; - private final FlexConfig config; + private final FlexParameters flexParameters; private final Collection streetAccesses; private final Collection streetEgresses; private final FlexIndex flexIndex; private final FlexPathCalculator accessFlexPathCalculator; private final FlexPathCalculator egressFlexPathCalculator; private final GraphPathToItineraryMapper graphPathToItineraryMapper; + private final FlexAccessEgressCallbackAdapter callbackService; /* Request data */ private final ZonedDateTime startOfTime; - private final int departureTime; - private final boolean arriveBy; - - private final FlexServiceDate[] dates; - - /* State */ - private List flexAccessTemplates = null; - private List flexEgressTemplates = null; + private final int requestedTime; + private final int requestedBookingTime; + private final List dates; public FlexRouter( Graph graph, TransitService transitService, - FlexConfig config, - Instant searchInstant, - boolean arriveBy, + FlexParameters flexParameters, + Instant requestedTime, + @Nullable Instant requestedBookingTime, int additionalPastSearchDays, int additionalFutureSearchDays, Collection streetAccesses, @@ -68,10 +66,11 @@ public FlexRouter( ) { this.graph = graph; this.transitService = transitService; - this.config = config; + this.flexParameters = flexParameters; this.streetAccesses = streetAccesses; this.streetEgresses = egressTransfers; this.flexIndex = transitService.getFlexIndex(); + this.callbackService = new CallbackAdapter(); this.graphPathToItineraryMapper = new GraphPathToItineraryMapper( transitService.getTimeZone(), @@ -81,9 +80,9 @@ public FlexRouter( if (graph.hasStreets) { this.accessFlexPathCalculator = - new StreetFlexPathCalculator(false, config.maxFlexTripDuration()); + new StreetFlexPathCalculator(false, flexParameters.maxFlexTripDuration()); this.egressFlexPathCalculator = - new StreetFlexPathCalculator(true, config.maxFlexTripDuration()); + new StreetFlexPathCalculator(true, flexParameters.maxFlexTripDuration()); } else { // this is only really useful in tests. in real world scenarios you're unlikely to get useful // results if you don't have streets @@ -92,161 +91,124 @@ public FlexRouter( } ZoneId tz = transitService.getTimeZone(); - LocalDate searchDate = LocalDate.ofInstant(searchInstant, tz); + LocalDate searchDate = LocalDate.ofInstant(requestedTime, tz); this.startOfTime = ServiceDateUtils.asStartOfService(searchDate, tz); - this.departureTime = ServiceDateUtils.secondsSinceStartOfTime(startOfTime, searchInstant); - this.arriveBy = arriveBy; + this.requestedTime = ServiceDateUtils.secondsSinceStartOfTime(startOfTime, requestedTime); + this.requestedBookingTime = + requestedBookingTime == null + ? RoutingBookingInfo.NOT_SET + : ServiceDateUtils.secondsSinceStartOfTime(startOfTime, requestedBookingTime); + this.dates = + createFlexServiceDates( + transitService, + additionalPastSearchDays, + additionalFutureSearchDays, + searchDate + ); + } - int totalDays = additionalPastSearchDays + 1 + additionalFutureSearchDays; + public List createFlexOnlyItineraries(boolean arriveBy) { + OTPRequestTimeoutException.checkForTimeout(); - this.dates = new FlexServiceDate[totalDays]; + var directFlexPaths = new FlexDirectPathFactory( + callbackService, + accessFlexPathCalculator, + egressFlexPathCalculator, + flexParameters.maxTransferDuration() + ) + .calculateDirectFlexPaths(streetAccesses, streetEgresses, dates, requestedTime, arriveBy); - for (int d = -additionalPastSearchDays; d <= additionalFutureSearchDays; ++d) { - LocalDate date = searchDate.plusDays(d); - int index = d + additionalPastSearchDays; - dates[index] = - new FlexServiceDate( - date, - ServiceDateUtils.secondsSinceStartOfTime(startOfTime, date), - transitService.getServiceCodesRunningForDate(date) - ); - } - } + var itineraries = new ArrayList(); - public Collection createFlexOnlyItineraries() { - OTPRequestTimeoutException.checkForTimeout(); - calculateFlexAccessTemplates(); - calculateFlexEgressTemplates(); - - Multimap streetEgressByStop = HashMultimap.create(); - streetEgresses.forEach(it -> streetEgressByStop.put(it.stop, it)); - - Collection itineraries = new ArrayList<>(); - - for (FlexAccessTemplate template : this.flexAccessTemplates) { - StopLocation transferStop = template.getTransferStop(); - if ( - this.flexEgressTemplates.stream() - .anyMatch(t -> t.getAccessEgressStop().equals(transferStop)) - ) { - for (NearbyStop egress : streetEgressByStop.get(transferStop)) { - Itinerary itinerary = template.createDirectGraphPath( - egress, - arriveBy, - departureTime, - startOfTime, - graphPathToItineraryMapper - ); - if (itinerary != null) { - itineraries.add(itinerary); - } - } + for (DirectFlexPath it : directFlexPaths) { + var startTime = startOfTime.plusSeconds(it.startTime()); + var itinerary = graphPathToItineraryMapper + .generateItinerary(new GraphPath<>(it.state())) + .withTimeShiftToStartAt(startTime); + + if (itinerary != null) { + itineraries.add(itinerary); } } - return itineraries; } public Collection createFlexAccesses() { OTPRequestTimeoutException.checkForTimeout(); - calculateFlexAccessTemplates(); - return this.flexAccessTemplates.stream() - .flatMap(template -> template.createFlexAccessEgressStream(graph, transitService)) - .toList(); + return new FlexAccessFactory( + callbackService, + accessFlexPathCalculator, + flexParameters.maxTransferDuration() + ) + .createFlexAccesses(streetAccesses, dates); } public Collection createFlexEgresses() { OTPRequestTimeoutException.checkForTimeout(); - calculateFlexEgressTemplates(); - - return this.flexEgressTemplates.stream() - .flatMap(template -> template.createFlexAccessEgressStream(graph, transitService)) - .toList(); + return new FlexEgressFactory( + callbackService, + egressFlexPathCalculator, + flexParameters.maxTransferDuration() + ) + .createFlexEgresses(streetEgresses, dates); } - private void calculateFlexAccessTemplates() { - if (this.flexAccessTemplates != null) { - return; - } + private List createFlexServiceDates( + TransitService transitService, + int additionalPastSearchDays, + int additionalFutureSearchDays, + LocalDate searchDate + ) { + final List dates = new ArrayList<>(); - // Fetch the closest flexTrips reachable from the access stops - this.flexAccessTemplates = - getClosestFlexTrips(streetAccesses, true) - // For each date the router has data for - .flatMap(it -> - Arrays - .stream(dates) - // Discard if service is not running on date - .filter(date -> date.isFlexTripRunning(it.flexTrip(), this.transitService)) - // Create templates from trip, boarding at the nearbyStop - .flatMap(date -> - it - .flexTrip() - .getFlexAccessTemplates(it.accessEgress(), date, accessFlexPathCalculator, config) - ) + // TODO - This code id not DRY, the same logic is in RaptorRoutingRequestTransitDataCreator + for (int d = -additionalPastSearchDays; d <= additionalFutureSearchDays; ++d) { + LocalDate date = searchDate.plusDays(d); + dates.add( + new FlexServiceDate( + date, + ServiceDateUtils.secondsSinceStartOfTime(startOfTime, date), + requestedBookingTime, + transitService.getServiceCodesRunningForDate(date) ) - .toList(); + ); + } + return List.copyOf(dates); } - private void calculateFlexEgressTemplates() { - if (this.flexEgressTemplates != null) { - return; + /** + * This class work as an adaptor around OTP services. This allows us to pass in this instance + * and not the implementations (graph, transitService, flexIndex). We can easily mock this in + * unit-tests. This also serves as documentation of which services the flex access/egress + * generation logic needs. + */ + private class CallbackAdapter implements FlexAccessEgressCallbackAdapter { + + @Override + public TransitStopVertex getStopVertexForStopId(FeedScopedId stopId) { + return graph.getStopVertexForStopId(stopId); } - // Fetch the closest flexTrips reachable from the egress stops - this.flexEgressTemplates = - getClosestFlexTrips(streetEgresses, false) - // For each date the router has data for - .flatMap(it -> - Arrays - .stream(dates) - // Discard if service is not running on date - .filter(date -> date.isFlexTripRunning(it.flexTrip(), this.transitService)) - // Create templates from trip, alighting at the nearbyStop - .flatMap(date -> - it - .flexTrip() - .getFlexEgressTemplates(it.accessEgress(), date, egressFlexPathCalculator, config) - ) - ) - .toList(); - } + @Override + public Collection getTransfersFromStop(StopLocation stop) { + return transitService.getTransfersByStop(stop); + } - private Stream getClosestFlexTrips( - Collection nearbyStops, - boolean pickup - ) { - // Find all trips reachable from the nearbyStops - Stream flexTripsReachableFromNearbyStops = nearbyStops - .stream() - .flatMap(accessEgress -> - flexIndex - .getFlexTripsByStop(accessEgress.stop) - .stream() - .filter(flexTrip -> - pickup - ? flexTrip.isBoardingPossible(accessEgress) - : flexTrip.isAlightingPossible(accessEgress) - ) - .map(flexTrip -> new AccessEgressAndNearbyStop(accessEgress, flexTrip)) - ); + @Override + public Collection getTransfersToStop(StopLocation stop) { + return transitService.getFlexIndex().getTransfersToStop(stop); + } - // Group all (NearbyStop, FlexTrip) tuples by flexTrip - Collection> groupedReachableFlexTrips = flexTripsReachableFromNearbyStops - .collect(Collectors.groupingBy(AccessEgressAndNearbyStop::flexTrip)) - .values(); - - // Get the tuple with least walking time from each group - return groupedReachableFlexTrips - .stream() - .map(t2s -> - t2s - .stream() - .min(Comparator.comparingLong(t2 -> t2.accessEgress().state.getElapsedTimeSeconds())) - ) - .flatMap(Optional::stream); - } + @Override + public Collection> getFlexTripsByStop(StopLocation stopLocation) { + return flexIndex.getFlexTripsByStop(stopLocation); + } - private record AccessEgressAndNearbyStop(NearbyStop accessEgress, FlexTrip flexTrip) {} + @Override + public boolean isDateActive(FlexServiceDate date, FlexTrip trip) { + int serviceCode = transitService.getServiceCodeForId(trip.getTrip().getServiceId()); + return date.isTripServiceRunning(serviceCode); + } + } } diff --git a/src/ext/java/org/opentripplanner/ext/flex/FlexServiceDate.java b/src/ext/java/org/opentripplanner/ext/flex/FlexServiceDate.java deleted file mode 100644 index d2ead1c80be..00000000000 --- a/src/ext/java/org/opentripplanner/ext/flex/FlexServiceDate.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.opentripplanner.ext.flex; - -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 - * made on. - */ -public class FlexServiceDate { - - /** The local date */ - public 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; - - /** Which services are running on the date. */ - public final TIntSet servicesRunning; - - public FlexServiceDate( - LocalDate serviceDate, - int secondsFromStartOfTime, - TIntSet servicesRunning - ) { - this.serviceDate = serviceDate; - this.secondsFromStartOfTime = secondsFromStartOfTime; - this.servicesRunning = servicesRunning; - } - - boolean isFlexTripRunning(FlexTrip flexTrip, TransitService transitService) { - return ( - servicesRunning != null && - servicesRunning.contains( - transitService.getServiceCodeForId(flexTrip.getTrip().getServiceId()) - ) - ); - } -} diff --git a/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java b/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java index b9e10e29214..0544a46da72 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java +++ b/src/ext/java/org/opentripplanner/ext/flex/FlexibleTransitLeg.java @@ -12,7 +12,6 @@ import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.lang.DoubleUtils; import org.opentripplanner.framework.tostring.ToStringBuilder; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.fare.FareProductUse; import org.opentripplanner.model.plan.Leg; @@ -28,6 +27,7 @@ import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.organization.Operator; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; /** * One leg of a trip -- that is, a temporally continuous piece of the journey that takes place on a @@ -133,17 +133,17 @@ public I18NString getHeadsign() { @Override public LocalDate getServiceDate() { - return edge.flexTemplate.serviceDate; + return edge.serviceDate(); } @Override public Place getFrom() { - return Place.forFlexStop(edge.s1, edge.getFromVertex()); + return Place.forFlexStop(edge.s1(), edge.getFromVertex()); } @Override public Place getTo() { - return Place.forFlexStop(edge.s2, edge.getToVertex()); + return Place.forFlexStop(edge.s2(), edge.getToVertex()); } @Override @@ -173,22 +173,22 @@ public PickDrop getAlightRule() { @Override public BookingInfo getDropOffBookingInfo() { - return edge.getFlexTrip().getDropOffBookingInfo(getBoardStopPosInPattern()); + return edge.getFlexTrip().getDropOffBookingInfo(getAlightStopPosInPattern()); } @Override public BookingInfo getPickupBookingInfo() { - return edge.getFlexTrip().getPickupBookingInfo(getAlightStopPosInPattern()); + return edge.getFlexTrip().getPickupBookingInfo(getBoardStopPosInPattern()); } @Override public Integer getBoardStopPosInPattern() { - return edge.flexTemplate.fromStopIndex; + return edge.boardStopPosInPattern(); } @Override public Integer getAlightStopPosInPattern() { - return edge.flexTemplate.toStopIndex; + return edge.alightStopPosInPattern(); } @Override diff --git a/src/ext/java/org/opentripplanner/ext/flex/edgetype/FlexTripEdge.java b/src/ext/java/org/opentripplanner/ext/flex/edgetype/FlexTripEdge.java index acf2f8f95dc..40201295d7b 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/edgetype/FlexTripEdge.java +++ b/src/ext/java/org/opentripplanner/ext/flex/edgetype/FlexTripEdge.java @@ -1,10 +1,10 @@ package org.opentripplanner.ext.flex.edgetype; +import java.time.LocalDate; import java.util.Objects; import javax.annotation.Nonnull; import org.locationtech.jts.geom.LineString; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPath; -import org.opentripplanner.ext.flex.template.FlexAccessEgressTemplate; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.street.model.edge.Edge; @@ -14,63 +14,66 @@ import org.opentripplanner.street.search.state.StateEditor; import org.opentripplanner.transit.model.site.StopLocation; +/** + * Flex trips edges are not connected to the graph. + */ public class FlexTripEdge extends Edge { - private final FlexTrip trip; - public final StopLocation s1; - public final StopLocation s2; - public final FlexAccessEgressTemplate flexTemplate; - public final FlexPath flexPath; + private final StopLocation s1; + private final StopLocation s2; + private final FlexTrip trip; + private final int boardStopPosInPattern; + private final int alightStopPosInPattern; + private final LocalDate serviceDate; + private final FlexPath flexPath; - private FlexTripEdge( + public FlexTripEdge( Vertex v1, Vertex v2, StopLocation s1, StopLocation s2, - FlexTrip trip, - FlexAccessEgressTemplate flexTemplate, + FlexTrip trip, + int boardStopPosInPattern, + int alightStopPosInPattern, + LocalDate serviceDate, FlexPath flexPath ) { super(v1, v2); this.s1 = s1; this.s2 = s2; this.trip = trip; - this.flexTemplate = flexTemplate; + this.boardStopPosInPattern = boardStopPosInPattern; + this.alightStopPosInPattern = alightStopPosInPattern; + this.serviceDate = serviceDate; this.flexPath = Objects.requireNonNull(flexPath); } - /** - * Create a Flex Trip. - * Flex trips are not connected to the graph. - */ - public static FlexTripEdge createFlexTripEdge( - Vertex v1, - Vertex v2, - StopLocation s1, - StopLocation s2, - FlexTrip trip, - FlexAccessEgressTemplate flexTemplate, - FlexPath flexPath - ) { - return new FlexTripEdge(v1, v2, s1, s2, trip, flexTemplate, flexPath); + public StopLocation s1() { + return s1; + } + + public StopLocation s2() { + return s2; + } + + public int boardStopPosInPattern() { + return boardStopPosInPattern; + } + + public int alightStopPosInPattern() { + return alightStopPosInPattern; + } + + public LocalDate serviceDate() { + return serviceDate; } public int getTimeInSeconds() { return flexPath.durationSeconds; } - @Override - @Nonnull - public State[] traverse(State s0) { - StateEditor editor = s0.edit(this); - editor.setBackMode(TraverseMode.FLEX); - // TODO: decide good value - editor.incrementWeight(10 * 60); - int timeInSeconds = getTimeInSeconds(); - editor.incrementTimeInSeconds(timeInSeconds); - editor.incrementWeight(timeInSeconds); - editor.resetEnteredNoThroughTrafficArea(); - return editor.makeStateArray(); + public FlexTrip getFlexTrip() { + return trip; } @Override @@ -88,7 +91,17 @@ public double getDistanceMeters() { return flexPath.distanceMeters; } - public FlexTrip getFlexTrip() { - return trip; + @Override + @Nonnull + public State[] traverse(State s0) { + StateEditor editor = s0.edit(this); + editor.setBackMode(TraverseMode.FLEX); + // TODO: decide good value + editor.incrementWeight(10 * 60); + int timeInSeconds = getTimeInSeconds(); + editor.incrementTimeInSeconds(timeInSeconds); + editor.incrementWeight(timeInSeconds); + editor.resetEnteredNoThroughTrafficArea(); + return editor.makeStateArray(); } } diff --git a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/DirectFlexPathCalculator.java b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/DirectFlexPathCalculator.java index 9a5b71257d0..793cd112444 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/DirectFlexPathCalculator.java +++ b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/DirectFlexPathCalculator.java @@ -22,7 +22,12 @@ public DirectFlexPathCalculator() { } @Override - public FlexPath calculateFlexPath(Vertex fromv, Vertex tov, int fromStopIndex, int toStopIndex) { + public FlexPath calculateFlexPath( + Vertex fromv, + Vertex tov, + int boardStopPosition, + int alightStopPosition + ) { double distance = SphericalDistanceLibrary.distance(fromv.getCoordinate(), tov.getCoordinate()); LineString geometry = GeometryUtils .getGeometryFactory() diff --git a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPathCalculator.java b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPathCalculator.java index 98e218a1c0d..77dbc703424 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPathCalculator.java +++ b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/FlexPathCalculator.java @@ -8,5 +8,10 @@ */ public interface FlexPathCalculator { @Nullable - FlexPath calculateFlexPath(Vertex fromv, Vertex tov, int fromStopIndex, int toStopIndex); + FlexPath calculateFlexPath( + Vertex fromv, + Vertex tov, + int boardStopPosition, + int alightStopPosition + ); } diff --git a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculator.java b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculator.java index cd5228dada5..7b4f6ecaf8b 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculator.java +++ b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculator.java @@ -13,26 +13,31 @@ public class ScheduledFlexPathCalculator implements FlexPathCalculator { private final FlexPathCalculator flexPathCalculator; private final FlexTrip trip; - public ScheduledFlexPathCalculator(FlexPathCalculator flexPathCalculator, FlexTrip trip) { + public ScheduledFlexPathCalculator(FlexPathCalculator flexPathCalculator, FlexTrip trip) { this.flexPathCalculator = flexPathCalculator; this.trip = trip; } @Override - public FlexPath calculateFlexPath(Vertex fromv, Vertex tov, int fromStopIndex, int toStopIndex) { + public FlexPath calculateFlexPath( + Vertex fromv, + Vertex tov, + int boardStopPosition, + int alightStopPosition + ) { final var flexPath = flexPathCalculator.calculateFlexPath( fromv, tov, - fromStopIndex, - toStopIndex + boardStopPosition, + alightStopPosition ); if (flexPath == null) { return null; } int departureTime = trip.earliestDepartureTime( Integer.MIN_VALUE, - fromStopIndex, - toStopIndex, + boardStopPosition, + alightStopPosition, 0 ); @@ -40,7 +45,12 @@ public FlexPath calculateFlexPath(Vertex fromv, Vertex tov, int fromStopIndex, i return null; } - int arrivalTime = trip.latestArrivalTime(Integer.MAX_VALUE, fromStopIndex, toStopIndex, 0); + int arrivalTime = trip.latestArrivalTime( + Integer.MAX_VALUE, + boardStopPosition, + alightStopPosition, + 0 + ); if (arrivalTime == MISSING_VALUE) { return null; diff --git a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/StreetFlexPathCalculator.java b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/StreetFlexPathCalculator.java index 9870f7013a5..597afd9b17a 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/StreetFlexPathCalculator.java +++ b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/StreetFlexPathCalculator.java @@ -42,7 +42,12 @@ public StreetFlexPathCalculator(boolean reverseDirection, Duration maxFlexTripDu } @Override - public FlexPath calculateFlexPath(Vertex fromv, Vertex tov, int fromStopIndex, int toStopIndex) { + public FlexPath calculateFlexPath( + Vertex fromv, + Vertex tov, + int boardStopPosition, + int alightStopPosition + ) { // These are the origin and destination vertices from the perspective of the one-to-many search, // which may be reversed Vertex originVertex = reverseDirection ? tov : fromv; diff --git a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculator.java b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculator.java index a2252f3fec8..e7a26ec1697 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculator.java +++ b/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/TimePenaltyCalculator.java @@ -20,8 +20,13 @@ public TimePenaltyCalculator(FlexPathCalculator delegate, TimePenalty penalty) { @Nullable @Override - public FlexPath calculateFlexPath(Vertex fromv, Vertex tov, int fromStopIndex, int toStopIndex) { - var path = delegate.calculateFlexPath(fromv, tov, fromStopIndex, toStopIndex); + public FlexPath calculateFlexPath( + Vertex fromv, + Vertex tov, + int boardStopPosition, + int alightStopPosition + ) { + var path = delegate.calculateFlexPath(fromv, tov, boardStopPosition, alightStopPosition); if (path == null) { return null; diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessEgressTemplate.java b/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java similarity index 63% rename from src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessEgressTemplate.java rename to src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java index 68f490f775f..9ce9cf8a4c4 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessEgressTemplate.java +++ b/src/ext/java/org/opentripplanner/ext/flex/template/AbstractFlexTemplate.java @@ -1,5 +1,6 @@ package org.opentripplanner.ext.flex.template; +import java.time.Duration; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; @@ -9,23 +10,18 @@ import javax.annotation.Nullable; import org.opentripplanner.ext.flex.FlexAccessEgress; import org.opentripplanner.ext.flex.FlexPathDurations; -import org.opentripplanner.ext.flex.FlexServiceDate; 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.tostring.ToStringBuilder; import org.opentripplanner.model.PathTransfer; -import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graphfinder.NearbyStop; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.street.model.edge.Edge; -import org.opentripplanner.street.model.vertex.TransitStopVertex; import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.street.search.state.EdgeTraverser; 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.service.TransitService; /** * A container for a few pieces of information that can be used to calculate flex accesses, egresses, @@ -33,90 +29,96 @@ *

* Please also see Flex.svg for an illustration of how the flex concepts relate to each other. */ -public abstract class FlexAccessEgressTemplate { +abstract class AbstractFlexTemplate { /** * We do not want extremely short flex trips, they will normally be dominated in the * routing later. We set an absolute min duration to 10 seconds (167m with 60 km/h). */ private static final int MIN_FLEX_TRIP_DURATION_SECONDS = 10; + + // TODO - This is confusing, and not following OO principles. The from/to stop + // - changes type for access/egress, move them down into child class. + // - this apply to transferStop as well. protected final NearbyStop accessEgress; - protected final FlexTrip trip; - public final int fromStopIndex; - public final int toStopIndex; + protected final FlexTrip trip; + protected final int boardStopPosition; + protected final int alightStopPosition; protected final StopLocation transferStop; protected final int secondsFromStartOfTime; - public final LocalDate serviceDate; + protected final LocalDate serviceDate; + protected final int requestedBookingTime; protected final FlexPathCalculator calculator; - private final FlexConfig flexConfig; + private final Duration maxTransferDuration; /** - * @param accessEgress Path from origin to the point of boarding for this flex trip - * @param trip The FlexTrip used for this Template - * @param fromStopIndex Stop sequence index where this FlexTrip is boarded - * @param toStopIndex The stop where this FlexTrip alights - * @param transferStop The stop location where this FlexTrip alights - * @param date The service date of this FlexTrip - * @param calculator Calculates the path and duration of the FlexTrip + * @param trip The FlexTrip used for this template + * @param accessEgress Path from origin/destination to the point of boarding/alighting for + * this flex trip + * @param transferStop The stop location where this FlexTrip transfers to another transit + * service. + * @param boardStopPosition The stop-board-position in the trip pattern + * @param alightStopPosition The stop-alight-position in the trip pattern + * @param date The service date of this FlexTrip + * @param calculator Calculates the path and duration of the FlexTrip + * @param maxTransferDuration The limit for how long a transfer is allowed to be */ - FlexAccessEgressTemplate( + AbstractFlexTemplate( + FlexTrip trip, NearbyStop accessEgress, - FlexTrip trip, - int fromStopIndex, - int toStopIndex, StopLocation transferStop, + int boardStopPosition, + int alightStopPosition, FlexServiceDate date, FlexPathCalculator calculator, - FlexConfig config + Duration maxTransferDuration ) { this.accessEgress = accessEgress; this.trip = trip; - this.fromStopIndex = fromStopIndex; - this.toStopIndex = toStopIndex; + this.boardStopPosition = boardStopPosition; + this.alightStopPosition = 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.flexConfig = config; + this.maxTransferDuration = maxTransferDuration; } - public StopLocation getTransferStop() { + StopLocation getTransferStop() { return transferStop; } - public StopLocation getAccessEgressStop() { + StopLocation getAccessEgressStop() { return accessEgress.stop; } /** - * This method is very much the hot code path in the flex access/egress search so any optimization - * here will lead to noticeable speedups. + * This method is very much the hot code path in the flex access/egress search, so any + * optimization here will lead to noticeable speedups. */ - public Stream createFlexAccessEgressStream( - Graph graph, - TransitService transitService - ) { + Stream createFlexAccessEgressStream(FlexAccessEgressCallbackAdapter callback) { if (transferStop instanceof RegularStop stop) { - TransitStopVertex flexVertex = graph.getStopVertexForStopId(stop.getId()); + var flexVertex = callback.getStopVertexForStopId(stop.getId()); return Stream - .of(getFlexAccessEgress(new ArrayList<>(), flexVertex, (RegularStop) transferStop)) + .of(createFlexAccessEgress(new ArrayList<>(), flexVertex, stop)) .filter(Objects::nonNull); } // transferStop is Location Area/Line else { double maxDistanceMeters = - flexConfig.maxTransferDuration().getSeconds() * + maxTransferDuration.getSeconds() * accessEgress.state.getRequest().preferences().walk().speed(); - return getTransfersFromTransferStop(transitService) + return getTransfersFromTransferStop(callback) .stream() .filter(pathTransfer -> pathTransfer.getDistanceMeters() <= maxDistanceMeters) .filter(transfer -> getFinalStop(transfer) != null) .map(transfer -> { - List edges = getTransferEdges(transfer); - Vertex flexVertex = getFlexVertex(edges.get(0)); - RegularStop finalStop = getFinalStop(transfer); - return getFlexAccessEgress(edges, flexVertex, finalStop); + var edges = getTransferEdges(transfer); + var flexVertex = getFlexVertex(edges.get(0)); + var finalStop = getFinalStop(transfer); + return createFlexAccessEgress(edges, flexVertex, finalStop); }) .filter(Objects::nonNull); } @@ -125,16 +127,16 @@ public Stream createFlexAccessEgressStream( @Override public String toString() { return ToStringBuilder - .of(FlexAccessEgressTemplate.class) + .of(AbstractFlexTemplate.class) .addObj("accessEgress", accessEgress) .addObj("trip", trip) - .addNum("fromStopIndex", fromStopIndex) - .addNum("toStopIndex", toStopIndex) + .addNum("boardStopPosition", boardStopPosition) + .addNum("alightStopPosition", alightStopPosition) .addObj("transferStop", transferStop) .addServiceTime("secondsFromStartOfTime", secondsFromStartOfTime) .addDate("serviceDate", serviceDate) .addObj("calculator", calculator) - .addObj("flexConfig", flexConfig) + .addDuration("maxTransferDuration", maxTransferDuration) .toString(); } @@ -154,7 +156,7 @@ public String toString() { * flex ride for the access/egress. */ protected abstract Collection getTransfersFromTransferStop( - TransitService transitService + FlexAccessEgressCallbackAdapter callback ); /** @@ -177,21 +179,21 @@ protected abstract FlexPathDurations calculateFlexPathDurations( protected abstract FlexTripEdge getFlexEdge(Vertex flexFromVertex, StopLocation transferStop); @Nullable - protected FlexAccessEgress getFlexAccessEgress( + private FlexAccessEgress createFlexAccessEgress( List transferEdges, Vertex flexVertex, RegularStop stop ) { var flexEdge = getFlexEdge(flexVertex, transferStop); - // Drop none routable and very short(<10s) trips + // Drop non-routable and very short(<10s) trips if (flexEdge == null || flexEdge.getTimeInSeconds() < MIN_FLEX_TRIP_DURATION_SECONDS) { return null; } // this code is a little repetitive but needed as a performance improvement. previously // the flex path was checked before this method was called. this meant that every path - // was traversed twice leading to a noticeable slowdown. + // was traversed twice, leading to a noticeable slowdown. final var afterFlexState = flexEdge.traverse(accessEgress.state); if (State.isEmpty(afterFlexState)) { return null; @@ -206,11 +208,12 @@ protected FlexAccessEgress getFlexAccessEgress( return new FlexAccessEgress( stop, durations, - fromStopIndex, - toStopIndex, + boardStopPosition, + alightStopPosition, 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 new file mode 100644 index 00000000000..a30ebacc497 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/flex/template/ClosestTrip.java @@ -0,0 +1,134 @@ +package org.opentripplanner.ext.flex.template; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +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. + */ +record ClosestTrip( + NearbyStop nearbyStop, + FlexTrip flexTrip, + int stopPos, + FlexServiceDate activeDate +) { + ClosestTrip( + NearbyStop nearbyStop, + FlexTrip flexTrip, + int stopPos, + FlexServiceDate activeDate + ) { + this.nearbyStop = Objects.requireNonNull(nearbyStop); + this.flexTrip = Objects.requireNonNull(flexTrip); + this.stopPos = IntUtils.requireNotNegative(stopPos, "stopPos"); + this.activeDate = activeDate; + } + + /** + * Create a temporary closest-trip without an active-date + */ + private ClosestTrip(NearbyStop nearbyStop, FlexTrip flexTrip, int stopPos) { + this(nearbyStop, flexTrip, stopPos, null); + } + + private ClosestTrip(ClosestTrip original, FlexServiceDate activeDate) { + this(original.nearbyStop, original.flexTrip, original.stopPos, activeDate); + } + + /** + * Create a set of the closest trips running on the dates provided. Only the + * combination of the closest nearby-stop and trip is kept. For each combination, + * the set of dates is checked, and an instance with each active date is returned. + */ + static Collection of( + FlexAccessEgressCallbackAdapter callbackService, + Collection nearbyStops, + List dates, + boolean pickup + ) { + var closestTrips = findAllTripsReachableFromNearbyStop(callbackService, nearbyStops, pickup); + return findActiveDatesForTripAndDecorateResult(callbackService, dates, closestTrips, true); + } + + @Override + public FlexServiceDate activeDate() { + // The active date is not required as an internal "trick" to create closest-trips + // in two steps, but the instance is not valid before the active-date is added. This + // method should not be used inside this class, only on fully constructed valid instances; + // Hence the active-date should not be null. + return Objects.requireNonNull(activeDate); + } + + private static Map, ClosestTrip> findAllTripsReachableFromNearbyStop( + FlexAccessEgressCallbackAdapter callbackService, + Collection nearbyStops, + boolean pickup + ) { + var map = new HashMap, ClosestTrip>(); + for (NearbyStop nearbyStop : nearbyStops) { + var stop = nearbyStop.stop; + for (var trip : callbackService.getFlexTripsByStop(stop)) { + int stopPos = pickup ? trip.findBoardIndex(stop) : trip.findAlightIndex(stop); + if (stopPos != FlexTrip.STOP_INDEX_NOT_FOUND) { + var existing = map.get(trip); + if (existing == null || nearbyStop.isBetter(existing.nearbyStop())) { + map.put(trip, new ClosestTrip(nearbyStop, trip, stopPos)); + } + } + } + } + return map; + } + + private static ArrayList findActiveDatesForTripAndDecorateResult( + FlexAccessEgressCallbackAdapter callbackService, + List dates, + Map, ClosestTrip> map, + boolean pickup + ) { + var result = new ArrayList(); + // Add active dates + for (Map.Entry, ClosestTrip> e : map.entrySet()) { + var trip = e.getKey(); + 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)); + } + } + } + return result; + } + + 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(date.requestedBookingTime(), trip.getPickupBookingInfo(stopPos)) + .exceedsLatestBookingTime(); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/DirectFlexPath.java b/src/ext/java/org/opentripplanner/ext/flex/template/DirectFlexPath.java new file mode 100644 index 00000000000..3ca6225c2ec --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/flex/template/DirectFlexPath.java @@ -0,0 +1,11 @@ +package org.opentripplanner.ext.flex.template; + +import org.opentripplanner.street.search.state.State; + +/** + * This is the result of a direct flex search. It only contains the start-time and + * the AStar state. It is used by the FlexRouter to build an itinerary. + *

+ * This is a simple data-transfer-object (design pattern). + */ +public record DirectFlexPath(int startTime, State state) {} diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessEgressCallbackAdapter.java b/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessEgressCallbackAdapter.java new file mode 100644 index 00000000000..792213cd2c8 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessEgressCallbackAdapter.java @@ -0,0 +1,39 @@ +package org.opentripplanner.ext.flex.template; + +import java.util.Collection; +import org.opentripplanner.ext.flex.trip.FlexTrip; +import org.opentripplanner.model.PathTransfer; +import org.opentripplanner.street.model.vertex.TransitStopVertex; +import org.opentripplanner.transit.model.framework.FeedScopedId; +import org.opentripplanner.transit.model.site.StopLocation; + +/** + * To perform access/egress/direct flex searches, this module (this package) needs these + * services. We do not want to inject the implementations here and create unnecessary + * hard dependencies. By doing this, we explicitly list all external services needed and make + * testing easier. This also serves as documentation. + *

+ * The implementation of this interface will for the most part just delegate to the implementing + * OTP service - look in these services for the documentation. + */ +public interface FlexAccessEgressCallbackAdapter { + /** Adapter, look at implementing service for documentation. */ + TransitStopVertex getStopVertexForStopId(FeedScopedId id); + + /** Adapter, look at implementing service for documentation. */ + Collection getTransfersFromStop(StopLocation stop); + + /** Adapter, look at implementing service for documentation. */ + Collection getTransfersToStop(StopLocation stop); + + /** Adapter, look at implementing service for documentation. */ + Collection> getFlexTripsByStop(StopLocation stopLocation); + + /** + * Return true if date is an active service date for the given trip, and can be used for + * the given boarding stop position. The implementation should check that the trip is in + * service for the given date. It should check other restrictions as well, like booking + * arrangement constraints. + */ + boolean isDateActive(FlexServiceDate date, FlexTrip trip); +} diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessFactory.java b/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessFactory.java new file mode 100644 index 00000000000..a4d348e6603 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessFactory.java @@ -0,0 +1,46 @@ +package org.opentripplanner.ext.flex.template; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import org.opentripplanner.ext.flex.FlexAccessEgress; +import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; +import org.opentripplanner.routing.graphfinder.NearbyStop; + +public class FlexAccessFactory { + + private final FlexAccessEgressCallbackAdapter callbackService; + private final FlexTemplateFactory templateFactory; + + public FlexAccessFactory( + FlexAccessEgressCallbackAdapter callbackService, + FlexPathCalculator pathCalculator, + Duration maxTransferDuration + ) { + this.callbackService = callbackService; + this.templateFactory = FlexTemplateFactory.of(pathCalculator, maxTransferDuration); + } + + public List createFlexAccesses( + Collection streetAccesses, + List dates + ) { + var flexAccessTemplates = calculateFlexAccessTemplates(streetAccesses, dates); + + return flexAccessTemplates + .stream() + .flatMap(template -> template.createFlexAccessEgressStream(callbackService)) + .toList(); + } + + List calculateFlexAccessTemplates( + Collection streetAccesses, + List dates + ) { + var closestFlexTrips = ClosestTrip.of(callbackService, streetAccesses, dates, true); + return closestFlexTrips + .stream() + .flatMap(it -> templateFactory.createAccessTemplates(it).stream()) + .toList(); + } +} 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 39963bcf69e..08d130ecec0 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessTemplate.java +++ b/src/ext/java/org/opentripplanner/ext/flex/template/FlexAccessTemplate.java @@ -1,113 +1,42 @@ package org.opentripplanner.ext.flex.template; -import static org.opentripplanner.model.StopTime.MISSING_VALUE; - -import java.time.ZonedDateTime; +import java.time.Duration; import java.util.Collection; import java.util.List; -import org.opentripplanner.astar.model.GraphPath; import org.opentripplanner.ext.flex.FlexPathDurations; -import org.opentripplanner.ext.flex.FlexServiceDate; import org.opentripplanner.ext.flex.edgetype.FlexTripEdge; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.model.PathTransfer; -import org.opentripplanner.model.plan.Itinerary; -import org.opentripplanner.routing.algorithm.mapping.GraphPathToItineraryMapper; import org.opentripplanner.routing.graphfinder.NearbyStop; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.vertex.Vertex; -import org.opentripplanner.street.search.state.EdgeTraverser; 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.service.TransitService; -public class FlexAccessTemplate extends FlexAccessEgressTemplate { +class FlexAccessTemplate extends AbstractFlexTemplate { - public FlexAccessTemplate( - NearbyStop accessEgress, - FlexTrip trip, - int fromStopTime, - int toStopTime, - StopLocation transferStop, + FlexAccessTemplate( + FlexTrip trip, + NearbyStop boardStop, + int boardStopPosition, + StopLocation alightStop, + int alightStopPosition, FlexServiceDate date, FlexPathCalculator calculator, - FlexConfig config - ) { - super(accessEgress, trip, fromStopTime, toStopTime, transferStop, date, calculator, config); - } - - public Itinerary createDirectGraphPath( - NearbyStop egress, - boolean arriveBy, - int departureTime, - ZonedDateTime startOfTime, - GraphPathToItineraryMapper graphPathToItineraryMapper + Duration maxTransferDuration ) { - List egressEdges = egress.edges; - - Vertex flexToVertex = egress.state.getVertex(); - - if (!isRouteable(flexToVertex)) { - return null; - } - - var flexEdge = getFlexEdge(flexToVertex, egress.stop); - - if (flexEdge == null) { - return null; - } - - final State[] afterFlexState = flexEdge.traverse(accessEgress.state); - - var finalStateOpt = EdgeTraverser.traverseEdges(afterFlexState[0], egressEdges); - - return finalStateOpt - .map(finalState -> { - var flexDurations = calculateFlexPathDurations(flexEdge, finalState); - - int timeShift; - - if (arriveBy) { - int lastStopArrivalTime = flexDurations.mapToFlexTripArrivalTime(departureTime); - int latestArrivalTime = trip.latestArrivalTime( - lastStopArrivalTime, - fromStopIndex, - toStopIndex, - flexDurations.trip() - ); - - if (latestArrivalTime == MISSING_VALUE) { - return null; - } - - // Shift from departing at departureTime to arriving at departureTime - timeShift = - flexDurations.mapToRouterArrivalTime(latestArrivalTime) - flexDurations.total(); - } else { - int firstStopDepartureTime = flexDurations.mapToFlexTripDepartureTime(departureTime); - int earliestDepartureTime = trip.earliestDepartureTime( - firstStopDepartureTime, - fromStopIndex, - toStopIndex, - flexDurations.trip() - ); - - if (earliestDepartureTime == MISSING_VALUE) { - return null; - } - timeShift = flexDurations.mapToRouterDepartureTime(earliestDepartureTime); - } - - ZonedDateTime startTime = startOfTime.plusSeconds(timeShift); - - return graphPathToItineraryMapper - .generateItinerary(new GraphPath<>(finalState)) - .withTimeShiftToStartAt(startTime); - }) - .orElse(null); + super( + trip, + boardStop, + alightStop, + boardStopPosition, + alightStopPosition, + date, + calculator, + maxTransferDuration + ); } protected List getTransferEdges(PathTransfer transfer) { @@ -118,8 +47,10 @@ protected RegularStop getFinalStop(PathTransfer transfer) { return transfer.to instanceof RegularStop ? (RegularStop) transfer.to : null; } - protected Collection getTransfersFromTransferStop(TransitService transitService) { - return transitService.getTransfersByStop(transferStop); + protected Collection getTransfersFromTransferStop( + FlexAccessEgressCallbackAdapter callback + ) { + return callback.getTransfersFromStop(transferStop); } protected Vertex getFlexVertex(Edge edge) { @@ -142,36 +73,24 @@ protected FlexTripEdge getFlexEdge(Vertex flexToVertex, StopLocation transferSto var flexPath = calculator.calculateFlexPath( accessEgress.state.getVertex(), flexToVertex, - fromStopIndex, - toStopIndex + boardStopPosition, + alightStopPosition ); if (flexPath == null) { return null; } - return FlexTripEdge.createFlexTripEdge( + return new FlexTripEdge( accessEgress.state.getVertex(), flexToVertex, accessEgress.stop, transferStop, trip, - this, + boardStopPosition, + alightStopPosition, + serviceDate, flexPath ); } - - protected boolean isRouteable(Vertex flexVertex) { - if (accessEgress.state.getVertex() == flexVertex) { - return false; - } else return ( - calculator.calculateFlexPath( - accessEgress.state.getVertex(), - flexVertex, - fromStopIndex, - toStopIndex - ) != - null - ); - } } diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java b/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java new file mode 100644 index 00000000000..f27a502911f --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/flex/template/FlexDirectPathFactory.java @@ -0,0 +1,192 @@ +package org.opentripplanner.ext.flex.template; + +import static org.opentripplanner.model.StopTime.MISSING_VALUE; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; +import org.opentripplanner.routing.graphfinder.NearbyStop; +import org.opentripplanner.street.model.vertex.Vertex; +import org.opentripplanner.street.search.state.EdgeTraverser; +import org.opentripplanner.street.search.state.State; +import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.booking.RoutingBookingInfo; + +public class FlexDirectPathFactory { + + private final FlexAccessEgressCallbackAdapter callbackService; + private final FlexPathCalculator accessPathCalculator; + private final FlexPathCalculator egressPathCalculator; + private final Duration maxTransferDuration; + + public FlexDirectPathFactory( + FlexAccessEgressCallbackAdapter callbackService, + FlexPathCalculator accessPathCalculator, + FlexPathCalculator egressPathCalculator, + Duration maxTransferDuration + ) { + this.callbackService = callbackService; + this.accessPathCalculator = accessPathCalculator; + this.egressPathCalculator = egressPathCalculator; + this.maxTransferDuration = maxTransferDuration; + } + + public Collection calculateDirectFlexPaths( + Collection streetAccesses, + Collection streetEgresses, + List dates, + int requestTime, + boolean arriveBy + ) { + Collection directFlexPaths = new ArrayList<>(); + + var flexAccessTemplates = new FlexAccessFactory( + callbackService, + accessPathCalculator, + maxTransferDuration + ) + .calculateFlexAccessTemplates(streetAccesses, dates); + + var flexEgressTemplates = new FlexEgressFactory( + callbackService, + egressPathCalculator, + maxTransferDuration + ) + .calculateFlexEgressTemplates(streetEgresses, dates); + + Multimap streetEgressByStop = HashMultimap.create(); + streetEgresses.forEach(it -> streetEgressByStop.put(it.stop, it)); + + for (FlexAccessTemplate template : flexAccessTemplates) { + StopLocation transferStop = template.getTransferStop(); + + // TODO: Document or reimplement this. Why are we using the egress to see if the + // access-transfer-stop (last-stop) is used by at least one egress-template? + // Is it because: + // - of the group-stop expansion? + // - of the alight-restriction check? + // - nearest stop to trip match? + // Fix: Find out why and refactor out the business logic and reuse it. + // Problem: Any asymmetrical restriction witch apply/do not apply to the egress, + // but do not apply/apply to the access, like booking-notice. + if ( + flexEgressTemplates.stream().anyMatch(t -> t.getAccessEgressStop().equals(transferStop)) + ) { + for (NearbyStop egress : streetEgressByStop.get(transferStop)) { + createDirectGraphPath(template, egress, arriveBy, requestTime) + .ifPresent(directFlexPaths::add); + } + } + } + + return directFlexPaths; + } + + private Optional createDirectGraphPath( + FlexAccessTemplate accessTemplate, + NearbyStop egress, + boolean arriveBy, + int requestTime + ) { + var accessNearbyStop = accessTemplate.accessEgress; + var trip = accessTemplate.trip; + int accessBoardStopPosition = accessTemplate.boardStopPosition; + int accessAlightStopPosition = accessTemplate.alightStopPosition; + int requestedBookingTime = accessTemplate.requestedBookingTime; + + var flexToVertex = egress.state.getVertex(); + + if (!isRouteable(accessTemplate, flexToVertex)) { + return Optional.empty(); + } + + var flexEdge = accessTemplate.getFlexEdge(flexToVertex, egress.stop); + + if (flexEdge == null) { + return Optional.empty(); + } + + final State[] afterFlexState = flexEdge.traverse(accessNearbyStop.state); + + var finalStateOpt = EdgeTraverser.traverseEdges(afterFlexState[0], egress.edges); + + if (finalStateOpt.isEmpty()) { + return Optional.empty(); + } + + var finalState = finalStateOpt.get(); + var flexDurations = accessTemplate.calculateFlexPathDurations(flexEdge, finalState); + + int timeShift; + + if (arriveBy) { + int lastStopArrivalTime = flexDurations.mapToFlexTripArrivalTime(requestTime); + int latestArrivalTime = trip.latestArrivalTime( + lastStopArrivalTime, + accessBoardStopPosition, + accessAlightStopPosition, + flexDurations.trip() + ); + + if (latestArrivalTime == MISSING_VALUE) { + 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( + requestedBookingTime, + trip.getPickupBookingInfo(accessTemplate.boardStopPosition) + ); + if (bookingInfo.exceedsMinimumBookingNotice(latestArrivalTime)) { + return Optional.empty(); + } + + // Shift from departing at departureTime to arriving at departureTime + timeShift = flexDurations.mapToRouterArrivalTime(latestArrivalTime) - flexDurations.total(); + } else { + int firstStopDepartureTime = flexDurations.mapToFlexTripDepartureTime(requestTime); + + // Time-shift departure so the minimum-booking-notice restriction is honored. + var bookingInfo = trip.getPickupBookingInfo(accessBoardStopPosition); + firstStopDepartureTime = + RoutingBookingInfo + .of(requestedBookingTime, bookingInfo) + .earliestDepartureTime(firstStopDepartureTime); + + int earliestDepartureTime = trip.earliestDepartureTime( + firstStopDepartureTime, + accessBoardStopPosition, + accessAlightStopPosition, + flexDurations.trip() + ); + + if (earliestDepartureTime == MISSING_VALUE) { + return Optional.empty(); + } + + timeShift = flexDurations.mapToRouterDepartureTime(earliestDepartureTime); + } + + return Optional.of(new DirectFlexPath(timeShift, finalState)); + } + + protected boolean isRouteable(FlexAccessTemplate accessTemplate, Vertex flexVertex) { + if (accessTemplate.accessEgress.state.getVertex() == flexVertex) { + return false; + } else return ( + accessTemplate.calculator.calculateFlexPath( + accessTemplate.accessEgress.state.getVertex(), + flexVertex, + accessTemplate.boardStopPosition, + accessTemplate.alightStopPosition + ) != + null + ); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressFactory.java b/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressFactory.java new file mode 100644 index 00000000000..28908cb7e7b --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressFactory.java @@ -0,0 +1,46 @@ +package org.opentripplanner.ext.flex.template; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import org.opentripplanner.ext.flex.FlexAccessEgress; +import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; +import org.opentripplanner.routing.graphfinder.NearbyStop; + +public class FlexEgressFactory { + + private final FlexAccessEgressCallbackAdapter callbackService; + private final FlexTemplateFactory templateFactory; + + public FlexEgressFactory( + FlexAccessEgressCallbackAdapter callbackService, + FlexPathCalculator pathCalculator, + Duration maxTransferDuration + ) { + this.callbackService = callbackService; + this.templateFactory = FlexTemplateFactory.of(pathCalculator, maxTransferDuration); + } + + public List createFlexEgresses( + Collection streetEgresses, + List dates + ) { + var flexEgressTemplates = calculateFlexEgressTemplates(streetEgresses, dates); + + return flexEgressTemplates + .stream() + .flatMap(template -> template.createFlexAccessEgressStream(callbackService)) + .toList(); + } + + List calculateFlexEgressTemplates( + Collection streetEgresses, + List dates + ) { + var closestFlexTrips = ClosestTrip.of(callbackService, streetEgresses, dates, false); + return closestFlexTrips + .stream() + .flatMap(it -> templateFactory.createEgressTemplates(it).stream()) + .toList(); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressTemplate.java b/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressTemplate.java index 27e9cd2f009..2ee5d4382ae 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressTemplate.java +++ b/src/ext/java/org/opentripplanner/ext/flex/template/FlexEgressTemplate.java @@ -1,36 +1,43 @@ package org.opentripplanner.ext.flex.template; import com.google.common.collect.Lists; +import java.time.Duration; import java.util.Collection; import java.util.List; import org.opentripplanner.ext.flex.FlexPathDurations; -import org.opentripplanner.ext.flex.FlexServiceDate; import org.opentripplanner.ext.flex.edgetype.FlexTripEdge; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; import org.opentripplanner.ext.flex.trip.FlexTrip; import org.opentripplanner.model.PathTransfer; import org.opentripplanner.routing.graphfinder.NearbyStop; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.vertex.Vertex; 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.service.TransitService; -public class FlexEgressTemplate extends FlexAccessEgressTemplate { +class FlexEgressTemplate extends AbstractFlexTemplate { - public FlexEgressTemplate( - NearbyStop accessEgress, - FlexTrip trip, - int fromStopIndex, - int toStopIndex, - StopLocation transferStop, + FlexEgressTemplate( + FlexTrip trip, + StopLocation boardStop, + int boardStopPosition, + NearbyStop alightStop, + int alightStopPosition, FlexServiceDate date, FlexPathCalculator calculator, - FlexConfig config + Duration maxTransferDuration ) { - super(accessEgress, trip, fromStopIndex, toStopIndex, transferStop, date, calculator, config); + super( + trip, + alightStop, + boardStop, + boardStopPosition, + alightStopPosition, + date, + calculator, + maxTransferDuration + ); } protected List getTransferEdges(PathTransfer transfer) { @@ -41,8 +48,10 @@ protected RegularStop getFinalStop(PathTransfer transfer) { return transfer.from instanceof RegularStop regularStop ? regularStop : null; } - protected Collection getTransfersFromTransferStop(TransitService transitService) { - return transitService.getFlexIndex().getTransfersToStop(transferStop); + protected Collection getTransfersFromTransferStop( + FlexAccessEgressCallbackAdapter callback + ) { + return callback.getTransfersToStop(transferStop); } protected Vertex getFlexVertex(Edge edge) { @@ -65,36 +74,24 @@ protected FlexTripEdge getFlexEdge(Vertex flexFromVertex, StopLocation transferS var flexPath = calculator.calculateFlexPath( flexFromVertex, accessEgress.state.getVertex(), - fromStopIndex, - toStopIndex + boardStopPosition, + alightStopPosition ); if (flexPath == null) { return null; } - return FlexTripEdge.createFlexTripEdge( + return new FlexTripEdge( flexFromVertex, accessEgress.state.getVertex(), transferStop, accessEgress.stop, trip, - this, + boardStopPosition, + alightStopPosition, + serviceDate, flexPath ); } - - protected boolean isRouteable(Vertex flexVertex) { - if (accessEgress.state.getVertex() == flexVertex) { - return false; - } else return ( - calculator.calculateFlexPath( - flexVertex, - accessEgress.state.getVertex(), - fromStopIndex, - toStopIndex - ) != - null - ); - } } diff --git a/src/ext/java/org/opentripplanner/ext/flex/template/FlexServiceDate.java b/src/ext/java/org/opentripplanner/ext/flex/template/FlexServiceDate.java new file mode 100644 index 00000000000..ee85cf77b03 --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/flex/template/FlexServiceDate.java @@ -0,0 +1,56 @@ +package org.opentripplanner.ext.flex.template; + +import gnu.trove.set.TIntSet; +import java.time.LocalDate; + +/** + * This class contains information used in a flex router, and depends on the date the search was + * made on. + */ +public class FlexServiceDate { + + /** The local date */ + 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. + */ + private final int secondsFromStartOfTime; + + /** Which services are running on the date. */ + 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; + } + + 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 new file mode 100644 index 00000000000..b7a9408af4a --- /dev/null +++ b/src/ext/java/org/opentripplanner/ext/flex/template/FlexTemplateFactory.java @@ -0,0 +1,160 @@ +package org.opentripplanner.ext.flex.template; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; +import org.opentripplanner.ext.flex.trip.FlexTrip; +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. + */ +class FlexTemplateFactory { + + private final FlexPathCalculator calculator; + private final Duration maxTransferDuration; + private NearbyStop nearbyStop; + private int stopPos; + private FlexTrip trip; + private FlexServiceDate date; + + private FlexTemplateFactory(FlexPathCalculator calculator, Duration maxTransferDuration) { + this.calculator = Objects.requireNonNull(calculator); + this.maxTransferDuration = Objects.requireNonNull(maxTransferDuration); + } + + static FlexTemplateFactory of(FlexPathCalculator calculator, Duration maxTransferDuration) { + return new FlexTemplateFactory(calculator, maxTransferDuration); + } + + List createAccessTemplates(ClosestTrip closestTrip) { + return with(closestTrip).createAccessTemplates(); + } + + List createEgressTemplates(ClosestTrip closestTrip) { + return with(closestTrip).createEgressTemplates(); + } + + /** + * Add required parameters to the factory before calling the create methods. + */ + private FlexTemplateFactory with(ClosestTrip closestTrip) { + this.nearbyStop = closestTrip.nearbyStop(); + this.stopPos = closestTrip.stopPos(); + this.trip = closestTrip.flexTrip(); + this.date = closestTrip.activeDate(); + return this; + } + + private List createAccessTemplates() { + int boardStopPos = stopPos; + + var result = new ArrayList(); + int alightStopPos = isBoardingAndAlightingAtSameStopPositionAllowed() + ? boardStopPos + : boardStopPos + 1; + + for (; alightStopPos < trip.numberOfStops(); alightStopPos++) { + if (trip.getAlightRule(alightStopPos).isRoutable()) { + for (var stop : expandStopsAt(trip, alightStopPos)) { + result.add(createAccessTemplate(trip, boardStopPos, stop, alightStopPos)); + } + } + } + return result; + } + + private List createEgressTemplates() { + var alightStopPos = stopPos; + + var result = new ArrayList(); + int end = isBoardingAndAlightingAtSameStopPositionAllowed() ? alightStopPos : alightStopPos - 1; + + 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(date.requestedBookingTime(), trip.getPickupBookingInfo(boardStopPosition)) + .exceedsLatestBookingTime() + ); + } + + /** + * 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 + * stops, but it would make sense to allow it for area stops or group stops. + *

+ * In NeTEx this is not allowed. + *

+ * In GTFS this is no longer allowed according to specification. But it was allowed earlier. + *

+ * This method simply returns {@code false}, but we keep it here for documentation. If requested, + * we can add code to be backward compatible with the old GTFS version here. + */ + private boolean isBoardingAndAlightingAtSameStopPositionAllowed() { + return false; + } + + private static List expandStopsAt(FlexTrip flexTrip, int index) { + var stop = flexTrip.getStop(index); + return stop instanceof GroupStop groupStop ? groupStop.getChildLocations() : List.of(stop); + } + + private FlexAccessTemplate createAccessTemplate( + FlexTrip flexTrip, + int boardStopPosition, + StopLocation alightStop, + int alightStopPosition + ) { + return new FlexAccessTemplate( + flexTrip, + nearbyStop, + boardStopPosition, + alightStop, + alightStopPosition, + date, + setupCalculator(flexTrip), + maxTransferDuration + ); + } + + private FlexEgressTemplate createEgressTemplate( + FlexTrip flexTrip, + StopLocation boardStop, + int boardStopPosition, + int alightStopPosition + ) { + return new FlexEgressTemplate( + flexTrip, + boardStop, + boardStopPosition, + nearbyStop, + alightStopPosition, + date, + setupCalculator(flexTrip), + maxTransferDuration + ); + } + + private FlexPathCalculator setupCalculator(FlexTrip flexTrip) { + return flexTrip.decorateFlexPathCalculator(calculator); + } +} diff --git a/src/ext/java/org/opentripplanner/ext/flex/trip/FlexTrip.java b/src/ext/java/org/opentripplanner/ext/flex/trip/FlexTrip.java index f12ce78d06d..8fdb1b8fd5c 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/trip/FlexTrip.java +++ b/src/ext/java/org/opentripplanner/ext/flex/trip/FlexTrip.java @@ -3,22 +3,16 @@ import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.stream.Stream; import javax.annotation.Nonnull; -import org.opentripplanner.ext.flex.FlexServiceDate; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; -import org.opentripplanner.ext.flex.template.FlexAccessTemplate; -import org.opentripplanner.ext.flex.template.FlexEgressTemplate; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.StopTime; -import org.opentripplanner.routing.graphfinder.NearbyStop; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.transit.model.framework.AbstractTransitEntity; import org.opentripplanner.transit.model.site.AreaStop; import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; /** * This class represents the different variations of what is considered flexible transit, and its @@ -28,6 +22,8 @@ public abstract class FlexTrip, B extends FlexTripBuilder> extends AbstractTransitEntity { + public static int STOP_INDEX_NOT_FOUND = -1; + private final Trip trip; FlexTrip(FlexTripBuilder builder) { @@ -43,60 +39,51 @@ public static boolean isFlexStop(StopLocation stop) { return stop instanceof GroupStop || stop instanceof AreaStop; } - public abstract Stream getFlexAccessTemplates( - NearbyStop access, - FlexServiceDate date, - FlexPathCalculator calculator, - FlexConfig config - ); - - public abstract Stream getFlexEgressTemplates( - NearbyStop egress, - FlexServiceDate date, - FlexPathCalculator calculator, - FlexConfig config - ); - /** - * Earliest departure time from fromStopIndex to toStopIndex, which departs after departureTime, + * Earliest departure time from boardStopPosition to alightStopPosition, which departs after departureTime, * and for which the flex trip has a duration of flexTime seconds. * * @return {@link StopTime#MISSING_VALUE} is returned if a departure does not exist. */ public abstract int earliestDepartureTime( int departureTime, - int fromStopIndex, - int toStopIndex, + int boardStopPosition, + int alightStopPosition, int flexTripDurationSeconds ); /** - * Earliest departure time from fromStopIndex. + * Earliest departure time from boardStopPosition. * * @return {@link StopTime#MISSING_VALUE} is returned if a departure does not exist. */ public abstract int earliestDepartureTime(int stopIndex); /** - * Latest arrival time to toStopIndex from fromStopIndex, which arrives before arrivalTime, + * Latest arrival time to alightStopPosition from boardStopPosition, which arrives before arrivalTime, * and for which the flex trip has a duration of flexTime seconds. * * @return {@link StopTime#MISSING_VALUE} is returned if a departure does not exist. */ public abstract int latestArrivalTime( int arrivalTime, - int fromStopIndex, - int toStopIndex, + int boardStopPosition, + int alightStopPosition, int tripDurationSeconds ); /** - * Latest arrival time to toStopIndex. + * Latest arrival time to alightStopPosition. * * @return {@link StopTime#MISSING_VALUE} is returned if a departure does not exist. */ public abstract int latestArrivalTime(int stopIndex); + /** + * Return number-of-stops this trip visit. + */ + public abstract int numberOfStops(); + /** * Returns all the stops that are in this trip. *

@@ -107,6 +94,12 @@ public abstract int latestArrivalTime( */ public abstract Set getStops(); + /** + * Return a stop at given stop-index. Note! The visited order may not be the same as the + * indexing order. + */ + public abstract StopLocation getStop(int stopIndex); + public Trip getTrip() { return trip; } @@ -119,9 +112,32 @@ public Trip getTrip() { public abstract PickDrop getAlightRule(int i); - public abstract boolean isBoardingPossible(NearbyStop stop); + public abstract boolean isBoardingPossible(StopLocation stop); + + public abstract boolean isAlightingPossible(StopLocation stop); - public abstract boolean isAlightingPossible(NearbyStop stop); + /** + * Find the first stop-position matching the given {@code fromStop} where + * boarding is allowed. + * + * @return stop position in the pattern or {@link #STOP_INDEX_NOT_FOUND} if not found. + */ + public abstract int findBoardIndex(StopLocation fromStop); + + /** + * Find the first stop-position matching the given {@code toStop} where + * alighting is allowed. + * + * @return the stop position in the pattern or {@link #STOP_INDEX_NOT_FOUND} if not found. + */ + public abstract int findAlightIndex(StopLocation toStop); + + /** + * Allow each FlexTrip type to decorate or replace the router defaultCalculator. + */ + public abstract FlexPathCalculator decorateFlexPathCalculator( + FlexPathCalculator defaultCalculator + ); @Override public boolean sameAs(@Nonnull T other) { diff --git a/src/ext/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTrip.java b/src/ext/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTrip.java index e16e1e5e1f7..5ea0cb9fb91 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTrip.java +++ b/src/ext/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTrip.java @@ -4,31 +4,22 @@ import static org.opentripplanner.model.StopTime.MISSING_VALUE; import java.io.Serializable; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; import java.util.List; 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.FlexServiceDate; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; import org.opentripplanner.ext.flex.flexpathcalculator.ScheduledFlexPathCalculator; -import org.opentripplanner.ext.flex.template.FlexAccessTemplate; -import org.opentripplanner.ext.flex.template.FlexEgressTemplate; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.StopTime; -import org.opentripplanner.routing.graphfinder.NearbyStop; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.framework.TransitBuilder; import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; /** * A scheduled deviated trip is similar to a regular scheduled trip, except that it contains stop @@ -74,95 +65,15 @@ public static boolean isScheduledFlexTrip(List stopTimes) { ); } - @Override - public Stream getFlexAccessTemplates( - NearbyStop access, - FlexServiceDate date, - FlexPathCalculator calculator, - FlexConfig config - ) { - FlexPathCalculator scheduledCalculator = new ScheduledFlexPathCalculator(calculator, this); - - int fromIndex = getFromIndex(access); - - if (fromIndex == -1) { - return Stream.empty(); - } - - ArrayList res = new ArrayList<>(); - - for (int toIndex = fromIndex; toIndex < stopTimes.length; toIndex++) { - if (getAlightRule(toIndex).isNotRoutable()) { - continue; - } - for (StopLocation stop : expandStops(stopTimes[toIndex].stop)) { - res.add( - new FlexAccessTemplate( - access, - this, - fromIndex, - toIndex, - stop, - date, - scheduledCalculator, - config - ) - ); - } - } - - return res.stream(); - } - - @Override - public Stream getFlexEgressTemplates( - NearbyStop egress, - FlexServiceDate date, - FlexPathCalculator calculator, - FlexConfig config - ) { - FlexPathCalculator scheduledCalculator = new ScheduledFlexPathCalculator(calculator, this); - - int toIndex = getToIndex(egress); - - if (toIndex == -1) { - return Stream.empty(); - } - - ArrayList res = new ArrayList<>(); - - for (int fromIndex = toIndex; fromIndex >= 0; fromIndex--) { - if (getBoardRule(fromIndex).isNotRoutable()) { - continue; - } - for (StopLocation stop : expandStops(stopTimes[fromIndex].stop)) { - res.add( - new FlexEgressTemplate( - egress, - this, - fromIndex, - toIndex, - stop, - date, - scheduledCalculator, - config - ) - ); - } - } - - return res.stream(); - } - @Override public int earliestDepartureTime( int departureTime, - int fromStopIndex, - int toStopIndex, + int boardStopPosition, + int alightStopPosition, int flexTripDurationSeconds ) { int stopTime = MISSING_VALUE; - for (int i = fromStopIndex; stopTime == MISSING_VALUE && i >= 0; i--) { + for (int i = boardStopPosition; stopTime == MISSING_VALUE && i >= 0; i--) { stopTime = stopTimes[i].departureTime; } return stopTime >= departureTime ? stopTime : MISSING_VALUE; @@ -176,12 +87,12 @@ public int earliestDepartureTime(int stopIndex) { @Override public int latestArrivalTime( int arrivalTime, - int fromStopIndex, - int toStopIndex, + int boardStopPosition, + int alightStopPosition, int flexTripDurationSeconds ) { int stopTime = MISSING_VALUE; - for (int i = toStopIndex; stopTime == MISSING_VALUE && i < stopTimes.length; i++) { + for (int i = alightStopPosition; stopTime == MISSING_VALUE && i < stopTimes.length; i++) { stopTime = stopTimes[i].arrivalTime; } return stopTime <= arrivalTime ? stopTime : MISSING_VALUE; @@ -192,6 +103,11 @@ public int latestArrivalTime(int stopIndex) { return stopTimes[stopIndex].arrivalTime; } + @Override + public int numberOfStops() { + return stopTimes.length; + } + @Override public Set getStops() { return Arrays @@ -200,6 +116,11 @@ public Set getStops() { .collect(Collectors.toSet()); } + @Override + public StopLocation getStop(int stopIndex) { + return stopTimes[stopIndex].stop; + } + @Override public BookingInfo getDropOffBookingInfo(int i) { return dropOffBookingInfos[i]; @@ -221,13 +142,13 @@ public PickDrop getAlightRule(int i) { } @Override - public boolean isBoardingPossible(NearbyStop stop) { - return getFromIndex(stop) != -1; + public boolean isBoardingPossible(StopLocation fromStop) { + return findBoardIndex(fromStop) != STOP_INDEX_NOT_FOUND; } @Override - public boolean isAlightingPossible(NearbyStop stop) { - return getToIndex(stop) != -1; + public boolean isAlightingPossible(StopLocation toStop) { + return findAlightIndex(toStop) != STOP_INDEX_NOT_FOUND; } @Override @@ -246,48 +167,49 @@ public TransitBuilder copy( return new ScheduledDeviatedTripBuilder(this); } - private Collection expandStops(StopLocation stop) { - return stop instanceof GroupStop groupStop - ? groupStop.getChildLocations() - : Collections.singleton(stop); - } - - private int getFromIndex(NearbyStop accessEgress) { + @Override + public int findBoardIndex(StopLocation fromStop) { for (int i = 0; i < stopTimes.length; i++) { if (getBoardRule(i).isNotRoutable()) { continue; } StopLocation stop = stopTimes[i].stop; if (stop instanceof GroupStop groupStop) { - if (groupStop.getChildLocations().contains(accessEgress.stop)) { + if (groupStop.getChildLocations().contains(fromStop)) { return i; } } else { - if (stop.equals(accessEgress.stop)) { + if (stop.equals(fromStop)) { return i; } } } - return -1; + return STOP_INDEX_NOT_FOUND; } - private int getToIndex(NearbyStop accessEgress) { + @Override + public int findAlightIndex(StopLocation toStop) { for (int i = stopTimes.length - 1; i >= 0; i--) { if (getAlightRule(i).isNotRoutable()) { continue; } StopLocation stop = stopTimes[i].stop; if (stop instanceof GroupStop groupStop) { - if (groupStop.getChildLocations().contains(accessEgress.stop)) { + if (groupStop.getChildLocations().contains(toStop)) { return i; } } else { - if (stop.equals(accessEgress.stop)) { + if (stop.equals(toStop)) { return i; } } } - return -1; + return STOP_INDEX_NOT_FOUND; + } + + @Override + public FlexPathCalculator decorateFlexPathCalculator(FlexPathCalculator defaultCalculator) { + return new ScheduledFlexPathCalculator(defaultCalculator, this); } private static class ScheduledDeviatedStopTime implements Serializable { 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 a4c8d9568ca..20a77bb94ed 100644 --- a/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java +++ b/src/ext/java/org/opentripplanner/ext/flex/trip/UnscheduledTrip.java @@ -8,30 +8,22 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; import javax.annotation.Nonnull; -import org.opentripplanner.ext.flex.FlexServiceDate; import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator; import org.opentripplanner.ext.flex.flexpathcalculator.TimePenaltyCalculator; -import org.opentripplanner.ext.flex.template.FlexAccessTemplate; -import org.opentripplanner.ext.flex.template.FlexEgressTemplate; import org.opentripplanner.framework.lang.DoubleUtils; import org.opentripplanner.framework.lang.IntRange; import org.opentripplanner.framework.time.DurationUtils; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.StopTime; import org.opentripplanner.routing.api.request.framework.TimePenalty; -import org.opentripplanner.routing.graphfinder.NearbyStop; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.transit.model.framework.FeedScopedId; import org.opentripplanner.transit.model.framework.TransitBuilder; import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.StopLocation; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; /** * This type of FlexTrip is used when a taxi-type service is modeled, which operates in any number @@ -48,7 +40,6 @@ public class UnscheduledTrip extends FlexTrip { private static final Set N_STOPS = Set.of(1, 2); - private static final int INDEX_NOT_FOUND = -1; private final StopTimeWindow[] stopTimes; @@ -104,125 +95,16 @@ public static boolean isUnscheduledTrip(List stopTimes) { } } - @Override - public Stream getFlexAccessTemplates( - NearbyStop access, - FlexServiceDate date, - FlexPathCalculator calculator, - FlexConfig config - ) { - // Find boarding index, also check if it's boardable - final int fromIndex = getFromIndex(access); - - // templates will be generated from the boardingIndex to the end of the trip - final int lastIndexInTrip = stopTimes.length - 1; - - // Check if trip is possible - if (fromIndex == INDEX_NOT_FOUND || fromIndex > lastIndexInTrip) { - return Stream.empty(); - } - - IntStream indices; - if (stopTimes.length == 1) { - indices = IntStream.of(fromIndex); - } else { - indices = IntStream.range(fromIndex + 1, lastIndexInTrip + 1); - } - - final var updatedCalculator = flexPathCalculator(calculator); - - // check for every stop after fromIndex if you can alight, if so return a template - return indices - // if you cannot alight at an index, the trip is not possible - .filter(alightIndex -> getAlightRule(alightIndex).isRoutable()) - // expand GroupStops and build IndexedStopLocations - .mapToObj(this::expandStops) - // flatten stream of streams - .flatMap(Function.identity()) - // create template - .map(alightStop -> - new FlexAccessTemplate( - access, - this, - fromIndex, - alightStop.index, - alightStop.stop, - date, - updatedCalculator, - config - ) - ); - } - - /** - * Get the correct {@link FlexPathCalculator} depending on the {@code timePenalty}. - * If the penalty would not change the result, we return the regular calculator. - */ - protected FlexPathCalculator flexPathCalculator(FlexPathCalculator calculator) { - if (timePenalty.modifies()) { - return new TimePenaltyCalculator(calculator, timePenalty); - } else { - return calculator; - } - } - - @Override - public Stream getFlexEgressTemplates( - NearbyStop egress, - FlexServiceDate date, - FlexPathCalculator calculator, - FlexConfig config - ) { - // templates will be generated from the first index to the toIndex - int firstIndexInTrip = 0; - - // Find alighting index, also check if alighting is allowed - int toIndex = getToIndex(egress); - - // Check if trip is possible - if (toIndex == INDEX_NOT_FOUND || firstIndexInTrip > toIndex) { - return Stream.empty(); - } - - IntStream indices; - if (stopTimes.length == 1) { - indices = IntStream.of(toIndex); - } else { - indices = IntStream.range(firstIndexInTrip, toIndex + 1); - } - // check for every stop after fromIndex if you can alight, if so return a template - return indices - // if you cannot board at this index, the trip is not possible - .filter(boardIndex -> getBoardRule(boardIndex).isRoutable()) - // expand GroupStops and build IndexedStopLocations - .mapToObj(this::expandStops) - // flatten stream of streams - .flatMap(Function.identity()) - // create template - .map(boardStop -> - new FlexEgressTemplate( - egress, - this, - boardStop.index, - toIndex, - boardStop.stop, - date, - calculator, - config - ) - ); - } - @Override public int earliestDepartureTime( int requestedDepartureTime, - int fromStopIndex, - int toStopIndex, + int boardStopPosition, + int alightStopPosition, int tripDurationSeconds ) { var optionalDepartureTimeWindow = departureTimeWindow( - fromStopIndex, - toStopIndex, + boardStopPosition, + alightStopPosition, tripDurationSeconds ); @@ -244,13 +126,13 @@ public int earliestDepartureTime(int stopIndex) { @Override public int latestArrivalTime( int requestedArrivalTime, - int fromStopIndex, - int toStopIndex, + int boardStopPosition, + int alightStopPosition, int tripDurationSeconds ) { var optionalArrivalTimeWindow = arrivalTimeWindow( - fromStopIndex, - toStopIndex, + boardStopPosition, + alightStopPosition, tripDurationSeconds ); @@ -269,39 +151,49 @@ public int latestArrivalTime(int stopIndex) { return stopTimes[stopIndex].end(); } + @Override + public int numberOfStops() { + return stopTimes.length; + } + @Override public Set getStops() { return Arrays.stream(stopTimes).map(StopTimeWindow::stop).collect(Collectors.toSet()); } @Override - public BookingInfo getDropOffBookingInfo(int i) { - return dropOffBookingInfos[i]; + public StopLocation getStop(int stopIndex) { + return stopTimes[stopIndex].stop(); + } + + @Override + public BookingInfo getDropOffBookingInfo(int stopIndex) { + return dropOffBookingInfos[stopIndex]; } @Override - public BookingInfo getPickupBookingInfo(int i) { - return pickupBookingInfos[i]; + public BookingInfo getPickupBookingInfo(int stopIndex) { + return pickupBookingInfos[stopIndex]; } @Override - public PickDrop getBoardRule(int i) { - return stopTimes[i].pickupType(); + public PickDrop getBoardRule(int stopIndex) { + return stopTimes[stopIndex].pickupType(); } @Override - public PickDrop getAlightRule(int i) { - return stopTimes[i].dropOffType(); + public PickDrop getAlightRule(int stopIndex) { + return stopTimes[stopIndex].dropOffType(); } @Override - public boolean isBoardingPossible(NearbyStop stop) { - return getFromIndex(stop) != INDEX_NOT_FOUND; + public boolean isBoardingPossible(StopLocation stop) { + return findBoardIndex(stop) != STOP_INDEX_NOT_FOUND; } @Override - public boolean isAlightingPossible(NearbyStop stop) { - return getToIndex(stop) != INDEX_NOT_FOUND; + public boolean isAlightingPossible(StopLocation stop) { + return findAlightIndex(stop) != STOP_INDEX_NOT_FOUND; } @Override @@ -320,59 +212,65 @@ public TransitBuilder copy() { return new UnscheduledTripBuilder(this); } - private Stream expandStops(int index) { - var stop = stopTimes[index].stop(); - return stop instanceof GroupStop groupStop - ? groupStop.getChildLocations().stream().map(s -> new IndexedStopLocation(index, s)) - : Stream.of(new IndexedStopLocation(index, stop)); - } - - private int getFromIndex(NearbyStop accessEgress) { + @Override + public int findBoardIndex(StopLocation fromStop) { for (int i = 0; i < stopTimes.length; i++) { if (getBoardRule(i).isNotRoutable()) { continue; } StopLocation stop = stopTimes[i].stop(); if (stop instanceof GroupStop groupStop) { - if (groupStop.getChildLocations().contains(accessEgress.stop)) { + if (groupStop.getChildLocations().contains(fromStop)) { return i; } } else { - if (stop.equals(accessEgress.stop)) { + if (stop.equals(fromStop)) { return i; } } } - return INDEX_NOT_FOUND; + return FlexTrip.STOP_INDEX_NOT_FOUND; } - private int getToIndex(NearbyStop accessEgress) { + @Override + public int findAlightIndex(StopLocation toStop) { for (int i = stopTimes.length - 1; i >= 0; i--) { if (getAlightRule(i).isNotRoutable()) { continue; } StopLocation stop = stopTimes[i].stop(); if (stop instanceof GroupStop groupStop) { - if (groupStop.getChildLocations().contains(accessEgress.stop)) { + if (groupStop.getChildLocations().contains(toStop)) { return i; } } else { - if (stop.equals(accessEgress.stop)) { + if (stop.equals(toStop)) { return i; } } } - return INDEX_NOT_FOUND; + return FlexTrip.STOP_INDEX_NOT_FOUND; + } + + @Override + public FlexPathCalculator decorateFlexPathCalculator(FlexPathCalculator defaultCalculator) { + // Get the correct {@link FlexPathCalculator} depending on the {@code timePenalty}. + // If the modifier does not change the result, we return the regular calculator. + if (timePenalty.modifies()) { + return new TimePenaltyCalculator(defaultCalculator, timePenalty); + } else { + return defaultCalculator; + } } private Optional departureTimeWindow( - int fromStopIndex, - int toStopIndex, + int boardStopPosition, + int alightStopPosition, int tripDurationSeconds ) { // Align the from and to time-windows by subtracting the trip-duration from the to-time-window. - var fromTime = stopTimes[fromStopIndex].timeWindow(); - var toTimeShifted = stopTimes[toStopIndex].timeWindow().minus(tripDurationSeconds); + var fromTime = stopTimes[boardStopPosition].timeWindow(); + var toTimeShifted = stopTimes[alightStopPosition].timeWindow().minus(tripDurationSeconds); // Then take the intersection of the aligned windows to find the window where the // requested-departure-time must be within @@ -380,13 +278,13 @@ private Optional departureTimeWindow( } private Optional arrivalTimeWindow( - int fromStopIndex, - int toStopIndex, + int boardStopPosition, + int alightStopPosition, int tripDurationSeconds ) { // Align the from and to time-windows by adding the trip-duration to the from-time-window. - var fromTimeShifted = stopTimes[fromStopIndex].timeWindow().plus(tripDurationSeconds); - var toTime = stopTimes[toStopIndex].timeWindow(); + var fromTimeShifted = stopTimes[boardStopPosition].timeWindow().plus(tripDurationSeconds); + var toTime = stopTimes[alightStopPosition].timeWindow(); // Then take the intersection of the aligned windows to find the window where the // requested-arrival-time must be within diff --git a/src/ext/java/org/opentripplanner/ext/restapi/mapping/BookingInfoMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/mapping/BookingInfoMapper.java index 4aa8b4a46b1..b4e3292b435 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/mapping/BookingInfoMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/mapping/BookingInfoMapper.java @@ -1,11 +1,23 @@ package org.opentripplanner.ext.restapi.mapping; import org.opentripplanner.ext.restapi.model.ApiBookingInfo; -import org.opentripplanner.model.BookingInfo; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; public class BookingInfoMapper { - static ApiBookingInfo mapBookingInfo(BookingInfo info, boolean isPickup) { + static ApiBookingInfo mapBookingInfoForPickup(BookingInfo info) { + return mapBookingInfo(info, true); + } + + static ApiBookingInfo mapBookingInfoForDropOff(BookingInfo info) { + return mapBookingInfo(info, false); + } + + /** + * @param isPickup either pickup or dropOff message must be set, not both. We only want to show + * the pick-up message for pickups, and the drop-off message for drop-offs. + */ + private static ApiBookingInfo mapBookingInfo(BookingInfo info, boolean isPickup) { if (info == null) { return null; } @@ -18,9 +30,7 @@ static ApiBookingInfo mapBookingInfo(BookingInfo info, boolean isPickup) { info.getMinimumBookingNotice(), info.getMaximumBookingNotice(), info.getMessage(), - // we only want to show the pick up message for pickups isPickup ? info.getPickupMessage() : null, - // and only the drop off message for drop offs !isPickup ? info.getDropOffMessage() : null ); } diff --git a/src/ext/java/org/opentripplanner/ext/restapi/mapping/BookingMethodMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/mapping/BookingMethodMapper.java index 9b61ff39543..0ab743e1cb2 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/mapping/BookingMethodMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/mapping/BookingMethodMapper.java @@ -3,7 +3,7 @@ import java.util.EnumSet; import java.util.Set; import java.util.stream.Collectors; -import org.opentripplanner.model.BookingMethod; +import org.opentripplanner.transit.model.timetable.booking.BookingMethod; public class BookingMethodMapper { diff --git a/src/ext/java/org/opentripplanner/ext/restapi/mapping/BookingTimeMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/mapping/BookingTimeMapper.java index e40309e9137..8b3034532fc 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/mapping/BookingTimeMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/mapping/BookingTimeMapper.java @@ -1,7 +1,7 @@ package org.opentripplanner.ext.restapi.mapping; import org.opentripplanner.ext.restapi.model.ApiBookingTime; -import org.opentripplanner.model.BookingTime; +import org.opentripplanner.transit.model.timetable.booking.BookingTime; public class BookingTimeMapper { diff --git a/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegMapper.java b/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegMapper.java index 766262bca89..b642426cd6d 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegMapper.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/mapping/LegMapper.java @@ -142,9 +142,10 @@ public ApiLeg mapLeg( api.boardRule = getBoardAlightMessage(domain.getBoardRule()); api.alightRule = getBoardAlightMessage(domain.getAlightRule()); - api.pickupBookingInfo = BookingInfoMapper.mapBookingInfo(domain.getPickupBookingInfo(), true); + api.pickupBookingInfo = + BookingInfoMapper.mapBookingInfoForPickup(domain.getPickupBookingInfo()); api.dropOffBookingInfo = - BookingInfoMapper.mapBookingInfo(domain.getDropOffBookingInfo(), false); + BookingInfoMapper.mapBookingInfoForDropOff(domain.getDropOffBookingInfo()); api.rentedBike = domain.getRentedVehicle(); api.walkingBike = domain.getWalkingBike(); diff --git a/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java b/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java index 1d985f0e555..3ae029fdb2d 100644 --- a/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java +++ b/src/ext/java/org/opentripplanner/ext/restapi/resources/RoutingResource.java @@ -8,6 +8,7 @@ import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MultivaluedMap; import java.time.Duration; +import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -171,6 +172,9 @@ public abstract class RoutingResource { @QueryParam("wheelchair") protected Boolean wheelchair; + @QueryParam("bookingTime") + protected String bookingTime; + /** * The maximum time (in seconds) of pre-transit travel when using drive-to-transit (park and ride * or kiss and ride). Defaults to unlimited. @@ -728,6 +732,10 @@ protected RouteRequest buildRequest(MultivaluedMap queryParamete } else { request.setDateTime(date, time, tz); } + + if (bookingTime != null) { + request.setBookingTime(LocalDateTime.parse(bookingTime).atZone(tz).toInstant()); + } } final Duration swDuration = DurationUtils.parseSecondsOrDuration(searchWindow).orElse(null); diff --git a/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessAdapter.java b/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessAdapter.java index beba8570b8b..257cf0a7ca5 100644 --- a/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessAdapter.java +++ b/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessAdapter.java @@ -3,6 +3,7 @@ import java.time.Duration; import org.opentripplanner.framework.model.TimeAndCost; import org.opentripplanner.routing.algorithm.raptoradapter.transit.DefaultAccessEgress; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; /** * This class is used to adapt the ride hailing accesses (not egresses) into a time-dependent @@ -12,7 +13,7 @@ public final class RideHailingAccessAdapter extends DefaultAccessEgress { private final Duration arrival; - public RideHailingAccessAdapter(DefaultAccessEgress access, Duration arrival) { + public RideHailingAccessAdapter(RoutingAccessEgress access, Duration arrival) { super(access.stop(), access.getLastState()); this.arrival = arrival; } @@ -49,7 +50,7 @@ public String openingHoursToString() { } @Override - public DefaultAccessEgress withPenalty(TimeAndCost penalty) { + public RoutingAccessEgress withPenalty(TimeAndCost penalty) { return new RideHailingAccessAdapter(this, penalty); } diff --git a/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessShifter.java b/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessShifter.java index b4a75c26aad..5e4eee09920 100644 --- a/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessShifter.java +++ b/src/ext/java/org/opentripplanner/ext/ridehailing/RideHailingAccessShifter.java @@ -9,7 +9,7 @@ import java.util.stream.Collectors; import org.opentripplanner.ext.ridehailing.model.ArrivalTime; import org.opentripplanner.framework.geometry.WgsCoordinate; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.DefaultAccessEgress; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.transit.model.framework.Result; @@ -29,13 +29,13 @@ public class RideHailingAccessShifter { private static final Duration MAX_DURATION_FROM_NOW = Duration.ofMinutes(30); /** - * Given a list of {@link DefaultAccessEgress} shift the access ones which contain driving + * Given a list of {@link RoutingAccessEgress}, shift the access ones that contain driving * so that they only start at the time when the ride hailing vehicle can actually be there * to pick up passengers. */ - public static List shiftAccesses( + public static List shiftAccesses( boolean isAccess, - List results, + List results, List services, RouteRequest request, Instant now diff --git a/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeResource.java b/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeResource.java index 67d8bb0d6cb..88742aebeb1 100644 --- a/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeResource.java +++ b/src/ext/java/org/opentripplanner/ext/traveltime/TravelTimeResource.java @@ -34,7 +34,7 @@ import org.opentripplanner.raptor.api.response.RaptorResponse; import org.opentripplanner.raptor.api.response.StopArrivals; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressRouter; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.DefaultAccessEgress; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.AccessEgressMapper; import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.RaptorRoutingRequestTransitData; @@ -178,7 +178,7 @@ private ZSampleGrid getSampleGrid() { } } - private Collection getAccess(TemporaryVerticesContainer temporaryVertices) { + private Collection getAccess(TemporaryVerticesContainer temporaryVertices) { final Collection accessStops = AccessEgressRouter.streetSearch( routingRequest, temporaryVertices, diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/BookingInfoImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/BookingInfoImpl.java index 9ba2c21f62d..44ee0985542 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/BookingInfoImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/BookingInfoImpl.java @@ -3,9 +3,9 @@ import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; -import org.opentripplanner.model.BookingInfo; -import org.opentripplanner.model.BookingTime; 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 BookingInfoImpl implements GraphQLDataFetchers.GraphQLBookingInfo { diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/BookingTimeImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/BookingTimeImpl.java index 8c6e581eba4..9d6cd00642d 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/BookingTimeImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/BookingTimeImpl.java @@ -4,7 +4,7 @@ import graphql.schema.DataFetchingEnvironment; import java.time.format.DateTimeFormatter; import org.opentripplanner.apis.gtfs.generated.GraphQLDataFetchers; -import org.opentripplanner.model.BookingTime; +import org.opentripplanner.transit.model.timetable.booking.BookingTime; public class BookingTimeImpl implements GraphQLDataFetchers.GraphQLBookingTime { diff --git a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java index 975d8d56831..9ec83a4bf67 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/datafetchers/LegImpl.java @@ -14,7 +14,6 @@ import org.opentripplanner.ext.ridehailing.model.RideEstimate; import org.opentripplanner.ext.ridehailing.model.RideHailingLeg; import org.opentripplanner.framework.graphql.GraphQLUtils; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.fare.FareProductUse; import org.opentripplanner.model.plan.Leg; @@ -30,6 +29,7 @@ import org.opentripplanner.transit.model.network.Route; import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; public class LegImpl implements GraphQLDataFetchers.GraphQLLeg { diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java index fbfc4965c62..103534689a9 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/GraphQLDataFetchers.java @@ -62,6 +62,8 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.organization.Agency; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; +import org.opentripplanner.transit.model.timetable.booking.BookingTime; public class GraphQLDataFetchers { @@ -211,9 +213,9 @@ public interface GraphQLBookingInfo { public DataFetcher dropOffMessage(); - public DataFetcher earliestBookingTime(); + public DataFetcher earliestBookingTime(); - public DataFetcher latestBookingTime(); + public DataFetcher latestBookingTime(); public DataFetcher maximumBookingNoticeSeconds(); @@ -449,7 +451,7 @@ public interface GraphQLLeg { public DataFetcher distance(); - public DataFetcher dropOffBookingInfo(); + public DataFetcher dropOffBookingInfo(); public DataFetcher dropoffType(); @@ -481,7 +483,7 @@ public interface GraphQLLeg { public DataFetcher> nextLegs(); - public DataFetcher pickupBookingInfo(); + public DataFetcher pickupBookingInfo(); public DataFetcher pickupType(); diff --git a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml index 29c2bff0257..70455ec3dad 100644 --- a/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml +++ b/src/main/java/org/opentripplanner/apis/gtfs/generated/graphql-codegen.yml @@ -45,8 +45,8 @@ config: RentalVehicle: org.opentripplanner.service.vehiclerental.model.VehicleRentalVehicle#VehicleRentalVehicle VehicleRentalUris: org.opentripplanner.service.vehiclerental.model.VehicleRentalStationUris#VehicleRentalStationUris BikesAllowed: org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLBikesAllowed#GraphQLBikesAllowed - BookingInfo: org.opentripplanner.model.BookingInfo - BookingTime: org.opentripplanner.model.BookingTime + BookingInfo: org.opentripplanner.transit.model.timetable.booking.BookingInfo#BookingInfo + BookingTime: org.opentripplanner.transit.model.timetable.booking.BookingTime#BookingTime CarPark: org.opentripplanner.routing.vehicle_parking.VehicleParking#VehicleParking ContactInfo: org.opentripplanner.transit.model.organization.ContactInfo Cluster: Object 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 5c579501254..72cd5fc6260 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapper.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapper.java @@ -52,6 +52,12 @@ public static RouteRequest createRequest(DataFetchingEnvironment environment) { "dateTime", millisSinceEpoch -> request.setDateTime(Instant.ofEpochMilli((long) millisSinceEpoch)) ); + + callWith.argument( + "bookingTime", + millisSinceEpoch -> request.setBookingTime(Instant.ofEpochMilli((long) millisSinceEpoch)) + ); + callWith.argument( "searchWindow", (Integer m) -> request.setSearchWindow(Duration.ofMinutes(m)) diff --git a/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java b/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java index b85d4ce19b9..6c173ba815f 100644 --- a/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java +++ b/src/main/java/org/opentripplanner/apis/transmodel/model/EnumTypes.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.function.Function; import org.opentripplanner.framework.doc.DocumentedEnum; -import org.opentripplanner.model.BookingMethod; import org.opentripplanner.model.plan.AbsoluteDirection; import org.opentripplanner.model.plan.RelativeDirection; import org.opentripplanner.model.plan.VertexType; @@ -27,6 +26,7 @@ import org.opentripplanner.transit.model.timetable.OccupancyStatus; import org.opentripplanner.transit.model.timetable.RealTimeState; import org.opentripplanner.transit.model.timetable.TripAlteration; +import org.opentripplanner.transit.model.timetable.booking.BookingMethod; public class EnumTypes { 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 bf34d9928e4..5ed3264a4fd 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 @@ -51,9 +51,26 @@ public static GraphQLFieldDefinition create( .newArgument() .name("dateTime") .description( - "Date and time for the earliest time the user is willing to start the journey " + - "(if arriveBy=false/not set) or the latest acceptable time of arriving " + - "(arriveBy=true). Defaults to now" + "The date and time for the earliest time the user is willing to start the journey " + + "(if `false` or not set) or the latest acceptable time of arriving " + + "(`true`). Defaults to now." + ) + .type(gqlUtil.dateTimeScalar) + .build() + ) + .argument( + GraphQLArgument + .newArgument() + .name("bookingTime") + .description( + """ + The date and time for the latest time the user is expected to book the journey. + Normally this is when the search is performed (now), plus a small grace period to + complete the booking. Services which must be booked before this time is excluded. The + `latestBookingTime` and `minimumBookingPeriod` in `BookingArrangement` (flexible + services only) is used to enforce this. If this parameter is _not set_, no booking-time + restrictions are applied - all journeys are listed. + """ ) .type(gqlUtil.dateTimeScalar) .build() 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 c2a11441695..de0f86b1166 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 @@ -8,9 +8,9 @@ import graphql.schema.GraphQLOutputType; import org.opentripplanner.apis.transmodel.model.EnumTypes; import org.opentripplanner.apis.transmodel.support.GqlUtil; -import org.opentripplanner.model.BookingInfo; -import org.opentripplanner.model.BookingTime; 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 { @@ -127,7 +127,8 @@ public static GraphQLObjectType create(GqlUtil gqlUtil) { return "other"; } } else if ( - earliestBookingTime.getDaysPrior() == 0 && latestBookingTime.getDaysPrior() == 0 + earliestBookingTime.getDaysPrior() == 0 && + (latestBookingTime == null || latestBookingTime.getDaysPrior() == 0) ) { return "dayOfTravelOnly"; } else { diff --git a/src/main/java/org/opentripplanner/framework/io/OtpHttpClientException.java b/src/main/java/org/opentripplanner/framework/io/OtpHttpClientException.java index a56fdcaa7f0..ff300cc7e3c 100644 --- a/src/main/java/org/opentripplanner/framework/io/OtpHttpClientException.java +++ b/src/main/java/org/opentripplanner/framework/io/OtpHttpClientException.java @@ -3,7 +3,7 @@ public class OtpHttpClientException extends RuntimeException { public OtpHttpClientException(Throwable cause) { - super(cause); + super(cause.getMessage(), cause); } public OtpHttpClientException(String message) { diff --git a/src/main/java/org/opentripplanner/framework/time/TimeUtils.java b/src/main/java/org/opentripplanner/framework/time/TimeUtils.java index 61549eeced3..9cc5594cb76 100644 --- a/src/main/java/org/opentripplanner/framework/time/TimeUtils.java +++ b/src/main/java/org/opentripplanner/framework/time/TimeUtils.java @@ -2,10 +2,12 @@ import java.security.SecureRandom; import java.time.Duration; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Locale; import java.util.Random; @@ -246,4 +248,14 @@ public static long busyWait(int waitMs) { } return value; } + + /** + * Calculate the relative time in seconds with the given {@code transitSearchTimeZero} as the + * base. There is no restriction on the returned time, it can be in the past(negative) and + * many days ahead of the base. This method can be used to translate an API instance of time + * into the OTP internal transit model time, when the search zero-point-in-time is known. + */ + public static int toTransitTimeSeconds(ZonedDateTime transitSearchTimeZero, Instant time) { + return (int) ChronoUnit.SECONDS.between(transitSearchTimeZero.toInstant(), time); + } } diff --git a/src/main/java/org/opentripplanner/graph_builder/module/AddTransitModelEntitiesToGraph.java b/src/main/java/org/opentripplanner/graph_builder/module/AddTransitModelEntitiesToGraph.java index 8b5653a4f55..a53730b104c 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/AddTransitModelEntitiesToGraph.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/AddTransitModelEntitiesToGraph.java @@ -27,7 +27,6 @@ import org.opentripplanner.street.model.vertex.TransitEntranceVertex; import org.opentripplanner.street.model.vertex.TransitPathwayNodeVertex; import org.opentripplanner.street.model.vertex.TransitStopVertex; -import org.opentripplanner.street.model.vertex.TransitStopVertexBuilder; import org.opentripplanner.street.model.vertex.VertexFactory; import org.opentripplanner.transit.model.basic.Accessibility; import org.opentripplanner.transit.model.basic.TransitMode; @@ -124,7 +123,7 @@ private void addStopsToGraphAndGenerateStopVertexes(TransitModel transitModel) { for (RegularStop stop : otpTransitService.stopModel().listRegularStops()) { Set modes = stopModeMap.get(stop); TransitStopVertex stopVertex = vertexFactory.transitStop( - new TransitStopVertexBuilder().withStop(stop).withModes(modes) + TransitStopVertex.of().withStop(stop).withModes(modes) ); if (modes != null && modes.contains(TransitMode.SUBWAY)) { diff --git a/src/main/java/org/opentripplanner/graph_builder/module/NearbyStopFinder.java b/src/main/java/org/opentripplanner/graph_builder/module/NearbyStopFinder.java index 7386b60b452..aca6e8a94cf 100644 --- a/src/main/java/org/opentripplanner/graph_builder/module/NearbyStopFinder.java +++ b/src/main/java/org/opentripplanner/graph_builder/module/NearbyStopFinder.java @@ -133,8 +133,8 @@ public Set findNearbyStopsConsideringPatterns( for (FlexTrip trip : transitService.getFlexIndex().getFlexTripsByStop(ts1)) { if ( reverseDirection - ? trip.isAlightingPossible(nearbyStop) - : trip.isBoardingPossible(nearbyStop) + ? trip.isAlightingPossible(nearbyStop.stop) + : trip.isBoardingPossible(nearbyStop.stop) ) { closestStopForFlexTrip.putMin(trip, nearbyStop); } diff --git a/src/main/java/org/opentripplanner/gtfs/mapping/BookingRuleMapper.java b/src/main/java/org/opentripplanner/gtfs/mapping/BookingRuleMapper.java index 54203bb91c1..74efdd52202 100644 --- a/src/main/java/org/opentripplanner/gtfs/mapping/BookingRuleMapper.java +++ b/src/main/java/org/opentripplanner/gtfs/mapping/BookingRuleMapper.java @@ -7,10 +7,10 @@ import java.util.Map; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.BookingRule; -import org.opentripplanner.model.BookingInfo; -import org.opentripplanner.model.BookingMethod; -import org.opentripplanner.model.BookingTime; import org.opentripplanner.transit.model.organization.ContactInfo; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; +import org.opentripplanner.transit.model.timetable.booking.BookingMethod; +import org.opentripplanner.transit.model.timetable.booking.BookingTime; /** Responsible for mapping GTFS BookingRule into the OTP model. */ class BookingRuleMapper { @@ -26,17 +26,18 @@ BookingInfo map(BookingRule rule) { return cachedBookingInfos.computeIfAbsent( rule.getId(), k -> - new BookingInfo( - contactInfo(rule), - bookingMethods(), - earliestBookingTime(rule), - latestBookingTime(rule), - minimumBookingNotice(rule), - maximumBookingNotice(rule), - message(rule), - pickupMessage(rule), - dropOffMessage(rule) - ) + BookingInfo + .of() + .withContactInfo(contactInfo(rule)) + .withBookingMethods(bookingMethods()) + .withEarliestBookingTime(earliestBookingTime(rule)) + .withLatestBookingTime(latestBookingTime(rule)) + .withMinimumBookingNotice(minimumBookingNotice(rule)) + .withMaximumBookingNotice(maximumBookingNotice(rule)) + .withMessage(message(rule)) + .withPickupMessage(pickupMessage(rule)) + .withDropOffMessage(dropOffMessage(rule)) + .build() ); } diff --git a/src/main/java/org/opentripplanner/model/BookingTime.java b/src/main/java/org/opentripplanner/model/BookingTime.java deleted file mode 100644 index 2582752f3c1..00000000000 --- a/src/main/java/org/opentripplanner/model/BookingTime.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.opentripplanner.model; - -import java.io.Serializable; -import java.time.LocalTime; - -/** - * Represents either an earliest or latest time a trip can be booked relative to the departure day - * of the trip. - */ -public class BookingTime implements Serializable { - - private final LocalTime time; - - private final int daysPrior; - - public BookingTime(LocalTime time, int daysPrior) { - this.time = time; - this.daysPrior = daysPrior; - } - - public LocalTime getTime() { - return time; - } - - public int getDaysPrior() { - return daysPrior; - } -} diff --git a/src/main/java/org/opentripplanner/model/StopTime.java b/src/main/java/org/opentripplanner/model/StopTime.java index e753b8d2885..edd5fbdb52d 100644 --- a/src/main/java/org/opentripplanner/model/StopTime.java +++ b/src/main/java/org/opentripplanner/model/StopTime.java @@ -7,6 +7,7 @@ import org.opentripplanner.transit.model.site.StopLocation; import org.opentripplanner.transit.model.timetable.StopTimeKey; import org.opentripplanner.transit.model.timetable.Trip; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; /** * This class is TEMPORALLY used during mapping of GTFS and Netex into the internal Model, it is not diff --git a/src/main/java/org/opentripplanner/model/TripTimeOnDate.java b/src/main/java/org/opentripplanner/model/TripTimeOnDate.java index 9ba85d6b532..1bfb0184138 100644 --- a/src/main/java/org/opentripplanner/model/TripTimeOnDate.java +++ b/src/main/java/org/opentripplanner/model/TripTimeOnDate.java @@ -13,6 +13,7 @@ import org.opentripplanner.transit.model.timetable.StopTimeKey; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/org/opentripplanner/model/plan/Leg.java b/src/main/java/org/opentripplanner/model/plan/Leg.java index 5a032def979..1ee72761d66 100644 --- a/src/main/java/org/opentripplanner/model/plan/Leg.java +++ b/src/main/java/org/opentripplanner/model/plan/Leg.java @@ -11,7 +11,6 @@ import org.locationtech.jts.geom.LineString; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.lang.Sandbox; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.fare.FareProductUse; import org.opentripplanner.model.plan.legreference.LegReference; @@ -25,6 +24,7 @@ import org.opentripplanner.transit.model.site.FareZone; import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; /** * One leg of a trip -- that is, a temporally continuous piece of the journey that takes place on a diff --git a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java index 6bf39d5aa4c..d94ec1895c2 100644 --- a/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java +++ b/src/main/java/org/opentripplanner/model/plan/ScheduledTransitLeg.java @@ -20,7 +20,6 @@ import org.opentripplanner.framework.lang.DoubleUtils; import org.opentripplanner.framework.time.ServiceDateUtils; import org.opentripplanner.framework.tostring.ToStringBuilder; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.model.PickDrop; import org.opentripplanner.model.fare.FareProductUse; import org.opentripplanner.model.plan.legreference.LegReference; @@ -38,6 +37,7 @@ import org.opentripplanner.transit.model.timetable.Trip; import org.opentripplanner.transit.model.timetable.TripOnServiceDate; import org.opentripplanner.transit.model.timetable.TripTimes; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; /** * One leg of a trip -- that is, a temporally continuous piece of the journey that takes place on a diff --git a/src/main/java/org/opentripplanner/netex/mapping/BookingInfoMapper.java b/src/main/java/org/opentripplanner/netex/mapping/BookingInfoMapper.java index afb9742fd85..d6c81c76e3d 100644 --- a/src/main/java/org/opentripplanner/netex/mapping/BookingInfoMapper.java +++ b/src/main/java/org/opentripplanner/netex/mapping/BookingInfoMapper.java @@ -9,10 +9,10 @@ import java.util.stream.Collectors; import javax.annotation.Nullable; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; -import org.opentripplanner.model.BookingInfo; -import org.opentripplanner.model.BookingMethod; -import org.opentripplanner.model.BookingTime; import org.opentripplanner.transit.model.organization.ContactInfo; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; +import org.opentripplanner.transit.model.timetable.booking.BookingMethod; +import org.opentripplanner.transit.model.timetable.booking.BookingTime; import org.rutebanken.netex.model.BookingArrangementsStructure; import org.rutebanken.netex.model.BookingMethodEnumeration; import org.rutebanken.netex.model.ContactStructure; @@ -45,14 +45,14 @@ BookingInfo map( ServiceJourney serviceJourney, FlexibleLine flexibleLine ) { - return new BookingInfoBuilder() + return new NetexBookingInfoBuilder() .withFlexibleLine(flexibleLine) .withServiceJourney(serviceJourney) .withStopPoint(stopPoint) .build(); } - private class BookingInfoBuilder { + private class NetexBookingInfoBuilder { private ContactStructure bookingContact; private List bookingMethods = new ArrayList<>(); @@ -65,7 +65,7 @@ private class BookingInfoBuilder { private String serviceJourneyRef; private String stopPointRef; - private BookingInfoBuilder withFlexibleLine(FlexibleLine flexibleLine) { + private NetexBookingInfoBuilder withFlexibleLine(FlexibleLine flexibleLine) { if (flexibleLine != null) { this.hasBookingInfo = true; this.flexibleLineRef = ref("FlexibleLine", flexibleLine); @@ -81,7 +81,7 @@ private BookingInfoBuilder withFlexibleLine(FlexibleLine flexibleLine) { return this; } - private BookingInfoBuilder withServiceJourney(ServiceJourney serviceJourney) { + private NetexBookingInfoBuilder withServiceJourney(ServiceJourney serviceJourney) { if (serviceJourney != null && serviceJourney.getFlexibleServiceProperties() != null) { this.hasBookingInfo = true; this.serviceJourneyRef = ref("ServiceJourney", serviceJourney); @@ -98,7 +98,7 @@ private BookingInfoBuilder withServiceJourney(ServiceJourney serviceJourney) { return this; } - private BookingInfoBuilder withStopPoint(StopPointInJourneyPattern stopPoint) { + private NetexBookingInfoBuilder withStopPoint(StopPointInJourneyPattern stopPoint) { BookingArrangementsStructure bookingArrangements = stopPoint.getBookingArrangements(); if (bookingArrangements != null) { this.hasBookingInfo = true; @@ -220,17 +220,16 @@ private BookingInfo build( } String bookingInfoMessage = bookingNote != null ? bookingNote.getValue() : null; - return new BookingInfo( - contactInfo, - filteredBookingMethods, - otpEarliestBookingTime, - otpLatestBookingTime, - minimumBookingNotice, - Duration.ZERO, - bookingInfoMessage, - null, - null - ); + return BookingInfo + .of() + .withContactInfo(contactInfo) + .withBookingMethods(filteredBookingMethods) + .withEarliestBookingTime(otpEarliestBookingTime) + .withLatestBookingTime(otpLatestBookingTime) + .withMinimumBookingNotice(minimumBookingNotice) + .withMaximumBookingNotice(Duration.ZERO) + .withMessage(bookingInfoMessage) + .build(); } private void setIfNotEmpty( @@ -241,24 +240,18 @@ private void setIfNotEmpty( Duration minimumBookingPeriod, MultilingualString bookingNote ) { - if (bookingContact != null) { - this.bookingContact = bookingContact; - } if (bookingMethods != null && !bookingMethods.isEmpty()) { this.bookingMethods = bookingMethods; } - if (latestBookingTime != null) { - this.latestBookingTime = latestBookingTime; - } - if (bookWhen != null) { - this.bookWhen = bookWhen; - } - if (minimumBookingPeriod != null) { - this.minimumBookingPeriod = minimumBookingPeriod; - } - if (bookingNote != null) { - this.bookingNote = bookingNote; - } + this.bookingContact = getOrDefault(bookingContact, this.bookingContact); + this.minimumBookingPeriod = getOrDefault(minimumBookingPeriod, this.minimumBookingPeriod); + this.latestBookingTime = getOrDefault(latestBookingTime, this.latestBookingTime); + this.bookWhen = getOrDefault(bookWhen, this.bookWhen); + this.bookingNote = getOrDefault(bookingNote, this.bookingNote); } } + + private static T getOrDefault(T value, T defaultValue) { + return value == null ? defaultValue : value; + } } diff --git a/src/main/java/org/opentripplanner/netex/mapping/BookingMethodMapper.java b/src/main/java/org/opentripplanner/netex/mapping/BookingMethodMapper.java index 6514667598b..4449913605b 100644 --- a/src/main/java/org/opentripplanner/netex/mapping/BookingMethodMapper.java +++ b/src/main/java/org/opentripplanner/netex/mapping/BookingMethodMapper.java @@ -1,6 +1,6 @@ package org.opentripplanner.netex.mapping; -import org.opentripplanner.model.BookingMethod; +import org.opentripplanner.transit.model.timetable.booking.BookingMethod; import org.rutebanken.netex.model.BookingMethodEnumeration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/org/opentripplanner/netex/mapping/StopTimesMapper.java b/src/main/java/org/opentripplanner/netex/mapping/StopTimesMapper.java index 06de1623337..676ab053da4 100644 --- a/src/main/java/org/opentripplanner/netex/mapping/StopTimesMapper.java +++ b/src/main/java/org/opentripplanner/netex/mapping/StopTimesMapper.java @@ -15,7 +15,6 @@ import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.model.StopTime; import org.opentripplanner.netex.index.api.ReadOnlyHierarchicalMap; import org.opentripplanner.netex.index.api.ReadOnlyHierarchicalMapById; @@ -27,6 +26,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.BookingInfo; import org.rutebanken.netex.model.DestinationDisplay; import org.rutebanken.netex.model.DestinationDisplay_VersionStructure; import org.rutebanken.netex.model.FlexibleLine; diff --git a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/framework/groupids/GroupByDistance.java b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/framework/groupids/GroupByDistance.java index 7ab605fb792..ee67e4f76d0 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/filterchain/framework/groupids/GroupByDistance.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/filterchain/framework/groupids/GroupByDistance.java @@ -10,7 +10,7 @@ import org.opentripplanner.routing.algorithm.filterchain.framework.spi.GroupId; /** - * This class create a group identifier for an itinerary based on the longest legs which together + * This class creates a group identifier for an itinerary based on the longest legs which together * account for more than 'p' part of the total distance. Transit legs must overlap and ride the * same trip, while street-legs only need to have the same mode. We call the set of legs the * 'key-set-of-legs' or just 'key-set'. diff --git a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java index 2780cc8ba14..d7098c20661 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/mapping/RaptorPathToItineraryMapper.java @@ -24,8 +24,8 @@ import org.opentripplanner.raptor.api.path.RaptorPath; import org.opentripplanner.raptor.api.path.TransferPathLeg; import org.opentripplanner.raptor.api.path.TransitPathLeg; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.DefaultAccessEgress; import org.opentripplanner.routing.algorithm.raptoradapter.transit.DefaultRaptorTransfer; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; import org.opentripplanner.routing.algorithm.raptoradapter.transit.Transfer; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; @@ -146,12 +146,12 @@ else if (pathLeg.isTransferLeg()) { } var penaltyCost = 0; - if (accessPathLeg.access() instanceof DefaultAccessEgress ae) { + if (accessPathLeg.access() instanceof RoutingAccessEgress ae) { itinerary.setAccessPenalty(ae.penalty()); penaltyCost += ae.penalty().cost().toSeconds(); } - if (egressPathLeg.egress() instanceof DefaultAccessEgress ae) { + if (egressPathLeg.egress() instanceof RoutingAccessEgress ae) { itinerary.setEgressPenalty(ae.penalty()); penaltyCost += ae.penalty().cost().toSeconds(); } @@ -169,7 +169,7 @@ private List mapAccessLeg(AccessPathLeg accessPathLeg) { return List.of(); } - DefaultAccessEgress accessPath = (DefaultAccessEgress) accessPathLeg.access(); + RoutingAccessEgress accessPath = (RoutingAccessEgress) accessPathLeg.access(); var graphPath = new GraphPath<>(accessPath.getLastState()); @@ -279,7 +279,7 @@ private Itinerary mapEgressLeg(EgressPathLeg egressPathLeg) { return null; } - DefaultAccessEgress egressPath = (DefaultAccessEgress) egressPathLeg.egress(); + RoutingAccessEgress egressPath = (RoutingAccessEgress) egressPathLeg.egress(); var graphPath = new GraphPath<>(egressPath.getLastState()); 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 0a9e46d2fe3..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 @@ -23,7 +23,7 @@ import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressType; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgresses; import org.opentripplanner.routing.algorithm.raptoradapter.router.street.FlexAccessEgressRouter; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.DefaultAccessEgress; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TransitLayer; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.AccessEgressMapper; @@ -171,8 +171,8 @@ private TransitRouterResult route() { } private AccessEgresses fetchAccessEgresses() { - final var asyncAccessList = new ArrayList(); - final var asyncEgressList = new ArrayList(); + final var accessList = new ArrayList(); + final var egressList = new ArrayList(); if (OTPFeature.ParallelRouting.isOn()) { try { @@ -180,19 +180,19 @@ private AccessEgresses fetchAccessEgresses() { // log-trace-parameters-propagation and graceful timeout handling here. CompletableFuture .allOf( - CompletableFuture.runAsync(() -> asyncAccessList.addAll(fetchAccess())), - CompletableFuture.runAsync(() -> asyncEgressList.addAll(fetchEgress())) + CompletableFuture.runAsync(() -> accessList.addAll(fetchAccess())), + CompletableFuture.runAsync(() -> egressList.addAll(fetchEgress())) ) .join(); } catch (CompletionException e) { RoutingValidationException.unwrapAndRethrowCompletionException(e); } } else { - asyncAccessList.addAll(fetchAccess()); - asyncEgressList.addAll(fetchEgress()); + accessList.addAll(fetchAccess()); + egressList.addAll(fetchEgress()); } - verifyAccessEgress(asyncAccessList, asyncEgressList); + verifyAccessEgress(accessList, egressList); // Decorate access/egress with a penalty to make it less favourable than transit var penaltyDecorator = new AccessEgressPenaltyDecorator( @@ -201,27 +201,27 @@ private AccessEgresses fetchAccessEgresses() { request.preferences().street().accessEgress().penalty() ); - var accessList = penaltyDecorator.decorateAccess(asyncAccessList); - var egressList = penaltyDecorator.decorateEgress(asyncEgressList); + var accessListWithPenalty = penaltyDecorator.decorateAccess(accessList); + var egressListWithPenalty = penaltyDecorator.decorateEgress(egressList); - return new AccessEgresses(accessList, egressList); + return new AccessEgresses(accessListWithPenalty, egressListWithPenalty); } - private Collection fetchAccess() { + private Collection fetchAccess() { debugTimingAggregator.startedAccessCalculating(); var list = fetchAccessEgresses(ACCESS); debugTimingAggregator.finishedAccessCalculating(); return list; } - private Collection fetchEgress() { + private Collection fetchEgress() { debugTimingAggregator.startedEgressCalculating(); var list = fetchAccessEgresses(EGRESS); debugTimingAggregator.finishedEgressCalculating(); return list; } - private Collection fetchAccessEgresses(AccessEgressType type) { + private Collection fetchAccessEgresses(AccessEgressType type) { var streetRequest = type.isAccess() ? request.journey().access() : request.journey().egress(); // Prepare access/egress lists @@ -255,7 +255,7 @@ private Collection fetchAccessEgresses(AccessEgressType typ stopCountLimit ); - List results = new ArrayList<>( + List results = new ArrayList<>( AccessEgressMapper.mapNearbyStops(nearbyStops, type.isEgress()) ); results = timeshiftRideHailing(streetRequest, type, results); @@ -267,7 +267,7 @@ private Collection fetchAccessEgresses(AccessEgressType typ temporaryVerticesContainer, serverContext, additionalSearchDays, - serverContext.flexConfig(), + serverContext.flexParameters(), serverContext.dataOverlayContext(accessRequest), type.isEgress() ); @@ -288,10 +288,10 @@ private Collection fetchAccessEgresses(AccessEgressType typ * This method is a good candidate to be moved to the access/egress filter chain when that has * been added. */ - private List timeshiftRideHailing( + private List timeshiftRideHailing( StreetRequest streetRequest, AccessEgressType type, - List accessEgressList + List accessEgressList ) { if (streetRequest.mode() != StreetMode.CAR_HAILING) { return accessEgressList; diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressPenaltyDecorator.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressPenaltyDecorator.java index 8f8d73c1acd..5adb1fdc001 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressPenaltyDecorator.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressPenaltyDecorator.java @@ -1,7 +1,7 @@ package org.opentripplanner.routing.algorithm.raptoradapter.router.street; import java.util.Collection; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.DefaultAccessEgress; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.framework.TimeAndCostPenaltyForEnum; @@ -25,19 +25,23 @@ public AccessEgressPenaltyDecorator( this.penalty = penalty; } - public Collection decorateAccess(Collection list) { + public Collection decorateAccess( + Collection list + ) { return decorate(list, accessMode); } - public Collection decorateEgress(Collection list) { + public Collection decorateEgress( + Collection list + ) { return decorate(list, egressMode); } /** * Decorate each access-egress with a penalty according to the specified street-mode. */ - private Collection decorate( - Collection input, + private Collection decorate( + Collection input, StreetMode requestedMode ) { if (input.isEmpty()) { diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgresses.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgresses.java index f2e8fe6c9f1..50b4f528c5b 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgresses.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgresses.java @@ -1,26 +1,26 @@ package org.opentripplanner.routing.algorithm.raptoradapter.router.street; import java.util.Collection; -import org.opentripplanner.routing.algorithm.raptoradapter.transit.DefaultAccessEgress; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; public class AccessEgresses { - private final Collection accesses; - private final Collection egresses; + private final Collection accesses; + private final Collection egresses; public AccessEgresses( - Collection accesses, - Collection egresses + Collection accesses, + Collection egresses ) { this.accesses = accesses; this.egresses = egresses; } - public Collection getAccesses() { + public Collection getAccesses() { return accesses; } - public Collection getEgresses() { + public Collection getEgresses() { return egresses; } } 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 5a4892ee96b..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 @@ -41,7 +41,7 @@ public static List route( request.journey().direct(), serverContext.dataOverlayContext(request), false, - serverContext.flexConfig().maxAccessWalkDuration(), + serverContext.flexParameters().maxAccessWalkDuration(), 0 ); Collection egressStops = AccessEgressRouter.streetSearch( @@ -51,23 +51,23 @@ public static List route( request.journey().direct(), serverContext.dataOverlayContext(request), true, - serverContext.flexConfig().maxEgressWalkDuration(), + serverContext.flexParameters().maxEgressWalkDuration(), 0 ); - FlexRouter flexRouter = new FlexRouter( + var flexRouter = new FlexRouter( serverContext.graph(), serverContext.transitService(), - serverContext.flexConfig(), + serverContext.flexParameters(), request.dateTime(), - request.arriveBy(), + request.bookingTime(), additionalSearchDays.additionalSearchDaysInPast(), additionalSearchDays.additionalSearchDaysInFuture(), accessStops, egressStops ); - return new ArrayList<>(flexRouter.createFlexOnlyItineraries()); + return new ArrayList<>(flexRouter.createFlexOnlyItineraries(request.arriveBy())); } } } 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 639a6dbb997..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 @@ -4,6 +4,7 @@ import java.util.List; import org.opentripplanner.ext.dataoverlay.routing.DataOverlayContext; import org.opentripplanner.ext.flex.FlexAccessEgress; +import org.opentripplanner.ext.flex.FlexParameters; import org.opentripplanner.ext.flex.FlexRouter; import org.opentripplanner.framework.application.OTPRequestTimeoutException; import org.opentripplanner.routing.algorithm.raptoradapter.router.AdditionalSearchDays; @@ -12,7 +13,6 @@ import org.opentripplanner.routing.api.request.request.StreetRequest; import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.standalone.api.OtpServerRequestContext; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.street.search.TemporaryVerticesContainer; import org.opentripplanner.transit.service.TransitService; @@ -25,7 +25,7 @@ public static Collection routeAccessEgress( TemporaryVerticesContainer verticesContainer, OtpServerRequestContext serverContext, AdditionalSearchDays searchDays, - FlexConfig config, + FlexParameters config, DataOverlayContext dataOverlayContext, boolean isEgress ) { @@ -41,7 +41,7 @@ public static Collection routeAccessEgress( new StreetRequest(StreetMode.WALK), dataOverlayContext, false, - serverContext.flexConfig().maxAccessWalkDuration(), + serverContext.flexParameters().maxAccessWalkDuration(), 0 ) : List.of(); @@ -54,7 +54,7 @@ public static Collection routeAccessEgress( new StreetRequest(StreetMode.WALK), dataOverlayContext, true, - serverContext.flexConfig().maxEgressWalkDuration(), + serverContext.flexParameters().maxEgressWalkDuration(), 0 ) : List.of(); @@ -64,7 +64,7 @@ public static Collection routeAccessEgress( transitService, config, request.dateTime(), - request.arriveBy(), + request.bookingTime(), searchDays.additionalSearchDaysInPast(), searchDays.additionalSearchDaysInFuture(), accessStops, diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java index 35987c0af7d..3f68d91321e 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgress.java @@ -2,7 +2,6 @@ import java.util.Objects; import org.opentripplanner.framework.model.TimeAndCost; -import org.opentripplanner.raptor.api.model.RaptorAccessEgress; import org.opentripplanner.raptor.api.model.RaptorConstants; import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.RaptorCostConverter; import org.opentripplanner.street.search.state.State; @@ -10,7 +9,7 @@ /** * Default implementation of the RaptorAccessEgress interface. */ -public class DefaultAccessEgress implements RaptorAccessEgress { +public class DefaultAccessEgress implements RoutingAccessEgress { private final int stop; private final int durationInSeconds; @@ -34,7 +33,7 @@ public DefaultAccessEgress(int stop, State lastState) { this.penalty = TimeAndCost.ZERO; } - protected DefaultAccessEgress(DefaultAccessEgress other, TimeAndCost penalty) { + protected DefaultAccessEgress(RoutingAccessEgress other, TimeAndCost penalty) { if (other.hasPenalty()) { throw new IllegalStateException("Can not add penalty twice..."); } @@ -49,15 +48,6 @@ protected DefaultAccessEgress(DefaultAccessEgress other, TimeAndCost penalty) { this.lastState = other.getLastState(); } - /** - * Return a new copy of this with the requested penalty. - *

- * OVERRIDE THIS IF KEEPING THE TYPE IS IMPORTANT! - */ - public DefaultAccessEgress withPenalty(TimeAndCost penalty) { - return new DefaultAccessEgress(this, penalty); - } - @Override public int durationInSeconds() { return durationInSeconds; @@ -83,22 +73,36 @@ public boolean hasOpeningHours() { return false; } + @Override public State getLastState() { return lastState; } + @Override public boolean isWalkOnly() { return lastState.containsOnlyWalkMode(); } + @Override public boolean hasPenalty() { return !penalty.isZero(); } + @Override public TimeAndCost penalty() { return penalty; } + /** + * Return a new copy of this with the requested penalty. + *

+ * OVERRIDE THIS IF KEEPING THE TYPE IS IMPORTANT! + */ + @Override + public RoutingAccessEgress withPenalty(TimeAndCost penalty) { + return new DefaultAccessEgress(this, penalty); + } + @Override public int earliestDepartureTime(int requestedDepartureTime) { return requestedDepartureTime; @@ -121,7 +125,7 @@ public final boolean equals(Object o) { } // We check the contract of DefaultAccessEgress used for routing for equality, we do not care // if the entries are different implementation or have different AStar paths(lastState). - if (!(o instanceof DefaultAccessEgress that)) { + if (!(o instanceof RoutingAccessEgress that)) { return false; } return ( 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 0cf95b544f4..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 @@ -59,7 +59,7 @@ public boolean isWalkOnly() { } @Override - public DefaultAccessEgress withPenalty(TimeAndCost penalty) { + public RoutingAccessEgress withPenalty(TimeAndCost penalty) { return new FlexAccessEgressAdapter(this, penalty); } 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 new file mode 100644 index 00000000000..d22ec0f71f9 --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/RoutingAccessEgress.java @@ -0,0 +1,33 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.transit; + +import org.opentripplanner.framework.model.TimeAndCost; +import org.opentripplanner.raptor.api.model.RaptorAccessEgress; +import org.opentripplanner.street.search.state.State; + +/** + * Encapsulate information about an access or egress path. This interface extends + * {@link RaptorAccessEgress} with methods relevant only to street routing and + * access/egress filtering. + */ +public interface RoutingAccessEgress extends RaptorAccessEgress { + /** + * Return a new copy of this with the requested penalty. + *

+ * OVERRIDE THIS IF KEEPING THE TYPE IS IMPORTANT! + */ + RoutingAccessEgress withPenalty(TimeAndCost penalty); + + /** + * Return the last state both in the case of access and egress. + */ + State getLastState(); + + /** + * Return true if all edges are traversed on foot. + */ + boolean isWalkOnly(); + + boolean hasPenalty(); + + TimeAndCost penalty(); +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/AccessEgressMapper.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/AccessEgressMapper.java index 6cef677f5a9..fd7619ac297 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/AccessEgressMapper.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/mappers/AccessEgressMapper.java @@ -7,12 +7,13 @@ import org.opentripplanner.ext.flex.FlexAccessEgress; import org.opentripplanner.routing.algorithm.raptoradapter.transit.DefaultAccessEgress; import org.opentripplanner.routing.algorithm.raptoradapter.transit.FlexAccessEgressAdapter; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; import org.opentripplanner.routing.graphfinder.NearbyStop; import org.opentripplanner.transit.model.site.RegularStop; public class AccessEgressMapper { - public static List mapNearbyStops( + public static List mapNearbyStops( Collection accessStops, boolean isEgress ) { @@ -23,7 +24,7 @@ public static List mapNearbyStops( .collect(Collectors.toList()); } - public static Collection mapFlexAccessEgresses( + public static Collection mapFlexAccessEgresses( Collection flexAccessEgresses, boolean isEgress ) { @@ -33,7 +34,7 @@ public static Collection mapFlexAccessEgresses( .collect(Collectors.toList()); } - private static DefaultAccessEgress mapNearbyStop(NearbyStop nearbyStop, boolean isEgress) { + private static RoutingAccessEgress mapNearbyStop(NearbyStop nearbyStop, boolean isEgress) { if (!(nearbyStop.stop instanceof RegularStop)) { return null; } 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 ce2fdb31c44..76e5dcc558a 100644 --- a/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java +++ b/src/main/java/org/opentripplanner/routing/api/request/RouteRequest.java @@ -81,6 +81,8 @@ public class RouteRequest implements Cloneable, Serializable { private boolean wheelchair = false; + private Instant bookingTime; + /* CONSTRUCTORS */ /** Constructor for options; modes defaults to walk and transit */ @@ -112,6 +114,20 @@ public void withPreferences(Consumer body) { this.preferences = preferences.copyOf().apply(body).build(); } + /** + * The booking time is used to exclude services which are not bookable at the + * requested booking time. If a service is bookable at this time or later, the service + * is included. This apply to FLEX access, egress and direct services. + */ + public Instant bookingTime() { + return bookingTime; + } + + public RouteRequest setBookingTime(Instant bookingTime) { + this.bookingTime = bookingTime; + return this; + } + void setPreferences(RoutingPreferences preferences) { this.preferences = preferences; } diff --git a/src/main/java/org/opentripplanner/routing/graphfinder/NearbyStop.java b/src/main/java/org/opentripplanner/routing/graphfinder/NearbyStop.java index 4c35f11de8a..ada17120c6b 100644 --- a/src/main/java/org/opentripplanner/routing/graphfinder/NearbyStop.java +++ b/src/main/java/org/opentripplanner/routing/graphfinder/NearbyStop.java @@ -43,6 +43,15 @@ public static NearbyStop nearbyStopForState(State state, StopLocation stop) { return new NearbyStop(stop, effectiveWalkDistance, edges, state); } + /** + * Return {@code true} if this instance has a lower weight/cost than the given {@code other}. + * If the state is not set, the distance is used for comparison instead. If the + * weight/cost/distance is equals (or worse) this method returns {@code false}. + */ + public boolean isBetter(NearbyStop other) { + return compareTo(other) < 0; + } + @Override public int compareTo(NearbyStop that) { if ((this.state == null) != (that.state == null)) { diff --git a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java index b632ea6104b..6552d82770f 100644 --- a/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/api/OtpServerRequestContext.java @@ -7,6 +7,7 @@ import org.opentripplanner.astar.spi.TraverseVisitor; import org.opentripplanner.ext.dataoverlay.routing.DataOverlayContext; import org.opentripplanner.ext.emissions.EmissionsService; +import org.opentripplanner.ext.flex.FlexParameters; import org.opentripplanner.ext.ridehailing.RideHailingService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.framework.application.OTPFeature; @@ -23,7 +24,6 @@ import org.opentripplanner.service.vehiclerental.VehicleRentalService; import org.opentripplanner.service.worldenvelope.WorldEnvelopeService; import org.opentripplanner.standalone.config.routerconfig.VectorTileConfig; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.search.state.State; import org.opentripplanner.street.service.StreetLimitationParametersService; @@ -124,7 +124,7 @@ default GraphFinder graphFinder() { return GraphFinder.getInstance(graph(), transitService()::findRegularStops); } - FlexConfig flexConfig(); + FlexParameters flexParameters(); VectorTileConfig vectorTileConfig(); diff --git a/src/main/java/org/opentripplanner/standalone/config/RouterConfig.java b/src/main/java/org/opentripplanner/standalone/config/RouterConfig.java index 4ac0688a759..55128af5659 100644 --- a/src/main/java/org/opentripplanner/standalone/config/RouterConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/RouterConfig.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.node.MissingNode; import java.io.Serializable; import java.util.List; +import org.opentripplanner.ext.flex.FlexParameters; import org.opentripplanner.ext.ridehailing.RideHailingServiceParameters; import org.opentripplanner.routing.api.request.RouteRequest; import org.opentripplanner.standalone.config.framework.json.NodeAdapter; @@ -127,7 +128,7 @@ public VectorTileConfig vectorTileConfig() { return vectorTileConfig; } - public FlexConfig flexConfig() { + public FlexParameters flexParameters() { return flexConfig; } diff --git a/src/main/java/org/opentripplanner/standalone/config/sandbox/FlexConfig.java b/src/main/java/org/opentripplanner/standalone/config/sandbox/FlexConfig.java index cefec90b09e..2521ca5e57b 100644 --- a/src/main/java/org/opentripplanner/standalone/config/sandbox/FlexConfig.java +++ b/src/main/java/org/opentripplanner/standalone/config/sandbox/FlexConfig.java @@ -4,11 +4,12 @@ import static org.opentripplanner.standalone.config.framework.json.OtpVersion.V2_3; import java.time.Duration; +import org.opentripplanner.ext.flex.FlexParameters; import org.opentripplanner.standalone.config.framework.json.NodeAdapter; -public class FlexConfig { +public class FlexConfig implements FlexParameters { - public static final FlexConfig DEFAULT = new FlexConfig(); + private static final FlexParameters DEFAULT = FlexParameters.defaultValues(); public static final String ACCESS_EGRESS_DESCRIPTION = """ @@ -58,7 +59,7 @@ public FlexConfig(NodeAdapter root, String parameterName) { A lower value means that the routing is faster. """ ) - .asDuration(DEFAULT.maxTransferDuration); + .asDuration(DEFAULT.maxTransferDuration()); maxFlexTripDuration = json @@ -70,7 +71,7 @@ public FlexConfig(NodeAdapter root, String parameterName) { "the access/egress duration to the boarding/alighting of the flex trip, as well as the " + "connection to the transit stop." ) - .asDuration(DEFAULT.maxFlexTripDuration); + .asDuration(DEFAULT.maxFlexTripDuration()); maxAccessWalkDuration = json @@ -80,7 +81,7 @@ public FlexConfig(NodeAdapter root, String parameterName) { "The maximum duration the passenger will be allowed to walk to reach a flex stop or zone." ) .description(ACCESS_EGRESS_DESCRIPTION) - .asDuration(DEFAULT.maxAccessWalkDuration); + .asDuration(DEFAULT.maxAccessWalkDuration()); maxEgressWalkDuration = json @@ -90,7 +91,7 @@ public FlexConfig(NodeAdapter root, String parameterName) { "The maximum duration the passenger will be allowed to walk after leaving the flex vehicle at the final destination." ) .description(ACCESS_EGRESS_DESCRIPTION) - .asDuration(DEFAULT.maxEgressWalkDuration); + .asDuration(DEFAULT.maxEgressWalkDuration()); } public Duration maxFlexTripDuration() { diff --git a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java index 93158f87cff..eb244ce726c 100644 --- a/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java +++ b/src/main/java/org/opentripplanner/standalone/configure/ConstructApplicationModule.java @@ -56,7 +56,7 @@ OtpServerRequestContext providesServerContext( realtimeVehicleService, vehicleRentalService, emissionsService, - routerConfig.flexConfig(), + routerConfig.flexParameters(), rideHailingServices, stopConsolidationService, streetLimitationParametersService, diff --git a/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java b/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java index 019b7267015..7a4ccea9247 100644 --- a/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java +++ b/src/main/java/org/opentripplanner/standalone/server/DefaultServerRequestContext.java @@ -6,6 +6,7 @@ import javax.annotation.Nullable; import org.opentripplanner.astar.spi.TraverseVisitor; import org.opentripplanner.ext.emissions.EmissionsService; +import org.opentripplanner.ext.flex.FlexParameters; import org.opentripplanner.ext.ridehailing.RideHailingService; import org.opentripplanner.ext.stopconsolidation.StopConsolidationService; import org.opentripplanner.inspector.raster.TileRendererManager; @@ -24,7 +25,6 @@ import org.opentripplanner.standalone.api.OtpServerRequestContext; import org.opentripplanner.standalone.config.routerconfig.TransitRoutingConfig; import org.opentripplanner.standalone.config.routerconfig.VectorTileConfig; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.street.service.StreetLimitationParametersService; import org.opentripplanner.transit.service.TransitService; @@ -41,7 +41,7 @@ public class DefaultServerRequestContext implements OtpServerRequestContext { private final RaptorConfig raptorConfig; private final TileRendererManager tileRendererManager; private final VectorTileConfig vectorTileConfig; - private final FlexConfig flexConfig; + private final FlexParameters flexParameters; private final TraverseVisitor traverseVisitor; private final WorldEnvelopeService worldEnvelopeService; private final RealtimeVehicleService realtimeVehicleService; @@ -69,7 +69,7 @@ private DefaultServerRequestContext( List rideHailingServices, StopConsolidationService stopConsolidationService, StreetLimitationParametersService streetLimitationParametersService, - FlexConfig flexConfig, + FlexParameters flexParameters, TraverseVisitor traverseVisitor ) { this.graph = graph; @@ -80,7 +80,7 @@ private DefaultServerRequestContext( this.tileRendererManager = tileRendererManager; this.vectorTileConfig = vectorTileConfig; this.vehicleRentalService = vehicleRentalService; - this.flexConfig = flexConfig; + this.flexParameters = flexParameters; this.traverseVisitor = traverseVisitor; this.routeRequestDefaults = routeRequestDefaults; this.worldEnvelopeService = worldEnvelopeService; @@ -106,7 +106,7 @@ public static DefaultServerRequestContext create( RealtimeVehicleService realtimeVehicleService, VehicleRentalService vehicleRentalService, @Nullable EmissionsService emissionsService, - FlexConfig flexConfig, + FlexParameters flexParameters, List rideHailingServices, @Nullable StopConsolidationService stopConsolidationService, StreetLimitationParametersService streetLimitationParametersService, @@ -128,7 +128,7 @@ public static DefaultServerRequestContext create( rideHailingServices, stopConsolidationService, streetLimitationParametersService, - flexConfig, + flexParameters, traverseVisitor ); } @@ -226,8 +226,8 @@ public TraverseVisitor traverseVisitor() { } @Override - public FlexConfig flexConfig() { - return flexConfig; + public FlexParameters flexParameters() { + return flexParameters; } @Override diff --git a/src/main/java/org/opentripplanner/street/model/vertex/TransitStopVertex.java b/src/main/java/org/opentripplanner/street/model/vertex/TransitStopVertex.java index d196c7d1830..a9d7d221d44 100644 --- a/src/main/java/org/opentripplanner/street/model/vertex/TransitStopVertex.java +++ b/src/main/java/org/opentripplanner/street/model/vertex/TransitStopVertex.java @@ -41,6 +41,10 @@ public class TransitStopVertex extends StationElementVertex { this.wheelchairAccessibility = stop.getWheelchairAccessibility(); } + public static TransitStopVertexBuilder of() { + return new TransitStopVertexBuilder(); + } + public Accessibility getWheelchairAccessibility() { return wheelchairAccessibility; } diff --git a/src/main/java/org/opentripplanner/street/model/vertex/TransitStopVertexBuilder.java b/src/main/java/org/opentripplanner/street/model/vertex/TransitStopVertexBuilder.java index 83369629af5..38fe0d45790 100644 --- a/src/main/java/org/opentripplanner/street/model/vertex/TransitStopVertexBuilder.java +++ b/src/main/java/org/opentripplanner/street/model/vertex/TransitStopVertexBuilder.java @@ -10,6 +10,12 @@ public class TransitStopVertexBuilder { private RegularStop stop; private Set modes; + /** + * Protected access to avoid instantiation, use + * {@link org.opentripplanner.street.model.vertex.TransitStopVertex#of()} method instead. + */ + TransitStopVertexBuilder() {} + public TransitStopVertexBuilder withStop(RegularStop stop) { this.stop = stop; return this; diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/RealTimeTripTimes.java b/src/main/java/org/opentripplanner/transit/model/timetable/RealTimeTripTimes.java index a62da95e79a..8eea45c092a 100644 --- a/src/main/java/org/opentripplanner/transit/model/timetable/RealTimeTripTimes.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/RealTimeTripTimes.java @@ -10,9 +10,9 @@ import java.util.function.IntUnaryOperator; import javax.annotation.Nullable; import org.opentripplanner.framework.i18n.I18NString; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.transit.model.basic.Accessibility; import org.opentripplanner.transit.model.framework.DataValidationException; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; /** * A TripTimes represents the arrival and departure times for a single trip in an Timetable. It is diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimes.java b/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimes.java index ff3f61394ae..d367932d24d 100644 --- a/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimes.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimes.java @@ -16,11 +16,11 @@ import org.opentripplanner.framework.lang.IntUtils; import org.opentripplanner.framework.time.DurationUtils; import org.opentripplanner.framework.time.TimeUtils; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.transit.model.basic.Accessibility; import org.opentripplanner.transit.model.framework.DataValidationException; import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.framework.DeduplicatorService; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; /** * Regular/planed/scheduled read-only version of {@link TripTimes}. The set of static diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimesBuilder.java b/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimesBuilder.java index 0eee5d3e183..ce142fd7628 100644 --- a/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimesBuilder.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/ScheduledTripTimesBuilder.java @@ -5,8 +5,8 @@ import javax.annotation.Nullable; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.time.TimeUtils; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.transit.model.framework.DeduplicatorService; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; public class ScheduledTripTimesBuilder { diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/StopTimeToScheduledTripTimesMapper.java b/src/main/java/org/opentripplanner/transit/model/timetable/StopTimeToScheduledTripTimesMapper.java index bd21905f7d1..0779575aee5 100644 --- a/src/main/java/org/opentripplanner/transit/model/timetable/StopTimeToScheduledTripTimesMapper.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/StopTimeToScheduledTripTimesMapper.java @@ -5,9 +5,9 @@ import java.util.Collection; import java.util.List; import org.opentripplanner.framework.i18n.I18NString; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.model.StopTime; import org.opentripplanner.transit.model.framework.DeduplicatorService; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; class StopTimeToScheduledTripTimesMapper { diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java b/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java index 7bf1a826c1c..ff6b022fb59 100644 --- a/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/TripTimes.java @@ -7,8 +7,8 @@ import java.util.OptionalInt; import javax.annotation.Nullable; import org.opentripplanner.framework.i18n.I18NString; -import org.opentripplanner.model.BookingInfo; import org.opentripplanner.transit.model.basic.Accessibility; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; /** * A TripTimes represents the arrival and departure times for a single trip in a timetable. It is diff --git a/src/main/java/org/opentripplanner/model/BookingInfo.java b/src/main/java/org/opentripplanner/transit/model/timetable/booking/BookingInfo.java similarity index 57% rename from src/main/java/org/opentripplanner/model/BookingInfo.java rename to src/main/java/org/opentripplanner/transit/model/timetable/booking/BookingInfo.java index 4dc0c17c784..3e23696b65a 100644 --- a/src/main/java/org/opentripplanner/model/BookingInfo.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/booking/BookingInfo.java @@ -1,15 +1,15 @@ -package org.opentripplanner.model; +package org.opentripplanner.transit.model.timetable.booking; import java.io.Serializable; import java.time.Duration; import java.util.EnumSet; +import javax.annotation.Nullable; +import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.transit.model.organization.ContactInfo; /** * Info about how a trip might be booked at a particular stop. All of this is pass-through * information, except information about booking time and booking notice. - *

- * // TODO Make the routing take into account booking time and booking notice. */ public class BookingInfo implements Serializable { @@ -20,58 +20,55 @@ public class BookingInfo implements Serializable { /** * Cannot be set at the same time as minimumBookingNotice or maximumBookingNotice */ + @Nullable private final BookingTime earliestBookingTime; /** * Cannot be set at the same time as minimumBookingNotice or maximumBookingNotice */ + @Nullable private final BookingTime latestBookingTime; /** * Cannot be set at the same time as earliestBookingTime or latestBookingTime */ + @Nullable private final Duration minimumBookingNotice; /** * Cannot be set at the same time as earliestBookingTime or latestBookingTime */ + @Nullable private final Duration maximumBookingNotice; + @Nullable private final String message; + @Nullable private final String pickupMessage; + @Nullable private final String dropOffMessage; - public BookingInfo( - ContactInfo contactInfo, - EnumSet bookingMethods, - BookingTime earliestBookingTime, - BookingTime latestBookingTime, - Duration minimumBookingNotice, - Duration maximumBookingNotice, - String message, - String pickupMessage, - String dropOffMessage - ) { - this.contactInfo = contactInfo; - this.bookingMethods = bookingMethods; - this.message = message; - this.pickupMessage = pickupMessage; - this.dropOffMessage = dropOffMessage; + BookingInfo(BookingInfoBuilder builder) { + this.contactInfo = builder.contactInfo; + this.bookingMethods = builder.bookingMethods; + this.message = builder.message; + this.pickupMessage = builder.pickupMessage; + this.dropOffMessage = builder.dropOffMessage; // Ensure that earliestBookingTime/latestBookingTime is not set at the same time as // minimumBookingNotice/maximumBookingNotice - if (earliestBookingTime != null || latestBookingTime != null) { - this.earliestBookingTime = earliestBookingTime; - this.latestBookingTime = latestBookingTime; + if (builder.earliestBookingTime != null || builder.latestBookingTime != null) { + this.earliestBookingTime = builder.earliestBookingTime; + this.latestBookingTime = builder.latestBookingTime; this.minimumBookingNotice = null; this.maximumBookingNotice = null; - } else if (minimumBookingNotice != null || maximumBookingNotice != null) { + } else if (builder.minimumBookingNotice != null || builder.maximumBookingNotice != null) { this.earliestBookingTime = null; this.latestBookingTime = null; - this.minimumBookingNotice = minimumBookingNotice; - this.maximumBookingNotice = maximumBookingNotice; + this.minimumBookingNotice = builder.minimumBookingNotice; + this.maximumBookingNotice = builder.maximumBookingNotice; } else { this.earliestBookingTime = null; this.latestBookingTime = null; @@ -80,6 +77,10 @@ public BookingInfo( } } + public static BookingInfoBuilder of() { + return new BookingInfoBuilder(); + } + public ContactInfo getContactInfo() { return contactInfo; } @@ -88,31 +89,54 @@ public EnumSet bookingMethods() { return bookingMethods; } + @Nullable public BookingTime getEarliestBookingTime() { return earliestBookingTime; } + @Nullable public BookingTime getLatestBookingTime() { return latestBookingTime; } + @Nullable public Duration getMinimumBookingNotice() { return minimumBookingNotice; } + @Nullable public Duration getMaximumBookingNotice() { return maximumBookingNotice; } + @Nullable public String getMessage() { return message; } + @Nullable public String getPickupMessage() { return pickupMessage; } + @Nullable public String getDropOffMessage() { return dropOffMessage; } + + @Override + public String toString() { + return ToStringBuilder + .of(BookingInfo.class) + .addObj("contactInfo", contactInfo) + .addObj("bookingMethods", bookingMethods) + .addObj("earliestBookingTime", earliestBookingTime) + .addObj("latestBookingTime", latestBookingTime) + .addDuration("minimumBookingNotice", minimumBookingNotice) + .addDuration("maximumBookingNotice", maximumBookingNotice) + .addStr("message", message) + .addStr("pickupMessage", pickupMessage) + .addStr("dropOffMessage", dropOffMessage) + .toString(); + } } diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/booking/BookingInfoBuilder.java b/src/main/java/org/opentripplanner/transit/model/timetable/booking/BookingInfoBuilder.java new file mode 100644 index 00000000000..fd91f9e781a --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/timetable/booking/BookingInfoBuilder.java @@ -0,0 +1,69 @@ +package org.opentripplanner.transit.model.timetable.booking; + +import java.time.Duration; +import java.util.EnumSet; +import org.opentripplanner.transit.model.organization.ContactInfo; + +public class BookingInfoBuilder { + + ContactInfo contactInfo; + EnumSet bookingMethods; + BookingTime earliestBookingTime; + BookingTime latestBookingTime; + Duration minimumBookingNotice; + Duration maximumBookingNotice; + String message; + String pickupMessage; + String dropOffMessage; + + BookingInfoBuilder() {} + + public BookingInfoBuilder withContactInfo(ContactInfo contactInfo) { + this.contactInfo = contactInfo; + return this; + } + + public BookingInfoBuilder withBookingMethods(EnumSet bookingMethods) { + this.bookingMethods = bookingMethods; + return this; + } + + public BookingInfoBuilder withEarliestBookingTime(BookingTime earliestBookingTime) { + this.earliestBookingTime = earliestBookingTime; + return this; + } + + public BookingInfoBuilder withLatestBookingTime(BookingTime latestBookingTime) { + this.latestBookingTime = latestBookingTime; + return this; + } + + public BookingInfoBuilder withMinimumBookingNotice(Duration minimumBookingNotice) { + this.minimumBookingNotice = minimumBookingNotice; + return this; + } + + public BookingInfoBuilder withMaximumBookingNotice(Duration maximumBookingNotice) { + this.maximumBookingNotice = maximumBookingNotice; + return this; + } + + public BookingInfoBuilder withMessage(String message) { + this.message = message; + return this; + } + + public BookingInfoBuilder withPickupMessage(String pickupMessage) { + this.pickupMessage = pickupMessage; + return this; + } + + public BookingInfoBuilder withDropOffMessage(String dropOffMessage) { + this.dropOffMessage = dropOffMessage; + return this; + } + + public BookingInfo build() { + return new BookingInfo(this); + } +} diff --git a/src/main/java/org/opentripplanner/model/BookingMethod.java b/src/main/java/org/opentripplanner/transit/model/timetable/booking/BookingMethod.java similarity index 63% rename from src/main/java/org/opentripplanner/model/BookingMethod.java rename to src/main/java/org/opentripplanner/transit/model/timetable/booking/BookingMethod.java index ac788ec8741..78cd2372698 100644 --- a/src/main/java/org/opentripplanner/model/BookingMethod.java +++ b/src/main/java/org/opentripplanner/transit/model/timetable/booking/BookingMethod.java @@ -1,4 +1,4 @@ -package org.opentripplanner.model; +package org.opentripplanner.transit.model.timetable.booking; public enum BookingMethod { CALL_DRIVER, diff --git a/src/main/java/org/opentripplanner/transit/model/timetable/booking/BookingTime.java b/src/main/java/org/opentripplanner/transit/model/timetable/booking/BookingTime.java new file mode 100644 index 00000000000..8034fe07388 --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/timetable/booking/BookingTime.java @@ -0,0 +1,60 @@ +package org.opentripplanner.transit.model.timetable.booking; + +import java.io.Serializable; +import java.time.LocalTime; +import java.util.Objects; +import org.opentripplanner.framework.time.TimeUtils; + +/** + * Represents either the earliest or latest time a trip can be booked relative to the departure day + * of the trip. + */ +public class BookingTime implements Serializable { + + private final LocalTime time; + + private final int daysPrior; + + public BookingTime(LocalTime time, int daysPrior) { + this.time = time; + this.daysPrior = daysPrior; + } + + public LocalTime getTime() { + return time; + } + + public int getDaysPrior() { + return daysPrior; + } + + /** + * Get the relative time of day, can be negative if the {@code daysPrior} is set. This method + * does account for DST changes within the relative time. + */ + public int relativeTimeSeconds() { + return time.toSecondOfDay() - daysPrior * TimeUtils.ONE_DAY_SECONDS; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BookingTime that = (BookingTime) o; + return daysPrior == that.daysPrior && Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + return Objects.hash(time, daysPrior); + } + + @Override + public String toString() { + return daysPrior == 0 ? time.toString() : time.toString() + "-" + daysPrior + "d"; + } +} 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 new file mode 100644 index 00000000000..8dad53a0bde --- /dev/null +++ b/src/main/java/org/opentripplanner/transit/model/timetable/booking/RoutingBookingInfo.java @@ -0,0 +1,185 @@ +package org.opentripplanner.transit.model.timetable.booking; + +import java.time.Duration; +import java.util.Objects; +import javax.annotation.Nullable; +import org.opentripplanner.framework.tostring.ToStringBuilder; + +/** + * This is the contract between booking info and the router. The router will enforce + * this information if the request sets the earliest-booking-time request parameter. + *

+ * Either {@code latestBookingTime} and {@code minimumBookingNotice} must be set to + * an actual value, both can not be set to {@NOT_SET} simultaneously. + *

+ * This class is not used by Raptor directly, but used by the BookingTimeAccessEgress which + * implements the RaptorAccessEgress interface. + */ +public final class RoutingBookingInfo { + + public static final int NOT_SET = -1_999_999; + private static final RoutingBookingInfo UNRESTRICTED = new RoutingBookingInfo(); + + private final int requestedBookingTime; + private final int latestBookingTime; + private final int minimumBookingNotice; + + /** Unrestricted booking info. */ + private RoutingBookingInfo() { + this.requestedBookingTime = NOT_SET; + this.latestBookingTime = NOT_SET; + this.minimumBookingNotice = NOT_SET; + } + + private RoutingBookingInfo( + int requestedBookingTime, + int latestBookingTime, + int minimumBookingNotice + ) { + if (notSet(requestedBookingTime)) { + throw new IllegalArgumentException("The requestedBookingTime must be set."); + } + if (notSet(latestBookingTime) && notSet(minimumBookingNotice)) { + throw new IllegalArgumentException( + "At least latestBookingTime or minimumBookingNotice must be set." + ); + } + this.requestedBookingTime = requestedBookingTime; + this.latestBookingTime = latestBookingTime; + this.minimumBookingNotice = minimumBookingNotice; + } + + public static RoutingBookingInfo.Builder of(int requestedBookingTime) { + return new Builder(requestedBookingTime); + } + + public static RoutingBookingInfo of(int requestedBookingTime, @Nullable BookingInfo bookingInfo) { + return of(requestedBookingTime).withBookingInfo(bookingInfo).build(); + } + + /** + * Return an instance without any booking restrictions. + */ + public static RoutingBookingInfo unrestricted() { + return UNRESTRICTED; + } + + /** + * Time-shift departureTime if the minimum-booking-notice requires it. If required, the + * new time-shifted departureTime is returned, if not the given {@code departureTime} is + * returned as is. For example, if a service is available between 12:00 and 15:00 and the + * minimum booking notice is 30 minutes, the first available trip at 13:00 + * ({@code requestedBookingTime}) is 13:30. + */ + public int earliestDepartureTime(int departureTime) { + return notSet(minimumBookingNotice) + ? departureTime + : Math.max(minBookingNoticeLimit(), departureTime); + } + + /** + * Check if the given time is after (or eq to) the earliest time allowed according to the minimum + * booking notice. + */ + public boolean exceedsMinimumBookingNotice(int departureTime) { + return exist(minimumBookingNotice) && departureTime < minBookingNoticeLimit(); + } + + public boolean exceedsLatestBookingTime() { + return exist(latestBookingTime) && requestedBookingTime > latestBookingTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + var other = (RoutingBookingInfo) o; + return ( + Objects.equals(latestBookingTime, other.latestBookingTime) && + Objects.equals(minimumBookingNotice, other.minimumBookingNotice) + ); + } + + @Override + public int hashCode() { + return Objects.hash(latestBookingTime, minimumBookingNotice); + } + + @Override + public String toString() { + return ToStringBuilder + .of(RoutingBookingInfo.class) + .addServiceTime("latestBookingTime", latestBookingTime, NOT_SET) + .addDurationSec("minimumBookingNotice", minimumBookingNotice, NOT_SET) + .toString(); + } + + private int minBookingNoticeLimit() { + return requestedBookingTime + minimumBookingNotice; + } + + private static boolean exist(int value) { + return value != NOT_SET; + } + + private static boolean notSet(int value) { + return value == NOT_SET; + } + + public static class Builder { + + private final int requestedBookingTime; + private int latestBookingTime; + private int minimumBookingNotice; + + public Builder(int requestedBookingTime) { + this.requestedBookingTime = requestedBookingTime; + setUnrestricted(); + } + + /** + * Convenience method to add booking info to builder. + */ + Builder withBookingInfo(@Nullable BookingInfo bookingInfo) { + // Clear booking + if (bookingInfo == null) { + setUnrestricted(); + return this; + } + withLatestBookingTime(bookingInfo.getLatestBookingTime()); + withMinimumBookingNotice(bookingInfo.getMinimumBookingNotice()); + return this; + } + + public Builder withLatestBookingTime(BookingTime latestBookingTime) { + this.latestBookingTime = + latestBookingTime == null ? NOT_SET : latestBookingTime.relativeTimeSeconds(); + return this; + } + + public Builder withMinimumBookingNotice(Duration minimumBookingNotice) { + this.minimumBookingNotice = + minimumBookingNotice == null ? NOT_SET : (int) minimumBookingNotice.toSeconds(); + return this; + } + + public RoutingBookingInfo build() { + if (notSet(requestedBookingTime)) { + return unrestricted(); + } + if (notSet(latestBookingTime) && notSet(minimumBookingNotice)) { + return RoutingBookingInfo.unrestricted(); + } + return new RoutingBookingInfo(requestedBookingTime, latestBookingTime, minimumBookingNotice); + } + + private void setUnrestricted() { + latestBookingTime = NOT_SET; + minimumBookingNotice = NOT_SET; + } + } +} diff --git a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql index e6f4208c643..426de70ebcc 100644 --- a/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql +++ b/src/main/resources/org/opentripplanner/apis/transmodel/schema.graphql @@ -792,7 +792,16 @@ type QueryType { boardSlackDefault: Int = 0, "List of boardSlack for a given set of modes. Defaults: []" boardSlackList: [TransportModeSlack], - "Date and time for the earliest time the user is willing to start the journey (if arriveBy=false/not set) or the latest acceptable time of arriving (arriveBy=true). Defaults to now" + """ + The date and time for the latest time the user is expected to book the journey. + Normally this is when the search is performed (now), plus a small grace period to + complete the booking. Services which must be booked before this time is excluded. The + `latestBookingTime` and `minimumBookingPeriod` in `BookingArrangement` (flexible + services only) is used to enforce this. If this parameter is _not set_, no booking-time + restrictions are applied - all journeys are listed. + """ + bookingTime: DateTime, + "The date and time for the earliest time the user is willing to start the journey (if `false` or not set) or the latest acceptable time of arriving (`true`). Defaults to now." dateTime: DateTime, "Debug the itinerary-filter-chain. OTP will attach a system notice to itineraries instead of removing them. This is very convenient when tuning the filters." debugItineraryFilter: Boolean = false @deprecated(reason : "Use `itineraryFilter.debug` instead."), diff --git a/src/test/java/org/opentripplanner/TestServerContext.java b/src/test/java/org/opentripplanner/TestServerContext.java index bf6a8d2bbd3..2f4ded1121d 100644 --- a/src/test/java/org/opentripplanner/TestServerContext.java +++ b/src/test/java/org/opentripplanner/TestServerContext.java @@ -51,7 +51,7 @@ public static OtpServerRequestContext createServerContext( createRealtimeVehicleService(transitService), createVehicleRentalService(), createEmissionsService(), - routerConfig.flexConfig(), + routerConfig.flexParameters(), List.of(), null, createStreetLimitationParametersService(), 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 654c52ce912..de6b48c5ef4 100644 --- a/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java +++ b/src/test/java/org/opentripplanner/apis/transmodel/mapping/TripRequestMapperTest.java @@ -142,7 +142,7 @@ void setup() { new DefaultRealtimeVehicleService(transitService), new DefaultVehicleRentalService(), new DefaultEmissionsService(new EmissionsDataModel()), - RouterConfig.DEFAULT.flexConfig(), + RouterConfig.DEFAULT.flexParameters(), List.of(), null, new DefaultStreetLimitationParametersService(new StreetLimitationParameters()), diff --git a/src/test/java/org/opentripplanner/framework/time/TimeUtilsTest.java b/src/test/java/org/opentripplanner/framework/time/TimeUtilsTest.java index 53a6e5d4be4..c2eb828cc21 100644 --- a/src/test/java/org/opentripplanner/framework/time/TimeUtilsTest.java +++ b/src/test/java/org/opentripplanner/framework/time/TimeUtilsTest.java @@ -3,6 +3,7 @@ import static java.time.ZoneOffset.UTC; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; import java.time.Month; @@ -182,6 +183,30 @@ void testMsToString() { assertEquals("-1.234s", TimeUtils.msToString(-1234)); } + @Test + void toTransitTimeSeconds() { + var timeZero = ZonedDateTime.of( + LocalDate.of(2024, Month.JANUARY, 15), + LocalTime.of(0, 0, 0), + ZoneIds.UTC + ); + // otp zero is identical to time + assertEquals( + 0, + TimeUtils.toTransitTimeSeconds(timeZero, Instant.parse("2024-01-15T00:00:00Z")) + ); + // Test positive offset - otp zero is 1h2m3s before time + assertEquals( + 3723, + TimeUtils.toTransitTimeSeconds(timeZero, Instant.parse("2024-01-15T01:02:03Z")) + ); + // Test negative offset - otp zero is 30m after time + assertEquals( + -1800, + TimeUtils.toTransitTimeSeconds(timeZero, Instant.parse("2024-01-14T23:30:00Z")) + ); + } + private static int time(int hour, int min, int sec) { return 60 * (60 * hour + min) + sec; } diff --git a/src/test/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModuleTest.java b/src/test/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModuleTest.java index 1d8d819b5cb..4756608f57d 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModuleTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModuleTest.java @@ -19,7 +19,7 @@ import org.opentripplanner.street.model.edge.Edge; import org.opentripplanner.street.model.edge.StreetEdge; import org.opentripplanner.street.model.vertex.OsmBoardingLocationVertex; -import org.opentripplanner.street.model.vertex.TransitStopVertexBuilder; +import org.opentripplanner.street.model.vertex.TransitStopVertex; import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.street.model.vertex.VertexFactory; import org.opentripplanner.street.model.vertex.VertexLabel; @@ -74,7 +74,7 @@ void addAndLinkBoardingLocations(boolean areaVisibility, Set linkedVerti var provider = new OsmProvider(file, false); var floatingBusVertex = factory.transitStop( - new TransitStopVertexBuilder().withStop(floatingBusStop).withModes(Set.of(TransitMode.BUS)) + TransitStopVertex.of().withStop(floatingBusStop).withModes(Set.of(TransitMode.BUS)) ); var floatingBoardingLocation = factory.osmBoardingLocation( floatingBusVertex.getCoordinate(), @@ -91,10 +91,10 @@ void addAndLinkBoardingLocations(boolean areaVisibility, Set linkedVerti osmModule.buildGraph(); var platformVertex = factory.transitStop( - new TransitStopVertexBuilder().withStop(platform).withModes(Set.of(TransitMode.RAIL)) + TransitStopVertex.of().withStop(platform).withModes(Set.of(TransitMode.RAIL)) ); var busVertex = factory.transitStop( - new TransitStopVertexBuilder().withStop(busStop).withModes(Set.of(TransitMode.BUS)) + TransitStopVertex.of().withStop(busStop).withModes(Set.of(TransitMode.BUS)) ); transitModel.index(); diff --git a/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java b/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java index 4d2141eb38e..06b10575ef9 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/StreetLinkerModuleTest.java @@ -21,7 +21,6 @@ import org.opentripplanner.street.model.edge.StreetTransitStopLink; import org.opentripplanner.street.model.vertex.SplitterVertex; import org.opentripplanner.street.model.vertex.TransitStopVertex; -import org.opentripplanner.street.model.vertex.TransitStopVertexBuilder; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.framework.Deduplicator; import org.opentripplanner.transit.model.site.RegularStop; @@ -68,7 +67,7 @@ void linkRegularStop() { void linkFlexStop() { OTPFeature.FlexRouting.testOn(() -> { var model = new TestModel(); - var flexTrip = TransitModelForTest.of().unscheduledTrip(id("flex"), model.stop()); + var flexTrip = TransitModelForTest.of().unscheduledTrip("flex", model.stop()); model.withFlexTrip(flexTrip); var module = model.streetLinkerModule(); @@ -126,7 +125,7 @@ public TestModel() { transitModel = new TransitModel(builder.build(), new Deduplicator()); - stopVertex = new TransitStopVertexBuilder().withStop(stop).build(); + stopVertex = TransitStopVertex.of().withStop(stop).build(); graph.addVertex(stopVertex); graph.hasStreets = true; diff --git a/src/test/java/org/opentripplanner/graph_builder/module/islandpruning/SubgraphOnlyFerryTest.java b/src/test/java/org/opentripplanner/graph_builder/module/islandpruning/SubgraphOnlyFerryTest.java index 26aaf12896b..f069a518fc0 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/islandpruning/SubgraphOnlyFerryTest.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/islandpruning/SubgraphOnlyFerryTest.java @@ -6,7 +6,6 @@ import java.util.Set; import org.junit.jupiter.api.Test; import org.opentripplanner.street.model.vertex.TransitStopVertex; -import org.opentripplanner.street.model.vertex.TransitStopVertexBuilder; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.basic.TransitMode; import org.opentripplanner.transit.model.site.RegularStop; @@ -19,7 +18,8 @@ class SubgraphOnlyFerryTest { @Test void subgraphHasOnlyFerry() { - TransitStopVertex transitStopVertex = new TransitStopVertexBuilder() + TransitStopVertex transitStopVertex = TransitStopVertex + .of() .withStop(regularStop1) .withModes(Set.of(TransitMode.FERRY)) .build(); @@ -32,7 +32,8 @@ void subgraphHasOnlyFerry() { @Test void subgraphHasOnlyNoFerry() { - TransitStopVertex transitStopVertex1 = new TransitStopVertexBuilder() + TransitStopVertex transitStopVertex1 = TransitStopVertex + .of() .withStop(regularStop1) .withModes(Set.of(TransitMode.BUS)) .build(); @@ -45,7 +46,8 @@ void subgraphHasOnlyNoFerry() { @Test void subgraphHasOnlyNoMode() { - TransitStopVertex transitStopVertex1 = new TransitStopVertexBuilder() + TransitStopVertex transitStopVertex1 = TransitStopVertex + .of() .withStop(regularStop1) .withModes(Set.of()) .build(); @@ -58,11 +60,13 @@ void subgraphHasOnlyNoMode() { @Test void subgraphHasOnlyFerryMoreStops() { - TransitStopVertex transitStopVertex1 = new TransitStopVertexBuilder() + TransitStopVertex transitStopVertex1 = TransitStopVertex + .of() .withStop(regularStop1) .withModes(Set.of(TransitMode.FERRY)) .build(); - TransitStopVertex transitStopVertex2 = new TransitStopVertexBuilder() + TransitStopVertex transitStopVertex2 = TransitStopVertex + .of() .withStop(regularStop2) .withModes(Set.of(TransitMode.FERRY)) .build(); @@ -76,11 +80,13 @@ void subgraphHasOnlyFerryMoreStops() { @Test void subgraphHasNotOnlyFerryMoreStops() { - TransitStopVertex transitStopVertex1 = new TransitStopVertexBuilder() + TransitStopVertex transitStopVertex1 = TransitStopVertex + .of() .withStop(regularStop1) .withModes(Set.of(TransitMode.FERRY)) .build(); - TransitStopVertex transitStopVertex2 = new TransitStopVertexBuilder() + TransitStopVertex transitStopVertex2 = TransitStopVertex + .of() .withStop(regularStop2) .withModes(Set.of(TransitMode.BUS)) .build(); @@ -94,11 +100,13 @@ void subgraphHasNotOnlyFerryMoreStops() { @Test void subgraphHasNoModeMoreStops() { - TransitStopVertex transitStopVertex1 = new TransitStopVertexBuilder() + TransitStopVertex transitStopVertex1 = TransitStopVertex + .of() .withStop(regularStop1) .withModes(Set.of(TransitMode.FERRY)) .build(); - TransitStopVertex transitStopVertex2 = new TransitStopVertexBuilder() + TransitStopVertex transitStopVertex2 = TransitStopVertex + .of() .withStop(regularStop2) .withModes(Set.of()) .build(); diff --git a/src/test/java/org/opentripplanner/graph_builder/module/linking/TestGraph.java b/src/test/java/org/opentripplanner/graph_builder/module/linking/TestGraph.java index e7ac5e16a68..a45b8eef239 100644 --- a/src/test/java/org/opentripplanner/graph_builder/module/linking/TestGraph.java +++ b/src/test/java/org/opentripplanner/graph_builder/module/linking/TestGraph.java @@ -6,7 +6,6 @@ import org.opentripplanner.routing.linking.VertexLinker; import org.opentripplanner.street.model.edge.StreetTransitStopLink; import org.opentripplanner.street.model.vertex.TransitStopVertex; -import org.opentripplanner.street.model.vertex.TransitStopVertexBuilder; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.street.search.TraverseModeSet; import org.opentripplanner.transit.model._data.TransitModelForTest; @@ -28,7 +27,7 @@ public static void addRegularStopGrid(Graph graph) { for (double lon = -83.1341; lon < -82.8646; lon += 0.005) { String id = Integer.toString(count++); RegularStop stop = TEST_MODEL.stop(id).withCoordinate(lat, lon).build(); - graph.addVertex(new TransitStopVertexBuilder().withStop(stop).build()); + graph.addVertex(TransitStopVertex.of().withStop(stop).build()); } } } @@ -40,7 +39,7 @@ public static void addExtraStops(Graph graph) { for (double lat = 40; lat < 40.01; lat += 0.005) { String id = "EXTRA_" + count++; RegularStop stop = TEST_MODEL.stop(id).withCoordinate(lat, lon).build(); - graph.addVertex(new TransitStopVertexBuilder().withStop(stop).build()); + graph.addVertex(TransitStopVertex.of().withStop(stop).build()); } // add some duplicate stops, identical to the regular stop grid @@ -48,7 +47,7 @@ public static void addExtraStops(Graph graph) { for (double lat = 39.9058; lat < 40.0281; lat += 0.005) { String id = "DUPE_" + count++; RegularStop stop = TEST_MODEL.stop(id).withCoordinate(lat, lon).build(); - graph.addVertex(new TransitStopVertexBuilder().withStop(stop).build()); + graph.addVertex(TransitStopVertex.of().withStop(stop).build()); } // add some almost duplicate stops @@ -56,7 +55,7 @@ public static void addExtraStops(Graph graph) { for (double lat = 39.9059; lat < 40.0281; lat += 0.005) { String id = "ALMOST_" + count++; RegularStop stop = TEST_MODEL.stop(id).withCoordinate(lat, lon).build(); - graph.addVertex(new TransitStopVertexBuilder().withStop(stop).build()); + graph.addVertex(TransitStopVertex.of().withStop(stop).build()); } } diff --git a/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java b/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java index dafcc639ceb..28be5b3a7e2 100644 --- a/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java +++ b/src/test/java/org/opentripplanner/model/plan/TestItineraryBuilder.java @@ -8,18 +8,15 @@ import static org.opentripplanner.transit.model._data.TransitModelForTest.id; import static org.opentripplanner.transit.model._data.TransitModelForTest.route; -import gnu.trove.set.hash.TIntHashSet; import java.time.Duration; import java.time.LocalDate; import java.time.Month; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; -import org.opentripplanner.ext.flex.FlexServiceDate; import org.opentripplanner.ext.flex.FlexibleTransitLeg; import org.opentripplanner.ext.flex.edgetype.FlexTripEdge; import org.opentripplanner.ext.flex.flexpathcalculator.DirectFlexPathCalculator; -import org.opentripplanner.ext.flex.template.FlexAccessTemplate; import org.opentripplanner.ext.flex.trip.UnscheduledTrip; import org.opentripplanner.ext.ridehailing.model.RideEstimate; import org.opentripplanner.ext.ridehailing.model.RideHailingLeg; @@ -30,7 +27,6 @@ import org.opentripplanner.model.StopTime; import org.opentripplanner.model.transfer.ConstrainedTransfer; import org.opentripplanner.model.transfer.TransferConstraint; -import org.opentripplanner.standalone.config.sandbox.FlexConfig; import org.opentripplanner.street.model._data.StreetModelForTest; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.transit.model._data.TransitModelForTest; @@ -212,16 +208,9 @@ public TestItineraryBuilder flex(int start, int end, Place to) { .withTrip(trip) .build(); - var template = new FlexAccessTemplate( - null, - flexTrip, - 0, - 1, - null, - new FlexServiceDate(LocalDate.now(), 0, new TIntHashSet()), - new DirectFlexPathCalculator(), - FlexConfig.DEFAULT - ); + int fromStopPos = 0; + int toStopPos = 1; + LocalDate serviceDate = LocalDate.of(2024, Month.MAY, 22); var fromv = StreetModelForTest.intersectionVertex( "v1", @@ -235,15 +224,17 @@ public TestItineraryBuilder flex(int start, int end, Place to) { ); var flexPath = new DirectFlexPathCalculator() - .calculateFlexPath(fromv, tov, template.fromStopIndex, template.toStopIndex); + .calculateFlexPath(fromv, tov, fromStopPos, toStopPos); - var edge = FlexTripEdge.createFlexTripEdge( + var edge = new FlexTripEdge( fromv, tov, lastPlace.stop, to.stop, flexTrip, - template, + fromStopPos, + toStopPos, + serviceDate, flexPath ); diff --git a/src/test/java/org/opentripplanner/netex/mapping/BookingInfoMapperTest.java b/src/test/java/org/opentripplanner/netex/mapping/BookingInfoMapperTest.java index b69cb2e9bad..e561a6155d3 100644 --- a/src/test/java/org/opentripplanner/netex/mapping/BookingInfoMapperTest.java +++ b/src/test/java/org/opentripplanner/netex/mapping/BookingInfoMapperTest.java @@ -7,7 +7,7 @@ import java.time.LocalTime; import org.junit.jupiter.api.Test; import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore; -import org.opentripplanner.model.BookingInfo; +import org.opentripplanner.transit.model.timetable.booking.BookingInfo; import org.rutebanken.netex.model.BookingArrangementsStructure; import org.rutebanken.netex.model.ContactStructure; import org.rutebanken.netex.model.FlexibleLine; diff --git a/src/test/java/org/opentripplanner/routing/TestHalfEdges.java b/src/test/java/org/opentripplanner/routing/TestHalfEdges.java index 13785564074..ba7e024e57a 100644 --- a/src/test/java/org/opentripplanner/routing/TestHalfEdges.java +++ b/src/test/java/org/opentripplanner/routing/TestHalfEdges.java @@ -40,7 +40,6 @@ import org.opentripplanner.street.model.vertex.IntersectionVertex; import org.opentripplanner.street.model.vertex.TemporaryStreetLocation; import org.opentripplanner.street.model.vertex.TransitStopVertex; -import org.opentripplanner.street.model.vertex.TransitStopVertexBuilder; import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.street.model.vertex.VertexFactory; import org.opentripplanner.street.search.StreetSearchBuilder; @@ -165,8 +164,8 @@ public void setUp() { stopModelBuilder.withRegularStop(s1).withRegularStop(s2); transitModel = new TransitModel(stopModelBuilder.build(), deduplicator); - station1 = factory.transitStop(new TransitStopVertexBuilder().withStop(s1)); - station2 = factory.transitStop(new TransitStopVertexBuilder().withStop(s2)); + station1 = factory.transitStop(TransitStopVertex.of().withStop(s1)); + station2 = factory.transitStop(TransitStopVertex.of().withStop(s2)); station1.addMode(TransitMode.RAIL); station2.addMode(TransitMode.RAIL); diff --git a/src/test/java/org/opentripplanner/routing/algorithm/GraphRoutingTest.java b/src/test/java/org/opentripplanner/routing/algorithm/GraphRoutingTest.java index 2c8847748ed..69e2ddaa944 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/GraphRoutingTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/GraphRoutingTest.java @@ -40,7 +40,6 @@ import org.opentripplanner.street.model.vertex.TemporaryVertex; import org.opentripplanner.street.model.vertex.TransitEntranceVertex; import org.opentripplanner.street.model.vertex.TransitStopVertex; -import org.opentripplanner.street.model.vertex.TransitStopVertexBuilder; import org.opentripplanner.street.model.vertex.VehicleParkingEntranceVertex; import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.street.model.vertex.VertexFactory; @@ -253,7 +252,7 @@ public TransitStopVertex stop( boolean noTransfers ) { return vertexFactory.transitStop( - new TransitStopVertexBuilder().withStop(stopEntity(id, latitude, longitude, noTransfers)) + TransitStopVertex.of().withStop(stopEntity(id, latitude, longitude, noTransfers)) ); } 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 8c0710d7db6..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; @@ -142,8 +144,23 @@ void penalty() { void createItineraryWithOnBoardFlexAccess() { RaptorPathToItineraryMapper mapper = getRaptorPathToItineraryMapper(); + var flexTrip = TEST_MODEL.unscheduledTrip( + "flex", + TEST_MODEL.stop("A:Stop:1").build(), + TEST_MODEL.stop("A:Stop:2").build() + ); + State state = TestStateBuilder.ofWalking().streetEdge().streetEdge().build(); - FlexAccessEgress flexAccessEgress = new FlexAccessEgress(S1, null, 0, 1, null, 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/routing/algorithm/raptoradapter/router/street/AccessEgressPenaltyDecoratorTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressPenaltyDecoratorTest.java index 7d68c5bfeeb..78b12a97b82 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressPenaltyDecoratorTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressPenaltyDecoratorTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.framework.time.DurationUtils; import org.opentripplanner.routing.algorithm.raptoradapter.transit.DefaultAccessEgress; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; import org.opentripplanner.routing.api.request.StreetMode; import org.opentripplanner.routing.api.request.framework.TimeAndCostPenalty; import org.opentripplanner.routing.api.request.framework.TimeAndCostPenaltyForEnum; @@ -24,8 +25,8 @@ class AccessEgressPenaltyDecoratorTest { private static final int DURATION_CAR_RENTAL = 45; private static final int DURATION_WALKING = 135; private static final Duration D10m = DurationUtils.duration("10m"); - private static final DefaultAccessEgress WALK = ofWalking(DURATION_WALKING); - private static final DefaultAccessEgress CAR_RENTAL = ofCarRental(DURATION_CAR_RENTAL); + private static final RoutingAccessEgress WALK = ofWalking(DURATION_WALKING); + private static final RoutingAccessEgress CAR_RENTAL = ofCarRental(DURATION_CAR_RENTAL); private static final TimeAndCostPenalty PENALTY = new TimeAndCostPenalty( TimePenalty.of(D10m, 1.5), 2.0 @@ -33,10 +34,10 @@ class AccessEgressPenaltyDecoratorTest { // We use the penalty to calculate the expected value, this is not pure, but the // TimeAndCostPenalty is unit-tested elsewhere. - private static final DefaultAccessEgress EXP_WALK_W_PENALTY = WALK.withPenalty( + private static final RoutingAccessEgress EXP_WALK_W_PENALTY = WALK.withPenalty( PENALTY.calculate(DURATION_WALKING) ); - private static final DefaultAccessEgress EXP_CAR_RENTAL_W_PENALTY = CAR_RENTAL.withPenalty( + private static final RoutingAccessEgress EXP_CAR_RENTAL_W_PENALTY = CAR_RENTAL.withPenalty( PENALTY.calculate(DURATION_CAR_RENTAL) ); @@ -60,7 +61,7 @@ private static List decorateCarRentalTestCase() { @ParameterizedTest @MethodSource("decorateCarRentalTestCase") - void decorateCarRentalTest(List expected, List input) { + void decorateCarRentalTest(List expected, List input) { var subject = new AccessEgressPenaltyDecorator( StreetMode.CAR_RENTAL, StreetMode.WALK, @@ -81,7 +82,7 @@ private static List decorateWalkTestCase() { @ParameterizedTest @MethodSource("decorateWalkTestCase") - void decorateWalkTest(List expected, List input) { + void decorateWalkTest(List expected, List input) { var subject = new AccessEgressPenaltyDecorator( StreetMode.CAR_RENTAL, StreetMode.WALK, @@ -111,18 +112,18 @@ void doNotDecorateAnyIfNoPenaltyIsSet() { @Test void filterEgress() {} - private static DefaultAccessEgress ofCarRental(int duration) { + private static RoutingAccessEgress ofCarRental(int duration) { return ofAccessEgress( duration, TestStateBuilder.ofCarRental().streetEdge().pickUpCarFromStation().build() ); } - private static DefaultAccessEgress ofWalking(int durationInSeconds) { + private static RoutingAccessEgress ofWalking(int durationInSeconds) { return ofAccessEgress(durationInSeconds, TestStateBuilder.ofWalking().streetEdge().build()); } - private static DefaultAccessEgress ofAccessEgress(int duration, State state) { + private static RoutingAccessEgress ofAccessEgress(int duration, State state) { // We do NOT need to override #withPenalty(...), because all fields including // 'durationInSeconds' is copied over using the getters. diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressesTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressesTest.java index 1779155a69b..bd498e82f65 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressesTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/router/street/AccessEgressesTest.java @@ -8,36 +8,37 @@ import org.opentripplanner.framework.model.Cost; import org.opentripplanner.framework.model.TimeAndCost; import org.opentripplanner.routing.algorithm.raptoradapter.transit.DefaultAccessEgress; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.RoutingAccessEgress; import org.opentripplanner.street.search.state.TestStateBuilder; class AccessEgressesTest { public static final Duration D3m = Duration.ofMinutes(3); public static final Duration D7m = Duration.ofMinutes(7); - private static final DefaultAccessEgress ACCESS_A = new DefaultAccessEgress( + private static final RoutingAccessEgress ACCESS_A = new DefaultAccessEgress( 1, TestStateBuilder.ofWalking().build() ) .withPenalty(new TimeAndCost(D3m, Cost.ZERO)); - private static final DefaultAccessEgress ACCESS_B = new DefaultAccessEgress( + private static final RoutingAccessEgress ACCESS_B = new DefaultAccessEgress( 1, TestStateBuilder.ofWalking().build() ) .withPenalty(new TimeAndCost(D7m, Cost.ZERO)); - private static final DefaultAccessEgress ACCESS_C = new DefaultAccessEgress( + private static final RoutingAccessEgress ACCESS_C = new DefaultAccessEgress( 1, TestStateBuilder.ofWalking().build() ); - private static final List ACCESSES = List.of(ACCESS_A, ACCESS_B, ACCESS_C); - private static final DefaultAccessEgress EGRESS_A = new DefaultAccessEgress( + private static final List ACCESSES = List.of(ACCESS_A, ACCESS_B, ACCESS_C); + private static final RoutingAccessEgress EGRESS_A = new DefaultAccessEgress( 1, TestStateBuilder.ofWalking().build() ); - private static final DefaultAccessEgress EGRESS_B = new DefaultAccessEgress( + private static final RoutingAccessEgress EGRESS_B = new DefaultAccessEgress( 1, TestStateBuilder.ofWalking().build() ); - private static final List EGRESSES = List.of(EGRESS_A, EGRESS_B); + private static final List EGRESSES = List.of(EGRESS_A, EGRESS_B); private final AccessEgresses subject = new AccessEgresses(ACCESSES, EGRESSES); diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgressTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgressTest.java index 92ed7aa4825..0fcb7c8cea3 100644 --- a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgressTest.java +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/DefaultAccessEgressTest.java @@ -22,7 +22,7 @@ class DefaultAccessEgressTest { public static final TimeAndCost PENALTY = new TimeAndCost(TIME_PENALTY, COST_PENALTY); private final DefaultAccessEgress subject = new DefaultAccessEgress(STOP, LAST_STATE); - private final DefaultAccessEgress subjectWithPenalty = subject.withPenalty(PENALTY); + private final RoutingAccessEgress subjectWithPenalty = subject.withPenalty(PENALTY); @Test void canNotAddPenaltyTwice() { diff --git a/src/test/java/org/opentripplanner/routing/graphfinder/NearbyStopTest.java b/src/test/java/org/opentripplanner/routing/graphfinder/NearbyStopTest.java new file mode 100644 index 00000000000..a81d7c39c5e --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/graphfinder/NearbyStopTest.java @@ -0,0 +1,29 @@ +package org.opentripplanner.routing.graphfinder; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model._data.TransitModelForTest; + +class NearbyStopTest { + + private static TransitModelForTest MODEL = TransitModelForTest.of(); + + // TODO Add tests for all public methods in NearbyStop here + + @Test + void testIsBetter() { + // We only test the distance here, since the compareTo method used should have a more complete + // unit-test including tests on state weight. + var a = new NearbyStop(MODEL.stop("A").build(), 20.0, null, null); + var b = new NearbyStop(MODEL.stop("A").build(), 30.0, null, null); + + assertTrue(a.isBetter(b)); + assertFalse(b.isBetter(a)); + + var sameDistance = new NearbyStop(MODEL.stop("A").build(), 20.0, null, null); + assertFalse(a.isBetter(sameDistance)); + assertFalse(sameDistance.isBetter(a)); + } +} diff --git a/src/test/java/org/opentripplanner/routing/linking/LinkStopToPlatformTest.java b/src/test/java/org/opentripplanner/routing/linking/LinkStopToPlatformTest.java index e44ecf48f2f..23607d2e0b3 100644 --- a/src/test/java/org/opentripplanner/routing/linking/LinkStopToPlatformTest.java +++ b/src/test/java/org/opentripplanner/routing/linking/LinkStopToPlatformTest.java @@ -26,7 +26,6 @@ import org.opentripplanner.street.model.vertex.IntersectionVertex; import org.opentripplanner.street.model.vertex.LabelledIntersectionVertex; import org.opentripplanner.street.model.vertex.TransitStopVertex; -import org.opentripplanner.street.model.vertex.TransitStopVertexBuilder; import org.opentripplanner.street.model.vertex.Vertex; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.street.search.TraverseModeSet; @@ -111,7 +110,7 @@ private Graph prepareTest(Coordinate[] platform, int[] visible, Coordinate[] sto graph.index(transitModel.getStopModel()); for (RegularStop s : transitStops) { - var v = new TransitStopVertexBuilder().withStop(s).build(); + var v = TransitStopVertex.of().withStop(s).build(); graph.addVertex(v); } diff --git a/src/test/java/org/opentripplanner/standalone/config/sandbox/FlexConfigTest.java b/src/test/java/org/opentripplanner/standalone/config/sandbox/FlexConfigTest.java deleted file mode 100644 index 2488fd626bf..00000000000 --- a/src/test/java/org/opentripplanner/standalone/config/sandbox/FlexConfigTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.opentripplanner.standalone.config.sandbox; - -import static org.junit.jupiter.api.Assertions.*; - -import org.junit.jupiter.api.Test; - -class FlexConfigTest { - - @Test - void initializationOrder() { - assertNotNull(FlexConfig.DEFAULT.maxTransferDuration()); - assertNotNull(FlexConfig.DEFAULT.maxFlexTripDuration()); - } -} diff --git a/src/test/java/org/opentripplanner/street/model/edge/StreetTransitEntityLinkTest.java b/src/test/java/org/opentripplanner/street/model/edge/StreetTransitEntityLinkTest.java index 25234cce3f6..6ba4dea0216 100644 --- a/src/test/java/org/opentripplanner/street/model/edge/StreetTransitEntityLinkTest.java +++ b/src/test/java/org/opentripplanner/street/model/edge/StreetTransitEntityLinkTest.java @@ -21,7 +21,7 @@ import org.opentripplanner.routing.api.request.preference.WheelchairPreferences; import org.opentripplanner.street.model._data.StreetModelForTest; import org.opentripplanner.street.model.vertex.StreetVertex; -import org.opentripplanner.street.model.vertex.TransitStopVertexBuilder; +import org.opentripplanner.street.model.vertex.TransitStopVertex; import org.opentripplanner.street.search.request.StreetSearchRequest; import org.opentripplanner.street.search.state.State; import org.opentripplanner.street.search.state.TestStateBuilder; @@ -89,10 +89,7 @@ void unknownStop() { private State[] traverse(RegularStop stop, boolean onlyAccessible) { var from = StreetModelForTest.intersectionVertex("A", 10, 10); - var to = new TransitStopVertexBuilder() - .withStop(stop) - .withModes(Set.of(TransitMode.RAIL)) - .build(); + var to = TransitStopVertex.of().withStop(stop).withModes(Set.of(TransitMode.RAIL)).build(); var req = StreetSearchRequest.of().withMode(StreetMode.BIKE); AccessibilityPreferences feature; @@ -166,7 +163,7 @@ void stationBasedVehiclesAreNotAllowedIntoStops(State state) { } private void testTraversalWithState(State state, boolean canTraverse) { - var transitStopVertex = new TransitStopVertexBuilder().withStop(ACCESSIBLE_STOP).build(); + var transitStopVertex = TransitStopVertex.of().withStop(ACCESSIBLE_STOP).build(); var edge = StreetTransitStopLink.createStreetTransitStopLink( (StreetVertex) state.getVertex(), transitStopVertex diff --git a/src/test/java/org/opentripplanner/street/model/vertex/OsmVertexTest.java b/src/test/java/org/opentripplanner/street/model/vertex/OsmVertexTest.java index 0ec80117426..b3a2b0baebb 100644 --- a/src/test/java/org/opentripplanner/street/model/vertex/OsmVertexTest.java +++ b/src/test/java/org/opentripplanner/street/model/vertex/OsmVertexTest.java @@ -7,7 +7,6 @@ import java.util.List; import javax.annotation.Nonnull; import org.junit.jupiter.api.Test; -import org.opentripplanner._support.geometry.Polygons; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.site.AreaStop; @@ -15,14 +14,8 @@ class OsmVertexTest { private static final TransitModelForTest TEST_MODEL = TransitModelForTest.of(); - private static final AreaStop AREA_STOP1 = TEST_MODEL.areaStopForTest( - "flex-zone-1", - Polygons.BERLIN - ); - private static final AreaStop AREA_STOP2 = TEST_MODEL.areaStopForTest( - "flex-zone-2", - Polygons.BERLIN - ); + private static final AreaStop AREA_STOP1 = TEST_MODEL.areaStop("flex-zone-1").build(); + private static final AreaStop AREA_STOP2 = TEST_MODEL.areaStop("flex-zone-2").build(); @Test void areaStops() { diff --git a/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java b/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java index a4dbf76e55e..e43c5a769d0 100644 --- a/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java +++ b/src/test/java/org/opentripplanner/street/search/state/TestStateBuilder.java @@ -32,7 +32,7 @@ import org.opentripplanner.street.model.vertex.ElevatorOffboardVertex; import org.opentripplanner.street.model.vertex.ElevatorOnboardVertex; import org.opentripplanner.street.model.vertex.StreetVertex; -import org.opentripplanner.street.model.vertex.TransitStopVertexBuilder; +import org.opentripplanner.street.model.vertex.TransitStopVertex; import org.opentripplanner.street.search.TraverseMode; import org.opentripplanner.street.search.request.StreetSearchRequest; import org.opentripplanner.transit.model._data.TransitModelForTest; @@ -266,7 +266,7 @@ public TestStateBuilder pathway(String s) { @Nonnull private TestStateBuilder arriveAtStop(RegularStop stop) { var from = (StreetVertex) currentState.vertex; - var to = new TransitStopVertexBuilder().withStop(stop).build(); + var to = TransitStopVertex.of().withStop(stop).build(); Edge edge; if (currentState.getRequest().arriveBy()) { diff --git a/src/test/java/org/opentripplanner/transit/model/TransitModelArchitectureTest.java b/src/test/java/org/opentripplanner/transit/model/TransitModelArchitectureTest.java index 8976e29e293..11db763d97f 100644 --- a/src/test/java/org/opentripplanner/transit/model/TransitModelArchitectureTest.java +++ b/src/test/java/org/opentripplanner/transit/model/TransitModelArchitectureTest.java @@ -23,6 +23,7 @@ public class TransitModelArchitectureTest { private static final Package NETWORK = TRANSIT_MODEL.subPackage("network"); private static final Package SITE = TRANSIT_MODEL.subPackage("site"); private static final Package TIMETABLE = TRANSIT_MODEL.subPackage("timetable"); + private static final Package TIMETABLE_BOOKING = TIMETABLE.subPackage("booking"); private static final Package LEGACY_MODEL = OTP_ROOT.subPackage("model"); @Test @@ -84,6 +85,7 @@ void enforceTimetablePackageDependencies() { ORGANIZATION, NETWORK, SITE, + TIMETABLE_BOOKING, LEGACY_MODEL ) .verify(); diff --git a/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java b/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java index 2a4f2dfb701..0f6d872c639 100644 --- a/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java +++ b/src/test/java/org/opentripplanner/transit/model/_data/TransitModelForTest.java @@ -5,8 +5,13 @@ import java.util.List; import java.util.function.Consumer; import java.util.stream.IntStream; -import org.locationtech.jts.geom.Geometry; +import java.util.stream.Stream; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Polygon; +import org.opentripplanner._support.geometry.Coordinates; +import org.opentripplanner.ext.flex.trip.ScheduledDeviatedTrip; import org.opentripplanner.ext.flex.trip.UnscheduledTrip; +import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.framework.geometry.WgsCoordinate; import org.opentripplanner.framework.i18n.I18NString; import org.opentripplanner.framework.i18n.NonLocalizedString; @@ -23,7 +28,7 @@ import org.opentripplanner.transit.model.network.TripPattern; import org.opentripplanner.transit.model.network.TripPatternBuilder; import org.opentripplanner.transit.model.organization.Agency; -import org.opentripplanner.transit.model.site.AreaStop; +import org.opentripplanner.transit.model.site.AreaStopBuilder; import org.opentripplanner.transit.model.site.GroupStop; import org.opentripplanner.transit.model.site.RegularStop; import org.opentripplanner.transit.model.site.RegularStopBuilder; @@ -52,6 +57,18 @@ public class TransitModelForTest { public static final String OTHER_TIME_ZONE_ID = "America/Los_Angeles"; public static final WgsCoordinate ANY_COORDINATE = new WgsCoordinate(60.0, 10.0); + // This is used to create valid objects - do not use it for verification + private static final Polygon ANY_POLYGON = GeometryUtils + .getGeometryFactory() + .createPolygon( + new Coordinate[] { + Coordinates.of(61.0, 10.0), + Coordinates.of(61.0, 12.0), + Coordinates.of(60.0, 11.0), + Coordinates.of(61.0, 10.0), + } + ); + public static final Agency AGENCY = Agency .of(id("A1")) .withName("Agency Test") @@ -149,22 +166,21 @@ public StationBuilder station(String idAndName) { .withPriority(StopTransferPriority.ALLOWED); } - public GroupStop groupStopForTest(String idAndName, List stops) { + public GroupStop groupStop(String idAndName, RegularStop... stops) { var builder = stopModelBuilder .groupStop(id(idAndName)) .withName(new NonLocalizedString(idAndName)); - stops.forEach(builder::addLocation); + Stream.of(stops).forEach(builder::addLocation); return builder.build(); } - public AreaStop areaStopForTest(String idAndName, Geometry geometry) { + public AreaStopBuilder areaStop(String idAndName) { return stopModelBuilder .areaStop(id(idAndName)) .withName(new NonLocalizedString(idAndName)) - .withGeometry(geometry) - .build(); + .withGeometry(ANY_POLYGON); } public StopTime stopTime(Trip trip, int seq) { @@ -250,7 +266,7 @@ public TripPatternBuilder pattern(TransitMode mode) { .withStopPattern(stopPattern(3)); } - public UnscheduledTrip unscheduledTrip(FeedScopedId id, StopLocation... stops) { + public UnscheduledTrip unscheduledTrip(String id, StopLocation... stops) { var stopTimes = Arrays .stream(stops) .map(s -> { @@ -262,10 +278,22 @@ public UnscheduledTrip unscheduledTrip(FeedScopedId id, StopLocation... stops) { return st; }) .toList(); + return unscheduledTrip(id, stopTimes); + } + + public UnscheduledTrip unscheduledTrip(String id, List stopTimes) { return UnscheduledTrip - .of(id) + .of(id(id)) .withTrip(trip("flex-trip").build()) .withStopTimes(stopTimes) .build(); } + + public ScheduledDeviatedTrip scheduledDeviatedTrip(String id, StopTime... stopTimes) { + return ScheduledDeviatedTrip + .of(id(id)) + .withTrip(trip("flex-trip").build()) + .withStopTimes(Arrays.asList(stopTimes)) + .build(); + } } diff --git a/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java b/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java index 1658505aa51..1bce2edf9dc 100644 --- a/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java +++ b/src/test/java/org/opentripplanner/transit/model/network/StopPatternTest.java @@ -6,8 +6,6 @@ import java.util.List; import org.junit.jupiter.api.Test; -import org.locationtech.jts.geom.Coordinate; -import org.opentripplanner.framework.geometry.GeometryUtils; import org.opentripplanner.transit.model._data.TransitModelForTest; import org.opentripplanner.transit.model.timetable.Trip; @@ -23,22 +21,9 @@ void boardingAlightingConditions() { var s3 = testModel.stop("3", 62.0, 11.0).build(); var s4 = testModel.stop("4", 62.1, 11.0).build(); - var s34 = testModel.groupStopForTest("3_4", List.of(s3, s4)); + var s34 = testModel.groupStop("3_4", s3, s4); - var areaStop = testModel.areaStopForTest( - "area", - GeometryUtils - .getGeometryFactory() - .createPolygon( - new Coordinate[] { - new Coordinate(11.0, 63.0), - new Coordinate(11.5, 63.0), - new Coordinate(11.5, 63.5), - new Coordinate(11.0, 63.5), - new Coordinate(11.0, 63.0), - } - ) - ); + var areaStop = testModel.areaStop("area").build(); Trip t = TransitModelForTest.trip("trip").build(); diff --git a/src/test/java/org/opentripplanner/transit/model/timetable/booking/BookingInfoTest.java b/src/test/java/org/opentripplanner/transit/model/timetable/booking/BookingInfoTest.java new file mode 100644 index 00000000000..0bf189a1c29 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/timetable/booking/BookingInfoTest.java @@ -0,0 +1,68 @@ +package org.opentripplanner.transit.model.timetable.booking; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.EnumSet; +import org.junit.jupiter.api.Test; +import org.opentripplanner.transit.model.organization.ContactInfo; + +class BookingInfoTest { + + public static final String URL = "http://booking.otp.org"; + public static final ContactInfo CONTACT = ContactInfo + .of() + .withBookingUrl(URL) + .withContactPerson("Jo Contact") + .build(); + public static final EnumSet BOOKING_METHODS = EnumSet.of( + BookingMethod.CALL_DRIVER + ); + public static final BookingTime BOOKING_TIME_NOON = new BookingTime(LocalTime.NOON, 0); + + @Test + void testBookingInfoWithLatestBookingTime() { + var subject = BookingInfo + .of() + .withContactInfo(CONTACT) + .withBookingMethods(BOOKING_METHODS) + .withLatestBookingTime(BOOKING_TIME_NOON) + .withMessage("message") + .withPickupMessage("pickup") + .withDropOffMessage("dropoff") + .build(); + + assertEquals(CONTACT, subject.getContactInfo()); + assertEquals(BOOKING_METHODS, subject.bookingMethods()); + assertNull(subject.getEarliestBookingTime()); + assertEquals(BOOKING_TIME_NOON, subject.getLatestBookingTime()); + assertEquals("message", subject.getMessage()); + assertEquals("pickup", subject.getPickupMessage()); + assertEquals("dropoff", subject.getDropOffMessage()); + + assertEquals( + "BookingInfo{contactInfo: ContactInfo{contactPerson: 'Jo Contact', bookingUrl: 'http://booking.otp.org'}, bookingMethods: [CALL_DRIVER], latestBookingTime: 12:00, message: 'message', pickupMessage: 'pickup', dropOffMessage: 'dropoff'}", + subject.toString() + ); + } + + @Test + void testBookingInfoWithMinBookingNotice() { + Duration minimumBookingNotice = Duration.ofMinutes(45); + var subject = BookingInfo + .of() + .withBookingMethods(BOOKING_METHODS) + .withMinimumBookingNotice(minimumBookingNotice) + .build(); + + assertNull(subject.getLatestBookingTime()); + assertEquals(minimumBookingNotice, subject.getMinimumBookingNotice()); + + assertEquals( + "BookingInfo{bookingMethods: [CALL_DRIVER], minimumBookingNotice: 45m}", + subject.toString() + ); + } +} diff --git a/src/test/java/org/opentripplanner/transit/model/timetable/booking/BookingTimeTest.java b/src/test/java/org/opentripplanner/transit/model/timetable/booking/BookingTimeTest.java new file mode 100644 index 00000000000..5036defdb88 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/timetable/booking/BookingTimeTest.java @@ -0,0 +1,53 @@ +package org.opentripplanner.transit.model.timetable.booking; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.time.LocalTime; +import org.junit.jupiter.api.Test; +import org.opentripplanner.framework.time.TimeUtils; + +class BookingTimeTest { + + BookingTime noon = new BookingTime(LocalTime.NOON, 0); + BookingTime noonYesterday = new BookingTime(LocalTime.NOON, 1); + BookingTime midnight = new BookingTime(LocalTime.MIDNIGHT, 0); + BookingTime noon2 = new BookingTime(LocalTime.NOON, 0); + + @Test + void equalsAndHashCode() { + assertEquals(noon, noon); + assertEquals(noon, noon2); + assertNotEquals(noon, noonYesterday); + assertNotEquals(noon, midnight); + + assertEquals(noon.hashCode(), noon.hashCode()); + assertEquals(noon.hashCode(), noon2.hashCode()); + assertNotEquals(noon.hashCode(), noonYesterday.hashCode()); + assertNotEquals(noon.hashCode(), midnight.hashCode()); + } + + @Test + void getTime() { + assertEquals(noon.getTime(), LocalTime.NOON); + } + + @Test + void testToString() { + assertEquals("12:00", noon.toString()); + assertEquals("12:00-1d", noonYesterday.toString()); + } + + @Test + void getDaysPrior() { + assertEquals(noon.getDaysPrior(), 0); + assertEquals(noonYesterday.getDaysPrior(), 1); + } + + @Test + void relativeTimeSeconds() { + assertEquals(midnight.relativeTimeSeconds(), 0); + assertEquals(noon.relativeTimeSeconds(), TimeUtils.ONE_DAY_SECONDS / 2); + assertEquals(noonYesterday.relativeTimeSeconds(), -TimeUtils.ONE_DAY_SECONDS / 2); + } +} 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 new file mode 100644 index 00000000000..e0f507a7983 --- /dev/null +++ b/src/test/java/org/opentripplanner/transit/model/timetable/booking/RoutingBookingInfoTest.java @@ -0,0 +1,152 @@ +package org.opentripplanner.transit.model.timetable.booking; + +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.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.List; +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.framework.time.TimeUtils; + +class RoutingBookingInfoTest { + + private static final Duration MINIMUM_BOOKING_NOTICE_20m = Duration.ofMinutes(20); + private static final LocalTime T13_20 = LocalTime.of(13, 20); + private static final LocalTime T13_00 = LocalTime.of(13, 0); + private static final LocalTime T13_00_01 = LocalTime.of(13, 0, 1); + private static final LocalTime T13_40 = LocalTime.of(13, 40); + private static final LocalTime T13_40_01 = LocalTime.of(13, 40, 1); + private static final LocalTime T14_00 = LocalTime.of(14, 0); + private static final LocalTime LATEST_BOOKING_TIME_13_00 = T13_00; + + static List testCase() { + // BOARD-TIME | REQUESTED-BOOKING-TIME | EXPECTED + return List.of( + // Test min-booking-notice <= 13:40 (14:00-20m) + Arguments.of(T14_00, T13_40, Expect.MIN_BOOKING_NOTICE), + Arguments.of(T14_00, T13_40_01, Expect.NONE), + // Test latest-booking-time <= 13_00 + Arguments.of(T13_00, T13_00, Expect.LATEST_BOOKING_TIME), + Arguments.of(T13_00, T13_00_01, Expect.NONE), + // Combination of both + Arguments.of(T13_20, LocalTime.of(13, 0, 0), Expect.BOTH) + ); + } + + @ParameterizedTest + @MethodSource("testCase") + void isThereEnoughTimeToBookWithMinBookingTimeBeforeDeparture( + LocalTime searchTime, + LocalTime requestedBookingTime, + Expect expect + ) { + int searchTimeSec = searchTime.toSecondOfDay(); + + var subject = RoutingBookingInfo + .of(requestedBookingTime.toSecondOfDay()) + .withMinimumBookingNotice(MINIMUM_BOOKING_NOTICE_20m) + .withLatestBookingTime(new BookingTime(LATEST_BOOKING_TIME_13_00, 0)) + .build(); + + // Since we have not set a duration or offset, departure and arrival is the same + assertEquals(expect.latestBookingTime, !subject.exceedsLatestBookingTime()); + assertEquals(expect.minBookingNotice, !subject.exceedsMinimumBookingNotice(searchTimeSec)); + } + + @Test + void earliestDepartureTime() { + int t11_35 = TimeUtils.time("11:35"); + int t11_55 = TimeUtils.time("11:35") + (int) MINIMUM_BOOKING_NOTICE_20m.toSeconds(); + + var subject = RoutingBookingInfo + .of(t11_35) + .withMinimumBookingNotice(MINIMUM_BOOKING_NOTICE_20m) + .build(); + + // 11:55 is the earliest departure time for any time before 11:55 + assertEquals(subject.earliestDepartureTime(0), t11_55); + assertEquals(subject.earliestDepartureTime(t11_55 - 1), t11_55); + assertEquals(subject.earliestDepartureTime(t11_55), t11_55); + assertEquals(subject.earliestDepartureTime(t11_55 + 1), t11_55 + 1); + } + + @Test + void unrestricted() { + assertFalse(RoutingBookingInfo.unrestricted().exceedsMinimumBookingNotice(10_000_000)); + assertFalse(RoutingBookingInfo.unrestricted().exceedsMinimumBookingNotice(0)); + assertFalse(RoutingBookingInfo.unrestricted().exceedsLatestBookingTime()); + + assertSame(RoutingBookingInfo.unrestricted(), RoutingBookingInfo.of(3600).build()); + + assertSame( + RoutingBookingInfo.unrestricted(), + RoutingBookingInfo + .of(RoutingBookingInfo.NOT_SET) + .withMinimumBookingNotice(MINIMUM_BOOKING_NOTICE_20m) + .build() + ); + assertSame( + RoutingBookingInfo.unrestricted(), + RoutingBookingInfo.of(T13_00.toSecondOfDay()).build() + ); + } + + @Test + void testToString() { + var subject = RoutingBookingInfo + .of(TimeUtils.time("11:35")) + .withMinimumBookingNotice(MINIMUM_BOOKING_NOTICE_20m) + .withLatestBookingTime(new BookingTime(LATEST_BOOKING_TIME_13_00, 0)) + .build(); + + assertEquals( + "RoutingBookingInfo{latestBookingTime: 13:00, minimumBookingNotice: 20m}", + subject.toString() + ); + } + + @Test + void testEqAndHashCode() { + var subject = RoutingBookingInfo.of( + TimeUtils.time("11:35"), + BookingInfo.of().withMinimumBookingNotice(MINIMUM_BOOKING_NOTICE_20m).build() + ); + var same = RoutingBookingInfo + .of(TimeUtils.time("11:35")) + .withMinimumBookingNotice(MINIMUM_BOOKING_NOTICE_20m) + .build(); + + // Equals + assertNotSame(subject, same); + assertEquals(subject, same); + assertEquals(true, subject.equals(subject)); + assertNotEquals(subject, RoutingBookingInfo.unrestricted()); + assertNotEquals(subject, ""); + + // HashCode + assertEquals(subject.hashCode(), same.hashCode()); + assertNotEquals(subject.hashCode(), RoutingBookingInfo.unrestricted().hashCode()); + } + + enum Expect { + NONE(false, false), + MIN_BOOKING_NOTICE(true, false), + LATEST_BOOKING_TIME(false, true), + BOTH(true, true); + + final boolean minBookingNotice; + final boolean latestBookingTime; + + Expect(boolean minBookingNotice, boolean latestBookingTime) { + this.minBookingNotice = minBookingNotice; + this.latestBookingTime = latestBookingTime; + } + } +}