-
Notifications
You must be signed in to change notification settings - Fork 1k
/
VertexLinker.java
678 lines (607 loc) · 24.7 KB
/
VertexLinker.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
package org.opentripplanner.routing.linking;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.linearref.LinearLocation;
import org.locationtech.jts.linearref.LocationIndexedLine;
import org.locationtech.jts.operation.distance.DistanceOp;
import org.opentripplanner.framework.application.OTPFeature;
import org.opentripplanner.framework.geometry.GeometryUtils;
import org.opentripplanner.framework.geometry.SphericalDistanceLibrary;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.index.EdgeSpatialIndex;
import org.opentripplanner.street.model.edge.AreaEdge;
import org.opentripplanner.street.model.edge.AreaEdgeBuilder;
import org.opentripplanner.street.model.edge.AreaEdgeList;
import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.edge.NamedArea;
import org.opentripplanner.street.model.edge.StreetEdge;
import org.opentripplanner.street.model.vertex.IntersectionVertex;
import org.opentripplanner.street.model.vertex.SplitterVertex;
import org.opentripplanner.street.model.vertex.StreetVertex;
import org.opentripplanner.street.model.vertex.TemporarySplitterVertex;
import org.opentripplanner.street.model.vertex.Vertex;
import org.opentripplanner.street.model.vertex.VertexFactory;
import org.opentripplanner.street.search.TraverseMode;
import org.opentripplanner.street.search.TraverseModeSet;
import org.opentripplanner.transit.service.StopModel;
/**
* This class links transit stops to streets by splitting the streets (unless the stop is extremely
* close to the street intersection).
* <p>
* It is intended to eventually completely replace the existing stop linking code, which had been
* through so many revisions and adaptations to different street and turn representations that it
* was very glitchy. This new code is also intended to be deterministic in linking to streets,
* independent of the order in which the JVM decides to iterate over Maps and even in the presence
* of points that are exactly halfway between multiple candidate linking points.
* <p>
* It would be wise to keep this new incarnation of the linking code relatively simple, considering
* what happened before.
* <p>
* See discussion in pull request #1922, follow up issue #1934, and the original issue calling for
* replacement of the stop linker, #1305.
*/
public class VertexLinker {
/**
* if there are two ways and the distances to them differ by less than this value, we link to both
* of them
*/
private static final double DUPLICATE_WAY_EPSILON_METERS = 0.001;
private static final int INITIAL_SEARCH_RADIUS_METERS = 100;
private static final int MAX_SEARCH_RADIUS_METERS = 1000;
// exit a complex area maximally via this many exit points
private static final int MAX_AREA_LINKS = 300;
private static final GeometryFactory GEOMETRY_FACTORY = GeometryUtils.getGeometryFactory();
/**
* Spatial index of StreetEdges in the graph.
*/
private final EdgeSpatialIndex edgeSpatialIndex;
private final Graph graph;
private final StopModel stopModel;
private final VertexFactory vertexFactory;
// TODO Temporary code until we refactor WalkableAreaBuilder (#3152)
private boolean addExtraEdgesToAreas = true;
/**
* Construct a new VertexLinker. NOTE: Only one VertexLinker should be active on a graph at any
* given time.
*/
public VertexLinker(Graph graph, StopModel stopModel, EdgeSpatialIndex edgeSpatialIndex) {
this.edgeSpatialIndex = edgeSpatialIndex;
this.graph = graph;
this.vertexFactory = new VertexFactory(graph);
this.stopModel = stopModel;
}
public void linkVertexPermanently(
Vertex vertex,
TraverseModeSet traverseModes,
LinkingDirection direction,
BiFunction<Vertex, StreetVertex, List<Edge>> edgeFunction
) {
link(vertex, traverseModes, direction, Scope.PERMANENT, edgeFunction);
}
public DisposableEdgeCollection linkVertexForRealTime(
Vertex vertex,
TraverseModeSet traverseModes,
LinkingDirection direction,
BiFunction<Vertex, StreetVertex, List<Edge>> edgeFunction
) {
return link(vertex, traverseModes, direction, Scope.REALTIME, edgeFunction);
}
public DisposableEdgeCollection linkVertexForRequest(
Vertex vertex,
TraverseModeSet traverseModes,
LinkingDirection direction,
BiFunction<Vertex, StreetVertex, List<Edge>> edgeFunction
) {
return link(vertex, traverseModes, direction, Scope.REQUEST, edgeFunction);
}
public void removeEdgeFromIndex(Edge edge, Scope scope) {
// Edges without geometry will not have been added to the index in the first place
if (edge.getGeometry() != null) {
edgeSpatialIndex.remove(edge.getGeometry().getEnvelopeInternal(), edge, scope);
}
}
public void removePermanentEdgeFromIndex(Edge edge) {
removeEdgeFromIndex(edge, Scope.PERMANENT);
}
// TODO Temporary code until we refactor WalkableAreaBuilder (#3152)
public void setAddExtraEdgesToAreas(Boolean addExtraEdgesToAreas) {
this.addExtraEdgesToAreas = addExtraEdgesToAreas;
}
/** projected distance from stop to edge, in latitude degrees */
private static double distance(Vertex tstop, StreetEdge edge, double xscale) {
// Despite the fact that we want to use a fast somewhat inaccurate projection, still use JTS library tools
// for the actual distance calculations.
LineString transformed = equirectangularProject(edge.getGeometry(), xscale);
return transformed.distance(
GEOMETRY_FACTORY.createPoint(new Coordinate(tstop.getLon() * xscale, tstop.getLat()))
);
}
/** project this linestring to an equirectangular projection */
private static LineString equirectangularProject(LineString geometry, double xScale) {
Coordinate[] coords = new Coordinate[geometry.getNumPoints()];
for (int i = 0; i < coords.length; i++) {
Coordinate c = geometry.getCoordinateN(i);
c = (Coordinate) c.clone();
c.x *= xScale;
coords[i] = c;
}
return GEOMETRY_FACTORY.createLineString(coords);
}
/**
* This method will link the provided vertex into the street graph. This may involve splitting an
* existing edge (if the scope is not PERMANENT, the existing edge will be kept).
* <p>
* In OTP2 where the transit search can be quite fast, searching for a good linking point can be a
* significant fraction of response time. Hannes Junnila has reported >70% speedups in searches by
* making the search radius smaller. Therefore we use an expanding-envelope search, which is more
* efficient in dense areas.
*
* @param vertex Vertex to be linked into the street graph
* @param traverseModes Only street edges allowing one of these modes will be linked
* @param direction The direction of the new edges to be created
* @param scope The scope of the split
* @param edgeFunction How the provided vertex should be linked into the street graph
* @return A DisposableEdgeCollection with edges created by this method. It is the caller's
* responsibility to call the dispose method on this object when the edges are no longer needed.
*/
private DisposableEdgeCollection link(
Vertex vertex,
TraverseModeSet traverseModes,
LinkingDirection direction,
Scope scope,
BiFunction<Vertex, StreetVertex, List<Edge>> edgeFunction
) {
DisposableEdgeCollection tempEdges = (scope != Scope.PERMANENT)
? new DisposableEdgeCollection(graph, scope)
: null;
try {
Set<StreetVertex> streetVertices = linkToStreetEdges(
vertex,
traverseModes,
direction,
scope,
INITIAL_SEARCH_RADIUS_METERS,
tempEdges
);
if (streetVertices.isEmpty()) {
streetVertices =
linkToStreetEdges(
vertex,
traverseModes,
direction,
scope,
MAX_SEARCH_RADIUS_METERS,
tempEdges
);
}
for (StreetVertex streetVertex : streetVertices) {
List<Edge> edges = edgeFunction.apply(vertex, streetVertex);
if (tempEdges != null) {
for (Edge edge : edges) {
tempEdges.addEdge(edge);
}
}
}
} catch (Exception e) {
if (tempEdges != null) {
tempEdges.disposeEdges();
}
throw e;
}
return tempEdges;
}
private Set<StreetVertex> linkToStreetEdges(
Vertex vertex,
TraverseModeSet traverseModes,
LinkingDirection direction,
Scope scope,
int radiusMeters,
DisposableEdgeCollection tempEdges
) {
final double radiusDeg = SphericalDistanceLibrary.metersToDegrees(radiusMeters);
Envelope env = new Envelope(vertex.getCoordinate());
// Perform a simple local equirectangular projection, so distances are expressed in degrees latitude.
final double xscale = Math.cos(vertex.getLat() * Math.PI / 180);
// Expand more in the longitude direction than the latitude direction to account for converging meridians.
env.expandBy(radiusDeg / xscale, radiusDeg);
// Perform several transformations at once on the edges returned by the index. Only consider
// street edges traversable by at least one of the given modes and are still present in the
// graph. Calculate a distance to each of those edges, and keep only the ones within the search
// radius.
List<DistanceTo<StreetEdge>> candidateEdges = edgeSpatialIndex
.query(env, scope)
.filter(StreetEdge.class::isInstance)
.map(StreetEdge.class::cast)
.filter(e -> e.canTraverse(traverseModes) && e.isReachableFromGraph())
.map(e -> new DistanceTo<>(e, distance(vertex, e, xscale)))
.filter(ead -> ead.distanceDegreesLat < radiusDeg)
.toList();
if (candidateEdges.isEmpty()) {
return Set.of();
}
Set<DistanceTo<StreetEdge>> closestEdges = getClosestEdgesPerMode(
traverseModes,
candidateEdges
);
Set<AreaEdgeList> linkedAreas = new HashSet<>();
return closestEdges
.stream()
.map(ce -> link(vertex, ce.item, xscale, scope, direction, tempEdges, linkedAreas))
.filter(v -> v != null)
.collect(Collectors.toSet());
}
/**
* We need to get the closest edges per mode to be sure that we are linking to edges traversable
* by all the specified modes. We use a set here to avoid duplicates in the case that edges are
* traversable by more than one of the modes specified.
*/
private Set<DistanceTo<StreetEdge>> getClosestEdgesPerMode(
TraverseModeSet traverseModeSet,
List<DistanceTo<StreetEdge>> candidateEdges
) {
final double DUPLICATE_WAY_EPSILON_DEGREES = SphericalDistanceLibrary.metersToDegrees(
DUPLICATE_WAY_EPSILON_METERS
);
// The following logic has gone through several different versions using different approaches.
// The core idea is to find all edges that are roughly the same distance from the given vertex, which will
// catch things like superimposed edges going in opposite directions.
// First, all edges within DUPLICATE_WAY_EPSILON_METERS of of the best distance were selected.
// More recently, the edges were sorted in order of increasing distance, and all edges in the list were selected
// up to the point where a distance increase of DUPLICATE_WAY_EPSILON_DEGREES from one edge to the next.
// This was in response to concerns about arbitrary cutoff distances: at any distance, it's always possible
// one half of a dual carriageway (or any other pair of edges in opposite directions) will be caught and the
// other half lost. It seems like this was based on some incorrect premises about floating point calculations
// being non-deterministic.
Set<DistanceTo<StreetEdge>> closesEdges = new HashSet<>();
for (TraverseMode mode : traverseModeSet.getModes()) {
TraverseModeSet modeSet = new TraverseModeSet(mode);
// There is at least one appropriate edge within range.
var candidateEdgesForMode = candidateEdges
.stream()
.filter(e -> e.item.canTraverse(modeSet))
.toList();
if (candidateEdgesForMode.isEmpty()) {
continue;
}
double closestDistance = candidateEdgesForMode
.stream()
.mapToDouble(ce -> ce.distanceDegreesLat)
.min()
.getAsDouble();
// Because this is a set, each instance of DistanceTo<StreetEdge> will only be added once
closesEdges.addAll(
candidateEdges
.stream()
.filter(ce -> ce.distanceDegreesLat <= closestDistance + DUPLICATE_WAY_EPSILON_DEGREES)
.collect(Collectors.toSet())
);
}
return closesEdges;
}
/** Split the edge if necessary return the closest vertex */
private StreetVertex link(
Vertex vertex,
StreetEdge edge,
double xScale,
Scope scope,
LinkingDirection direction,
DisposableEdgeCollection tempEdges,
Set<AreaEdgeList> linkedAreas
) {
// TODO: we've already built this line string, we should save it
LineString orig = edge.getGeometry();
LineString transformed = equirectangularProject(orig, xScale);
LocationIndexedLine il = new LocationIndexedLine(transformed);
LinearLocation ll = il.project(new Coordinate(vertex.getLon() * xScale, vertex.getLat()));
double length = SphericalDistanceLibrary.length(orig);
IntersectionVertex start = null;
boolean snapped = true;
// if we're very close to one end of the line or the other, or endwise, don't bother to split,
// cut to the chase and link directly
// We use a really tiny epsilon here because we only want points that actually snap to exactly the same location on the
// street to use the same vertices. Otherwise the order the stops are loaded in will affect where they are snapped.
if (
ll.getSegmentIndex() == 0 &&
(ll.getSegmentFraction() < 1e-8 || ll.getSegmentFraction() * length < 0.1)
) {
start = (IntersectionVertex) edge.getFromVertex();
}
// -1 converts from count to index. Because of the fencepost problem, npoints - 1 is the "segment"
// past the last point
else if (ll.getSegmentIndex() == orig.getNumPoints() - 1) {
start = (IntersectionVertex) edge.getToVertex();
}
// nPoints - 2: -1 to correct for index vs count, -1 to account for fencepost problem
else if (
ll.getSegmentIndex() == orig.getNumPoints() - 2 &&
(ll.getSegmentFraction() > 1 - 1e-8 || (1 - ll.getSegmentFraction()) * length < 0.1)
) {
start = (IntersectionVertex) edge.getToVertex();
} else {
snapped = false;
boolean split = true;
// if vertex is inside an area, no need to snap to nearest edge and split it
if (this.addExtraEdgesToAreas && edge instanceof AreaEdge aEdge) {
AreaEdgeList ael = aEdge.getArea();
if (ael.getGeometry().contains(GEOMETRY_FACTORY.createPoint(vertex.getCoordinate()))) {
// do not relink again to the area when many edges are equally close
if (!linkedAreas.add(ael)) {
return null;
}
if (vertex instanceof IntersectionVertex iv) {
start = iv;
} else {
start = splitVertex(aEdge, scope, direction, vertex.getLon(), vertex.getLat());
}
split = false;
}
}
if (split) {
// split the edge, get the split vertex
start = split(edge, ll, scope, direction, tempEdges);
}
}
if (this.addExtraEdgesToAreas && edge instanceof AreaEdge aEdge) {
if (!snapped || !aEdge.getArea().visibilityVertices.contains(start)) {
addAreaVertex(start, aEdge.getArea(), scope, tempEdges);
}
}
// TODO Consider moving this code
if (OTPFeature.FlexRouting.isOn()) {
FlexLocationAdder.addFlexLocations(edge, start, stopModel);
}
return start;
}
/**
* Split the street edge at the given fraction
*
* @param originalEdge to be split
* @param ll fraction at which to split the edge
* @param scope the scope of the split
* @param direction what direction to link the edges
* @param tempEdges collection of temporary edges
* @return Splitter vertex with added new edges
*/
private SplitterVertex split(
StreetEdge originalEdge,
LinearLocation ll,
Scope scope,
LinkingDirection direction,
DisposableEdgeCollection tempEdges
) {
LineString geometry = originalEdge.getGeometry();
// create the geometries
Coordinate splitPoint = ll.getCoordinate(geometry);
SplitterVertex v = splitVertex(originalEdge, scope, direction, splitPoint.x, splitPoint.y);
// Split the 'edge' at 'v' in 2 new edges and connect these 2 edges to the
// existing vertices
var newEdges = scope == Scope.PERMANENT
? originalEdge.splitDestructively(v)
: originalEdge.splitNonDestructively(v, tempEdges, direction);
if (scope == Scope.REALTIME || scope == Scope.PERMANENT) {
// update indices of new edges
if (newEdges.head() != null) {
edgeSpatialIndex.insert(newEdges.head().getGeometry(), newEdges.head(), scope);
}
if (newEdges.tail() != null) {
edgeSpatialIndex.insert(newEdges.tail().getGeometry(), newEdges.tail(), scope);
}
if (scope == Scope.PERMANENT) {
// remove original edges from the spatial index
// This iterates over the entire rectangular envelope of the edge rather than the segments making it up.
// It will be inefficient for very long edges, but creating a new remove method mirroring the more efficient
// insert logic is not trivial and would require additional testing of the spatial index.
removeEdgeFromIndex(originalEdge, scope);
// remove original edge from the graph
graph.removeEdge(originalEdge);
}
}
return v;
}
private SplitterVertex splitVertex(
StreetEdge originalEdge,
Scope scope,
LinkingDirection direction,
double x,
double y
) {
SplitterVertex v;
String uniqueSplitLabel = "split_" + graph.nextSplitNumber++;
if (scope != Scope.PERMANENT) {
TemporarySplitterVertex tsv = new TemporarySplitterVertex(
uniqueSplitLabel,
x,
y,
originalEdge,
direction == LinkingDirection.OUTGOING
);
tsv.setWheelchairAccessible(originalEdge.isWheelchairAccessible());
v = tsv;
} else {
v = vertexFactory.splitter(originalEdge, x, y, uniqueSplitLabel);
}
v.addRentalRestriction(originalEdge.getFromVertex().rentalRestrictions());
v.addRentalRestriction(originalEdge.getToVertex().rentalRestrictions());
return v;
}
private static class DistanceTo<T> {
T item;
// Possible optimization: store squared lat to skip thousands of sqrt operations
// However we're using JTS distance functions that probably won't allow us to skip the final sqrt call.
double distanceDegreesLat;
public DistanceTo(T item, double distanceDegreesLat) {
this.item = item;
this.distanceDegreesLat = distanceDegreesLat;
}
@Override
public int hashCode() {
return Objects.hash(item);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DistanceTo<?> that = (DistanceTo<?>) o;
return Objects.equals(item, that.item);
}
}
/**
* Link a new vertex permanently with area geometry
*/
public void addPermanentAreaVertex(IntersectionVertex newVertex, AreaEdgeList edgeList) {
addAreaVertex(newVertex, edgeList, Scope.PERMANENT, null);
}
/**
* Safely add a vertex to an area. This creates edges to all other vertices unless those edges
* would cross one of the original edges.
*/
public void addAreaVertex(
IntersectionVertex newVertex,
AreaEdgeList edgeList,
Scope scope,
DisposableEdgeCollection tempEdges
) {
List<NamedArea> areas = edgeList.getAreas();
Geometry origPolygon = edgeList.getGeometry();
Geometry polygon = origPolygon.union(origPolygon.getBoundary()).buffer(0.000001);
HashSet<IntersectionVertex> visibilityVertices = edgeList.visibilityVertices;
// Due to truncating of precision in storage of the edge geometry, the new split vertex
// might be located just outside the area, so we calculate the point closest to the polygon
// for the comparison.
Coordinate[] nearestPoints = DistanceOp.nearestPoints(
polygon,
GEOMETRY_FACTORY.createPoint(newVertex.getCoordinate())
);
int added = 0;
// if area is too complex, consider only part of visibility nodes
float skip_ratio = (float) MAX_AREA_LINKS / (float) visibilityVertices.size();
int i = 0;
float sum_i = 0;
for (IntersectionVertex v : visibilityVertices) {
sum_i += skip_ratio;
if (Math.floor(sum_i) < i + 1) {
continue;
}
i = (int) Math.floor(sum_i);
LineString newGeometry = GEOMETRY_FACTORY.createLineString(
new Coordinate[] { nearestPoints[0], v.getCoordinate() }
);
// ensure that new edge does not leave the bounds of the original area, or
// fall into any holes
if (!polygon.contains(newGeometry)) {
continue;
}
// check to see if this splits multiple NamedAreas. This code is rather similar to
// code in OSMGBI, but the data structures are different
createSegments(newVertex, v, edgeList, areas, scope, tempEdges);
added++;
}
// TODO: Temporary fix for unconnected area edges. This should go away when moving walkable
// area calculation to be done after stop linking
if (added == 0) {
for (IntersectionVertex v : visibilityVertices) {
createSegments(newVertex, v, edgeList, areas, scope, tempEdges);
}
}
if (scope == Scope.PERMANENT) {
visibilityVertices.add(newVertex);
}
}
static final Set<TraverseMode> noThruModes = Set.of(
TraverseMode.WALK,
TraverseMode.BICYCLE,
TraverseMode.CAR
);
private Set<TraverseMode> getNoThruModes(Collection<Edge> edges) {
var modes = new HashSet<>(noThruModes);
for (Edge e : edges) {
if (e instanceof StreetEdge se) {
for (TraverseMode tm : noThruModes) {
if (!se.isNoThruTraffic(tm)) {
modes.remove(tm);
}
}
}
}
return modes;
}
private void createSegments(
IntersectionVertex from,
IntersectionVertex to,
AreaEdgeList ael,
List<NamedArea> areas,
Scope scope,
DisposableEdgeCollection tempEdges
) {
// Check that vertices are not yet linked
if (from.isConnected(to)) {
return;
}
LineString line = GEOMETRY_FACTORY.createLineString(
new Coordinate[] { from.getCoordinate(), to.getCoordinate() }
);
NamedArea hit = null;
for (NamedArea area : areas) {
Geometry polygon = area.getPolygon();
Geometry intersection = polygon.intersection(line);
if (intersection.getLength() > 0.000001) {
hit = area;
break;
}
}
if (hit != null) {
// If more than one area intersects, we pick one by random for the name & properties
double length = SphericalDistanceLibrary.distance(to.getCoordinate(), from.getCoordinate());
// apply consistent NoThru restrictions
// if all joining edges are nothru, then the new edge should be as well
var incomingNoThruModes = getNoThruModes(to.getIncoming());
var outgoingNoThruModes = getNoThruModes(to.getIncoming());
AreaEdgeBuilder areaEdgeBuilder = new AreaEdgeBuilder()
.withFromVertex(from)
.withToVertex(to)
.withGeometry(line)
.withName(hit.getName())
.withMeterLength(length)
.withPermission(hit.getPermission())
.withBack(false)
.withArea(ael);
for (TraverseMode tm : outgoingNoThruModes) {
areaEdgeBuilder.withNoThruTrafficTraverseMode(tm);
}
AreaEdge areaEdge = areaEdgeBuilder.buildAndConnect();
if (scope != Scope.PERMANENT) {
tempEdges.addEdge(areaEdge);
}
AreaEdgeBuilder reverseAreaEdgeBuilder = new AreaEdgeBuilder()
.withFromVertex(to)
.withToVertex(from)
.withGeometry(line.reverse())
.withName(hit.getName())
.withMeterLength(length)
.withPermission(hit.getPermission())
.withBack(true)
.withArea(ael);
for (TraverseMode tm : incomingNoThruModes) {
reverseAreaEdgeBuilder.withNoThruTrafficTraverseMode(tm);
}
AreaEdge reverseAreaEdge = reverseAreaEdgeBuilder.buildAndConnect();
if (scope != Scope.PERMANENT) {
tempEdges.addEdge(reverseAreaEdge);
}
}
}
}