Skip to content

Commit

Permalink
optionally match heading (azimuth)
Browse files Browse the repository at this point in the history
  • Loading branch information
abyrd committed Jan 12, 2024
1 parent a4bf241 commit 3105352
Showing 1 changed file with 61 additions and 1 deletion.
62 changes: 61 additions & 1 deletion src/main/java/com/conveyal/r5/analyst/scenario/SelectLink.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
import gnu.trove.list.array.TIntArrayList;
import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.hash.TIntObjectHashMap;
import org.geotools.referencing.GeodeticCalculator;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineSegment;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Polygon;
import org.slf4j.Logger;
Expand All @@ -23,6 +28,7 @@
import static com.conveyal.r5.common.GeometryUtils.envelopeForCircle;
import static com.conveyal.r5.common.GeometryUtils.polygonForEnvelope;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.geotools.referencing.crs.DefaultGeographicCRS.WGS84;

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

public double radiusMeters;

public double headingDegrees = Double.NaN;

public double headingTolerance = 45;

/// Private derived fields used in subsequent calculations.

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

private int nPatternsWithoutGtfs = 0;

private boolean matchHeading;

@Override
public boolean resolve(TransportNetwork network) {
// Convert the incoming description of the selected link area to a Geometry for computing intersections.
Expand All @@ -72,6 +84,16 @@ public boolean resolve(TransportNetwork network) {
addError("Could not find feed for ID " + feedId);
}
}
// TODO use heading tolerance of 180 to mean "any direction".
if (Double.isFinite(headingDegrees)) {
matchHeading = true;
if (headingDegrees < 0 || headingDegrees >= 360) {
addError("Heading must be in the range [0...360).");
}
if (!Double.isFinite(headingDegrees) || headingTolerance <= 0 || headingTolerance > 80) {
addError("Heading tolerance must be in the range (0...80].");
}
}
return hasErrors();
}

Expand Down Expand Up @@ -108,7 +130,7 @@ public boolean apply(TransportNetwork network) {
TIntArrayList intersectedHops = new TIntArrayList();
for (int hop = 0; hop < hopGeometries.size(); hop++) {
LineString hopGeometry = hopGeometries.get(hop);
if (boxPolygon.intersects(hopGeometry)) {
if (boxPolygon.intersects(hopGeometry) && headingMatches(hopGeometry)) {
intersectedHops.add(hop);
}
}
Expand Down Expand Up @@ -153,6 +175,44 @@ public boolean apply(TransportNetwork network) {
return hasErrors();
}

/**
* NOTE this depends entirely on the hop geometries being directional, in the direction of vehicle movement.
* In shapes from GTFS feeds, this depends on shape_dist_traveled increasing as stop_sequence increases on a trip.
* This seems to be a requirement in the GTFS spec but could stand to be reworded for clarity:
* https://gtfs.org/schedule/reference/#shapestxt
* We also don't seem to validate this requirement when we load GTFS or add shapes to a TripPattern.
*/
private boolean headingMatches (LineString hopGeometry) {
if (!matchHeading) {
return true;
}
// First cut out only the part of the lineString that's inside the area of interest.
Geometry intersection = boxPolygon.intersection(hopGeometry);
if (intersection instanceof LineString fragment) {
// Iterate over line segments, check if any inside the bounding box match heading.
GeodeticCalculator geodeticCalculator = new GeodeticCalculator(WGS84);
CoordinateSequence coords = fragment.getCoordinateSequence();
for (int i = 0; i < coords.size() - 1; i++) {
Coordinate c0 = coords.getCoordinate(i);
Coordinate c1 = coords.getCoordinate(i + 1);
geodeticCalculator.setStartingGeographicPoint(c0.x, c0.y);
geodeticCalculator.setDestinationGeographicPoint(c1.x, c1.y);
double azimuth = geodeticCalculator.getAzimuth();
double delta = (headingDegrees - azimuth) % 360;
if (delta > 180) {
delta = 360 - delta;
}
// LOG.info("Target {} measured {} diff {}", headingDegrees, azimuth, delta);
if (delta <= headingTolerance) {
return true;
}
}
} else {
LOG.warn("Intersection yielded non-linestring type " + intersection.getGeometryType());
}
return false;
}

// By returning false for both affects methods, we make a very shallow copy of the TransitNetwork for efficiency.

@Override
Expand Down

0 comments on commit 3105352

Please sign in to comment.