geovectorslib.geod.direct()   B
last analyzed

Complexity

Conditions 3

Size

Total Lines 98
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 59
nop 4
dl 0
loc 98
rs 8.3417
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
import numpy as np
2
from geographiclib.geodesic import Geodesic
3
4
from geovectorslib.utils import wrap90deg, wrap180deg, wrap360deg
5
6
7
def inverse(lats1: 'list', lons1: 'list', lats2: 'list', lons2: 'list') -> 'dict':
8
    """
9
    Inverse geodesic using Vincenty approach.
10
11
    Calculate the great circle distance between two points
12
    on the earth (specified in decimal degrees)
13
14
    All args must be of equal length.
15
16
    Ref:
17
    https://www.movable-type.co.uk/scripts/latlong-vincenty.html
18
    """
19
20
    eps = np.finfo(float).eps
21
22
    lon1, lon2 = map(wrap180deg, [np.array(lons1), np.array(lons2)])
23
    lat1, lat2 = map(wrap90deg, [np.array(lats1), np.array(lats2)])
24
    lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2])
25
26
    # WGS84
27
    a = 6378137.0
28
    b = 6356752.314245
29
    f = 1.0 / 298.257223563
30
31
    # L = difference in longitude
32
    L = lon2 - lon1
33
34
    # U = reduced latitude, defined by tan U = (1-f)·tanφ.
35
    tan_U1 = (1 - f) * np.tan(lat1)
36
    cos_U1 = 1 / np.sqrt((1 + tan_U1 * tan_U1))
37
    sin_U1 = tan_U1 * cos_U1
38
    tan_U2 = (1 - f) * np.tan(lat2)
39
    cos_U2 = 1 / np.sqrt((1 + tan_U2 * tan_U2))
40
    sin_U2 = tan_U2 * cos_U2
41
42
    # checks for antipodal points
43
    antipodal = (np.abs(L) > np.pi / 2) | (np.abs(lat2 - lat1) > np.pi / 2)
44
45
    # delta = difference in longitude on an auxiliary sphere
46
    delta = L
47
    sin_delta = np.sin(delta)
48
    cos_delta = np.cos(delta)
49
50
    # sigma = angular distance P₁ P₂ on the sphere
51
    sigma = np.zeros(lat1.shape)
52
    sigma[antipodal] = np.pi
53
54
    cos_sigma = np.ones(lat1.shape)
55
    cos_sigma[antipodal] = -1
56
57
    # sigmam = angular distance on the sphere from the equator
58
    # to the midpoint of the line
59
    # azi = azimuth of the geodesic at the equator
60
    # init delta_prime at very high value to not miss iterations when delta=0
61
    delta_prime = 1 / eps * np.ones(lat1.shape)
62
    iterations = 0
63
64
    # init before loop to allow mask indexing
65
    sin_Sq_sigma = np.zeros(lat1.shape)
66
    sin_sigma = np.zeros(lat1.shape)
67
    cos_sigma = np.zeros(lat1.shape)
68
    sin_azi = np.zeros(lat1.shape)
69
    cos_Sq_azi = np.ones(lat1.shape)
70
    cos_2_sigma_m = np.ones(lat1.shape)
71
    C = np.ones(lat1.shape)
72
73
    # init mask
74
    m = np.ones(lat1.shape, dtype=bool)
75
    conv_mask = np.zeros(lat1.shape, dtype=bool)
76
77
    while (np.abs(delta[m] - delta_prime[m]) > 1e-12).any():
78
        sin_delta = np.sin(delta)
79
        cos_delta = np.cos(delta)
80
        sin_Sq_sigma = (cos_U2 * sin_delta) * (cos_U2 * sin_delta) + (
81
            cos_U1 * sin_U2 - sin_U1 * cos_U2 * cos_delta
82
        ) * (cos_U1 * sin_U2 - sin_U1 * cos_U2 * cos_delta)
83
84
        # co-incident/antipodal points mask - exclude from the rest of the loop
85
        # the value has to be about 1.e-4 experimentally
86
        # otherwise there are issues with convergence for near antipodal points
87
        m = (np.abs(sin_Sq_sigma) > eps) & (sin_Sq_sigma != np.nan)
88
89
        sin_sigma[m] = np.sqrt(sin_Sq_sigma[m])
