Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 18 additions & 122 deletions web-bundle/src/main/java/com/graphhopper/resources/BufferResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.PrecisionModel;

import jakarta.inject.Inject;
Expand All @@ -43,37 +42,13 @@ public class BufferResource {
private final GraphHopperConfig config;
private final DistanceCalculationHelper distanceHelper;
private final EdgeAngleCalculator angleCalculator;
private final DirectionFilterHelper directionFilterHelper;

private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(1E8));

//region Constants, Enums and Records
private static final double PROXIMITY_THRESHOLD_METERS = 6.096; // 20 feet
private static final double INITIAL_SEARCH_RADIUS_DEGREES = 0.0001; // Roughly 11 meters
private static final int DUE_NORTHEAST = 45;
private static final int DUE_SOUTHEAST = 135;
private static final int DUE_SOUTHWEST = 225;
private static final int DUE_NORTHWEST = 315;


enum Direction {
UNKNOWN,
NORTH,
SOUTH,
EAST,
WEST,
NORTHWEST,
NORTHEAST,
SOUTHWEST,
SOUTHEAST,
BOTH
}

private static final EnumSet<Direction> CARDINAL_DIRECTIONS = EnumSet.of(
Direction.NORTH,
Direction.SOUTH,
Direction.EAST,
Direction.WEST
);

/**
* Represents the result of edge matching logic for the given location.
Expand Down Expand Up @@ -103,6 +78,7 @@ public BufferResource(GraphHopperConfig config, GraphHopper graphHopper) {
this.edgeExplorer = graph.createEdgeExplorer();
this.distanceHelper = new DistanceCalculationHelper(graph, nodeAccess);
this.angleCalculator = new EdgeAngleCalculator(graph);
this.directionFilterHelper = new DirectionFilterHelper();

EncodingManager encodingManager = graphHopper.getEncodingManager();
this.roundaboutAccessEnc = encodingManager.getBooleanEncodedValue(Roundabout.KEY);
Expand All @@ -126,7 +102,6 @@ public Response doGet(
validateInputParameters(queryMultiplier, requireRoadNameMatch, roadName);

StopWatch stopWatch = new StopWatch().start();
Direction directionEnum = Direction.valueOf(direction.toUpperCase());

EdgeMatchingDecision edgeMatchingDecision = determineEdgeMatchingStrategy(roadName, requireRoadNameMatch, point);
String edgeRoadName = edgeMatchingDecision.roadName();
Expand All @@ -150,7 +125,10 @@ public Response doGet(
}
}

List<LineString> filteredLineStrings = filterPathsByDirection(generatedPaths, buildUpstream, directionEnum);
List<LineString> filteredLineStrings = directionFilterHelper.filterPathsByDirection(
generatedPaths,
buildUpstream,
DirectionFilterHelper.Direction.valueOf(direction.toUpperCase()));
return createGeoJsonResponse(filteredLineStrings, stopWatch);
}

Expand Down Expand Up @@ -567,7 +545,7 @@ private BufferFeature buildPathToThresholdDistance(final BufferFeature startFeat
List<Integer> potentialEdges = new ArrayList<>();
List<Integer> potentialRoundaboutEdges = new ArrayList<>();
List<Integer> potentialRoundaboutEdgesWithoutName = new ArrayList<>();
List<Integer> potentialUnnamedEdges = new ArrayList<>();
List<Integer> candidateEdgesFromUnnamedRoad = new ArrayList<>();
currentEdge = -1;

while (iterator.next()) {
Expand Down Expand Up @@ -608,15 +586,15 @@ else if (roadName == null && !tempState.get(this.roundaboutAccessEnc)) {
boolean matchesPreviousEdgeName = (currentIsBlank && previousIsBlank)
|| (currentEdgeRoadName != null && currentEdgeRoadName.equals(previousRoadName));

if (matchesPreviousEdgeName) {
if (!currentIsBlank) {
currentEdge = tempEdge;
usedEdges.add(tempEdge);
break;
}
potentialUnnamedEdges.add(tempEdge);
} else if (previousIsBlank) {
potentialEdges.add(tempEdge);
if (matchesPreviousEdgeName && !currentIsBlank) {
currentEdge = tempEdge;
usedEdges.add(tempEdge);
break;
}

// Collect both named and unnamed edges for angle-based selection when continuing from an unnamed road.
if (previousIsBlank) {
candidateEdgesFromUnnamedRoad.add(tempEdge);
}
}

Expand All @@ -628,8 +606,8 @@ else if (tempState.get(this.roundaboutAccessEnc)) {
}

// No bidirectional edge found. Choose from potential edge lists.
if (!potentialUnnamedEdges.isEmpty()) {
currentEdge = angleCalculator.selectStraightestEdge(potentialUnnamedEdges, currentState, currentNode);
if (!candidateEdgesFromUnnamedRoad.isEmpty()) {
currentEdge = angleCalculator.selectStraightestEdge(candidateEdgesFromUnnamedRoad, currentState, currentNode);
usedEdges.add(currentEdge);
}
else if (currentEdge == -1) {
Expand Down Expand Up @@ -899,88 +877,6 @@ private Integer findClosestEdgeByDistance(List<Integer> edges, double lat, doubl
return closestEdge;
}

//endregion
//region Direction Filtering

/**
* Filters a list of LineStrings based on the specified cardinal or intercardinal direction.
* Compares the furthest points of the first and last LineStrings to determine which aligns
* best with the desired direction, returning only the selected LineString.
* @param lineStrings list of LineStrings to filter
* @param buildUpstream whether the paths were built up or downstream
* @param directionEnum the direction to filter by
* @return A list of LineStrings filtered by the specified direction.
*/
private List<LineString> filterPathsByDirection(List<LineString> lineStrings, Boolean buildUpstream, Direction directionEnum) {
if (lineStrings == null || lineStrings.isEmpty() || lineStrings.size() == 1) {
return lineStrings != null ? lineStrings : Collections.emptyList();
}

if (directionEnum == Direction.BOTH || directionEnum == Direction.UNKNOWN) {
return lineStrings;
}

Point furthestPointOfFirstPath = buildUpstream ? lineStrings.get(0).getStartPoint() : lineStrings.get(0).getEndPoint();
Point furthestPointOfSecondPath = buildUpstream ? lineStrings.get(lineStrings.size() - 1).getStartPoint() : lineStrings.get(lineStrings.size() - 1).getEndPoint();
boolean selectFirstPath = true;
boolean isNonCardinal = !CARDINAL_DIRECTIONS.contains(directionEnum);

switch (directionEnum) {
case NORTH:
selectFirstPath = buildUpstream
? furthestPointOfFirstPath.getY() < furthestPointOfSecondPath.getY()
: furthestPointOfFirstPath.getY() > furthestPointOfSecondPath.getY();
break;
case SOUTH:
selectFirstPath = buildUpstream
? furthestPointOfFirstPath.getY() > furthestPointOfSecondPath.getY()
: furthestPointOfFirstPath.getY() < furthestPointOfSecondPath.getY();
break;
case EAST:
selectFirstPath = buildUpstream
? furthestPointOfFirstPath.getX() < furthestPointOfSecondPath.getX()
: furthestPointOfFirstPath.getX() > furthestPointOfSecondPath.getX();
break;
case WEST:
selectFirstPath = buildUpstream
? furthestPointOfFirstPath.getX() > furthestPointOfSecondPath.getX()
: furthestPointOfFirstPath.getX() < furthestPointOfSecondPath.getX();
break;
default:
break;
}

if (isNonCardinal) {
// For non-cardinal directions: Calculate bearing between terminal points for better direction accuracy.
// Splits the circle into two halves and checks if the angle is within the specified half.
int bearing = (int) Math.round(AngleCalc.ANGLE_CALC.calcAzimuth(
furthestPointOfFirstPath.getY(), furthestPointOfFirstPath.getX(),
furthestPointOfSecondPath.getY(),furthestPointOfSecondPath.getX()));

switch (directionEnum) {
case NORTHEAST:
selectFirstPath = bearing >= DUE_NORTHWEST || bearing <= DUE_SOUTHEAST;
break;
case SOUTHWEST:
selectFirstPath = bearing > DUE_SOUTHEAST && bearing < DUE_NORTHWEST;
break;
case NORTHWEST:
selectFirstPath = bearing >= DUE_SOUTHWEST || bearing <= DUE_NORTHEAST;
break;
case SOUTHEAST:
selectFirstPath = bearing > DUE_NORTHEAST && bearing < DUE_SOUTHWEST;
break;
default:
break;
}
}


return selectFirstPath
? Collections.singletonList(lineStrings.get(0))
: Collections.singletonList(lineStrings.get(lineStrings.size() - 1));
}

