Skip to content

Commit bdb8a09

Browse files
committed
fix: support multiple networks on BusinessAreaBorder vertices
BusinessAreaBorder previously held a single network string, so when multiple operators' business areas shared boundary edges, the last one to be applied would overwrite earlier ones. This caused riders on overwritten networks to pass through the border unchecked. BusinessAreaBorder now holds a Set<String> of networks. Vertex methods are additive (addBusinessAreaBorderNetwork) and removal is per-network (removeBusinessAreaBorderNetwork), so updaters only remove their own network without affecting others. Also fixes the graph builder to group business areas by network before computing the union polygon
1 parent 0219b66 commit bdb8a09

8 files changed

Lines changed: 76 additions & 51 deletions

File tree

application/src/main/java/org/opentripplanner/updater/vehicle_rental/VehicleRentalUpdater.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import org.opentripplanner.service.vehiclerental.VehicleRentalRepository;
1414
import org.opentripplanner.service.vehiclerental.model.GeofencingZone;
1515
import org.opentripplanner.service.vehiclerental.model.VehicleRentalPlace;
16-
import org.opentripplanner.service.vehiclerental.street.BusinessAreaBorder;
1716
import org.opentripplanner.service.vehiclerental.street.GeofencingBoundaryExtension;
1817
import org.opentripplanner.service.vehiclerental.street.GeofencingZoneApplier;
1918
import org.opentripplanner.service.vehiclerental.street.GeofencingZoneIndex;
@@ -54,7 +53,7 @@ public class VehicleRentalUpdater extends PollingGraphUpdater {
5453
private final String nameForLogging;
5554
private final boolean applyBusinessAreas;
5655

57-
private Map<StreetEdge, BusinessAreaBorder> latestBusinessAreaEdges = Map.of();
56+
private Map<StreetEdge, String> latestBusinessAreaEdges = Map.of();
5857
private Map<StreetEdge, GeofencingBoundaryExtension> latestBoundaryEdges = Map.of();
5958
private GeofencingZoneIndex latestZoneIndex;
6059
private Set<GeofencingZone> latestAppliedGeofencingZones = Set.of();
@@ -217,7 +216,7 @@ public void run(RealTimeUpdateContext context) {
217216
LOG.info("Computing geofencing zones for {}", nameForLogging);
218217
var start = System.currentTimeMillis();
219218

220-
latestBusinessAreaEdges.forEach((edge, border) -> edge.removeBusinessAreaBorder());
219+
latestBusinessAreaEdges.forEach(StreetEdge::removeBusinessAreaBorderNetwork);
221220
latestBoundaryEdges.forEach(StreetEdge::removeGeofencingBoundary);
222221

223222
var graph = context.graph();
Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package org.opentripplanner.service.vehiclerental.street;
22

3+
import java.util.HashSet;
4+
import java.util.Set;
35
import org.opentripplanner.street.search.state.State;
46

57
/**
6-
* Marks a vertex as being on the border of a rental network's business area.
7-
* Traversal is banned for vehicles of the matching network — they cannot leave
8-
* the business area. Enforced via {@code Vertex.rentalTraversalBanned(State)}.
8+
* Marks a vertex as being on the border of one or more rental networks' business areas.
9+
* Traversal is banned for vehicles of any matching network — they cannot leave
10+
* their business area. Enforced via {@code Vertex.rentalTraversalBanned(State)}.
911
*
1012
* @deprecated Business areas are an OTP-specific concept not defined in the GBFS spec.
1113
* They are inferred from GBFS zones with no restrictions, but this inference is
@@ -16,10 +18,22 @@
1618
@Deprecated
1719
public final class BusinessAreaBorder {
1820

19-
private final String network;
21+
private final Set<String> networks;
2022

21-
public BusinessAreaBorder(String network) {
22-
this.network = network;
23+
public BusinessAreaBorder() {
24+
this.networks = new HashSet<>();
25+
}
26+
27+
public void addNetwork(String network) {
28+
networks.add(network);
29+
}
30+
31+
public void removeNetwork(String network) {
32+
networks.remove(network);
33+
}
34+
35+
public boolean isEmpty() {
36+
return networks.isEmpty();
2337
}
2438

2539
public boolean traversalBanned(State state) {
@@ -31,10 +45,10 @@ public boolean traversalBanned(State state) {
3145
if (state.getVehicleRentalNetwork() == null) {
3246
return false;
3347
}
34-
return network.equals(state.getVehicleRentalNetwork());
48+
return networks.contains(state.getVehicleRentalNetwork());
3549
}
3650

37-
public String network() {
38-
return network;
51+
public Set<String> networks() {
52+
return Set.copyOf(networks);
3953
}
4054
}

street/src/main/java/org/opentripplanner/service/vehiclerental/street/GeofencingZoneApplier.java

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.Map;
77
import java.util.Set;
88
import java.util.function.Function;
9+
import java.util.stream.Collectors;
910
import org.locationtech.jts.geom.Envelope;
1011
import org.locationtech.jts.geom.Geometry;
1112
import org.locationtech.jts.geom.LineString;
@@ -54,21 +55,22 @@ public GeofencingZoneApplierResult applyGeofencingZones(
5455
.filter(z -> z.geometry() != null)
5556
.toList();
5657

57-
var businessAreaEdges = new HashMap<StreetEdge, BusinessAreaBorder>();
58+
var businessAreaEdges = new HashMap<StreetEdge, String>();
5859

5960
// Boundary marking: apply GeofencingBoundaryExtension to boundary-crossing edges
6061
var boundaryEdges = addBoundaryExtensions(zonesWithGeometry);
6162

6263
// Business area borders (deprecated — not a GBFS concept)
6364
if (applyBusinessAreas) {
64-
var generalBusinessAreas = geofencingZones
65+
var businessAreasByNetwork = geofencingZones
6566
.stream()
6667
.filter(GeofencingZone::isBusinessArea)
67-
.toList();
68+
.collect(Collectors.groupingBy(z -> z.id().getFeedId()));
6869

69-
if (!generalBusinessAreas.isEmpty()) {
70-
var network = generalBusinessAreas.get(0).id().getFeedId();
71-
var polygons = generalBusinessAreas
70+
for (var entry : businessAreasByNetwork.entrySet()) {
71+
var network = entry.getKey();
72+
var polygons = entry
73+
.getValue()
7274
.stream()
7375
.map(GeofencingZone::geometry)
7476
.toArray(Geometry[]::new);
@@ -77,9 +79,7 @@ public GeofencingZoneApplierResult applyGeofencingZones(
7779
.createGeometryCollection(polygons)
7880
.union();
7981

80-
businessAreaEdges.putAll(
81-
applyBusinessAreaBorder(unionOfBusinessAreas, new BusinessAreaBorder(network))
82-
);
82+
businessAreaEdges.putAll(applyBusinessAreaBorder(unionOfBusinessAreas, network));
8383
}
8484
}
8585

@@ -161,11 +161,8 @@ private Map<StreetEdge, GeofencingBoundaryExtension> addBoundaryExtensions(
161161
* Apply a business area border extension to edges that cross the boundary of the polygon.
162162
* Uses vertex containment to detect boundary crossings without decompressing edge geometry.
163163
*/
164-
private Map<StreetEdge, BusinessAreaBorder> applyBusinessAreaBorder(
165-
Geometry polygon,
166-
BusinessAreaBorder ext
167-
) {
168-
var edgesUpdated = new HashMap<StreetEdge, BusinessAreaBorder>();
164+
private Map<StreetEdge, String> applyBusinessAreaBorder(Geometry polygon, String network) {
165+
var edgesUpdated = new HashMap<StreetEdge, String>();
169166
Set<Edge> candidates = Set.copyOf(findEdgesForEnvelope.apply(polygon.getEnvelopeInternal()));
170167
var preparedPolygon = PreparedGeometryFactory.prepare(polygon);
171168
var polygonBBox = polygon.getEnvelopeInternal();
@@ -186,8 +183,8 @@ private Map<StreetEdge, BusinessAreaBorder> applyBusinessAreaBorder(
186183
boolean toInZone = toMayBeInZone && preparedPolygon.covers(gf.createPoint(toCoord));
187184

188185
if (fromInZone != toInZone) {
189-
streetEdge.setBusinessAreaBorder(ext);
190-
edgesUpdated.put(streetEdge, ext);
186+
streetEdge.addBusinessAreaBorderNetwork(network);
187+
edgesUpdated.put(streetEdge, network);
191188
}
192189
}
193190
}

street/src/main/java/org/opentripplanner/service/vehiclerental/street/GeofencingZoneApplierResult.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
/**
77
* Result of applying geofencing zones to the street graph.
88
*
9-
* @param businessAreaEdges edges with BusinessAreaBorder, tracked for cleanup on subsequent runs
9+
* @param businessAreaEdges edges with BusinessAreaBorder (edge → network), tracked for cleanup
1010
* @param boundaryEdges edges with boundary-crossing extensions, tracked for cleanup
1111
* @param zoneIndex spatial index of all zones for containment queries
1212
*/
1313
public record GeofencingZoneApplierResult(
14-
Map<StreetEdge, BusinessAreaBorder> businessAreaEdges,
14+
Map<StreetEdge, String> businessAreaEdges,
1515
Map<StreetEdge, GeofencingBoundaryExtension> boundaryEdges,
1616
GeofencingZoneIndex zoneIndex
1717
) {}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -557,8 +557,10 @@ private SplitterVertex createSplitVertex(
557557
v.copyRentalRestrictionsFrom(originalEdge.getFromVertex());
558558
// Also copy from tov if it has different restrictions
559559
var toVertex = originalEdge.getToVertex();
560-
if (toVertex.getBusinessAreaBorder() != null && v.getBusinessAreaBorder() == null) {
561-
v.setBusinessAreaBorder(toVertex.getBusinessAreaBorder());
560+
if (toVertex.getBusinessAreaBorder() != null) {
561+
for (var network : toVertex.getBusinessAreaBorder().networks()) {
562+
v.addBusinessAreaBorderNetwork(network);
563+
}
562564
}
563565
for (var boundary : toVertex.getGeofencingBoundaries()) {
564566
if (!v.getGeofencingBoundaries().contains(boundary)) {

street/src/main/java/org/opentripplanner/street/model/edge/StreetEdge.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
1313
import org.opentripplanner.core.model.i18n.I18NString;
1414
import org.opentripplanner.service.vehiclerental.model.RentalVehicleType.PropulsionType;
15-
import org.opentripplanner.service.vehiclerental.street.BusinessAreaBorder;
1615
import org.opentripplanner.service.vehiclerental.street.GeofencingBoundaryExtension;
1716
import org.opentripplanner.street.geometry.CompactLineStringUtils;
1817
import org.opentripplanner.street.geometry.DirectionUtils;
@@ -535,9 +534,9 @@ public void removeGeofencingBoundary(GeofencingBoundaryExtension ext) {
535534
tov.removeGeofencingBoundary(ext);
536535
}
537536

538-
public void removeBusinessAreaBorder() {
539-
fromv.removeBusinessAreaBorder();
540-
tov.removeBusinessAreaBorder();
537+
public void removeBusinessAreaBorderNetwork(String network) {
538+
fromv.removeBusinessAreaBorderNetwork(network);
539+
tov.removeBusinessAreaBorderNetwork(network);
541540
}
542541

543542
@Override
@@ -660,8 +659,8 @@ public void addGeofencingBoundary(GeofencingBoundaryExtension ext) {
660659
fromv.addGeofencingBoundary(ext);
661660
}
662661

663-
public void setBusinessAreaBorder(BusinessAreaBorder border) {
664-
fromv.setBusinessAreaBorder(border);
662+
public void addBusinessAreaBorderNetwork(String network) {
663+
fromv.addBusinessAreaBorderNetwork(network);
665664
}
666665

667666
/**

street/src/main/java/org/opentripplanner/street/model/vertex/Vertex.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,20 @@ private boolean hasGeofencingBoundaryMatching(State currentState, boolean traver
313313
return false;
314314
}
315315

316-
public void setBusinessAreaBorder(BusinessAreaBorder border) {
317-
this.businessAreaBorder = border;
316+
public void addBusinessAreaBorderNetwork(String network) {
317+
if (businessAreaBorder == null) {
318+
businessAreaBorder = new BusinessAreaBorder();
319+
}
320+
businessAreaBorder.addNetwork(network);
321+
}
322+
323+
public void removeBusinessAreaBorderNetwork(String network) {
324+
if (businessAreaBorder != null) {
325+
businessAreaBorder.removeNetwork(network);
326+
if (businessAreaBorder.isEmpty()) {
327+
businessAreaBorder = null;
328+
}
329+
}
318330
}
319331

320332
@javax.annotation.Nullable
@@ -341,15 +353,19 @@ public void removeGeofencingBoundary(GeofencingBoundaryExtension ext) {
341353
geofencingBoundaries = List.copyOf(newList);
342354
}
343355

344-
public void removeBusinessAreaBorder() {
356+
public void removeAllBusinessAreaBorders() {
345357
this.businessAreaBorder = null;
346358
}
347359

348360
/**
349361
* Copy all rental restriction data from another vertex to this one.
350362
*/
351363
public void copyRentalRestrictionsFrom(Vertex other) {
352-
this.businessAreaBorder = other.businessAreaBorder;
364+
if (other.businessAreaBorder != null) {
365+
for (var network : other.businessAreaBorder.networks()) {
366+
addBusinessAreaBorderNetwork(network);
367+
}
368+
}
353369
this.geofencingBoundaries = other.geofencingBoundaries;
354370
}
355371

street/src/test/java/org/opentripplanner/street/model/edge/StreetEdgeGeofencingTest.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import org.opentripplanner.core.model.id.FeedScopedId;
1919
import org.opentripplanner.service.vehiclerental.model.GeofencingZone;
2020
import org.opentripplanner.service.vehiclerental.model.RentalVehicleType.PropulsionType;
21-
import org.opentripplanner.service.vehiclerental.street.BusinessAreaBorder;
2221
import org.opentripplanner.service.vehiclerental.street.GeofencingBoundaryExtension;
2322
import org.opentripplanner.street.model.RentalFormFactor;
2423
import org.opentripplanner.street.model.StreetMode;
@@ -64,30 +63,30 @@ class StreetEdgeGeofencingTest {
6463
StreetVertex V4 = intersectionVertex("V4", 3, 3);
6564

6665
@Test
67-
public void setBusinessAreaBorder() {
66+
public void addBusinessAreaBorderNetwork() {
6867
var edge = streetEdge(V1, V2);
69-
edge.setBusinessAreaBorder(new BusinessAreaBorder("a"));
68+
edge.addBusinessAreaBorderNetwork("a");
7069

7170
assertTrue(edge.fromv.rentalTraversalBanned(forwardState("a")));
7271
assertFalse(edge.fromv.rentalTraversalBanned(forwardState("b")));
7372
}
7473

7574
@Test
76-
public void removeBusinessAreaBorder() {
75+
public void removeBusinessAreaBorderNetwork() {
7776
var edge = streetEdge(V1, V2);
78-
edge.setBusinessAreaBorder(new BusinessAreaBorder("a"));
77+
edge.addBusinessAreaBorderNetwork("a");
7978

8079
assertTrue(edge.fromv.rentalTraversalBanned(forwardState("a")));
8180

82-
edge.removeBusinessAreaBorder();
81+
edge.removeBusinessAreaBorderNetwork("a");
8382

8483
assertFalse(edge.fromv.rentalTraversalBanned(forwardState("a")));
8584
}
8685

8786
@Test
8887
public void checkNetwork() {
8988
var edge = streetEdge(V1, V2);
90-
edge.setBusinessAreaBorder(new BusinessAreaBorder("a"));
89+
edge.addBusinessAreaBorderNetwork("a");
9190

9291
var state = traverseFromV1(edge);
9392

@@ -108,8 +107,7 @@ public void finishInEdgeWithoutRestrictions() {
108107
@Test
109108
public void leaveBusinessAreaOnFoot() {
110109
var edge1 = streetEdge(V1, V2);
111-
var ext = new BusinessAreaBorder(NETWORK_TIER);
112-
V2.setBusinessAreaBorder(ext);
110+
V2.addBusinessAreaBorderNetwork(NETWORK_TIER);
113111

114112
var results = traverseFromV1(edge1);
115113

0 commit comments

Comments
 (0)