90
        cos_sigma[m] = sin_U1[m] * sin_U2[m] + cos_U1[m] * cos_U2[m] * cos_delta[m]
91
        sigma[m] = np.arctan2(sin_sigma[m], cos_sigma[m])
92
        sin_azi[m] = cos_U1[m] * cos_U2[m] * sin_delta[m] / sin_sigma[m]
93
        cos_Sq_azi[m] = 1 - sin_azi[m] * sin_azi[m]
94
95
        # on equatorial line cos²azi = 0
96
        cos_2_sigma_m[m] = cos_sigma[m] - 2 * sin_U1[m] * sin_U2[m] / cos_Sq_azi[m]
97
        cos_2_sigma_m[cos_Sq_azi == 0] = 0
98
99
        C[m] = f / 16 * cos_Sq_azi[m] * (4 + f * (4 - 3 * cos_Sq_azi[m]))
100
101
        delta_prime = delta
102
        delta = L + (1 - C) * f * sin_azi * (
103
            sigma
104
            + (C * sin_sigma)
105
            * (cos_2_sigma_m + C * cos_sigma * (-1 + 2 * cos_2_sigma_m * cos_2_sigma_m))
106
        )
107
108
        # Exceptions
109
        iteration_check = np.abs(delta)
110
        iteration_check[antipodal] = iteration_check[antipodal] - np.pi
111
        if (iteration_check > np.pi).any():
112
            raise Exception('delta > np.pi')
113
114
        iterations += 1
115
        if iterations >= 50:
116
            conv_mask = np.abs(delta - delta_prime) > 1e-12
117
            break
118
119
    uSq = cos_Sq_azi * (a * a - b * b) / (b * b)
120
    A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)))
121
    B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)))
122
    d_sigma = (B * sin_sigma) * (
123
        cos_2_sigma_m
124
        + (B / 4)
125
        * (
126
            cos_sigma * (-1 + 2 * cos_2_sigma_m * cos_2_sigma_m)
127
            - (B / 6 * cos_2_sigma_m)
128
            * (-3 + 4 * sin_sigma * sin_sigma)
129
            * (-3 + 4 * cos_2_sigma_m * cos_2_sigma_m)
130
        )
131
    )
132
    # length of the geodesic
133
    s12 = b * A * (sigma - d_sigma)
134
135
    # note special handling of exactly antipodal points where sin²sigma = 0
136
    # (due to discontinuity atan2(0, 0) = 0 but atan2(eps, 0) = np.pi/2 / 90°)
137
    # in which case bearing is always meridional, due north (or due south!)
138
139
    azi1 = np.arctan2(cos_U2 * sin_delta, cos_U1 * sin_U2 - sin_U1 * cos_U2 * cos_delta)
140
    azi1[np.abs(sin_Sq_sigma) < eps] = 0
141
142
    azi2 = np.arctan2(
143
        cos_U1 * sin_delta, -sin_U1 * cos_U2 + cos_U1 * sin_U2 * cos_delta
144
    )
145
    azi2[np.abs(sin_Sq_sigma) < eps] = np.pi
146
147
    azi1 = wrap360deg(azi1 * 180 / np.pi)
148
    azi2 = wrap360deg(azi2 * 180 / np.pi)
149
150
    # if distance is too small - return 0 instead of `None`
151
    # to be consistent with geographiclib
152
    mask = s12 < eps
153
    azi1[mask] = 0
154
    azi2[mask] = 0
155
156
    # use geographiclib for points which didn't converge
157
    if conv_mask[conv_mask].shape[0] > 0:
158
        vInverse = np.vectorize(Geodesic.WGS84.Inverse)
159
        _ps = vInverse(
160
            np.array(lats1)[conv_mask],
161
            np.array(lons1)[conv_mask],
162
            np.array(lats2)[conv_mask],
163
            np.array(lons2)[conv_mask],
164
        )
165
        s12[conv_mask] = [_p['s12'] for _p in _ps]
166
        azi1[conv_mask] = [_p['azi1'] for _p in _ps]
167
        azi2[conv_mask] = [_p['azi2'] for _p in _ps]
168
169
    return {'s12': s12, 'azi1': azi1, 'azi2': azi2, 'iterations': iterations}