//endregion
//region Road Name Helpers

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package com.graphhopper.util;

import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;

import java.util.Collections;
import java.util.EnumSet;
import java.util.List;

/**
* Helper class for filtering paths based on cardinal and intercardinal directions.
*/
public class DirectionFilterHelper {

private static final int BEARING_DUE_NORTHEAST = 45;
private static final int BEARING_DUE_SOUTHEAST = 135;
private static final int BEARING_DUE_SOUTHWEST = 225;
private static final int BEARING_DUE_NORTHWEST = 315;

public enum Direction {
UNKNOWN,
NORTH,
SOUTH,
EAST,
WEST,
NORTHWEST,
NORTHEAST,
SOUTHWEST,
SOUTHEAST,
BOTH
}

private static final EnumSet<Direction> CARDINAL_DIRECTIONS = EnumSet.of(
Direction.NORTH,
Direction.SOUTH,
Direction.EAST,
Direction.WEST
);

/**
* Filters a list of 2 LineStrings based on the specified cardinal or intercardinal direction.
* Compares the furthest points of the first and last LineStrings to determine which aligns
* best with the desired direction, returning only the selected LineString.
* @param lineStrings list of 2 LineStrings to filter
* @param buildUpstream whether the paths were built up or downstream
* @param direction the direction to filter by
* @return A list of LineStrings filtered by the specified direction.
*/
public List<LineString> filterPathsByDirection(List<LineString> lineStrings, Boolean buildUpstream, Direction direction) {
if (lineStrings == null || lineStrings.isEmpty() || lineStrings.size() == 1) {
return lineStrings != null ? lineStrings : Collections.emptyList();
}

if (direction == Direction.BOTH || direction == Direction.UNKNOWN) {
return lineStrings;
}

Point furthestPointOfFirstPath = getTerminalPoint(lineStrings.get(0), buildUpstream);
Point furthestPointOfSecondPath = getTerminalPoint(lineStrings.get(lineStrings.size() - 1), buildUpstream);

boolean selectFirstPath = true;
boolean isNonCardinal = !CARDINAL_DIRECTIONS.contains(direction);

if (isNonCardinal) {
// For non-cardinal directions: Calculate bearing between terminal points for better direction accuracy.
// Since selectFirstPath defaults to true, we only need to adjust if the bearing is 'clearly the other direction.'
// As in the cardinal directions, if the bearing is within 30 degrees of perpendicular to the desired direction,
// then leave selectFirstPath = true.
int bearing = (int) Math.round(AngleCalc.ANGLE_CALC.calcAzimuth(
furthestPointOfFirstPath.getY(), furthestPointOfFirstPath.getX(),
furthestPointOfSecondPath.getY(),furthestPointOfSecondPath.getX()));

// compute target bearing for the requested intercardinal direction
int targetBearing = switch (direction) {
case NORTHEAST -> buildUpstream ? BEARING_DUE_NORTHEAST : BEARING_DUE_SOUTHWEST;
case SOUTHEAST -> buildUpstream ? BEARING_DUE_SOUTHEAST : BEARING_DUE_NORTHWEST;
case SOUTHWEST -> buildUpstream ? BEARING_DUE_SOUTHWEST : BEARING_DUE_NORTHEAST;
case NORTHWEST -> buildUpstream ? BEARING_DUE_NORTHWEST : BEARING_DUE_SOUTHEAST;
default -> 0;
};

// convert difference to range 0-180 (for instance, if two bearings are a difference of 190 then use 170 since going the other way is shorter)
int diff = Math.abs(bearing - targetBearing);
if (diff > 180) diff = 360 - diff;

// more than 120 degrees off -> selectFirstPath = false
selectFirstPath = diff <= 120;
}
else {
// Calculate the total separation between the two terminal points
double totalSeparation = Math.sqrt(
Math.pow(furthestPointOfFirstPath.getX() - furthestPointOfSecondPath.getX(), 2) +
Math.pow(furthestPointOfFirstPath.getY() - furthestPointOfSecondPath.getY(), 2)
);
double halfSeparation = totalSeparation / 2;

switch (direction) {
case NORTH:
case SOUTH:
// Default to the first path if Y difference < half of total separation (paths too horizontal for N/S determination)
double yDiff = Math.abs(furthestPointOfFirstPath.getY() - furthestPointOfSecondPath.getY());
if (yDiff < halfSeparation) break;

selectFirstPath = compareCoordinates(
furthestPointOfFirstPath.getY(),
furthestPointOfSecondPath.getY(),
direction == Direction.NORTH,
buildUpstream
);
break;
case EAST:
case WEST:
// Default to the first path if X difference < half of total separation (paths too vertical for E/W determination)
double xDiff = Math.abs(furthestPointOfFirstPath.getX() - furthestPointOfSecondPath.getX());
if (xDiff < halfSeparation) break;

selectFirstPath = compareCoordinates(
furthestPointOfFirstPath.getX(),
furthestPointOfSecondPath.getX(),
direction == Direction.EAST,
buildUpstream
);
break;
default:
break;
}
}

return selectFirstPath
? Collections.singletonList(lineStrings.get(0))
: Collections.singletonList(lineStrings.get(lineStrings.size() - 1));
}

/**
* Retrieves the terminal point of a LineString based on the build direction.
*
* @param lineString the LineString to extract the terminal point from
* @param buildUpstream if true, returns the start point; if false, returns the end point
* @return the terminal Point of the LineString
*/
private Point getTerminalPoint(LineString lineString, boolean buildUpstream) {
return buildUpstream ? lineString.getStartPoint() : lineString.getEndPoint();
}

/**
* Compares coordinate values to determine path selection based on the direction.
*
* @param firstCoordinate coordinate value from the first path (X for E/W, Y for N/S)
* @param secondCoordinate coordinate value from the second path
* @param isPositiveDirection true for NORTH/EAST, false for SOUTH/WEST
* @param buildUpstream whether paths were built upstream
* @return true to select the first path, false to select the second path
*/
private boolean compareCoordinates(double firstCoordinate, double secondCoordinate, boolean isPositiveDirection, boolean buildUpstream) {
if (isPositiveDirection) {
return buildUpstream ? firstCoordinate < secondCoordinate : firstCoordinate > secondCoordinate;
} else {
return buildUpstream ? firstCoordinate > secondCoordinate : firstCoordinate < secondCoordinate;
}
}
}
Loading