Skip to content

Commit 699043f

Browse files
msbarrykarussell
andauthored
Improve resolution of elevation profiles with 3D Douglas Peucker and long edge sampling (graphhopper#1953)
* replace calcMean with bilinear interpolate * fix * refactor all distance calculations to be elevation-aware * some edge case handling and remove DistanceCalc2D * throw error on graph.elevation.calcmean * tighten range check * change config parameter to 'graph.elevation.interpolate: bilinear' * rm extra change * Revert "change config parameter to 'graph.elevation.interpolate: bilinear'" This reverts commit 25c3d9b. * handle graph.elevation.interpolate=bilinear * edge sampling and 3d polyline simplification * add integration test * integration test * todo * simplify IT * license * switch elevation factor to elevation max distance * fix comment and warn on long edge sampling without bilinear interpolation * new line * add import * config-example comments * tweak config-example * fix integration test * fix bad merge * calc 3d distance * fix * fix start/end being the same during simplification * fix normalized edge distance 3d euclidean * fix calc edge distance * fix * use great circle interpolation on long segments * dont recompute same thing * default values * fix test * fix merge issues * rm extra file * remove local, duplicated variable elevationWayPointMaxDistance * let elevation_way_point_max_distance be controlled per-request Co-authored-by: Peter <[email protected]>
1 parent d94783d commit 699043f

File tree

22 files changed

+620
-55
lines changed

22 files changed

+620
-55
lines changed

api/src/main/java/com/graphhopper/util/DistanceCalc.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ GHPoint calcCrossingPointToEdge(double r_lat_deg, double r_lon_deg,
113113
GHPoint projectCoordinate(double lat, double lon,
114114
double distanceInMeter, double headingClockwiseFromNorth);
115115

116+
/**
117+
* This methods creates a point (lat, lon in degrees) a fraction of the distance along the path from (lat1, lon1)
118+
* to (lat2, lon2).
119+
*/
120+
GHPoint intermediatePoint(double f, double lat1, double lon1, double lat2, double lon2);
121+
116122
/*
117123
* Simple heuristic to detect if the specified two points are crossing the boundary +-180°. See
118124
* #667

api/src/main/java/com/graphhopper/util/DistanceCalcEarth.java

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -119,91 +119,71 @@ public BBox createBBox(double lat, double lon, double radiusInMeter) {
119119
public double calcNormalizedEdgeDistance(double r_lat_deg, double r_lon_deg,
120120
double a_lat_deg, double a_lon_deg,
121121
double b_lat_deg, double b_lon_deg) {
122-
return calcNormalizedEdgeDistanceNew(r_lat_deg, r_lon_deg, a_lat_deg, a_lon_deg, b_lat_deg, b_lon_deg, false);
123-
}
124-
125-
@Override
126-
public double calcNormalizedEdgeDistance3D(double r_lat_deg, double r_lon_deg, double r_ele_m,
127-
double a_lat_deg, double a_lon_deg, double a_ele_m,
128-
double b_lat_deg, double b_lon_deg, double b_ele_m) {
129-
if (Double.isNaN(r_ele_m) || Double.isNaN(a_ele_m) || Double.isNaN(b_ele_m))
130-
return calcNormalizedEdgeDistance(r_lat_deg, r_lon_deg, a_lat_deg, a_lon_deg, b_lat_deg, b_lon_deg);
131-
132122
double shrinkFactor = calcShrinkFactor(a_lat_deg, b_lat_deg);
133123

134124
double a_lat = a_lat_deg;
135125
double a_lon = a_lon_deg * shrinkFactor;
136-
double a_ele = a_ele_m / METERS_PER_DEGREE;
137126

138127
double b_lat = b_lat_deg;
139128
double b_lon = b_lon_deg * shrinkFactor;
140-
double b_ele = b_ele_m / METERS_PER_DEGREE;
141129

142130
double r_lat = r_lat_deg;
143131
double r_lon = r_lon_deg * shrinkFactor;
144-
double r_ele = r_ele_m / METERS_PER_DEGREE;
145132

146133
double delta_lon = b_lon - a_lon;
147134
double delta_lat = b_lat - a_lat;
148-
double delta_ele = b_ele - a_ele;
149135

150-
double norm = delta_lon * delta_lon + delta_lat * delta_lat + delta_ele * delta_ele;
151-
double factor = ((r_lon - a_lon) * delta_lon + (r_lat - a_lat) * delta_lat + (r_ele - a_ele) * delta_ele) / norm;
136+
if (delta_lat == 0)
137+
// special case: horizontal edge
138+
return calcNormalizedDist(a_lat_deg, r_lon_deg, r_lat_deg, r_lon_deg);
152139

153-
// x,y,z is projection of r onto segment a-b
140+
if (delta_lon == 0)
141+
// special case: vertical edge
142+
return calcNormalizedDist(r_lat_deg, a_lon_deg, r_lat_deg, r_lon_deg);
143+
144+
double norm = delta_lon * delta_lon + delta_lat * delta_lat;
145+
double factor = ((r_lon - a_lon) * delta_lon + (r_lat - a_lat) * delta_lat) / norm;
146+
147+
// x,y is projection of r onto segment a-b
154148
double c_lon = a_lon + factor * delta_lon;
155149
double c_lat = a_lat + factor * delta_lat;
156-
double c_ele_m = (a_ele + factor * delta_ele) * METERS_PER_DEGREE;
157-
return calcNormalizedDist(c_lat, c_lon / shrinkFactor, r_lat_deg, r_lon_deg) + calcNormalizedDist(r_ele_m - c_ele_m);
150+
return calcNormalizedDist(c_lat, c_lon / shrinkFactor, r_lat_deg, r_lon_deg);
158151
}
159152

160-
/**
161-
* New edge distance calculation where no validEdgeDistance check would be necessary
162-
* <p>
163-
*
164-
* @return the normalized distance of the query point "r" to the project point "c" onto the line
165-
* segment a-b
166-
*/
167-
public double calcNormalizedEdgeDistanceNew(double r_lat_deg, double r_lon_deg,
168-
double a_lat_deg, double a_lon_deg,
169-
double b_lat_deg, double b_lon_deg,
170-
boolean reduceToSegment) {
153+
@Override
154+
public double calcNormalizedEdgeDistance3D(double r_lat_deg, double r_lon_deg, double r_ele_m,
155+
double a_lat_deg, double a_lon_deg, double a_ele_m,
156+
double b_lat_deg, double b_lon_deg, double b_ele_m) {
157+
if (Double.isNaN(r_ele_m) || Double.isNaN(a_ele_m) || Double.isNaN(b_ele_m))
158+
return calcNormalizedEdgeDistance(r_lat_deg, r_lon_deg, a_lat_deg, a_lon_deg, b_lat_deg, b_lon_deg);
159+
171160
double shrinkFactor = calcShrinkFactor(a_lat_deg, b_lat_deg);
172161

173162
double a_lat = a_lat_deg;
174163
double a_lon = a_lon_deg * shrinkFactor;
164+
double a_ele = a_ele_m / METERS_PER_DEGREE;
175165

176166
double b_lat = b_lat_deg;
177167
double b_lon = b_lon_deg * shrinkFactor;
168+
double b_ele = b_ele_m / METERS_PER_DEGREE;
178169

179170
double r_lat = r_lat_deg;
180171
double r_lon = r_lon_deg * shrinkFactor;
172+
double r_ele = r_ele_m / METERS_PER_DEGREE;
181173

182174
double delta_lon = b_lon - a_lon;
183175
double delta_lat = b_lat - a_lat;
176+
double delta_ele = b_ele - a_ele;
184177

185-
if (delta_lat == 0)
186-
// special case: horizontal edge
187-
return calcNormalizedDist(a_lat_deg, r_lon_deg, r_lat_deg, r_lon_deg);
188-
189-
if (delta_lon == 0)
190-
// special case: vertical edge
191-
return calcNormalizedDist(r_lat_deg, a_lon_deg, r_lat_deg, r_lon_deg);
192-
193-
double norm = delta_lon * delta_lon + delta_lat * delta_lat;
194-
double factor = ((r_lon - a_lon) * delta_lon + (r_lat - a_lat) * delta_lat) / norm;
178+
double norm = delta_lon * delta_lon + delta_lat * delta_lat + delta_ele * delta_ele;
179+
double factor = ((r_lon - a_lon) * delta_lon + (r_lat - a_lat) * delta_lat + (r_ele - a_ele) * delta_ele) / norm;
180+
if (Double.isNaN(factor)) factor = 0;
195181

196-
// make new calculation compatible to old
197-
if (reduceToSegment) {
198-
if (factor > 1)
199-
factor = 1;
200-
else if (factor < 0)
201-
factor = 0;
202-
}
203-
// x,y is projection of r onto segment a-b
182+
// x,y,z is projection of r onto segment a-b
204183
double c_lon = a_lon + factor * delta_lon;
205184
double c_lat = a_lat + factor * delta_lat;
206-
return calcNormalizedDist(c_lat, c_lon / shrinkFactor, r_lat_deg, r_lon_deg);
185+
double c_ele_m = (a_ele + factor * delta_ele) * METERS_PER_DEGREE;
186+
return calcNormalizedDist(c_lat, c_lon / shrinkFactor, r_lat_deg, r_lon_deg) + calcNormalizedDist(r_ele_m - c_ele_m);
207187
}
208188

209189
double calcShrinkFactor(double a_lat_deg, double b_lat_deg) {
@@ -300,6 +280,41 @@ public GHPoint projectCoordinate(double latInDeg, double lonInDeg, double distan
300280
return new GHPoint(projectedLat, projectedLon);
301281
}
302282

283+
@Override
284+
public GHPoint intermediatePoint(double f, double lat1, double lon1, double lat2, double lon2) {
285+
double lat1radians = Math.toRadians(lat1);
286+
double lon1radians = Math.toRadians(lon1);
287+
double lat2radians = Math.toRadians(lat2);
288+
double lon2radians = Math.toRadians(lon2);
289+
290+
// This formula is taken from: (http://www.movable-type.co.uk/scripts/latlong.html -> https://github.com/chrisveness/geodesy MIT)
291+
292+
double deltaLat = lat2radians - lat1radians;
293+
double deltaLon = lon2radians - lon1radians;
294+
double cosLat1 = cos(lat1radians);
295+
double cosLat2 = cos(lat2radians);
296+
double sinHalfDeltaLat = sin(deltaLat / 2);
297+
double sinHalfDeltaLon = sin(deltaLon / 2);
298+
299+
double a = sinHalfDeltaLat * sinHalfDeltaLat + cosLat1 * cosLat2 * sinHalfDeltaLon * sinHalfDeltaLon;
300+
double angularDistance = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
301+
double sinDistance = sin(angularDistance);
302+
303+
if (angularDistance == 0) return new GHPoint(lat1, lon1);
304+
305+
double A = Math.sin((1-f)*angularDistance) / sinDistance;
306+
double B = Math.sin(f*angularDistance) / sinDistance;
307+
308+
double x = A * cosLat1 * cos(lon1radians) + B * cosLat2 * cos(lon2radians);
309+
double y = A * cosLat1 * sin(lon1radians) + B * cosLat2 * sin(lon2radians);
310+
double z = A * sin(lat1radians) + B * sin(lat2radians);
311+
312+
double midLat = Math.toDegrees(Math.atan2(z, Math.sqrt(x*x + y*y)));
313+
double midLon = Math.toDegrees(Math.atan2(y, x));
314+
315+
return new GHPoint(midLat, midLon);
316+
}
317+
303318
@Override
304319
public boolean isCrossBoundary(double lon1, double lon2) {
305320
return abs(lon1 - lon2) > 300;

api/src/main/java/com/graphhopper/util/Parameters.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public static final class Routing {
110110
*/
111111
public static final String WAY_POINT_MAX_DISTANCE = "way_point_max_distance";
112112
public static final String INIT_WAY_POINT_MAX_DISTANCE = ROUTING_INIT_PREFIX + "way_point_max_distance";
113+
public static final String ELEVATION_WAY_POINT_MAX_DISTANCE = "elevation_way_point_max_distance";
113114
/**
114115
* true or false. If routes at via points should avoid u-turns. (not for CH) See related
115116
* 'heading' parameter:

api/src/test/java/com/graphhopper/util/DistanceCalcEarthTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,46 @@ public void testEdgeDistance3dPlane() {
142142
assertEquals(10, calc.calcDenormalizedDist(dist), 1e-4);
143143
}
144144

145+
@Test
146+
public void testEdgeDistanceStartEndSame() {
147+
DistanceCalc calc = new DistancePlaneProjection();
148+
// just change elevation
149+
double dist = calc.calcNormalizedEdgeDistance3D(0, 0, 10,
150+
0, 0, 0,
151+
0, 0, 0);
152+
assertEquals(10, calc.calcDenormalizedDist(dist), 1e-4);
153+
// just change lat
154+
dist = calc.calcNormalizedEdgeDistance3D(1, 0, 0,
155+
0, 0, 0,
156+
0, 0, 0);
157+
assertEquals(DistanceCalcEarth.METERS_PER_DEGREE, calc.calcDenormalizedDist(dist), 1e-4);
158+
// just change lon
159+
dist = calc.calcNormalizedEdgeDistance3D(0, 1, 0,
160+
0, 0, 0,
161+
0, 0, 0);
162+
assertEquals(DistanceCalcEarth.METERS_PER_DEGREE, calc.calcDenormalizedDist(dist), 1e-4);
163+
}
164+
165+
@Test
166+
public void testEdgeDistanceStartEndDifferentElevation() {
167+
DistanceCalc calc = new DistancePlaneProjection();
168+
// just change elevation
169+
double dist = calc.calcNormalizedEdgeDistance3D(0, 0, 10,
170+
0, 0, 0,
171+
0, 0, 1);
172+
assertEquals(0, calc.calcDenormalizedDist(dist), 1e-4);
173+
// just change lat
174+
dist = calc.calcNormalizedEdgeDistance3D(1, 0, 0,
175+
0, 0, 0,
176+
0, 0, 1);
177+
assertEquals(DistanceCalcEarth.METERS_PER_DEGREE, calc.calcDenormalizedDist(dist), 1e-4);
178+
// just change lon
179+
dist = calc.calcNormalizedEdgeDistance3D(0, 1, 0,
180+
0, 0, 0,
181+
0, 0, 1);
182+
assertEquals(DistanceCalcEarth.METERS_PER_DEGREE, calc.calcDenormalizedDist(dist), 1e-4);
183+
}
184+
145185
@Test
146186
public void testValidEdgeDistance() {
147187
assertTrue(dc.validEdgeDistance(49.94241, 11.544356, 49.937964, 11.541824, 49.942272, 11.555643));
@@ -257,4 +297,38 @@ public void testDistance3dPlaneNaN() {
257297
0, 0, Double.NaN
258298
), 1e-6);
259299
}
300+
301+
@Test
302+
public void testIntermediatePoint() {
303+
DistanceCalc distCalc = new DistanceCalcEarth();
304+
GHPoint point = distCalc.intermediatePoint(0, 0, 0, 0, 0);
305+
assertEquals(0, point.getLat(), 1e-5);
306+
assertEquals(0, point.getLon(), 1e-5);
307+
308+
point = distCalc.intermediatePoint(0.5, 0, 0, 10, 0);
309+
assertEquals(5, point.getLat(), 1e-5);
310+
assertEquals(0, point.getLon(), 1e-5);
311+
312+
point = distCalc.intermediatePoint(0.5, 0, 0, 0, 10);
313+
assertEquals(0, point.getLat(), 1e-5);
314+
assertEquals(5, point.getLon(), 1e-5);
315+
316+
// cross international date line going west
317+
point = distCalc.intermediatePoint(0.5, 45, -179, 45, 177);
318+
assertEquals(45, point.getLat(), 1);
319+
assertEquals(179, point.getLon(), 1e-5);
320+
321+
// cross international date line going east
322+
point = distCalc.intermediatePoint(0.5, 45, 179, 45, -177);
323+
assertEquals(45, point.getLat(), 1);
324+
assertEquals(-179, point.getLon(), 1e-5);
325+
326+
// cross north pole
327+
point = distCalc.intermediatePoint(0.25, 45, -90, 45, 90);
328+
assertEquals(67.5, point.getLat(), 1e-1);
329+
assertEquals(-90, point.getLon(), 1e-5);
330+
point = distCalc.intermediatePoint(0.75, 45, -90, 45, 90);
331+
assertEquals(67.5, point.getLat(), 1e-1);
332+
assertEquals(90, point.getLon(), 1e-5);
333+
}
260334
}

config-example.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,19 @@ graphhopper:
9292
# graph.elevation.dataaccess: RAM_STORE
9393

9494

95+
# To enable bilinear interpolation when sampling elevation at points (default uses nearest neighbor):
96+
# graph.elevation.interpolate: bilinear
97+
98+
99+
# To increase elevation profile resolution, use the following two parameters to tune the extra resolution you need
100+
# against the additional storage space used for edge geometries. You should enable bilinear interpolation when using
101+
# these features (see #1953 for details).
102+
# - first, set the distance (in meters) at which elevation samples should be taken on long edges
103+
# graph.elevation.long_edge_sampling_distance: 60
104+
# - second, set the elevation tolerance (in meters) to use when simplifying polylines since the default ignores
105+
# elevation and will remove the extra points that long edge sampling added
106+
# graph.elevation.way_point_max_distance: 10
107+
95108

96109
#### Speed, hybrid and flexible mode ####
97110

core/src/main/java/com/graphhopper/GraphHopper.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public class GraphHopper implements GraphHopperAPI {
8686
private boolean allowWrites = true;
8787
private boolean fullyLoaded = false;
8888
private boolean smoothElevation = false;
89+
private double longEdgeSamplingDistance = Double.MAX_VALUE;
8990
// for routing
9091
private final RouterConfig routerConfig = new RouterConfig();
9192
// for index
@@ -328,6 +329,22 @@ public GraphHopper setElevation(boolean includeElevation) {
328329
return this;
329330
}
330331

332+
/**
333+
* Sets the distance distance between elevation samples on long edges
334+
*/
335+
public GraphHopper setLongEdgeSamplingDistance(double longEdgeSamplingDistance) {
336+
this.longEdgeSamplingDistance = longEdgeSamplingDistance;
337+
return this;
338+
}
339+
340+
/**
341+
* Sets the max elevation discrepancy between way points and the simplified polyline in meters
342+
*/
343+
public GraphHopper setElevationWayPointMaxDistance(double elevationWayPointMaxDistance) {
344+
this.routerConfig.setElevationWayPointMaxDistance(elevationWayPointMaxDistance);
345+
return this;
346+
}
347+
331348
public String getGraphHopperLocation() {
332349
return ghLocation;
333350
}
@@ -485,9 +502,14 @@ public GraphHopper init(GraphHopperConfig ghConfig) {
485502

486503
// elevation
487504
this.smoothElevation = ghConfig.getBool("graph.elevation.smoothing", false);
505+
this.longEdgeSamplingDistance = ghConfig.getDouble("graph.elevation.long_edge_sampling_distance", Double.MAX_VALUE);
506+
setElevationWayPointMaxDistance(ghConfig.getDouble("graph.elevation.way_point_max_distance", Double.MAX_VALUE));
488507
ElevationProvider elevationProvider = createElevationProvider(ghConfig);
489508
setElevationProvider(elevationProvider);
490509

510+
if (longEdgeSamplingDistance < Double.MAX_VALUE && !elevationProvider.getInterpolate())
511+
logger.warn("Long edge sampling enabled, but bilinear interpolation disabled. See #1953");
512+
491513
// optimizable prepare
492514
minNetworkSize = ghConfig.getInt("prepare.min_network_size", minNetworkSize);
493515

@@ -687,7 +709,9 @@ protected DataReader initDataReader(DataReader reader) {
687709
setElevationProvider(eleProvider).
688710
setWorkerThreads(dataReaderWorkerThreads).
689711
setWayPointMaxDistance(dataReaderWayPointMaxDistance).
690-
setSmoothElevation(this.smoothElevation);
712+
setWayPointElevationMaxDistance(routerConfig.getElevationWayPointMaxDistance()).
713+
setSmoothElevation(smoothElevation).
714+
setLongEdgeSamplingDistance(longEdgeSamplingDistance);
691715
}
692716

693717
/**

core/src/main/java/com/graphhopper/reader/DataReader.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,12 @@ public interface DataReader {
3636

3737
DataReader setWayPointMaxDistance(double wayPointMaxDistance);
3838

39+
DataReader setWayPointElevationMaxDistance(double elevationWayPointMaxDistance);
40+
3941
DataReader setSmoothElevation(boolean smoothElevation);
4042

43+
DataReader setLongEdgeSamplingDistance(double longEdgeSamplingDistance);
44+
4145
/**
4246
* This method triggers reading the underlying data to create a graph
4347
*/

core/src/main/java/com/graphhopper/reader/dem/AbstractElevationProvider.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ public void setInterpolate(boolean interpolate) {
5959
this.interpolate = interpolate;
6060
}
6161

62+
@Override
63+
public boolean getInterpolate() {
64+
return this.interpolate;
65+
}
66+
6267
void setSleep(long sleep) {
6368
this.sleep = sleep;
6469
}

0 commit comments

Comments
 (0)