Skip to content

Commit 4224026

Browse files
authored
[H3] Compute destination point from distance and azimuth using planar 3d math (#93084)
Replace the method geoAzDistanceRads from using trigonometric maths to use planar 3d math
1 parent 573a0a9 commit 4224026

File tree

6 files changed

+126
-117
lines changed

6 files changed

+126
-117
lines changed

docs/changelog/93084.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 93084
2+
summary: "[H3] Compute destination point from distance and azimuth using planar 3d\
3+
\ math"
4+
area: Geo
5+
type: enhancement
6+
issues: []

libs/h3/src/main/java/org/elasticsearch/h3/Constants.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ final class Constants {
3030
* sqrt(3) / 2.0
3131
*/
3232
public static double M_SQRT3_2 = 0.8660254037844386467637231707529361834714;
33+
/**
34+
* 2.0 * PI
35+
*/
36+
public static final double M_2PI = 6.28318530717958647692528676655900576839433;
3337
/**
3438
* max H3 resolution; H3 version 1 has 16 resolutions, numbered 0 through 15
3539
*/

libs/h3/src/main/java/org/elasticsearch/h3/LatLng.java

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@
2828
public final class LatLng {
2929

3030
/** Minimum Angular resolution. */
31-
public static final double MINIMUM_ANGULAR_RESOLUTION = Math.PI * 1.0e-12; // taken from lucene's spatial3d
31+
private static final double MINIMUM_ANGULAR_RESOLUTION = Math.PI * 1.0e-12; // taken from lucene's spatial3d
32+
33+
/**
34+
* pi / 2.0
35+
*/
36+
private static final double M_PI_2 = 1.5707963267948966;
3237

3338
// lat / lon in radians
3439
private final double lon;
@@ -67,13 +72,59 @@ public double getLonDeg() {
6772
* @return The azimuth in radians.
6873
*/
6974
double geoAzimuthRads(double lat, double lon) {
75+
// algorithm from the original H3 library
7076
final double cosLat = FastMath.cos(lat);
7177
return FastMath.atan2(
7278
cosLat * FastMath.sin(lon - this.lon),
7379
FastMath.cos(this.lat) * FastMath.sin(lat) - FastMath.sin(this.lat) * cosLat * FastMath.cos(lon - this.lon)
7480
);
7581
}
7682

83+
/**
84+
* Computes the point on the sphere with a specified azimuth and distance from
85+
* this point.
86+
*
87+
* @param az The desired azimuth.
88+
* @param distance The desired distance.
89+
* @return The LatLng point.
90+
*/
91+
LatLng geoAzDistanceRads(double az, double distance) {
92+
// algorithm from the original H3 library
93+
az = Vec2d.posAngleRads(az);
94+
final double sinDistance = FastMath.sin(distance);
95+
final double cosDistance = FastMath.cos(distance);
96+
final double sinP1Lat = FastMath.sin(getLatRad());
97+
final double cosP1Lat = FastMath.cos(getLatRad());
98+
final double sinlat = Math.max(-1.0, Math.min(1.0, sinP1Lat * cosDistance + cosP1Lat * sinDistance * FastMath.cos(az)));
99+
final double lat = FastMath.asin(sinlat);
100+
if (Math.abs(lat - M_PI_2) < Constants.EPSILON) { // north pole
101+
return new LatLng(M_PI_2, 0.0);
102+
} else if (Math.abs(lat + M_PI_2) < Constants.EPSILON) { // south pole
103+
return new LatLng(-M_PI_2, 0.0);
104+
} else {
105+
final double cosLat = FastMath.cos(lat);
106+
final double sinlng = Math.max(-1.0, Math.min(1.0, FastMath.sin(az) * sinDistance / cosLat));
107+
final double coslng = Math.max(-1.0, Math.min(1.0, (cosDistance - sinP1Lat * FastMath.sin(lat)) / cosP1Lat / cosLat));
108+
return new LatLng(lat, constrainLng(getLonRad() + FastMath.atan2(sinlng, coslng)));
109+
}
110+
}
111+
112+
/**
113+
* constrainLng makes sure longitudes are in the proper bounds
114+
*
115+
* @param lng The origin lng value
116+
* @return The corrected lng value
117+
*/
118+
private static double constrainLng(double lng) {
119+
while (lng > Math.PI) {
120+
lng = lng - Constants.M_2PI;
121+
}
122+
while (lng < -Math.PI) {
123+
lng = lng + Constants.M_2PI;
124+
}
125+
return lng;
126+
}
127+
77128
/**
78129
* Determines the maximum latitude of the great circle defined by this LatLng to the provided LatLng.
79130
*
@@ -92,7 +143,7 @@ private static double greatCircleMaxLatitude(LatLng latLng1, LatLng latLng2) {
92143
assert latLng1.lat >= latLng2.lat;
93144
final double az = latLng1.geoAzimuthRads(latLng2.lat, latLng2.lon);
94145
// the great circle contains the maximum latitude only if the azimuth is between -90 and 90 degrees.
95-
if (Math.abs(az) < Math.PI / 2) {
146+
if (Math.abs(az) < M_PI_2) {
96147
return FastMath.acos(Math.abs(FastMath.sin(az) * FastMath.cos(latLng1.lat)));
97148
}
98149
return latLng1.lat;
@@ -116,14 +167,14 @@ private static double greatCircleMinLatitude(LatLng latLng1, LatLng latLng2) {
116167
// we compute the min latitude using Clairaut's formula (https://streckenflug.at/download/formeln.pdf)
117168
final double az = latLng1.geoAzimuthRads(latLng2.lat, latLng2.lon);
118169
// the great circle contains the minimum latitude only if the azimuth is not between -90 and 90 degrees.
119-
if (Math.abs(az) > Math.PI / 2) {
170+
if (Math.abs(az) > M_PI_2) {
120171
// note the sign
121172
return -FastMath.acos(Math.abs(FastMath.sin(az) * FastMath.cos(latLng1.lat)));
122173
}
123174
return latLng1.lat;
124175
}
125176

126-
private boolean isNumericallyIdentical(LatLng latLng) {
177+
boolean isNumericallyIdentical(LatLng latLng) {
127178
return Math.abs(this.lat - latLng.lat) < MINIMUM_ANGULAR_RESOLUTION && Math.abs(this.lon - latLng.lon) < MINIMUM_ANGULAR_RESOLUTION;
128179
}
129180

libs/h3/src/main/java/org/elasticsearch/h3/Vec2d.java

Lines changed: 4 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,6 @@ final class Vec2d {
8787
{ 2.361378999196363184, 0.266983896803167583, 4.455774101589558636 }, // face 19
8888
};
8989

90-
/**
91-
* pi
92-
*/
93-
private static final double M_PI = 3.14159265358979323846;
94-
/**
95-
* pi / 2.0
96-
*/
97-
private static final double M_PI_2 = 1.5707963267948966;
98-
/**
99-
* 2.0 * PI
100-
*/
101-
public static final double M_2PI = 6.28318530717958647692528676655900576839433;
102-
10390
private final double x; /// < x component
10491
private final double y; /// < y component
10592

@@ -155,7 +142,7 @@ public LatLng hex2dToGeo(int face, int res, boolean substrate) {
155142
// find theta as an azimuth
156143
theta = posAngleRads(faceAxesAzRadsCII[face][0] - theta);
157144
// now find the point at (r,theta) from the face center
158-
return geoAzDistanceRads(faceCenterGeo[face], theta, r);
145+
return Vec3d.faceCenterPoint[face].geoAzDistanceRads(theta, r);
159146
}
160147

161148
/**
@@ -313,106 +300,11 @@ private double v2dMag() {
313300
*/
314301
static double posAngleRads(double rads) {
315302
if (rads < 0.0) {
316-
return rads + M_2PI;
317-
} else if (rads >= M_2PI) {
318-
return rads - M_2PI;
303+
return rads + Constants.M_2PI;
304+
} else if (rads >= Constants.M_2PI) {
305+
return rads - Constants.M_2PI;
319306
} else {
320307
return rads;
321308
}
322309
}
323-
324-
/**
325-
* Computes the point on the sphere a specified azimuth and distance from
326-
* another point.
327-
*
328-
* @param p1 The first spherical coordinates.
329-
* @param az The desired azimuth from p1.
330-
* @param distance The desired distance from p1, must be non-negative.
331-
* p1.
332-
*/
333-
private static LatLng geoAzDistanceRads(LatLng p1, double az, double distance) {
334-
if (distance < Constants.EPSILON) {
335-
return p1;
336-
}
337-
338-
double sinlat, sinlng, coslng;
339-
340-
az = posAngleRads(az);
341-
342-
double lat, lon;
343-
344-
// check for due north/south azimuth
345-
if (az < Constants.EPSILON || Math.abs(az - M_PI) < Constants.EPSILON) {
346-
if (az < Constants.EPSILON) {// due north
347-
lat = p1.getLatRad() + distance;
348-
} else { // due south
349-
lat = p1.getLatRad() - distance;
350-
}
351-
if (Math.abs(lat - M_PI_2) < Constants.EPSILON) { // north pole
352-
lat = M_PI_2;
353-
lon = 0.0;
354-
} else if (Math.abs(lat + M_PI_2) < Constants.EPSILON) { // south pole
355-
lat = -M_PI_2;
356-
lon = 0.0;
357-
} else {
358-
lon = constrainLng(p1.getLonRad());
359-
}
360-
} else { // not due north or south
361-
final double sinDistance = FastMath.sin(distance);
362-
final double cosDistance = FastMath.cos(distance);
363-
final double sinP1Lat = FastMath.sin(p1.getLatRad());
364-
final double cosP1Lat = FastMath.cos(p1.getLatRad());
365-
sinlat = sinP1Lat * cosDistance + cosP1Lat * sinDistance * FastMath.cos(az);
366-
if (sinlat > 1.0) {
367-
sinlat = 1.0;
368-
}
369-
if (sinlat < -1.0) {
370-
sinlat = -1.0;
371-
}
372-
lat = FastMath.asin(sinlat);
373-
if (Math.abs(lat - M_PI_2) < Constants.EPSILON) // north pole
374-
{
375-
lat = M_PI_2;
376-
lon = 0.0;
377-
} else if (Math.abs(lat + M_PI_2) < Constants.EPSILON) // south pole
378-
{
379-
lat = -M_PI_2;
380-
lon = 0.0;
381-
} else {
382-
final double cosLat = FastMath.cos(lat);
383-
sinlng = FastMath.sin(az) * sinDistance / cosLat;
384-
coslng = (cosDistance - sinP1Lat * FastMath.sin(lat)) / cosP1Lat / cosLat;
385-
if (sinlng > 1.0) {
386-
sinlng = 1.0;
387-
}
388-
if (sinlng < -1.0) {
389-
sinlng = -1.0;
390-
}
391-
if (coslng > 1.0) {
392-
coslng = 1.0;
393-
}
394-
if (coslng < -1.0) {
395-
coslng = -1.0;
396-
}
397-
lon = constrainLng(p1.getLonRad() + FastMath.atan2(sinlng, coslng));
398-
}
399-
}
400-
return new LatLng(lat, lon);
401-
}
402-
403-
/**
404-
* constrainLng makes sure longitudes are in the proper bounds
405-
*
406-
* @param lng The origin lng value
407-
* @return The corrected lng value
408-
*/
409-
private static double constrainLng(double lng) {
410-
while (lng > M_PI) {
411-
lng = lng - (2 * M_PI);
412-
}
413-
while (lng < -M_PI) {
414-
lng = lng + (2 * M_PI);
415-
}
416-
return lng;
417-
}
418310
}

libs/h3/src/main/java/org/elasticsearch/h3/Vec3d.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,51 @@ private static double square(double x) {
167167
return FastMath.atan2(sign * magnitude(c1c2X, c1c2Y, c1c2Z), dotProduct(c1X, c1Y, c1Z, c2X, c2Y, c2Z));
168168
}
169169

170+
/**
171+
* Computes the point on the sphere with a specified azimuth and distance from
172+
* this point.
173+
*
174+
* @param az The desired azimuth.
175+
* @param distance The desired distance.
176+
* @return The LatLng point.
177+
*/
178+
LatLng geoAzDistanceRads(double az, double distance) {
179+
az = Vec2d.posAngleRads(az);
180+
// from https://www.movable-type.co.uk/scripts/latlong-vectors.html
181+
// N = {0,0,1} – vector representing north pole
182+
// d̂e = N×a – east vector at a
183+
// dn = a×de – north vector at a
184+
// d = dn·cosθ + de·sinθ – direction vector in dir’n of θ
185+
// b = a·cosδ + d·sinδ
186+
187+
// east direction vector @ n1 (Gade's k_e_E)
188+
final double magnitude = magnitude(this.x, this.y, 0);
189+
final double deX = -this.y / magnitude;
190+
final double deY = this.x / magnitude;
191+
192+
// north direction vector @ n1 (Gade's (k_n_E)
193+
final double dnX = -this.z * deY;
194+
final double dnY = this.z * deX;
195+
final double dnZ = this.x * deY - this.y * deX;
196+
197+
final double sinAz = FastMath.sin(az);
198+
final double cosAz = FastMath.cos(az);
199+
final double sinDistance = FastMath.sin(distance);
200+
final double cosDistance = FastMath.cos(distance);
201+
202+
// direction vector @ n1 (≡ C×n1; C = great circle)
203+
final double dX = dnX * cosAz + deX * sinAz;
204+
final double dY = dnY * cosAz + deY * sinAz;
205+
final double dZ = dnZ * cosAz;
206+
207+
// Gade's n_EB_E = component of n2 parallel to n1 + component of n2 perpendicular to n1
208+
final double n2X = this.x * cosDistance + dX * sinDistance;
209+
final double n2Y = this.y * cosDistance + dY * sinDistance;
210+
final double n2Z = this.z * cosDistance + dZ * sinDistance;
211+
212+
return new LatLng(FastMath.asin(n2Z), FastMath.atan2(n2Y, n2X));
213+
}
214+
170215
/**
171216
* Calculate the dot product between two 3D coordinates.
172217
*

libs/h3/src/test/java/org/elasticsearch/h3/LatLngTests.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,23 @@
2828

2929
public class LatLngTests extends ESTestCase {
3030

31-
public void testLatLonVec3d() {
31+
public void testLatLonAzimuthRads() {
3232
final GeoPoint point = safePoint();
3333
for (int i = 0; i < Vec3d.faceCenterPoint.length; i++) {
3434
final double azVec3d = Vec3d.faceCenterPoint[i].geoAzimuthRads(point.x, point.y, point.z);
3535
final double azVec2d = Vec2d.faceCenterGeo[i].geoAzimuthRads(point.getLatitude(), point.getLongitude());
3636
assertEquals("Face " + i, azVec2d, azVec3d, 1e-12);
37+
38+
}
39+
}
40+
41+
public void testLatLonAzDistanceRads() {
42+
final double az = randomDoubleBetween(-2 * Math.PI, 2 * Math.PI, true);
43+
final double distance = randomDoubleBetween(-Math.PI, Math.PI / 2, true);
44+
for (int i = 0; i < Vec3d.faceCenterPoint.length; i++) {
45+
final LatLng latLng3d = Vec3d.faceCenterPoint[i].geoAzDistanceRads(az, distance);
46+
final LatLng latLng2d = Vec2d.faceCenterGeo[i].geoAzDistanceRads(az, distance);
47+
assertTrue("Face " + i, latLng2d.isNumericallyIdentical(latLng3d));
3748
}
3849
}
3950

0 commit comments

Comments
 (0)