Passed
Push — master ( 2e2bcd...8764ec )
by Oleg
01:26
created

geovectorslib.geod.inverse()   C

Complexity

Conditions 5

Size

Total Lines 156
Code Lines 89

Duplication

Lines 0
Ratio 0 %

Importance

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