Skip to content

Commit 3105352

Browse files
committed
optionally match heading (azimuth)
1 parent a4bf241 commit 3105352

File tree

1 file changed

+61
-1
lines changed

1 file changed

+61
-1
lines changed

src/main/java/com/conveyal/r5/analyst/scenario/SelectLink.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
import gnu.trove.list.array.TIntArrayList;
1010
import gnu.trove.map.TIntObjectMap;
1111
import gnu.trove.map.hash.TIntObjectHashMap;
12+
import org.geotools.referencing.GeodeticCalculator;
13+
import org.locationtech.jts.geom.Coordinate;
14+
import org.locationtech.jts.geom.CoordinateSequence;
15+
import org.locationtech.jts.geom.Geometry;
16+
import org.locationtech.jts.geom.LineSegment;
1217
import org.locationtech.jts.geom.LineString;
1318
import org.locationtech.jts.geom.Polygon;
1419
import org.slf4j.Logger;
@@ -23,6 +28,7 @@
2328
import static com.conveyal.r5.common.GeometryUtils.envelopeForCircle;
2429
import static com.conveyal.r5.common.GeometryUtils.polygonForEnvelope;
2530
import static com.google.common.base.Strings.isNullOrEmpty;
31+
import static org.geotools.referencing.crs.DefaultGeographicCRS.WGS84;
2632

2733
/**
2834
* This custom Modification restricts CSV path output to only include transit passing through a specified rectangle.
@@ -42,6 +48,10 @@ public class SelectLink extends Modification {
4248

4349
public double radiusMeters;
4450

51+
public double headingDegrees = Double.NaN;
52+
53+
public double headingTolerance = 45;
54+
4555
/// Private derived fields used in subsequent calculations.
4656

4757
private Polygon boxPolygon;
@@ -52,6 +62,8 @@ public class SelectLink extends Modification {
5262

5363
private int nPatternsWithoutGtfs = 0;
5464

65+
private boolean matchHeading;
66+
5567
@Override
5668
public boolean resolve(TransportNetwork network) {
5769
// Convert the incoming description of the selected link area to a Geometry for computing intersections.
@@ -72,6 +84,16 @@ public boolean resolve(TransportNetwork network) {
7284
addError("Could not find feed for ID " + feedId);
7385
}
7486
}
87+
// TODO use heading tolerance of 180 to mean "any direction".
88+
if (Double.isFinite(headingDegrees)) {
89+
matchHeading = true;
90+
if (headingDegrees < 0 || headingDegrees >= 360) {
91+
addError("Heading must be in the range [0...360).");
92+
}
93+
if (!Double.isFinite(headingDegrees) || headingTolerance <= 0 || headingTolerance > 80) {
94+
addError("Heading tolerance must be in the range (0...80].");
95+
}
96+
}
7597
return hasErrors();
7698
}
7799

@@ -108,7 +130,7 @@ public boolean apply(TransportNetwork network) {
108130
TIntArrayList intersectedHops = new TIntArrayList();
109131
for (int hop = 0; hop < hopGeometries.size(); hop++) {
110132
LineString hopGeometry = hopGeometries.get(hop);
111-
if (boxPolygon.intersects(hopGeometry)) {
133+
if (boxPolygon.intersects(hopGeometry) && headingMatches(hopGeometry)) {
112134
intersectedHops.add(hop);
113135
}
114136
}
@@ -153,6 +175,44 @@ public boolean apply(TransportNetwork network) {
153175
return hasErrors();
154176
}
155177

178+
/**
179+
* NOTE this depends entirely on the hop geometries being directional, in the direction of vehicle movement.
180+
* In shapes from GTFS feeds, this depends on shape_dist_traveled increasing as stop_sequence increases on a trip.
181+
* This seems to be a requirement in the GTFS spec but could stand to be reworded for clarity:
182+
* https://gtfs.org/schedule/reference/#shapestxt
183+
* We also don't seem to validate this requirement when we load GTFS or add shapes to a TripPattern.
184+
*/
185+
private boolean headingMatches (LineString hopGeometry) {
186+
if (!matchHeading) {
187+
return true;
188+
}
189+
// First cut out only the part of the lineString that's inside the area of interest.
190+
Geometry intersection = boxPolygon.intersection(hopGeometry);
191+
if (intersection instanceof LineString fragment) {
192+
// Iterate over line segments, check if any inside the bounding box match heading.
193+
GeodeticCalculator geodeticCalculator = new GeodeticCalculator(WGS84);
194+
CoordinateSequence coords = fragment.getCoordinateSequence();
195+
for (int i = 0; i < coords.size() - 1; i++) {
196+
Coordinate c0 = coords.getCoordinate(i);
197+
Coordinate c1 = coords.getCoordinate(i + 1);
198+
geodeticCalculator.setStartingGeographicPoint(c0.x, c0.y);
199+
geodeticCalculator.setDestinationGeographicPoint(c1.x, c1.y);
200+
double azimuth = geodeticCalculator.getAzimuth();
201+
double delta = (headingDegrees - azimuth) % 360;
202+
if (delta > 180) {
203+
delta = 360 - delta;
204+
}
205+
// LOG.info("Target {} measured {} diff {}", headingDegrees, azimuth, delta);
206+
if (delta <= headingTolerance) {
207+
return true;
208+
}
209+
}
210+
} else {
211+
LOG.warn("Intersection yielded non-linestring type " + intersection.getGeometryType());
212+
}
213+
return false;
214+
}
215+
156216
// By returning false for both affects methods, we make a very shallow copy of the TransitNetwork for efficiency.
157217

158218
@Override

0 commit comments

Comments
 (0)