diff --git a/README.md b/README.md index 79e5e04..294abeb 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,17 @@ type Point [2]float64 Point represents a latitude-longitude pair in decimal degrees +Constants + +```go +const ( + LatLowerBound = float64(-90) + LatUpperBound = float64(90) + LonLowerBound = float64(-180) + LonUpperBound = float64(180) +) +``` + #### func (Point) Antipode ```go @@ -106,8 +117,10 @@ the returned distance will be math.NaN(). func VincentyInverse(p1, p2 geodesy.Point, accuracy float64, calculateAzimuth bool) (float64, float64, float64) ``` -VincentyInverse calculates the ellipsoidal distance in meters and azimuth in degrees between 2 points using the inverse Vincenty formulae and the WGS-84 ellipsoid constants. As it is an iterative operation it will converge to the defined accuracy, if accuracy < 0 it will use the default accuracy of 1e-12 (approximately 0.06 mm). If -calculateAzimuth is set to true, it will compute the forward and reverse azimuths (otherwise, these default to math.NaN()) +VincentyInverse calculates the ellipsoidal distance in meters and azimuth in degrees between 2 points using the +inverse Vincenty formulae and the WGS-84 ellipsoid constants. As it is an iterative operation it will converge to +the defined accuracy, if accuracy < 0 it will use the default accuracy of 1e-12 (approximately 0.06 mm, magnitude should be no bigger than 1e-6). +If calculateAzimuth is set to true, it will compute the forward and reverse azimuths (otherwise, these default to math.NaN()). If any of the points does not constitute a valid geographic coordinate, the returned distance will be math.NaN(). The following notations are used in the implementation: diff --git a/distance/haversine_test.go b/distance/haversine_test.go index 5f4096b..1b8a517 100644 --- a/distance/haversine_test.go +++ b/distance/haversine_test.go @@ -1,6 +1,7 @@ package distance_test import ( + "math" "testing" "github.com/lggomez/go-geodesy" @@ -118,11 +119,31 @@ func TestHaversine(t *testing.T) { }, expectedDistance: 2.0015114352233686e+07, }, + { + name: "FAIL/invalid_p1", + args: args{ + p1: geodesy.Point{geodesy.LatUpperBound+1, -57.534954}, + p2: geodesy.Point{-34.579340, -57.534954}, + }, + expectedDistance: math.NaN(), + }, + { + name: "FAIL/invalid_p2", + args: args{ + p1: geodesy.Point{-34.579340, -57.534954}, + p2: geodesy.Point{geodesy.LatUpperBound+1, -57.534954}, + }, + expectedDistance: math.NaN(), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { d := distance.Haversine(tt.args.p1, tt.args.p2) - assert.Equal(t, tt.expectedDistance, d) + if math.IsNaN(tt.expectedDistance) { + assert.True(t, math.IsNaN(d), "got %f", d) + } else { + assert.EqualValues(t, tt.expectedDistance, d) + } }) } } diff --git a/distance/vincenty.go b/distance/vincenty.go index 8fe4c45..335a7c4 100644 --- a/distance/vincenty.go +++ b/distance/vincenty.go @@ -17,8 +17,8 @@ const ( /* VincentyInverse calculates the ellipsoidal distance in meters and azimuth in degrees between 2 points using the inverse Vincenty formulae and the WGS-84 ellipsoid constants. As it is an iterative operation it will converge to -the defined accuracy, if accuracy < 0 it will use the default accuracy of 1e-12 (approximately 0.06 mm). If -calculateAzimuth is set to true, it will compute the forward and reverse azimuths (otherwise, these default to math.NaN()). +the defined accuracy, if accuracy < 0 it will use the default accuracy of 1e-12 (approximately 0.06 mm, magnitude should be no bigger than 1e-6). +If calculateAzimuth is set to true, it will compute the forward and reverse azimuths (otherwise, these default to math.NaN()). If any of the points does not constitute a valid geographic coordinate, the returned distance will be math.NaN(). The following notations are used: diff --git a/distance/vincenty_test.go b/distance/vincenty_test.go index b5a0b94..2ebba3e 100644 --- a/distance/vincenty_test.go +++ b/distance/vincenty_test.go @@ -22,6 +22,12 @@ func TestVincentyInverse(t *testing.T) { antipodeOriginSE := geodesy.Point{-46.272337, 169.398118} antipodeSE := antipodeOriginSE.Antipode() + // Antipodes will fail to converge on the exact points + // and within a short radius, so induce a non-convergent + // point pair + antipodeNonconvOrigin := geodesy.Point{42.35831235, -95.31046631} + antipodeNonconv := geodesy.Point{antipodeNonconvOrigin.Antipode().Lat() + 0.00000001, antipodeNonconvOrigin.Antipode().Lon() + 0.00000001} + type args struct { p1 geodesy.Point p2 geodesy.Point @@ -71,6 +77,18 @@ func TestVincentyInverse(t *testing.T) { expectedAzimuth1: math.NaN(), expectedAzimuth2: math.NaN(), }, + { + name: "OK/SW_sub_1k_km_less_accurate", + args: args{ + p1: geodesy.Point{-37.550643, -56.51251}, + p2: geodesy.Point{-37.5507, -56.5126}, + accuracy: 1e-6, + calculateAzimuth: false, + }, + expectedDistance: 10.149099956337418, + expectedAzimuth1: math.NaN(), + expectedAzimuth2: math.NaN(), + }, { name: "OK/SW/SW_sub_500km", args: args{ @@ -179,6 +197,18 @@ func TestVincentyInverse(t *testing.T) { expectedAzimuth1: 28.09660240174846, expectedAzimuth2: 208.17248801650823, }, + { + name: "FAIL/divergent", + args: args{ + p1: antipodeNonconvOrigin, + p2: antipodeNonconv, + accuracy: -1, + calculateAzimuth: true, + }, + expectedDistance: math.NaN(), + expectedAzimuth1: math.NaN(), + expectedAzimuth2: math.NaN(), + }, { name: "FAIL/antipode", args: args{ @@ -191,23 +221,47 @@ func TestVincentyInverse(t *testing.T) { expectedAzimuth1: math.NaN(), expectedAzimuth2: math.NaN(), }, + { + name: "FAIL/invalid_p1", + args: args{ + p1: geodesy.Point{geodesy.LatUpperBound+1, -57.534954}, + p2: geodesy.Point{-34.579340, -57.534954}, + accuracy: -1, + calculateAzimuth: true, + }, + expectedDistance: math.NaN(), + expectedAzimuth1: math.NaN(), + expectedAzimuth2: math.NaN(), + }, + { + name: "FAIL/invalid_p2", + args: args{ + p1: geodesy.Point{-34.579340, -57.534954}, + p2: geodesy.Point{geodesy.LatUpperBound+1, -57.534954}, + accuracy: -1, + calculateAzimuth: true, + }, + expectedDistance: math.NaN(), + expectedAzimuth1: math.NaN(), + expectedAzimuth2: math.NaN(), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { d, az1, az2 := distance.VincentyInverse(tt.args.p1, tt.args.p2, tt.args.accuracy, tt.args.calculateAzimuth) if math.IsNaN(tt.expectedDistance) { - assert.True(t, math.IsNaN(d)) + assert.True(t, math.IsNaN(d), "got %f", d) } else { assert.EqualValues(t, tt.expectedDistance, d) } if math.IsNaN(tt.expectedAzimuth1) { - assert.True(t, math.IsNaN(az1)) + assert.True(t, math.IsNaN(az1), "got %f", az1) } else { assert.EqualValues(t, tt.expectedAzimuth1, az1) } if math.IsNaN(tt.expectedAzimuth2) { - assert.True(t, math.IsNaN(az2)) + assert.True(t, math.IsNaN(az2), "got %f", az2) } else { assert.EqualValues(t, tt.expectedAzimuth2, az2) } diff --git a/point.go b/point.go index e462d55..ac5c4a9 100644 --- a/point.go +++ b/point.go @@ -3,10 +3,10 @@ package geodesy import "math" const ( - latLowerBound = float64(-90) - latUpperBound = float64(90) - lonLowerBound = float64(-180) - lonUpperBound = float64(180) + LatLowerBound = float64(-90) + LatUpperBound = float64(90) + LonLowerBound = float64(-180) + LonUpperBound = float64(180) ) // Point represents a latitude-longitude pair in decimal degrees @@ -19,7 +19,7 @@ func (p Point) Lat() float64 { // LatRadians returns point p's latitude in radians func (p Point) LatRadians() float64 { - return (p[0]*math.Pi)/180 + return (p[0] * math.Pi) / 180 } // Lon returns point p's longitude @@ -29,7 +29,7 @@ func (p Point) Lon() float64 { // LonRadians returns point p's longitude in radians func (p Point) LonRadians() float64 { - return (p[1]*math.Pi)/180 + return (p[1] * math.Pi) / 180 } // Antipode returns a new point representing the geographical antipode of p @@ -41,7 +41,7 @@ func (p Point) Antipode() Point { func (p Point) IsAntipodeOf(p2 Point) bool { // Shorthand check to avoid Equals() calls between p and p2 return ((p[0] == -p2[0]) && (p[1] == (180 - math.Abs(p2[1])))) || - (p2[0] == -p[0]) && (p2[1] == (180 - math.Abs(p[1]))) + (p2[0] == -p[0]) && (p2[1] == (180-math.Abs(p[1]))) } // Equals returns whether p is equal in latitude and longitude to p2 @@ -52,6 +52,6 @@ func (p Point) Equals(p2 Point) bool { // Valid returns whether p is valid, that is, contained within the valid range of // geographic coordinates func (p Point) Valid() bool { - return ((p[0] >= latLowerBound) && (p[0] <= latUpperBound)) && - ((p[1] >= lonLowerBound) && (p[1] <= lonUpperBound)) + return ((p[0] >= LatLowerBound) && (p[0] <= LatUpperBound)) && + ((p[1] >= LonLowerBound) && (p[1] <= LonUpperBound)) }