Skip to content

Commit b5061df

Browse files
committed
fix for calculation of carpool trip duration when having more than 2 stops
1 parent 75d76fd commit b5061df

File tree

4 files changed

+125
-45
lines changed

4 files changed

+125
-45
lines changed

application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionCandidate.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ public record InsertionCandidate(
2222
int pickupPosition,
2323
int dropoffPosition,
2424
List<GraphPath<State, Edge, Vertex>> routeSegments,
25-
Duration baselineDuration,
25+
Duration durationBetweenOriginAndDestination,
2626
Duration totalDuration
2727
) {
2828
/**
29-
* Calculates the additional duration caused by inserting this passenger.
29+
* Calculates the difference between the total duration when inserting this passenger,
30+
* and the duration when driving directly from the start to the end of the trip.
3031
*/
3132
public Duration additionalDuration() {
32-
return totalDuration.minus(baselineDuration);
33+
return totalDuration.minus(durationBetweenOriginAndDestination);
3334
}
3435

3536
/**

application/src/ext/java/org/opentripplanner/ext/carpooling/routing/InsertionEvaluator.java

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.opentripplanner.ext.carpooling.routing;
22

33
import static org.opentripplanner.ext.carpooling.util.GraphPathUtils.calculateCumulativeDurations;
4+
import static org.opentripplanner.ext.carpooling.util.GraphPathUtils.calculateDuration;
45

56
import java.time.Duration;
67
import java.util.ArrayList;
@@ -57,24 +58,18 @@ public InsertionEvaluator(
5758
}
5859

5960
/**
60-
* Routes all baseline segments and caches the results.
61+
* Routes all segments of routePoints.
6162
*
6263
* @return Array of routed segments, or null if any segment fails to route
6364
*/
6465
@SuppressWarnings("unchecked")
65-
private GraphPath<State, Edge, Vertex>[] routeBaselineSegments(List<WgsCoordinate> routePoints) {
66+
private GraphPath<State, Edge, Vertex>[] routeSegments(List<WgsCoordinate> routePoints) {
6667
GraphPath<State, Edge, Vertex>[] segments = new GraphPath[routePoints.size() - 1];
6768

6869
for (int i = 0; i < routePoints.size() - 1; i++) {
6970
var fromCoord = routePoints.get(i);
7071
var toCoord = routePoints.get(i + 1);
71-
GenericLocation from = GenericLocation.fromCoordinate(
72-
fromCoord.latitude(),
73-
fromCoord.longitude()
74-
);
75-
GenericLocation to = GenericLocation.fromCoordinate(toCoord.latitude(), toCoord.longitude());
76-
77-
GraphPath<State, Edge, Vertex> segment = routingFunction.route(from, to, linkingContext);
72+
GraphPath<State, Edge, Vertex> segment = routeSegment(fromCoord, toCoord);
7873
if (segment == null) {
7974
LOG.debug("Baseline routing failed for segment {} → {}", i, i + 1);
8075
return null;
@@ -86,6 +81,23 @@ private GraphPath<State, Edge, Vertex>[] routeBaselineSegments(List<WgsCoordinat
8681
return segments;
8782
}
8883

84+
/**
85+
* Routes from start to stop
86+
*
87+
* @return route from coordinate from to coordinate to, or null if routing function fails
88+
*/
89+
private GraphPath<State, Edge, Vertex> routeSegment(WgsCoordinate from, WgsCoordinate to) {
90+
GenericLocation fromGenericLocation = GenericLocation.fromCoordinate(
91+
from.latitude(),
92+
from.longitude()
93+
);
94+
GenericLocation toGenericLocation = GenericLocation.fromCoordinate(
95+
to.latitude(),
96+
to.longitude()
97+
);
98+
return routingFunction.route(fromGenericLocation, toGenericLocation, linkingContext);
99+
}
100+
89101
/**
90102
* Evaluates pre-filtered insertion positions using A* routing.
91103
* <p>
@@ -107,17 +119,28 @@ public InsertionCandidate findBestInsertion(
107119
WgsCoordinate passengerPickup,
108120
WgsCoordinate passengerDropoff
109121
) {
110-
GraphPath<State, Edge, Vertex>[] baselineSegments = routeBaselineSegments(trip.routePoints());
122+
GraphPath<State, Edge, Vertex>[] baselineSegments = routeSegments(trip.routePoints());
111123
if (baselineSegments == null) {
112124
LOG.warn("Could not route baseline for trip {}", trip.getId());
113125
return null;
114126
}
115127

116128
Duration[] cumulativeDurations = calculateCumulativeDurations(baselineSegments);
117129

130+
GraphPath<State, Edge, Vertex> pathBetweenStartAndStop = routeSegment(
131+
trip.stops().getFirst().getCoordinate(),
132+
trip.stops().getLast().getCoordinate()
133+
);
134+
135+
if (pathBetweenStartAndStop == null) {
136+
LOG.warn("Could not create route between start and stop for trip {}", trip.getId());
137+
return null;
138+
}
139+
140+
Duration durationBetweenOriginAndDestination = calculateDuration(pathBetweenStartAndStop);
141+
118142
InsertionCandidate bestCandidate = null;
119143
Duration minAdditionalDuration = INITIAL_ADDITIONAL_DURATION;
120-
Duration baselineDuration = cumulativeDurations[cumulativeDurations.length - 1];
121144

122145
for (InsertionPosition position : viablePositions) {
123146
InsertionCandidate candidate = evaluateInsertion(
@@ -128,7 +151,7 @@ public InsertionCandidate findBestInsertion(
128151
passengerDropoff,
129152
baselineSegments,
130153
cumulativeDurations,
131-
baselineDuration
154+
durationBetweenOriginAndDestination
132155
);
133156

134157
if (candidate == null) {
@@ -168,7 +191,7 @@ private InsertionCandidate evaluateInsertion(
168191
WgsCoordinate passengerDropoff,
169192
GraphPath<State, Edge, Vertex>[] baselineSegments,
170193
Duration[] originalCumulativeDurations,
171-
Duration baselineDuration
194+
Duration durationBetweenOriginAndDestination
172195
) {
173196
// Build modified route segments by reusing cached baseline segments
174197
List<GraphPath<State, Edge, Vertex>> modifiedSegments = buildModifiedSegments(
@@ -217,7 +240,7 @@ private InsertionCandidate evaluateInsertion(
217240
pickupPos,
218241
dropoffPos,
219242
modifiedSegments,
220-
baselineDuration,
243+
durationBetweenOriginAndDestination,
221244
totalDuration
222245
);
223246
}

application/src/ext/java/org/opentripplanner/ext/carpooling/util/GraphPathUtils.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,20 @@ public static Duration[] calculateCumulativeDurations(GraphPath<State, Edge, Ver
1616
cumulativeDurations[0] = Duration.ZERO;
1717

1818
for (int i = 0; i < segments.length; i++) {
19-
Duration segmentDuration = Duration.between(
20-
segments[i].states.getFirst().getTime(),
21-
segments[i].states.getLast().getTime()
22-
);
19+
Duration segmentDuration = calculateDuration(segments[i]);
2320
cumulativeDurations[i + 1] = cumulativeDurations[i].plus(segmentDuration);
2421
}
2522

2623
return cumulativeDurations;
2724
}
25+
26+
/**
27+
* Calculates duration for a segment
28+
*/
29+
public static Duration calculateDuration(GraphPath<State, Edge, Vertex> segment) {
30+
return Duration.between(
31+
segment.states.getFirst().getTime(),
32+
segment.states.getLast().getTime()
33+
);
34+
}
2835
}

application/src/test/java/org/opentripplanner/ext/carpooling/routing/InsertionEvaluatorTest.java

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,34 @@ private InsertionCandidate findOptimalInsertion(
7070
return evaluator.findBestInsertion(trip, viablePositions, passengerPickup, passengerDropoff);
7171
}
7272

73+
private WgsCoordinate getCoordinateBetween(WgsCoordinate coordinate1, WgsCoordinate coordinate2) {
74+
return new WgsCoordinate(
75+
(coordinate1.latitude() + coordinate2.latitude()) / 2,
76+
(coordinate1.longitude() + coordinate2.longitude()) / 2
77+
);
78+
}
79+
80+
@Test
81+
void findOptimalInsertion_onDeviationBudgetExceeded_returnsNull() {
82+
var trip = createTripWithStops(
83+
OSLO_SOUTH,
84+
List.of(createStopAt(0, OSLO_CENTER), createStopAt(0, OSLO_NORTHEAST)),
85+
OSLO_NORTH
86+
);
87+
88+
var mockPath = createGraphPath(Duration.ofMinutes(4));
89+
RoutingFunction routingFunction = (from, to, linkingContext) -> mockPath;
90+
91+
var result = findOptimalInsertion(
92+
trip,
93+
getCoordinateBetween(OSLO_SOUTH, OSLO_CENTER),
94+
getCoordinateBetween(OSLO_NORTHEAST, OSLO_NORTH),
95+
routingFunction
96+
);
97+
98+
assertNull(result);
99+
}
100+
73101
@Test
74102
void findOptimalInsertion_noValidPositions_returnsNull() {
75103
var trip = createSimpleTrip(OSLO_CENTER, OSLO_NORTH);
@@ -103,20 +131,20 @@ void findOptimalInsertion_routingFails_skipsPosition() {
103131
var stop1 = createStopAt(0, OSLO_EAST);
104132
var trip = createTripWithStops(OSLO_CENTER, List.of(stop1), OSLO_NORTH);
105133

106-
var mockPath = createGraphPath(Duration.ofMinutes(5));
134+
var mockPath = createGraphPath(Duration.ofMinutes(3));
107135

108136
// Routing sequence:
109137
// 1. Baseline calculation (2 segments: OSLO_CENTER → OSLO_EAST → OSLO_NORTH) = mockPath x2
110138
// 2. First insertion attempt fails (null for first segment)
111139
// 3. Second insertion attempt succeeds (mockPath for all segments)
112140
@SuppressWarnings("ConstantConditions")
113141
RoutingFunction routingFunction = (from, to, linkingContext) -> {
114-
if(
142+
if (
115143
new WgsCoordinate(from.lat, from.lng).equals(OSLO_CENTER) &&
116144
new WgsCoordinate(to.lat, to.lng).equals(OSLO_MIDPOINT_NORTH)
117145
) {
118146
return null;
119-
}else{
147+
} else {
120148
return mockPath;
121149
}
122150
};
@@ -181,16 +209,24 @@ void findOptimalInsertion_baselineDurationCalculationFails_returnsNull() {
181209
void findOptimalInsertion_selectsMinimumAdditionalDuration() {
182210
var trip = createTripWithDeviationBudget(Duration.ofMinutes(20), OSLO_CENTER, OSLO_NORTH);
183211

184-
final Map<Pair<WgsCoordinate>, GraphPath<State, Edge, Vertex>> pathsMap = new HashMap<>(Map.of(
185-
new Pair<>(OSLO_CENTER, OSLO_NORTH), createGraphPath(Duration.ofMinutes(10)),
186-
new Pair<>(OSLO_CENTER, OSLO_EAST), createGraphPath(Duration.ofMinutes(4)),
187-
new Pair<>(OSLO_EAST, OSLO_WEST), createGraphPath(Duration.ofMinutes(5)),
188-
new Pair<>(OSLO_WEST, OSLO_NORTH), createGraphPath(Duration.ofMinutes(6))
189-
));
212+
final Map<Pair<WgsCoordinate>, GraphPath<State, Edge, Vertex>> pathsMap = new HashMap<>(
213+
Map.of(
214+
new Pair<>(OSLO_CENTER, OSLO_NORTH),
215+
createGraphPath(Duration.ofMinutes(10)),
216+
new Pair<>(OSLO_CENTER, OSLO_EAST),
217+
createGraphPath(Duration.ofMinutes(4)),
218+
new Pair<>(OSLO_EAST, OSLO_WEST),
219+
createGraphPath(Duration.ofMinutes(5)),
220+
new Pair<>(OSLO_WEST, OSLO_NORTH),
221+
createGraphPath(Duration.ofMinutes(6))
222+
)
223+
);
190224

191225
@SuppressWarnings("ConstantConditions")
192226
RoutingFunction routingFunction = (from, to, linkingContext) ->
193-
pathsMap.get(new Pair<>(new WgsCoordinate(from.lat, from.lng), new WgsCoordinate(to.lat, to.lng)));
227+
pathsMap.get(
228+
new Pair<>(new WgsCoordinate(from.lat, from.lng), new WgsCoordinate(to.lat, to.lng))
229+
);
194230

195231
var result = findOptimalInsertion(trip, OSLO_EAST, OSLO_WEST, routingFunction);
196232

@@ -241,18 +277,26 @@ void findOptimalInsertion_insertBetweenTwoPoints_routesAllSegments() {
241277
// MIDPOINT_NORTH → NORTH
242278
var segmentDB = createGraphPath(Duration.ofMinutes(4));
243279

244-
final Map<Pair<WgsCoordinate>, GraphPath<State, Edge, Vertex>> pathsMap = new HashMap<>(Map.of(
245-
new Pair<>(OSLO_CENTER, OSLO_NORTH), baselinePath,
246-
new Pair<>(OSLO_CENTER, OSLO_EAST), segmentAC,
247-
new Pair<>(OSLO_EAST, OSLO_MIDPOINT_NORTH), segmentCD,
248-
new Pair<>(OSLO_MIDPOINT_NORTH, OSLO_NORTH), segmentDB
249-
));
280+
final Map<Pair<WgsCoordinate>, GraphPath<State, Edge, Vertex>> pathsMap = new HashMap<>(
281+
Map.of(
282+
new Pair<>(OSLO_CENTER, OSLO_NORTH),
283+
baselinePath,
284+
new Pair<>(OSLO_CENTER, OSLO_EAST),
285+
segmentAC,
286+
new Pair<>(OSLO_EAST, OSLO_MIDPOINT_NORTH),
287+
segmentCD,
288+
new Pair<>(OSLO_MIDPOINT_NORTH, OSLO_NORTH),
289+
segmentDB
290+
)
291+
);
250292

251293
final int[] callCount = { 0 };
252294
@SuppressWarnings("ConstantConditions")
253295
RoutingFunction routingFunction = (from, to, linkingContext) -> {
254296
callCount[0]++;
255-
return pathsMap.get(new Pair<>(new WgsCoordinate(from.lat, from.lng), new WgsCoordinate(to.lat, to.lng)));
297+
return pathsMap.get(
298+
new Pair<>(new WgsCoordinate(from.lat, from.lng), new WgsCoordinate(to.lat, to.lng))
299+
);
256300
};
257301

258302
// Passenger pickup at OSLO_EAST, dropoff at OSLO_MIDPOINT_NORTH
@@ -268,8 +312,9 @@ void findOptimalInsertion_insertBetweenTwoPoints_routesAllSegments() {
268312
// (typically 1-2 seconds per edge due to millisecond rounding in StreetEdge.doTraverse())
269313
// The baseline should be approximately 10 minutes (within 10 seconds tolerance)
270314
assertTrue(
271-
Math.abs(result.baselineDuration().toSeconds() - 600) < 10,
272-
"Baseline should be approximately 10 min (within 10s), got " + result.baselineDuration()
315+
Math.abs(result.durationBetweenOriginAndDestination().toSeconds() - 600) < 10,
316+
"Baseline should be approximately 10 min (within 10s), got " +
317+
result.durationBetweenOriginAndDestination()
273318
);
274319

275320
// CRITICAL: Total duration should be sum of NEW segments, NOT baseline duration
@@ -302,7 +347,7 @@ void findOptimalInsertion_insertAtEnd_reusesMostSegments() {
302347
var trip = createTripWithStops(OSLO_CENTER, List.of(stop1), OSLO_NORTH);
303348

304349
// Baseline has 2 segments: CENTER→EAST, EAST→NORTH
305-
var mockPath = createGraphPath(Duration.ofMinutes(5));
350+
var mockPath = createGraphPath(Duration.ofMinutes(3));
306351

307352
final int[] callCount = { 0 };
308353
RoutingFunction routingFunction = (from, to, linkingContext) -> {
@@ -315,8 +360,12 @@ void findOptimalInsertion_insertAtEnd_reusesMostSegments() {
315360

316361
assertNotNull(result, "Should find valid insertion");
317362

318-
// Baseline should be calculated correctly
319-
assertEquals(Duration.ofMinutes(10), result.baselineDuration());
363+
// Duration between start and stop should be calculated correctly
364+
assertTrue(
365+
Duration.ofMinutes(3).minus(result.durationBetweenOriginAndDestination()).toSeconds() < 10,
366+
"Baseline should be approximately 3 min (within 10s), got " +
367+
result.durationBetweenOriginAndDestination()
368+
);
320369

321370
// The modified route should have more segments than baseline
322371
assertTrue(
@@ -342,7 +391,7 @@ void findOptimalInsertion_pickupAtExistingPoint_handlesCorrectly() {
342391
var stop1 = createStopAt(0, OSLO_EAST);
343392
var trip = createTripWithStops(OSLO_CENTER, List.of(stop1), OSLO_NORTHEAST);
344393

345-
var mockPath = createGraphPath(Duration.ofMinutes(5));
394+
var mockPath = createGraphPath(Duration.ofMinutes(3));
346395

347396
final int[] callCount = { 0 };
348397
RoutingFunction routingFunction = (from, to, linkingContext) -> {
@@ -388,6 +437,6 @@ void findOptimalInsertion_singleSegmentTrip_routesAllNewSegments() {
388437

389438
// Total duration should be positive
390439
assertTrue(result.totalDuration().compareTo(Duration.ZERO) > 0);
391-
assertTrue(result.baselineDuration().compareTo(Duration.ZERO) > 0);
440+
assertTrue(result.durationBetweenOriginAndDestination().compareTo(Duration.ZERO) > 0);
392441
}
393442
}

0 commit comments

Comments
 (0)