@@ -32,9 +32,6 @@ import kotlin.math.sin
32
32
import kotlin.math.sqrt
33
33
import kotlin.math.tan
34
34
35
- private fun Double.toRadians () = this * (PI / 180.0 )
36
- private fun Double.toDegrees () = this * (180.0 / PI )
37
-
38
35
object SphericalUtil {
39
36
/* *
40
37
* Returns the heading from one LatLng to another LatLng. Headings are
@@ -51,7 +48,8 @@ object SphericalUtil {
51
48
52
49
// Breaking the formula down into Y and X components for atan2().
53
50
val y = sin(deltaLngRad) * cos(toLatRad)
54
- val x = cos(fromLatRad) * sin(toLatRad) - sin(fromLatRad) * cos(toLatRad) * cos(deltaLngRad)
51
+ val x = cos(fromLatRad) * sin(toLatRad) -
52
+ sin(fromLatRad) * cos(toLatRad) * cos(deltaLngRad)
55
53
56
54
val headingRad = atan2(y, x)
57
55
@@ -68,23 +66,26 @@ object SphericalUtil {
68
66
*/
69
67
@JvmStatic
70
68
fun computeOffset (from : LatLng , distance : Double , heading : Double ): LatLng {
71
- var distance = distance
72
- var heading = heading
73
- distance / = EARTH_RADIUS
74
- heading = Math .toRadians(heading)
75
- // http://williams.best.vwh.net/avform.htm#LL
76
- val fromLat = Math .toRadians(from.latitude)
77
- val fromLng = Math .toRadians(from.longitude)
78
- val cosDistance = cos(distance)
79
- val sinDistance = sin(distance)
80
- val sinFromLat = sin(fromLat)
81
- val cosFromLat = cos(fromLat)
82
- val sinLat = cosDistance * sinFromLat + sinDistance * cosFromLat * cos(heading)
83
- val dLng = atan2(
84
- sinDistance * cosFromLat * sin(heading),
85
- cosDistance - sinFromLat * sinLat
86
- )
87
- return LatLng (Math .toDegrees(asin(sinLat)), Math .toDegrees(fromLng + dLng))
69
+ val distanceRad = distance / EARTH_RADIUS
70
+ val headingRad = heading.toRadians()
71
+
72
+ val (fromLatRad, fromLngRad) = from.toRadians()
73
+
74
+ val cosDistance = cos(distanceRad)
75
+ val sinDistance = sin(distanceRad)
76
+ val sinFromLat = sin(fromLatRad)
77
+ val cosFromLat = cos(fromLatRad)
78
+
79
+ val sinToLat = cosDistance * sinFromLat + sinDistance * cosFromLat * cos(headingRad)
80
+ val toLatRad = asin(sinToLat)
81
+
82
+ val y = sin(headingRad) * sinDistance * cosFromLat
83
+ val x = cosDistance - sinFromLat * sinToLat
84
+ val dLngRad = atan2(y, x)
85
+
86
+ val toLngRad = fromLngRad + dLngRad
87
+
88
+ return LatLng (toLatRad.toDegrees(), toLngRad.toDegrees())
88
89
}
89
90
90
91
/* *
@@ -99,15 +100,13 @@ object SphericalUtil {
99
100
*/
100
101
@JvmStatic
101
102
fun computeOffsetOrigin (to : LatLng , distance : Double , heading : Double ): LatLng ? {
102
- var distance = distance
103
- var heading = heading
104
- heading = Math .toRadians(heading)
105
- distance / = EARTH_RADIUS
103
+ val headingRad = heading.toRadians()
104
+ val distanceRad = distance / EARTH_RADIUS
106
105
// http://lists.maptools.org/pipermail/proj/2008-October/003939.html
107
- val n1 = cos(distance )
108
- val n2 = sin(distance ) * cos(heading )
109
- val n3 = sin(distance ) * sin(heading )
110
- val n4 = sin(Math .toRadians( to.latitude))
106
+ val n1 = cos(distanceRad )
107
+ val n2 = sin(distanceRad ) * cos(headingRad )
108
+ val n3 = sin(distanceRad ) * sin(headingRad )
109
+ val n4 = sin(to.latitude.toRadians( ))
111
110
// There are two solutions for b. b = n2 * n4 +/- sqrt(), one solution results
112
111
// in the latitude outside the [-90, 90] range. We first try one solution and
113
112
// back off to the other if we are outside that range.
@@ -130,9 +129,9 @@ object SphericalUtil {
130
129
// No solution which would make sense in LatLng-space.
131
130
return null
132
131
}
133
- val fromLngRadians = Math .toRadians( to.longitude) -
132
+ val fromLngRadians = to.longitude.toRadians( ) -
134
133
atan2(n3, n1 * cos(fromLatRadians) - n2 * sin(fromLatRadians))
135
- return LatLng (Math .toDegrees(fromLatRadians ), Math .toDegrees(fromLngRadians ))
134
+ return LatLng (fromLatRadians .toDegrees(), fromLngRadians .toDegrees())
136
135
}
137
136
138
137
/* *
@@ -147,17 +146,17 @@ object SphericalUtil {
147
146
@JvmStatic
148
147
fun interpolate (from : LatLng , to : LatLng , fraction : Double ): LatLng {
149
148
// http://en.wikipedia.org/wiki/Slerp
150
- val fromLat = Math .toRadians(from.latitude)
151
- val fromLng = Math .toRadians(from.longitude)
152
- val toLat = Math .toRadians(to.latitude)
153
- val toLng = Math .toRadians(to.longitude)
154
- val cosFromLat = cos(fromLat)
155
- val cosToLat = cos(toLat)
149
+ val (fromLatRad, fromLngRad) = from.toRadians()
150
+ val (toLatRad, toLngRad) = to.toRadians()
151
+
152
+ val cosFromLat = cos(fromLatRad)
153
+ val cosToLat = cos(toLatRad)
156
154
157
155
// Computes Spherical interpolation coefficients.
158
156
val angle = computeAngleBetween(from, to)
159
157
val sinAngle = sin(angle)
160
158
if (sinAngle < 1E- 6 ) {
159
+ // Fall back to linear interpolation for very small angles.
161
160
return LatLng (
162
161
from.latitude + fraction * (to.latitude - from.latitude),
163
162
from.longitude + fraction * (to.longitude - from.longitude)
@@ -167,14 +166,15 @@ object SphericalUtil {
167
166
val b = sin(fraction * angle) / sinAngle
168
167
169
168
// Converts from polar to vector and interpolate.
170
- val x = a * cosFromLat * cos(fromLng ) + b * cosToLat * cos(toLng )
171
- val y = a * cosFromLat * sin(fromLng ) + b * cosToLat * sin(toLng )
172
- val z = a * sin(fromLat ) + b * sin(toLat )
169
+ val x = a * cosFromLat * cos(fromLngRad ) + b * cosToLat * cos(toLngRad )
170
+ val y = a * cosFromLat * sin(fromLngRad ) + b * cosToLat * sin(toLngRad )
171
+ val z = a * sin(fromLatRad ) + b * sin(toLatRad )
173
172
174
173
// Converts interpolated vector back to polar.
175
- val lat = atan2(z, sqrt(x * x + y * y))
176
- val lng = atan2(y, x)
177
- return LatLng (Math .toDegrees(lat), Math .toDegrees(lng))
174
+ val latRad = atan2(z, sqrt(x * x + y * y))
175
+ val lngRad = atan2(y, x)
176
+
177
+ return LatLng (latRad.toDegrees(), lngRad.toDegrees())
178
178
}
179
179
180
180
/* *
@@ -184,7 +184,7 @@ object SphericalUtil {
184
184
arcHav(havDistance(lat1, lat2, lng1 - lng2))
185
185
186
186
/* *
187
- * Returns the angle between two LatLngs , in radians. This is the same as the distance
187
+ * Returns the angle between two [LatLng]s , in radians. This is the same as the distance
188
188
* on the unit sphere.
189
189
*/
190
190
@JvmStatic
@@ -194,10 +194,11 @@ object SphericalUtil {
194
194
)
195
195
196
196
/* *
197
- * Returns the distance between two LatLngs , in meters.
197
+ * Returns the distance between two [LatLng]s , in meters.
198
198
*/
199
199
@JvmStatic
200
- fun computeDistanceBetween (from : LatLng , to : LatLng ) = computeAngleBetween(from, to) * EARTH_RADIUS
200
+ fun computeDistanceBetween (from : LatLng , to : LatLng ) =
201
+ computeAngleBetween(from, to) * EARTH_RADIUS
201
202
202
203
/* *
203
204
* Returns the length of the given path, in meters, on Earth.
@@ -207,19 +208,16 @@ object SphericalUtil {
207
208
if (path.size < 2 ) {
208
209
return 0.0
209
210
}
210
- var length = 0.0
211
- var prev: LatLng ? = null
212
- for (point in path) {
213
- if (prev != null ) {
214
- val prevLat = Math .toRadians(prev.latitude)
215
- val prevLng = Math .toRadians(prev.longitude)
216
- val lat = Math .toRadians(point.latitude)
217
- val lng = Math .toRadians(point.longitude)
218
- length + = distanceRadians(prevLat, prevLng, lat, lng)
219
- }
220
- prev = point
211
+
212
+ // Using zipWithNext() is a more functional and idiomatic way to handle
213
+ // adjacent pairs in a collection. We then sum the distances between each pair.
214
+ val totalDistance = path.zipWithNext().sumOf { (prev, point) ->
215
+ val (prevLatRad, prevLngRad) = prev.toRadians()
216
+ val (latRad, lngRad) = point.toRadians()
217
+ distanceRadians(prevLatRad, prevLngRad, latRad, lngRad)
221
218
}
222
- return length * EARTH_RADIUS
219
+
220
+ return totalDistance * EARTH_RADIUS
223
221
}
224
222
225
223
/* *
@@ -242,31 +240,33 @@ object SphericalUtil {
242
240
@JvmStatic
243
241
fun computeSignedArea (path : Polygon ) = computeSignedArea(path, EARTH_RADIUS )
244
242
243
+
245
244
/* *
246
245
* Returns the signed area of a closed path on a sphere of given radius.
247
246
* The computed area uses the same units as the radius squared.
248
247
* Used by SphericalUtilTest.
249
248
*/
250
249
@JvmStatic
251
- fun computeSignedArea (path : Polygon , radius : Double ): Double {
252
- val size = path.size
253
- if (size < 3 ) {
250
+ fun computeSignedArea (path : List <LatLng >, radius : Double ): Double {
251
+ if (path.size < 3 ) {
254
252
return 0.0
255
253
}
256
- var total = 0.0
257
- val prev = path[size - 1 ]
258
- var prevTanLat = tan(( PI / 2 - prev.latitude.toRadians()) / 2 )
259
- var prevLng = prev.longitude.toRadians()
254
+
255
+ // Create a closed path by appending the first point at the end.
256
+ val closedPath = path + path.first( )
257
+
260
258
// For each edge, accumulate the signed area of the triangle formed by the North Pole
261
259
// and that edge ("polar triangle").
262
- for (point in path) {
260
+ // `zipWithNext` creates pairs of consecutive vertices, representing the edges of the polygon.
261
+ val totalArea = closedPath.zipWithNext { prev, point ->
262
+ val prevTanLat = tan((PI / 2 - prev.latitude.toRadians()) / 2 )
263
263
val tanLat = tan((PI / 2 - point.latitude.toRadians()) / 2 )
264
+ val prevLng = prev.longitude.toRadians()
264
265
val lng = point.longitude.toRadians()
265
- total + = polarTriangleArea(tanLat, lng, prevTanLat, prevLng)
266
- prevTanLat = tanLat
267
- prevLng = lng
268
- }
269
- return total * (radius * radius)
266
+ polarTriangleArea(tanLat, lng, prevTanLat, prevLng)
267
+ }.sum()
268
+
269
+ return totalArea * (radius * radius)
270
270
}
271
271
272
272
/* *
@@ -281,4 +281,12 @@ object SphericalUtil {
281
281
val t = tan1 * tan2
282
282
return 2 * atan2(t * sin(deltaLng), 1 + t * cos(deltaLng))
283
283
}
284
- }
284
+ }
285
+
286
+ /* *
287
+ * Helper extension function to convert a LatLng to a pair of radians.
288
+ */
289
+ private fun LatLng.toRadians () = Pair (latitude.toRadians(), longitude.toRadians())
290
+
291
+ private fun Double.toRadians () = this * (PI / 180.0 )
292
+ private fun Double.toDegrees () = this * (180.0 / PI )
0 commit comments