170
171
172
def direct(lats1: 'list', lons1: 'list', brgs: 'list', dists: 'list') -> 'dict':
173
    """
174
    Direct geodesic using Vincenty approach.
175
176
    Given a start point, initial bearing (0° is North, clockwise)
177
    and distance in meters, this will calculate the destination point
178
    and final bearing travelling along a (shortest distance) great circle arc.
179
180
    Bearing in 0-360deg starting from North clockwise.
181
182
    all variables must be of same length
183
184
    Ref:
185
    https://www.movable-type.co.uk/scripts/latlong.html
186
    https://www.movable-type.co.uk/scripts/latlong-vincenty.html
187
    """
188
189
    lon1, lat1, brg = map(np.radians, [lons1, lats1, brgs])
190
191
    # WGS84
192
    a = 6378137.0
193
    b = 6356752.314245
194
    f = 1.0 / 298.257223563
195
196
    azi1 = brg
197
    s = dists
198
199
    sin_azi1 = np.sin(azi1)
200
    cos_azi1 = np.cos(azi1)
201
202
    tan_U1 = (1 - f) * np.tan(lat1)
203
    cos_U1 = 1 / np.sqrt((1 + tan_U1 * tan_U1))
204
    sin_U1 = tan_U1 * cos_U1
205
206
    # sigma1 = angular distance on the sphere from the equator to P1
207
    sigma1 = np.arctan2(tan_U1, cos_azi1)
208
209
    sin_azi = cos_U1 * sin_azi1  # azi = azimuth of the geodesic at the equator
210
    cos_Sq_azi = 1 - sin_azi * sin_azi
211
    uSq = cos_Sq_azi * (a * a - b * b) / (b * b)
212
    A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)))
213
    B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)))
214
215
    sigma = s / (b * A)
216
    # sigma = angular distance P₁ P₂ on the sphere
217
    # sigmam = angular distance on the sphere from the equator to
218
    # the midpoint of the line
219
    sin_sigma = np.sin(sigma)
220
    cos_sigma = np.cos(sigma)
221
    cos_2_sigma_m = np.cos(2 * sigma1 + sigma)
222
223
    iterations = 0
224
    sigma_prime = 0
225
226
    while (np.abs(sigma - sigma_prime) > 1e-12).any():
227
        cos_2_sigma_m = np.cos(2 * sigma1 + sigma)
228
        sin_sigma = np.sin(sigma)
229
        cos_sigma = np.cos(sigma)
230
        delta_sigma = (B * sin_sigma) * (
231
            cos_2_sigma_m
232
            + B
233
            / 4
234
            * (
235
                cos_sigma * (-1 + 2 * cos_2_sigma_m * cos_2_sigma_m)
236
                - (B / 6 * cos_2_sigma_m)
237
                * (-3 + 4 * sin_sigma * sin_sigma)
238
                * (-3 + 4 * cos_2_sigma_m * cos_2_sigma_m)
239
            )
240
        )
241
        sigma_prime = sigma
242
        sigma = s / (b * A) + delta_sigma
243
        iterations += 1
244
        if iterations >= 50:
245
            raise Exception('Vincenty formula failed to converge')
246
247
    x = sin_U1 * sin_sigma - cos_U1 * cos_sigma * cos_azi1
248
    lat2 = np.arctan2(
249
        sin_U1 * cos_sigma + cos_U1 * sin_sigma * cos_azi1,
250
        (1 - f) * np.sqrt(sin_azi * sin_azi + x * x),
251
    )
252
    lambda_ = np.arctan2(
253
        sin_sigma * sin_azi1, cos_U1 * cos_sigma - sin_U1 * sin_sigma * cos_azi1
254
    )
255
    C = f / 16 * cos_Sq_azi * (4 + f * (4 - 3 * cos_Sq_azi))
256
    L = lambda_ - (1 - C) * f * sin_azi * (
257
        sigma
258
        + C
259
        * sin_sigma
260
        * (cos_2_sigma_m + C * cos_sigma * (-1 + 2 * cos_2_sigma_m * cos_2_sigma_m))
261
    )
262
    lon2 = lon1 + L
263
    azi2 = np.arctan2(sin_azi, -x)
264
265
    lat2 = wrap90deg(lat2 * 180.0 / np.pi)
266
    lon2 = wrap180deg(lon2 * 180.0 / np.pi)
267
    azi2 = wrap360deg(azi2 * 180.0 / np.pi)
268
269
    return {'lat2': lat2, 'lon2': lon2, 'azi2': azi2, 'iterations': iterations}
270