Skip to content

Commit e066199

Browse files
committed
perf(street): reuse the prepared area index for the point-in-area test
The vertex-in-area check in snapAndLink used a raw Polygon.contains(point), which rebuilds a sweep-line index over the polygon boundary on every call, while addAreaVertex separately built a PreparedGeometry for the visibility fan-out. Build one PreparedAreaGroup per area and reuse it for the point test, the forced split-point edge and the fan-out, so the index is built once. Add PreparedAreaGroup.containsPoint and pass the prepared group into addAreaVertex instead of rebuilding it there. Behavior is unchanged (PreparedGeometry.contains matches Geometry.contains for points and segments).
1 parent 9516d05 commit e066199

3 files changed

Lines changed: 40 additions & 8 deletions

File tree

street/src/main/java/org/opentripplanner/street/linking/PreparedAreaGroup.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,24 @@ AreaGroup areaGroup() {
5454
* indexed {@link #areasCrossedBy} crossing test on this same group.
5555
*/
5656
boolean containsSegment(Coordinate from, Coordinate to) {
57+
return preparedGroup().contains(createShrunkLine(from, to));
58+
}
59+
60+
/**
61+
* Whether the area group polygon contains the given point. Reuses the same lazily built prepared
62+
* index as {@link #containsSegment}, so the point-in-area test and the per-candidate segment tests
63+
* share a single index build instead of each rebuilding a sweep-line index over the polygon
64+
* boundary.
65+
*/
66+
boolean containsPoint(Coordinate point) {
67+
return preparedGroup().contains(GEOMETRY_FACTORY.createPoint(point));
68+
}
69+
70+
private PreparedGeometry preparedGroup() {
5771
if (preparedGroup == null) {
5872
preparedGroup = PreparedGeometryFactory.prepare(areaGroup.getGeometry());
5973
}
60-
return preparedGroup.contains(createShrunkLine(from, to));
74+
return preparedGroup;
6175
}
6276

6377
/**

street/src/main/java/org/opentripplanner/street/linking/VertexLinker.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -442,10 +442,13 @@ private StreetVertex snapAndLink(
442442
edge instanceof AreaEdge aEdge
443443
) {
444444
AreaGroup ag = aEdge.getArea();
445+
// Prepared once and reused for the point-in-area test, the forced split-point edge and the
446+
// visibility fan-out below, so the area's spatial index is built a single time.
447+
var area = new PreparedAreaGroup(ag);
445448
// is area already linked ?
446449
start = linkedAreas.get(ag);
447450
if (start == null) {
448-
if (ag.getGeometry().contains(GEOMETRY_FACTORY.createPoint(vertex.getCoordinate()))) {
451+
if (area.containsPoint(vertex.getCoordinate())) {
449452
// vertex is inside an area
450453
if (distSquared(vertex, split) <= DUPLICATE_NODE_EPSILON_DEGREES_SQUARED) {
451454
// vertex is so close to the edge that we can use the split point directly
@@ -464,14 +467,14 @@ private StreetVertex snapAndLink(
464467
// vertex is inside the area. try connecting the vertex to the edge's split point, because
465468
// connections to visibility vertices may fail or do not always provide an optimal route
466469
// note that by definition, connection to closest edge cannot be blocked and edge can be forced
467-
addVisibilityEdges(start, split, new PreparedAreaGroup(ag), scope, tempEdges, true);
470+
addVisibilityEdges(start, split, area, scope, tempEdges, true);
468471
} else {
469472
// vertex is outside an area. Use split point for area connections
470473
start = split;
471474
}
472475
// connect start point to area visibility points to achieve optimal paths
473476
if (!ag.visibilityVertices().contains(start)) {
474-
addAreaVertex(start, ag, scope, tempEdges, false);
477+
addAreaVertex(start, area, scope, tempEdges, false);
475478
}
476479
} else {
477480
start = split;
@@ -652,7 +655,7 @@ public boolean equals(Object o) {
652655
* Link a new vertex permanently with area geometry
653656
*/
654657
public boolean addPermanentAreaVertex(IntersectionVertex newVertex, AreaGroup areaGroup) {
655-
return addAreaVertex(newVertex, areaGroup, Scope.PERMANENT, null, true);
658+
return addAreaVertex(newVertex, new PreparedAreaGroup(areaGroup), Scope.PERMANENT, null, true);
656659
}
657660

658661
/**
@@ -672,14 +675,13 @@ private double distSquared(Vertex a, Vertex b) {
672675
*/
673676
private boolean addAreaVertex(
674677
IntersectionVertex newVertex,
675-
AreaGroup areaGroup,
678+
PreparedAreaGroup area,
676679
Scope scope,
677680
DisposableEdgeCollection tempEdges,
678681
boolean force
679682
) {
683+
AreaGroup areaGroup = area.areaGroup();
680684
Geometry polygon = areaGroup.getGeometry();
681-
// Reused across all candidate contains() checks below so the spatial index is built only once.
682-
var area = new PreparedAreaGroup(areaGroup);
683685

684686
int added = 0;
685687

street/src/test/java/org/opentripplanner/street/linking/PreparedAreaGroupTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,22 @@ void doesNotContainExteriorSegment() {
5050
assertFalse(area.containsSegment(new Coordinate(11, 11), new Coordinate(12, 12)));
5151
}
5252

53+
@Test
54+
void containsInteriorPoint() {
55+
assertTrue(area.containsPoint(new Coordinate(1, 1)));
56+
}
57+
58+
@Test
59+
void doesNotContainPointInHole() {
60+
// The 2x2 hole is centred at x,y in (4,6); a point in its middle is not inside the area.
61+
assertFalse(area.containsPoint(new Coordinate(5, 5)));
62+
}
63+
64+
@Test
65+
void doesNotContainExteriorPoint() {
66+
assertFalse(area.containsPoint(new Coordinate(11, 11)));
67+
}
68+
5369
@Test
5470
void exposesWrappedAreaGroup() {
5571
var ag = new AreaGroup(SQUARE_WITH_HOLE);

0 commit comments

Comments
 (0)