diff --git a/.github/workflows/performance-test.yml b/.github/workflows/performance-test.yml
index f4e2d021d21..ddc730fbeae 100644
--- a/.github/workflows/performance-test.yml
+++ b/.github/workflows/performance-test.yml
@@ -39,12 +39,6 @@ jobs:
# extended locations that are run only after merging to dev-2.x
- # Hamburg is disabled because of https://github.com/opentripplanner/OpenTripPlanner/issues/6430
- # - location: hamburg # German city
- # iterations: 1
- # jfr-delay: "50s"
- # profile: extended
-
- location: baden-wuerttemberg # German state of Baden-Württemberg: https://en.wikipedia.org/wiki/Baden-W%C3%BCrttemberg
iterations: 1
jfr-delay: "50s"
diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml
index f0932f007ab..fff11b02076 100644
--- a/.github/workflows/smoke-tests.yml
+++ b/.github/workflows/smoke-tests.yml
@@ -19,8 +19,8 @@ jobs:
locations:
- name: seattle
sleep: 30
- - name: atlanta
- sleep: 15
+ #- name: atlanta
+ # sleep: 15
- name: houston
sleep: 30
- name: denver
diff --git a/application/pom.xml b/application/pom.xml
index a54d76e9458..a9c495ca082 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -296,12 +296,6 @@
onebusaway-gtfs11.2.2
-
-
- org.processing
- core
- 2.2.1
- net.java.dev.jets3t
@@ -323,7 +317,7 @@
com.graphql-javagraphql-java
- 25.0
+ 26.0com.graphql-java
@@ -333,7 +327,7 @@
org.apache.httpcomponents.client5httpclient5
- 5.6
+ 5.6.1commons-cli
@@ -349,7 +343,7 @@
com.hivemqhivemq-mqtt-client
- 1.3.12
+ 1.3.13io.github.ci-cmg
diff --git a/application/src/client/index.html b/application/src/client/index.html
index ec630252590..12e8ecb4b28 100644
--- a/application/src/client/index.html
+++ b/application/src/client/index.html
@@ -5,8 +5,8 @@
OTP Debug
-
-
+
+
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/CarpoolEstimatedVehicleJourneyData.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/CarpoolEstimatedVehicleJourneyData.java
index ef15793c74f..6f3ac78704e 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/CarpoolEstimatedVehicleJourneyData.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/CarpoolEstimatedVehicleJourneyData.java
@@ -19,7 +19,9 @@
import uk.org.siri.siri21.EstimatedVehicleJourney;
import uk.org.siri.siri21.NaturalLanguageStringStructure;
import uk.org.siri.siri21.OperatorRefStructure;
+import uk.org.siri.siri21.PassengerCapacityStructure;
import uk.org.siri.siri21.StopAssignmentStructure;
+import uk.org.siri.siri21.VehicleOccupancyStructure;
public class CarpoolEstimatedVehicleJourneyData {
@@ -174,6 +176,139 @@ static EstimatedCall forPolygon(String posList) {
return call;
}
+ public static EstimatedVehicleJourney journeyWithTotalCapacity(int capacity) {
+ var journey = minimalCompleteJourney();
+ for (var call : journey.getEstimatedCalls().getEstimatedCalls()) {
+ addTotalCapacity(call, capacity);
+ }
+ return journey;
+ }
+
+ public static EstimatedVehicleJourney journeyWithDifferentCapacitiesPerCall(
+ int firstCapacity,
+ int lastCapacity
+ ) {
+ var journey = minimalCompleteJourney();
+ var calls = journey.getEstimatedCalls().getEstimatedCalls();
+ addTotalCapacity(calls.getFirst(), firstCapacity);
+ addTotalCapacity(calls.getLast(), lastCapacity);
+ return journey;
+ }
+
+ public static EstimatedVehicleJourney journeyWithOnboardCounts(int... onboardCounts) {
+ var journey = minimalCompleteJourney();
+ var calls = journey.getEstimatedCalls().getEstimatedCalls();
+ for (int i = 0; i < Math.min(onboardCounts.length, calls.size()); i++) {
+ addOnboardCount(calls.get(i), onboardCounts[i]);
+ }
+ return journey;
+ }
+
+ private static void addTotalCapacity(EstimatedCall call, int totalCapacity) {
+ var capacity = new PassengerCapacityStructure();
+ capacity.setTotalCapacity(BigInteger.valueOf(totalCapacity));
+ call.getExpectedDepartureCapacities().add(capacity);
+ }
+
+ public static EstimatedVehicleJourney journeyWithLatestExpectedArrivalTime(
+ int expectedArrivalMinutes,
+ int latestExpectedArrivalMinutes
+ ) {
+ var journey = minimalCompleteJourney();
+ var lastStop = journey.getEstimatedCalls().getEstimatedCalls().getLast();
+ var base = lastStop.getAimedArrivalTime();
+ lastStop.setExpectedArrivalTime(base.plusMinutes(expectedArrivalMinutes));
+ lastStop.setLatestExpectedArrivalTime(base.plusMinutes(latestExpectedArrivalMinutes));
+ return journey;
+ }
+
+ public static EstimatedVehicleJourney journeyWithLatestExpectedArrivalTimeAimedOnly(
+ int latestExpectedArrivalMinutes
+ ) {
+ var journey = minimalCompleteJourney();
+ var lastStop = journey.getEstimatedCalls().getEstimatedCalls().getLast();
+ var base = lastStop.getAimedArrivalTime();
+ lastStop.setExpectedArrivalTime(null);
+ lastStop.setLatestExpectedArrivalTime(base.plusMinutes(latestExpectedArrivalMinutes));
+ return journey;
+ }
+
+ /**
+ * Builds a 3-stop journey (origin, intermediate, destination) where the intermediate and
+ * destination stops each get their own {@code expectedArrivalTime} and
+ * {@code latestExpectedArrivalTime}, enabling assertions on per-stop deviation budgets.
+ * Arrival times are offset from {@code now} in minutes.
+ */
+ public static EstimatedVehicleJourney journeyWithPerStopLatestExpectedArrivalTimes(
+ int intermediateExpectedArrivalMinutes,
+ int intermediateLatestExpectedArrivalMinutes,
+ int lastExpectedArrivalMinutes,
+ int lastLatestExpectedArrivalMinutes
+ ) {
+ var base = ZonedDateTime.now();
+
+ var origin = forPoint(OSLO_EAST);
+ origin.setAimedDepartureTime(base);
+ addStopName(origin, "Origin");
+
+ var intermediate = createArrivalStop(
+ OSLO_NORTH,
+ "Intermediate",
+ base,
+ intermediateExpectedArrivalMinutes,
+ intermediateLatestExpectedArrivalMinutes
+ );
+ intermediate.setAimedDepartureTime(base.plusMinutes(intermediateExpectedArrivalMinutes));
+
+ var last = createArrivalStop(
+ OSLO_NORTH,
+ "Last",
+ base,
+ lastExpectedArrivalMinutes,
+ lastLatestExpectedArrivalMinutes
+ );
+
+ var journey = new EstimatedVehicleJourney();
+ var operator = new OperatorRefStructure();
+ operator.setValue("TESTOPERATOR");
+ journey.setEstimatedVehicleJourneyCode("unittest");
+ journey.setOperatorRef(operator);
+ journey.setEstimatedCalls(new EstimatedVehicleJourney.EstimatedCalls());
+ journey.getEstimatedCalls().getEstimatedCalls().add(origin);
+ journey.getEstimatedCalls().getEstimatedCalls().add(intermediate);
+ journey.getEstimatedCalls().getEstimatedCalls().add(last);
+
+ return journey;
+ }
+
+ private static EstimatedCall createArrivalStop(
+ WgsCoordinate coordinate,
+ String name,
+ ZonedDateTime base,
+ int expectedArrivalMinutes,
+ int latestExpectedArrivalMinutes
+ ) {
+ var arrivalTime = base.plusMinutes(expectedArrivalMinutes);
+ var call = forPoint(coordinate);
+ call.setAimedArrivalTime(arrivalTime);
+ call.setExpectedArrivalTime(arrivalTime);
+ call.setLatestExpectedArrivalTime(base.plusMinutes(latestExpectedArrivalMinutes));
+ addStopName(call, name);
+ return call;
+ }
+
+ private static void addStopName(EstimatedCall call, String name) {
+ var nameStruct = new NaturalLanguageStringStructure();
+ nameStruct.setValue(name);
+ call.getStopPointNames().add(nameStruct);
+ }
+
+ private static void addOnboardCount(EstimatedCall call, int onboardCount) {
+ var occupancy = new VehicleOccupancyStructure();
+ occupancy.setOnboardCount(BigInteger.valueOf(onboardCount));
+ call.getExpectedDepartureOccupancies().add(occupancy);
+ }
+
static AimedFlexibleArea poslistToAimedFlexibleArea(String coordinates) {
var gmlFactory = new ObjectFactory();
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/CarpoolGraphPathBuilder.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/CarpoolGraphPathBuilder.java
index c5a9d22dd79..27b7647b30c 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/CarpoolGraphPathBuilder.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/CarpoolGraphPathBuilder.java
@@ -18,9 +18,6 @@ public class CarpoolGraphPathBuilder {
// Walking speed in m/s (OTP default from WalkPreferences)
private static final double WALKING_SPEED_MPS = 1.33;
- // Default number of edges to distribute duration across
- private static final int DEFAULT_NUM_EDGES = 3;
-
/**
* Creates a GraphPath with default 5-minute duration.
*/
@@ -30,6 +27,10 @@ public static GraphPath createGraphPath() {
/**
* Creates a GraphPath with specified duration using State chain.
+ * Uses a single edge with floor distance to avoid rounding errors: the edge traversal
+ * applies ceiling when converting to milliseconds, and State.getTime() applies ceiling
+ * when converting to seconds, so floor distance ensures the final second-precision
+ * duration matches the requested value.
*
* @param duration Total duration for the path
* @return GraphPath with real State objects and accurate timing
@@ -37,17 +38,9 @@ public static GraphPath createGraphPath() {
public static GraphPath createGraphPath(Duration duration) {
var builder = TestStateBuilder.ofWalking();
- // Calculate distance needed for target duration
- double totalDistanceMeters = duration.toSeconds() * WALKING_SPEED_MPS;
-
- // Distribute across multiple edges for realistic path
- int numEdges = DEFAULT_NUM_EDGES;
- int distancePerEdge = (int) Math.ceil(totalDistanceMeters / numEdges);
+ int distanceMeters = (int) (duration.toSeconds() * WALKING_SPEED_MPS);
- // Build state chain with calculated distances
- for (int i = 0; i < numEdges; i++) {
- builder.streetEdge("segment-" + i, distancePerEdge);
- }
+ builder.streetEdge("segment-0", distanceMeters);
return new GraphPath<>(builder.build());
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/CarpoolTripTestData.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/CarpoolTripTestData.java
index 23f30b9dd71..11674add0d3 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/CarpoolTripTestData.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/CarpoolTripTestData.java
@@ -4,11 +4,10 @@
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
import org.opentripplanner.core.model.id.FeedScopedId;
import org.opentripplanner.ext.carpooling.model.CarpoolStop;
-import org.opentripplanner.ext.carpooling.model.CarpoolStopType;
import org.opentripplanner.ext.carpooling.model.CarpoolTrip;
+import org.opentripplanner.ext.carpooling.model.CarpoolTripBuilder;
import org.opentripplanner.street.geometry.WgsCoordinate;
/**
@@ -16,16 +15,17 @@
*/
public class CarpoolTripTestData {
- private static final AtomicInteger ID_COUNTER = new AtomicInteger(0);
- private static final AtomicInteger AREA_STOP_COUNTER = new AtomicInteger(0);
+ private static int idCounter = 0;
+
+ private static final int DEFAULT_TOTAL_CAPACITY = CarpoolTrip.DEFAULT_TOTAL_CAPACITY;
+ private static final Duration DEFAULT_DEVIATION_BUDGET = Duration.ofMinutes(10);
/**
- * Creates a simple trip with origin and destination stops, default capacity of 4.
+ * Creates a simple trip with origin and destination stops.
*/
public static CarpoolTrip createSimpleTrip(WgsCoordinate boarding, WgsCoordinate alighting) {
- var origin = createOriginStop(boarding);
- var destination = createDestinationStop(alighting, 1);
- return createTripWithCapacity(4, List.of(origin, destination));
+ var stops = List.of(createOriginStop(boarding), createDestinationStop(alighting));
+ return buildTrip(DEFAULT_TOTAL_CAPACITY, null, stops);
}
/**
@@ -36,14 +36,11 @@ public static CarpoolTrip createSimpleTripWithTime(
WgsCoordinate alighting,
ZonedDateTime startTime
) {
- var origin = createOriginStopWithTime(boarding, startTime, startTime);
- var destination = createDestinationStopWithTime(
- alighting,
- 1,
- startTime.plusHours(1),
- startTime.plusHours(1)
+ var stops = List.of(
+ createOriginStopWithTime(boarding, startTime, startTime),
+ createDestinationStopWithTime(alighting, startTime.plusHours(1), startTime.plusHours(1))
);
- return createTripWithTime(startTime, 4, List.of(origin, destination));
+ return buildTrip(DEFAULT_TOTAL_CAPACITY, startTime, stops);
}
/**
@@ -57,160 +54,201 @@ public static CarpoolTrip createTripWithStops(
List allStops = new ArrayList<>();
allStops.add(createOriginStop(boarding));
- // Renumber intermediate stops to account for origin at position 0
for (int i = 0; i < intermediateStops.size(); i++) {
CarpoolStop intermediate = intermediateStops.get(i);
allStops.add(
- CarpoolStop.of(intermediate.getId(), () -> intermediate.getIndex() + 1)
+ CarpoolStop.of(intermediate.getId())
.withCoordinate(intermediate.getCoordinate())
- .withCarpoolStopType(intermediate.getCarpoolStopType())
.withExpectedDepartureTime(intermediate.getExpectedDepartureTime())
+ .withAimedDepartureTime(intermediate.getAimedDepartureTime())
+ .withExpectedArrivalTime(intermediate.getExpectedArrivalTime())
.withAimedArrivalTime(intermediate.getAimedArrivalTime())
+ .withOnboardCount(intermediate.getOnboardCount())
+ .withDeviationBudget(intermediate.getDeviationBudget())
+ .build()
+ );
+ }
+
+ allStops.add(createDestinationStop(alighting));
+ return buildTrip(DEFAULT_TOTAL_CAPACITY, null, allStops);
+ }
+
+ /**
+ * Creates a trip with origin, intermediate stops, and destination. The deviation budget is applied
+ * to the origin and destination stops, while intermediate stops retain their own deviation budget.
+ */
+ public static CarpoolTrip createTripWithStops(
+ WgsCoordinate boarding,
+ List intermediateStops,
+ WgsCoordinate alighting,
+ Duration deviationBudget
+ ) {
+ List allStops = new ArrayList<>();
+ allStops.add(createOriginStopWithDeviationBudget(boarding, deviationBudget));
+
+ for (int i = 0; i < intermediateStops.size(); i++) {
+ CarpoolStop intermediate = intermediateStops.get(i);
+ allStops.add(
+ CarpoolStop.of(intermediate.getId())
+ .withCoordinate(intermediate.getCoordinate())
+ .withExpectedDepartureTime(intermediate.getExpectedDepartureTime())
+ .withAimedDepartureTime(intermediate.getAimedDepartureTime())
.withExpectedArrivalTime(intermediate.getExpectedArrivalTime())
- .withAimedArrivalTime(intermediate.getAimedDepartureTime())
- .withSequenceNumber(intermediate.getSequenceNumber() + 1)
- .withPassengerDelta(intermediate.getPassengerDelta())
+ .withAimedArrivalTime(intermediate.getAimedArrivalTime())
+ .withOnboardCount(intermediate.getOnboardCount())
+ .withDeviationBudget(intermediate.getDeviationBudget())
.build()
);
}
- allStops.add(createDestinationStop(alighting, allStops.size()));
- return createTripWithCapacity(4, allStops);
+ allStops.add(createDestinationStopWithDeviationBudget(alighting, deviationBudget));
+ return buildTrip(DEFAULT_TOTAL_CAPACITY, null, allStops);
}
/**
* Creates a trip with specified capacity and all stops (including origin/destination).
*/
- public static CarpoolTrip createTripWithCapacity(int seats, List stops) {
- return createTripWithDeviationBudget(Duration.ofMinutes(10), seats, stops);
+ public static CarpoolTrip createTripWithCapacity(int capacity, List stops) {
+ return buildTrip(capacity, null, stops);
}
/**
- * Creates a trip with specified deviation budget.
+ * Creates a trip with specified deviation budget on all stops.
*/
public static CarpoolTrip createTripWithDeviationBudget(
Duration deviationBudget,
WgsCoordinate boarding,
WgsCoordinate alighting
) {
- var origin = createOriginStop(boarding);
- var destination = createDestinationStop(alighting, 1);
- return createTripWithDeviationBudget(deviationBudget, 4, List.of(origin, destination));
+ var origin = createOriginStopWithDeviationBudget(boarding, deviationBudget);
+ var destination = createDestinationStopWithDeviationBudget(alighting, deviationBudget);
+ return buildTrip(DEFAULT_TOTAL_CAPACITY, null, List.of(origin, destination));
}
/**
- * Creates a trip with all parameters specified.
+ * Creates a trip with specific start time.
*/
- public static CarpoolTrip createTripWithDeviationBudget(
- Duration deviationBudget,
- int seats,
+ public static CarpoolTrip createTripWithTime(
+ ZonedDateTime startTime,
+ int capacity,
List stops
) {
- return new org.opentripplanner.ext.carpooling.model.CarpoolTripBuilder(
- FeedScopedId.ofNullable("TEST", "trip-" + ID_COUNTER.incrementAndGet())
- )
- .withStops(stops)
- .withAvailableSeats(seats)
- .withStartTime(ZonedDateTime.now())
- .withDeviationBudget(deviationBudget)
- .build();
+ return buildTrip(capacity, startTime, stops);
}
/**
- * Creates a trip with specific start time and all other parameters.
- * End time is calculated as startTime + 1 hour.
+ * Creates a CarpoolStop with specified onboard count.
*/
- public static CarpoolTrip createTripWithTime(
- ZonedDateTime startTime,
- int seats,
- List stops
- ) {
- return new org.opentripplanner.ext.carpooling.model.CarpoolTripBuilder(
- FeedScopedId.ofNullable("TEST", "trip-" + ID_COUNTER.incrementAndGet())
- )
- .withStops(stops)
- .withAvailableSeats(seats)
- .withStartTime(startTime)
- .withEndTime(startTime.plusHours(1))
- .withDeviationBudget(Duration.ofMinutes(10))
- .build();
+ public static CarpoolStop createStop(int onboardCount) {
+ return createStopAt(onboardCount, CarpoolTestCoordinates.OSLO_CENTER);
}
/**
- * Creates a CarpoolStop with specified sequence (0-based) and passenger delta.
+ * Creates a CarpoolStop at a specific location with onboardCount=1 (driver only).
*/
- public static CarpoolStop createStop(int zeroBasedSequence, int passengerDelta) {
- return createStopAt(zeroBasedSequence, passengerDelta, CarpoolTestCoordinates.OSLO_CENTER);
+ public static CarpoolStop createStopAt(WgsCoordinate location) {
+ return createStopAt(1, location);
}
/**
- * Creates a CarpoolStop at a specific location.
+ * Creates a CarpoolStop at a specific location with a specific deviation budget.
*/
- public static CarpoolStop createStopAt(int sequence, WgsCoordinate location) {
- return createStopAt(sequence, 0, location);
+ public static CarpoolStop createStopAt(WgsCoordinate location, Duration deviationBudget) {
+ return CarpoolStop.of(FeedScopedId.ofNullable("TEST", "area-" + ++idCounter))
+ .withCoordinate(location)
+ .withOnboardCount(1)
+ .withDeviationBudget(deviationBudget)
+ .build();
}
/**
* Creates a CarpoolStop with all parameters.
*/
- public static CarpoolStop createStopAt(int sequence, int passengerDelta, WgsCoordinate location) {
- return CarpoolStop.of(
- FeedScopedId.ofNullable("TEST", "area-" + AREA_STOP_COUNTER.incrementAndGet()),
- AREA_STOP_COUNTER::getAndIncrement
- )
+ public static CarpoolStop createStopAt(int onboardCount, WgsCoordinate location) {
+ return CarpoolStop.of(FeedScopedId.ofNullable("TEST", "area-" + ++idCounter))
.withCoordinate(location)
- .withSequenceNumber(sequence)
- .withPassengerDelta(passengerDelta)
+ .withOnboardCount(onboardCount)
+ .withDeviationBudget(DEFAULT_DEVIATION_BUDGET)
.build();
}
- /**
- * Creates an origin stop (first stop, PICKUP_ONLY, passengerDelta=0, departure times only).
- */
public static CarpoolStop createOriginStop(WgsCoordinate location) {
return createOriginStopWithTime(location, null, null);
}
- /**
- * Creates an origin stop with specific departure times.
- */
public static CarpoolStop createOriginStopWithTime(
WgsCoordinate location,
ZonedDateTime expectedDepartureTime,
ZonedDateTime aimedDepartureTime
) {
- return CarpoolStop.of(FeedScopedId.ofNullable("TEST", "area-0"), () -> 0)
+ return CarpoolStop.of(FeedScopedId.ofNullable("TEST", "area-" + ++idCounter))
.withCoordinate(location)
+ .withOnboardCount(1)
.withExpectedDepartureTime(expectedDepartureTime)
.withAimedDepartureTime(aimedDepartureTime)
+ .withDeviationBudget(DEFAULT_DEVIATION_BUDGET)
.build();
}
/**
- * Creates a destination stop (last stop, DROP_OFF_ONLY, passengerDelta=0, arrival times only).
+ * Creates an origin stop with specific deviation budget.
*/
- public static CarpoolStop createDestinationStop(WgsCoordinate location, int sequenceNumber) {
- return createDestinationStopWithTime(location, sequenceNumber, null, null);
+ public static CarpoolStop createOriginStopWithDeviationBudget(
+ WgsCoordinate location,
+ Duration deviationBudget
+ ) {
+ return CarpoolStop.of(FeedScopedId.ofNullable("TEST", "area-" + ++idCounter))
+ .withCoordinate(location)
+ .withOnboardCount(1)
+ .withDeviationBudget(deviationBudget)
+ .build();
+ }
+
+ public static CarpoolStop createDestinationStop(WgsCoordinate location) {
+ return createDestinationStopWithTime(location, null, null);
}
- /**
- * Creates a destination stop with specific arrival times.
- */
public static CarpoolStop createDestinationStopWithTime(
WgsCoordinate location,
- int sequenceNumber,
ZonedDateTime expectedArrivalTime,
ZonedDateTime aimedArrivalTime
) {
- return CarpoolStop.of(
- FeedScopedId.ofNullable("TEST", "area-" + AREA_STOP_COUNTER.incrementAndGet()),
- AREA_STOP_COUNTER::getAndIncrement
- )
+ return CarpoolStop.of(FeedScopedId.ofNullable("TEST", "area-" + ++idCounter))
.withCoordinate(location)
- .withCarpoolStopType(CarpoolStopType.DROP_OFF_ONLY)
- .withSequenceNumber(sequenceNumber)
+ .withOnboardCount(1)
.withExpectedArrivalTime(expectedArrivalTime)
.withAimedArrivalTime(aimedArrivalTime)
+ .withDeviationBudget(DEFAULT_DEVIATION_BUDGET)
.build();
}
+
+ /**
+ * Creates a destination stop with specific deviation budget.
+ */
+ public static CarpoolStop createDestinationStopWithDeviationBudget(
+ WgsCoordinate location,
+ Duration deviationBudget
+ ) {
+ return CarpoolStop.of(FeedScopedId.ofNullable("TEST", "area-" + ++idCounter))
+ .withCoordinate(location)
+ .withOnboardCount(1)
+ .withDeviationBudget(deviationBudget)
+ .build();
+ }
+
+ private static CarpoolTrip buildTrip(
+ int capacity,
+ ZonedDateTime startTime,
+ List stops
+ ) {
+ var actualStartTime = startTime != null ? startTime : ZonedDateTime.now();
+ var builder = new CarpoolTripBuilder(FeedScopedId.ofNullable("TEST", "trip-" + ++idCounter))
+ .withStops(stops)
+ .withTotalCapacity(capacity)
+ .withStartTime(actualStartTime);
+ if (startTime != null) {
+ builder.withEndTime(startTime.plusHours(1));
+ }
+ return builder.build();
+ }
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/constraints/PassengerDelayConstraintsTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/constraints/PassengerDelayConstraintsTest.java
index 8364132d2e5..a29b8b62686 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/constraints/PassengerDelayConstraintsTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/constraints/PassengerDelayConstraintsTest.java
@@ -1,140 +1,164 @@
package org.opentripplanner.ext.carpooling.constraints;
-import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.opentripplanner.ext.carpooling.util.GraphPathUtils.calculateCumulativeDurations;
import java.time.Duration;
-import org.junit.jupiter.api.BeforeEach;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import org.opentripplanner.astar.model.GraphPath;
+import org.opentripplanner.core.model.id.FeedScopedId;
import org.opentripplanner.ext.carpooling.CarpoolGraphPathBuilder;
+import org.opentripplanner.ext.carpooling.model.CarpoolStop;
import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.vertex.Vertex;
import org.opentripplanner.street.search.state.State;
class PassengerDelayConstraintsTest {
- private PassengerDelayConstraints constraints;
+ private static final AtomicInteger STOP_COUNTER = new AtomicInteger(0);
+ private static final Duration FIVE_MINUTES = Duration.ofMinutes(5);
- @BeforeEach
- void setup() {
- constraints = new PassengerDelayConstraints();
+ private static CarpoolStop stopWithBudget(Duration budget) {
+ return CarpoolStop.of(FeedScopedId.ofNullable("TEST", "stop-" + STOP_COUNTER.incrementAndGet()))
+ .withCoordinate(new org.opentripplanner.street.geometry.WgsCoordinate(59.9, 10.7))
+ .withDeviationBudget(budget)
+ .build();
}
@Test
- void satisfiesConstraints_noExistingStops_alwaysAccepts() {
- Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(10) };
+ void satisfiesConstraints_delayWellUnderBudget_accepts() {
+ Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(5), Duration.ofMinutes(15) };
+ var stops = List.of(
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES)
+ );
- // Modified route with passenger inserted
+ // Stop1 delay: 7min - 5min = 2min (within 5min budget)
+ // Destination delay: 17min - 15min = 2min (within 5min budget)
GraphPath[] modifiedSegments = new GraphPath[] {
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(3)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(4)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(7)),
};
- // Should accept - no existing passengers to protect
assertTrue(
- constraints.satisfiesConstraints(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
1,
- 2
+ 3,
+ stops
)
);
}
@Test
- void satisfiesConstraints_delayWellUnderThreshold_accepts() {
- // Original timings: 0min -> 5min -> 15min
- Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(5), Duration.ofMinutes(15) };
+ void satisfiesConstraints_delayExactlyAtBudget_accepts() {
+ Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(10), Duration.ofMinutes(20) };
+ var stops = List.of(
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES)
+ );
- // Modified route: boarding -> pickup -> stop1 -> dropoff -> alighting
- // Timings: 0min -> 3min -> 7min -> 12min -> 17min
- // Stop1 delay: 7min - 5min = 2min (well under 5min threshold)
+ // Stop1 delay: 15min - 10min = 5min (exactly at 5min budget)
GraphPath[] modifiedSegments = new GraphPath[] {
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(3)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(4)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
};
assertTrue(
- constraints.satisfiesConstraints(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
1,
- 3
+ 3,
+ stops
)
);
}
@Test
- void satisfiesConstraints_delayExactlyAtThreshold_accepts() {
- // Original route with one stop
+ void satisfiesConstraints_delayOverBudget_rejects() {
Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(10), Duration.ofMinutes(20) };
+ var stops = List.of(
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES)
+ );
- // Modified route where stop1 is delayed by exactly 5 minutes
- // Timings: 0min -> 5min -> 15min -> 20min -> 25min
- // Stop1 delay: 15min - 10min = 5min (exactly at threshold)
+ // Stop1 delay: 16min - 10min = 6min (exceeds 5min budget)
GraphPath[] modifiedSegments = new GraphPath[] {
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(11)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
};
- assertTrue(
- constraints.satisfiesConstraints(
+ assertFalse(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
1,
- 3
+ 3,
+ stops
)
);
}
@Test
- void satisfiesConstraints_delayOverThreshold_rejects() {
- // Original route with one stop
+ void satisfiesConstraints_destinationOverBudget_rejects() {
Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(10), Duration.ofMinutes(20) };
+ var stops = List.of(
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(Duration.ofMinutes(20)),
+ stopWithBudget(FIVE_MINUTES)
+ );
- // Modified route where stop1 is delayed by 6 minutes (over 5min threshold)
- // Timings: 0min -> 5min -> 16min -> 21min -> 26min
- // Stop1 delay: 16min - 10min = 6min (exceeds threshold)
+ // Stop1 delay: 12min - 10min = 2min (within 20min budget)
+ // Destination delay: 27min - 20min = 7min (exceeds 5min budget)
GraphPath[] modifiedSegments = new GraphPath[] {
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(11)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(7)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
};
assertFalse(
- constraints.satisfiesConstraints(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
1,
- 3
+ 3,
+ stops
)
);
}
@Test
- void satisfiesConstraints_multipleStops_oneOverThreshold_rejects() {
- // Original route: boarding -> stop1 -> stop2 -> alighting
+ void satisfiesConstraints_multipleStops_oneOverBudget_rejects() {
Duration[] originalTimes = {
Duration.ZERO,
Duration.ofMinutes(10),
Duration.ofMinutes(20),
Duration.ofMinutes(30),
};
+ var stops = List.of(
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES)
+ );
- // Modified route where stop1 is ok (3min delay) but stop2 exceeds (7min delay)
- // Timings: 0min -> 5min -> 13min -> 18min -> 27min -> 32min
- // Stop1 delay: 13min - 10min = 3min ✓
- // Stop2 delay: 27min - 20min = 7min ✗
+ // Stop1 delay: 13min - 10min = 3min ok
+ // Stop2 delay: 27min - 20min = 7min exceeds
GraphPath[] modifiedSegments = new GraphPath[] {
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(8)),
@@ -144,29 +168,31 @@ void satisfiesConstraints_multipleStops_oneOverThreshold_rejects() {
};
assertFalse(
- constraints.satisfiesConstraints(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
1,
- 3
+ 3,
+ stops
)
);
}
@Test
- void satisfiesConstraints_multipleStops_allUnderThreshold_accepts() {
- // Original route: boarding -> stop1 -> stop2 -> alighting
+ void satisfiesConstraints_multipleStops_allUnderBudget_accepts() {
Duration[] originalTimes = {
Duration.ZERO,
Duration.ofMinutes(10),
Duration.ofMinutes(20),
Duration.ofMinutes(30),
};
+ var stops = List.of(
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES)
+ );
- // Modified route where both stops have acceptable delays
- // Timings: 0min -> 5min -> 12min -> 17min -> 24min -> 34min
- // Stop1 delay: 12min - 10min = 2min ✓
- // Stop2 delay: 24min - 20min = 4min ✓
GraphPath[] modifiedSegments = new GraphPath[] {
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(7)),
@@ -176,294 +202,325 @@ void satisfiesConstraints_multipleStops_allUnderThreshold_accepts() {
};
assertTrue(
- constraints.satisfiesConstraints(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
1,
- 3
+ 3,
+ stops
)
);
}
@Test
- void satisfiesConstraints_passengerBeforeAllStops_checksAllStops() {
- // Original route: boarding -> stop1 -> stop2 -> alighting
- Duration[] originalTimes = {
- Duration.ZERO,
- Duration.ofMinutes(10),
- Duration.ofMinutes(20),
- Duration.ofMinutes(30),
- };
+ void satisfiesConstraints_differentBudgetsPerStop() {
+ Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(10), Duration.ofMinutes(20) };
+ // Stop 1 has 2min budget, destination has 10min budget
+ var stops = List.of(
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(Duration.ofMinutes(2)),
+ stopWithBudget(Duration.ofMinutes(10))
+ );
- // Passenger inserted at very beginning (pickup at 1, dropoff at 2)
- // Modified: boarding -> pickup -> dropoff -> stop1 -> stop2 -> alighting
- // Mapping: stop1 (orig 1) -> mod 3, stop2 (orig 2) -> mod 4
- // Timings: 0min -> 3min -> 5min -> 13min -> 24min -> 34min
- // Stop1 delay: 13min - 10min = 3min ✓
- // Stop2 delay: 24min - 20min = 4min ✓
+ // Stop1 delay: 13min - 10min = 3min (exceeds 2min budget)
GraphPath[] modifiedSegments = new GraphPath[] {
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(3)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(2)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(8)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(11)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
};
- assertTrue(
- constraints.satisfiesConstraints(
+ assertFalse(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
1,
- 2
+ 3,
+ stops
)
);
}
@Test
- void satisfiesConstraints_passengerAfterAllStops_checksAllStops() {
- // Original route: boarding -> stop1 -> stop2 -> alighting
- Duration[] originalTimes = {
- Duration.ZERO,
- Duration.ofMinutes(10),
- Duration.ofMinutes(20),
- Duration.ofMinutes(30),
- };
+ void satisfiesConstraints_noDelay_accepts() {
+ Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(10), Duration.ofMinutes(20) };
+ var stops = List.of(
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES)
+ );
- // Passenger inserted at very end (pickup at 3, dropoff at 4)
- // Modified: boarding -> stop1 -> stop2 -> pickup -> dropoff -> alighting
- // Mapping: stop1 (orig 1) -> mod 1, stop2 (orig 2) -> mod 2
- // Even though passenger comes after, routing to pickup might cause delays
- // Timings: 0min -> 11min -> 22min -> 27min -> 30min -> 40min
- // Stop1 delay: 11min - 10min = 1min ✓
- // Stop2 delay: 22min - 20min = 2min ✓
GraphPath[] modifiedSegments = new GraphPath[] {
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(11)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(11)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(4)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(6)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(3)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
};
assertTrue(
- constraints.satisfiesConstraints(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
+ 1,
3,
- 4
+ stops
)
);
}
@Test
- void satisfiesConstraints_passengerBetweenStops_checksAllStops() {
- // Original route: boarding -> stop1 -> stop2 -> alighting
- Duration[] originalTimes = {
- Duration.ZERO,
- Duration.ofMinutes(10),
- Duration.ofMinutes(20),
- Duration.ofMinutes(30),
- };
+ void satisfiesConstraints_zeroBudget_rejectsAnyDelay() {
+ Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(10), Duration.ofMinutes(20) };
+ var stops = List.of(
+ stopWithBudget(Duration.ZERO),
+ stopWithBudget(Duration.ZERO),
+ stopWithBudget(Duration.ZERO)
+ );
- // Passenger inserted between stops (pickup at 2, dropoff at 3)
- // Modified: boarding -> stop1 -> pickup -> dropoff -> stop2 -> alighting
- // Mapping: stop1 (orig 1) -> mod 1, stop2 (orig 2) -> mod 4
- // Timings: 0min -> 11min -> 14min -> 17min -> 24min -> 34min
- // Stop1 delay: 11min - 10min = 1min ✓
- // Stop2 delay: 24min - 20min = 4min ✓
+ // Stop1 delay: 10min + 1s - 10min = 1s (exceeds zero budget)
GraphPath[] modifiedSegments = new GraphPath[] {
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(11)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(3)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(3)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(7)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5).plusSeconds(1)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
};
- assertTrue(
- constraints.satisfiesConstraints(
+ assertFalse(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
- 2,
- 3
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
+ 1,
+ 3,
+ stops
)
);
}
@Test
- void customMaxDelay_acceptsWithinCustomThreshold() {
- var customConstraints = new PassengerDelayConstraints(Duration.ofMinutes(10));
-
- Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(10), Duration.ofMinutes(20) };
+ void satisfiesConstraints_zeroBudget_noDelay_accepts() {
+ var stops = List.of(
+ stopWithBudget(Duration.ZERO),
+ stopWithBudget(Duration.ZERO),
+ stopWithBudget(Duration.ZERO)
+ );
- // Stop1 delayed by 8 minutes (within 10min custom threshold)
+ // Use the same GraphPaths to derive both original and modified times
+ // so there is truly zero delay (avoids rounding from GraphPath construction)
GraphPath[] modifiedSegments = new GraphPath[] {
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(13)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(4)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(6)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
};
+ Duration[] cumulativeDurations = calculateCumulativeDurations(modifiedSegments, Duration.ZERO);
+
+ // originalTimes = modified times at the original stop positions
+ // With pickup=1, dropoff=3: original indices [0,1,2] map to modified [0,2,4]
+ Duration[] originalTimes = {
+ cumulativeDurations[0],
+ cumulativeDurations[2],
+ cumulativeDurations[4],
+ };
assertTrue(
- customConstraints.satisfiesConstraints(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ cumulativeDurations,
1,
- 3
+ 3,
+ stops
)
);
}
@Test
- void customMaxDelay_rejectsOverCustomThreshold() {
- var customConstraints = new PassengerDelayConstraints(Duration.ofMinutes(2));
-
+ void satisfiesConstraints_largeBudget_acceptsLargeDelay() {
Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(10), Duration.ofMinutes(20) };
+ var stops = List.of(
+ stopWithBudget(Duration.ZERO),
+ stopWithBudget(Duration.ofHours(1)),
+ stopWithBudget(Duration.ofHours(1))
+ );
- // Stop1 delayed by 3 minutes (over 2min custom threshold)
+ // Stop1 delay: 40min - 10min = 30min (within 60min budget)
GraphPath[] modifiedSegments = new GraphPath[] {
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(8)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(35)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
};
- assertFalse(
- customConstraints.satisfiesConstraints(
+ assertTrue(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
1,
- 3
+ 3,
+ stops
)
);
}
@Test
- void customMaxDelay_zeroTolerance_rejectsAnyDelay() {
- var strictConstraints = new PassengerDelayConstraints(Duration.ZERO);
-
+ void satisfiesConstraints_tightAndPermissiveStops_respectsEachBudget() {
Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(10), Duration.ofMinutes(20) };
+ // Stop 1 is strict (3min), destination is permissive (30min)
+ var stops = List.of(
+ stopWithBudget(Duration.ZERO),
+ stopWithBudget(Duration.ofMinutes(3)),
+ stopWithBudget(Duration.ofMinutes(30))
+ );
- // Stop1 delayed by even 1 second
+ // Stop1 delay: 12min - 10min = 2min (within 3min budget, ok)
+ // Destination delay: 47min - 20min = 27min (within 30min budget, ok)
GraphPath[] modifiedSegments = new GraphPath[] {
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5).plusSeconds(1)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(7)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(30)),
};
- assertFalse(
- strictConstraints.satisfiesConstraints(
+ assertTrue(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
1,
- 3
+ 3,
+ stops
)
);
}
@Test
- void customMaxDelay_veryPermissive_acceptsLargeDelays() {
- var permissiveConstraints = new PassengerDelayConstraints(Duration.ofHours(1));
-
- Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(10), Duration.ofMinutes(20) };
+ void satisfiesConstraints_passengerBeforeAllStops_checksAllStops() {
+ Duration[] originalTimes = {
+ Duration.ZERO,
+ Duration.ofMinutes(10),
+ Duration.ofMinutes(20),
+ Duration.ofMinutes(30),
+ };
+ var stops = List.of(
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES)
+ );
- // Stop1 delayed by 30 minutes (well within 1 hour threshold)
+ // Passenger inserted at very beginning (pickup at 1, dropoff at 2)
+ // Stop1 delay: 13min - 10min = 3min ok
+ // Stop2 delay: 24min - 20min = 4min ok
+ // Destination delay: 36min - 30min = 6min exceeds 5min budget
GraphPath[] modifiedSegments = new GraphPath[] {
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(35)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(3)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(2)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(8)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(11)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(12)),
};
- assertTrue(
- permissiveConstraints.satisfiesConstraints(
+ assertFalse(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
1,
- 3
+ 2,
+ stops
)
);
}
@Test
- void getMaxDelay_returnsConfiguredValue() {
- assertEquals(Duration.ofMinutes(5), constraints.getMaxDelay());
+ void satisfiesConstraints_nonZeroStopDuration_countsDwellAtIntermediateStops() {
+ // Uses a non-zero stopDuration to verify dwell at intermediate stops is included in the
+ // budget check. The modified route has 2 extra dwells vs. the baseline (4 segments vs. 2),
+ // which alone accounts for 2 of the 6-minute destination delay that pushes it over budget.
+ Duration stopDuration = Duration.ofMinutes(1);
+ var stops = List.of(
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES)
+ );
- var customConstraints = new PassengerDelayConstraints(Duration.ofMinutes(10));
- assertEquals(Duration.ofMinutes(10), customConstraints.getMaxDelay());
- }
+ // Baseline: 2 segments of 10min. With 1-min dwell: cumulative = [0, 10, 21]
+ GraphPath[] baselineSegments = new GraphPath[] {
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
+ };
+ Duration[] originalTimes = calculateCumulativeDurations(baselineSegments, stopDuration);
- @Test
- void defaultMaxDelay_isFiveMinutes() {
- assertEquals(Duration.ofMinutes(5), PassengerDelayConstraints.DEFAULT_MAX_DELAY);
- }
+ // Modified (pickup=1, dropoff=3): 4 segments of 6min. With 1-min dwell: cumulative = [0, 6, 13, 20, 27]
+ // Destination delay: 27 - 21 = 6min, exceeds 5min budget.
+ GraphPath[] overBudgetSegments = new GraphPath[] {
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(6)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(6)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(6)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(6)),
+ };
- @Test
- void constructor_negativeDelay_throwsException() {
- assertThrows(IllegalArgumentException.class, () ->
- new PassengerDelayConstraints(Duration.ofMinutes(-1))
+ assertFalse(
+ PassengerDelayConstraints.satisfiesConstraints(
+ originalTimes,
+ calculateCumulativeDurations(overBudgetSegments, stopDuration),
+ 1,
+ 3,
+ stops
+ )
);
- }
-
- @Test
- void satisfiesConstraints_noDelay_accepts() {
- // Route where insertion doesn't add any delay
- Duration[] originalTimes = { Duration.ZERO, Duration.ofMinutes(10), Duration.ofMinutes(20) };
- // Modified route where stop1 arrives at exactly the same time
- // (perfect routing somehow)
- GraphPath[] modifiedSegments = new GraphPath[] {
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(4)),
+ // Shortening one segment to 5min: cumulative = [0, 6, 12, 19, 26]
+ // Destination delay: 26 - 21 = 5min, exactly at budget → accepts.
+ GraphPath[] atBudgetSegments = new GraphPath[] {
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(6)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(5)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(6)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(6)),
};
assertTrue(
- constraints.satisfiesConstraints(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(atBudgetSegments, stopDuration),
1,
- 3
+ 3,
+ stops
)
);
}
@Test
- void satisfiesConstraints_tripWithManyStops_checksAll() {
- // Original route with 5 stops
+ void satisfiesConstraints_passengerBetweenStops_checksAllStops() {
Duration[] originalTimes = {
Duration.ZERO,
Duration.ofMinutes(10),
Duration.ofMinutes(20),
Duration.ofMinutes(30),
- Duration.ofMinutes(40),
- Duration.ofMinutes(50),
- Duration.ofMinutes(60),
};
+ var stops = List.of(
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES),
+ stopWithBudget(FIVE_MINUTES)
+ );
- // Insert passenger between stop2 and stop3 (positions 3, 4)
- // All stops should have delays <= 5 minutes
- // Modified indices: 0,1,2,pickup@3,dropoff@4,3,4,5,6
- // Note: With real State objects, durations will be slightly longer due to rounding
- // (typically 1-3 seconds per path). We use slightly shorter durations to ensure
- // the cumulative delays stay within the 5-minute threshold.
+ // Passenger inserted between stops (pickup at 2, dropoff at 3)
+ // Stop1 delay: 11min - 10min = 1min ok
+ // Stop2 delay: 24min - 20min = 4min ok
+ // Destination delay: 36min - 30min = 6min exceeds 5min budget
GraphPath[] modifiedSegments = new GraphPath[] {
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(11)),
CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(3)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(2)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(8)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
- CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(10)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(3)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(7)),
+ CarpoolGraphPathBuilder.createGraphPath(Duration.ofMinutes(12)),
};
- assertTrue(
- constraints.satisfiesConstraints(
+ assertFalse(
+ PassengerDelayConstraints.satisfiesConstraints(
originalTimes,
- calculateCumulativeDurations(modifiedSegments),
+ calculateCumulativeDurations(modifiedSegments, Duration.ZERO),
+ 2,
3,
- 4
+ stops
)
);
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/CapacityFilterTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/CapacityFilterTest.java
deleted file mode 100644
index ca741e20b73..00000000000
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/CapacityFilterTest.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package org.opentripplanner.ext.carpooling.filter;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_CENTER;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_EAST;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_NORTH;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_SOUTH;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_WEST;
-import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createDestinationStop;
-import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createOriginStop;
-import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createStop;
-import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createTripWithCapacity;
-import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createTripWithStops;
-
-import java.util.List;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-class CapacityFilterTest {
-
- private CapacityFilter filter;
-
- @BeforeEach
- void setup() {
- filter = new CapacityFilter();
- }
-
- @Test
- void accepts_tripWithCapacity_returnsTrue() {
- var trip = createTripWithStops(OSLO_CENTER, List.of(), OSLO_NORTH);
-
- assertTrue(filter.accepts(trip, OSLO_EAST, OSLO_WEST));
- }
-
- @Test
- void accepts_tripAtFullCapacity_returnsTrue() {
- // CapacityFilter only checks configured capacity, not actual occupancy
- // Detailed capacity validation happens in the validator layer
- // All 4 seats taken
- var stop1 = createStop(0, 4);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1), OSLO_NORTH);
-
- // Filter accepts because trip has capacity configured (even if currently full)
- assertTrue(filter.accepts(trip, OSLO_EAST, OSLO_WEST));
- }
-
- @Test
- void accepts_tripWithOneOpenSeat_returnsTrue() {
- // 3 of 4 seats taken
- var stop1 = createStop(0, 3);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1), OSLO_NORTH);
-
- assertTrue(filter.accepts(trip, OSLO_EAST, OSLO_WEST));
- }
-
- @Test
- void accepts_zeroCapacityTrip_returnsFalse() {
- var stops = List.of(createOriginStop(OSLO_CENTER), createDestinationStop(OSLO_NORTH, 1));
- var trip = createTripWithCapacity(0, stops);
-
- assertFalse(filter.accepts(trip, OSLO_EAST, OSLO_WEST));
- }
-
- @Test
- void accepts_passengerCoordinatesIgnored() {
- // Filter only checks if ANY capacity exists, not position-specific
- var stops = List.of(createOriginStop(OSLO_CENTER), createDestinationStop(OSLO_NORTH, 1));
- var trip = createTripWithCapacity(2, stops);
-
- // Should accept regardless of passenger coordinates
- assertTrue(filter.accepts(trip, OSLO_SOUTH, OSLO_EAST));
- assertTrue(filter.accepts(trip, OSLO_NORTH, OSLO_SOUTH));
- }
-
- @Test
- void accepts_tripWithFluctuatingCapacity_checksOverallAvailability() {
- // 2 passengers
- var stop1 = createStop(0, 2);
- // Dropoff 2
- var stop2 = createStop(1, -2);
- // Pickup 1
- var stop3 = createStop(2, 1);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1, stop2, stop3), OSLO_NORTH);
-
- // At some point there's capacity (positions 0, 2+)
- assertTrue(filter.accepts(trip, OSLO_EAST, OSLO_WEST));
- }
-
- @Test
- void accepts_tripAlwaysAtCapacity_returnsTrue() {
- // CapacityFilter only checks configured capacity, not actual occupancy
- // Fill to capacity
- var stop1 = createStop(0, 4);
- // Drop 1
- var stop2 = createStop(1, -1);
- // Pick 1 (back to full)
- var stop3 = createStop(2, 1);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1, stop2, stop3), OSLO_NORTH);
-
- // Filter accepts because trip has capacity configured
- // The validator will determine if there's actual room for insertion
- assertTrue(filter.accepts(trip, OSLO_EAST, OSLO_WEST));
- }
-}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/DirectionalCompatibilityFilterTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/DirectionalCompatibilityFilterTest.java
deleted file mode 100644
index d27b492490e..00000000000
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/DirectionalCompatibilityFilterTest.java
+++ /dev/null
@@ -1,185 +0,0 @@
-package org.opentripplanner.ext.carpooling.filter;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.LAKE_EAST;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.LAKE_NORTH;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.LAKE_SOUTH;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.LAKE_WEST;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_CENTER;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_EAST;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_NORTH;
-import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_NORTHEAST;
-import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createSimpleTrip;
-import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createStopAt;
-import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createTripWithStops;
-
-import java.util.List;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.opentripplanner.street.geometry.WgsCoordinate;
-
-class DirectionalCompatibilityFilterTest {
-
- private DirectionalCompatibilityFilter filter;
-
- @BeforeEach
- void setup() {
- filter = new DirectionalCompatibilityFilter();
- }
-
- @Test
- void accepts_passengerAlignedWithTrip_returnsTrue() {
- // Trip goes north
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
-
- // Passenger also going north
- var passengerPickup = OSLO_EAST;
- // Northeast
- var passengerDropoff = new WgsCoordinate(59.9549, 10.7922);
-
- assertTrue(filter.accepts(trip, passengerPickup, passengerDropoff));
- }
-
- @Test
- void accepts_passengerOppositeDirection_returnsFalse() {
- // Trip goes north
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
-
- // Passenger going south
- var passengerPickup = OSLO_EAST;
- var passengerDropoff = OSLO_CENTER;
-
- assertFalse(filter.accepts(trip, passengerPickup, passengerDropoff));
- }
-
- @Test
- void accepts_tripAroundLake_passengerOnSegment_returnsTrue() {
- // Trip goes around a lake: North → East → South → West
- var stop1 = createStopAt(0, LAKE_EAST);
- var stop2 = createStopAt(1, LAKE_SOUTH);
- var trip = createTripWithStops(LAKE_NORTH, List.of(stop1, stop2), LAKE_WEST);
-
- // Passenger aligned with the southward segment (East → South)
- // East side
- var passengerPickup = new WgsCoordinate(59.9339, 10.7922);
- // South of east
- var passengerDropoff = new WgsCoordinate(59.9139, 10.7922);
-
- // Should accept because passenger aligns with East→South segment
- assertTrue(filter.accepts(trip, passengerPickup, passengerDropoff));
- }
-
- @Test
- void accepts_passengerFarFromRoute_butDirectionallyAligned_returnsTrue() {
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
-
- // Passenger far to the east but directionally aligned (both going north)
- // Way east
- var passengerPickup = new WgsCoordinate(59.9139, 11.0000);
- var passengerDropoff = new WgsCoordinate(59.9439, 11.0000);
-
- // Should accept - only checks direction, not distance (that's DistanceBasedFilter's job)
- assertTrue(filter.accepts(trip, passengerPickup, passengerDropoff));
- }
-
- @Test
- void accepts_passengerPartiallyAligned_withinTolerance_returnsTrue() {
- // Going north
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
-
- // Passenger going northeast (~45° off)
- // Should accept within default tolerance (60°)
- assertTrue(filter.accepts(trip, OSLO_CENTER, OSLO_NORTHEAST));
- }
-
- @Test
- void accepts_passengerPerpendicularToTrip_returnsFalse() {
- // Going north
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
-
- // Passenger going east (90° perpendicular)
- // Should reject (exceeds 60° tolerance)
- assertFalse(filter.accepts(trip, OSLO_CENTER, OSLO_EAST));
- }
-
- @Test
- void accepts_complexRoute_multipleSegments_findsCompatibleSegment() {
- // Trip with multiple segments going different directions
- // Go east first
- var stop1 = createStopAt(0, OSLO_EAST);
- // Then northeast
- var stop2 = createStopAt(1, OSLO_NORTHEAST);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1, stop2), OSLO_NORTH);
-
- // Passenger going northeast (aligns with second segment)
- var passengerPickup = new WgsCoordinate(59.9289, 10.7722);
- var passengerDropoff = new WgsCoordinate(59.9389, 10.7822);
-
- assertTrue(filter.accepts(trip, passengerPickup, passengerDropoff));
- }
-
- @Test
- void accepts_tripWithSingleStop_checksAllSegments() {
- var stop1 = createStopAt(0, OSLO_EAST);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1), OSLO_NORTH);
-
- // Passenger aligned with first segment (Center → East)
- var passengerPickup = new WgsCoordinate(59.9139, 10.7622);
- var passengerDropoff = new WgsCoordinate(59.9139, 10.7822);
-
- assertTrue(filter.accepts(trip, passengerPickup, passengerDropoff));
- }
-
- @Test
- void accepts_passengerWithinCorridorButWrongDirection_returnsFalse() {
- // Going north
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
-
- // Passenger nearby but going opposite direction
- // North
- var passengerPickup = new WgsCoordinate(59.9239, 10.7522);
- // South (backtracking)
- var passengerDropoff = new WgsCoordinate(59.9139, 10.7522);
-
- assertFalse(filter.accepts(trip, passengerPickup, passengerDropoff));
- }
-
- @Test
- void customBearingTolerance_acceptsWithinCustomTolerance() {
- // Custom filter with 90° tolerance (very permissive)
- var customFilter = new DirectionalCompatibilityFilter(90.0);
-
- // Going north
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
-
- // Passenger going east (90° perpendicular)
- // Should accept with 90° tolerance (default 60° would reject)
- assertTrue(customFilter.accepts(trip, OSLO_CENTER, OSLO_EAST));
- }
-
- @Test
- void customBearingTolerance_rejectsOutsideCustomTolerance() {
- // Custom filter with 30° tolerance (strict)
- var customFilter = new DirectionalCompatibilityFilter(30.0);
-
- // Going north
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
-
- // Passenger going northeast (~45° off)
- // Should reject with 30° tolerance (default 60° would accept)
- assertFalse(customFilter.accepts(trip, OSLO_CENTER, OSLO_NORTHEAST));
- }
-
- @Test
- void getBearingToleranceDegrees_returnsConfiguredValue() {
- var customFilter = new DirectionalCompatibilityFilter(45.0);
- assertEquals(45.0, customFilter.getBearingToleranceDegrees());
- }
-
- @Test
- void defaultBearingTolerance_is60Degrees() {
- assertEquals(60.0, filter.getBearingToleranceDegrees());
- }
-}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/DistanceBasedFilterTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/DistanceBasedFilterTest.java
index c2f8e43b4f8..ddcaf570ae4 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/DistanceBasedFilterTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/DistanceBasedFilterTest.java
@@ -214,8 +214,8 @@ void accepts_horizontalRoute_passengerAlongRoute_returnsTrue() {
@Test
void accepts_tripWithMultipleStops_passengerNearAnySegment() {
// Trip with multiple stops - filter checks ALL segments
- var stop1 = createStopAt(0, LAKE_EAST);
- var stop2 = createStopAt(1, LAKE_SOUTH);
+ var stop1 = createStopAt(LAKE_EAST);
+ var stop2 = createStopAt(LAKE_SOUTH);
var trip = createTripWithStops(LAKE_NORTH, java.util.List.of(stop1, stop2), LAKE_WEST);
// Passenger journey near the LAKE_SOUTH to LAKE_WEST segment
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/FilterChainTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/FilterChainTest.java
index b36b690fe46..bed4e6d9cc9 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/FilterChainTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/filter/FilterChainTest.java
@@ -6,10 +6,7 @@
import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_EAST;
import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_NORTH;
import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_WEST;
-import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createDestinationStop;
-import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createOriginStop;
import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createSimpleTrip;
-import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createTripWithCapacity;
import java.util.List;
import org.junit.jupiter.api.Test;
@@ -73,30 +70,6 @@ void accepts_firstFilterRejects_doesNotCallOthers() {
assertFalse(filter2Called[0], "Filter2 should not have been called due to short-circuit");
}
- @Test
- void standard_includesAllStandardFilters() {
- var chain = FilterChain.standard();
-
- // Should contain CapacityFilter and DirectionalCompatibilityFilter
- // Verify by testing behavior with a trip that has no capacity
- var stops = List.of(createOriginStop(OSLO_CENTER), createDestinationStop(OSLO_NORTH, 1));
- var emptyTrip = createTripWithCapacity(0, stops);
-
- // Should reject due to capacity filter
- assertFalse(chain.accepts(emptyTrip, OSLO_EAST, OSLO_WEST));
- }
-
- @Test
- void standard_checksDirectionalCompatibility() {
- var chain = FilterChain.standard();
-
- // Trip going north, passenger going south
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
-
- // Should reject due to directional filter
- assertFalse(chain.accepts(trip, OSLO_EAST, OSLO_CENTER));
- }
-
@Test
void emptyChain_acceptsAll() {
var chain = new FilterChain(List.of());
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/model/CarpoolStopBuilderTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/model/CarpoolStopBuilderTest.java
index a7c3521331b..a468587379c 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/model/CarpoolStopBuilderTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/model/CarpoolStopBuilderTest.java
@@ -3,13 +3,10 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_CENTER;
import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_NORTH;
-import static org.opentripplanner.ext.carpooling.model.CarpoolStopType.DROP_OFF_ONLY;
import java.time.ZoneId;
import java.time.ZonedDateTime;
-import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
-import org.opentripplanner.core.model.i18n.NonLocalizedString;
import org.opentripplanner.core.model.id.FeedScopedId;
public class CarpoolStopBuilderTest {
@@ -57,70 +54,44 @@ public class CarpoolStopBuilderTest {
@Test
void buildFromValues_usingWith_buildToCorrectValues() {
- var builder = new CarpoolStopBuilder(new FeedScopedId("feed", "id"), () -> -1);
+ var builder = new CarpoolStopBuilder(new FeedScopedId("feed", "id"));
builder
- .withSequenceNumber(1)
- .withPassengerDelta(2)
+ .withOnboardCount(2)
.withCoordinate(OSLO_NORTH)
- .withCarpoolStopType(DROP_OFF_ONLY)
.withAimedArrivalTime(AIMED_ARRIVAL_TIME)
.withExpectedArrivalTime(EXPECTED_ARRIVAL_TIME)
.withAimedDepartureTime(AIMED_DEPARTURE_TIME)
- .withExpectedDepartureTime(EXPECTED_DEPARTURE_TIME)
- .withName(new NonLocalizedString("name"))
- .withDescription(new NonLocalizedString("description"))
- .withUrl(new NonLocalizedString("http://url.value"));
+ .withExpectedDepartureTime(EXPECTED_DEPARTURE_TIME);
var stop = builder.buildFromValues();
- assertEquals(-1, stop.getIndex());
- assertEquals(1, stop.getSequenceNumber());
- assertEquals(2, stop.getPassengerDelta());
+ assertEquals(2, stop.getOnboardCount());
assertEquals(OSLO_NORTH, stop.getCoordinate());
- assertEquals(DROP_OFF_ONLY, stop.getCarpoolStopType());
assertEquals(AIMED_ARRIVAL_TIME, stop.getAimedArrivalTime());
assertEquals(EXPECTED_ARRIVAL_TIME, stop.getExpectedArrivalTime());
assertEquals(AIMED_DEPARTURE_TIME, stop.getAimedDepartureTime());
assertEquals(EXPECTED_DEPARTURE_TIME, stop.getExpectedDepartureTime());
- assertEquals("name", stop.getName().toString());
- assertEquals("description", stop.getDescription().toString());
- assertEquals("http://url.value", stop.getUrl().toString());
}
@Test
void buildFromValues_usingCarPoolStop_buildsCorrectValues() {
- var i = new AtomicInteger(0);
- var originalBuilder = new CarpoolStopBuilder(
- new FeedScopedId("feed", "id"),
- i::incrementAndGet
- );
+ var originalBuilder = new CarpoolStopBuilder(new FeedScopedId("feed", "id"));
originalBuilder
- .withSequenceNumber(2)
- .withPassengerDelta(3)
+ .withOnboardCount(3)
.withCoordinate(OSLO_CENTER)
- .withCarpoolStopType(DROP_OFF_ONLY)
.withAimedArrivalTime(AIMED_ARRIVAL_TIME)
.withExpectedArrivalTime(EXPECTED_ARRIVAL_TIME)
.withAimedDepartureTime(AIMED_DEPARTURE_TIME)
- .withExpectedDepartureTime(EXPECTED_DEPARTURE_TIME)
- .withName(new NonLocalizedString("name value"))
- .withDescription(new NonLocalizedString("description value"))
- .withUrl(new NonLocalizedString("http://url.value"));
+ .withExpectedDepartureTime(EXPECTED_DEPARTURE_TIME);
var original = originalBuilder.buildFromValues();
var copyBuilder = new CarpoolStopBuilder(original);
var copy = copyBuilder.buildFromValues();
- assertEquals(1, copy.getIndex());
- assertEquals(original.getSequenceNumber(), copy.getSequenceNumber());
- assertEquals(original.getPassengerDelta(), copy.getPassengerDelta());
+ assertEquals(original.getOnboardCount(), copy.getOnboardCount());
assertEquals(original.getCoordinate(), copy.getCoordinate());
- assertEquals(original.getCarpoolStopType(), copy.getCarpoolStopType());
assertEquals(original.getAimedArrivalTime(), copy.getAimedArrivalTime());
assertEquals(original.getExpectedArrivalTime(), copy.getExpectedArrivalTime());
assertEquals(original.getAimedDepartureTime(), copy.getAimedDepartureTime());
assertEquals(original.getExpectedDepartureTime(), copy.getExpectedDepartureTime());
- assertEquals(original.getName(), copy.getName());
- assertEquals(original.getDescription(), copy.getDescription());
- assertEquals(original.getUrl(), copy.getUrl());
}
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/model/CarpoolTripBuilderTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/model/CarpoolTripBuilderTest.java
index 5a0fc8f7207..1ead1a0f674 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/model/CarpoolTripBuilderTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/model/CarpoolTripBuilderTest.java
@@ -6,9 +6,7 @@
import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createSimpleTrip;
import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createStopAt;
-import java.time.Duration;
import java.time.ZonedDateTime;
-import java.time.temporal.ChronoUnit;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.opentripplanner.core.model.id.FeedScopedId;
@@ -19,21 +17,19 @@ public class CarpoolTripBuilderTest {
void buildFromValues_fromId_buildToCorrectValues() {
var startTime = ZonedDateTime.now();
var endTime = ZonedDateTime.now().plusMinutes(45);
- var stop = createStopAt(1, OSLO_EAST);
+ var stop = createStopAt(OSLO_EAST);
var builder = new CarpoolTripBuilder(new FeedScopedId("feed", "id"));
var trip = builder
- .withAvailableSeats(2)
+ .withTotalCapacity(2)
.withProvider("UNIT")
- .withDeviationBudget(Duration.of(8, ChronoUnit.MINUTES))
.withStartTime(startTime)
.withEndTime(endTime)
.withStops(List.of(stop))
.buildFromValues();
- assertEquals(2, trip.availableSeats());
+ assertEquals(2, trip.totalCapacity());
assertEquals("UNIT", trip.provider());
- assertEquals(Duration.of(8, ChronoUnit.MINUTES), trip.deviationBudget());
assertEquals(startTime, trip.startTime());
assertEquals(endTime, trip.endTime());
assertEquals(stop, trip.stops().getFirst());
@@ -46,9 +42,8 @@ void buildFromValues_fromOriginal_buildToCorrectValues() {
var builder = new CarpoolTripBuilder(original);
var trip = builder.buildFromValues();
- assertEquals(original.availableSeats(), trip.availableSeats());
+ assertEquals(original.totalCapacity(), trip.totalCapacity());
assertEquals(original.provider(), trip.provider());
- assertEquals(original.deviationBudget(), trip.deviationBudget());
assertEquals(original.startTime(), trip.startTime());
assertEquals(original.endTime(), trip.endTime());
assertEquals(original.stops().getFirst(), trip.stops().getFirst());
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/model/CarpoolTripCapacityTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/model/CarpoolTripCapacityTest.java
index 2f7c5c43469..aadd58b84ab 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/model/CarpoolTripCapacityTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/model/CarpoolTripCapacityTest.java
@@ -15,181 +15,152 @@
/**
* Tests for capacity checking methods on {@link CarpoolTrip}.
+ *
+ * All trips created via {@code createTripWithStops} have totalCapacity=5.
+ * The method wraps intermediate stops with an Origin (onboard=1) at the front
+ * and a Destination (onboard=1) at the end.
+ *
+ * {@code pickupPosition} and {@code dropoffPosition} in {@code hasCapacityForInsertion}
+ * are 0-based indices of the passenger's stops in the modified route (the route after the
+ * passenger's pickup and dropoff have been inserted into the carpool trip).
*/
class CarpoolTripCapacityTest {
+ // -- getPassengerCountAtDepartureOfStop tests --
+
@Test
- void getPassengerCountAtPosition_noStops_allZeros() {
+ void getPassengerCountAtDepartureOfStop_driverOnly() {
var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
- // Boarding
- assertEquals(0, trip.getPassengerCountAtPosition(0));
- // Beyond stops
- assertEquals(0, trip.getPassengerCountAtPosition(1));
+ assertEquals(1, trip.getPassengerCountAtDepartureOfStop(0));
+ assertEquals(1, trip.getPassengerCountAtDepartureOfStop(1));
}
@Test
- void getPassengerCountAtPosition_onePickupStop_incrementsAtStop() {
- // Pickup 1 passenger, then drop off 1 passenger
- var stop1 = createStop(0, 1);
- var stop2 = createStop(1, -1);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1, stop2), OSLO_NORTH);
-
- // Position 0: Before origin stop
- assertEquals(0, trip.getPassengerCountAtPosition(0));
- // Position 1: After origin stop (passengerDelta=0)
- assertEquals(0, trip.getPassengerCountAtPosition(1));
- // Position 2: After pickup stop (passengerDelta=1)
- assertEquals(1, trip.getPassengerCountAtPosition(2));
- // Position 3: After dropoff stop (passengerDelta=-1)
- assertEquals(0, trip.getPassengerCountAtPosition(3));
- // Position 4: After destination stop (passengerDelta=0)
- assertEquals(0, trip.getPassengerCountAtPosition(4));
+ void getPassengerCountAtDepartureOfStop_withIntermediateStops() {
+ // Stops: [Origin(1), A(2), B(1), Destination(1)]
+ var trip = createTripWithStops(OSLO_CENTER, List.of(createStop(2), createStop(1)), OSLO_NORTH);
+
+ assertEquals(1, trip.getPassengerCountAtDepartureOfStop(0));
+ assertEquals(2, trip.getPassengerCountAtDepartureOfStop(1));
+ assertEquals(1, trip.getPassengerCountAtDepartureOfStop(2));
+ assertEquals(1, trip.getPassengerCountAtDepartureOfStop(3));
}
@Test
- void getPassengerCountAtPosition_pickupAndDropoff_incrementsThenDecrements() {
- // Pickup 2 passengers
- var stop1 = createStop(0, 2);
- // Dropoff 1 passenger
- var stop2 = createStop(1, -1);
- // Dropoff remaining passenger
- var stop3 = createStop(2, -1);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1, stop2, stop3), OSLO_NORTH);
-
- // Position 0: Before origin stop
- assertEquals(0, trip.getPassengerCountAtPosition(0));
- // Position 1: After origin stop (passengerDelta=0)
- assertEquals(0, trip.getPassengerCountAtPosition(1));
- // Position 2: After first intermediate stop (passengerDelta=2)
- assertEquals(2, trip.getPassengerCountAtPosition(2));
- // Position 3: After second intermediate stop (passengerDelta=-1)
- assertEquals(1, trip.getPassengerCountAtPosition(3));
- // Position 4: After third intermediate stop (passengerDelta=-1)
- assertEquals(0, trip.getPassengerCountAtPosition(4));
- // Position 5: After destination stop (passengerDelta=0)
- assertEquals(0, trip.getPassengerCountAtPosition(5));
+ void getPassengerCountAtDepartureOfStop_negativeIndex_throwsException() {
+ var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
+ assertThrows(IllegalArgumentException.class, () -> trip.getPassengerCountAtDepartureOfStop(-1));
}
@Test
- void getPassengerCountAtPosition_multipleStops_cumulativeCount() {
- var stop1 = createStop(0, 1);
- var stop2 = createStop(1, 2);
- var stop3 = createStop(2, -1);
- var stop4 = createStop(3, 1);
- var stop5 = createStop(4, -3);
- var trip = createTripWithStops(
- OSLO_CENTER,
- List.of(stop1, stop2, stop3, stop4, stop5),
- OSLO_NORTH
- );
-
- // Position 0: Before origin
- assertEquals(0, trip.getPassengerCountAtPosition(0));
- // Position 1: After origin (passengerDelta=0)
- assertEquals(0, trip.getPassengerCountAtPosition(1));
- // Position 2: After stop1 (0 + 1)
- assertEquals(1, trip.getPassengerCountAtPosition(2));
- // Position 3: After stop2 (1 + 2)
- assertEquals(3, trip.getPassengerCountAtPosition(3));
- // Position 4: After stop3 (3 - 1)
- assertEquals(2, trip.getPassengerCountAtPosition(4));
- // Position 5: After stop4 (2 + 1)
- assertEquals(3, trip.getPassengerCountAtPosition(5));
- // Position 6: After stop5 (3 - 3)
- assertEquals(0, trip.getPassengerCountAtPosition(6));
- // Position 7: After destination (passengerDelta=0)
- assertEquals(0, trip.getPassengerCountAtPosition(7));
+ void getPassengerCountAtDepartureOfStop_indexTooLarge_throwsException() {
+ var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
+ // Trip has 2 stops (Origin, Destination), valid indices are 0 and 1
+ assertThrows(IllegalArgumentException.class, () -> trip.getPassengerCountAtDepartureOfStop(2));
}
@Test
- void getPassengerCountAtPosition_negativePosition_throwsException() {
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
+ void adjacentPositions_onlyChecksStopBeforePickup() {
+ // Original stops: [Origin(1), A(5), Destination(1)] totalCapacity=5
+ var trip = createTripWithStops(OSLO_CENTER, List.of(createStop(5)), OSLO_NORTH);
- assertThrows(IllegalArgumentException.class, () -> trip.getPassengerCountAtPosition(-1));
+ // Modified route: [Origin, Pickup, Dropoff, A, Destination]
+ // 0 1 2 3 4
+ // Checked original stops: Origin (index 0, onboard=1). A is NOT checked.
+ assertTrue(trip.hasCapacityForInsertion(1, 2, 4));
}
@Test
- void getPassengerCountAtPosition_positionTooLarge_throwsException() {
- var stop1 = createStop(0, 1);
- var stop2 = createStop(1, 1);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1, stop2), OSLO_NORTH);
-
- // Trip has: origin (0), stop1 (1), stop2 (2), destination (3) = 4 stops total
- // Valid positions are 0 to 4 (0 to stops.size())
- // Position 5 should throw
- assertThrows(IllegalArgumentException.class, () -> trip.getPassengerCountAtPosition(5));
- // Position 999 should also throw
- assertThrows(IllegalArgumentException.class, () -> trip.getPassengerCountAtPosition(999));
+ void adjacentPositions_fullAtStopBeforePickup() {
+ // Original stops: [Origin(1), A(5), Destination(1)] totalCapacity=5
+ var trip = createTripWithStops(OSLO_CENTER, List.of(createStop(5)), OSLO_NORTH);
+
+ // Modified route: [Origin, A, Pickup, Dropoff, Destination]
+ // 0 1 2 3 4
+ // Checked original stops: A (index 1, onboard=5). No room.
+ assertFalse(trip.hasCapacityForInsertion(2, 3, 1));
}
@Test
- void hasCapacityForInsertion_noPassengers_hasCapacity() {
- var trip = createTripWithStops(OSLO_CENTER, List.of(), OSLO_NORTH);
+ void widerGap_checksAllOriginalStopsBetweenPickupAndDropoff() {
+ // Original stops: [Origin(1), A(2), B(4), C(1), Destination(1)] totalCapacity=5
+ var trip = createTripWithStops(
+ OSLO_CENTER,
+ List.of(createStop(2), createStop(4), createStop(1)),
+ OSLO_NORTH
+ );
- assertTrue(trip.hasCapacityForInsertion(1, 2, 1));
- // Can fit all 4 seats
- assertTrue(trip.hasCapacityForInsertion(1, 2, 4));
+ // Modified route: [Origin, Pickup, A, B, Dropoff, C, Destination]
+ // 0 1 2 3 4 5 6
+ // Checked original stops: Origin(1), A(2), B(4). Max is 4, room for 1.
+ assertTrue(trip.hasCapacityForInsertion(1, 4, 1));
+ assertFalse(trip.hasCapacityForInsertion(1, 4, 2));
}
@Test
- void hasCapacityForInsertion_fullCapacity_noCapacity() {
- // Fill all 4 seats
- var stop1 = createStop(0, 4);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1), OSLO_NORTH);
-
- // No room for additional passenger after stop 1
- assertFalse(trip.hasCapacityForInsertion(2, 3, 1));
+ void stopAfterDropoff_isNotChecked() {
+ // Original stops: [Origin(1), A(1), B(5), Destination(1)] totalCapacity=5
+ // B is full, but the passenger is dropped off before B.
+ var trip = createTripWithStops(OSLO_CENTER, List.of(createStop(1), createStop(5)), OSLO_NORTH);
+
+ // Modified route: [Origin, Pickup, A, Dropoff, B, Destination]
+ // 0 1 2 3 4 5
+ // Checked original stops: Origin(1), A(1). B is after the dropoff.
+ assertTrue(trip.hasCapacityForInsertion(1, 3, 3));
}
@Test
- void hasCapacityForInsertion_partialCapacity_hasCapacityForOne() {
- // 3 of 4 seats taken
- var stop1 = createStop(0, 3);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1), OSLO_NORTH);
+ void pickupNearEnd_limitedByStopBeforePickup() {
+ // Original stops: [Origin(1), A(4), Destination(1)] totalCapacity=5
+ var trip = createTripWithStops(OSLO_CENTER, List.of(createStop(4)), OSLO_NORTH);
- // Room for 1
+ // Modified route: [Origin, A, Pickup, Dropoff, Destination]
+ // 0 1 2 3 4
+ // Checked original stops: A (index 1, onboard=4). Room for 1.
assertTrue(trip.hasCapacityForInsertion(2, 3, 1));
- // No room for 2
assertFalse(trip.hasCapacityForInsertion(2, 3, 2));
}
@Test
- void hasCapacityForInsertion_acrossMultiplePositions_checksAll() {
- var stop1 = createStop(0, 2);
- // Total 3 passengers at position 3
- var stop2 = createStop(1, 1);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1, stop2), OSLO_NORTH);
-
- // Trip positions: 0 (before origin), 1 (after origin=0), 2 (after stop1=2), 3 (after stop2=3), 4 (after dest=0)
- // Range 2-4 includes position 3 with 3 passengers, so only 1 seat available
- assertTrue(trip.hasCapacityForInsertion(2, 4, 1));
- assertFalse(trip.hasCapacityForInsertion(2, 4, 2));
+ void fullRangeInsertion_checksAllOriginalStops() {
+ // Original stops: [Origin(2), A(2), Destination(2)] totalCapacity=5
+ var trip = createTripWithStops(OSLO_CENTER, List.of(createStop(2)), OSLO_NORTH);
+
+ // Modified route: [Origin, Pickup, A, Dropoff, Destination]
+ // 0 1 2 3 4
+ // Checked original stops: Origin(2), A(2). Both onboard=2, room for 3.
+ assertTrue(trip.hasCapacityForInsertion(1, 3, 3));
+ assertFalse(trip.hasCapacityForInsertion(1, 3, 4));
}
@Test
- void hasCapacityForInsertion_rangeBeforeStop_usesInitialCapacity() {
- // Fill capacity at position 1
- var stop1 = createStop(0, 4);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1), OSLO_NORTH);
-
- // Pickup at position 1, dropoff at position 1 - only checks capacity at boarding (position 0)
- // At boarding there are no passengers yet, so we have full capacity
- assertTrue(trip.hasCapacityForInsertion(1, 1, 4));
+ void fullStopBeforePickup_notInCheckedRange() {
+ // Original stops: [Origin(1), A(5), Destination(1)] totalCapacity=5
+ // A is full, but pickup and dropoff are inserted after A.
+ var trip = createTripWithStops(OSLO_CENTER, List.of(createStop(5)), OSLO_NORTH);
+
+ // Modified route: [Origin, A, Destination, Pickup, Dropoff]
+ // 0 1 2 3 4
+ // firstOriginalStop = 3 - 1 = 2, lastOriginalStop = 4 - 2 = 2
+ // Checked original stop: Destination (index 2, onboard=1). A is outside the range.
+ assertTrue(trip.hasCapacityForInsertion(3, 4, 4));
+ assertFalse(trip.hasCapacityForInsertion(3, 4, 5));
}
@Test
- void hasCapacityForInsertion_capacityFreesUpInRange_checksMaxInRange() {
- // 3 passengers
- var stop1 = createStop(0, 3);
- // 2 dropoff, leaving 1
- var stop2 = createStop(1, -2);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1, stop2), OSLO_NORTH);
-
- // Range includes both positions - max passengers is 3 (at position 1)
- // 4 total - 3 max = 1 available
- assertTrue(trip.hasCapacityForInsertion(1, 3, 1));
- // Not enough
- assertFalse(trip.hasCapacityForInsertion(1, 3, 2));
+ void bottleneckInMiddle_limitsCapacity() {
+ // Original stops: [Origin(1), A(1), B(4), C(1), D(1), Destination(1)] totalCapacity=5
+ var trip = createTripWithStops(
+ OSLO_CENTER,
+ List.of(createStop(1), createStop(4), createStop(1), createStop(1)),
+ OSLO_NORTH
+ );
+
+ // Modified route: [Origin, Pickup, A, B, C, Dropoff, D, Destination]
+ // 0 1 2 3 4 5 6 7
+ // Checked original stops: Origin(1), A(1), B(4), C(1). Max is 4 at B, room for 1.
+ assertTrue(trip.hasCapacityForInsertion(1, 5, 1));
+ assertFalse(trip.hasCapacityForInsertion(1, 5, 2));
}
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/routing/InsertionCandidateTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/routing/InsertionCandidateTest.java
index e4770c3b231..d5d3ada4d17 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/routing/InsertionCandidateTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/routing/InsertionCandidateTest.java
@@ -1,114 +1,42 @@
package org.opentripplanner.ext.carpooling.routing;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.opentripplanner.ext.carpooling.CarpoolGraphPathBuilder.createGraphPath;
import static org.opentripplanner.ext.carpooling.CarpoolGraphPathBuilder.createGraphPaths;
import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_CENTER;
import static org.opentripplanner.ext.carpooling.CarpoolTestCoordinates.OSLO_NORTH;
import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createSimpleTrip;
-import static org.opentripplanner.ext.carpooling.CarpoolTripTestData.createTripWithDeviationBudget;
import java.time.Duration;
+import java.util.List;
import org.junit.jupiter.api.Test;
+import org.opentripplanner.ext.carpooling.util.GraphPathUtils;
class InsertionCandidateTest {
- @Test
- void additionalDuration_calculatesCorrectly() {
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
- // 3 segments
- var segments = createGraphPaths(3);
-
- var candidate = new InsertionCandidate(
- trip,
- 1,
- 2,
- segments,
- Duration.ofMinutes(10),
- Duration.ofMinutes(15),
- null
- );
-
- assertEquals(Duration.ofMinutes(5), candidate.additionalDuration());
- }
+ private static final Duration STOP_DURATION = Duration.ofMinutes(2);
@Test
- void additionalDuration_zeroAdditional_returnsZero() {
+ void totalTripDuration_calculatesFromSegments() {
+ // Simple trip origin → destination, with passenger pickup and dropoff inserted:
+ // origin → pickup (5 min) → dropoff (10 min) → destination (8 min)
var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
- var segments = createGraphPaths(2);
+ var originToPickup = createGraphPath(Duration.ofMinutes(5));
+ var pickupToDropoff = createGraphPath(Duration.ofMinutes(10));
+ var dropoffToDestination = createGraphPath(Duration.ofMinutes(8));
var candidate = new InsertionCandidate(
trip,
1,
2,
- segments,
- Duration.ofMinutes(10),
- // Same as baseline
- Duration.ofMinutes(10),
+ List.of(originToPickup, pickupToDropoff, dropoffToDestination),
+ STOP_DURATION,
null
);
- assertEquals(Duration.ZERO, candidate.additionalDuration());
- }
-
- @Test
- void isWithinDeviationBudget_withinBudget_returnsTrue() {
- var trip = createTripWithDeviationBudget(Duration.ofMinutes(10), OSLO_CENTER, OSLO_NORTH);
- var segments = createGraphPaths(2);
-
- var candidate = new InsertionCandidate(
- trip,
- 1,
- 2,
- segments,
- // baseline
- Duration.ofMinutes(10),
- // total (8 min additional, within 10 min budget)
- Duration.ofMinutes(18),
- null
- );
-
- assertTrue(candidate.isWithinDeviationBudget());
- }
-
- @Test
- void isWithinDeviationBudget_exceedsBudget_returnsFalse() {
- var trip = createTripWithDeviationBudget(Duration.ofMinutes(5), OSLO_CENTER, OSLO_NORTH);
- var segments = createGraphPaths(2);
-
- var candidate = new InsertionCandidate(
- trip,
- 1,
- 2,
- segments,
- // baseline
- Duration.ofMinutes(10),
- // total (10 min additional, exceeds 5 min budget)
- Duration.ofMinutes(20),
- null
- );
-
- assertFalse(candidate.isWithinDeviationBudget());
- }
-
- @Test
- void isWithinDeviationBudget_exactlyAtBudget_returnsTrue() {
- var trip = createTripWithDeviationBudget(Duration.ofMinutes(5), OSLO_CENTER, OSLO_NORTH);
- var segments = createGraphPaths(2);
-
- var candidate = new InsertionCandidate(
- trip,
- 1,
- 2,
- segments,
- Duration.ofMinutes(10),
- // Exactly 5 min additional
- Duration.ofMinutes(15),
- null
- );
-
- assertTrue(candidate.isWithinDeviationBudget());
+ // 5 + 2 (stop) + 10 + 2 (stop) + 8 = 27 minutes
+ assertEquals(Duration.ofMinutes(27), candidate.totalTripDuration());
}
@Test
@@ -116,18 +44,9 @@ void getPickupSegments_returnsCorrectRange() {
var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
var segments = createGraphPaths(5);
- var candidate = new InsertionCandidate(
- trip,
- 2,
- 4,
- segments,
- Duration.ofMinutes(10),
- Duration.ofMinutes(15),
- null
- );
+ var candidate = new InsertionCandidate(trip, 2, 4, segments, STOP_DURATION, null);
var pickupSegments = candidate.getPickupSegments();
- // Segments 0-1 (before position 2)
assertEquals(2, pickupSegments.size());
assertEquals(segments.subList(0, 2), pickupSegments);
}
@@ -137,15 +56,7 @@ void getPickupSegments_positionZero_returnsEmpty() {
var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
var segments = createGraphPaths(3);
- var candidate = new InsertionCandidate(
- trip,
- 0,
- 2,
- segments,
- Duration.ofMinutes(10),
- Duration.ofMinutes(15),
- null
- );
+ var candidate = new InsertionCandidate(trip, 0, 2, segments, STOP_DURATION, null);
var pickupSegments = candidate.getPickupSegments();
assertTrue(pickupSegments.isEmpty());
@@ -156,18 +67,9 @@ void getSharedSegments_returnsCorrectRange() {
var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
var segments = createGraphPaths(5);
- var candidate = new InsertionCandidate(
- trip,
- 1,
- 3,
- segments,
- Duration.ofMinutes(10),
- Duration.ofMinutes(15),
- null
- );
+ var candidate = new InsertionCandidate(trip, 1, 3, segments, STOP_DURATION, null);
var sharedSegments = candidate.getSharedSegments();
- // Segments 1-2 (positions 1 to 3)
assertEquals(2, sharedSegments.size());
assertEquals(segments.subList(1, 3), sharedSegments);
}
@@ -177,15 +79,7 @@ void getSharedSegments_adjacentPositions_returnsSingleSegment() {
var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
var segments = createGraphPaths(3);
- var candidate = new InsertionCandidate(
- trip,
- 1,
- 2,
- segments,
- Duration.ofMinutes(10),
- Duration.ofMinutes(15),
- null
- );
+ var candidate = new InsertionCandidate(trip, 1, 2, segments, STOP_DURATION, null);
var sharedSegments = candidate.getSharedSegments();
assertEquals(1, sharedSegments.size());
@@ -196,18 +90,9 @@ void getDropoffSegments_returnsCorrectRange() {
var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
var segments = createGraphPaths(5);
- var candidate = new InsertionCandidate(
- trip,
- 1,
- 3,
- segments,
- Duration.ofMinutes(10),
- Duration.ofMinutes(15),
- null
- );
+ var candidate = new InsertionCandidate(trip, 1, 3, segments, STOP_DURATION, null);
var dropoffSegments = candidate.getDropoffSegments();
- // Segments 3-4 (after position 3)
assertEquals(2, dropoffSegments.size());
assertEquals(segments.subList(3, 5), dropoffSegments);
}
@@ -217,15 +102,7 @@ void getDropoffSegments_atEnd_returnsEmpty() {
var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
var segments = createGraphPaths(3);
- var candidate = new InsertionCandidate(
- trip,
- 1,
- 3,
- segments,
- Duration.ofMinutes(10),
- Duration.ofMinutes(15),
- null
- );
+ var candidate = new InsertionCandidate(trip, 1, 3, segments, STOP_DURATION, null);
var dropoffSegments = candidate.getDropoffSegments();
assertTrue(dropoffSegments.isEmpty());
@@ -236,21 +113,128 @@ void toString_includesKeyInformation() {
var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
var segments = createGraphPaths(3);
+ var candidate = new InsertionCandidate(trip, 1, 2, segments, STOP_DURATION, null);
+
+ var str = candidate.toString();
+ assertTrue(str.contains("pickup@1"));
+ assertTrue(str.contains("dropoff@2"));
+ assertTrue(str.contains("duration="));
+ assertTrue(str.contains("segments=3"));
+ }
+
+ /**
+ * No pickup segments → durationUntilPickup is zero and no boarding dwell is added to the ride.
+ * Single shared segment → passengerRideDuration is just the segment duration.
+ */
+ @Test
+ void durations_noPickupSegments_singleSharedSegment() {
+ var stopDuration = Duration.ofMinutes(2);
+ var sharedPath = createGraphPath(Duration.ofMinutes(10));
+ var sharedDuration = GraphPathUtils.calculateDuration(sharedPath);
+
+ var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
+ var candidate = new InsertionCandidate(trip, 0, 1, List.of(sharedPath), stopDuration, null);
+
+ assertEquals(Duration.ofMinutes(10), sharedDuration);
+ assertEquals(Duration.ZERO, candidate.getDurationUntilPickupArrival());
+ assertEquals(sharedDuration, candidate.getPassengerRideDuration());
+ }
+
+ /**
+ * Single pickup segment → durationUntilPickup = segment duration (boarding excluded).
+ * Single shared segment → passengerRideDuration = boarding time + segment duration.
+ */
+ @Test
+ void durations_onePickupSegment_singleSharedSegment() {
+ var stopDuration = Duration.ofMinutes(3);
+ var pickupPath = createGraphPath(Duration.ofMinutes(8));
+ var sharedPath = createGraphPath(Duration.ofMinutes(15));
+
+ var pickupDuration = GraphPathUtils.calculateDuration(pickupPath);
+ var sharedDuration = GraphPathUtils.calculateDuration(sharedPath);
+
+ var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
var candidate = new InsertionCandidate(
trip,
1,
2,
- segments,
- Duration.ofMinutes(10),
- Duration.ofMinutes(15),
+ List.of(pickupPath, sharedPath),
+ stopDuration,
null
);
- var str = candidate.toString();
- assertTrue(str.contains("pickup@1"));
- assertTrue(str.contains("dropoff@2"));
- // 5 min = 300s additional
- assertTrue(str.contains("300s"));
- assertTrue(str.contains("segments=3"));
+ assertEquals(pickupDuration, candidate.getDurationUntilPickupArrival());
+ assertEquals(stopDuration.plus(sharedDuration), candidate.getPassengerRideDuration());
+ }
+
+ /**
+ * Two pickup segments → travel + 1 intermediate stop, no boarding dwell.
+ * Two shared segments → boarding dwell + travel + 1 intermediate stop.
+ */
+ @Test
+ void durations_multiplePickupAndSharedSegments() {
+ var stopDuration = Duration.ofMinutes(2);
+ var pickup0 = createGraphPath(Duration.ofMinutes(5));
+ var pickup1 = createGraphPath(Duration.ofMinutes(7));
+ var shared0 = createGraphPath(Duration.ofMinutes(10));
+ var shared1 = createGraphPath(Duration.ofMinutes(12));
+
+ var pickup0Duration = GraphPathUtils.calculateDuration(pickup0);
+ var pickup1Duration = GraphPathUtils.calculateDuration(pickup1);
+ var shared0Duration = GraphPathUtils.calculateDuration(shared0);
+ var shared1Duration = GraphPathUtils.calculateDuration(shared1);
+
+ var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
+ var candidate = new InsertionCandidate(
+ trip,
+ 2,
+ 4,
+ List.of(pickup0, pickup1, shared0, shared1),
+ stopDuration,
+ null
+ );
+
+ // 2 pickup segments: travel + 1 intermediate stop (boarding now belongs to the ride)
+ var expectedPickup = pickup0Duration.plus(stopDuration).plus(pickup1Duration);
+ assertEquals(expectedPickup, candidate.getDurationUntilPickupArrival());
+
+ // 2 shared segments: boarding dwell + travel + 1 intermediate stop delay
+ var expectedRide = stopDuration.plus(shared0Duration).plus(stopDuration).plus(shared1Duration);
+ assertEquals(expectedRide, candidate.getPassengerRideDuration());
+ }
+
+ /**
+ * Larger stop duration scales the durations proportionally.
+ */
+ @Test
+ void durations_scaleWithStopDuration() {
+ var shared0 = createGraphPath(Duration.ofMinutes(10));
+ var shared1 = createGraphPath(Duration.ofMinutes(10));
+
+ var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
+
+ var candidateSmall = new InsertionCandidate(
+ trip,
+ 0,
+ 2,
+ List.of(shared0, shared1),
+ Duration.ofMinutes(1),
+ null
+ );
+ var candidateLarge = new InsertionCandidate(
+ trip,
+ 0,
+ 2,
+ List.of(shared0, shared1),
+ Duration.ofMinutes(5),
+ null
+ );
+
+ // Pickup at origin (no pickup segments) → no boarding dwell, so only the 1 intermediate
+ // stop between the 2 shared segments scales: 1x stopDuration difference.
+ var difference = candidateLarge
+ .getPassengerRideDuration()
+ .minus(candidateSmall.getPassengerRideDuration());
+ assertEquals(Duration.ofMinutes(4), difference);
}
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/routing/InsertionEvaluatorTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/routing/InsertionEvaluatorTest.java
index badc0b555bb..b4e593bb316 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/routing/InsertionEvaluatorTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/routing/InsertionEvaluatorTest.java
@@ -27,7 +27,6 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opentripplanner.astar.model.GraphPath;
-import org.opentripplanner.ext.carpooling.constraints.PassengerDelayConstraints;
import org.opentripplanner.ext.carpooling.model.CarpoolTrip;
import org.opentripplanner.ext.carpooling.util.BeelineEstimator;
import org.opentripplanner.ext.carpooling.util.StreetVertexUtils;
@@ -42,7 +41,6 @@
class InsertionEvaluatorTest {
- private PassengerDelayConstraints delayConstraints;
private InsertionPositionFinder positionFinder;
private LinkingContext linkingContext;
private StreetVertexUtils streetVertexUtils;
@@ -50,8 +48,7 @@ class InsertionEvaluatorTest {
@BeforeEach
void setup() {
- delayConstraints = new PassengerDelayConstraints();
- positionFinder = new InsertionPositionFinder(delayConstraints, new BeelineEstimator());
+ positionFinder = new InsertionPositionFinder(new BeelineEstimator());
vertexMap = new HashMap<>();
Map> locationVertices = new HashMap<>();
@@ -110,7 +107,8 @@ private InsertionCandidate findOptimalInsertion(
List viablePositions = positionFinder.findViablePositions(
trip,
passengerPickup,
- passengerDropoff
+ passengerDropoff,
+ Duration.ZERO
);
if (viablePositions.isEmpty()) {
@@ -118,10 +116,10 @@ private InsertionCandidate findOptimalInsertion(
}
var evaluator = new InsertionEvaluator(
- delayConstraints,
linkingContext,
streetVertexUtils,
- carpoolRouter
+ carpoolRouter,
+ Duration.ZERO
);
return evaluator.findBestInsertion(
tripWithVertices,
@@ -140,10 +138,15 @@ private WgsCoordinate getCoordinateBetween(WgsCoordinate coordinate1, WgsCoordin
@Test
void findOptimalInsertion_onDeviationBudgetExceeded_returnsNull() {
+ var deviationBudget = Duration.ofMinutes(5);
var trip = createTripWithStops(
OSLO_SOUTH,
- List.of(createStopAt(0, OSLO_CENTER), createStopAt(0, OSLO_NORTHEAST)),
- OSLO_NORTH
+ List.of(
+ createStopAt(OSLO_CENTER, deviationBudget),
+ createStopAt(OSLO_NORTHEAST, deviationBudget)
+ ),
+ OSLO_NORTH,
+ deviationBudget
);
var mockPath = createGraphPath(Duration.ofMinutes(4));
@@ -161,7 +164,7 @@ void findOptimalInsertion_onDeviationBudgetExceeded_returnsNull() {
@Test
void findOptimalInsertion_noValidPositions_returnsNull() {
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
+ var trip = createTripWithDeviationBudget(Duration.ofMinutes(20), OSLO_CENTER, OSLO_NORTH);
// Routing function returns null (simulating routing failure)
// This causes evaluator to skip all positions
CarpoolRouter routingFunction = (from, to) -> null;
@@ -173,7 +176,7 @@ void findOptimalInsertion_noValidPositions_returnsNull() {
@Test
void findOptimalInsertion_oneValidPosition_returnsCandidate() {
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
+ var trip = createTripWithDeviationBudget(Duration.ofMinutes(20), OSLO_CENTER, OSLO_NORTH);
var mockPath = createGraphPath();
@@ -189,7 +192,7 @@ void findOptimalInsertion_oneValidPosition_returnsCandidate() {
@Test
void findOptimalInsertion_routingFails_skipsPosition() {
// Use a trip with one stop to have multiple viable insertion positions
- var stop1 = createStopAt(0, OSLO_EAST);
+ var stop1 = createStopAt(OSLO_EAST);
var trip = createTripWithStops(OSLO_CENTER, List.of(stop1), OSLO_NORTH);
var mockPath = createGraphPath(Duration.ofMinutes(3));
@@ -241,8 +244,8 @@ void findOptimalInsertion_exceedsDeviationBudget_returnsNull() {
@Test
void findOptimalInsertion_tripWithStops_evaluatesAllPositions() {
- var stop1 = createStopAt(0, OSLO_EAST);
- var stop2 = createStopAt(1, OSLO_WEST);
+ var stop1 = createStopAt(OSLO_EAST);
+ var stop2 = createStopAt(OSLO_WEST);
var trip = createTripWithStops(OSLO_CENTER, List.of(stop1, stop2), OSLO_NORTH);
var mockPath = createGraphPath();
@@ -256,7 +259,7 @@ void findOptimalInsertion_tripWithStops_evaluatesAllPositions() {
@Test
void findOptimalInsertion_baselineDurationCalculationFails_returnsNull() {
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
+ var trip = createTripWithDeviationBudget(Duration.ofMinutes(20), OSLO_CENTER, OSLO_NORTH);
CarpoolRouter routingFunction = (from, to) -> null;
@@ -265,20 +268,44 @@ void findOptimalInsertion_baselineDurationCalculationFails_returnsNull() {
assertNull(result);
}
+ /**
+ * Given two viable insertion positions with different total trip durations,
+ * the evaluator should select the one with the shorter total.
+ *
+ * Trip: SOUTH → CENTER → NORTH (baseline: 10 + 10 = 20 min)
+ * Passenger: pickup at EAST, dropoff at WEST
+ *
+ * Position (1,2) modified route: SOUTH → EAST → WEST → CENTER → NORTH
+ * segments: 8 + 4 + 9 + 10(reused) = 31 min
+ *
+ * Position (2,3) modified route: SOUTH → CENTER → EAST → WEST → NORTH
+ * segments: 10(reused) + 3 + 4 + 5 = 22 min ← shorter, should be selected
+ */
@Test
- void findOptimalInsertion_selectsMinimumAdditionalDuration() {
- var trip = createTripWithDeviationBudget(Duration.ofMinutes(20), OSLO_CENTER, OSLO_NORTH);
+ void findBestInsertion_selectsShorterTotalTripDuration() {
+ var stop = createStopAt(OSLO_CENTER, Duration.ofMinutes(30));
+ var trip = createTripWithStops(OSLO_SOUTH, List.of(stop), OSLO_NORTH, Duration.ofMinutes(30));
+ var tripWithVertices = createTripWithVertices(trip);
final Map, GraphPath> pathsMap = new HashMap<>(
Map.of(
+ // Baseline segments
+ new Pair<>(OSLO_SOUTH, OSLO_CENTER),
+ createGraphPath(Duration.ofMinutes(10)),
new Pair<>(OSLO_CENTER, OSLO_NORTH),
createGraphPath(Duration.ofMinutes(10)),
- new Pair<>(OSLO_CENTER, OSLO_EAST),
- createGraphPath(Duration.ofMinutes(4)),
+ // Position (1,2) new segments
+ new Pair<>(OSLO_SOUTH, OSLO_EAST),
+ createGraphPath(Duration.ofMinutes(8)),
new Pair<>(OSLO_EAST, OSLO_WEST),
- createGraphPath(Duration.ofMinutes(5)),
+ createGraphPath(Duration.ofMinutes(4)),
+ new Pair<>(OSLO_WEST, OSLO_CENTER),
+ createGraphPath(Duration.ofMinutes(9)),
+ // Position (2,3) new segments
+ new Pair<>(OSLO_CENTER, OSLO_EAST),
+ createGraphPath(Duration.ofMinutes(3)),
new Pair<>(OSLO_WEST, OSLO_NORTH),
- createGraphPath(Duration.ofMinutes(6))
+ createGraphPath(Duration.ofMinutes(5))
)
);
@@ -286,18 +313,30 @@ void findOptimalInsertion_selectsMinimumAdditionalDuration() {
CarpoolRouter routingFunction = (from, to) ->
pathsMap.get(new Pair<>(getCoordinate(from), getCoordinate(to)));
- var result = findOptimalInsertion(trip, OSLO_EAST, OSLO_WEST, routingFunction);
+ var viablePositions = List.of(new InsertionPosition(1, 2), new InsertionPosition(2, 3));
+
+ var evaluator = new InsertionEvaluator(
+ linkingContext,
+ streetVertexUtils,
+ routingFunction,
+ Duration.ZERO
+ );
+ var result = evaluator.findBestInsertion(
+ tripWithVertices,
+ viablePositions,
+ OSLO_EAST,
+ OSLO_WEST
+ );
assertNotNull(result);
- // Should have selected one of the evaluated insertions
- // The exact additional duration depends on which position was evaluated first
- assertTrue(result.additionalDuration().compareTo(Duration.ofMinutes(20)) <= 0);
- assertTrue(result.additionalDuration().compareTo(Duration.ZERO) > 0);
+ assertEquals(2, result.pickupPosition());
+ assertEquals(3, result.dropoffPosition());
+ assertEquals(Duration.ofMinutes(22), result.totalTripDuration());
}
@Test
void findOptimalInsertion_simpleTrip_hasExpectedStructure() {
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
+ var trip = createTripWithDeviationBudget(Duration.ofMinutes(20), OSLO_CENTER, OSLO_NORTH);
var mockPath = createGraphPath();
@@ -366,85 +405,25 @@ void findOptimalInsertion_insertBetweenTwoPoints_routesAllSegments() {
// Note: With real State objects, exact durations will have minor rounding differences
// (typically 1-2 seconds per edge due to millisecond rounding in StreetEdge.doTraverse())
- // The baseline should be approximately 10 minutes (within 10 seconds tolerance)
- assertTrue(
- Math.abs(result.durationBetweenOriginAndDestination().toSeconds() - 600) < 10,
- "Baseline should be approximately 10 min (within 10s), got " +
- result.durationBetweenOriginAndDestination()
- );
-
- // CRITICAL: Total duration should be sum of NEW segments, NOT baseline duration
+ // CRITICAL: Total driving duration should be sum of NEW segments, NOT baseline duration
// Total = 3 + 2 + 4 = 9 minutes (approximately, with rounding)
// If bug exists, segment A→C would incorrectly use baseline (10 min) → total would be wrong
assertTrue(
- Math.abs(result.totalDuration().toSeconds() - 540) < 10,
- "Total duration should be approximately 9 min (within 10s), got " + result.totalDuration()
- );
-
- // Additional duration should be negative (this insertion is actually faster!)
- // This is realistic for insertions that "shortcut" part of the baseline route
- assertTrue(
- result.additionalDuration().isNegative(),
- "Additional duration should be negative (insertion is faster), got " +
- result.additionalDuration()
+ Math.abs(result.totalTripDuration().toSeconds() - 540) < 10,
+ "Total driving duration should be approximately 9 min (within 10s), got " +
+ result.totalTripDuration()
);
// Routing was called at least 4 times (1 baseline + 3 new segments minimum)
assertTrue(callCount[0] >= 4, "Should have called routing at least 4 times");
}
- @Test
- void findOptimalInsertion_insertAtEnd_reusesMostSegments() {
- // This test verifies that segment reuse optimization still works correctly
- // Scenario: Trip A→B→C, insert passenger that allows some segment reuse
- // Expected: Segments that have matching endpoints should be REUSED
-
- var stop1 = createStopAt(0, OSLO_EAST);
- var trip = createTripWithStops(OSLO_CENTER, List.of(stop1), OSLO_NORTH);
-
- // Baseline has 2 segments: CENTER→EAST, EAST→NORTH
- var mockPath = createGraphPath(Duration.ofMinutes(3));
-
- final int[] callCount = { 0 };
- CarpoolRouter carpoolRouter = (from, to) -> {
- callCount[0]++;
- return mockPath;
- };
-
- // Insert passenger - the algorithm will find the best position
- var result = findOptimalInsertion(trip, OSLO_WEST, OSLO_SOUTH, carpoolRouter);
-
- assertNotNull(result, "Should find valid insertion");
-
- // Duration between start and stop should be calculated correctly
- assertTrue(
- Duration.ofMinutes(3).minus(result.durationBetweenOriginAndDestination()).toSeconds() < 10,
- "Baseline should be approximately 3 min (within 10s), got " +
- result.durationBetweenOriginAndDestination()
- );
-
- // The modified route should have more segments than baseline
- assertTrue(
- result.routeSegments().size() >= 2,
- "Modified route should have at least baseline segments"
- );
-
- // Additional duration should be positive (adding detour)
- assertTrue(
- result.additionalDuration().compareTo(Duration.ZERO) > 0,
- "Adding passenger should increase duration"
- );
-
- // Routing was called for baseline and new segments
- assertTrue(callCount[0] >= 2, "Should have called routing at least 2 times");
- }
-
@Test
void findOptimalInsertion_pickupAtExistingPoint_handlesCorrectly() {
// Scenario: Trip A→B→C, passenger pickup at B (existing point), dropoff at new point
// Expected: Segment A→B should be reused, B→dropoff and dropoff→C should be routed
- var stop1 = createStopAt(0, OSLO_EAST);
+ var stop1 = createStopAt(OSLO_EAST);
var trip = createTripWithStops(OSLO_CENTER, List.of(stop1), OSLO_NORTHEAST);
var mockPath = createGraphPath(Duration.ofMinutes(3));
@@ -473,7 +452,7 @@ void findOptimalInsertion_singleSegmentTrip_routesAllNewSegments() {
// Edge case: Simplest possible trip (2 points, 1 segment)
// Any insertion will require routing all new segments
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
+ var trip = createTripWithDeviationBudget(Duration.ofMinutes(20), OSLO_CENTER, OSLO_NORTH);
var mockPath = createGraphPath(Duration.ofMinutes(5));
@@ -491,8 +470,7 @@ void findOptimalInsertion_singleSegmentTrip_routesAllNewSegments() {
// Routing was called for baseline and new segments
assertTrue(callCount[0] >= 4, "Should have called routing at least 4 times");
- // Total duration should be positive
- assertTrue(result.totalDuration().compareTo(Duration.ZERO) > 0);
- assertTrue(result.durationBetweenOriginAndDestination().compareTo(Duration.ZERO) > 0);
+ // Total driving duration should be positive
+ assertTrue(result.totalTripDuration().compareTo(Duration.ZERO) > 0);
}
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/routing/InsertionPositionFinderTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/routing/InsertionPositionFinderTest.java
index 0ae740b984b..ca1a46a9761 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/routing/InsertionPositionFinderTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/routing/InsertionPositionFinderTest.java
@@ -19,12 +19,10 @@
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.opentripplanner.ext.carpooling.constraints.PassengerDelayConstraints;
-import org.opentripplanner.ext.carpooling.util.BeelineEstimator;
/**
* Tests for {@link InsertionPositionFinder}.
- * Focuses on heuristic validation: capacity, directional compatibility, and beeline delays.
+ * Focuses on heuristic validation: capacity and beeline delays.
*/
class InsertionPositionFinderTest {
@@ -39,33 +37,19 @@ void setup() {
void findViablePositions_simpleTrip_findsPositions() {
var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
- var viablePositions = finder.findViablePositions(trip, OSLO_EAST, OSLO_WEST);
+ // Passenger picked up east of route, dropped off at destination — small compatible detour
+ var viablePositions = finder.findViablePositions(trip, OSLO_EAST, OSLO_NORTH, Duration.ZERO);
assertFalse(viablePositions.isEmpty());
- // Simple trip (2 points) allows insertions at positions (1,2) and (1,3)
- assertTrue(viablePositions.size() >= 1);
- }
-
- @Test
- void findViablePositions_incompatibleDirection_rejectsPosition() {
- var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
-
- // Passenger going perpendicular (EAST to WEST when trip is CENTER to NORTH)
- // This should result in some positions being rejected by directional checks
- var viablePositions = finder.findViablePositions(trip, OSLO_SOUTH, OSLO_CENTER);
-
- // May not be completely empty, but should have fewer positions than compatible directions
- // The directional check filters out positions that cause too much backtracking
- assertNotNull(viablePositions);
}
@Test
void findViablePositions_noCapacity_rejectsPosition() {
// Create a trip with 0 available seats
- var stops = List.of(createOriginStop(OSLO_CENTER), createDestinationStop(OSLO_NORTH, 1));
+ var stops = List.of(createOriginStop(OSLO_CENTER), createDestinationStop(OSLO_NORTH));
var trip = createTripWithCapacity(0, stops);
- var viablePositions = finder.findViablePositions(trip, OSLO_EAST, OSLO_WEST);
+ var viablePositions = finder.findViablePositions(trip, OSLO_EAST, OSLO_WEST, Duration.ZERO);
// Should reject all positions due to capacity
assertTrue(viablePositions.isEmpty());
@@ -73,37 +57,31 @@ void findViablePositions_noCapacity_rejectsPosition() {
@Test
void findViablePositions_exceedsBeelineDelay_rejectsPosition() {
- // Create finder with very restrictive delay constraints
- var restrictiveConstraints = new PassengerDelayConstraints(Duration.ofSeconds(1));
- var restrictiveFinder = new InsertionPositionFinder(
- restrictiveConstraints,
- new BeelineEstimator()
+ // Create stops with very restrictive deviation budgets (1 second)
+ var restrictiveBudget = Duration.ofSeconds(1);
+ var trip = createTripWithStops(
+ OSLO_CENTER,
+ List.of(createStopAt(OSLO_EAST, restrictiveBudget)),
+ OSLO_NORTH,
+ restrictiveBudget
);
- var trip = createTripWithStops(OSLO_CENTER, List.of(createStopAt(0, OSLO_EAST)), OSLO_NORTH);
-
- // Try to insert passenger that would cause significant detour
- // Far from route
- // Even farther
- var viablePositions = restrictiveFinder.findViablePositions(trip, OSLO_WEST, OSLO_SOUTH);
+ // Passenger going opposite direction (WEST→SOUTH) with 1s budget — all positions should be rejected
+ var viablePositions = finder.findViablePositions(trip, OSLO_WEST, OSLO_SOUTH, Duration.ZERO);
- // With very restrictive constraints, positions causing significant detours should be rejected
- // However, the beeline check only applies if there are existing stops (routePoints.size() > 2)
- // With CENTER, EAST, NORTH we have 3 points, so the check should apply
- // The result depends on the actual distances and heuristics
- assertNotNull(viablePositions);
+ assertTrue(viablePositions.isEmpty());
}
@Test
void findViablePositions_multipleStops_checksAllCombinations() {
- var stop1 = createStopAt(0, OSLO_EAST);
- var stop2 = createStopAt(1, OSLO_WEST);
+ var stop1 = createStopAt(OSLO_EAST);
+ var stop2 = createStopAt(OSLO_WEST);
var trip = createTripWithStops(OSLO_CENTER, List.of(stop1, stop2), OSLO_NORTH);
- var viablePositions = finder.findViablePositions(trip, OSLO_SOUTH, OSLO_NORTH);
+ var viablePositions = finder.findViablePositions(trip, OSLO_SOUTH, OSLO_NORTH, Duration.ZERO);
// Should evaluate multiple pickup/dropoff combinations
- // Exact count depends on directional and beeline filtering
+ // Exact count depends on beeline filtering
assertNotNull(viablePositions);
}
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/service/DefaultCarpoolingServiceAccessEgressTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/service/DefaultCarpoolingServiceAccessEgressTest.java
index a9fc0d84658..e2aab820ea1 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/service/DefaultCarpoolingServiceAccessEgressTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/service/DefaultCarpoolingServiceAccessEgressTest.java
@@ -21,13 +21,12 @@
import org.opentripplanner.ext.carpooling.internal.DefaultCarpoolingRepository;
import org.opentripplanner.ext.carpooling.routing.CarpoolAccessEgress;
import org.opentripplanner.ext.carpooling.routing.CarpoolTreeStreetRouter;
-import org.opentripplanner.graph_builder.module.nearbystops.SiteRepositoryResolver;
-import org.opentripplanner.graph_builder.module.nearbystops.StopResolver;
import org.opentripplanner.model.GenericLocation;
import org.opentripplanner.routing.algorithm.GraphRoutingTest;
import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressType;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.routing.api.request.request.StreetRequest;
+import org.opentripplanner.routing.graphfinder.TransitServiceResolver;
import org.opentripplanner.routing.linking.LinkingContext;
import org.opentripplanner.routing.linking.VertexLinkerTestFactory;
import org.opentripplanner.street.geometry.WgsCoordinate;
@@ -80,7 +79,7 @@ class DefaultCarpoolingServiceAccessEgressTest extends GraphRoutingTest {
private DefaultCarpoolingService service;
private CarpoolingRepository repository;
- private StopResolver stopResolver;
+ private TransitServiceResolver transitServiceResolver;
private LinkingContext linkingContext;
private TransitStopVertex stopT1;
@@ -177,9 +176,9 @@ public void build() {
Graph graph = model.graph();
var timetableRepository = model.timetableRepository();
- stopResolver = new SiteRepositoryResolver(timetableRepository.getSiteRepository());
VertexLinker vertexLinker = VertexLinkerTestFactory.of(graph);
TransitService transitService = new DefaultTransitService(timetableRepository);
+ transitServiceResolver = new TransitServiceResolver(transitService);
repository = new DefaultCarpoolingRepository();
// LinkingContext: P1 -> {iP1}, P2 -> {iP2}, P3 -> {iP3}
@@ -256,7 +255,7 @@ void returnsEmptyWhenAccessModeIsNotCarpool() {
request,
new StreetRequest(StreetMode.WALK),
AccessEgressType.ACCESS,
- stopResolver,
+ transitServiceResolver,
linkingContext,
SEARCH_TIME
);
@@ -276,7 +275,7 @@ void returnsEmptyWhenEgressModeIsNotCarpool() {
request,
new StreetRequest(StreetMode.WALK),
AccessEgressType.EGRESS,
- stopResolver,
+ transitServiceResolver,
linkingContext,
SEARCH_TIME
);
@@ -292,7 +291,7 @@ void returnsEmptyWhenNoCarpoolTripsInRepository() {
request,
new StreetRequest(StreetMode.CARPOOL),
AccessEgressType.ACCESS,
- stopResolver,
+ transitServiceResolver,
linkingContext,
SEARCH_TIME
);
@@ -312,7 +311,7 @@ void returnsEmptyWhenTripsFailTimeFilter() {
request,
new StreetRequest(StreetMode.CARPOOL),
AccessEgressType.ACCESS,
- stopResolver,
+ transitServiceResolver,
linkingContext,
SEARCH_TIME
);
@@ -333,7 +332,7 @@ void findsAccessResultsForCompatibleTrip() {
request,
new StreetRequest(StreetMode.CARPOOL),
AccessEgressType.ACCESS,
- stopResolver,
+ transitServiceResolver,
linkingContext,
SEARCH_TIME
);
@@ -348,8 +347,8 @@ void findsAccessResultsForCompatibleTrip() {
assertFalse(accessEgress.getSegments().isEmpty(), "Segments should not be empty");
}
- int stopT3Index = stopResolver.getRegularStop(stopT3.getId()).getIndex();
- int stopT4Index = stopResolver.getRegularStop(stopT4.getId()).getIndex();
+ int stopT3Index = transitServiceResolver.getStop(stopT3.getId()).getIndex();
+ int stopT4Index = transitServiceResolver.getStop(stopT4.getId()).getIndex();
assertTrue(
results.stream().anyMatch(r -> r.stop() == stopT3Index),
"At least one access result should include stopT3"
@@ -373,7 +372,7 @@ void findsEgressResultsForCompatibleTrip() {
request,
new StreetRequest(StreetMode.CARPOOL),
AccessEgressType.EGRESS,
- stopResolver,
+ transitServiceResolver,
linkingContext,
SEARCH_TIME
);
@@ -385,8 +384,8 @@ void findsEgressResultsForCompatibleTrip() {
assertNotNull(accessEgress.getSegments(), "Segments should not be null");
}
- int stopT1Index = stopResolver.getRegularStop(stopT1.getId()).getIndex();
- int stopT2Index = stopResolver.getRegularStop(stopT2.getId()).getIndex();
+ int stopT1Index = transitServiceResolver.getStop(stopT1.getId()).getIndex();
+ int stopT2Index = transitServiceResolver.getStop(stopT2.getId()).getIndex();
assertTrue(
results.stream().anyMatch(r -> r.stop() == stopT1Index),
"At least one egress result should include stopT1"
@@ -410,7 +409,7 @@ void accessResultsHaveMatchingArrivalDepartureAndDuration() {
request,
new StreetRequest(StreetMode.CARPOOL),
AccessEgressType.ACCESS,
- stopResolver,
+ transitServiceResolver,
linkingContext,
transitSearchTimeZero
);
@@ -436,8 +435,8 @@ void accessResultsHaveMatchingArrivalDepartureAndDuration() {
);
}
- int stopT3Index = stopResolver.getRegularStop(stopT3.getId()).getIndex();
- int stopT4Index = stopResolver.getRegularStop(stopT4.getId()).getIndex();
+ int stopT3Index = transitServiceResolver.getStop(stopT3.getId()).getIndex();
+ int stopT4Index = transitServiceResolver.getStop(stopT4.getId()).getIndex();
assertTrue(
results.stream().anyMatch(r -> r.stop() == stopT3Index),
"At least one access result should include stopT3"
@@ -465,7 +464,7 @@ void twoTripsReturnTwoResultsPerStop() {
request,
new StreetRequest(StreetMode.CARPOOL),
AccessEgressType.ACCESS,
- stopResolver,
+ transitServiceResolver,
linkingContext,
SEARCH_TIME
);
@@ -482,8 +481,8 @@ void twoTripsReturnTwoResultsPerStop() {
);
}
- int stopT3Index = stopResolver.getRegularStop(stopT3.getId()).getIndex();
- int stopT4Index = stopResolver.getRegularStop(stopT4.getId()).getIndex();
+ int stopT3Index = transitServiceResolver.getStop(stopT3.getId()).getIndex();
+ int stopT4Index = transitServiceResolver.getStop(stopT4.getId()).getIndex();
assertTrue(
results.stream().anyMatch(r -> r.stop() == stopT3Index),
"At least one access result should include stopT3"
@@ -502,11 +501,10 @@ void tripWithIntermediateStopsProducesResults() {
4,
List.of(
CarpoolTripTestData.createOriginStopWithTime(coordA, departureTime, departureTime),
- CarpoolTripTestData.createStopAt(1, coordB),
- CarpoolTripTestData.createStopAt(2, coordC),
+ CarpoolTripTestData.createStopAt(coordB),
+ CarpoolTripTestData.createStopAt(coordC),
CarpoolTripTestData.createDestinationStopWithTime(
coordD,
- 3,
departureTime.plusHours(1),
departureTime.plusHours(1)
)
@@ -521,15 +519,15 @@ void tripWithIntermediateStopsProducesResults() {
request,
new StreetRequest(StreetMode.CARPOOL),
AccessEgressType.ACCESS,
- stopResolver,
+ transitServiceResolver,
linkingContext,
SEARCH_TIME
);
assertFalse(results.isEmpty(), "Trip with intermediate stops should produce results");
- int stopT3Index = stopResolver.getRegularStop(stopT3.getId()).getIndex();
- int stopT4Index = stopResolver.getRegularStop(stopT4.getId()).getIndex();
+ int stopT3Index = transitServiceResolver.getStop(stopT3.getId()).getIndex();
+ int stopT4Index = transitServiceResolver.getStop(stopT4.getId()).getIndex();
assertTrue(
results.stream().anyMatch(r -> r.stop() == stopT3Index),
"At least one access result should include stopT3"
@@ -552,7 +550,7 @@ void earliestDepartureTimeRespectsRequestedDepartureTime() {
request,
new StreetRequest(StreetMode.CARPOOL),
AccessEgressType.ACCESS,
- stopResolver,
+ transitServiceResolver,
linkingContext,
SEARCH_TIME
);
@@ -570,8 +568,8 @@ void earliestDepartureTimeRespectsRequestedDepartureTime() {
);
}
- int stopT3Index = stopResolver.getRegularStop(stopT3.getId()).getIndex();
- int stopT4Index = stopResolver.getRegularStop(stopT4.getId()).getIndex();
+ int stopT3Index = transitServiceResolver.getStop(stopT3.getId()).getIndex();
+ int stopT4Index = transitServiceResolver.getStop(stopT4.getId()).getIndex();
assertTrue(
results.stream().anyMatch(r -> r.stop() == stopT3Index),
"At least one access result should include stopT3"
@@ -632,20 +630,24 @@ void accessDepartureAndArrivalTimesMatchIndependentRouting() {
request,
new StreetRequest(StreetMode.CARPOOL),
AccessEgressType.ACCESS,
- stopResolver,
+ transitServiceResolver,
linkingContext,
transitSearchTimeZero
);
assertFalse(results.isEmpty(), "Should find access results");
+ // Departure time of the passenger is when the car arrives at the pickup (P2). The
+ // boarding dwell at P2 is part of the CarpoolAccessEgress duration, not added before
+ // the departure time.
+ var pickupTime = RouteRequest.defaultValue().preferences().car().pickupTime();
var expectedDeparture = (int) Duration.between(
transitSearchTimeZero.toInstant(),
departureTime.plus(drivingDurationAToP2).toInstant()
).getSeconds();
- int stopT3Index = stopResolver.getRegularStop(stopT3.getId()).getIndex();
- int stopT4Index = stopResolver.getRegularStop(stopT4.getId()).getIndex();
+ int stopT3Index = transitServiceResolver.getStop(stopT3.getId()).getIndex();
+ int stopT4Index = transitServiceResolver.getStop(stopT4.getId()).getIndex();
var targetStopIndices = Set.of(stopT3Index, stopT4Index);
Map expectedDrivingP2ToStop = Map.of(
@@ -681,15 +683,15 @@ void accessDepartureAndArrivalTimesMatchIndependentRouting() {
);
var drivingP2ToStop = expectedDrivingP2ToStop.get(accessEgress.stop());
- // The service adds CARPOOL_STOP_DURATION (1 min) for the passenger pickup at P2
+ // Boarding dwell at P2 is now part of the passenger's ride duration, so the arrival
+ // time is the departure (arrival at P2) plus boarding plus driving from P2 to the stop.
var expectedArrival =
- expectedDeparture +
- (int) drivingP2ToStop.getSeconds() +
- (int) DefaultCarpoolingService.CARPOOL_STOP_DURATION.getSeconds();
+ expectedDeparture + (int) pickupTime.getSeconds() + (int) drivingP2ToStop.getSeconds();
assertEquals(
expectedArrival,
accessEgress.getArrivalTimeOfPassenger(),
- "Arrival time should equal passenger departure plus driving time from P2 to stop plus pickup duration"
+ "Arrival time should equal passenger departure plus boarding dwell plus driving " +
+ "time from P2 to stop"
);
}
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/service/DefaultCarpoolingServiceDirectTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/service/DefaultCarpoolingServiceDirectTest.java
index 226b0662c85..1dfd2482fbb 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/service/DefaultCarpoolingServiceDirectTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/service/DefaultCarpoolingServiceDirectTest.java
@@ -42,17 +42,27 @@
* These tests use a real street graph to verify the full direct routing pipeline
* including filtering, position finding, insertion evaluation, and itinerary mapping.
*
- * Graph layout (going east, ~2km total):
+ * Graph layout (main road going east, P and Q sit south of the road):
*
- * A ---- B ----------- C ---- D
- * 0m 500m 1500m 2000m
- * \ /
- * P ------(shortcut)------Q
+ * 500m 1000m 500m
+ * A ---------- B ----------- C ---------- D
+ * |\ / \ /
+ * | \ / 255 255 \ / 255
+ * | \ / \ /
+ * | P ---------- 1400 ---------- Q
+ * | (P-Q shortcut) /
+ * +-------------- 1500 ----------+
+ * (direct A-Q bypass)
*
- * A = tripStart, D = tripEnd (graph intersections)
- * P-Q direct shortcut edge exists (bypasses B and C)
- * P = passenger pickup (south, between A and B, linked via LinkingContext)
- * Q = passenger dropoff (south, between C and D, linked via LinkingContext)
+ * A = tripStart, D = tripEnd
+ * P = passenger pickup, connected to both A and B (255m each)
+ * Q = passenger dropoff, connected to both C and D (255m each)
+ * P-Q direct shortcut (1400m) beats the main-road P-B-C-Q path (1510m) by 110m,
+ * so the carpool's shared segment (pickup -> dropoff) routes over it.
+ * A-Q direct bypass (1500m) is shorter than A-P-Q (255 + 1400 = 1655m), so the
+ * shortest A->Q path skips the pickup. The carpool itself is still forced to drive
+ * A->P->Q to pick up the passenger; this edge exists so the test cannot rely on
+ * "route tripStart to dropoff directly" as a proxy for what the carpool drives.
*
*/
class DefaultCarpoolingServiceDirectTest extends GraphRoutingTest {
@@ -123,7 +133,8 @@ public void build() {
biStreet(B, P, 255);
biStreet(C, Q, 255);
biStreet(D, Q, 255);
- biStreet(P, Q, 1500);
+ biStreet(P, Q, 1400);
+ biStreet(A, Q, 1500);
biStreet(Q, F, (int) DistanceBasedFilter.DEFAULT_MAX_DISTANCE_METERS + 10000);
}
}
@@ -270,26 +281,6 @@ void returnsEmptyWhenDropoffExceedsMaxDistance() {
);
}
- @Test
- void returnsAtMostMaxDirectResults() {
- int maxResults = DefaultCarpoolingService.DEFAULT_MAX_CARPOOL_DIRECT_RESULTS;
- for (int i = 0; i < maxResults + 2; i++) {
- var departureTime = SEARCH_TIME.plusMinutes(5 + i * 5);
- var trip = CarpoolTripTestData.createSimpleTripWithTime(tripStart, tripEnd, departureTime);
- repository.upsertCarpoolTrip(trip);
- }
-
- var request = buildDirectCarpoolRequest(passengerPickup, passengerDropoff, SEARCH_TIME);
-
- var results = service.routeDirect(request, linkingContext);
-
- assertEquals(
- maxResults,
- results.size(),
- "Should return exactly DEFAULT_MAX_CARPOOL_DIRECT_RESULTS results"
- );
- }
-
@Test
void twoTripsReturnTwoResults() {
var departureTime1 = SEARCH_TIME.plusMinutes(10);
@@ -316,11 +307,10 @@ void tripWithIntermediateStopsProducesResults() {
4,
List.of(
CarpoolTripTestData.createOriginStopWithTime(tripStart, departureTime, departureTime),
- CarpoolTripTestData.createStopAt(1, coordB),
- CarpoolTripTestData.createStopAt(2, coordC),
+ CarpoolTripTestData.createStopAt(coordB),
+ CarpoolTripTestData.createStopAt(coordC),
CarpoolTripTestData.createDestinationStopWithTime(
tripEnd,
- 3,
departureTime.plusHours(1),
departureTime.plusHours(1)
)
@@ -348,11 +338,10 @@ void routeFollowsIntermediateStopsInsteadOfDirectPath() {
4,
List.of(
CarpoolTripTestData.createOriginStopWithTime(tripStart, departureTime, departureTime),
- CarpoolTripTestData.createStopAt(1, coordB),
- CarpoolTripTestData.createStopAt(2, coordC),
+ CarpoolTripTestData.createStopAt(coordB),
+ CarpoolTripTestData.createStopAt(coordC),
CarpoolTripTestData.createDestinationStopWithTime(
tripEnd,
- 3,
departureTime.plusHours(1),
departureTime.plusHours(1)
)
@@ -401,15 +390,84 @@ void routeFollowsIntermediateStopsInsteadOfDirectPath() {
);
}
+ @Test
+ void itineraryReflectsDriverScheduleWhenTripDepartsBeforeRequestTime() {
+ // Trip starts 10 min before the passenger's requested time but within the 30-min
+ // TimeBasedFilter window, so the trip is accepted. The driver arrives at the pickup
+ // well before the requested time — the question is what the returned itinerary says.
+ var departureTime = SEARCH_TIME.minusMinutes(10);
+ var trip = CarpoolTripTestData.createSimpleTripWithTime(tripStart, tripEnd, departureTime);
+ repository.upsertCarpoolTrip(trip);
+
+ var router = new CarpoolTreeStreetRouter();
+ router.addVertex(vertexTripStart, CarpoolTreeStreetRouter.Direction.FROM, Duration.ofHours(2));
+ router.addVertex(vertexPickup, CarpoolTreeStreetRouter.Direction.FROM, Duration.ofHours(2));
+
+ var pathToPickup = router.route(vertexTripStart, vertexPickup);
+ assertNotNull(pathToPickup);
+ var drivingToPickup = Duration.between(
+ pathToPickup.states.getFirst().getTime(),
+ pathToPickup.states.getLast().getTime()
+ );
+
+ var pathPickupToDropoff = router.route(vertexPickup, vertexDropoff);
+ assertNotNull(pathPickupToDropoff);
+ var drivingPickupToDropoff = Duration.between(
+ pathPickupToDropoff.states.getFirst().getTime(),
+ pathPickupToDropoff.states.getLast().getTime()
+ );
+
+ var request = buildDirectCarpoolRequest(passengerPickup, passengerDropoff, SEARCH_TIME);
+ var stopDuration = request.preferences().car().pickupTime();
+
+ // The driver's pickup arrival time is fixed by the trip's schedule. It does NOT shift
+ // forward just because the passenger requested a later departure — the driver cannot
+ // wait (committed schedule / other passengers).
+ var actualPickupArrivalTime = departureTime.plus(drivingToPickup);
+
+ // Guard the premise of this test: the requested time is after the real pickup arrival.
+ assertTrue(
+ request.dateTime().isAfter(actualPickupArrivalTime.toInstant()),
+ "Test premise: request time must be after the driver's real pickup arrival time"
+ );
+
+ // Itinerary start time is when the car arrives at the pickup; the boarding dwell is part
+ // of the leg's duration, so it shows up in the end time.
+ var expectedStartTime = actualPickupArrivalTime;
+ var expectedEndTime = expectedStartTime.plus(stopDuration).plus(drivingPickupToDropoff);
+
+ var results = service.routeDirect(request, linkingContext);
+
+ assertFalse(results.isEmpty(), "Trip within search window should produce a result");
+
+ var itinerary = results.getFirst();
+ assertEquals(
+ expectedStartTime.toInstant(),
+ itinerary.startTime().toInstant(),
+ "Itinerary start time must match the driver's pickup arrival time, not the passenger's " +
+ "requested time — the driver cannot wait for the passenger"
+ );
+ assertEquals(
+ expectedEndTime.toInstant(),
+ itinerary.endTime().toInstant(),
+ "Itinerary end time must match the driver's real dropoff time"
+ );
+ }
+
@Test
void resultItinerariesHaveValidStartAndEndTimes() {
var departureTime = SEARCH_TIME.plusMinutes(10);
var trip = CarpoolTripTestData.createSimpleTripWithTime(tripStart, tripEnd, departureTime);
repository.upsertCarpoolTrip(trip);
- // Independently compute driving durations to derive expected start/end times
+ // The carpool is forced to route via the pickup, so we sum the two segments it actually
+ // drives (tripStart -> pickup, then pickup -> dropoff) rather than routing tripStart -> dropoff
+ // directly. The graph includes an A-Q bypass edge whose shortest path skips the pickup, so a
+ // test that used router.route(tripStart, dropoff) here would not match what the carpool
+ // drives; this guards against regressing to that shortcut-in-the-test.
var router = new CarpoolTreeStreetRouter();
router.addVertex(vertexTripStart, CarpoolTreeStreetRouter.Direction.FROM, Duration.ofHours(2));
+ router.addVertex(vertexPickup, CarpoolTreeStreetRouter.Direction.FROM, Duration.ofHours(2));
var pathToPickup = router.route(vertexTripStart, vertexPickup);
assertNotNull(pathToPickup, "Should route from trip start to pickup");
@@ -418,17 +476,19 @@ void resultItinerariesHaveValidStartAndEndTimes() {
pathToPickup.states.getLast().getTime()
);
- var pathToDropoff = router.route(vertexTripStart, vertexDropoff);
- assertNotNull(pathToDropoff, "Should route from trip start to dropoff");
- var drivingToDropoff = Duration.between(
- pathToDropoff.states.getFirst().getTime(),
- pathToDropoff.states.getLast().getTime()
+ var pathPickupToDropoff = router.route(vertexPickup, vertexDropoff);
+ assertNotNull(pathPickupToDropoff, "Should route from pickup to dropoff");
+ var drivingPickupToDropoff = Duration.between(
+ pathPickupToDropoff.states.getFirst().getTime(),
+ pathPickupToDropoff.states.getLast().getTime()
);
- var expectedStartTime = departureTime.plus(drivingToPickup);
- var expectedEndTime = departureTime.plus(drivingToDropoff);
-
var request = buildDirectCarpoolRequest(passengerPickup, passengerDropoff, SEARCH_TIME);
+ var stopDuration = request.preferences().car().pickupTime();
+ // Start time is when the car arrives at the pickup. The boarding dwell is part of the
+ // leg's duration, so it is included in the end time rather than before the start.
+ var expectedStartTime = departureTime.plus(drivingToPickup);
+ var expectedEndTime = expectedStartTime.plus(stopDuration).plus(drivingPickupToDropoff);
var results = service.routeDirect(request, linkingContext);
@@ -441,12 +501,13 @@ void resultItinerariesHaveValidStartAndEndTimes() {
assertEquals(
expectedStartTime.toInstant(),
itinerary.startTime().toInstant(),
- "Start time should equal trip departure plus driving time to pickup"
+ "Start time should equal trip departure plus driving time to pickup (arrival at pickup)"
);
assertEquals(
expectedEndTime.toInstant(),
itinerary.endTime().toInstant(),
- "End time should equal trip departure plus driving time to dropoff"
+ "End time should equal start time plus boarding dwell plus driving time from pickup " +
+ "to dropoff"
);
}
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/updater/CarpoolSiriMapperTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/updater/CarpoolSiriMapperTest.java
index 99686794eef..d685fc0747d 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/updater/CarpoolSiriMapperTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/updater/CarpoolSiriMapperTest.java
@@ -4,13 +4,23 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.arrivalIsAfterDepartureTime;
+import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.journeyWithDifferentCapacitiesPerCall;
+import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.journeyWithLatestExpectedArrivalTime;
+import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.journeyWithLatestExpectedArrivalTimeAimedOnly;
+import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.journeyWithOnboardCounts;
+import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.journeyWithPerStopLatestExpectedArrivalTimes;
+import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.journeyWithTotalCapacity;
import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.lessThanTwoStops;
import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.minimalCompleteJourney;
import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.minimalCompleteJourneyWithPolygon;
import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.stopTimesAreOutOfOrder;
import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.tripHasAimedTimesOnly;
import static org.opentripplanner.ext.carpooling.CarpoolEstimatedVehicleJourneyData.tripHasExpectedTimesOnly;
+import static org.opentripplanner.ext.carpooling.model.CarpoolStop.DEFAULT_DEVIATION_BUDGET;
+import static org.opentripplanner.ext.carpooling.model.CarpoolStop.DEFAULT_ONBOARD_COUNT;
+import static org.opentripplanner.ext.carpooling.model.CarpoolTrip.DEFAULT_TOTAL_CAPACITY;
+import java.time.Duration;
import org.junit.jupiter.api.Test;
import uk.org.siri.siri21.EstimatedCall;
@@ -19,14 +29,14 @@ public class CarpoolSiriMapperTest {
private final CarpoolSiriMapper mapper = new CarpoolSiriMapper();
@Test
- void mapSiriToCarpoolTrip_arrivalIsAfterDepartureTime_trowsIllegalArgumentException() {
+ void mapSiriToCarpoolTrip_arrivalIsAfterDepartureTime_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () ->
mapper.mapSiriToCarpoolTrip(arrivalIsAfterDepartureTime())
);
}
@Test
- void mapSiriToCarpoolTrip_lessThanTwoStops_trowsIllegalArgumentException() {
+ void mapSiriToCarpoolTrip_lessThanTwoStops_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () ->
mapper.mapSiriToCarpoolTrip(lessThanTwoStops())
);
@@ -103,9 +113,131 @@ void mapSiriToCarpoolTrip_tripHasOnlyExpectedTimes_mapsOk() {
}
@Test
- void mapSiriToCarpoolTrip_stopTimesAreOutOfOrder_trowsIllegalArgumentException() {
+ void mapSiriToCarpoolTrip_stopTimesAreOutOfOrder_throwsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class, () ->
mapper.mapSiriToCarpoolTrip(stopTimesAreOutOfOrder())
);
}
+
+ // -- extractTotalCapacity tests --
+
+ @Test
+ void mapSiriToCarpoolTrip_noCapacityData_returnsDefaultCapacity() {
+ var mapped = mapper.mapSiriToCarpoolTrip(minimalCompleteJourney());
+ assertEquals(DEFAULT_TOTAL_CAPACITY, mapped.totalCapacity());
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_withCapacityData_usesProvidedCapacity() {
+ var mapped = mapper.mapSiriToCarpoolTrip(journeyWithTotalCapacity(3));
+ assertEquals(3, mapped.totalCapacity());
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_zeroCapacity_returnsDefaultCapacity() {
+ var mapped = mapper.mapSiriToCarpoolTrip(journeyWithTotalCapacity(0));
+ assertEquals(DEFAULT_TOTAL_CAPACITY, mapped.totalCapacity());
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_negativeCapacity_returnsDefaultCapacity() {
+ var mapped = mapper.mapSiriToCarpoolTrip(journeyWithTotalCapacity(-1));
+ assertEquals(DEFAULT_TOTAL_CAPACITY, mapped.totalCapacity());
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_differentCapacitiesPerCall_usesFirstValue() {
+ var mapped = mapper.mapSiriToCarpoolTrip(journeyWithDifferentCapacitiesPerCall(3, 7));
+ assertEquals(3, mapped.totalCapacity());
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_consistentCapacitiesPerCall_usesValue() {
+ var mapped = mapper.mapSiriToCarpoolTrip(journeyWithDifferentCapacitiesPerCall(4, 4));
+ assertEquals(4, mapped.totalCapacity());
+ }
+
+ // -- extractOnboardCount tests --
+
+ @Test
+ void mapSiriToCarpoolTrip_noOccupancyData_returnsDefaultOnboardCount() {
+ var mapped = mapper.mapSiriToCarpoolTrip(minimalCompleteJourney());
+ for (var stop : mapped.stops()) {
+ assertEquals(DEFAULT_ONBOARD_COUNT, stop.getOnboardCount());
+ }
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_withOccupancyData_usesProvidedOnboardCount() {
+ var mapped = mapper.mapSiriToCarpoolTrip(journeyWithOnboardCounts(2, 3));
+ assertEquals(2, mapped.stops().getFirst().getOnboardCount());
+ assertEquals(3, mapped.stops().getLast().getOnboardCount());
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_zeroOnboardCount_returnsDefaultOnboardCount() {
+ var mapped = mapper.mapSiriToCarpoolTrip(journeyWithOnboardCounts(0, 0));
+ for (var stop : mapped.stops()) {
+ assertEquals(DEFAULT_ONBOARD_COUNT, stop.getOnboardCount());
+ }
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_negativeOnboardCount_returnsDefaultOnboardCount() {
+ var mapped = mapper.mapSiriToCarpoolTrip(journeyWithOnboardCounts(-1, -1));
+ for (var stop : mapped.stops()) {
+ assertEquals(DEFAULT_ONBOARD_COUNT, stop.getOnboardCount());
+ }
+ }
+
+ // -- extractDeviationBudget tests --
+
+ @Test
+ void mapSiriToCarpoolTrip_noLatestExpectedArrivalTime_returnsDefaultDeviationBudget() {
+ var mapped = mapper.mapSiriToCarpoolTrip(minimalCompleteJourney());
+ assertEquals(Duration.ZERO, mapped.stops().getFirst().getDeviationBudget());
+ assertEquals(DEFAULT_DEVIATION_BUDGET, mapped.stops().getLast().getDeviationBudget());
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_withLatestExpectedArrivalTime_computesDeviationBudget() {
+ var mapped = mapper.mapSiriToCarpoolTrip(journeyWithLatestExpectedArrivalTime(0, 10));
+ var lastStop = mapped.stops().getLast();
+ assertEquals(Duration.ofMinutes(10), lastStop.getDeviationBudget());
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_withLatestExpectedArrivalTimeNoExpected_usesAimedArrivalTime() {
+ var mapped = mapper.mapSiriToCarpoolTrip(journeyWithLatestExpectedArrivalTimeAimedOnly(20));
+ var lastStop = mapped.stops().getLast();
+ assertEquals(Duration.ofMinutes(20), lastStop.getDeviationBudget());
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_originStop_hasZeroDeviationBudget() {
+ var mapped = mapper.mapSiriToCarpoolTrip(journeyWithLatestExpectedArrivalTime(0, 10));
+ assertEquals(Duration.ZERO, mapped.stops().getFirst().getDeviationBudget());
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_latestBeforeExpected_returnsZeroDeviationBudget() {
+ // latestExpectedArrival is before expectedArrival — schedule has slipped past commitment,
+ // no further deviation is acceptable
+ var mapped = mapper.mapSiriToCarpoolTrip(journeyWithLatestExpectedArrivalTime(10, 5));
+ var lastStop = mapped.stops().getLast();
+ assertEquals(Duration.ZERO, lastStop.getDeviationBudget());
+ }
+
+ @Test
+ void mapSiriToCarpoolTrip_multiStopWithDifferingBudgets_eachStopHasOwnBudget() {
+ // 3-stop journey. Intermediate arrives at +20 with latest +23 (3 min slack),
+ // last arrives at +45 with latest +55 (10 min slack).
+ var mapped = mapper.mapSiriToCarpoolTrip(
+ journeyWithPerStopLatestExpectedArrivalTimes(20, 23, 45, 55)
+ );
+ assertEquals(3, mapped.stops().size());
+ assertEquals(Duration.ZERO, mapped.stops().get(0).getDeviationBudget());
+ assertEquals(Duration.ofMinutes(3), mapped.stops().get(1).getDeviationBudget());
+ assertEquals(Duration.ofMinutes(10), mapped.stops().get(2).getDeviationBudget());
+ }
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/util/BeelineEstimatorTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/util/BeelineEstimatorTest.java
index d3c2fb48d54..61023d92683 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/util/BeelineEstimatorTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/util/BeelineEstimatorTest.java
@@ -73,7 +73,7 @@ void calculateCumulativeTimes_simpleRoute_calculatesCorrectly() {
// Route: Oslo Center → Oslo East → Oslo North
List points = List.of(OSLO_CENTER, OSLO_EAST, OSLO_NORTH);
- Duration[] times = estimator.calculateCumulativeTimes(points);
+ Duration[] times = estimator.calculateCumulativeTimes(points, Duration.ZERO);
assertEquals(3, times.length);
// Start at 0
@@ -94,7 +94,7 @@ void calculateCumulativeTimes_simpleRoute_calculatesCorrectly() {
void calculateCumulativeTimes_singlePoint_returnsZero() {
List points = List.of(OSLO_CENTER);
- Duration[] times = estimator.calculateCumulativeTimes(points);
+ Duration[] times = estimator.calculateCumulativeTimes(points, Duration.ZERO);
assertEquals(1, times.length);
assertEquals(Duration.ZERO, times[0]);
@@ -104,7 +104,7 @@ void calculateCumulativeTimes_singlePoint_returnsZero() {
void calculateCumulativeTimes_emptyList_returnsEmptyArray() {
List points = List.of();
- Duration[] times = estimator.calculateCumulativeTimes(points);
+ Duration[] times = estimator.calculateCumulativeTimes(points, Duration.ZERO);
assertEquals(0, times.length);
}
@@ -120,7 +120,7 @@ void calculateCumulativeTimes_multipleStops_timesAreMonotonic() {
OSLO_NORTHWEST
);
- Duration[] times = estimator.calculateCumulativeTimes(points);
+ Duration[] times = estimator.calculateCumulativeTimes(points, Duration.ZERO);
// Times should be strictly increasing
for (int i = 1; i < times.length; i++) {
@@ -257,7 +257,7 @@ void estimateDuration_longDistance_scalesCorrectly() {
void calculateCumulativeTimes_twoPoints_calculatesCorrectly() {
List points = List.of(OSLO_CENTER, OSLO_NORTH);
- Duration[] times = estimator.calculateCumulativeTimes(points);
+ Duration[] times = estimator.calculateCumulativeTimes(points, Duration.ZERO);
assertEquals(2, times.length);
assertEquals(Duration.ZERO, times[0]);
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/carpooling/util/GraphPathUtilsTest.java b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/util/GraphPathUtilsTest.java
new file mode 100644
index 00000000000..8984f7366e4
--- /dev/null
+++ b/application/src/ext-test/java/org/opentripplanner/ext/carpooling/util/GraphPathUtilsTest.java
@@ -0,0 +1,102 @@
+package org.opentripplanner.ext.carpooling.util;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.time.Duration;
+import org.junit.jupiter.api.Test;
+
+class GraphPathUtilsTest {
+
+ private static final Duration TEN_MINUTES = Duration.ofMinutes(10);
+ private static final Duration ONE_MINUTE = Duration.ofMinutes(1);
+
+ @Test
+ void calculateCumulativeDurations_sixSegmentsWithStopDelay() {
+ Duration[] segments = {
+ TEN_MINUTES,
+ TEN_MINUTES,
+ TEN_MINUTES,
+ TEN_MINUTES,
+ TEN_MINUTES,
+ TEN_MINUTES,
+ };
+
+ Duration[] result = GraphPathUtils.calculateCumulativeDurations(segments, ONE_MINUTE);
+
+ assertArrayEquals(
+ new Duration[] {
+ Duration.ofMinutes(0),
+ Duration.ofMinutes(10),
+ Duration.ofMinutes(21),
+ Duration.ofMinutes(32),
+ Duration.ofMinutes(43),
+ Duration.ofMinutes(54),
+ Duration.ofMinutes(65),
+ },
+ result
+ );
+ }
+
+ @Test
+ void calculateCumulativeDurations_noStopDelay() {
+ Duration[] segments = { TEN_MINUTES, TEN_MINUTES, TEN_MINUTES };
+
+ Duration[] result = GraphPathUtils.calculateCumulativeDurations(segments, Duration.ZERO);
+
+ assertArrayEquals(
+ new Duration[] {
+ Duration.ofMinutes(0),
+ Duration.ofMinutes(10),
+ Duration.ofMinutes(20),
+ Duration.ofMinutes(30),
+ },
+ result
+ );
+ }
+
+ @Test
+ void calculateCumulativeDurations_singleSegment_noStopDelayApplied() {
+ Duration[] segments = { TEN_MINUTES };
+
+ Duration[] result = GraphPathUtils.calculateCumulativeDurations(segments, ONE_MINUTE);
+
+ assertArrayEquals(new Duration[] { Duration.ZERO, TEN_MINUTES }, result);
+ }
+
+ @Test
+ void calculateCumulativeDurations_twoSegments_stopDelayOnlyAtSecondPoint() {
+ Duration[] segments = { TEN_MINUTES, TEN_MINUTES };
+
+ Duration[] result = GraphPathUtils.calculateCumulativeDurations(segments, ONE_MINUTE);
+
+ assertArrayEquals(
+ new Duration[] { Duration.ofMinutes(0), Duration.ofMinutes(10), Duration.ofMinutes(21) },
+ result
+ );
+ }
+
+ @Test
+ void calculateCumulativeDurations_noSegments() {
+ Duration[] segments = {};
+
+ Duration[] result = GraphPathUtils.calculateCumulativeDurations(segments, ONE_MINUTE);
+
+ assertArrayEquals(new Duration[] { Duration.ZERO }, result);
+ }
+
+ @Test
+ void calculateCumulativeDurations_varyingSegmentDurations() {
+ Duration[] segments = { Duration.ofMinutes(5), Duration.ofMinutes(15), Duration.ofMinutes(10) };
+
+ Duration[] result = GraphPathUtils.calculateCumulativeDurations(
+ segments,
+ Duration.ofMinutes(2)
+ );
+
+ assertEquals(Duration.ofMinutes(0), result[0]);
+ assertEquals(Duration.ofMinutes(5), result[1]);
+ assertEquals(Duration.ofMinutes(22), result[2]);
+ assertEquals(Duration.ofMinutes(34), result[3]);
+ }
+}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/FaresFilterTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/FaresFilterTest.java
index bb2e2014530..fc79e22ae73 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/FaresFilterTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/FaresFilterTest.java
@@ -1,8 +1,8 @@
package org.opentripplanner.ext.fares;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
import java.util.List;
import org.junit.jupiter.api.Test;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/GtfsFaresServiceTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/GtfsFaresServiceTest.java
index e534fe1404c..ff7db8c76ee 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/GtfsFaresServiceTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/GtfsFaresServiceTest.java
@@ -1,7 +1,7 @@
package org.opentripplanner.ext.fares.service.gtfs;
import static com.google.common.truth.Truth.assertThat;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import java.time.Duration;
import org.junit.jupiter.api.Test;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v1/custom/HighestFareInFreeTransferWindowFareServiceTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v1/custom/HighestFareInFreeTransferWindowFareServiceTest.java
index 5a275126330..0645a9530aa 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v1/custom/HighestFareInFreeTransferWindowFareServiceTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v1/custom/HighestFareInFreeTransferWindowFareServiceTest.java
@@ -2,8 +2,8 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.FEED_ID;
import java.time.Duration;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/AreasTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/AreasTest.java
index 5d4e9f8af03..244c9014a2e 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/AreasTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/AreasTest.java
@@ -1,8 +1,8 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
import com.google.common.collect.Multimaps;
import java.util.Map;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/CostedTransferAcrossNetworksTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/CostedTransferAcrossNetworksTest.java
index 194618bba6d..03bfaf576fc 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/CostedTransferAcrossNetworksTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/CostedTransferAcrossNetworksTest.java
@@ -6,6 +6,7 @@
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
+import org.opentripplanner.core.model.id.FeedScopedIdForTestFactory;
import org.opentripplanner.ext.fares.model.FareLegRule;
import org.opentripplanner.ext.fares.model.FareTestConstants;
import org.opentripplanner.ext.fares.model.FareTransferRule;
@@ -38,26 +39,26 @@ class CostedTransferAcrossNetworksTest implements PlanTestConstants, FareTestCon
List.of(
// transferring from A to A is free
FareTransferRule.of()
- .withId(TimetableRepositoryForTest.id("t1"))
+ .withId(FeedScopedIdForTestFactory.id("t1"))
.withFromLegGroup(LEG_GROUP_A)
.withToLegGroup(LEG_GROUP_A)
.build(),
// transferring from B to B is also free
FareTransferRule.of()
- .withId(TimetableRepositoryForTest.id("t2"))
+ .withId(FeedScopedIdForTestFactory.id("t2"))
.withFromLegGroup(LEG_GROUP_B)
.withToLegGroup(LEG_GROUP_B)
.build(),
// transferring from A to B costs one EUR
FareTransferRule.of()
- .withId(TimetableRepositoryForTest.id("t3"))
+ .withId(FeedScopedIdForTestFactory.id("t3"))
.withFromLegGroup(LEG_GROUP_A)
.withToLegGroup(LEG_GROUP_B)
.withFareProducts(TRANSFER_1)
.build(),
// transferring from B to A is free
FareTransferRule.of()
- .withId(TimetableRepositoryForTest.id("t4"))
+ .withId(FeedScopedIdForTestFactory.id("t4"))
.withFromLegGroup(LEG_GROUP_B)
.withToLegGroup(LEG_GROUP_A)
.build()
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/CostedTransferInNetworkTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/CostedTransferInNetworkTest.java
index 8330460cd39..0801c35b9a4 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/CostedTransferInNetworkTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/CostedTransferInNetworkTest.java
@@ -2,8 +2,8 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
import java.util.List;
import java.util.Set;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/DepartureToArrivalTimeLimitTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/DepartureToArrivalTimeLimitTest.java
index 783b6df108b..da80500a699 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/DepartureToArrivalTimeLimitTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/DepartureToArrivalTimeLimitTest.java
@@ -1,8 +1,8 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static com.google.common.truth.Truth.assertThat;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.utils.time.TimeUtils.time;
import java.time.Duration;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/DepartureToDepartureTimeLimitTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/DepartureToDepartureTimeLimitTest.java
index 57fc5b89ac4..56e8d3df822 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/DepartureToDepartureTimeLimitTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/DepartureToDepartureTimeLimitTest.java
@@ -1,8 +1,8 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static com.google.common.truth.Truth.assertThat;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.utils.time.TimeUtils.time;
import java.time.Duration;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FlexLegTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FlexLegTest.java
index 860edb4b7bc..5c9927b93a8 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FlexLegTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FlexLegTest.java
@@ -1,8 +1,8 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static com.google.common.truth.Truth.assertThat;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
import com.google.common.collect.ImmutableMultimap;
import java.util.List;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FreeTransferAcrossNetworksTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FreeTransferAcrossNetworksTest.java
index b814c074fa7..dfdb3ed2f13 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FreeTransferAcrossNetworksTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FreeTransferAcrossNetworksTest.java
@@ -1,8 +1,8 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static com.google.common.truth.Truth.assertThat;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
import java.util.List;
import org.junit.jupiter.api.Test;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FreeTransferInNetworkTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FreeTransferInNetworkTest.java
index 0f6ee6a5449..31b512727bf 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FreeTransferInNetworkTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FreeTransferInNetworkTest.java
@@ -2,8 +2,8 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.groupOfRoutes;
import java.util.List;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FreeTransferTimeLimitTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FreeTransferTimeLimitTest.java
index b21a0e28295..626e5c29604 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FreeTransferTimeLimitTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/FreeTransferTimeLimitTest.java
@@ -1,8 +1,8 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static com.google.common.truth.Truth.assertThat;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.utils.time.TimeUtils.time;
import java.time.Duration;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/GtfsFaresV2ServiceTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/GtfsFaresV2ServiceTest.java
index f292926ecbf..74139d94164 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/GtfsFaresV2ServiceTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/GtfsFaresV2ServiceTest.java
@@ -2,8 +2,8 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.model.plan.TestItineraryBuilder.newItinerary;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.FEED_ID;
import java.util.Set;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OnlyFromTimeframeMatcherTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OnlyFromTimeframeMatcherTest.java
index 46494d6f5e3..2bd9c448731 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OnlyFromTimeframeMatcherTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OnlyFromTimeframeMatcherTest.java
@@ -2,7 +2,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import com.google.common.collect.ImmutableMultimap;
import java.time.LocalTime;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OnlyToTimeframeMatcherTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OnlyToTimeframeMatcherTest.java
index 6eb4fe5fe8e..17e6c4ef648 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OnlyToTimeframeMatcherTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OnlyToTimeframeMatcherTest.java
@@ -2,7 +2,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import com.google.common.collect.ImmutableMultimap;
import java.util.List;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OverlappingTimeframeTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OverlappingTimeframeTest.java
index 1f0c69c9604..39f69869b16 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OverlappingTimeframeTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OverlappingTimeframeTest.java
@@ -1,7 +1,7 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static com.google.common.truth.Truth.assertThat;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import java.time.LocalDate;
import java.util.List;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OverlappingTimeframesWithPriorityTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OverlappingTimeframesWithPriorityTest.java
index 0324be08e10..44b57d05280 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OverlappingTimeframesWithPriorityTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/OverlappingTimeframesWithPriorityTest.java
@@ -1,7 +1,7 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static com.google.common.truth.Truth.assertThat;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import java.time.LocalDate;
import java.util.List;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/SameGroupIdPriorityTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/SameGroupIdPriorityTest.java
index 1ebb383b359..3bd8803e352 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/SameGroupIdPriorityTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/SameGroupIdPriorityTest.java
@@ -1,7 +1,7 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static com.google.common.truth.Truth.assertThat;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TimeframeMatcherTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TimeframeMatcherTest.java
index ec19683355f..9d48c8a7721 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TimeframeMatcherTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TimeframeMatcherTest.java
@@ -2,7 +2,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import com.google.common.collect.ImmutableMultimap;
import java.util.List;
@@ -10,10 +10,10 @@
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.opentripplanner.core.model.id.FeedScopedId;
+import org.opentripplanner.core.model.id.FeedScopedIdForTestFactory;
import org.opentripplanner.ext.fares.model.FareLegRule;
import org.opentripplanner.ext.fares.model.FareTestConstants;
import org.opentripplanner.model.plan.TestTransitLeg;
-import org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory;
class TimeframeMatcherTest implements FareTestConstants {
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TimeframeTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TimeframeTest.java
index d7362cd62b0..243ce47d1c6 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TimeframeTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TimeframeTest.java
@@ -1,7 +1,7 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static com.google.common.truth.Truth.assertThat;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import java.time.LocalDate;
import java.util.List;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TransferCountTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TransferCountTest.java
index e0912de9bce..5a9d1856dbb 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TransferCountTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TransferCountTest.java
@@ -1,7 +1,7 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static com.google.common.truth.Truth.assertThat;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import java.util.List;
import org.junit.jupiter.api.Test;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TransferCountWithTimeLimitTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TransferCountWithTimeLimitTest.java
index d4843095991..94ce64c5713 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TransferCountWithTimeLimitTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/TransferCountWithTimeLimitTest.java
@@ -1,7 +1,7 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static com.google.common.truth.Truth.assertThat;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import java.time.Duration;
import java.util.List;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/WildcardNetworkTransferTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/WildcardNetworkTransferTest.java
index 893a46c3fb0..83cc4e8e460 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/WildcardNetworkTransferTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/WildcardNetworkTransferTest.java
@@ -1,7 +1,7 @@
package org.opentripplanner.ext.fares.service.gtfs.v2;
import static com.google.common.truth.Truth.assertThat;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import java.time.Duration;
import org.junit.jupiter.api.Test;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/custom/OregonHopFareFactoryTest.java b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/custom/OregonHopFareFactoryTest.java
index 3ddb47ff5c9..4dc5017193c 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/custom/OregonHopFareFactoryTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/fares/service/gtfs/v2/custom/OregonHopFareFactoryTest.java
@@ -1,13 +1,13 @@
package org.opentripplanner.ext.fares.service.gtfs.v2.custom;
import static com.google.common.truth.Truth.assertThat;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.ext.fares.service.gtfs.v2.custom.OregonHopFareFactory.ADULT_REGIONAL_SINGLE_RIDE;
import static org.opentripplanner.ext.fares.service.gtfs.v2.custom.OregonHopFareFactory.CATEGORY_ADULT;
import static org.opentripplanner.ext.fares.service.gtfs.v2.custom.OregonHopFareFactory.HOP_FASTPASS;
import static org.opentripplanner.ext.fares.service.gtfs.v2.custom.OregonHopFareFactory.LG_CTRAN_REGIONAL;
import static org.opentripplanner.ext.fares.service.gtfs.v2.custom.OregonHopFareFactory.LG_TRIMET_TRIMET;
import static org.opentripplanner.ext.fares.service.gtfs.v2.custom.OregonHopFareFactory.TRIMET_ADULT_SINGLE_RIDE;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
import java.util.List;
import java.util.Set;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIndexTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIndexTest.java
index f0ee672b247..d8c923cd437 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIndexTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIndexTest.java
@@ -1,9 +1,12 @@
package org.opentripplanner.ext.flex;
+import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.opentripplanner.ext.flex.FlexStopTimesForTest.area;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.model.FlexStopTimesFactory.area;
+import static org.opentripplanner.model.FlexStopTimesFactory.groupStop;
+import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.trip;
import java.time.LocalDate;
import java.util.Collection;
@@ -13,17 +16,21 @@
import org.opentripplanner.ext.flex.trip.UnscheduledTrip;
import org.opentripplanner.model.calendar.CalendarServiceData;
import org.opentripplanner.transit.model._data.TimetableRepositoryForTest;
+import org.opentripplanner.transit.model.network.Route;
+import org.opentripplanner.transit.model.site.GroupStop;
import org.opentripplanner.transit.model.timetable.Trip;
import org.opentripplanner.transit.service.TimetableRepository;
class FlexIndexTest {
+ public static final Route ROUTE_2 = TimetableRepositoryForTest.route("r2").build();
+
@Test
void testFlexTripSpanningMidnight() {
TimetableRepository repo = new TimetableRepository();
FeedScopedId serviceId = id("S1");
- Trip trip = TimetableRepositoryForTest.trip("T1").withServiceId(serviceId).build();
+ Trip trip = trip("T1").withServiceId(serviceId).build();
UnscheduledTrip flexTrip = UnscheduledTrip.of(id("FT1"))
.withTrip(trip)
@@ -65,7 +72,7 @@ void testFlexTripSpanningMidnight() {
void testFlexTripStartingAfterMidnight() {
TimetableRepository repo = new TimetableRepository();
FeedScopedId serviceId = id("S2");
- Trip trip = TimetableRepositoryForTest.trip("T2").withServiceId(serviceId).build();
+ Trip trip = trip("T2").withServiceId(serviceId).build();
UnscheduledTrip flexTrip = UnscheduledTrip.of(id("FT2"))
.withTrip(trip)
@@ -90,4 +97,50 @@ void testFlexTripStartingAfterMidnight() {
assertEquals(1, tripsOnNextDay.size(), "Should have 1 trip on next day");
assertEquals(serviceDate, tripsOnNextDay.iterator().next().serviceDate());
}
+
+ @Test
+ void routesAtArea() {
+ var repo = new TimetableRepository();
+
+ var st1 = area("10:00", "12:00");
+ var st2 = area("14:00", "16:00");
+
+ var flexTrip = UnscheduledTrip.of(id("T2"))
+ .withTrip(trip("T2").withRoute(ROUTE_2).build())
+ .withStopTimes(List.of(st1, st2))
+ .build();
+
+ repo.addFlexTrip(flexTrip.getId(), flexTrip);
+
+ var index = new FlexIndex(repo);
+
+ assertThat(index.findRoutes(st1.getStop())).containsExactly(ROUTE_2);
+ assertThat(index.findRoutes(st2.getStop())).containsExactly(ROUTE_2);
+ }
+
+ @Test
+ void routesAtGroup() {
+ var repo = new TimetableRepository();
+
+ var st1 = groupStop("10:00", "12:00");
+ var st2 = groupStop("14:00", "16:00");
+
+ var flexTrip = UnscheduledTrip.of(id("T2"))
+ .withTrip(trip("T2").withRoute(ROUTE_2).build())
+ .withStopTimes(List.of(st1, st2))
+ .build();
+
+ repo.addFlexTrip(flexTrip.getId(), flexTrip);
+
+ var index = new FlexIndex(repo);
+
+ var groupStop = (GroupStop) st1.getStop();
+ assertThat(groupStop.getChildLocations()).isNotEmpty();
+ groupStop
+ .getChildLocations()
+ .forEach(child -> {
+ assertThat(index.findRoutes(child)).containsExactly(ROUTE_2);
+ assertThat(index.findRoutes(child)).containsExactly(ROUTE_2);
+ });
+ }
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java
index 3f7fa497cf2..939286c451c 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexIntegrationTest.java
@@ -18,6 +18,7 @@
import org.junit.jupiter.api.Test;
import org.opentripplanner.TestOtpModel;
import org.opentripplanner.TestServerContext;
+import org.opentripplanner.core.model.id.FeedScopedId;
import org.opentripplanner.core.model.time.LocalDateInterval;
import org.opentripplanner.framework.application.OTPFeature;
import org.opentripplanner.graph_builder.issue.api.DataImportIssueStore;
@@ -123,7 +124,7 @@ void shouldReturnARouteTransferringFromBusToFlex() {
@Test
void shouldReturnARouteWithTwoTransfers() {
- var from = GenericLocation.fromStopId("ALEX DR@ALEX WAY", "MARTA", "97266");
+ var from = GenericLocation.fromStopId(new FeedScopedId("MARTA", "97266"), "ALEX DR@ALEX WAY");
var to = GenericLocation.fromCoordinate(33.86701256815635, -84.61787939071655);
var itin = getItinerary(from, to, 3);
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexTransferIndexTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexTransferIndexTest.java
index 9b1d51c5fac..12f5c1ac799 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexTransferIndexTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexTransferIndexTest.java
@@ -2,7 +2,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexibleTransitLegTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexibleTransitLegTest.java
index cd843b3d18e..f836fb36ce7 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexibleTransitLegTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/FlexibleTransitLegTest.java
@@ -3,8 +3,8 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.ext.fares.model.FareModelForTest.ANY_FARE_OFFER;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
import java.time.Duration;
import java.time.LocalDate;
@@ -26,8 +26,8 @@ class FlexibleTransitLegTest implements PlanTestConstants {
private static final FlexTripEdge EDGE = new FlexTripEdge(
StreetModelForTest.intersectionVertex(1, 1),
StreetModelForTest.intersectionVertex(2, 2),
- A.stop,
- B.stop,
+ A.stop.getId(),
+ B.stop.getId(),
null,
1,
2,
@@ -48,6 +48,8 @@ void listsAreInitialized() {
.withStartTime(START_TIME)
.withEndTime(END_TIME)
.withFlexTripEdge(EDGE)
+ .withFromStop(A.stop)
+ .withToStop(B.stop)
.build();
assertNotNull(leg.fareOffers());
assertNotNull(leg.listTransitAlerts());
@@ -61,6 +63,8 @@ void everythingIsNonNull() {
assertThrows(expectedType, () ->
new FlexibleTransitLegBuilder().withFlexTripEdge(null).build()
);
+ assertThrows(expectedType, () -> new FlexibleTransitLegBuilder().withFromStop(null).build());
+ assertThrows(expectedType, () -> new FlexibleTransitLegBuilder().withToStop(null).build());
assertThrows(expectedType, () -> new FlexibleTransitLegBuilder().withAlerts(null).build());
assertThrows(expectedType, () ->
new FlexibleTransitLegBuilder().withFareProducts(null).build()
@@ -73,6 +77,8 @@ void copyOf() {
.withStartTime(START_TIME)
.withEndTime(END_TIME)
.withFlexTripEdge(EDGE)
+ .withFromStop(A.stop)
+ .withToStop(B.stop)
.withFareProducts(List.of(ANY_FARE_OFFER))
.withAlerts(Set.of(ALERT))
.withEmissionPerPerson(EMISSION)
@@ -94,6 +100,8 @@ void timeShift() {
.withStartTime(START_TIME)
.withEndTime(END_TIME)
.withFlexTripEdge(EDGE)
+ .withFromStop(A.stop)
+ .withToStop(B.stop)
.withFareProducts(List.of(ANY_FARE_OFFER))
.withAlerts(Set.of(ALERT))
.build();
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/filter/FilterMapperTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/filter/FilterMapperTest.java
index e2ebad57302..6836a2ce493 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/filter/FilterMapperTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/filter/FilterMapperTest.java
@@ -1,7 +1,7 @@
package org.opentripplanner.ext.flex.filter;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import java.util.List;
import org.junit.jupiter.api.Test;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculatorTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculatorTest.java
index d35ee6436d0..391da6b3a94 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculatorTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/flexpathcalculator/ScheduledFlexPathCalculatorTest.java
@@ -1,11 +1,11 @@
package org.opentripplanner.ext.flex.flexpathcalculator;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.opentripplanner.ext.flex.FlexStopTimesForTest.area;
-import static org.opentripplanner.ext.flex.FlexStopTimesForTest.regularStop;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.model.FlexStopTimesFactory.area;
+import static org.opentripplanner.model.FlexStopTimesFactory.regularStop;
import static org.opentripplanner.street.model.StreetModelForTest.V1;
import static org.opentripplanner.street.model.StreetModelForTest.V2;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
import java.time.Duration;
import java.util.List;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/template/ClosestTripTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/template/ClosestTripTest.java
index d0213119f0a..4cfb9d8ba0c 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/template/ClosestTripTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/template/ClosestTripTest.java
@@ -2,8 +2,8 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.opentripplanner.ext.flex.FlexStopTimesForTest.area;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.model.FlexStopTimesFactory.area;
import java.time.Instant;
import java.time.LocalDate;
@@ -61,7 +61,7 @@ public Collection getTransfersToStop(StopLocation stop) {
}
@Override
- public Collection> getFlexTripsByStop(StopLocation stopLocation) {
+ public Collection> getFlexTripsByStopId(FeedScopedId stopLocationId) {
return List.of(FLEX_TRIP);
}
@@ -96,7 +96,7 @@ void filter() {
private static Collection closestTrips(Matcher matcher) {
return ClosestTrip.of(
ADAPTER,
- List.of(new NearbyStop(STOP, 100, List.of(), null)),
+ List.of(new NearbyStop(STOP.getId(), 100, List.of(), null)),
matcher,
List.of(FSD),
true
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/template/FlexTemplateFactoryTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/template/FlexTemplateFactoryTest.java
index 448b2adc5b2..7a1e1c397c2 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/template/FlexTemplateFactoryTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/template/FlexTemplateFactoryTest.java
@@ -330,7 +330,7 @@ void testCreateEgressTemplateForScheduledDeviatedTrip() {
private static NearbyStop nearbyStop(StopLocation transferPoint) {
var id = "NearbyStop:" + transferPoint.getId().getId();
return new NearbyStop(
- transferPoint,
+ transferPoint.getId(),
0,
List.of(),
new State(
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/FlexTripsMapperTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/FlexTripsMapperTest.java
index a236b87e967..a23cb83a679 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/FlexTripsMapperTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/FlexTripsMapperTest.java
@@ -2,8 +2,8 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
-import static org.opentripplanner.ext.flex.FlexStopTimesForTest.area;
import static org.opentripplanner.graph_builder.issue.api.DataImportIssueStore.NOOP;
+import static org.opentripplanner.model.FlexStopTimesFactory.area;
import java.util.List;
import org.junit.jupiter.api.Test;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripIntegrationTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripIntegrationTest.java
index 2a6b6783fee..f35d159c0bb 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripIntegrationTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripIntegrationTest.java
@@ -101,11 +101,13 @@ void flexTripInTransitMode() {
);
// from zone 3 to zone 2
- var from = GenericLocation.fromStopId("Transfer Point for Route 30", feedId, "cujv");
+ var from = GenericLocation.fromStopId(
+ new FeedScopedId(feedId, "cujv"),
+ "Transfer Point for Route 30"
+ );
var to = GenericLocation.fromStopId(
- "Zone 1 - PUBLIX Super Market,Zone 1 Collection Point",
- feedId,
- "yz85"
+ new FeedScopedId(feedId, "yz85"),
+ "Zone 1 - PUBLIX Super Market,Zone 1 Collection Point"
);
var itineraries = getItineraries(from, to, serverContext);
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java
index 43f7c3e1451..f7abcccf82d 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/ScheduledDeviatedTripTest.java
@@ -3,11 +3,11 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.opentripplanner.ext.flex.FlexStopTimesForTest.area;
-import static org.opentripplanner.ext.flex.FlexStopTimesForTest.areaWithContinuousStopping;
-import static org.opentripplanner.ext.flex.FlexStopTimesForTest.regularStop;
-import static org.opentripplanner.ext.flex.FlexStopTimesForTest.regularStopWithContinuousStopping;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.model.FlexStopTimesFactory.area;
+import static org.opentripplanner.model.FlexStopTimesFactory.areaWithContinuousStopping;
+import static org.opentripplanner.model.FlexStopTimesFactory.regularStop;
+import static org.opentripplanner.model.FlexStopTimesFactory.regularStopWithContinuousStopping;
import java.util.List;
import org.junit.jupiter.api.Test;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledDrivingDurationTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledDrivingDurationTest.java
index 61ea07c20bd..4f78826f347 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledDrivingDurationTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledDrivingDurationTest.java
@@ -1,17 +1,17 @@
package org.opentripplanner.ext.flex.trip;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.street.model.StreetModelForTest.V1;
import static org.opentripplanner.street.model.StreetModelForTest.V2;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
import java.time.Duration;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.opentripplanner._support.geometry.LineStrings;
-import org.opentripplanner.ext.flex.FlexStopTimesForTest;
import org.opentripplanner.ext.flex.flexpathcalculator.FlexPath;
import org.opentripplanner.ext.flex.flexpathcalculator.FlexPathCalculator;
+import org.opentripplanner.model.FlexStopTimesFactory;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.routing.api.request.framework.TimePenalty;
@@ -23,7 +23,7 @@ class UnscheduledDrivingDurationTest {
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");
+ private static final StopTime STOP_TIME = FlexStopTimesFactory.area("10:00", "18:00");
@Test
void noPenalty() {
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java
index d5760946a8e..65d3b10e421 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/flex/trip/UnscheduledTripTest.java
@@ -3,11 +3,11 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.ext.flex.trip.UnscheduledTrip.isUnscheduledTrip;
import static org.opentripplanner.ext.flex.trip.UnscheduledTripTest.TestCase.tc;
import static org.opentripplanner.model.PickDrop.NONE;
import static org.opentripplanner.model.StopTime.MISSING_VALUE;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
import java.util.Collections;
import java.util.List;
@@ -17,7 +17,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
-import org.opentripplanner.ext.flex.FlexStopTimesForTest;
+import org.opentripplanner.model.FlexStopTimesFactory;
import org.opentripplanner.model.PickDrop;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.transit.model._data.TimetableRepositoryForTest;
@@ -46,18 +46,18 @@ class UnscheduledTripTest {
@Nested
class IsUnscheduledTrip {
- private static final StopTime SCHEDULED_STOP = FlexStopTimesForTest.regularStop("10:00");
- private static final StopTime UNSCHEDULED_STOP = FlexStopTimesForTest.area("10:10", "10:20");
+ private static final StopTime SCHEDULED_STOP = FlexStopTimesFactory.regularStop("10:00");
+ private static final StopTime UNSCHEDULED_STOP = FlexStopTimesFactory.area("10:10", "10:20");
private static final StopTime CONTINUOUS_PICKUP_STOP =
- FlexStopTimesForTest.regularStopWithContinuousPickup("10:30");
+ FlexStopTimesFactory.regularStopWithContinuousPickup("10:30");
private static final StopTime CONTINUOUS_DROP_OFF_STOP =
- FlexStopTimesForTest.regularStopWithContinuousDropOff("10:40");
+ FlexStopTimesFactory.regularStopWithContinuousDropOff("10:40");
// disallowed by the GTFS spec
private static final StopTime FLEX_AND_CONTINUOUS_PICKUP_STOP =
- FlexStopTimesForTest.areaWithContinuousPickup("10:50");
+ FlexStopTimesFactory.areaWithContinuousPickup("10:50");
private static final StopTime FLEX_AND_CONTINUOUS_DROP_OFF_STOP =
- FlexStopTimesForTest.areaWithContinuousDropOff("11:00");
+ FlexStopTimesFactory.areaWithContinuousDropOff("11:00");
static List> notUnscheduled() {
return List.of(
@@ -103,8 +103,8 @@ void isUnscheduled(List stopTimes) {
@Test
void testMaxSpanDays() {
var stopTimes = List.of(
- FlexStopTimesForTest.area("10:10", "14:10"),
- FlexStopTimesForTest.area("11:10", "15:10")
+ FlexStopTimesFactory.area("10:10", "14:10"),
+ FlexStopTimesFactory.area("11:10", "15:10")
);
var trip = UnscheduledTrip.of(id("1")).withStopTimes(stopTimes).build();
@@ -114,8 +114,8 @@ void testMaxSpanDays() {
@Test
void testMaxSpanDaysOvernight() {
var stopTimes = List.of(
- FlexStopTimesForTest.area("10:10", "14:10"),
- FlexStopTimesForTest.area("21:10", "26:10")
+ FlexStopTimesFactory.area("10:10", "14:10"),
+ FlexStopTimesFactory.area("21:10", "26:10")
);
var trip = UnscheduledTrip.of(id("1")).withStopTimes(stopTimes).build();
assertEquals(1, trip.maxSpanDays());
@@ -124,8 +124,8 @@ void testMaxSpanDaysOvernight() {
@Test
void testMaxSpanDaysNextDay() {
var stopTimes = List.of(
- FlexStopTimesForTest.area("24:00", "26:00"),
- FlexStopTimesForTest.area("24:00", "26:00")
+ FlexStopTimesFactory.area("24:00", "26:00"),
+ FlexStopTimesFactory.area("24:00", "26:00")
);
var trip = UnscheduledTrip.of(id("1")).withStopTimes(stopTimes).build();
assertEquals(1, trip.maxSpanDays());
@@ -566,11 +566,11 @@ void boardingAlighting() {
.build()
.trip();
- assertTrue(trip.isBoardingPossible(AREA_STOP1));
- assertFalse(trip.isAlightingPossible(AREA_STOP1));
+ assertTrue(trip.isBoardingPossible(AREA_STOP1.getId()));
+ assertFalse(trip.isAlightingPossible(AREA_STOP1.getId()));
- assertFalse(trip.isBoardingPossible(AREA_STOP2));
- assertTrue(trip.isAlightingPossible(AREA_STOP2));
+ assertFalse(trip.isBoardingPossible(AREA_STOP2.getId()));
+ assertTrue(trip.isAlightingPossible(AREA_STOP2.getId()));
}
private static String timeToString(int time) {
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/geocoder/LuceneIndexTest.java b/application/src/ext-test/java/org/opentripplanner/ext/geocoder/LuceneIndexTest.java
index 33f8c29f948..54ee43e8f85 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/geocoder/LuceneIndexTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/geocoder/LuceneIndexTest.java
@@ -2,7 +2,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.transit.model.basic.TransitMode.BUS;
import static org.opentripplanner.transit.model.basic.TransitMode.FERRY;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/geocoder/SerializationTest.java b/application/src/ext-test/java/org/opentripplanner/ext/geocoder/SerializationTest.java
index 220566888ac..bedfd6c7f96 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/geocoder/SerializationTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/geocoder/SerializationTest.java
@@ -1,8 +1,8 @@
package org.opentripplanner.ext.geocoder;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.ext.geocoder.StopCluster.LocationType.STOP;
import static org.opentripplanner.test.support.JsonAssertions.assertEqualJson;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
import java.util.List;
import java.util.Set;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/ojp/mapping/RouteRequestMapperTest.java b/application/src/ext-test/java/org/opentripplanner/ext/ojp/mapping/RouteRequestMapperTest.java
index 069a059cacf..73dd4b4828e 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/ojp/mapping/RouteRequestMapperTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/ojp/mapping/RouteRequestMapperTest.java
@@ -4,7 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import de.vdv.ojp20.LineDirectionFilterStructure;
import de.vdv.ojp20.ModeAndModeOfOperationFilterStructure;
@@ -45,10 +45,10 @@ void mapWithCoordinates() {
var routeRequest = mapper.map(tripRequest);
- assertEquals(47.3769, routeRequest.from().lat);
- assertEquals(8.5417, routeRequest.from().lng);
- assertEquals(46.9480, routeRequest.to().lat);
- assertEquals(7.4474, routeRequest.to().lng);
+ assertEquals(47.3769, routeRequest.from().wgsCoordinate().latitude());
+ assertEquals(8.5417, routeRequest.from().wgsCoordinate().longitude());
+ assertEquals(46.9480, routeRequest.to().wgsCoordinate().latitude());
+ assertEquals(7.4474, routeRequest.to().wgsCoordinate().longitude());
assertTransitFilters("[ALLOW_ALL]", routeRequest);
assertEquals(StreetMode.WALK, routeRequest.journey().access().mode());
@@ -62,8 +62,8 @@ void mapWithStopPlaceRef() {
var routeRequest = mapper.map(tripRequest);
- assertEquals(id("stop1"), routeRequest.from().stopId);
- assertEquals(id("stop2"), routeRequest.to().stopId);
+ assertEquals(id("stop1"), routeRequest.from().stopId());
+ assertEquals(id("stop2"), routeRequest.to().stopId());
}
@Test
@@ -75,8 +75,8 @@ void mapWithStopPointRef() {
var routeRequest = mapper.map(tripRequest);
assertNotNull(routeRequest.to());
- assertEquals(id("stopPoint1"), routeRequest.from().stopId);
- assertEquals(id("stopPoint2"), routeRequest.to().stopId);
+ assertEquals(id("stopPoint1"), routeRequest.from().stopId());
+ assertEquals(id("stopPoint2"), routeRequest.to().stopId());
}
@Test
@@ -87,9 +87,9 @@ void mapWithMixedLocationTypes() {
var routeRequest = mapper.map(tripRequest);
- assertEquals(47.3769, routeRequest.from().lat);
- assertEquals(8.5417, routeRequest.from().lng);
- assertEquals(id("stop1"), routeRequest.to().stopId);
+ assertEquals(47.3769, routeRequest.from().wgsCoordinate().latitude());
+ assertEquals(8.5417, routeRequest.from().wgsCoordinate().longitude());
+ assertEquals(id("stop1"), routeRequest.to().stopId());
}
@Test
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/ojp/resource/OjpMapperTest.java b/application/src/ext-test/java/org/opentripplanner/ext/ojp/resource/OjpMapperTest.java
index 2846522b2e0..4fc06b20fff 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/ojp/resource/OjpMapperTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/ojp/resource/OjpMapperTest.java
@@ -1,7 +1,7 @@
package org.opentripplanner.ext.ojp.resource;
import static jakarta.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import de.vdv.ojp20.OJP;
import jakarta.xml.bind.JAXBContext;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/ojp/service/CallAtStopServiceTest.java b/application/src/ext-test/java/org/opentripplanner/ext/ojp/service/CallAtStopServiceTest.java
index e70c0dd21db..7efe12961e1 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/ojp/service/CallAtStopServiceTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/ojp/service/CallAtStopServiceTest.java
@@ -3,7 +3,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import java.time.Duration;
import java.util.List;
@@ -102,7 +102,12 @@ void coordinates() {
@Override
public List findClosestStops(Coordinate coordinate, double radiusMeters) {
return List.of(
- new NearbyStop(STOP_A, 100, List.of(), TestStateBuilder.ofWalking().streetEdge().build())
+ new NearbyStop(
+ STOP_A.getId(),
+ 100,
+ List.of(),
+ TestStateBuilder.ofWalking().streetEdge().build()
+ )
);
}
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/ojp/service/StopEventParamsMapperTest.java b/application/src/ext-test/java/org/opentripplanner/ext/ojp/service/StopEventParamsMapperTest.java
index 4511aca913f..fc9d47aae5a 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/ojp/service/StopEventParamsMapperTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/ojp/service/StopEventParamsMapperTest.java
@@ -3,7 +3,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.opentripplanner.transit.model._data.FeedScopedIdForTestFactory.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.transit.model.basic.TransitMode.BUS;
import static org.opentripplanner.transit.model.basic.TransitMode.FERRY;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationRepositoryTest.java b/application/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationRepositoryTest.java
index 7bf87b4c89c..98cc695228e 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationRepositoryTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/internal/DefaultStopConsolidationRepositoryTest.java
@@ -2,7 +2,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import java.util.List;
import org.junit.jupiter.api.Test;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopLegBuilderTest.java b/application/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopLegBuilderTest.java
index b3a983850bd..288f568b8e5 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopLegBuilderTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/stopconsolidation/model/ConsolidatedStopLegBuilderTest.java
@@ -1,8 +1,8 @@
package org.opentripplanner.ext.stopconsolidation.model;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import static org.opentripplanner.ext.fares.model.FareModelForTest.ANY_FARE_OFFER;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
import java.time.ZonedDateTime;
import java.util.List;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stations/DigitransitStationPropertyMapperTest.java b/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stations/DigitransitStationPropertyMapperTest.java
index 3c9a3fb5a59..00eb659805c 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stations/DigitransitStationPropertyMapperTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stations/DigitransitStationPropertyMapperTest.java
@@ -1,7 +1,7 @@
package org.opentripplanner.ext.vectortiles.layers.stations;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import java.util.HashMap;
import java.util.Locale;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerTest.java b/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerTest.java
index faf6f8766df..eb10dc6fa03 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/stops/StopsLayerTest.java
@@ -1,7 +1,7 @@
package org.opentripplanner.ext.vectortiles.layers.stops;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.opentripplanner.transit.model._data.TimetableRepositoryForTest.id;
+import static org.opentripplanner.core.model.id.FeedScopedIdForTestFactory.id;
import java.util.HashMap;
import java.util.Locale;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingGroupsLayerTest.java b/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingGroupsLayerTest.java
index 0f8d40c7d76..8e3dec8266a 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingGroupsLayerTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingGroupsLayerTest.java
@@ -16,6 +16,7 @@
import org.opentripplanner.core.model.i18n.NonLocalizedString;
import org.opentripplanner.core.model.i18n.TranslatedString;
import org.opentripplanner.core.model.id.FeedScopedId;
+import org.opentripplanner.core.model.id.FeedScopedIdForTestFactory;
import org.opentripplanner.ext.vectortiles.VectorTilesResource;
import org.opentripplanner.inspector.vector.KeyValue;
import org.opentripplanner.inspector.vector.LayerParameters;
@@ -28,11 +29,10 @@
import org.opentripplanner.service.vehicleparking.model.VehicleParkingState;
import org.opentripplanner.standalone.config.routerconfig.VectorTileConfig;
import org.opentripplanner.street.geometry.WgsCoordinate;
-import org.opentripplanner.transit.model._data.TimetableRepositoryForTest;
public class VehicleParkingGroupsLayerTest {
- private static final FeedScopedId ID = TimetableRepositoryForTest.id("id");
+ private static final FeedScopedId ID = FeedScopedIdForTestFactory.id("id");
private VehicleParkingGroup vehicleParkingGroup;
private VehicleParking vehicleParking;
diff --git a/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java b/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java
index de4d16f799d..994373288ed 100644
--- a/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java
+++ b/application/src/ext-test/java/org/opentripplanner/ext/vectortiles/layers/vehicleparkings/VehicleParkingsLayerTest.java
@@ -22,6 +22,7 @@
import org.locationtech.jts.geom.Geometry;
import org.opentripplanner.core.model.i18n.TranslatedString;
import org.opentripplanner.core.model.id.FeedScopedId;
+import org.opentripplanner.core.model.id.FeedScopedIdForTestFactory;
import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingRepository;
import org.opentripplanner.service.vehicleparking.internal.DefaultVehicleParkingService;
import org.opentripplanner.service.vehicleparking.model.VehicleParking;
@@ -30,12 +31,11 @@
import org.opentripplanner.standalone.config.routerconfig.VectorTileConfig;
import org.opentripplanner.street.geometry.WgsCoordinate;
import org.opentripplanner.street.model.openinghours.OpeningHoursCalendarService;
-import org.opentripplanner.transit.model._data.TimetableRepositoryForTest;
import org.opentripplanner.transit.model.framework.Deduplicator;
public class VehicleParkingsLayerTest {
- private static final FeedScopedId ID = TimetableRepositoryForTest.id("id");
+ private static final FeedScopedId ID = FeedScopedIdForTestFactory.id("id");
private VehicleParking vehicleParking;
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/Architecture.md b/application/src/ext/java/org/opentripplanner/ext/carpooling/Architecture.md
index 7ffa3b64ee7..3baadb83d06 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/Architecture.md
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/Architecture.md
@@ -18,15 +18,12 @@ org.opentripplanner.ext.carpooling/
│ └── CarpoolStreetRouter # Street routing for carpooling
├── filter/ # Trip pre-filtering
│ ├── TripFilter # Filter interface
-│ ├── CapacityFilter # Checks available capacity
│ ├── TimeBasedFilter # Time window filtering
-│ ├── DistanceBasedFilter # Geographic distance checks
-│ └── DirectionalCompatibilityFilter # Directional alignment
+│ └── DistanceBasedFilter # Geographic distance checks
├── constraints/ # Post-routing constraints
│ └── PassengerDelayConstraints # Protects existing passengers
├── util/ # Utilities
-│ ├── BeelineEstimator # Fast travel time estimates
-│ └── DirectionalCalculator # Geographic bearing calculations
+│ └── BeelineEstimator # Fast travel time estimates
├── updater/ # Real-time updates
│ ├── SiriETCarpoolingUpdater # SIRI-ET integration
│ └── CarpoolSiriMapper # Maps SIRI to domain model
@@ -44,7 +41,6 @@ Fast pre-screening to eliminate incompatible trips:
- **Capacity Filter**: Checks if any seats are available
- **Time-Based Filter**: Ensures departure time compatibility
- **Distance-Based Filter**: Validates pickup/dropoff are within 50km of driver's route
-- **Directional Compatibility Filter**: Verifies passenger direction aligns with trip route
### 2. Routing Phase
Optimal insertion point calculation:
@@ -55,7 +51,6 @@ Optimal insertion point calculation:
### 3. Constraint Validation
- **Capacity constraints**: Ensures vehicle capacity is not exceeded
-- **Directional constraints**: Prevents backtracking (90° tolerance)
- **Passenger delay constraints**: Protects existing passengers (max 5 minutes additional delay)
- **Deviation budget**: Respects driver's maximum acceptable detour time
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/CarpoolingService.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/CarpoolingService.java
index 995ae8bb286..1ea1a7b4b6a 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/CarpoolingService.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/CarpoolingService.java
@@ -3,11 +3,11 @@
import java.time.ZonedDateTime;
import java.util.List;
import org.opentripplanner.ext.carpooling.routing.CarpoolAccessEgress;
-import org.opentripplanner.graph_builder.module.nearbystops.StopResolver;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.routing.algorithm.raptoradapter.router.street.AccessEgressType;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.routing.api.request.request.StreetRequest;
+import org.opentripplanner.routing.graphfinder.TransitServiceResolver;
import org.opentripplanner.routing.linking.LinkingContext;
/**
@@ -24,8 +24,7 @@ public interface CarpoolingService {
*
* @param request the routing request containing passenger origin, destination, and preferences
* @param linkingContext linking context with pre-linked vertices for the request
- * @return list of carpool itineraries, sorted by quality (additional travel time), may be empty
- * if no compatible trips found. Results are limited to avoid overwhelming users.
+ * @return list of carpool itineraries, may be empty if no compatible trips found
* @throws IllegalArgumentException if request is null
*/
List routeDirect(RouteRequest request, LinkingContext linkingContext);
@@ -34,7 +33,7 @@ List routeAccessEgress(
RouteRequest request,
StreetRequest streetRequest,
AccessEgressType accessOrEgress,
- StopResolver stopResolver,
+ TransitServiceResolver transitServiceResolver,
LinkingContext linkingContext,
ZonedDateTime transitSearchTimeZero
);
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/README.md b/application/src/ext/java/org/opentripplanner/ext/carpooling/README.md
index dff8c5d4461..6689cc46afc 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/README.md
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/README.md
@@ -35,14 +35,12 @@ The carpooling extension enables OpenTripPlanner to find carpool trip options by
│ 1. Filter Phase (FilterChain) │
│ - Capacity check │
│ - Time window check │
-│ - Direction check │
│ - Distance check │
│ │
│ 2. Insertion Phase │
│ 2a. Position Pre-screening │
│ (InsertionPositionFinder) │
│ - Capacity check │
-│ - Directional check │
│ - Beeline delay heuristic │
│ │
│ 2b. Routing & Selection │
@@ -54,7 +52,6 @@ The carpooling extension enables OpenTripPlanner to find carpool trip options by
│ │
│ 3. Validation Phase (CompositeValidator) │
│ - Capacity timeline check │
-│ - Directional consistency check │
│ - Deviation budget check │
│ │
└────────┬───────────────────────────────────┘
@@ -84,9 +81,7 @@ org.opentripplanner.ext.carpooling/
│
├── filter/ # Pre-screening filters
│ ├── FilterChain.java # Composite filter
-│ ├── CapacityFilter.java # Seat availability check
│ ├── TimeBasedFilter.java # Time window check
-│ ├── DirectionalCompatibilityFilter.java # Direction check
│ └── DistanceBasedFilter.java # Distance check
│
├── routing/ # Insertion optimization
@@ -97,8 +92,7 @@ org.opentripplanner.ext.carpooling/
│
├── validation/ # Constraint validation
│ ├── CompositeValidator.java # Composite validator
-│ ├── CapacityValidator.java # Capacity timeline check
-│ └── DirectionalValidator.java # Backtracking check
+│ └── CapacityValidator.java # Capacity timeline check
│
├── internal/ # Implementation details
│ ├── DefaultCarpoolingRepository.java # In-memory repository
@@ -108,8 +102,7 @@ org.opentripplanner.ext.carpooling/
│ └── SiriETCarpoolingUpdater.java # SIRI-ET message processing
│
├── util/ # Utilities
-│ ├── BeelineEstimator.java # Straight-line distance estimation
-│ └── DirectionalCalculator.java # Bearing and direction calculations
+│ └── BeelineEstimator.java # Straight-line distance estimation
│
├── constraints/ # Constraint definitions
│ └── PassengerDelayConstraints.java # Delay limits for passengers
@@ -124,10 +117,8 @@ org.opentripplanner.ext.carpooling/
Filters eliminate obviously incompatible trips **without any street routing**:
-1. **CapacityFilter**: Does the vehicle have available seats?
-2. **TimeBasedFilter**: Is the trip timing compatible with passenger request?
-3. **DirectionalCompatibilityFilter**: Are driver and passenger heading the same direction?
-4. **DistanceBasedFilter**: Is the passenger's journey within reasonable distance of driver route?
+1. **TimeBasedFilter**: Is the trip timing compatible with passenger request?
+2. **DistanceBasedFilter**: Is the passenger's journey within reasonable distance of driver route?
**Performance**: O(n) where n = number of active trips.
@@ -142,20 +133,18 @@ Fast heuristic checks eliminate impossible positions **before any A* routing**:
```
For each remaining trip:
1. Generate all position combinations (pickup, dropoff) where:
- - Pickup: between any two consecutive stops (1-indexed)
+ - Pickup: between any two consecutive stops (0-based index in modified route)
- Dropoff: after pickup position
2. For each position pair, check:
a. Capacity: Does insertion exceed vehicle capacity at any point?
- b. Direction: Does insertion cause backtracking or U-turns?
- c. Beeline delay: Do straight-line estimates exceed delay threshold?
+ b. Beeline delay: Do straight-line estimates exceed delay threshold?
3. Return only "viable" positions that pass all checks
```
**Key optimizations**:
- **Capacity validation**: Uses `CarpoolTrip.hasCapacityForInsertion()` to check entire journey range
-- **Directional filtering**: Prevents insertions that deviate >90° from route bearing
- **Beeline heuristic**: Optimistic straight-line estimates eliminate positions early
- **No routing yet**: All checks use geometric calculations only
@@ -193,11 +182,7 @@ Ensures the proposed insertion satisfies all constraints:
- Tracks passenger count at each stop
- Ensures capacity never exceeds vehicle limit
-2. **DirectionalValidator**: Ensures no backtracking
- - Computes bearings between consecutive stops
- - Rejects if bearing changes > threshold (indicates backtracking)
-
-3. **Deviation Budget Check**: Ensures additional time ≤ driver's stated willingness
+2. **Deviation Budget Check**: Ensures additional time ≤ driver's stated willingness
**All validators must pass** for an insertion to be considered valid.
@@ -292,13 +277,10 @@ Configure the SIRI-ET updater to receive trip updates:
Represents a driver's journey offering carpool seats:
- **id**: Unique trip identifier
-- **boardingArea**: Start zone for driver journey
-- **alightingArea**: End zone for driver journey
-- **startTime**: When driver departs
-- **endTime**: When driver arrives (includes deviation budget)
-- **deviationBudget**: Extra time driver is willing to spend for passengers
-- **availableSeats**: Current remaining capacity
-- **stops**: Ordered list of waypoints (includes booked passenger stops)
+- **startTime**: When the driver departs
+- **endTime**: When the driver arrives
+- **totalCapacity**: Number of seats in the car, including the driver seat
+- **stops**: Ordered list of waypoints; the first stop is the origin, the last is the destination, and booked passenger stops are inserted in between
- **provider**: Source system identifier
### CarpoolStop
@@ -306,31 +288,32 @@ Represents a driver's journey offering carpool seats:
Waypoint along a carpool route:
- **coordinate**: Geographic location
-- **sequenceNumber**: Order in route (0-indexed)
-- **estimatedArrivalTime**: When driver expects to arrive
-- **stopType**: PICKUP or DROPOFF
-- **passengerDelta**: Change in passenger count (+1 for pickup, -1 for dropoff)
+- **aimedArrivalTime**: Planned arrival time (null for the origin stop)
+- **expectedArrivalTime**: Currently expected arrival time, updated via real-time (null for the origin stop)
+- **latestExpectedArrivalTime**: Latest arrival time the driver commits to (null if not provided); used to derive `deviationBudget`
+- **aimedDepartureTime**: Planned departure time (null for the destination stop)
+- **expectedDepartureTime**: Currently expected departure time (null for the destination stop)
+- **deviationBudget**: Extra time the driver is willing to spend on deviations before reaching this stop
+- **onboardCount**: Number of passengers onboard (including the driver) when departing this stop
### InsertionPosition
Represents a viable pickup/dropoff position pair:
-- **pickupPos**: Position to insert passenger pickup (1-indexed)
-- **dropoffPos**: Position to insert passenger dropoff (1-indexed)
-
-Note: Positions are 1-indexed to match insertion semantics (insert between existing points).
+- **pickupPos**: 0-based index of the passenger's pickup in the modified route
+- **dropoffPos**: 0-based index of the passenger's dropoff in the modified route
### InsertionCandidate
Result of finding optimal passenger insertion:
- **trip**: The original carpool trip
-- **pickupPosition**: Where to insert passenger pickup (index)
-- **dropoffPosition**: Where to insert passenger dropoff (index)
-- **segments**: Routed path segments for modified route
-- **baselineDuration**: Original trip duration
-- **totalDuration**: Modified trip duration (with passenger)
-- **additionalDuration**: Extra time added (= totalDuration - baselineDuration)
+- **pickupPosition**: 0-based index of the passenger's pickup in the modified route
+- **dropoffPosition**: 0-based index of the passenger's dropoff in the modified route
+- **routeSegments**: Routed path segments forming the complete modified route
+- **stopDuration**: Dwell time added at each intermediate stop (from the car routing preferences' `pickupTime`)
+- **transitStop**: Passenger's access/egress stop, if any
+- **totalTripDuration**: Total trip duration including driving and stop delays, computed from `routeSegments` and `stopDuration`
## Performance Characteristics
@@ -374,7 +357,6 @@ public class CustomFilter implements TripFilter {
// Add to filter chain
FilterChain chain = FilterChain.of(
- new CapacityFilter(),
new TimeBasedFilter(),
new CustomFilter()
);
@@ -405,17 +387,12 @@ Test individual components in isolation:
```java
@Test
-void testCapacityFilter() {
- var filter = new CapacityFilter();
- var trip = createTripWithSeats(2); // 2 available seats
+void testTimeBasedFilter() {
+ var filter = new TimeBasedFilter();
+ var trip = createSimpleTrip(origin, destination);
- // Should pass - within capacity
+ // Should pass - within time window
assertTrue(filter.accepts(trip, pickup, dropoff, now()));
-
- var fullTrip = createTripWithSeats(0); // No seats
-
- // Should fail - no capacity
- assertFalse(filter.accepts(fullTrip, pickup, dropoff, now()));
}
```
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/constraints/PassengerDelayConstraints.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/constraints/PassengerDelayConstraints.java
index fe3ffa17b95..f7564ac4df7 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/constraints/PassengerDelayConstraints.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/constraints/PassengerDelayConstraints.java
@@ -1,110 +1,70 @@
package org.opentripplanner.ext.carpooling.constraints;
import java.time.Duration;
+import java.util.List;
+import org.opentripplanner.ext.carpooling.model.CarpoolStop;
import org.opentripplanner.ext.carpooling.routing.InsertionPosition;
-import org.opentripplanner.utils.time.DurationUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Validates that inserting a new passenger does not cause excessive delays
- * for existing passengers in a carpool trip.
+ * for existing stops in a carpool trip.
*
- * Ensures that no existing passenger experiences:
- * - More than {@code maxDelay} additional wait time at their pickup location
- * - More than {@code maxDelay} later arrival at their dropoff location
+ * Ensures that no existing stop experiences more delay than its own
+ * {@link CarpoolStop#getDeviationBudget() deviationBudget} allows.
*
* This protects the rider experience by preventing situations where accepting
* one more passenger significantly inconveniences existing bookings.
*/
-public class PassengerDelayConstraints {
+public final class PassengerDelayConstraints {
private static final Logger LOG = LoggerFactory.getLogger(PassengerDelayConstraints.class);
- /**
- * Default maximum delay: 5 minutes.
- * No existing passenger should wait more than 5 minutes longer or arrive
- * more than 5 minutes later due to a new passenger insertion.
- */
- public static final Duration DEFAULT_MAX_DELAY = Duration.ofMinutes(5);
-
- private final Duration maxDelay;
-
- /**
- * Creates constraints with default 5-minute maximum delay.
- */
- public PassengerDelayConstraints() {
- this(DEFAULT_MAX_DELAY);
- }
-
- /**
- * Creates constraints with custom maximum delay.
- *
- * @param maxDelay Maximum acceptable delay for existing passengers
- */
- public PassengerDelayConstraints(Duration maxDelay) {
- this.maxDelay = DurationUtils.requireNonNegative(maxDelay);
- }
+ private PassengerDelayConstraints() {}
/**
- * Checks if a passenger insertion satisfies delay constraints.
+ * Checks if a passenger insertion satisfies delay constraints for all existing stops.
+ * Each stop is checked against its own deviation budget.
*
* @param originalCumulativeDurations Cumulative duration to each point in original route
* @param modifiedCumulativeDurations Cumulative duration to each point in modified route
- * @param pickupPos Position where passenger pickup is inserted (1-indexed)
- * @param dropoffPos Position where passenger dropoff is inserted (1-indexed)
- * @return true if all existing passengers experience acceptable delays
+ * @param pickupPos 0-based index of the passenger's pickup in the modified route
+ * @param dropoffPos 0-based index of the passenger's dropoff in the modified route
+ * @param stops The ordered list of stops in the original trip
+ * @return true if all existing stops experience acceptable delays
*/
- public boolean satisfiesConstraints(
+ public static boolean satisfiesConstraints(
Duration[] originalCumulativeDurations,
Duration[] modifiedCumulativeDurations,
int pickupPos,
- int dropoffPos
+ int dropoffPos,
+ List stops
) {
- // If no existing stops (only boarding and alighting), no constraint to check
- if (originalCumulativeDurations.length <= 2) {
- return true;
- }
-
- // Check delay at each existing stop (exclude boarding at 0 and alighting at end)
+ // Check delay at each existing stop (exclude origin at index 0)
for (
int originalIndex = 1;
- originalIndex < originalCumulativeDurations.length - 1;
+ originalIndex < originalCumulativeDurations.length;
originalIndex++
) {
int modifiedIndex = InsertionPosition.mapOriginalIndex(originalIndex, pickupPos, dropoffPos);
- Duration originalTime = originalCumulativeDurations[originalIndex];
- Duration modifiedTime = modifiedCumulativeDurations[modifiedIndex];
- Duration delay = modifiedTime.minus(originalTime);
+ Duration delay = modifiedCumulativeDurations[modifiedIndex].minus(
+ originalCumulativeDurations[originalIndex]
+ );
+ Duration stopBudget = stops.get(originalIndex).getDeviationBudget();
- if (delay.compareTo(maxDelay) > 0) {
+ if (delay.compareTo(stopBudget) > 0) {
LOG.debug(
- "Insertion rejected: stop at position {} delayed by {}s (max: {}s)",
+ "Stop at position {} delayed by {}s exceeds budget of {}s",
originalIndex,
delay.getSeconds(),
- maxDelay.getSeconds()
+ stopBudget.getSeconds()
);
return false;
}
-
- LOG.trace(
- "Stop at position {} delay: {}s (acceptable, max: {}s)",
- originalIndex,
- delay.getSeconds(),
- maxDelay.getSeconds()
- );
}
return true;
}
-
- /**
- * Gets the configured maximum delay.
- *
- * @return Maximum delay duration
- */
- public Duration getMaxDelay() {
- return maxDelay;
- }
}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/CapacityFilter.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/CapacityFilter.java
deleted file mode 100644
index 1d296cc0956..00000000000
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/CapacityFilter.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.opentripplanner.ext.carpooling.filter;
-
-import java.time.Duration;
-import java.time.Instant;
-import org.opentripplanner.ext.carpooling.model.CarpoolTrip;
-import org.opentripplanner.street.geometry.WgsCoordinate;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Filters trips based on available capacity.
- *
- * This is a fast pre-filter that checks if the trip has any capacity at all.
- * More detailed per-position capacity checking happens during insertion validation.
- */
-public class CapacityFilter implements TripFilter {
-
- private static final Logger LOG = LoggerFactory.getLogger(CapacityFilter.class);
-
- private boolean accepts(CarpoolTrip trip) {
- boolean hasCapacity = trip.availableSeats() > 0;
-
- if (!hasCapacity) {
- LOG.debug("Trip {} rejected by capacity filter: no available seats", trip.getId());
- }
-
- return hasCapacity;
- }
-
- @Override
- public boolean accepts(
- CarpoolTrip trip,
- WgsCoordinate passengerPickup,
- WgsCoordinate passengerDropoff
- ) {
- return accepts(trip);
- }
-
- @Override
- public boolean acceptsAccessEgress(
- CarpoolTrip trip,
- WgsCoordinate coordinateOfPassenger,
- Instant passengerDepartureTime,
- Duration searchWindow
- ) {
- return accepts(trip);
- }
-}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/DirectionalCompatibilityFilter.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/DirectionalCompatibilityFilter.java
deleted file mode 100644
index 9cd14a1c2a1..00000000000
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/DirectionalCompatibilityFilter.java
+++ /dev/null
@@ -1,140 +0,0 @@
-package org.opentripplanner.ext.carpooling.filter;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-import org.opentripplanner.ext.carpooling.model.CarpoolTrip;
-import org.opentripplanner.street.geometry.DirectionUtils;
-import org.opentripplanner.street.geometry.WgsCoordinate;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Filters trips based on directional compatibility with the passenger journey.
- *
- * This prevents carpooling from becoming a taxi service by ensuring trips and
- * passengers are going in generally the same direction. Uses optimized segment-based
- * analysis to handle routes that take detours (e.g., driving around a lake).
- *
- */
-public class DirectionalCompatibilityFilter implements TripFilter {
-
- private static final Logger LOG = LoggerFactory.getLogger(DirectionalCompatibilityFilter.class);
-
- /**
- * Default maximum bearing difference for compatibility.
- * 60° allows for reasonable detours while preventing perpendicular or opposite directions.
- */
- public static final double DEFAULT_BEARING_TOLERANCE_DEGREES = 60.0;
-
- private final double bearingToleranceDegrees;
-
- public DirectionalCompatibilityFilter() {
- this(DEFAULT_BEARING_TOLERANCE_DEGREES);
- }
-
- public DirectionalCompatibilityFilter(double bearingToleranceDegrees) {
- this.bearingToleranceDegrees = bearingToleranceDegrees;
- }
-
- @Override
- public boolean accepts(
- CarpoolTrip trip,
- WgsCoordinate passengerPickup,
- WgsCoordinate passengerDropoff
- ) {
- List routePoints = trip.routePoints();
-
- if (routePoints.size() < 2) {
- LOG.warn("Trip {} has fewer than 2 route points, rejecting", trip.getId());
- return false;
- }
-
- double passengerBearing = DirectionUtils.getAzimuth(
- passengerPickup.asJtsCoordinate(),
- passengerDropoff.asJtsCoordinate()
- );
-
- for (int i = 0; i < routePoints.size() - 1; i++) {
- if (isSegmentCompatible(routePoints.get(i), routePoints.get(i + 1), passengerBearing)) {
- LOG.debug(
- "Trip {} accepted: passenger journey aligns with segment {} ({} to {})",
- trip.getId(),
- i,
- routePoints.get(i),
- routePoints.get(i + 1)
- );
- return true;
- }
- }
-
- // Check full route as fallback
- if (isSegmentCompatible(routePoints.getFirst(), routePoints.getLast(), passengerBearing)) {
- LOG.debug(
- "Trip {} accepted: passenger journey aligns with full route ({} to {})",
- trip.getId(),
- routePoints.getFirst(),
- routePoints.getLast()
- );
- return true;
- }
-
- LOG.debug(
- "Trip {} rejected by directional filter: passenger journey (bearing {}°) not aligned with any route segments",
- trip.getId(),
- Math.round(passengerBearing)
- );
- return false;
- }
-
- @Override
- public boolean acceptsAccessEgress(
- CarpoolTrip trip,
- WgsCoordinate coordinateOfPassenger,
- Instant passengerDepartureTime,
- Duration searchWindow
- ) {
- var tripStartCoordinate = trip.routePoints().getFirst().asJtsCoordinate();
- var tripEndCoordinate = trip.routePoints().getLast().asJtsCoordinate();
- var passengerCoordJts = coordinateOfPassenger.asJtsCoordinate();
-
- var tripBearing = DirectionUtils.getAzimuth(tripStartCoordinate, tripEndCoordinate);
- var startToPassengerBearing = DirectionUtils.getAzimuth(tripStartCoordinate, passengerCoordJts);
- var endToPassengerBearing = DirectionUtils.getAzimuth(passengerCoordJts, tripEndCoordinate);
-
- return (
- bearingsAreWithinTolerance(tripBearing, startToPassengerBearing) &&
- bearingsAreWithinTolerance(tripBearing, endToPassengerBearing)
- );
- }
-
- double getBearingToleranceDegrees() {
- return bearingToleranceDegrees;
- }
-
- /**
- * Checks if a segment is directionally compatible with the passenger journey.
- *
- * @param segmentStart Start coordinate of the segment
- * @param segmentEnd End coordinate of the segment
- * @param passengerBearing Bearing of passenger journey
- * @return true if segment bearing is within tolerance of passenger bearing
- */
- private boolean isSegmentCompatible(
- WgsCoordinate segmentStart,
- WgsCoordinate segmentEnd,
- double passengerBearing
- ) {
- double segmentBearing = DirectionUtils.getAzimuth(
- segmentStart.asJtsCoordinate(),
- segmentEnd.asJtsCoordinate()
- );
-
- return bearingsAreWithinTolerance(segmentBearing, passengerBearing);
- }
-
- private boolean bearingsAreWithinTolerance(double bearing1, double bearing2) {
- double bearingDiff = DirectionUtils.bearingDifference(bearing1, bearing2);
- return bearingDiff <= bearingToleranceDegrees;
- }
-}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/FilterChain.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/FilterChain.java
index 8031328095c..f3caa61d9f9 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/FilterChain.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/FilterChain.java
@@ -13,10 +13,8 @@
* as soon as one filter rejects a trip, evaluation stops.
*
* The standard filter chain includes (in order of performance impact):
- * 1. CapacityFilter - Very fast (O(1))
- * 2. TimeBasedFilter - Very fast (O(1))
- * 3. DistanceBasedFilter - Fast (O(1) with 4 distance calculations)
- * 4. DirectionalCompatibilityFilter - Medium (O(n) with n = number of stops)
+ * 1. TimeBasedFilter - Very fast (O(1))
+ * 2. DistanceBasedFilter - Fast (O(1) with 4 distance calculations)
*/
public class FilterChain implements TripFilter {
@@ -33,14 +31,7 @@ public FilterChain(List filters) {
* the benefit of short-circuit evaluation.
*/
public static FilterChain standard() {
- return new FilterChain(
- List.of(
- new CapacityFilter(),
- new TimeBasedFilter(),
- new DistanceBasedFilter(),
- new DirectionalCompatibilityFilter()
- )
- );
+ return new FilterChain(List.of(new TimeBasedFilter(), new DistanceBasedFilter()));
}
@Override
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/TripFilter.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/TripFilter.java
index 92086bf3ff5..a8a011c3c6a 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/TripFilter.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/filter/TripFilter.java
@@ -9,7 +9,7 @@
* Interface for filtering carpool trips before expensive routing calculations.
*
* Filters are applied as a pre-screening mechanism to quickly eliminate
- * incompatible trips based on various criteria (direction, capacity, time, distance, etc.).
+ * incompatible trips based on various criteria (capacity, time, distance, etc.).
*
- * The passenger's start time is the later of:
- *
- *
The passenger's requested departure time
- *
When the driver arrives at the pickup location
- *
- *
- * This ensures the itinerary reflects realistic timing: passengers can't board before the
- * driver arrives, but they also won't board earlier than they wanted to depart.
+ * The passenger's start time is the moment the driver arrives at the pickup location
+ * (trip start + pickup travel); the boarding dwell is included in the leg's duration, not
+ * added before it. The start time is not shifted to match the passenger's requested
+ * departure time: the driver is on a committed schedule and cannot wait. Whether the
+ * passenger should show up early, or whether a trip starting before the requested time
+ * should be matched at all, is a filtering concern and lives upstream of this mapper.
*
*
Geometry and Cost
*
@@ -71,25 +65,17 @@
*/
public class CarpoolItineraryMapper {
- private final ZoneId timeZone;
private final ZonedDateTime transitSearchTimeZero;
/**
- * Creates a new carpool itinerary mapper with the specified timezone.
- *
- * The timezone is used to convert passenger requested departure times from Instant to
- * ZonedDateTime for comparison with driver pickup times.
- *
- * @param timeZone the timezone for time conversions, typically from TransitService.getTimeZone()
- * @param transitSearchTimeZero the base time for access egress requests. It is not used for access / egress
+ * @param transitSearchTimeZero the base time for access egress requests; not used for direct
*/
- public CarpoolItineraryMapper(ZoneId timeZone, ZonedDateTime transitSearchTimeZero) {
- this.timeZone = ZoneIdFallback.zoneId(timeZone);
+ public CarpoolItineraryMapper(ZonedDateTime transitSearchTimeZero) {
this.transitSearchTimeZero = transitSearchTimeZero;
}
- public CarpoolItineraryMapper(ZoneId timeZone) {
- this(timeZone, null);
+ public CarpoolItineraryMapper() {
+ this(null);
}
/**
@@ -101,11 +87,15 @@ public CarpoolItineraryMapper(ZoneId timeZone) {
*
*
Time Calculation Details
*
- * The method calculates three key times:
+ * Start and end times come entirely from the driver's schedule:
*
- *
Driver pickup arrival: Driver's start time + pickup segment durations
- *
Passenger start: max(requested time, driver arrival time)
- *
Passenger end: start time + shared segment durations
+ *
Start: {@code trip.startTime() +}
+ * {@link InsertionCandidate#getDurationUntilPickupArrival()} — the moment the driver
+ * arrives at the pickup point.
+ *
End: {@code start +}
+ * {@link InsertionCandidate#getPassengerRideDuration()}, which already includes the
+ * boarding dwell at the pickup and any intermediate stop delays along the shared
+ * segments.
*
*
*
Null Return Cases
@@ -113,41 +103,20 @@ public CarpoolItineraryMapper(ZoneId timeZone) {
* Returns {@code null} if the candidate has no shared segments, which should never happen
* for valid insertion candidates but serves as a safety check.
*
- * @param request the original routing request containing passenger preferences and timing
* @param candidate the insertion candidate containing route segments and trip details
* @return an itinerary with a single carpool leg, or null if shared segments are empty
* (should not occur for valid candidates)
*/
@Nullable
- public Itinerary toItinerary(RouteRequest request, InsertionCandidate candidate) {
+ public Itinerary toItinerary(InsertionCandidate candidate) {
var sharedSegments = candidate.getSharedSegments();
if (sharedSegments.isEmpty()) {
return null;
}
- var pickupSegments = candidate.getPickupSegments();
- Duration pickupDuration = Duration.ZERO;
- for (var segment : pickupSegments) {
- pickupDuration = pickupDuration.plus(
- Duration.between(segment.states.getFirst().getTime(), segment.states.getLast().getTime())
- );
- }
-
- var driverPickupTime = candidate.trip().startTime().plus(pickupDuration);
+ var startTime = candidate.trip().startTime().plus(candidate.getDurationUntilPickupArrival());
- var startTime = request.dateTime().isAfter(driverPickupTime.toInstant())
- ? request.dateTime().atZone(timeZone)
- : driverPickupTime;
-
- // Calculate shared journey duration
- Duration carpoolDuration = Duration.ZERO;
- for (var segment : sharedSegments) {
- carpoolDuration = carpoolDuration.plus(
- Duration.between(segment.states.getFirst().getTime(), segment.states.getLast().getTime())
- );
- }
-
- var endTime = startTime.plus(carpoolDuration);
+ var endTime = startTime.plus(candidate.getPassengerRideDuration());
var firstSegment = sharedSegments.getFirst();
var lastSegment = sharedSegments.getLast();
@@ -193,7 +162,6 @@ public Itinerary toItinerary(CarpoolAccessEgress accessEgress) {
.withEndTime(endTime)
.withFrom(Place.normal(fromVertex, new NonLocalizedString("Carpool boarding")))
.withTo(Place.normal(toVertex, new NonLocalizedString("Carpool alighting")))
- .withGeometry(GeometryUtils.concatenateLineStrings(allEdges, Edge::getGeometry))
.withDistanceMeters(allEdges.stream().mapToDouble(Edge::getDistanceMeters).sum())
.withGeneralizedCost((int) cost)
.withGeometry(geometry)
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolStop.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolStop.java
index d395025a740..bc8147a8dae 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolStop.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolStop.java
@@ -1,141 +1,67 @@
package org.opentripplanner.ext.carpooling.model;
+import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Objects;
-import java.util.function.IntSupplier;
import javax.annotation.Nullable;
-import org.locationtech.jts.geom.Geometry;
-import org.opentripplanner.core.model.i18n.I18NString;
-import org.opentripplanner.core.model.i18n.NonLocalizedString;
import org.opentripplanner.core.model.id.FeedScopedId;
import org.opentripplanner.street.geometry.WgsCoordinate;
import org.opentripplanner.transit.model.framework.AbstractTransitEntity;
-import org.opentripplanner.transit.model.site.StopLocation;
-import org.opentripplanner.transit.model.site.StopType;
/**
- * Represents a stop along a carpool trip route with passenger pickup/drop-off information.
- * Each stop tracks the passenger delta (number of passengers picked up or dropped off).
+ * Represents a stop along a carpool trip route with occupancy and timing information.
* Stops are ordered sequentially along the route.
*/
-public class CarpoolStop
- extends AbstractTransitEntity
- implements StopLocation {
-
- private final int index;
- private final I18NString name;
- private final I18NString description;
- private final I18NString url;
+public class CarpoolStop extends AbstractTransitEntity {
+
+ /** Default onboard count per stop (1 = driver only) when no occupancy information is provided. */
+ public static final int DEFAULT_ONBOARD_COUNT = 1;
+
+ /**
+ * Default per-stop deviation budget used when the SIRI feed does not supply a
+ * {@code latestExpectedArrivalTime} for the stop.
+ */
+ public static final Duration DEFAULT_DEVIATION_BUDGET = Duration.ofMinutes(15);
+
private final WgsCoordinate coordinate;
- private final Geometry geometry;
- private final CarpoolStopType carpoolStopType;
- private final ZonedDateTime expectedArrivalTime;
private final ZonedDateTime aimedArrivalTime;
- private final ZonedDateTime expectedDepartureTime;
+ private final ZonedDateTime expectedArrivalTime;
+ private final ZonedDateTime latestExpectedArrivalTime;
private final ZonedDateTime aimedDepartureTime;
- private final int sequenceNumber;
- private final int passengerDelta;
+ private final ZonedDateTime expectedDepartureTime;
+ private final int onboardCount;
+ private final Duration deviationBudget;
public CarpoolStop(CarpoolStopBuilder builder) {
super(builder.getId());
- this.index = builder.createIndex();
- // According to the spec, stop location names are optional for flex zones, so we set the ID as the dummy name.
- if (builder.name() == null) {
- this.name = new NonLocalizedString(builder.getId().toString());
- } else {
- this.name = builder.name();
- }
- this.description = builder.description();
- this.url = builder.url();
this.coordinate = Objects.requireNonNull(builder.coordinate());
- this.geometry = builder.geometry();
- this.carpoolStopType = builder.carpoolStopType();
this.expectedArrivalTime = builder.expectedArrivalTime();
this.aimedArrivalTime = builder.aimedArrivalTime();
+ this.latestExpectedArrivalTime = builder.latestExpectedArrivalTime();
this.expectedDepartureTime = builder.expectedDepartureTime();
this.aimedDepartureTime = builder.aimedDepartureTime();
- this.sequenceNumber = builder.sequenceNumber();
- this.passengerDelta = builder.passengerDelta();
+ this.onboardCount = builder.onboardCount();
+ this.deviationBudget = builder.deviationBudget();
}
- public static CarpoolStopBuilder of(FeedScopedId id, IntSupplier indexCounter) {
- return new CarpoolStopBuilder(id, indexCounter);
+ public static CarpoolStopBuilder of(FeedScopedId id) {
+ return new CarpoolStopBuilder(id);
}
public static CarpoolStopBuilder of(CarpoolStop carpoolStop) {
return new CarpoolStopBuilder(carpoolStop);
}
- // StopLocation interface implementation - delegate to the underlying AreaStop
-
- @Override
- public int getIndex() {
- return index;
- }
-
- @Override
- @Nullable
- public I18NString getName() {
- return name;
- }
-
- @Override
- @Nullable
- public I18NString getDescription() {
- return description;
- }
-
- @Override
- @Nullable
- public I18NString getUrl() {
- return url;
- }
-
- @Override
- public StopType getStopType() {
- return StopType.REGULAR;
- }
-
- @Override
- @Nullable
- public String getCode() {
- return null;
- }
-
- @Override
- @Nullable
- public String getPlatformCode() {
- return null;
- }
-
- @Override
public WgsCoordinate getCoordinate() {
return coordinate;
}
- @Override
- @Nullable
- public Geometry getGeometry() {
- return geometry;
- }
-
- @Override
- public boolean isPartOfStation() {
- return false;
- }
-
- @Override
- public boolean isPartOfSameStationAs(StopLocation alternativeStop) {
- return false;
- }
-
- // Carpool-specific methods
-
/**
- * @return The type of carpool operation allowed at this stop
+ * @return The aimed arrival time, or null if not applicable (e.g., origin stop)
*/
- public CarpoolStopType getCarpoolStopType() {
- return carpoolStopType;
+ @Nullable
+ public ZonedDateTime getAimedArrivalTime() {
+ return aimedArrivalTime;
}
/**
@@ -147,46 +73,45 @@ public ZonedDateTime getExpectedArrivalTime() {
}
/**
- * @return The aimed arrival time, or null if not applicable (e.g., origin stop)
+ * @return The latest expected arrival time, or null if not provided
*/
@Nullable
- public ZonedDateTime getAimedArrivalTime() {
- return aimedArrivalTime;
+ public ZonedDateTime getLatestExpectedArrivalTime() {
+ return latestExpectedArrivalTime;
}
/**
- * @return The expected departure time, or null if not applicable (e.g., destination stop)
+ * @return The aimed departure time, or null if not applicable (e.g., destination stop)
*/
@Nullable
- public ZonedDateTime getExpectedDepartureTime() {
- return expectedDepartureTime;
- }
-
- public int getSequenceNumber() {
- return sequenceNumber;
+ public ZonedDateTime getAimedDepartureTime() {
+ return aimedDepartureTime;
}
/**
- * @return The aimed departure time, or null if not applicable (e.g., destination stop)
+ * @return The expected departure time, or null if not applicable (e.g., destination stop)
*/
@Nullable
- public ZonedDateTime getAimedDepartureTime() {
- return aimedDepartureTime;
+ public ZonedDateTime getExpectedDepartureTime() {
+ return expectedDepartureTime;
}
/**
- * Returns the primary timing for this stop, preferring aimed arrival time.
- * This provides backward compatibility for code that expects a single time value.
- *
- * @return The aimed arrival time if set, otherwise aimed departure time
+ * @return The number of passengers onboard (including the driver) when departing this stop
*/
- @Nullable
- public ZonedDateTime getEstimatedTime() {
- return aimedArrivalTime != null ? aimedArrivalTime : aimedDepartureTime;
+ public int getOnboardCount() {
+ return onboardCount;
}
- public int getPassengerDelta() {
- return passengerDelta;
+ /**
+ * Returns the remaining slack the carpool may consume before this stop without breaking the
+ * driver's commitment to passengers already onboard. This is not the original
+ * commitment from the SIRI feed: as the trip is updated with additional SIRI messages,
+ * the budget shrinks as prior detours eat into it.
+ * A value of {@link Duration#ZERO} means no further deviation is acceptable here.
+ */
+ public Duration getDeviationBudget() {
+ return deviationBudget;
}
@Override
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolStopBuilder.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolStopBuilder.java
index 0bfc5518311..c8b496f88a4 100755
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolStopBuilder.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolStopBuilder.java
@@ -1,55 +1,42 @@
package org.opentripplanner.ext.carpooling.model;
+import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Objects;
-import java.util.function.IntSupplier;
-import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.GeometryFactory;
-import org.opentripplanner.core.model.i18n.I18NString;
import org.opentripplanner.core.model.id.FeedScopedId;
import org.opentripplanner.street.geometry.WgsCoordinate;
import org.opentripplanner.transit.model.framework.AbstractEntityBuilder;
+/**
+ * Builder for {@link CarpoolStop} instances.
+ */
public class CarpoolStopBuilder extends AbstractEntityBuilder {
- private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
- private final IntSupplier indexCounter;
- private I18NString name;
- private I18NString description;
- private I18NString url;
private WgsCoordinate coordinate;
- private Geometry geometry;
- private CarpoolStopType carpoolStopType;
private ZonedDateTime expectedArrivalTime;
private ZonedDateTime aimedArrivalTime;
+ private ZonedDateTime latestExpectedArrivalTime;
private ZonedDateTime expectedDepartureTime;
private ZonedDateTime aimedDepartureTime;
- private int sequenceNumber;
- private int passengerDelta;
+ private int onboardCount = CarpoolStop.DEFAULT_ONBOARD_COUNT;
- CarpoolStopBuilder(FeedScopedId id, IntSupplier indexCounter) {
+ private Duration deviationBudget = CarpoolStop.DEFAULT_DEVIATION_BUDGET;
+
+ CarpoolStopBuilder(FeedScopedId id) {
super(id);
- this.indexCounter = Objects.requireNonNull(indexCounter);
}
CarpoolStopBuilder(CarpoolStop original) {
super(original);
- this.indexCounter = original::getIndex;
- // Optional fields
- this.name = original.getName();
- this.description = original.getDescription();
- this.url = original.getUrl();
this.coordinate = original.getCoordinate();
- this.geometry = original.getGeometry();
- this.sequenceNumber = original.getSequenceNumber();
-
- this.carpoolStopType = original.getCarpoolStopType();
this.expectedArrivalTime = original.getExpectedArrivalTime();
this.aimedArrivalTime = original.getAimedArrivalTime();
+ this.latestExpectedArrivalTime = original.getLatestExpectedArrivalTime();
this.expectedDepartureTime = original.getExpectedDepartureTime();
this.aimedDepartureTime = original.getAimedDepartureTime();
- this.passengerDelta = original.getPassengerDelta();
+ this.onboardCount = original.getOnboardCount();
+ this.deviationBudget = original.getDeviationBudget();
}
@Override
@@ -57,29 +44,8 @@ protected CarpoolStop buildFromValues() {
return new CarpoolStop(this);
}
- public CarpoolStopBuilder withName(I18NString name) {
- this.name = name;
- return this;
- }
-
- public CarpoolStopBuilder withDescription(I18NString description) {
- this.description = description;
- return this;
- }
-
- public CarpoolStopBuilder withUrl(I18NString url) {
- this.url = url;
- return this;
- }
-
public CarpoolStopBuilder withCoordinate(WgsCoordinate coordinate) {
this.coordinate = coordinate;
- this.geometry = toGeometry(coordinate);
- return this;
- }
-
- public CarpoolStopBuilder withCarpoolStopType(CarpoolStopType carpoolStopType) {
- this.carpoolStopType = carpoolStopType;
return this;
}
@@ -93,6 +59,11 @@ public CarpoolStopBuilder withAimedArrivalTime(ZonedDateTime aimedArrivalTime) {
return this;
}
+ public CarpoolStopBuilder withLatestExpectedArrivalTime(ZonedDateTime latestExpectedArrivalTime) {
+ this.latestExpectedArrivalTime = latestExpectedArrivalTime;
+ return this;
+ }
+
public CarpoolStopBuilder withExpectedDepartureTime(ZonedDateTime expectedDepartureTime) {
this.expectedDepartureTime = expectedDepartureTime;
return this;
@@ -103,44 +74,29 @@ public CarpoolStopBuilder withAimedDepartureTime(ZonedDateTime aimedDepartureTim
return this;
}
- public CarpoolStopBuilder withSequenceNumber(int sequenceNumber) {
- this.sequenceNumber = sequenceNumber;
+ public CarpoolStopBuilder withOnboardCount(int onboardCount) {
+ this.onboardCount = onboardCount;
return this;
}
- public CarpoolStopBuilder withPassengerDelta(int passengerDelta) {
- this.passengerDelta = passengerDelta;
+ /**
+ * Sets the per-stop deviation budget. See {@link CarpoolStop#getDeviationBudget()} for the
+ * semantics of the value.
+ *
+ * @param deviationBudget remaining slack at this stop; must be non-null. Use
+ * {@link Duration#ZERO} for stops where no further deviation is
+ * acceptable (always for the trip origin).
+ * @throws NullPointerException if {@code deviationBudget} is null
+ */
+ public CarpoolStopBuilder withDeviationBudget(Duration deviationBudget) {
+ this.deviationBudget = Objects.requireNonNull(deviationBudget);
return this;
}
- int createIndex() {
- return indexCounter.getAsInt();
- }
-
- public I18NString name() {
- return name;
- }
-
- public I18NString description() {
- return description;
- }
-
- public I18NString url() {
- return url;
- }
-
public WgsCoordinate coordinate() {
return coordinate;
}
- public Geometry geometry() {
- return geometry;
- }
-
- public CarpoolStopType carpoolStopType() {
- return carpoolStopType;
- }
-
public ZonedDateTime expectedArrivalTime() {
return expectedArrivalTime;
}
@@ -149,6 +105,10 @@ public ZonedDateTime aimedArrivalTime() {
return aimedArrivalTime;
}
+ public ZonedDateTime latestExpectedArrivalTime() {
+ return latestExpectedArrivalTime;
+ }
+
public ZonedDateTime expectedDepartureTime() {
return expectedDepartureTime;
}
@@ -157,15 +117,11 @@ public ZonedDateTime aimedDepartureTime() {
return aimedDepartureTime;
}
- public int sequenceNumber() {
- return sequenceNumber;
- }
-
- public int passengerDelta() {
- return passengerDelta;
+ public int onboardCount() {
+ return onboardCount;
}
- private Geometry toGeometry(WgsCoordinate coordinate) {
- return GEOMETRY_FACTORY.createPoint(coordinate.asJtsCoordinate());
+ public Duration deviationBudget() {
+ return deviationBudget;
}
}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolStopType.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolStopType.java
deleted file mode 100644
index 2235fd78280..00000000000
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolStopType.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.opentripplanner.ext.carpooling.model;
-
-/**
- * The type of carpool stop operation.
- */
-public enum CarpoolStopType {
- /** Only passengers can be picked up at this stop */
- PICKUP_ONLY,
- /** Only passengers can be dropped off at this stop */
- DROP_OFF_ONLY,
- /** Both pickup and drop-off are allowed */
- PICKUP_AND_DROP_OFF,
-}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolTrip.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolTrip.java
index bb6d0e1d09b..f20e5ec4691 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolTrip.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolTrip.java
@@ -1,6 +1,5 @@
package org.opentripplanner.ext.carpooling.model;
-import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.List;
@@ -14,7 +13,7 @@
* Represents a driver's carpool journey with planned route, timing, and passenger capacity.
*
* A carpool trip models a driver offering their vehicle journey for passengers to join. It includes
- * the driver's planned route as a sequence of stops, available seating capacity, and timing
+ * the driver's planned route as a sequence of stops, total vehicle capacity, and timing
* constraints including a deviation budget that allows the driver to slightly adjust their route
* to accommodate passengers.
*
@@ -23,9 +22,7 @@
*
Origin/Destination Areas: Start and end zones for the driver's journey
*
Stops: Ordered sequence of waypoints along the route where passengers
* can be picked up or dropped off. Stops are dynamically updated as bookings occur.
- *
Deviation Budget: Maximum additional time the driver is willing to spend
- * to pick up/drop off passengers (e.g., 5 minutes). This represents the driver's flexibility.
- *
Available Seats: Current passenger capacity remaining in the vehicle
+ *
Total Capacity: Number of seats in the car, including the driver seat
*
*
*
Data Source
@@ -57,14 +54,13 @@ public class CarpoolTrip
extends AbstractTransitEntity
implements LogInfo {
+ /** Default total capacity (including driver) when no capacity information is provided. */
+ public static final int DEFAULT_TOTAL_CAPACITY = 5;
+
private final ZonedDateTime startTime;
private final ZonedDateTime endTime;
private final String provider;
-
- // The amount of time the trip can deviate from the scheduled time in order to pick up or drop off
- // a passenger.
- private final Duration deviationBudget;
- private final int availableSeats;
+ private final int totalCapacity;
// Ordered list of stops along the carpool route where passengers can be picked up or dropped off
private final List stops;
@@ -74,8 +70,7 @@ public CarpoolTrip(CarpoolTripBuilder builder) {
this.startTime = builder.startTime();
this.endTime = builder.endTime();
this.provider = builder.provider();
- this.availableSeats = builder.availableSeats();
- this.deviationBudget = builder.deviationBudget();
+ this.totalCapacity = builder.totalCapacity();
this.stops = Collections.unmodifiableList(builder.stops());
}
@@ -117,23 +112,22 @@ public String provider() {
return provider;
}
- public Duration deviationBudget() {
- return deviationBudget;
- }
-
- public int availableSeats() {
- return availableSeats;
+ /**
+ * @return Total number of seats in the vehicle, including the driver seat
+ */
+ public int totalCapacity() {
+ return totalCapacity;
}
/**
* Returns the ordered sequence of stops along the carpool route.
*
* Stops include both the driver's originally planned stops and any dynamically added stops
- * for passenger pickups and dropoffs. The list is ordered by sequence number, representing
- * the order in which stops are visited along the route.
+ * for passenger pickups and dropoffs. The list is ordered by visit order along the route:
+ * the first element is the origin and the last is the destination.
*
- * @return an immutable list of stops along the carpool route, ordered by sequence number,
- * never null but may be empty for trips with no intermediate stops
+ * @return an immutable list of stops along the carpool route, in visit order; never null,
+ * and always contains at least the origin and destination
*/
public List stops() {
return stops;
@@ -152,65 +146,67 @@ public List routePoints() {
}
/**
- * Calculates the number of passengers in the vehicle after visiting the specified position.
- *
- * Position semantics:
- * - Position 0: Before any stops → 0 passengers
- * - Position N: After Nth stop → cumulative passenger delta up to stop N
+ * Returns the number of passengers onboard the vehicle when departing the given stop.
*
- * @param position The position index (0 = before any stops, 1 = after first stop, etc.)
- * @return Number of passengers after this position
- * @throws IllegalArgumentException if position is negative or greater than stops.size()
+ * @param stopIndex The 0-based index of the stop in the stop list
+ * @return Number of passengers onboard when departing from this stop
+ * @throws IllegalArgumentException if stopIndex is out of bounds
*/
- public int getPassengerCountAtPosition(int position) {
- if (position < 0) {
- throw new IllegalArgumentException("Position must be non-negative, got: " + position);
- }
-
- if (position > stops.size()) {
+ public int getPassengerCountAtDepartureOfStop(int stopIndex) {
+ if (stopIndex < 0 || stopIndex >= stops.size()) {
throw new IllegalArgumentException(
- "Position " + position + " exceeds valid range (0 to " + stops.size() + ")"
+ "Stop index " + stopIndex + " is out of bounds (0 to " + (stops.size() - 1) + ")"
);
}
- // Position 0 is before any stops
- if (position == 0) {
- return 0;
- }
-
- // Accumulate passenger deltas up to this position
- int count = 0;
- for (int i = 0; i < position; i++) {
- count += stops.get(i).getPassengerDelta();
- }
-
- return count;
+ return stops.get(stopIndex).getOnboardCount();
}
/**
- * Checks if there's capacity to add passengers throughout a range of positions.
+ * Checks if there's capacity to insert a passenger at the given pickup and dropoff positions
+ * in the modified route.
+ *
+ * The positions are 0-based indices of the passenger's pickup and dropoff stops in the
+ * modified route (the route after the passenger's stops have been inserted). For example,
+ * with original stops [Origin, A, B, Destination] and pickupPosition=1, dropoffPosition=3:
+ * the modified route is [Origin, Pickup, A, Dropoff, B, Destination].
+ * All stops between (inclusive) pickupPosition - 1 and dropoffPosition - 2 are checked for capacity.
+ * In the example this is between stops 0 and 1, meaning that stops Origin and A need to have sufficient
+ * capacity for {@code additionalPassengers} extra passengers.
*
- * This validates that adding passengers won't exceed vehicle capacity at any point
- * between pickup and dropoff positions.
*
- * @param pickupPosition The pickup position (1-indexed, inclusive)
- * @param dropoffPosition The dropoff position (1-indexed, exclusive)
+ * @param pickupPosition 0-based index of the passenger's pickup in the modified route.
+ * Must be >= 1 (position 0 is the driver's origin).
+ * @param dropoffPosition 0-based index of the passenger's dropoff in the modified route.
+ * Must be > pickupPosition.
* @param additionalPassengers Number of passengers to add (typically 1)
* @return true if capacity is available throughout the entire range, false otherwise
+ * @throws IllegalArgumentException if pickupPosition < 1 or dropoffPosition <= pickupPosition
*/
public boolean hasCapacityForInsertion(
int pickupPosition,
int dropoffPosition,
int additionalPassengers
) {
- int pickupPassengers = getPassengerCountAtPosition(pickupPosition - 1);
- if (pickupPassengers + additionalPassengers > availableSeats) {
- return false;
+ if (pickupPosition < 1) {
+ throw new IllegalArgumentException(
+ "pickupPosition must be >= 1 (position 0 is the driver's origin), got: " + pickupPosition
+ );
}
+ if (dropoffPosition <= pickupPosition) {
+ throw new IllegalArgumentException(
+ "dropoffPosition must be > pickupPosition, got: pickupPosition=" +
+ pickupPosition +
+ ", dropoffPosition=" +
+ dropoffPosition
+ );
+ }
+
+ int firstOriginalStop = pickupPosition - 1;
+ int lastOriginalStop = dropoffPosition - 2;
- for (int pos = pickupPosition; pos < dropoffPosition; pos++) {
- int currentPassengers = getPassengerCountAtPosition(pos);
- if (currentPassengers + additionalPassengers > availableSeats) {
+ for (int i = firstOriginalStop; i <= lastOriginalStop; i++) {
+ if (getPassengerCountAtDepartureOfStop(i) + additionalPassengers > totalCapacity) {
return false;
}
}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolTripBuilder.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolTripBuilder.java
index 106232ff62e..b9969b5b430 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolTripBuilder.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/model/CarpoolTripBuilder.java
@@ -1,20 +1,20 @@
package org.opentripplanner.ext.carpooling.model;
-import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
-import java.util.Comparator;
import java.util.List;
import org.opentripplanner.core.model.id.FeedScopedId;
import org.opentripplanner.transit.model.framework.AbstractEntityBuilder;
+/**
+ * Builder for {@link CarpoolTrip} instances.
+ */
public class CarpoolTripBuilder extends AbstractEntityBuilder {
private ZonedDateTime startTime;
private ZonedDateTime endTime;
private String provider;
- private Duration deviationBudget = Duration.ofMinutes(15);
- private int availableSeats = 1;
+ private int totalCapacity = CarpoolTrip.DEFAULT_TOTAL_CAPACITY;
private List stops = new ArrayList<>();
public CarpoolTripBuilder(FeedScopedId id) {
@@ -26,8 +26,7 @@ public CarpoolTripBuilder(CarpoolTrip original) {
this.startTime = original.startTime();
this.endTime = original.endTime();
this.provider = original.provider();
- this.deviationBudget = original.deviationBudget();
- this.availableSeats = original.availableSeats();
+ this.totalCapacity = original.totalCapacity();
this.stops = new ArrayList<>(original.stops());
}
@@ -46,13 +45,8 @@ public CarpoolTripBuilder withProvider(String provider) {
return this;
}
- public CarpoolTripBuilder withDeviationBudget(Duration deviationBudget) {
- this.deviationBudget = deviationBudget;
- return this;
- }
-
- public CarpoolTripBuilder withAvailableSeats(int availableSeats) {
- this.availableSeats = availableSeats;
+ public CarpoolTripBuilder withTotalCapacity(int totalCapacity) {
+ this.totalCapacity = totalCapacity;
return this;
}
@@ -68,12 +62,8 @@ public String provider() {
return provider;
}
- public Duration deviationBudget() {
- return deviationBudget;
- }
-
- public int availableSeats() {
- return availableSeats;
+ public int totalCapacity() {
+ return totalCapacity;
}
public CarpoolTripBuilder withStops(List stops) {
@@ -81,18 +71,6 @@ public CarpoolTripBuilder withStops(List stops) {
return this;
}
- public CarpoolTripBuilder addStop(CarpoolStop stop) {
- this.stops.add(stop);
- // Sort stops by sequence number to maintain order
- this.stops.sort(Comparator.comparingInt(CarpoolStop::getSequenceNumber));
- return this;
- }
-
- public CarpoolTripBuilder clearStops() {
- this.stops.clear();
- return this;
- }
-
public List stops() {
return stops;
}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/CarpoolAccessEgress.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/CarpoolAccessEgress.java
index 35643aa1755..2e629ffd5e1 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/CarpoolAccessEgress.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/CarpoolAccessEgress.java
@@ -14,12 +14,19 @@
public class CarpoolAccessEgress implements RoutingAccessEgress {
/**
- * The departure time of the passenger in seconds since transitSearchTimeZero.
+ * The Raptor departure time of this access/egress leg, in seconds since
+ * {@code transitSearchTimeZero}. For a carpool leg this is the moment the car arrives at the
+ * pickup: the passenger must be ready by this instant, since the driver is on a committed
+ * schedule and cannot wait. The boarding dwell at the pickup is part of
+ * {@link #durationInSeconds}, not of the time before departure.
*/
private final int departureTimeOfPassenger;
/**
- * The arrival time of the passenger in seconds since transitSearchTimeZero.
+ * The Raptor arrival time of this access/egress leg, in seconds since
+ * {@code transitSearchTimeZero}. For a carpool leg this is the moment the car reaches the
+ * dropoff (the transit stop for access, the passenger's destination for egress), after
+ * boarding dwell and shared travel.
*/
private final int arrivalTimeOfPassenger;
private final int stop;
@@ -112,7 +119,7 @@ public RoutingAccessEgress withPenalty(TimeAndCost penalty) {
It is never used for instances of CarpoolAccessEgress, but this might change in the future.
*/
@Override
- public State getLastState() {
+ public State getFinalState() {
throw new UnsupportedOperationException(
"Fetching last state of CarpoolAccessEgress is not yet implemented"
);
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionCandidate.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionCandidate.java
index 43f3472cc02..1ffb28491c8 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionCandidate.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionCandidate.java
@@ -4,6 +4,7 @@
import java.util.List;
import org.opentripplanner.astar.model.GraphPath;
import org.opentripplanner.ext.carpooling.model.CarpoolTrip;
+import org.opentripplanner.ext.carpooling.util.GraphPathUtils;
import org.opentripplanner.routing.graphfinder.NearbyStop;
import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.vertex.Vertex;
@@ -14,31 +15,51 @@
*
* Contains all information needed to construct an itinerary, including:
* - The original trip
- * - Insertion positions (where pickup and dropoff occur in the route)
+ * - Insertion positions (where pickup and dropoff occur in the modified route)
* - Route segments (all GraphPaths forming the complete modified route)
- * - Timing information (baseline and total duration, deviation)
+ * - Timing information
+ *
+ * {@code pickupPosition} and {@code dropoffPosition} are 0-based indices of the passenger's
+ * pickup and dropoff stops in the modified route (the route after the passenger's stops have
+ * been inserted into the carpool trip).
*/
public record InsertionCandidate(
CarpoolTrip trip,
int pickupPosition,
int dropoffPosition,
List> routeSegments,
- Duration durationBetweenOriginAndDestination,
- Duration totalDuration,
- NearbyStop transitStop
+ Duration stopDuration,
+ NearbyStop transitStop,
+ Duration totalTripDuration
) {
- /**
- * Calculates the additional duration caused by inserting this passenger.
- */
- public Duration additionalDuration() {
- return totalDuration.minus(durationBetweenOriginAndDestination);
+ public InsertionCandidate(
+ CarpoolTrip trip,
+ int pickupPosition,
+ int dropoffPosition,
+ List> routeSegments,
+ Duration stopDuration,
+ NearbyStop transitStop
+ ) {
+ this(
+ trip,
+ pickupPosition,
+ dropoffPosition,
+ routeSegments,
+ stopDuration,
+ transitStop,
+ computeTotalTripDuration(routeSegments, stopDuration)
+ );
}
- /**
- * Checks if this insertion is within the trip's deviation budget.
- */
- public boolean isWithinDeviationBudget() {
- return additionalDuration().compareTo(trip.deviationBudget()) <= 0;
+ private static Duration computeTotalTripDuration(
+ List> routeSegments,
+ Duration stopDuration
+ ) {
+ Duration[] cumulativeDurations = GraphPathUtils.calculateCumulativeDurations(
+ routeSegments.toArray(new GraphPath[0]),
+ stopDuration
+ );
+ return cumulativeDurations[cumulativeDurations.length - 1];
}
/**
@@ -71,14 +92,49 @@ public List> getDropoffSegments() {
return routeSegments.subList(dropoffPosition, routeSegments.size());
}
+ /**
+ * Calculates the duration from trip start until the car arrives at the passenger's pickup.
+ * Includes travel time through pickup segments and intermediate stop delays between them, but
+ * excludes the boarding dwell at the pickup itself — that is accounted for in
+ * {@link #getPassengerRideDuration()}.
+ * Returns {@link Duration#ZERO} when the passenger boards at the trip origin (no pickup segments).
+ */
+ public Duration getDurationUntilPickupArrival() {
+ return totalSegmentDuration(getPickupSegments(), stopDuration);
+ }
+
+ /**
+ * Calculates the duration of the passenger's ride from pickup arrival to dropoff.
+ * Includes the boarding dwell at the pickup (when there are pickup segments preceding it),
+ * travel time through shared segments, and stop delays at intermediate stops between shared
+ * segments. The no-pickup-segments case (passenger boarding at the trip origin) cannot occur
+ * today — the search never places {@code pickupPosition == 0} — but the branch guards against
+ * it by omitting the boarding dwell.
+ */
+ public Duration getPassengerRideDuration() {
+ Duration boardingDwell = pickupPosition == 0 ? Duration.ZERO : stopDuration;
+ return totalSegmentDuration(getSharedSegments(), stopDuration).plus(boardingDwell);
+ }
+
+ private static Duration totalSegmentDuration(
+ List> segments,
+ Duration stopDuration
+ ) {
+ return segments
+ .stream()
+ .map(GraphPathUtils::calculateDuration)
+ .reduce(Duration.ZERO, Duration::plus)
+ .plus(stopDuration.multipliedBy(Math.max(0, segments.size() - 1)));
+ }
+
@Override
public String toString() {
return String.format(
- "InsertionCandidate{trip=%s, pickup@%d, dropoff@%d, additional=%ds, segments=%d}",
+ "InsertionCandidate{trip=%s, pickup@%d, dropoff@%d, duration=%ds, segments=%d}",
trip.getId(),
pickupPosition,
dropoffPosition,
- additionalDuration().getSeconds(),
+ totalTripDuration.getSeconds(),
routeSegments.size()
);
}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionEvaluator.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionEvaluator.java
index f50b3a324da..195ca0ae8eb 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionEvaluator.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionEvaluator.java
@@ -1,7 +1,6 @@
package org.opentripplanner.ext.carpooling.routing;
import static org.opentripplanner.ext.carpooling.util.GraphPathUtils.calculateCumulativeDurations;
-import static org.opentripplanner.ext.carpooling.util.GraphPathUtils.calculateDuration;
import java.time.Duration;
import java.util.ArrayList;
@@ -36,29 +35,27 @@ public class InsertionEvaluator {
private static final Logger LOG = LoggerFactory.getLogger(InsertionEvaluator.class);
- private static final Duration INITIAL_ADDITIONAL_DURATION = Duration.ofDays(1);
-
- private final PassengerDelayConstraints delayConstraints;
private final LinkingContext linkingContext;
private final StreetVertexUtils streetVertexUtils;
private final CarpoolRouter carpoolRouter;
+ private final Duration stopDuration;
/**
- * Creates an evaluator with the specified routing function, delay constraints, and linking context.
+ * Creates an evaluator with the specified routing function and linking context.
*
- * @param delayConstraints Constraints for acceptable passenger delays
* @param linkingContext Linking context with pre-linked vertices for routing
+ * @param stopDuration Duration added at each intermediate stop (from car pickupTime preference)
*/
public InsertionEvaluator(
- PassengerDelayConstraints delayConstraints,
LinkingContext linkingContext,
StreetVertexUtils streetVertexUtils,
- CarpoolRouter carpoolRouter
+ CarpoolRouter carpoolRouter,
+ Duration stopDuration
) {
- this.delayConstraints = delayConstraints;
this.linkingContext = linkingContext;
this.streetVertexUtils = streetVertexUtils;
this.carpoolRouter = carpoolRouter;
+ this.stopDuration = stopDuration;
}
/**
@@ -102,23 +99,7 @@ public List findBestInsertions(
return List.of();
}
- Duration[] cumulativeDurations = calculateCumulativeDurations(baselineSegments);
-
- GraphPath pathBetweenOriginAndDestination = carpoolRouter.route(
- tripWithVertices.vertices().getFirst(),
- tripWithVertices.vertices().getLast()
- );
- if (pathBetweenOriginAndDestination == null) {
- LOG.error(
- "Could not route between origin and destination for trip {}",
- tripWithVertices.trip().getId()
- );
- return List.of();
- }
-
- Duration durationBetweenOriginAndDestination = calculateDuration(
- pathBetweenOriginAndDestination
- );
+ Duration[] cumulativeDurations = calculateCumulativeDurations(baselineSegments, stopDuration);
return tripWithViableAccessEgress
.viableAccessEgress()
@@ -138,7 +119,6 @@ public List findBestInsertions(
dropOffVertex,
baselineSegments,
cumulativeDurations,
- durationBetweenOriginAndDestination,
viableAccessEgress.transitStop()
);
})
@@ -182,23 +162,7 @@ public InsertionCandidate findBestInsertion(
linkingContext
);
- Duration[] cumulativeDurations = calculateCumulativeDurations(baselineSegments);
-
- GraphPath pathBetweenOriginAndDestination = carpoolRouter.route(
- tripWithVertices.vertices().getFirst(),
- tripWithVertices.vertices().getLast()
- );
- if (pathBetweenOriginAndDestination == null) {
- LOG.error(
- "Could not route between origin and destination for trip {}",
- tripWithVertices.trip().getId()
- );
- return null;
- }
-
- Duration durationBetweenOriginAndDestination = calculateDuration(
- pathBetweenOriginAndDestination
- );
+ Duration[] cumulativeDurations = calculateCumulativeDurations(baselineSegments, stopDuration);
return findBestInsertion(
tripWithVertices,
@@ -207,7 +171,6 @@ public InsertionCandidate findBestInsertion(
passengerDropoffVertex,
baselineSegments,
cumulativeDurations,
- durationBetweenOriginAndDestination,
null
);
}
@@ -220,11 +183,9 @@ private InsertionCandidate findBestInsertion(
Vertex passengerDropoff,
GraphPath[] baselineSegments,
Duration[] cumulativeDurations,
- Duration durationBetweenOriginAndDestination,
NearbyStop transitStop
) {
InsertionCandidate bestCandidate = null;
- Duration minAdditionalDuration = INITIAL_ADDITIONAL_DURATION;
for (InsertionPosition position : viablePositions) {
InsertionCandidate candidate = evaluateInsertion(
@@ -235,7 +196,6 @@ private InsertionCandidate findBestInsertion(
passengerDropoff,
baselineSegments,
cumulativeDurations,
- durationBetweenOriginAndDestination,
transitStop
);
@@ -243,20 +203,16 @@ private InsertionCandidate findBestInsertion(
continue;
}
- Duration additionalDuration = candidate.additionalDuration();
-
- // Check if this is the best so far and within deviation budget
if (
- additionalDuration.compareTo(minAdditionalDuration) < 0 &&
- additionalDuration.compareTo(tripWithVertices.trip().deviationBudget()) <= 0
+ bestCandidate == null ||
+ candidate.totalTripDuration().compareTo(bestCandidate.totalTripDuration()) < 0
) {
- minAdditionalDuration = additionalDuration;
bestCandidate = candidate;
LOG.debug(
- "New best insertion: pickup@{}, dropoff@{}, additional={}s",
+ "New best insertion: pickup@{}, dropoff@{}, duration={}s",
position.pickupPos(),
position.dropoffPos(),
- additionalDuration.getSeconds()
+ candidate.totalTripDuration().getSeconds()
);
}
}
@@ -276,7 +232,6 @@ private InsertionCandidate evaluateInsertion(
Vertex passengerDropoff,
GraphPath[] baselineSegments,
Duration[] originalCumulativeDurations,
- Duration durationBetweenOriginAndDestination,
NearbyStop transitStop
) {
List> modifiedSegments = buildModifiedSegments(
@@ -292,23 +247,18 @@ private InsertionCandidate evaluateInsertion(
return null;
}
- // Calculate total duration
- Duration totalDuration = Duration.ZERO;
- for (GraphPath segment : modifiedSegments) {
- totalDuration = totalDuration.plus(
- Duration.between(segment.states.getFirst().getTime(), segment.states.getLast().getTime())
- );
- }
-
// Check passenger delay constraints
+ Duration[] modifiedCumulativeDurations = calculateCumulativeDurations(
+ modifiedSegments.toArray(new GraphPath[modifiedSegments.size()]),
+ stopDuration
+ );
if (
- !delayConstraints.satisfiesConstraints(
+ !PassengerDelayConstraints.satisfiesConstraints(
originalCumulativeDurations,
- calculateCumulativeDurations(
- modifiedSegments.toArray(new GraphPath[modifiedSegments.size()])
- ),
+ modifiedCumulativeDurations,
pickupPos,
- dropoffPos
+ dropoffPos,
+ tripWithVertices.trip().stops()
)
) {
LOG.trace(
@@ -324,8 +274,7 @@ private InsertionCandidate evaluateInsertion(
pickupPos,
dropoffPos,
modifiedSegments,
- durationBetweenOriginAndDestination,
- totalDuration,
+ stopDuration,
transitStop
);
}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionPosition.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionPosition.java
index 2471e39599a..16d875dd026 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionPosition.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionPosition.java
@@ -9,11 +9,12 @@ public class InsertionPosition {
* Represents a pickup and dropoff position pair that passed heuristic validation.
*
* This is an intermediate value used between finding viable positions (via heuristics)
- * and evaluating them (via A* routing). Positions are 1-indexed to match the insertion
- * point semantics in the route modification algorithm.
+ * and evaluating them (via A* routing). Positions are 0-based indices of the passenger's
+ * pickup and dropoff stops in the modified route (the route after the passenger's stops
+ * have been inserted into the carpool trip).
*
- * @param pickupPos Position to insert passenger pickup (1-indexed)
- * @param dropoffPos Position to insert passenger dropoff (1-indexed, always > pickupPos)
+ * @param pickupPos 0-based index of the passenger's pickup in the modified route
+ * @param dropoffPos 0-based index of the passenger's dropoff in the modified route (always > pickupPos)
*/
public InsertionPosition(int pickupPos, int dropoffPos) {
if (dropoffPos <= pickupPos) {
@@ -41,8 +42,8 @@ public int dropoffPos() {
* indices shift. This method calculates the new index for an original route point.
*
* @param originalIndex Index in original route (before passenger insertion)
- * @param pickupPos Position where pickup was inserted (1-indexed)
- * @param dropoffPos Position where dropoff was inserted (1-indexed)
+ * @param pickupPos 0-based index of the passenger's pickup in the modified route
+ * @param dropoffPos 0-based index of the passenger's dropoff in the modified route
* @return Corresponding index in modified route (after passenger insertion)
*/
public static int mapOriginalIndex(int originalIndex, int pickupPos, int dropoffPos) {
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionPositionFinder.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionPositionFinder.java
index ffc69cd4591..25e094a63cd 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionPositionFinder.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionPositionFinder.java
@@ -6,7 +6,6 @@
import org.opentripplanner.ext.carpooling.constraints.PassengerDelayConstraints;
import org.opentripplanner.ext.carpooling.model.CarpoolTrip;
import org.opentripplanner.ext.carpooling.util.BeelineEstimator;
-import org.opentripplanner.street.geometry.DirectionUtils;
import org.opentripplanner.street.geometry.WgsCoordinate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -18,7 +17,6 @@
* are worth evaluating with expensive A* routing. It validates positions using:
*
*
Capacity constraints - ensures available seats throughout the journey
- *
Directional compatibility - prevents backtracking and U-turns
*
Beeline delay heuristic - optimistic straight-line time estimates
*
*
@@ -29,30 +27,21 @@ public class InsertionPositionFinder {
private static final Logger LOG = LoggerFactory.getLogger(InsertionPositionFinder.class);
- /** Maximum bearing deviation allowed for forward progress (90° allows detours, prevents U-turns) */
- private static final double FORWARD_PROGRESS_TOLERANCE_DEGREES = 90.0;
-
- private final PassengerDelayConstraints delayConstraints;
private final BeelineEstimator beelineEstimator;
/**
- * Creates a finder with default constraints and estimator.
+ * Creates a finder with default estimator.
*/
public InsertionPositionFinder() {
- this(new PassengerDelayConstraints(), new BeelineEstimator());
+ this(new BeelineEstimator());
}
/**
- * Creates a finder with specified constraints and estimator.
+ * Creates a finder with specified estimator.
*
- * @param delayConstraints Constraints for acceptable passenger delays
* @param beelineEstimator Estimator for beeline travel times
*/
- public InsertionPositionFinder(
- PassengerDelayConstraints delayConstraints,
- BeelineEstimator beelineEstimator
- ) {
- this.delayConstraints = delayConstraints;
+ public InsertionPositionFinder(BeelineEstimator beelineEstimator) {
this.beelineEstimator = beelineEstimator;
}
@@ -63,22 +52,27 @@ public InsertionPositionFinder(
* @param trip The carpool trip being evaluated
* @param passengerPickup Passenger's pickup location
* @param passengerDropoff Passenger's dropoff location
+ * @param stopDuration Dwell time added at each intermediate stop; used by the beeline delay
+ * heuristic so its cumulative-time estimates match the per-stop budget
+ * check used downstream
* @return List of viable insertion positions (may be empty)
*/
public List findViablePositions(
CarpoolTrip trip,
WgsCoordinate passengerPickup,
- WgsCoordinate passengerDropoff
+ WgsCoordinate passengerDropoff,
+ Duration stopDuration
) {
List routePoints = trip.routePoints();
- Duration[] beelineTimes = beelineEstimator.calculateCumulativeTimes(routePoints);
+ Duration[] beelineTimes = beelineEstimator.calculateCumulativeTimes(routePoints, stopDuration);
List viable = new ArrayList<>();
- // Pickup positions: 1 to routePoints.size()-1 (cannot pick up at position 0/origin)
+ // pickupPos/dropoffPos are 0-based indices of the passenger's stops in the modified route.
+ // Pickup cannot be at index 0 (that's the driver's origin).
for (int pickupPos = 1; pickupPos < routePoints.size(); pickupPos++) {
- // Dropoff positions: pickupPos+1 to routePoints.size() (can drop off up to and including destination)
+ // Dropoff must be after pickup. Max is routePoints.size() (appended after all original stops except the last).
for (int dropoffPos = pickupPos + 1; dropoffPos <= routePoints.size(); dropoffPos++) {
if (!trip.hasCapacityForInsertion(pickupPos, dropoffPos, 1)) {
LOG.trace(
@@ -90,42 +84,25 @@ public List findViablePositions(
}
if (
- !insertionMaintainsForwardProgress(
+ !passesBeelineDelayCheck(
routePoints,
+ beelineTimes,
+ passengerPickup,
+ passengerDropoff,
pickupPos,
dropoffPos,
- passengerPickup,
- passengerDropoff
+ trip,
+ stopDuration
)
) {
LOG.trace(
- "Insertion at pickup={}, dropoff={} rejected by directional check",
+ "Insertion at pickup={}, dropoff={} rejected by beeline delay heuristic",
pickupPos,
dropoffPos
);
continue;
}
- if (routePoints.size() > 2) {
- if (
- !passesBeelineDelayCheck(
- routePoints,
- beelineTimes,
- passengerPickup,
- passengerDropoff,
- pickupPos,
- dropoffPos
- )
- ) {
- LOG.trace(
- "Insertion at pickup={}, dropoff={} rejected by beeline delay heuristic",
- pickupPos,
- dropoffPos
- );
- continue;
- }
- }
-
viable.add(new InsertionPosition(pickupPos, dropoffPos));
}
}
@@ -133,95 +110,6 @@ public List findViablePositions(
return viable;
}
- /**
- * Checks if inserting pickup/dropoff points maintains forward progress.
- * Prevents backtracking by ensuring insertions don't cause the route
- * to deviate too far from its intended direction.
- *
- * @param routePoints Current route points
- * @param pickupPos Position to insert pickup (1-indexed)
- * @param dropoffPos Position to insert dropoff (1-indexed)
- * @param passengerPickup Passenger pickup coordinate
- * @param passengerDropoff Passenger dropoff coordinate
- * @return true if insertion maintains forward progress
- */
- private boolean insertionMaintainsForwardProgress(
- List routePoints,
- int pickupPos,
- int dropoffPos,
- WgsCoordinate passengerPickup,
- WgsCoordinate passengerDropoff
- ) {
- if (pickupPos > 0 && pickupPos < routePoints.size()) {
- WgsCoordinate prevPoint = routePoints.get(pickupPos - 1);
- WgsCoordinate nextPoint = routePoints.get(pickupPos);
-
- if (!maintainsForwardProgress(prevPoint, passengerPickup, nextPoint)) {
- return false;
- }
- }
-
- if (dropoffPos > 0 && dropoffPos <= routePoints.size()) {
- WgsCoordinate prevPoint;
- if (dropoffPos == pickupPos) {
- prevPoint = passengerPickup;
- } else if (dropoffPos - 1 < routePoints.size()) {
- prevPoint = routePoints.get(dropoffPos - 1);
- } else {
- return true;
- }
-
- if (dropoffPos < routePoints.size()) {
- WgsCoordinate nextPoint = routePoints.get(dropoffPos);
-
- return maintainsForwardProgress(prevPoint, passengerDropoff, nextPoint);
- }
- }
-
- return true;
- }
-
- /**
- * Checks if inserting a new point maintains forward progress.
- */
- private boolean maintainsForwardProgress(
- WgsCoordinate previous,
- WgsCoordinate newPoint,
- WgsCoordinate next
- ) {
- // Skip check if inserting at an existing point (newPoint equals next or previous)
- // This avoids undefined bearing calculations from a point to itself
- if (newPoint.equals(next) || newPoint.equals(previous)) {
- return true;
- }
-
- // Calculate intended direction (previous → next)
- double intendedBearing = DirectionUtils.getAzimuth(
- previous.asJtsCoordinate(),
- next.asJtsCoordinate()
- );
-
- // Calculate detour directions
- double bearingToNew = DirectionUtils.getAzimuth(
- previous.asJtsCoordinate(),
- newPoint.asJtsCoordinate()
- );
- double bearingFromNew = DirectionUtils.getAzimuth(
- newPoint.asJtsCoordinate(),
- next.asJtsCoordinate()
- );
-
- // Check deviations
- double deviationToNew = DirectionUtils.bearingDifference(intendedBearing, bearingToNew);
- double deviationFromNew = DirectionUtils.bearingDifference(intendedBearing, bearingFromNew);
-
- // Allow some deviation but not complete reversal
- return (
- deviationToNew <= FORWARD_PROGRESS_TOLERANCE_DEGREES &&
- deviationFromNew <= FORWARD_PROGRESS_TOLERANCE_DEGREES
- );
- }
-
/**
* Checks if an insertion position passes the beeline delay heuristic.
* This is a fast, optimistic check using straight-line distance estimates.
@@ -232,8 +120,9 @@ private boolean maintainsForwardProgress(
* @param originalBeelineTimes Beeline cumulative times for original route
* @param passengerPickup Passenger pickup location
* @param passengerDropoff Passenger dropoff location
- * @param pickupPos Pickup insertion position (1-indexed)
- * @param dropoffPos Dropoff insertion position (1-indexed)
+ * @param pickupPos 0-based index of the passenger's pickup in the modified route
+ * @param dropoffPos 0-based index of the passenger's dropoff in the modified route
+ * @param trip The carpool trip being evaluated
* @return true if insertion might satisfy delay constraints (proceed with A* routing)
*/
private boolean passesBeelineDelayCheck(
@@ -242,7 +131,9 @@ private boolean passesBeelineDelayCheck(
WgsCoordinate passengerPickup,
WgsCoordinate passengerDropoff,
int pickupPos,
- int dropoffPos
+ int dropoffPos,
+ CarpoolTrip trip,
+ Duration stopDuration
) {
// Build modified coordinate list with passenger stops inserted
List modifiedCoords = new ArrayList<>(originalCoords);
@@ -250,28 +141,18 @@ private boolean passesBeelineDelayCheck(
modifiedCoords.add(dropoffPos, passengerDropoff);
// Calculate beeline times for modified route
- Duration[] modifiedBeelineTimes = beelineEstimator.calculateCumulativeTimes(modifiedCoords);
-
- // Check delays at each existing stop (exclude boarding at 0 and alighting at end)
- for (int originalIndex = 1; originalIndex < originalCoords.size() - 1; originalIndex++) {
- int modifiedIndex = InsertionPosition.mapOriginalIndex(originalIndex, pickupPos, dropoffPos);
-
- Duration originalTime = originalBeelineTimes[originalIndex];
- Duration modifiedTime = modifiedBeelineTimes[modifiedIndex];
- Duration beelineDelay = modifiedTime.minus(originalTime);
-
- // If even the optimistic beeline estimate exceeds threshold, actual routing will too
- if (beelineDelay.compareTo(delayConstraints.getMaxDelay()) > 0) {
- LOG.trace(
- "Stop at position {} has beeline delay {}s (exceeds {}s threshold)",
- originalIndex,
- beelineDelay.getSeconds(),
- delayConstraints.getMaxDelay().getSeconds()
- );
- return false;
- }
- }
+ Duration[] modifiedBeelineTimes = beelineEstimator.calculateCumulativeTimes(
+ modifiedCoords,
+ stopDuration
+ );
- return true;
+ // If even the optimistic beeline estimate exceeds a stop's budget, actual routing will too
+ return PassengerDelayConstraints.satisfiesConstraints(
+ originalBeelineTimes,
+ modifiedBeelineTimes,
+ pickupPos,
+ dropoffPos,
+ trip.stops()
+ );
}
}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/service/DefaultCarpoolingService.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/service/DefaultCarpoolingService.java
index 6cbe2eb276d..202a33b4750 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/service/DefaultCarpoolingService.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/service/DefaultCarpoolingService.java
@@ -3,15 +3,12 @@
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Set;
-import org.opentripplanner.astar.model.GraphPath;
import org.opentripplanner.ext.carpooling.CarpoolingRepository;
import org.opentripplanner.ext.carpooling.CarpoolingService;
-import org.opentripplanner.ext.carpooling.constraints.PassengerDelayConstraints;
import org.opentripplanner.ext.carpooling.filter.FilterChain;
import org.opentripplanner.ext.carpooling.internal.CarpoolItineraryMapper;
import org.opentripplanner.ext.carpooling.routing.CarpoolAccessEgress;
@@ -27,7 +24,6 @@
import org.opentripplanner.ext.carpooling.util.BeelineEstimator;
import org.opentripplanner.ext.carpooling.util.StreetVertexUtils;
import org.opentripplanner.framework.model.TimeAndCost;
-import org.opentripplanner.graph_builder.module.nearbystops.StopResolver;
import org.opentripplanner.graph_builder.module.nearbystops.StreetNearbyStopFinder;
import org.opentripplanner.model.GenericLocation;
import org.opentripplanner.model.plan.Itinerary;
@@ -39,14 +35,13 @@
import org.opentripplanner.routing.api.response.RoutingErrorCode;
import org.opentripplanner.routing.error.RoutingValidationException;
import org.opentripplanner.routing.graphfinder.NearbyStop;
+import org.opentripplanner.routing.graphfinder.TransitServiceResolver;
import org.opentripplanner.routing.linking.LinkingContext;
import org.opentripplanner.street.geometry.WgsCoordinate;
import org.opentripplanner.street.linking.TemporaryVerticesContainer;
import org.opentripplanner.street.linking.VertexLinker;
import org.opentripplanner.street.model.StreetMode;
-import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.vertex.Vertex;
-import org.opentripplanner.street.search.state.State;
import org.opentripplanner.street.service.StreetLimitationParametersService;
import org.opentripplanner.transit.model.site.AreaStop;
import org.opentripplanner.transit.service.TransitService;
@@ -67,10 +62,10 @@
* The service executes routing requests in three distinct phases:
*
*
Pre-filtering ({@link FilterChain}): Quickly eliminates incompatible
- * trips based on capacity, time windows, direction, and distance.
+ * trips based on capacity, time windows, and distance.
*
Position Finding ({@link InsertionPositionFinder}): For trips that
* pass filtering, identifies viable pickup/dropoff position pairs using fast heuristics
- * (capacity, direction, beeline delay estimates). No routing is performed in this phase.
+ * (capacity, beeline delay estimates). No routing is performed in this phase.
*
Insertion Evaluation ({@link InsertionEvaluator}): For viable positions,
* computes actual routes using A* street routing. Evaluates all feasible insertion positions
* and selects the one minimizing additional travel time while satisfying delay constraints.
@@ -95,17 +90,9 @@
public class DefaultCarpoolingService implements CarpoolingService {
private static final Logger LOG = LoggerFactory.getLogger(DefaultCarpoolingService.class);
- static final int DEFAULT_MAX_CARPOOL_DIRECT_RESULTS = 3;
private static final Duration DEFAULT_SEARCH_WINDOW = Duration.ofMinutes(30);
// How far away in time a carpooling trip can be from the requested departure time to be considered
private static final Duration ACCESS_EGRESS_SEARCH_WINDOW = Duration.ofHours(12);
- /*
- The time it takes to pick up or drop off a passenger and start driving again.
- It is only used for access/egress, and is a temporary solution.
- The implementation will be changed for both direct and access/egress when implementing the field
- latestExpectedArrivalTime from siri.
- */
- static final Duration CARPOOL_STOP_DURATION = Duration.ofMinutes(1);
/*
This is needed for managing computational complexity unless we find a smarter way of searching
for nearby stops.
@@ -116,7 +103,6 @@ public class DefaultCarpoolingService implements CarpoolingService {
private final StreetLimitationParametersService streetLimitationParametersService;
private final FilterChain preFilters;
private final CarpoolItineraryMapper itineraryMapper;
- private final PassengerDelayConstraints delayConstraints;
private final InsertionPositionFinder positionFinder;
private final VertexLinker vertexLinker;
@@ -142,9 +128,8 @@ public DefaultCarpoolingService(
this.repository = repository;
this.streetLimitationParametersService = streetLimitationParametersService;
this.preFilters = FilterChain.standard();
- this.itineraryMapper = new CarpoolItineraryMapper(transitService.getTimeZone());
- this.delayConstraints = new PassengerDelayConstraints();
- this.positionFinder = new InsertionPositionFinder(delayConstraints, new BeelineEstimator());
+ this.itineraryMapper = new CarpoolItineraryMapper();
+ this.positionFinder = new InsertionPositionFinder(new BeelineEstimator());
this.vertexLinker = vertexLinker;
}
@@ -154,21 +139,18 @@ public DefaultCarpoolingService(
* This method executes the full three-phase carpooling algorithm:
*
*
Pre-filtering: All trips from the repository are filtered by capacity,
- * time window, direction, and distance to quickly eliminate incompatible matches.
+ * time window, and distance to quickly eliminate incompatible matches.
*
Position finding: For each surviving trip, viable pickup/dropoff
* insertion positions are identified using beeline heuristics (no routing).
*
Insertion evaluation: Viable positions are evaluated with A* street
* routing to find the insertion that minimizes additional driver travel time while
* respecting delay constraints.
*
- *
- * Results are sorted by additional travel time and limited to
- * {@value #DEFAULT_MAX_CARPOOL_DIRECT_RESULTS} itineraries.
*
* @param request the routing request. Must have {@link StreetMode#CARPOOL} as the direct mode.
* @param linkingContext pre-linked vertices for the passenger's origin and destination
- * @return a list of carpool itineraries sorted by additional travel time, or an empty list
- * if no viable matches are found or the direct mode is not CARPOOL
+ * @return a list of carpool itineraries, or an empty list if no viable matches are found
+ * or the direct mode is not CARPOOL
* @throws RoutingValidationException if origin or destination coordinates are missing
*/
@Override
@@ -226,11 +208,13 @@ public List routeDirect(RouteRequest request, LinkingContext linkingC
var streetVertexUtils = new StreetVertexUtils(this.vertexLinker, temporaryVerticesContainer);
+ var stopDuration = request.preferences().car().pickupTime();
+
var insertionEvaluator = new InsertionEvaluator(
- delayConstraints,
linkingContext,
streetVertexUtils,
- router
+ router,
+ stopDuration
);
// Find optimal insertions for remaining trips
@@ -240,7 +224,8 @@ public List routeDirect(RouteRequest request, LinkingContext linkingC
List viablePositions = positionFinder.findViablePositions(
trip,
passengerPickup,
- passengerDropoff
+ passengerDropoff,
+ stopDuration
);
if (viablePositions.isEmpty()) {
@@ -274,15 +259,13 @@ public List routeDirect(RouteRequest request, LinkingContext linkingC
);
})
.filter(Objects::nonNull)
- .sorted(Comparator.comparing(InsertionCandidate::additionalDuration))
- .limit(DEFAULT_MAX_CARPOOL_DIRECT_RESULTS)
.toList();
LOG.debug("Found {} viable insertion candidates", insertionCandidates.size());
itineraries = insertionCandidates
.stream()
- .map(candidate -> itineraryMapper.toItinerary(request, candidate))
+ .map(itineraryMapper::toItinerary)
.filter(Objects::nonNull)
.toList();
}
@@ -314,7 +297,7 @@ public List routeDirect(RouteRequest request, LinkingContext linkingC
* @param streetRequest
* @param accessOrEgress whether this is an access leg (origin to transit) or egress leg
* (transit to destination)
- * @param stopResolver used for nearby stop search
+ * @param transitServiceResolver used for nearby stop search
* @param linkingContext pre-linked vertices for the passenger's origin and destination
* @param transitSearchTimeZero the reference time for computing relative start/end times
* used by Raptor
@@ -327,7 +310,7 @@ public List routeAccessEgress(
RouteRequest request,
StreetRequest streetRequest,
AccessEgressType accessOrEgress,
- StopResolver stopResolver,
+ TransitServiceResolver transitServiceResolver,
LinkingContext linkingContext,
ZonedDateTime transitSearchTimeZero
) throws RoutingValidationException {
@@ -353,10 +336,7 @@ public List routeAccessEgress(
or the passenger's destination if the request is for egress
*/
GenericLocation passengerLocation = accessOrEgress.isAccess() ? request.from() : request.to();
- WgsCoordinate passengerCoordinates = new WgsCoordinate(
- passengerLocation.lat,
- passengerLocation.lng
- );
+ WgsCoordinate passengerCoordinates = passengerLocation.wgsCoordinate();
var passengerDepartureTime = request.dateTime();
@@ -391,7 +371,6 @@ public List routeAccessEgress(
}
var streetNearbyStopFinder = StreetNearbyStopFinder.of(
- stopResolver,
MAX_SEARCH_DURATION_FOR_NEARBY_STOPS_FOR_ACCESS_EGRESS,
0
);
@@ -405,7 +384,7 @@ public List routeAccessEgress(
accessOrEgress.isEgress()
)
.stream()
- .filter(stop -> !(stop.stop instanceof AreaStop))
+ .filter(stop -> !(transitServiceResolver.getStopLocation(stop.stopId) instanceof AreaStop))
.toList();
var nearbyStopsWithVertices = new HashMap();
@@ -449,11 +428,13 @@ public List routeAccessEgress(
});
});
+ var stopDuration = request.preferences().car().pickupTime();
+
var insertionEvaluator = new InsertionEvaluator(
- delayConstraints,
linkingContext,
streetVertexUtils,
- carpoolTreeVertexRouter
+ carpoolTreeVertexRouter,
+ stopDuration
);
var candidateTripsWithViableStopsAndPositions = candidateTripsWithVertices
@@ -463,17 +444,19 @@ public List routeAccessEgress(
.keySet()
.stream()
.map(nearbyStop -> {
+ var stop = transitServiceResolver.getStopLocation(nearbyStop.stopId);
var pickUpCoord = accessOrEgress.isAccess()
? passengerCoordinates
- : nearbyStop.stop.getCoordinate();
+ : stop.getCoordinate();
var dropOffCoord = accessOrEgress.isAccess()
- ? nearbyStop.stop.getCoordinate()
+ ? stop.getCoordinate()
: passengerCoordinates;
var viablePositions = positionFinder.findViablePositions(
tripWithVertices.trip(),
pickUpCoord,
- dropOffCoord
+ dropOffCoord,
+ stopDuration
);
return new ViableAccessEgress(
nearbyStop,
@@ -498,6 +481,7 @@ public List routeAccessEgress(
.stream()
.map(it ->
createCarpoolAccessEgress(
+ transitServiceResolver,
it,
transitSearchTimeZero,
/*
@@ -514,55 +498,30 @@ public List routeAccessEgress(
private void validateRequest(RouteRequest request) throws RoutingValidationException {
Objects.requireNonNull(request.from());
Objects.requireNonNull(request.to());
- if (request.from().lat == null || request.from().lng == null) {
+ if (request.from().wgsCoordinate() == null) {
throw new RoutingValidationException(
List.of(new RoutingError(RoutingErrorCode.LOCATION_NOT_FOUND, InputField.FROM_PLACE))
);
}
- if (request.to().lat == null || request.to().lng == null) {
+ if (request.to().wgsCoordinate() == null) {
throw new RoutingValidationException(
List.of(new RoutingError(RoutingErrorCode.LOCATION_NOT_FOUND, InputField.TO_PLACE))
);
}
}
- private Duration getTotalDurationOfSegments(
- List> segments,
- Duration extraTimeForStop
- ) {
- return segments
- .stream()
- .map(it -> Duration.between(it.states.getFirst().getTime(), it.states.getLast().getTime()))
- .reduce(Duration.ZERO, Duration::plus)
- .plus(extraTimeForStop.multipliedBy(segments.size() - 1));
- }
-
private CarpoolAccessEgress createCarpoolAccessEgress(
+ TransitServiceResolver transitServiceResolver,
InsertionCandidate insertionCandidate,
ZonedDateTime transitSearchTimeZero,
Double carpoolReluctance
) {
- var pickUpIndex = insertionCandidate.pickupPosition();
- var dropOffIndex = insertionCandidate.dropoffPosition() - 1;
-
- var segmentsBeforeInsertion = insertionCandidate.routeSegments().subList(0, pickUpIndex);
- var segmentsWithPassenger = insertionCandidate
- .routeSegments()
- .subList(pickUpIndex, dropOffIndex + 1);
+ var sharedSegments = insertionCandidate.getSharedSegments();
+ var durationUntilPickup = insertionCandidate.getDurationUntilPickupArrival();
+ var passengerRideDuration = insertionCandidate.getPassengerRideDuration();
- var durationBeforeInsertion = getTotalDurationOfSegments(
- segmentsBeforeInsertion,
- CARPOOL_STOP_DURATION
- );
-
- // Adding an extra CARPOOL_STOP_DURATION for the time it takes to pick up the passenger
- var durationWithPassenger = getTotalDurationOfSegments(
- segmentsWithPassenger,
- CARPOOL_STOP_DURATION
- ).plus(CARPOOL_STOP_DURATION);
-
- var startTimeOfSegment = insertionCandidate.trip().startTime().plus(durationBeforeInsertion);
- var endTimeOfSegment = startTimeOfSegment.plus(durationWithPassenger);
+ var startTimeOfSegment = insertionCandidate.trip().startTime().plus(durationUntilPickup);
+ var endTimeOfSegment = startTimeOfSegment.plus(passengerRideDuration);
var relativeStartTime = TimeUtils.toTransitTimeSeconds(
transitSearchTimeZero,
@@ -573,16 +532,14 @@ private CarpoolAccessEgress createCarpoolAccessEgress(
endTimeOfSegment.toInstant()
);
- var accessEgress = new CarpoolAccessEgress(
- insertionCandidate.transitStop().stop.getIndex(),
- durationWithPassenger,
+ return new CarpoolAccessEgress(
+ transitServiceResolver.getStopLocation(insertionCandidate.transitStop().stopId).getIndex(),
+ passengerRideDuration,
relativeStartTime,
relativeEndTime,
- segmentsWithPassenger,
+ sharedSegments,
TimeAndCost.ZERO,
carpoolReluctance
);
-
- return accessEgress;
}
}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/updater/CarpoolSiriMapper.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/updater/CarpoolSiriMapper.java
index 0f1536eb5b6..246ca26ffdf 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/updater/CarpoolSiriMapper.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/updater/CarpoolSiriMapper.java
@@ -1,5 +1,9 @@
package org.opentripplanner.ext.carpooling.updater;
+import static org.opentripplanner.ext.carpooling.model.CarpoolStop.DEFAULT_ONBOARD_COUNT;
+import static org.opentripplanner.ext.carpooling.model.CarpoolTrip.DEFAULT_TOTAL_CAPACITY;
+
+import java.math.BigInteger;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
@@ -10,10 +14,8 @@
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Polygon;
-import org.opentripplanner.core.model.i18n.I18NString;
import org.opentripplanner.core.model.id.FeedScopedId;
import org.opentripplanner.ext.carpooling.model.CarpoolStop;
-import org.opentripplanner.ext.carpooling.model.CarpoolStopType;
import org.opentripplanner.ext.carpooling.model.CarpoolTrip;
import org.opentripplanner.ext.carpooling.model.CarpoolTripBuilder;
import org.opentripplanner.street.geometry.WgsCoordinate;
@@ -24,18 +26,16 @@
import uk.org.siri.siri21.EstimatedCall;
import uk.org.siri.siri21.EstimatedVehicleJourney;
+/**
+ * Maps SIRI EstimatedVehicleJourney messages to {@link CarpoolTrip} instances.
+ * Extracts stop geometry, timing, capacity and occupancy from the SIRI data.
+ */
public class CarpoolSiriMapper {
private static final Logger LOG = LoggerFactory.getLogger(CarpoolSiriMapper.class);
private static final String FEED_ID = "ENT";
private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
- private static final int DEFAULT_AVAILABLE_SEATS = 5;
- private static final Duration DEFAULT_DEVIATION_BUDGET = Duration.ofMinutes(15);
- // INDEX is not relevant for our stop type. Also set index to a hard coded value to avoid
- // run-away memory use if it by error ends up in global repositories.
- public static final int CARPOOLING_DUMMY_INDEX = -9_999;
-
public CarpoolTrip mapSiriToCarpoolTrip(EstimatedVehicleJourney journey) {
var calls = journey.getEstimatedCalls().getEstimatedCalls();
if (calls.size() < 2) {
@@ -70,14 +70,13 @@ public CarpoolTrip mapSiriToCarpoolTrip(EstimatedVehicleJourney journey) {
? lastStop.getExpectedArrivalTime()
: lastStop.getAimedArrivalTime();
+ int totalCapacity = extractTotalCapacity(tripId, calls);
+
return new CarpoolTripBuilder(new FeedScopedId(FEED_ID, tripId))
.withStartTime(startTime)
.withEndTime(endTime)
.withProvider(journey.getOperatorRef().getValue())
- // TODO: Find a better way to exchange deviation budget with providers.
- .withDeviationBudget(DEFAULT_DEVIATION_BUDGET)
- // TODO: Make available seats dynamic based on EstimatedVehicleJourney data
- .withAvailableSeats(DEFAULT_AVAILABLE_SEATS)
+ .withTotalCapacity(totalCapacity)
.withStops(stops)
.build();
}
@@ -87,7 +86,7 @@ public CarpoolTrip mapSiriToCarpoolTrip(EstimatedVehicleJourney journey) {
*
* @param call The SIRI EstimatedCall containing stop information
* @param tripId The trip ID for generating unique stop IDs
- * @param sequenceNumber The 0-based sequence number of this stop
+ * @param stopIndex The 0-based index of this stop in the call list
* @param isFirst true if this is the first stop (origin)
* @param isLast true if this is the last stop (destination)
* @return A CarpoolStop representing the stop
@@ -95,7 +94,7 @@ public CarpoolTrip mapSiriToCarpoolTrip(EstimatedVehicleJourney journey) {
private CarpoolStop buildCarpoolStopForPosition(
EstimatedCall call,
String tripId,
- int sequenceNumber,
+ int stopIndex,
boolean isFirst,
boolean isLast
) {
@@ -103,49 +102,130 @@ private CarpoolStop buildCarpoolStopForPosition(
? tripId + "_trip_origin"
: isLast
? tripId + "_trip_destination"
- : tripId + "_stop_" + sequenceNumber;
+ : tripId + "_stop_" + stopIndex;
- return toCarpoolStop(call, stopId, isFirst, isLast);
+ return toCarpoolStop(call, stopId, tripId, isFirst, isLast);
}
/**
- * Determine the carpool stop type from the EstimatedCall data.
+ * Extracts the total capacity from the EstimatedCalls' ExpectedDepartureCapacities.
+ * Only the first element of each call's capacities list is inspected; additional
+ * entries are ignored. Uses the value from the first call that has it. Logs a warning
+ * if different calls report different capacity values. Returns
+ * {@link CarpoolTrip#DEFAULT_TOTAL_CAPACITY} if no call has capacity data or if the value is invalid.
*/
- private CarpoolStopType determineCarpoolStopType(EstimatedCall call) {
- boolean hasArrival =
- call.getExpectedArrivalTime() != null || call.getAimedArrivalTime() != null;
- boolean hasDeparture =
- call.getExpectedDepartureTime() != null || call.getAimedDepartureTime() != null;
-
- if (hasArrival && hasDeparture) {
- return CarpoolStopType.PICKUP_AND_DROP_OFF;
- } else if (hasDeparture) {
- return CarpoolStopType.PICKUP_ONLY;
- } else if (hasArrival) {
- return CarpoolStopType.DROP_OFF_ONLY;
- } else {
- return CarpoolStopType.PICKUP_AND_DROP_OFF;
+ private int extractTotalCapacity(String tripId, List calls) {
+ Integer firstCapacity = null;
+ int firstCapacityIndex = -1;
+
+ for (int i = 0; i < calls.size(); i++) {
+ var capacities = calls.get(i).getExpectedDepartureCapacities();
+ if (capacities == null || capacities.isEmpty()) {
+ continue;
+ }
+ BigInteger value = capacities.getFirst().getTotalCapacity();
+ if (value == null) {
+ continue;
+ }
+ int intValue = value.intValue();
+ if (firstCapacity == null) {
+ firstCapacity = intValue;
+ firstCapacityIndex = i;
+ } else if (intValue != firstCapacity) {
+ LOG.warn(
+ "Trip {}: totalCapacity differs between calls (call {} has {}, call {} has {})",
+ tripId,
+ firstCapacityIndex,
+ firstCapacity,
+ i,
+ intValue
+ );
+ }
+ }
+
+ if (firstCapacity == null) {
+ return DEFAULT_TOTAL_CAPACITY;
+ }
+ if (firstCapacity <= 0) {
+ LOG.warn(
+ "Trip {}: invalid totalCapacity {} at call {}, using default {}",
+ tripId,
+ firstCapacity,
+ firstCapacityIndex,
+ DEFAULT_TOTAL_CAPACITY
+ );
+ return DEFAULT_TOTAL_CAPACITY;
}
+ return firstCapacity;
}
/**
- * Calculate the passenger delta (change in passenger count) from the EstimatedCall.
+ * Extracts the onboard count from the EstimatedCall's ExpectedDepartureOccupancies.
+ * Only the first element of the occupancies list is inspected; additional entries are
+ * ignored. Returns {@link CarpoolStop#DEFAULT_ONBOARD_COUNT} if not present or if the value is invalid.
*/
- private int calculatePassengerDelta(EstimatedCall call, CarpoolStopType stopType) {
- // This is a placeholder implementation - adapt based on SIRI ET data structure
- // SIRI ET may have passenger count changes, boarding/alighting numbers, etc.
-
- // For now, return a default value of 1 passenger pickup/dropoff
- if (stopType == CarpoolStopType.DROP_OFF_ONLY) {
- // Assume 1 passenger drop-off
- return -1;
- } else if (stopType == CarpoolStopType.PICKUP_ONLY) {
- // Assume 1 passenger pickup
- return 1;
- } else {
- // No net change for both pickup and drop-off
- return 0;
+ private int extractOnboardCount(String tripId, EstimatedCall call) {
+ var occupancies = call.getExpectedDepartureOccupancies();
+ if (occupancies != null && !occupancies.isEmpty()) {
+ BigInteger onboardCount = occupancies.getFirst().getOnboardCount();
+ if (onboardCount != null) {
+ int value = onboardCount.intValue();
+ if (value <= 0) {
+ LOG.warn(
+ "Trip {}: invalid onboardCount {}, using default {}",
+ tripId,
+ value,
+ DEFAULT_ONBOARD_COUNT
+ );
+ return DEFAULT_ONBOARD_COUNT;
+ }
+ return value;
+ }
+ }
+ return DEFAULT_ONBOARD_COUNT;
+ }
+
+ /**
+ * Extracts the deviation budget from the EstimatedCall by computing the difference between
+ * {@code latestExpectedArrivalTime} and the arrival time ({@code expectedArrivalTime} if
+ * present, otherwise {@code aimedArrivalTime}).
+ *
+ * The result is the remaining slack at this stop, not an initial contract:
+ * {@code expectedArrivalTime} already reflects detours committed by prior passenger
+ * insertions, and {@code latestExpectedArrivalTime} is the unchanged commitment to the
+ * passenger at this stop. Each time this mapper runs against a fresh SIRI snapshot, the
+ * extracted value therefore shrinks in step with the consumed slack.
+ *
+ * Fallbacks:
+ *
+ *
Returns {@link CarpoolStop#DEFAULT_DEVIATION_BUDGET} if either timestamp is missing.
+ * This is intentionally permissive — the absence of a commitment should not block
+ * insertions.
+ *
Returns {@link Duration#ZERO} (and logs a warning) if {@code latestExpectedArrivalTime}
+ * is before the arrival time — the schedule has slipped past the commitment, so no
+ * further deviation is acceptable.
+ *
+ */
+ private Duration extractDeviationBudget(EstimatedCall call) {
+ var latestExpected = call.getLatestExpectedArrivalTime();
+ var arrivalTime = call.getExpectedArrivalTime() != null
+ ? call.getExpectedArrivalTime()
+ : call.getAimedArrivalTime();
+
+ if (latestExpected == null || arrivalTime == null) {
+ return CarpoolStop.DEFAULT_DEVIATION_BUDGET;
+ }
+
+ Duration budget = Duration.between(arrivalTime, latestExpected);
+ if (budget.isNegative()) {
+ LOG.warn(
+ "latestExpectedArrivalTime ({}) is before arrivalTime ({}), using zero deviation budget",
+ latestExpected,
+ arrivalTime
+ );
+ return Duration.ZERO;
}
+ return budget;
}
/**
@@ -201,9 +281,15 @@ private void validateEstimatedCallOrder(List calls) {
}
}
+ /**
+ * Builds a {@link CarpoolStop} from a SIRI call. The origin (when {@code isFirst} is true)
+ * always gets {@link Duration#ZERO} as its deviation budget — the trip cannot start later
+ * than scheduled — regardless of any value extracted from the call.
+ */
private CarpoolStop toCarpoolStop(
EstimatedCall call,
String id,
+ String tripId,
boolean isFirst,
boolean isLast
) {
@@ -214,24 +300,15 @@ private CarpoolStop toCarpoolStop(
? toWgsCoordinate(toPolygon(legacyGeometry))
: toWgsCoordinate(circleLocation);
- CarpoolStopType stopType;
- if (isFirst) {
- stopType = CarpoolStopType.PICKUP_ONLY;
- } else if (isLast) {
- stopType = CarpoolStopType.DROP_OFF_ONLY;
- } else {
- stopType = determineCarpoolStopType(call);
- }
-
- return CarpoolStop.of(new FeedScopedId(FEED_ID, id), () -> CARPOOLING_DUMMY_INDEX)
- .withName(I18NString.of(call.getStopPointNames().getFirst().getValue()))
+ return CarpoolStop.of(new FeedScopedId(FEED_ID, id))
.withCoordinate(centroid)
- .withCarpoolStopType(stopType)
.withAimedDepartureTime(isLast ? null : call.getAimedDepartureTime())
.withExpectedDepartureTime(isLast ? null : call.getExpectedDepartureTime())
.withAimedArrivalTime(isFirst ? null : call.getAimedArrivalTime())
.withExpectedArrivalTime(isFirst ? null : call.getExpectedArrivalTime())
- .withPassengerDelta(isLast ? 0 : calculatePassengerDelta(call, stopType))
+ .withLatestExpectedArrivalTime(isFirst ? null : call.getLatestExpectedArrivalTime())
+ .withOnboardCount(extractOnboardCount(tripId, call))
+ .withDeviationBudget(isFirst ? Duration.ZERO : extractDeviationBudget(call))
.build();
}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/util/BeelineEstimator.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/util/BeelineEstimator.java
index 8b00c19f681..96fd7f1da72 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/util/BeelineEstimator.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/util/BeelineEstimator.java
@@ -85,25 +85,22 @@ public Duration estimateDuration(WgsCoordinate from, WgsCoordinate to) {
}
/**
- * Calculates cumulative travel times to each point in a route.
- * Returns an array where index i contains the cumulative duration from the start to point i.
+ * Calculates cumulative travel times to each point in a route, including stop duration
+ * at each intermediate point.
*
* @param points Route points in order
+ * @param stopDuration Duration added at each intermediate stop (not at the first point)
* @return Array of cumulative durations (first element is always Duration.ZERO)
*/
- public Duration[] calculateCumulativeTimes(List points) {
+ public Duration[] calculateCumulativeTimes(List points, Duration stopDuration) {
if (points.isEmpty()) {
return new Duration[0];
}
- Duration[] cumulativeTimes = new Duration[points.size()];
- cumulativeTimes[0] = Duration.ZERO;
-
- for (int i = 0; i < points.size() - 1; i++) {
- Duration segmentDuration = estimateDuration(points.get(i), points.get(i + 1));
- cumulativeTimes[i + 1] = cumulativeTimes[i].plus(segmentDuration);
+ Duration[] segmentDurations = new Duration[points.size() - 1];
+ for (int i = 0; i < segmentDurations.length; i++) {
+ segmentDurations[i] = estimateDuration(points.get(i), points.get(i + 1));
}
-
- return cumulativeTimes;
+ return GraphPathUtils.calculateCumulativeDurations(segmentDurations, stopDuration);
}
}
diff --git a/application/src/ext/java/org/opentripplanner/ext/carpooling/util/GraphPathUtils.java b/application/src/ext/java/org/opentripplanner/ext/carpooling/util/GraphPathUtils.java
index 12bb9449238..169408af479 100644
--- a/application/src/ext/java/org/opentripplanner/ext/carpooling/util/GraphPathUtils.java
+++ b/application/src/ext/java/org/opentripplanner/ext/carpooling/util/GraphPathUtils.java
@@ -6,18 +6,59 @@
import org.opentripplanner.street.model.vertex.Vertex;
import org.opentripplanner.street.search.state.State;
-public class GraphPathUtils {
+public final class GraphPathUtils {
+
+ private GraphPathUtils() {}
+
+ /**
+ * Calculates cumulative durations from pre-routed segments, including stop duration
+ * at each intermediate stop.
+ *
+ * @param segments Pre-routed segments
+ * @param stopDuration Duration added at each intermediate stop
+ */
+ public static Duration[] calculateCumulativeDurations(
+ GraphPath[] segments,
+ Duration stopDuration
+ ) {
+ Duration[] segmentDurations = new Duration[segments.length];
+ for (int i = 0; i < segments.length; i++) {
+ segmentDurations[i] = calculateDuration(segments[i]);
+ }
+ return calculateCumulativeDurations(segmentDurations, stopDuration);
+ }
/**
- * Calculates cumulative durations from pre-routed segments.
+ * Calculates cumulative arrival times from segment durations, including a stop delay
+ * at each intermediate point. The stop delay is added before each segment
+ * except the first, modelling time spent at an intermediate stop before departing
+ * to the next point. No delay is added at the origin (before segment 0) or after the
+ * final segment.
+ *
+ * Given N segments, the result has N+1 entries:
+ *