9
9
import gnu .trove .list .array .TIntArrayList ;
10
10
import gnu .trove .map .TIntObjectMap ;
11
11
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 ;
12
17
import org .locationtech .jts .geom .LineString ;
13
18
import org .locationtech .jts .geom .Polygon ;
14
19
import org .slf4j .Logger ;
23
28
import static com .conveyal .r5 .common .GeometryUtils .envelopeForCircle ;
24
29
import static com .conveyal .r5 .common .GeometryUtils .polygonForEnvelope ;
25
30
import static com .google .common .base .Strings .isNullOrEmpty ;
31
+ import static org .geotools .referencing .crs .DefaultGeographicCRS .WGS84 ;
26
32
27
33
/**
28
34
* 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 {
42
48
43
49
public double radiusMeters ;
44
50
51
+ public double headingDegrees = Double .NaN ;
52
+
53
+ public double headingTolerance = 45 ;
54
+
45
55
/// Private derived fields used in subsequent calculations.
46
56
47
57
private Polygon boxPolygon ;
@@ -52,6 +62,8 @@ public class SelectLink extends Modification {
52
62
53
63
private int nPatternsWithoutGtfs = 0 ;
54
64
65
+ private boolean matchHeading ;
66
+
55
67
@ Override
56
68
public boolean resolve (TransportNetwork network ) {
57
69
// Convert the incoming description of the selected link area to a Geometry for computing intersections.
@@ -72,6 +84,16 @@ public boolean resolve(TransportNetwork network) {
72
84
addError ("Could not find feed for ID " + feedId );
73
85
}
74
86
}
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
+ }
75
97
return hasErrors ();
76
98
}
77
99
@@ -108,7 +130,7 @@ public boolean apply(TransportNetwork network) {
108
130
TIntArrayList intersectedHops = new TIntArrayList ();
109
131
for (int hop = 0 ; hop < hopGeometries .size (); hop ++) {
110
132
LineString hopGeometry = hopGeometries .get (hop );
111
- if (boxPolygon .intersects (hopGeometry )) {
133
+ if (boxPolygon .intersects (hopGeometry ) && headingMatches ( hopGeometry ) ) {
112
134
intersectedHops .add (hop );
113
135
}
114
136
}
@@ -153,6 +175,44 @@ public boolean apply(TransportNetwork network) {
153
175
return hasErrors ();
154
176
}
155
177
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
+
156
216
// By returning false for both affects methods, we make a very shallow copy of the TransitNetwork for efficiency.
157
217
158
218
@ Override
0 commit comments