Skip to content

Commit 283eed8

Browse files
Merge pull request opentripplanner#7480 from opentripplanner/flex-graph-path-perf
Optimize computation of flex paths
2 parents 3cfea82 + ad9b93b commit 283eed8

11 files changed

Lines changed: 209 additions & 20 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.opentripplanner.ext.flex.flexpathcalculator;
2+
3+
import static com.google.common.truth.Truth.assertThat;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.opentripplanner.street.search.state.TestStateBuilder.ofDriving;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
class StateToFlexPathMapperTest {
10+
11+
private static final int EPSILON = 100;
12+
13+
@Test
14+
void departAt() {
15+
var state = ofDriving().streetEdge().streetEdge().streetEdge().build();
16+
var flexPath = StateToFlexPathMapper.map(state);
17+
assertThat(flexPath.distanceMeters).isWithin(EPSILON).of(471509);
18+
assertEquals(42100, flexPath.durationSeconds);
19+
assertEquals("LINESTRING (1 1, 2 2, 3 3, 4 4)", flexPath.getGeometry().toString());
20+
}
21+
22+
@Test
23+
void arriveBy() {
24+
var state = ofDriving().streetEdge().streetEdge().streetEdge().build().reverse();
25+
var flexPath = StateToFlexPathMapper.map(state);
26+
assertThat(flexPath.distanceMeters).isWithin(EPSILON).of(471509);
27+
assertEquals(42100, flexPath.durationSeconds);
28+
assertEquals("LINESTRING (1 1, 2 2, 3 3, 4 4)", flexPath.getGeometry().toString());
29+
}
30+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.opentripplanner.ext.flex.flexpathcalculator;
2+
3+
import com.google.common.collect.Iterables;
4+
import java.util.function.Supplier;
5+
import org.locationtech.jts.geom.LineString;
6+
import org.opentripplanner.street.geometry.GeometryUtils;
7+
import org.opentripplanner.street.model.edge.Edge;
8+
import org.opentripplanner.street.search.state.State;
9+
10+
/**
11+
* Extracts the geometry and distance from a {@link State} chain produced by
12+
* an A* street search. The state chain is a linked list from the final state back to the origin via
13+
* {@link State#getBackState()}/{@link State#getBackEdge()}.
14+
* <p>
15+
* This utility encapsulates the direction-dependent ordering: for depart-after searches the chain
16+
* yields a geometry in reverse chronological order (newest first), while for arriveBy searches the chain
17+
* already yields edges in chronological order.
18+
* Implementation note: an earlier design relied on {@link org.opentripplanner.astar.model.GraphPath}
19+
* to extract the list of edges in chronological order / reverse chronological order.
20+
* The current implementation is optimized for reducing memory allocation and CPU usage.
21+
*/
22+
class StateToFlexPathMapper {
23+
24+
/**
25+
* Walk the state chain and collect edges in chronological order (origin → destination), summing
26+
* up the distance along the way.
27+
*/
28+
static FlexPath map(State state) {
29+
double distance_m = 0.0;
30+
31+
// TODO: compute this during traversal of the edges in a follow up PR
32+
for (var backEdge : state.listBackEdges()) {
33+
distance_m += backEdge.getDistanceMeters();
34+
}
35+
36+
// computing the linestring from the graph path is a surprisingly expensive operation
37+
// so we delay it until it's actually needed. since most flex paths are never shown to the user
38+
// this improves performance quite a bit.
39+
40+
Supplier<LineString> geometrySupplier = () -> {
41+
if (state.getRequest().arriveBy()) {
42+
var geometries = Iterables.transform(state.listBackEdges(), Edge::getGeometry);
43+
return GeometryUtils.concatenateLineStrings(geometries);
44+
} else {
45+
var geometries = Iterables.transform(state.listBackEdges(), StateToFlexPathMapper::reverse);
46+
return GeometryUtils.concatenateLineStrings(geometries).reverse();
47+
}
48+
};
49+
50+
return new FlexPath((int) distance_m, (int) state.getElapsedTimeSeconds(), geometrySupplier);
51+
}
52+
53+
private static LineString reverse(Edge e) {
54+
var geom = e.getGeometry();
55+
if (geom == null) {
56+
return null;
57+
} else {
58+
return geom.reverse();
59+
}
60+
}
61+
}

application/src/ext/java/org/opentripplanner/ext/flex/flexpathcalculator/StreetFlexPathCalculator.java

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
import java.time.Duration;
44
import java.util.HashMap;
55
import java.util.Map;
6-
import org.opentripplanner.astar.model.GraphPath;
76
import org.opentripplanner.astar.model.ShortestPathTree;
87
import org.opentripplanner.astar.strategy.DurationSkipEdgeStrategy;
98
import org.opentripplanner.framework.application.OTPRequestTimeoutException;
10-
import org.opentripplanner.street.geometry.GeometryUtils;
119
import org.opentripplanner.street.model.StreetMode;
1210
import org.opentripplanner.street.model.edge.Edge;
1311
import org.opentripplanner.street.model.vertex.Vertex;
@@ -61,20 +59,12 @@ public FlexPath calculateFlexPath(
6159
cache.put(originVertex, shortestPathTree);
6260
}
6361

64-
GraphPath<State, Edge, Vertex> path = shortestPathTree.getPath(destinationVertex);
65-
if (path == null) {
62+
var state = shortestPathTree.getState(destinationVertex);
63+
if (state == null) {
6664
return null;
6765
}
6866

69-
int distance = (int) path.edges.stream().mapToDouble(Edge::getDistanceMeters).sum();
70-
int duration = path.getDuration();
71-
72-
// computing the linestring from the graph path is a surprisingly expensive operation
73-
// so we delay it until it's actually needed. since most flex paths are never shown to the user
74-
// this improves performance quite a bit.
75-
return new FlexPath(distance, duration, () ->
76-
GeometryUtils.concatenateLineStrings(path.edges, Edge::getGeometry)
77-
);
67+
return StateToFlexPathMapper.map(state);
7868
}
7969

8070
private ShortestPathTree<State, Edge, Vertex> routeToMany(Vertex vertex) {

application/src/test-fixtures/java/org/opentripplanner/street/search/state/TestStateBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public static TestStateBuilder ofWalking() {
7373
}
7474

7575
/**
76-
* Create an initial state that start in a car.
76+
* Create an initial state that starts in a car.
7777
*/
7878
public static TestStateBuilder ofDriving() {
7979
return new TestStateBuilder(StreetMode.CAR);

astar/src/main/java/org/opentripplanner/astar/model/GraphPath.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
/**
99
* A shortest path on the graph.
10+
* <p>
11+
* WARNING: This class is often a hotspot as it eagerly traverses the state chain. Avoid
12+
* as much as possible.
1013
*/
1114
public class GraphPath<
1215
State extends AStarState<State, Edge, Vertex>,

astar/src/main/java/org/opentripplanner/astar/model/ShortestPathTree.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.Iterator;
77
import java.util.LinkedList;
88
import java.util.List;
9+
import javax.annotation.Nullable;
910
import org.opentripplanner.astar.spi.AStarEdge;
1011
import org.opentripplanner.astar.spi.AStarState;
1112
import org.opentripplanner.astar.spi.AStarVertex;
@@ -141,6 +142,7 @@ public boolean add(State newState) {
141142
* @return a 'best' state at that vertex
142143
*/
143144
@SuppressWarnings("unchecked")
145+
@Nullable
144146
public State getState(Vertex dest) {
145147
Object existing = stateSets.get(dest);
146148
if (existing == null) {

street/src/main/java/org/opentripplanner/street/geometry/GeometryUtils.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.opentripplanner.street.geometry;
22

3+
import com.google.common.collect.Iterables;
34
import java.util.ArrayList;
45
import java.util.Arrays;
56
import java.util.Collection;
@@ -8,6 +9,7 @@
89
import java.util.function.Function;
910
import java.util.function.Predicate;
1011
import java.util.stream.Stream;
12+
import java.util.stream.StreamSupport;
1113
import org.geojson.GeoJsonObject;
1214
import org.geojson.LngLatAlt;
1315
import org.locationtech.jts.algorithm.ConvexHull;
@@ -70,19 +72,29 @@ public static LineString makeLineString(WgsCoordinate... coordinates) {
7072
return makeLineString(Arrays.stream(coordinates).map(WgsCoordinate::asJtsCoordinate).toList());
7173
}
7274

75+
/// Convert an iterable of T by applying a mapping function to each element and concatenate the
76+
/// resulting LineStrings.
77+
///
78+
/// For the best performance and lowest number of allocations pass in an [Iterable] rather
79+
/// than a materialized [Collection].
7380
public static <T> LineString concatenateLineStrings(
74-
List<T> inputObjects,
81+
Iterable<T> inputObjects,
7582
Function<T, LineString> mapper
7683
) {
77-
return concatenateLineStrings(inputObjects.stream().map(mapper).toList());
84+
return concatenateLineStrings(Iterables.transform(inputObjects, mapper::apply));
7885
}
7986

80-
public static LineString concatenateLineStrings(List<LineString> lineStrings) {
87+
/// Convert an [Iterable] of [LineString] by applying a mapping function to each element and
88+
/// concatenate the resulting LineStrings.
89+
///
90+
/// For the best performance and lowest number of allocations pass in an [Iterable] rather
91+
/// than a materialized [Collection].
92+
public static LineString concatenateLineStrings(Iterable<LineString> lineStrings) {
8193
GeometryFactory factory = getGeometryFactory();
8294
Predicate<Coordinate[]> nonZeroLength = coordinates -> coordinates.length != 0;
95+
8396
return factory.createLineString(
84-
lineStrings
85-
.stream()
97+
StreamSupport.stream(lineStrings.spliterator(), false)
8698
.filter(Objects::nonNull)
8799
.map(LineString::getCoordinates)
88100
.filter(nonZeroLength)

street/src/main/java/org/opentripplanner/street/search/request/StreetSearchRequestBuilder.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,5 +163,4 @@ Instant startTimeOrNow() {
163163
public StreetSearchRequest build() {
164164
return new StreetSearchRequest(this);
165165
}
166-
167166
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.opentripplanner.street.search.state;
2+
3+
import java.util.Iterator;
4+
import java.util.NoSuchElementException;
5+
import org.opentripplanner.street.model.edge.Edge;
6+
7+
/**
8+
* Iterator for traversing back edges in a state chain without any extra allocations.
9+
*/
10+
class BackEdgeIterator implements Iterator<Edge> {
11+
12+
private State current;
13+
14+
public BackEdgeIterator(State state) {
15+
current = state;
16+
}
17+
18+
@Override
19+
public boolean hasNext() {
20+
return current != null && current.getBackState() != null;
21+
}
22+
23+
@Override
24+
public Edge next() {
25+
if(!hasNext()){
26+
throw new NoSuchElementException("No more back edges available");
27+
}
28+
var ret = current;
29+
current = current.getBackState();
30+
return ret.getBackEdge();
31+
}
32+
}

street/src/main/java/org/opentripplanner/street/search/state/State.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,13 @@ protected State clone() {
490490
return ret;
491491
}
492492

493+
/**
494+
* Returns an efficient iterable that allows traversing the edge chain backwards.
495+
*/
496+
public Iterable<Edge> listBackEdges() {
497+
return () -> new BackEdgeIterator(this);
498+
}
499+
493500
public String toString() {
494501
return ToStringBuilder.of(State.class)
495502
.addDateTime("time", getTime())

0 commit comments

Comments
 (0)