Coverage for /usr/local/lib/python3.13/dist-packages/pyrocko/orthodrome.py: 77%

464 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-12-04 10:41 +0000

1# http://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6''' 

7Some basic geodetic functions. 

8''' 

9 

10import math 

11from functools import wraps 

12 

13import numpy as num 

14from matplotlib.path import Path 

15 

16from .config import config 

17from .moment_tensor import euler_to_matrix 

18from .plot.beachball import spoly_cut 

19 

20d2r = math.pi/180. 

21r2d = 1./d2r 

22earth_oblateness = 1./298.257223563 

23earthradius_equator = 6378.14 * 1000. 

24earthradius = config().earthradius 

25d2m = earthradius_equator*math.pi/180. 

26m2d = 1./d2m 

27 

28_testpath = Path([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)], closed=True) 

29 

30raise_if_slow_path_contains_points = False 

31 

32 

33class Slow(Exception): 

34 pass 

35 

36 

37if hasattr(_testpath, 'contains_points') and num.all( 

38 _testpath.contains_points([(0.5, 0.5), (1.5, 0.5)]) == [True, False]): 

39 

40 def path_contains_points(verts, points): 

41 p = Path(verts, closed=True) 

42 return p.contains_points(points).astype(bool) 

43 

44else: 

45 # work around missing contains_points and bug in matplotlib ~ v1.2.0 

46 

47 def path_contains_points(verts, points): 

48 if raise_if_slow_path_contains_points: 

49 # used by unit test to skip slow gshhg_example.py 

50 raise Slow() 

51 

52 p = Path(verts, closed=True) 

53 result = num.zeros(points.shape[0], dtype=bool) 

54 for i in range(result.size): 

55 result[i] = p.contains_point(points[i, :]) 

56 

57 return result 

58 

59 

60try: 

61 cbrt = num.cbrt 

62except AttributeError: 

63 def cbrt(x): 

64 return x**(1./3.) 

65 

66 

67def float_array_broadcast(*args): 

68 return num.broadcast_arrays(*[ 

69 num.asarray(x, dtype=float) for x in args]) 

70 

71 

72class Loc(object): 

73 ''' 

74 Simple location representation. 

75 

76 :attrib lat: Latitude in [deg]. 

77 :attrib lon: Longitude in [deg]. 

78 ''' 

79 __slots__ = ['lat', 'lon'] 

80 

81 def __init__(self, lat, lon): 

82 self.lat = lat 

83 self.lon = lon 

84 

85 

86def clip(x, mi, ma): 

87 ''' 

88 Clip values of an array. 

89 

90 :param x: Continunous data to be clipped. 

91 :param mi: Clip minimum. 

92 :param ma: Clip maximum. 

93 :type x: :py:class:`numpy.ndarray` 

94 :type mi: float 

95 :type ma: float 

96 

97 :return: Clipped data. 

98 :rtype: :py:class:`numpy.ndarray` 

99 ''' 

100 return num.minimum(num.maximum(mi, x), ma) 

101 

102 

103def wrap(x, mi, ma): 

104 ''' 

105 Wrapping continuous data to fundamental phase values. 

106 

107 .. math:: 

108 x_{\\mathrm{wrapped}} = x_{\\mathrm{cont},i} - 

109 \\frac{ x_{\\mathrm{cont},i} - r_{\\mathrm{min}} } 

110 { r_{\\mathrm{max}} - r_{\\mathrm{min}}} 

111 \\cdot ( r_{\\mathrm{max}} - r_{\\mathrm{min}}),\\quad 

112 x_{\\mathrm{wrapped}}\\; \\in 

113 \\;[ r_{\\mathrm{min}},\\, r_{\\mathrm{max}}]. 

114 

115 :param x: Continunous data to be wrapped. 

116 :param mi: Minimum value of wrapped data. 

117 :param ma: Maximum value of wrapped data. 

118 :type x: :py:class:`numpy.ndarray` 

119 :type mi: float 

120 :type ma: float 

121 

122 :return: Wrapped data. 

123 :rtype: :py:class:`numpy.ndarray` 

124 ''' 

125 return x - num.floor((x-mi)/(ma-mi)) * (ma-mi) 

126 

127 

128def _latlon_pair(args): 

129 if len(args) == 2: 

130 a, b = args 

131 return a.lat, a.lon, b.lat, b.lon 

132 

133 elif len(args) == 4: 

134 return args 

135 

136 

137def cosdelta(*args): 

138 ''' 

139 Cosine of the angular distance between two points ``a`` and ``b`` on a 

140 sphere. 

141 

142 This function (find implementation below) returns the cosine of the 

143 distance angle 'delta' between two points ``a`` and ``b``, coordinates of 

144 which are expected to be given in geographical coordinates and in degrees. 

145 For numerical stability a maximum of 1.0 is enforced. 

146 

147 .. math:: 

148 

149 A_{\\mathrm{lat'}} = \\frac{ \\pi}{180} \\cdot A_{lat}, \\quad 

150 A_{\\mathrm{lon'}} = \\frac{ \\pi}{180} \\cdot A_{lon}, \\quad 

151 B_{\\mathrm{lat'}} = \\frac{ \\pi}{180} \\cdot B_{lat}, \\quad 

152 B_{\\mathrm{lon'}} = \\frac{ \\pi}{180} \\cdot B_{lon}\\\\[0.5cm] 

153 

154 \\cos(\\Delta) = \\min( 1.0, \\quad \\sin( A_{\\mathrm{lat'}}) 

155 \\sin( B_{\\mathrm{lat'}} ) + 

156 \\cos(A_{\\mathrm{lat'}}) \\cos( B_{\\mathrm{lat'}} ) 

157 \\cos( B_{\\mathrm{lon'}} - A_{\\mathrm{lon'}} ) 

158 

159 :param a: Location point A. 

160 :type a: :py:class:`pyrocko.orthodrome.Loc` 

161 :param b: Location point B. 

162 :type b: :py:class:`pyrocko.orthodrome.Loc` 

163 

164 :return: Cosdelta. 

165 :rtype: float 

166 ''' 

167 

168 alat, alon, blat, blon = _latlon_pair(args) 

169 

170 return min( 

171 1.0, 

172 math.sin(alat*d2r) * math.sin(blat*d2r) + 

173 math.cos(alat*d2r) * math.cos(blat*d2r) * 

174 math.cos(d2r*(blon-alon))) 

175 

176 

177def cosdelta_numpy(a_lats, a_lons, b_lats, b_lons): 

178 ''' 

179 Cosine of the angular distance between two points ``a`` and ``b`` on a 

180 sphere. 

181 

182 This function returns the cosines of the distance 

183 angles *delta* between two points ``a`` and ``b`` given as 

184 :py:class:`numpy.ndarray`. 

185 The coordinates are expected to be given in geographical coordinates 

186 and in degrees. For numerical stability a maximum of ``1.0`` is enforced. 

187 

188 Please find the details of the implementation in the documentation of 

189 the function :py:func:`pyrocko.orthodrome.cosdelta` above. 

190 

191 :param a_lats: Latitudes in [deg] point A. 

192 :param a_lons: Longitudes in [deg] point A. 

193 :param b_lats: Latitudes in [deg] point B. 

194 :param b_lons: Longitudes in [deg] point B. 

195 :type a_lats: :py:class:`numpy.ndarray` 

196 :type a_lons: :py:class:`numpy.ndarray` 

197 :type b_lats: :py:class:`numpy.ndarray` 

198 :type b_lons: :py:class:`numpy.ndarray` 

199 

200 :return: Cosdelta. 

201 :type b_lons: :py:class:`numpy.ndarray`, ``(N)`` 

202 ''' 

203 return num.minimum( 

204 1.0, 

205 num.sin(a_lats*d2r) * num.sin(b_lats*d2r) + 

206 num.cos(a_lats*d2r) * num.cos(b_lats*d2r) * 

207 num.cos(d2r*(b_lons-a_lons))) 

208 

209 

210def azimuth(*args): 

211 ''' 

212 Azimuth calculation. 

213 

214 This function (find implementation below) returns azimuth ... 

215 between points ``a`` and ``b``, coordinates of 

216 which are expected to be given in geographical coordinates and in degrees. 

217 

218 .. math:: 

219 

220 A_{\\mathrm{lat'}} = \\frac{ \\pi}{180} \\cdot A_{lat}, \\quad 

221 A_{\\mathrm{lon'}} = \\frac{ \\pi}{180} \\cdot A_{lon}, \\quad 

222 B_{\\mathrm{lat'}} = \\frac{ \\pi}{180} \\cdot B_{lat}, \\quad 

223 B_{\\mathrm{lon'}} = \\frac{ \\pi}{180} \\cdot B_{lon}\\\\ 

224 

225 \\varphi_{\\mathrm{azi},AB} = \\frac{180}{\\pi} \\arctan \\left[ 

226 \\frac{ 

227 \\cos( A_{\\mathrm{lat'}}) \\cos( B_{\\mathrm{lat'}} ) 

228 \\sin(B_{\\mathrm{lon'}} - A_{\\mathrm{lon'}} )} 

229 {\\sin ( B_{\\mathrm{lat'}} ) - \\sin( A_{\\mathrm{lat'}} 

230 cosdelta) } \\right] 

231 

232 :param a: Location point A. 

233 :type a: :py:class:`pyrocko.orthodrome.Loc` 

234 :param b: Location point B. 

235 :type b: :py:class:`pyrocko.orthodrome.Loc` 

236 

237 :return: Azimuth in degree 

238 ''' 

239 

240 alat, alon, blat, blon = _latlon_pair(args) 

241 

242 return r2d*math.atan2( 

243 math.cos(alat*d2r) * math.cos(blat*d2r) * 

244 math.sin(d2r*(blon-alon)), 

245 math.sin(d2r*blat) - math.sin(d2r*alat) * cosdelta( 

246 alat, alon, blat, blon)) 

247 

248 

249def azimuth_numpy(a_lats, a_lons, b_lats, b_lons, _cosdelta=None): 

250 ''' 

251 Calculation of the azimuth (*track angle*) from a location A towards B. 

252 

253 This function returns azimuths (*track angles*) from locations A towards B 

254 given in :py:class:`numpy.ndarray`. Coordinates are expected to be given in 

255 geographical coordinates and in degrees. 

256 

257 Please find the details of the implementation in the documentation of the 

258 function :py:func:`pyrocko.orthodrome.azimuth`. 

259 

260 :param a_lats: Latitudes in [deg] point A. 

261 :param a_lons: Longitudes in [deg] point A. 

262 :param b_lats: Latitudes in [deg] point B. 

263 :param b_lons: Longitudes in [deg] point B. 

264 :type a_lats: :py:class:`numpy.ndarray`, ``(N)`` 

265 :type a_lons: :py:class:`numpy.ndarray`, ``(N)`` 

266 :type b_lats: :py:class:`numpy.ndarray`, ``(N)`` 

267 :type b_lons: :py:class:`numpy.ndarray`, ``(N)`` 

268 

269 :return: Azimuths in [deg]. 

270 :rtype: :py:class:`numpy.ndarray`, ``(N)`` 

271 ''' 

272 if _cosdelta is None: 

273 _cosdelta = cosdelta_numpy(a_lats, a_lons, b_lats, b_lons) 

274 

275 return r2d*num.arctan2( 

276 num.cos(a_lats*d2r) * num.cos(b_lats*d2r) * 

277 num.sin(d2r*(b_lons-a_lons)), 

278 num.sin(d2r*b_lats) - num.sin(d2r*a_lats) * _cosdelta) 

279 

280 

281def azibazi(*args, **kwargs): 

282 ''' 

283 Azimuth and backazimuth from location A towards B and back. 

284 

285 :returns: Azimuth in [deg] from A to B, back azimuth in [deg] from B to A. 

286 :rtype: tuple[float, float] 

287 ''' 

288 

289 alat, alon, blat, blon = _latlon_pair(args) 

290 if alat == blat and alon == blon: 

291 return 0., 180. 

292 

293 implementation = kwargs.get('implementation', 'c') 

294 assert implementation in ('c', 'python') 

295 if implementation == 'c': 

296 from pyrocko import orthodrome_ext 

297 return orthodrome_ext.azibazi(alat, alon, blat, blon) 

298 

299 cd = cosdelta(alat, alon, blat, blon) 

300 azi = r2d*math.atan2( 

301 math.cos(alat*d2r) * math.cos(blat*d2r) * 

302 math.sin(d2r*(blon-alon)), 

303 math.sin(d2r*blat) - math.sin(d2r*alat) * cd) 

304 bazi = r2d*math.atan2( 

305 math.cos(blat*d2r) * math.cos(alat*d2r) * 

306 math.sin(d2r*(alon-blon)), 

307 math.sin(d2r*alat) - math.sin(d2r*blat) * cd) 

308 

309 return azi, bazi 

310 

311 

312def azibazi_numpy(a_lats, a_lons, b_lats, b_lons, implementation='c'): 

313 ''' 

314 Azimuth and backazimuth from location A towards B and back. 

315 

316 Arguments are given as :py:class:`numpy.ndarray`. 

317 

318 :param a_lats: Latitude(s) in [deg] of point A. 

319 :type a_lats: :py:class:`numpy.ndarray` 

320 :param a_lons: Longitude(s) in [deg] of point A. 

321 :type a_lons: :py:class:`numpy.ndarray` 

322 :param b_lats: Latitude(s) in [deg] of point B. 

323 :type b_lats: :py:class:`numpy.ndarray` 

324 :param b_lons: Longitude(s) in [deg] of point B. 

325 :type b_lons: :py:class:`numpy.ndarray` 

326 

327 :returns: Azimuth(s) in [deg] from A to B, 

328 back azimuth(s) in [deg] from B to A. 

329 :rtype: :py:class:`numpy.ndarray`, :py:class:`numpy.ndarray` 

330 ''' 

331 

332 a_lats, a_lons, b_lats, b_lons = float_array_broadcast( 

333 a_lats, a_lons, b_lats, b_lons) 

334 

335 assert implementation in ('c', 'python') 

336 if implementation == 'c': 

337 from pyrocko import orthodrome_ext 

338 return orthodrome_ext.azibazi_numpy(a_lats, a_lons, b_lats, b_lons) 

339 

340 _cosdelta = cosdelta_numpy(a_lats, a_lons, b_lats, b_lons) 

341 azis = azimuth_numpy(a_lats, a_lons, b_lats, b_lons, _cosdelta) 

342 bazis = azimuth_numpy(b_lats, b_lons, a_lats, a_lons, _cosdelta) 

343 

344 eq = num.logical_and(a_lats == b_lats, a_lons == b_lons) 

345 ii_eq = num.where(eq)[0] 

346 azis[ii_eq] = 0.0 

347 bazis[ii_eq] = 180.0 

348 return azis, bazis 

349 

350 

351def azidist_numpy(*args): 

352 ''' 

353 Calculation of the azimuth (*track angle*) and the distance from locations 

354 A towards B on a sphere. 

355 

356 The assisting functions used are :py:func:`pyrocko.orthodrome.cosdelta` and 

357 :py:func:`pyrocko.orthodrome.azimuth` 

358 

359 :param a_lats: Latitudes in [deg] point A. 

360 :param a_lons: Longitudes in [deg] point A. 

361 :param b_lats: Latitudes in [deg] point B. 

362 :param b_lons: Longitudes in [deg] point B. 

363 :type a_lats: :py:class:`numpy.ndarray`, ``(N)`` 

364 :type a_lons: :py:class:`numpy.ndarray`, ``(N)`` 

365 :type b_lats: :py:class:`numpy.ndarray`, ``(N)`` 

366 :type b_lons: :py:class:`numpy.ndarray`, ``(N)`` 

367 

368 :return: Azimuths in [deg], distances in [deg]. 

369 :rtype: :py:class:`numpy.ndarray`, ``(2xN)`` 

370 ''' 

371 _cosdelta = cosdelta_numpy(*args) 

372 _azimuths = azimuth_numpy(_cosdelta=_cosdelta, *args) 

373 return _azimuths, r2d*num.arccos(_cosdelta) 

374 

375 

376def distance_accurate50m(*args, **kwargs): 

377 ''' 

378 Accurate distance calculation based on a spheroid of rotation. 

379 

380 Function returns distance in meter between points A and B, coordinates of 

381 which must be given in geographical coordinates and in degrees. 

382 The returned distance should be accurate to 50 m using WGS84. 

383 Values for the Earth's equator radius and the Earth's oblateness 

384 (``f_oblate``) are defined in the pyrocko configuration file 

385 :py:class:`pyrocko.config`. 

386 

387 From wikipedia (http://de.wikipedia.org/wiki/Orthodrome), based on: 

388 

389 ``Meeus, J.: Astronomical Algorithms, S 85, Willmann-Bell, 

390 Richmond 2000 (2nd ed., 2nd printing), ISBN 0-943396-61-1`` 

391 

392 .. math:: 

393 

394 F = \\frac{\\pi}{180} 

395 \\frac{(A_{lat} + B_{lat})}{2}, \\quad 

396 G = \\frac{\\pi}{180} 

397 \\frac{(A_{lat} - B_{lat})}{2}, \\quad 

398 l = \\frac{\\pi}{180} 

399 \\frac{(A_{lon} - B_{lon})}{2} \\quad 

400 \\\\[0.5cm] 

401 S = \\sin^2(G) \\cdot \\cos^2(l) + 

402 \\cos^2(F) \\cdot \\sin^2(l), \\quad \\quad 

403 C = \\cos^2(G) \\cdot \\cos^2(l) + 

404 \\sin^2(F) \\cdot \\sin^2(l) 

405 

406 .. math:: 

407 

408 w = \\arctan \\left( \\sqrt{ \\frac{S}{C}} \\right) , \\quad 

409 r = \\sqrt{\\frac{S}{C} } 

410 

411 The spherical-earth distance D between A and B, can be given with: 

412 

413 .. math:: 

414 

415 D_{sphere} = 2w \\cdot R_{equator} 

416 

417 The oblateness of the Earth requires some correction with 

418 correction factors h1 and h2: 

419 

420 .. math:: 

421 

422 h_1 = \\frac{3r - 1}{2C}, \\quad 

423 h_2 = \\frac{3r +1 }{2S}\\\\[0.5cm] 

424 

425 D = D_{\\mathrm{sphere}} \\cdot [ 1 + h_1 \\,f_{\\mathrm{oblate}} 

426 \\cdot \\sin^2(F) 

427 \\cos^2(G) - h_2\\, f_{\\mathrm{oblate}} 

428 \\cdot \\cos^2(F) \\sin^2(G)] 

429 

430 

431 :param a: Location point A. 

432 :type a: :py:class:`pyrocko.orthodrome.Loc` 

433 :param b: Location point B. 

434 :type b: :py:class:`pyrocko.orthodrome.Loc` 

435 

436 :return: Distance in [m]. 

437 :rtype: float 

438 ''' 

439 

440 alat, alon, blat, blon = _latlon_pair(args) 

441 

442 implementation = kwargs.get('implementation', 'c') 

443 assert implementation in ('c', 'python') 

444 if implementation == 'c': 

445 from pyrocko import orthodrome_ext 

446 return orthodrome_ext.distance_accurate50m(alat, alon, blat, blon) 

447 

448 f = (alat + blat)*d2r / 2. 

449 g = (alat - blat)*d2r / 2. 

450 h = (alon - blon)*d2r / 2. 

451 

452 s = math.sin(g)**2 * math.cos(h)**2 + math.cos(f)**2 * math.sin(h)**2 

453 c = math.cos(g)**2 * math.cos(h)**2 + math.sin(f)**2 * math.sin(h)**2 

454 

455 w = math.atan(math.sqrt(s/c)) 

456 

457 if w == 0.0: 

458 return 0.0 

459 

460 r = math.sqrt(s*c)/w 

461 d = 2.*w*earthradius_equator 

462 h1 = (3.*r-1.)/(2.*c) 

463 h2 = (3.*r+1.)/(2.*s) 

464 

465 return d * (1. + 

466 earth_oblateness * h1 * math.sin(f)**2 * math.cos(g)**2 - 

467 earth_oblateness * h2 * math.cos(f)**2 * math.sin(g)**2) 

468 

469 

470def distance_accurate50m_numpy( 

471 a_lats, a_lons, b_lats, b_lons, implementation='c'): 

472 

473 ''' 

474 Accurate distance calculation based on a spheroid of rotation. 

475 

476 Function returns distance in meter between points ``a`` and ``b``, 

477 coordinates of which must be given in geographical coordinates and in 

478 degrees. 

479 The returned distance should be accurate to 50 m using WGS84. 

480 Values for the Earth's equator radius and the Earth's oblateness 

481 (``f_oblate``) are defined in the pyrocko configuration file 

482 :py:class:`pyrocko.config`. 

483 

484 From wikipedia (http://de.wikipedia.org/wiki/Orthodrome), based on: 

485 

486 ``Meeus, J.: Astronomical Algorithms, S 85, Willmann-Bell, 

487 Richmond 2000 (2nd ed., 2nd printing), ISBN 0-943396-61-1`` 

488 

489 .. math:: 

490 

491 F_i = \\frac{\\pi}{180} 

492 \\frac{(a_{lat,i} + a_{lat,i})}{2}, \\quad 

493 G_i = \\frac{\\pi}{180} 

494 \\frac{(a_{lat,i} - b_{lat,i})}{2}, \\quad 

495 l_i= \\frac{\\pi}{180} 

496 \\frac{(a_{lon,i} - b_{lon,i})}{2} \\\\[0.5cm] 

497 S_i = \\sin^2(G_i) \\cdot \\cos^2(l_i) + 

498 \\cos^2(F_i) \\cdot \\sin^2(l_i), \\quad \\quad 

499 C_i = \\cos^2(G_i) \\cdot \\cos^2(l_i) + 

500 \\sin^2(F_i) \\cdot \\sin^2(l_i) 

501 

502 .. math:: 

503 

504 w_i = \\arctan \\left( \\sqrt{\\frac{S_i}{C_i}} \\right), \\quad 

505 r_i = \\sqrt{\\frac{S_i}{C_i} } 

506 

507 The spherical-earth distance ``D`` between ``a`` and ``b``, 

508 can be given with: 

509 

510 .. math:: 

511 

512 D_{\\mathrm{sphere},i} = 2w_i \\cdot R_{\\mathrm{equator}} 

513 

514 The oblateness of the Earth requires some correction with 

515 correction factors ``h1`` and ``h2``: 

516 

517 .. math:: 

518 

519 h_{1.i} = \\frac{3r - 1}{2C_i}, \\quad 

520 h_{2,i} = \\frac{3r +1 }{2S_i}\\\\[0.5cm] 

521 

522 D_{AB,i} = D_{\\mathrm{sphere},i} \\cdot [1 + h_{1,i} 

523 \\,f_{\\mathrm{oblate}} 

524 \\cdot \\sin^2(F_i) 

525 \\cos^2(G_i) - h_{2,i}\\, f_{\\mathrm{oblate}} 

526 \\cdot \\cos^2(F_i) \\sin^2(G_i)] 

527 

528 :param a_lats: Latitudes in [deg] point A. 

529 :param a_lons: Longitudes in [deg] point A. 

530 :param b_lats: Latitudes in [deg] point B. 

531 :param b_lons: Longitudes in [deg] point B. 

532 :type a_lats: :py:class:`numpy.ndarray`, ``(N)`` 

533 :type a_lons: :py:class:`numpy.ndarray`, ``(N)`` 

534 :type b_lats: :py:class:`numpy.ndarray`, ``(N)`` 

535 :type b_lons: :py:class:`numpy.ndarray`, ``(N)`` 

536 

537 :return: Distances in [m]. 

538 :rtype: :py:class:`numpy.ndarray`, ``(N)`` 

539 ''' 

540 

541 a_lats, a_lons, b_lats, b_lons = float_array_broadcast( 

542 a_lats, a_lons, b_lats, b_lons) 

543 

544 assert implementation in ('c', 'python') 

545 if implementation == 'c': 

546 from pyrocko import orthodrome_ext 

547 return orthodrome_ext.distance_accurate50m_numpy( 

548 a_lats, a_lons, b_lats, b_lons) 

549 

550 eq = num.logical_and(a_lats == b_lats, a_lons == b_lons) 

551 ii_neq = num.where(num.logical_not(eq))[0] 

552 

553 if num.all(eq): 

554 return num.zeros_like(eq, dtype=float) 

555 

556 def extr(x): 

557 if isinstance(x, num.ndarray) and x.size > 1: 

558 return x[ii_neq] 

559 else: 

560 return x 

561 

562 a_lats = extr(a_lats) 

563 a_lons = extr(a_lons) 

564 b_lats = extr(b_lats) 

565 b_lons = extr(b_lons) 

566 

567 f = (a_lats + b_lats)*d2r / 2. 

568 g = (a_lats - b_lats)*d2r / 2. 

569 h = (a_lons - b_lons)*d2r / 2. 

570 

571 s = num.sin(g)**2 * num.cos(h)**2 + num.cos(f)**2 * num.sin(h)**2 

572 c = num.cos(g)**2 * num.cos(h)**2 + num.sin(f)**2 * num.sin(h)**2 

573 

574 w = num.arctan(num.sqrt(s/c)) 

575 

576 r = num.sqrt(s*c)/w 

577 

578 d = 2.*w*earthradius_equator 

579 h1 = (3.*r-1.)/(2.*c) 

580 h2 = (3.*r+1.)/(2.*s) 

581 

582 dists = num.zeros(eq.size, dtype=float) 

583 dists[ii_neq] = d * ( 

584 1. + 

585 earth_oblateness * h1 * num.sin(f)**2 * num.cos(g)**2 - 

586 earth_oblateness * h2 * num.cos(f)**2 * num.sin(g)**2) 

587 

588 return dists 

589 

590 

591def ne_to_latlon(lat0, lon0, north_m, east_m): 

592 ''' 

593 Transform local cartesian coordinates to latitude and longitude. 

594 

595 From east and north coordinates (``x`` and ``y`` coordinate 

596 :py:class:`numpy.ndarray`) relative to a reference differences in 

597 longitude and latitude are calculated, which are effectively changes in 

598 azimuth and distance, respectively: 

599 

600 .. math:: 

601 

602 \\text{distance change:}\\; \\Delta {\\bf{a}} &= \\sqrt{{\\bf{y}}^2 + 

603 {\\bf{x}}^2 }/ \\mathrm{R_E}, 

604 

605 \\text{azimuth change:}\\; \\Delta \\bf{\\gamma} &= \\arctan( \\bf{x} 

606 / \\bf{y}). 

607 

608 The projection used preserves the azimuths of the input points. 

609 

610 :param lat0: Latitude origin of the cartesian coordinate system in [deg]. 

611 :param lon0: Longitude origin of the cartesian coordinate system in [deg]. 

612 :param north_m: Northing distances from origin in [m]. 

613 :param east_m: Easting distances from origin in [m]. 

614 :type north_m: :py:class:`numpy.ndarray`, ``(N)`` 

615 :type east_m: :py:class:`numpy.ndarray`, ``(N)`` 

616 :type lat0: float 

617 :type lon0: float 

618 

619 :return: Array with latitudes and longitudes in [deg]. 

620 :rtype: :py:class:`numpy.ndarray`, ``(2xN)`` 

621 

622 ''' 

623 

624 a = num.sqrt(north_m**2+east_m**2)/earthradius 

625 gamma = num.arctan2(east_m, north_m) 

626 

627 return azidist_to_latlon_rad(lat0, lon0, gamma, a) 

628 

629 

630def azidist_to_latlon(lat0, lon0, azimuth_deg, distance_deg): 

631 ''' 

632 Absolute latitudes and longitudes are calculated from relative changes. 

633 

634 Convenience wrapper to :py:func:`azidist_to_latlon_rad` with azimuth and 

635 distance given in degrees. 

636 

637 :param lat0: Latitude origin of the cartesian coordinate system in [deg]. 

638 :type lat0: float 

639 :param lon0: Longitude origin of the cartesian coordinate system in [deg]. 

640 :type lon0: float 

641 :param azimuth_deg: Azimuth from origin in [deg]. 

642 :type azimuth_deg: :py:class:`numpy.ndarray`, ``(N)`` 

643 :param distance_deg: Distances from origin in [deg]. 

644 :type distance_deg: :py:class:`numpy.ndarray`, ``(N)`` 

645 

646 :return: Array with latitudes and longitudes in [deg]. 

647 :rtype: :py:class:`numpy.ndarray`, ``(2xN)`` 

648 ''' 

649 

650 return azidist_to_latlon_rad( 

651 lat0, lon0, azimuth_deg/180.*num.pi, distance_deg/180.*num.pi) 

652 

653 

654def azidist_to_latlon_rad(lat0, lon0, azimuth_rad, distance_rad): 

655 ''' 

656 Absolute latitudes and longitudes are calculated from relative changes. 

657 

658 For numerical stability a range between of ``-1.0`` and ``1.0`` is 

659 enforced for ``c`` and ``alpha``. 

660 

661 .. math:: 

662 

663 \\Delta {\\bf a}_i \\; \\text{and} \\; \\Delta \\gamma_i \\; 

664 \\text{are relative distances and azimuths from lat0 and lon0 for 

665 \\textit{i} source points of a finite source.} 

666 

667 .. math:: 

668 

669 \\mathrm{b} &= \\frac{\\pi}{2} -\\frac{\\pi}{180}\\;\\mathrm{lat_0}\\\\ 

670 {\\bf c}_i &=\\arccos[\\; \\cos(\\Delta {\\bf{a}}_i) 

671 \\cos(\\mathrm{b}) + |\\Delta \\gamma_i| \\, 

672 \\sin(\\Delta {\\bf a}_i) 

673 \\sin(\\mathrm{b})\\; ] \\\\ 

674 \\mathrm{lat}_i &= \\frac{180}{\\pi} 

675 \\left(\\frac{\\pi}{2} - {\\bf c}_i \\right) 

676 

677 .. math:: 

678 

679 \\alpha_i &= \\arcsin \\left[ \\; \\frac{ \\sin(\\Delta {\\bf a}_i ) 

680 \\sin(|\\Delta \\gamma_i|)}{\\sin({\\bf c}_i)}\\; 

681 \\right] \\\\ 

682 \\alpha_i &= \\begin{cases} 

683 \\alpha_i, &\\text{if} \\; \\cos(\\Delta {\\bf a}_i) - 

684 \\cos(\\mathrm{b}) \\cos({\\bf{c}}_i) > 0, \\; 

685 \\text{else} \\\\ 

686 \\pi - \\alpha_i, & \\text{if} \\; \\alpha_i > 0,\\; 

687 \\text{else}\\\\ 

688 -\\pi - \\alpha_i, & \\text{if} \\; \\alpha_i < 0. 

689 \\end{cases} \\\\ 

690 \\mathrm{lon}_i &= \\mathrm{lon_0} + 

691 \\frac{180}{\\pi} \\, 

692 \\frac{\\Delta \\gamma_i }{|\\Delta \\gamma_i|} 

693 \\cdot \\alpha_i 

694 \\text{, with $\\alpha_i \\in [-\\pi,\\pi]$} 

695 

696 :param lat0: Latitude origin of the cartesian coordinate system in [deg]. 

697 :param lon0: Longitude origin of the cartesian coordinate system in [deg]. 

698 :param distance_rad: Distances from origin in [rad]. 

699 :param azimuth_rad: Azimuth from origin in [rad]. 

700 :type distance_rad: :py:class:`numpy.ndarray`, ``(N)`` 

701 :type azimuth_rad: :py:class:`numpy.ndarray`, ``(N)`` 

702 :type lat0: float 

703 :type lon0: float 

704 

705 :return: Array with latitudes and longitudes in [deg]. 

706 :rtype: :py:class:`numpy.ndarray`, ``(2xN)`` 

707 ''' 

708 

709 a = distance_rad 

710 gamma = azimuth_rad 

711 

712 b = math.pi/2.-lat0*d2r 

713 

714 alphasign = 1. 

715 alphasign = num.where(gamma < 0, -1., 1.) 

716 gamma = num.abs(gamma) 

717 

718 c = num.arccos(clip( 

719 num.cos(a)*num.cos(b)+num.sin(a)*num.sin(b)*num.cos(gamma), -1., 1.)) 

720 

721 alpha = num.arcsin(clip( 

722 num.sin(a)*num.sin(gamma)/num.sin(c), -1., 1.)) 

723 

724 alpha = num.where( 

725 num.cos(a)-num.cos(b)*num.cos(c) < 0, 

726 num.where(alpha > 0, math.pi-alpha, -math.pi-alpha), 

727 alpha) 

728 

729 lat = r2d * (math.pi/2. - c) 

730 lon = wrap(lon0 + r2d*alpha*alphasign, -180., 180.) 

731 

732 return lat, lon 

733 

734 

735def crosstrack_distance(begin_lat, begin_lon, end_lat, end_lon, 

736 point_lat, point_lon): 

737 '''Calculate distance of a point to a great-circle path. 

738 

739 The sign of the results shows side of the path the point is on. Negative 

740 distance is right of the path, positive left. 

741 

742 .. math :: 

743 

744 d_{xt} = \\arcsin \\left( \\sin \\left( \\Delta_{13} \\right) \\cdot 

745 \\sin \\left( \\gamma_{13} - \\gamma_{12} \\right) \\right) 

746 

747 :param begin_lat: Latitude of the great circle start in [deg]. 

748 :param begin_lon: Longitude of the great circle start in [deg]. 

749 :param end_lat: Latitude of the great circle end in [deg]. 

750 :param end_lon: Longitude of the great circle end in [deg]. 

751 :param point_lat: Latitude of the point in [deg]. 

752 :param point_lon: Longitude of the point in [deg]. 

753 :type begin_lat: float 

754 :type begin_lon: float 

755 :type end_lat: float 

756 :type end_lon: float 

757 :type point_lat: float 

758 :type point_lon: float 

759 

760 :return: Distance of the point to the great-circle path in [deg]. 

761 :rtype: float 

762 ''' 

763 start = Loc(begin_lat, begin_lon) 

764 end = Loc(end_lat, end_lon) 

765 point = Loc(point_lat, point_lon) 

766 

767 dist_ang = math.acos(cosdelta(start, point)) 

768 azi_point = azimuth(start, point) * d2r 

769 azi_end = azimuth(start, end) * d2r 

770 

771 return math.asin(math.sin(dist_ang) * math.sin(azi_point - azi_end)) * r2d 

772 

773 

774def alongtrack_distance(begin_lat, begin_lon, end_lat, end_lon, 

775 point_lat, point_lon): 

776 '''Calculate distance of a point along a great-circle path in [deg]. 

777 

778 Distance is relative to the beginning of the path. Negative distance is 

779 before the beginning of the path. 

780 

781 .. math :: 

782 

783 d_{At} = \\arccos \\left( \\frac{\\cos \\left( \\Delta_{13} \\right)} 

784 {\\cos \\left( \\Delta_{xt} \\right) } \\right) 

785 

786 :param begin_lat: Latitude of the great circle start in [deg]. 

787 :param begin_lon: Longitude of the great circle start in [deg]. 

788 :param end_lat: Latitude of the great circle end in [deg]. 

789 :param end_lon: Longitude of the great circle end in [deg]. 

790 :param point_lat: Latitude of the point in [deg]. 

791 :param point_lon: Longitude of the point in [deg]. 

792 :type begin_lat: float 

793 :type begin_lon: float 

794 :type end_lat: float 

795 :type end_lon: float 

796 :type point_lat: float 

797 :type point_lon: float 

798 

799 :return: Distance of the point along the great-circle path in [deg]. 

800 :rtype: float 

801 ''' 

802 begin = Loc(begin_lat, begin_lon) 

803 end = Loc(end_lat, end_lon) 

804 point = Loc(point_lat, point_lon) 

805 

806 def along_distance(begin, end): 

807 cos_dist_ang = cosdelta(begin, point) 

808 dist_rad = crosstrack_distance( 

809 begin.lat, begin.lon, end.lat, end.lon, point.lat, point.lon) * d2r 

810 return math.acos(cos_dist_ang / math.cos(dist_rad)) * r2d 

811 

812 distance_profile = math.acos(cosdelta(begin, end)) * r2d 

813 distance_begin = along_distance(begin, end) 

814 distance_end = along_distance(end, begin) 

815 

816 if distance_end > distance_profile: 

817 return -distance_begin 

818 return distance_begin 

819 

820 

821def alongtrack_distance_m(begin_lat, begin_lon, end_lat, end_lon, 

822 point_lat, point_lon): 

823 '''Calculate distance of a point along a great-circle path in [m]. 

824 

825 Distance is relative to the beginning of the path. 

826 

827 .. math :: 

828 

829 d_{At} = \\arccos \\left( \\frac{\\cos \\left( \\Delta_{13} \\right)} 

830 {\\cos \\left( \\Delta_{xt} \\right) } \\right) 

831 

832 :param begin_lat: Latitude of the great circle start in [deg]. 

833 :param begin_lon: Longitude of the great circle start in [deg]. 

834 :param end_lat: Latitude of the great circle end in [deg]. 

835 :param end_lon: Longitude of the great circle end in [deg]. 

836 :param point_lat: Latitude of the point in [deg]. 

837 :param point_lon: Longitude of the point in [deg]. 

838 :type begin_lat: float 

839 :type begin_lon: float 

840 :type end_lat: float 

841 :type end_lon: float 

842 :type point_lat: float 

843 :type point_lon: float 

844 

845 :return: Distance of the point along the great-circle path in [m]. 

846 :rtype: float 

847 ''' 

848 start = Loc(begin_lat, begin_lon) 

849 end = Loc(end_lat, end_lon) 

850 azi_end = azimuth(start, end) 

851 dist_deg = alongtrack_distance( 

852 begin_lat, begin_lon, end_lat, end_lon, 

853 point_lat, point_lon) 

854 along_point = Loc( 

855 *azidist_to_latlon(begin_lat, begin_lon, azi_end, dist_deg)) 

856 

857 distance = distance_accurate50m(start, along_point) 

858 if dist_deg < 0: 

859 distance = -distance 

860 return distance 

861 

862 

863def ne_to_latlon_alternative_method(lat0, lon0, north_m, east_m): 

864 ''' 

865 Transform local cartesian coordinates to latitude and longitude. 

866 

867 Like :py:func:`pyrocko.orthodrome.ne_to_latlon`, 

868 but this method (implementation below), although it should be numerically 

869 more stable, suffers problems at points which are *across the pole* 

870 as seen from the cartesian origin. 

871 

872 .. math:: 

873 

874 \\text{distance change:}\\; \\Delta {{\\bf a}_i} &= 

875 \\sqrt{{\\bf{y}}^2_i + {\\bf{x}}^2_i }/ \\mathrm{R_E},\\\\ 

876 \\text{azimuth change:}\\; \\Delta {\\bf \\gamma}_i &= 

877 \\arctan( {\\bf x}_i {\\bf y}_i). \\\\ 

878 \\mathrm{b} &= 

879 \\frac{\\pi}{2} -\\frac{\\pi}{180} \\;\\mathrm{lat_0}\\\\ 

880 

881 .. math:: 

882 

883 {{\\bf z}_1}_i &= \\cos{\\left( \\frac{\\Delta {\\bf a}_i - 

884 \\mathrm{b}}{2} \\right)} 

885 \\cos {\\left( \\frac{|\\gamma_i|}{2} \\right) }\\\\ 

886 {{\\bf n}_1}_i &= \\cos{\\left( \\frac{\\Delta {\\bf a}_i + 

887 \\mathrm{b}}{2} \\right)} 

888 \\sin {\\left( \\frac{|\\gamma_i|}{2} \\right) }\\\\ 

889 {{\\bf z}_2}_i &= \\sin{\\left( \\frac{\\Delta {\\bf a}_i - 

890 \\mathrm{b}}{2} \\right)} 

891 \\cos {\\left( \\frac{|\\gamma_i|}{2} \\right) }\\\\ 

892 {{\\bf n}_2}_i &= \\sin{\\left( \\frac{\\Delta {\\bf a}_i + 

893 \\mathrm{b}}{2} \\right)} 

894 \\sin {\\left( \\frac{|\\gamma_i|}{2} \\right) }\\\\ 

895 {{\\bf t}_1}_i &= \\arctan{\\left( \\frac{{{\\bf z}_1}_i} 

896 {{{\\bf n}_1}_i} \\right) }\\\\ 

897 {{\\bf t}_2}_i &= \\arctan{\\left( \\frac{{{\\bf z}_2}_i} 

898 {{{\\bf n}_2}_i} \\right) } \\\\[0.5cm] 

899 c &= \\begin{cases} 

900 2 \\cdot \\arccos \\left( {{\\bf z}_1}_i / \\sin({{\\bf t}_1}_i) 

901 \\right),\\; \\text{if } 

902 |\\sin({{\\bf t}_1}_i)| > 

903 |\\sin({{\\bf t}_2}_i)|,\\; \\text{else} \\\\ 

904 2 \\cdot \\arcsin{\\left( {{\\bf z}_2}_i / 

905 \\sin({{\\bf t}_2}_i) \\right)}. 

906 \\end{cases}\\\\ 

907 

908 .. math:: 

909 

910 {\\bf {lat}}_i &= \\frac{180}{ \\pi } \\left( \\frac{\\pi}{2} 

911 - {\\bf {c}}_i \\right) \\\\ 

912 {\\bf {lon}}_i &= {\\bf {lon}}_0 + \\frac{180}{ \\pi } 

913 \\frac{\\gamma_i}{|\\gamma_i|}, 

914 \\text{ with}\\; \\gamma \\in [-\\pi,\\pi] 

915 

916 :param lat0: Latitude origin of the cartesian coordinate system in [deg]. 

917 :param lon0: Longitude origin of the cartesian coordinate system in [deg]. 

918 :param north_m: Northing distances from origin in [m]. 

919 :param east_m: Easting distances from origin in [m]. 

920 :type north_m: :py:class:`numpy.ndarray`, ``(N)`` 

921 :type east_m: :py:class:`numpy.ndarray`, ``(N)`` 

922 :type lat0: float 

923 :type lon0: float 

924 

925 :return: Array with latitudes and longitudes in [deg]. 

926 :rtype: :py:class:`numpy.ndarray`, ``(2xN)`` 

927 ''' 

928 

929 b = math.pi/2.-lat0*d2r 

930 a = num.sqrt(north_m**2+east_m**2)/earthradius 

931 

932 gamma = num.arctan2(east_m, north_m) 

933 alphasign = 1. 

934 alphasign = num.where(gamma < 0., -1., 1.) 

935 gamma = num.abs(gamma) 

936 

937 z1 = num.cos((a-b)/2.)*num.cos(gamma/2.) 

938 n1 = num.cos((a+b)/2.)*num.sin(gamma/2.) 

939 z2 = num.sin((a-b)/2.)*num.cos(gamma/2.) 

940 n2 = num.sin((a+b)/2.)*num.sin(gamma/2.) 

941 t1 = num.arctan2(z1, n1) 

942 t2 = num.arctan2(z2, n2) 

943 

944 alpha = t1 + t2 

945 

946 sin_t1 = num.sin(t1) 

947 sin_t2 = num.sin(t2) 

948 c = num.where( 

949 num.abs(sin_t1) > num.abs(sin_t2), 

950 num.arccos(z1/sin_t1)*2., 

951 num.arcsin(z2/sin_t2)*2.) 

952 

953 lat = r2d * (math.pi/2. - c) 

954 lon = wrap(lon0 + r2d*alpha*alphasign, -180., 180.) 

955 return lat, lon 

956 

957 

958def angle_difference(angle_a, angle_b): 

959 ''' 

960 Difference between two angles in [deg]. 

961 

962 :param angle_a: Angle A [deg]. 

963 :param angle_b: Angle B [deg]. 

964 

965 :return: Difference between the two angles in [deg]. 

966 :rtype: float 

967 ''' 

968 return ((angle_a - angle_b) + 180.) % 360. - 180. 

969 

970 

971def latlon_to_ne(*args): 

972 ''' 

973 Relative cartesian coordinates with respect to a reference location. 

974 

975 For two locations, a reference location A and another location B, given in 

976 geographical coordinates in degrees, the corresponding cartesian 

977 coordinates are calculated. 

978 Assisting functions are :py:func:`pyrocko.orthodrome.azimuth` and 

979 :py:func:`pyrocko.orthodrome.distance_accurate50m`. 

980 

981 .. math:: 

982 

983 D_{AB} &= \\mathrm{distance\\_accurate50m(}A, B \\mathrm{)}, \\quad 

984 \\varphi_{\\mathrm{azi},AB} = \\mathrm{azimuth(}A,B 

985 \\mathrm{)}\\\\[0.3cm] 

986 

987 n &= D_{AB} \\cdot \\cos( \\frac{\\pi }{180} 

988 \\varphi_{\\mathrm{azi},AB} )\\\\ 

989 e &= D_{AB} \\cdot 

990 \\sin( \\frac{\\pi }{180} \\varphi_{\\mathrm{azi},AB}) 

991 

992 :param refloc: Location reference point. 

993 :type refloc: :py:class:`pyrocko.orthodrome.Loc` 

994 :param loc: Location of interest. 

995 :type loc: :py:class:`pyrocko.orthodrome.Loc` 

996 

997 :return: Northing and easting from refloc to location in [m]. 

998 :rtype: tuple[float, float] 

999 

1000 ''' 

1001 

1002 azi = azimuth(*args) 

1003 dist = distance_accurate50m(*args) 

1004 n, e = math.cos(azi*d2r)*dist, math.sin(azi*d2r)*dist 

1005 return n, e 

1006 

1007 

1008def latlon_to_ne_numpy(lat0, lon0, lat, lon): 

1009 ''' 

1010 Relative cartesian coordinates with respect to a reference location. 

1011 

1012 For two locations, a reference location (``lat0``, ``lon0``) and another 

1013 location B, given in geographical coordinates in degrees, 

1014 the corresponding cartesian coordinates are calculated. 

1015 Assisting functions are :py:func:`azimuth` 

1016 and :py:func:`distance_accurate50m`. 

1017 

1018 :param lat0: Latitude of the reference location in [deg]. 

1019 :param lon0: Longitude of the reference location in [deg]. 

1020 :param lat: Latitude of the absolute location in [deg]. 

1021 :param lon: Longitude of the absolute location in [deg]. 

1022 

1023 :return: ``(n, e)``: relative north and east positions in [m]. 

1024 :rtype: :py:class:`numpy.ndarray`, ``(2xN)`` 

1025 

1026 Implemented formulations: 

1027 

1028 .. math:: 

1029 

1030 D_{AB} &= \\mathrm{distance\\_accurate50m(}A, B \\mathrm{)}, \\quad 

1031 \\varphi_{\\mathrm{azi},AB} = \\mathrm{azimuth(}A,B 

1032 \\mathrm{)}\\\\[0.3cm] 

1033 

1034 n &= D_{AB} \\cdot \\cos( \\frac{\\pi }{180} \\varphi_{ 

1035 \\mathrm{azi},AB} )\\\\ 

1036 e &= D_{AB} \\cdot \\sin( \\frac{\\pi }{180} \\varphi_{ 

1037 \\mathrm{azi},AB} ) 

1038 ''' 

1039 

1040 azi = azimuth_numpy(lat0, lon0, lat, lon) 

1041 dist = distance_accurate50m_numpy(lat0, lon0, lat, lon) 

1042 n = num.cos(azi*d2r)*dist 

1043 e = num.sin(azi*d2r)*dist 

1044 return n, e 

1045 

1046 

1047_wgs84 = None 

1048 

1049 

1050def get_wgs84(): 

1051 global _wgs84 

1052 if _wgs84 is None: 

1053 from geographiclib.geodesic import Geodesic 

1054 _wgs84 = Geodesic.WGS84 

1055 

1056 return _wgs84 

1057 

1058 

1059def amap(n): 

1060 

1061 def wrap(f): 

1062 if n == 1: 

1063 @wraps(f) 

1064 def func(*args): 

1065 it = num.nditer(args + (None,)) 

1066 for ops in it: 

1067 ops[-1][...] = f(*ops[:-1]) 

1068 

1069 return it.operands[-1] 

1070 elif n == 2: 

1071 @wraps(f) 

1072 def func(*args): 

1073 it = num.nditer(args + (None, None)) 

1074 for ops in it: 

1075 ops[-2][...], ops[-1][...] = f(*ops[:-2]) 

1076 

1077 return it.operands[-2], it.operands[-1] 

1078 else: 

1079 raise ValueError('Cannot wrap %s' % f.__qualname__) 

1080 

1081 return func 

1082 return wrap 

1083 

1084 

1085@amap(2) 

1086def ne_to_latlon2(lat0, lon0, north_m, east_m): 

1087 wgs84 = get_wgs84() 

1088 az = num.arctan2(east_m, north_m)*r2d 

1089 dist = num.sqrt(east_m**2 + north_m**2) 

1090 x = wgs84.Direct(lat0, lon0, az, dist) 

1091 return x['lat2'], x['lon2'] 

1092 

1093 

1094@amap(2) 

1095def latlon_to_ne2(lat0, lon0, lat1, lon1): 

1096 wgs84 = get_wgs84() 

1097 x = wgs84.Inverse(lat0, lon0, lat1, lon1) 

1098 dist = x['s12'] 

1099 az = x['azi1'] 

1100 n = num.cos(az*d2r)*dist 

1101 e = num.sin(az*d2r)*dist 

1102 return n, e 

1103 

1104 

1105@amap(1) 

1106def distance_accurate15nm(lat1, lon1, lat2, lon2): 

1107 wgs84 = get_wgs84() 

1108 return wgs84.Inverse(lat1, lon1, lat2, lon2)['s12'] 

1109 

1110 

1111def positive_region(region): 

1112 ''' 

1113 Normalize parameterization of a rectangular geographical region. 

1114 

1115 :param region: ``(west, east, south, north)`` in [deg]. 

1116 :type region: :py:class:`tuple` of :py:class:`float` 

1117 

1118 :returns: ``(west, east, south, north)``, where ``west <= east`` and 

1119 where ``west`` and ``east`` are in the range ``[-180., 180.+360.]``. 

1120 :rtype: :py:class:`tuple` of :py:class:`float` 

1121 ''' 

1122 west, east, south, north = [float(x) for x in region] 

1123 

1124 assert -180. - 360. <= west < 180. 

1125 assert -180. < east <= 180. + 360. 

1126 assert -90. <= south < 90. 

1127 assert -90. < north <= 90. 

1128 

1129 if east < west: 

1130 east += 360. 

1131 

1132 if west < -180.: 

1133 west += 360. 

1134 east += 360. 

1135 

1136 return (west, east, south, north) 

1137 

1138 

1139def points_in_region(p, region): 

1140 ''' 

1141 Check what points are contained in a rectangular geographical region. 

1142 

1143 :param p: ``(lat, lon)`` pairs in [deg]. 

1144 :type p: :py:class:`numpy.ndarray` ``(N, 2)`` 

1145 :param region: ``(west, east, south, north)`` region boundaries in [deg]. 

1146 :type region: :py:class:`tuple` of :py:class:`float` 

1147 

1148 :returns: Mask, returning ``True`` for each point within the region. 

1149 :rtype: :py:class:`numpy.ndarray` of bool, shape ``(N)`` 

1150 ''' 

1151 

1152 w, e, s, n = positive_region(region) 

1153 return num.logical_and( 

1154 num.logical_and(s <= p[:, 0], p[:, 0] <= n), 

1155 num.logical_or( 

1156 num.logical_and(w <= p[:, 1], p[:, 1] <= e), 

1157 num.logical_and(w-360. <= p[:, 1], p[:, 1] <= e-360.))) 

1158 

1159 

1160def point_in_region(p, region): 

1161 ''' 

1162 Check if a point is contained in a rectangular geographical region. 

1163 

1164 :param p: ``(lat, lon)`` in [deg]. 

1165 :type p: :py:class:`tuple` of :py:class:`float` 

1166 :param region: ``(west, east, south, north)`` region boundaries in [deg]. 

1167 :type region: :py:class:`tuple` of :py:class:`float` 

1168 

1169 :returns: ``True``, if point is in region, else ``False``. 

1170 :rtype: bool 

1171 ''' 

1172 

1173 w, e, s, n = positive_region(region) 

1174 return num.logical_and( 

1175 num.logical_and(s <= p[0], p[0] <= n), 

1176 num.logical_or( 

1177 num.logical_and(w <= p[1], p[1] <= e), 

1178 num.logical_and(w-360. <= p[1], p[1] <= e-360.))) 

1179 

1180 

1181def radius_to_region(lat, lon, radius): 

1182 ''' 

1183 Get a rectangular region which fully contains a given circular region. 

1184 

1185 :param lat: Latitude of the center point of circular region in [deg]. 

1186 :type lat: float 

1187 :param lon: Longitude of the center point of circular region in [deg]. 

1188 :type lon: float 

1189 :param radius: Radius of circular region in [m]. 

1190 :type radius: float 

1191 

1192 :returns: Rectangular region as ``(east, west, south, north)`` in [deg] or 

1193 ``None``. 

1194 :rtype: tuple[float, float, float, float] 

1195 ''' 

1196 radius_deg = radius * m2d 

1197 if radius_deg < 45.: 

1198 lat_min = max(-90., lat - radius_deg) 

1199 lat_max = min(90., lat + radius_deg) 

1200 absmaxlat = max(abs(lat_min), abs(lat_max)) 

1201 if absmaxlat > 89: 

1202 lon_min = -180. 

1203 lon_max = 180. 

1204 else: 

1205 lon_min = max( 

1206 -180. - 360., 

1207 lon - radius_deg / math.cos(absmaxlat*d2r)) 

1208 lon_max = min( 

1209 180. + 360., 

1210 lon + radius_deg / math.cos(absmaxlat*d2r)) 

1211 

1212 lon_min, lon_max, lat_min, lat_max = positive_region( 

1213 (lon_min, lon_max, lat_min, lat_max)) 

1214 

1215 return lon_min, lon_max, lat_min, lat_max 

1216 

1217 else: 

1218 return None 

1219 

1220 

1221def geographic_midpoint( 

1222 lats, lons, weights=None, depths=None, earthradius=earthradius): 

1223 

1224 ''' 

1225 Calculate geographic midpoints by finding the center of gravity. 

1226 

1227 This method suffers from instabilities if points are centered around the 

1228 poles. 

1229 

1230 :param lats: Latitudes in [deg]. 

1231 :param lons: Longitudes in [deg]. 

1232 :param weights: Weighting factors. 

1233 :param depths: Depths in [m]. 

1234 :type lats: :py:class:`numpy.ndarray`, ``(N)`` 

1235 :type lons: :py:class:`numpy.ndarray`, ``(N)`` 

1236 :type weights: optional, :py:class:`numpy.ndarray`, ``(N)`` 

1237 :type depths: optional, :py:class:`numpy.ndarray`, ``(N)`` 

1238 

1239 :return: Latitudes and longitudes of the midpoint in [deg] (and depth [m] 

1240 if depths are given). 

1241 :rtype: ``(lat, lon)`` or ``(lat, lon, depth)`` 

1242 ''' 

1243 if not weights: 

1244 weights = num.ones(len(lats)) 

1245 

1246 total_weigth = num.sum(weights) 

1247 weights /= total_weigth 

1248 lats = lats * d2r 

1249 lons = lons * d2r 

1250 if depths is not None: 

1251 radii = (earthradius - depths) / earthradius 

1252 else: 

1253 radii = 1.0 

1254 

1255 x = num.sum(num.cos(lats) * num.cos(lons) * weights * radii) 

1256 y = num.sum(num.cos(lats) * num.sin(lons) * weights * radii) 

1257 z = num.sum(num.sin(lats) * weights * radii) 

1258 

1259 lon = num.arctan2(y, x) 

1260 hyp = num.sqrt(x**2 + y**2) 

1261 lat = num.arctan2(z, hyp) 

1262 depth = earthradius - num.sqrt(x**2 + y**2 + z**2) * earthradius 

1263 

1264 if depths is None: 

1265 return lat/d2r, lon/d2r 

1266 else: 

1267 return lat/d2r, lon/d2r, depth 

1268 

1269 

1270def geographic_midpoint_locations( 

1271 locations, weights=None, include_depth=False): 

1272 

1273 if not include_depth: 

1274 coords = num.array([loc.effective_latlon 

1275 for loc in locations]) 

1276 return geographic_midpoint( 

1277 coords[:, 0], coords[:, 1], weights) 

1278 else: 

1279 coords = num.array([loc.effective_latlon + (loc.depth,) 

1280 for loc in locations]) 

1281 return geographic_midpoint( 

1282 coords[:, 0], coords[:, 1], weights=weights, depths=coords[:, 2]) 

1283 

1284 

1285def geodetic_to_ecef(lat, lon, alt): 

1286 ''' 

1287 Convert geodetic coordinates to Earth-Centered, Earth-Fixed (ECEF) 

1288 Cartesian coordinates. [#1]_ [#2]_ 

1289 

1290 :param lat: Geodetic latitude in [deg]. 

1291 :param lon: Geodetic longitude in [deg]. 

1292 :param alt: Geodetic altitude (height) in [m] (positive for points outside 

1293 the geoid). 

1294 :type lat: float 

1295 :type lon: float 

1296 :type alt: float 

1297 

1298 :return: ECEF Cartesian coordinates (X, Y, Z) in [m]. 

1299 :rtype: tuple[float, float, float] 

1300 

1301 .. [#1] https://en.wikipedia.org/wiki/ECEF 

1302 .. [#2] https://en.wikipedia.org/wiki/Geographic_coordinate_conversion 

1303 #From_geodetic_to_ECEF_coordinates 

1304 ''' 

1305 

1306 f = earth_oblateness 

1307 a = earthradius_equator 

1308 e2 = 2*f - f**2 

1309 

1310 lat, lon = num.radians(lat), num.radians(lon) 

1311 # Normal (plumb line) 

1312 N = a / num.sqrt(1.0 - (e2 * num.sin(lat)**2)) 

1313 

1314 X = (N+alt) * num.cos(lat) * num.cos(lon) 

1315 Y = (N+alt) * num.cos(lat) * num.sin(lon) 

1316 Z = (N*(1.0-e2) + alt) * num.sin(lat) 

1317 

1318 return (X, Y, Z) 

1319 

1320 

1321def ecef_to_geodetic(X, Y, Z): 

1322 ''' 

1323 Convert Earth-Centered, Earth-Fixed (ECEF) Cartesian coordinates to 

1324 geodetic coordinates (Ferrari's solution). 

1325 

1326 :param X: ECEF X coordinate [m]. 

1327 :type X: float 

1328 :param Y: ECEF Y coordinate [m]. 

1329 :type Y: float 

1330 :param Z: ECEF Z coordinate [m]. 

1331 :type Z: float 

1332 

1333 :return: Geodetic coordinates (lat, lon, alt). Latitude and longitude are 

1334 in [deg] and altitude is in [m] 

1335 (positive for points outside the geoid). 

1336 :rtype: :py:class:`tuple` of :py:class:`float` 

1337 

1338 .. seealso :: 

1339 https://en.wikipedia.org/wiki/Geographic_coordinate_conversion 

1340 #The_application_of_Ferrari.27s_solution 

1341 ''' 

1342 f = earth_oblateness 

1343 a = earthradius_equator 

1344 b = a * (1. - f) 

1345 e2 = 2.*f - f**2 

1346 

1347 # usefull 

1348 a2 = a**2 

1349 b2 = b**2 

1350 e4 = e2**2 

1351 X2 = X**2 

1352 Y2 = Y**2 

1353 Z2 = Z**2 

1354 

1355 r = num.sqrt(X2 + Y2) 

1356 r2 = r**2 

1357 

1358 e_prime2 = (a2 - b2)/b2 

1359 E2 = a2 - b2 

1360 F = 54. * b2 * Z2 

1361 G = r2 + (1.-e2)*Z2 - (e2*E2) 

1362 C = (e4 * F * r2) / (G**3) 

1363 S = cbrt(1. + C + num.sqrt(C**2 + 2.*C)) 

1364 P = F / (3. * (S + 1./S + 1.)**2 * G**2) 

1365 Q = num.sqrt(1. + (2.*e4*P)) 

1366 

1367 dum1 = -(P*e2*r) / (1.+Q) 

1368 dum2 = 0.5 * a2 * (1. + 1./Q) 

1369 dum3 = (P * (1.-e2) * Z2) / (Q * (1.+Q)) 

1370 dum4 = 0.5 * P * r2 

1371 r0 = dum1 + num.sqrt(dum2 - dum3 - dum4) 

1372 

1373 U = num.sqrt((r - e2*r0)**2 + Z2) 

1374 V = num.sqrt((r - e2*r0)**2 + (1.-e2)*Z2) 

1375 Z0 = (b2*Z) / (a*V) 

1376 

1377 alt = U * (1. - (b2 / (a*V))) 

1378 lat = num.arctan((Z + e_prime2 * Z0)/r) 

1379 lon = num.arctan2(Y, X) 

1380 

1381 return (lat*r2d, lon*r2d, alt) 

1382 

1383 

1384class Farside(Exception): 

1385 pass 

1386 

1387 

1388def latlon_to_xyz(latlons): 

1389 if latlons.ndim == 1: 

1390 return latlon_to_xyz(latlons[num.newaxis, :])[0] 

1391 

1392 points = num.zeros((latlons.shape[0], 3)) 

1393 lats = latlons[:, 0] 

1394 lons = latlons[:, 1] 

1395 points[:, 0] = num.cos(lats*d2r) * num.cos(lons*d2r) 

1396 points[:, 1] = num.cos(lats*d2r) * num.sin(lons*d2r) 

1397 points[:, 2] = num.sin(lats*d2r) 

1398 return points 

1399 

1400 

1401def xyz_to_latlon(xyz): 

1402 if xyz.ndim == 1: 

1403 return xyz_to_latlon(xyz[num.newaxis, :])[0] 

1404 

1405 latlons = num.zeros((xyz.shape[0], 2)) 

1406 latlons[:, 0] = num.arctan2( 

1407 xyz[:, 2], num.sqrt(xyz[:, 0]**2 + xyz[:, 1]**2)) * r2d 

1408 latlons[:, 1] = num.arctan2( 

1409 xyz[:, 1], xyz[:, 0]) * r2d 

1410 return latlons 

1411 

1412 

1413def rot_to_00(lat, lon): 

1414 rot0 = euler_to_matrix(0., -90.*d2r, 0.) 

1415 rot1 = euler_to_matrix(-d2r*lat, 0., -d2r*lon) 

1416 return num.dot(rot0.T, num.dot(rot1, rot0)).T 

1417 

1418 

1419def distances3d(a, b): 

1420 return num.sqrt(num.sum((a-b)**2, axis=a.ndim-1)) 

1421 

1422 

1423def circulation(points2): 

1424 return num.sum( 

1425 (points2[1:, 0] - points2[:-1, 0]) 

1426 * (points2[1:, 1] + points2[:-1, 1])) 

1427 

1428 

1429def stereographic(points): 

1430 dists = distances3d(points[1:, :], points[:-1, :]) 

1431 if dists.size > 0: 

1432 maxdist = num.max(dists) 

1433 cutoff = maxdist**2 / 2. 

1434 else: 

1435 cutoff = 1.0e-5 

1436 

1437 points = points.copy() 

1438 if num.any(points[:, 0] < -1. + cutoff): 

1439 raise Farside() 

1440 

1441 points_out = points[:, 1:].copy() 

1442 factor = 1.0 / (1.0 + points[:, 0]) 

1443 points_out *= factor[:, num.newaxis] 

1444 

1445 return points_out 

1446 

1447 

1448def stereographic_poly(points): 

1449 dists = distances3d(points[1:, :], points[:-1, :]) 

1450 if dists.size > 0: 

1451 maxdist = num.max(dists) 

1452 cutoff = maxdist**2 / 2. 

1453 else: 

1454 cutoff = 1.0e-5 

1455 

1456 points = points.copy() 

1457 if num.any(points[:, 0] < -1. + cutoff): 

1458 raise Farside() 

1459 

1460 points_out = points[:, 1:].copy() 

1461 factor = 1.0 / (1.0 + points[:, 0]) 

1462 points_out *= factor[:, num.newaxis] 

1463 

1464 if circulation(points_out) >= 0: 

1465 raise Farside() 

1466 

1467 return points_out 

1468 

1469 

1470def gnomonic_x(points, cutoff=0.01): 

1471 points_out = points[:, 1:].copy() 

1472 if num.any(points[:, 0] < cutoff): 

1473 raise Farside() 

1474 

1475 factor = 1.0 / points[:, 0] 

1476 points_out *= factor[:, num.newaxis] 

1477 return points_out 

1478 

1479 

1480def cneg(i, x): 

1481 if i == 1: 

1482 return x 

1483 else: 

1484 return num.logical_not(x) 

1485 

1486 

1487def contains_points(polygon, points): 

1488 ''' 

1489 Test which points are inside polygon on a sphere. 

1490 

1491 The inside of the polygon is defined as the area which is to the left hand 

1492 side of an observer walking the polygon line, points in order, on the 

1493 sphere. Lines between the polygon points are treated as great circle paths. 

1494 The polygon may be arbitrarily complex, as long as it does not have any 

1495 crossings or thin parts with zero width. The polygon may contain the poles 

1496 and is allowed to wrap around the sphere multiple times. 

1497 

1498 The algorithm works by consecutive cutting of the polygon into (almost) 

1499 hemispheres and subsequent Gnomonic projections to perform the 

1500 point-in-polygon tests on a 2D plane. 

1501 

1502 :param polygon: Point coordinates defining the polygon [deg]. 

1503 :type polygon: :py:class:`numpy.ndarray` of shape ``(N, 2)``, second index 

1504 0=lat, 1=lon 

1505 :param points: Coordinates of points to test [deg]. 

1506 :type points: :py:class:`numpy.ndarray` of shape ``(N, 2)``, second index 

1507 0=lat, 1=lon 

1508 

1509 :returns: Boolean mask array. 

1510 :rtype: :py:class:`numpy.ndarray` of shape ``(N,)``. 

1511 ''' 

1512 

1513 and_ = num.logical_and 

1514 

1515 points_xyz = latlon_to_xyz(points) 

1516 mask_x = 0. <= points_xyz[:, 0] 

1517 mask_y = 0. <= points_xyz[:, 1] 

1518 mask_z = 0. <= points_xyz[:, 2] 

1519 

1520 result = num.zeros(points.shape[0], dtype=int) 

1521 

1522 for ix in [-1, 1]: 

1523 for iy in [-1, 1]: 

1524 for iz in [-1, 1]: 

1525 mask = and_( 

1526 and_(cneg(ix, mask_x), cneg(iy, mask_y)), 

1527 cneg(iz, mask_z)) 

1528 

1529 center_xyz = num.array([ix, iy, iz], dtype=float) 

1530 

1531 lat, lon = xyz_to_latlon(center_xyz) 

1532 rot = rot_to_00(lat, lon) 

1533 

1534 points_rot_xyz = num.dot(rot, points_xyz[mask, :].T).T 

1535 points_rot_pro = gnomonic_x(points_rot_xyz) 

1536 

1537 offset = 0.01 

1538 

1539 poly_xyz = latlon_to_xyz(polygon) 

1540 poly_rot_xyz = num.dot(rot, poly_xyz.T).T 

1541 poly_rot_xyz[:, 0] -= offset 

1542 groups = spoly_cut([poly_rot_xyz], axis=0) 

1543 

1544 for poly_rot_group_xyz in groups[1]: 

1545 poly_rot_group_xyz[:, 0] += offset 

1546 

1547 poly_rot_group_pro = gnomonic_x( 

1548 poly_rot_group_xyz) 

1549 

1550 if circulation(poly_rot_group_pro) > 0: 

1551 result[mask] += path_contains_points( 

1552 poly_rot_group_pro, points_rot_pro) 

1553 else: 

1554 result[mask] -= path_contains_points( 

1555 poly_rot_group_pro, points_rot_pro) 

1556 

1557 return result.astype(bool) 

1558 

1559 

1560def contains_point(polygon, point): 

1561 ''' 

1562 Test if point is inside polygon on a sphere. 

1563 

1564 Convenience wrapper to :py:func:`contains_points` to test a single point. 

1565 

1566 :param polygon: Point coordinates defining the polygon [deg]. 

1567 :type polygon: :py:class:`numpy.ndarray` of shape ``(N, 2)``, second index 

1568 0=lat, 1=lon 

1569 :param point: Coordinates ``(lat, lon)`` of point to test [deg]. 

1570 :type point: :py:class:`tuple` of :py:class:`float` 

1571 

1572 :returns: ``True``, if point is located within polygon, else ``False``. 

1573 :rtype: bool 

1574 ''' 

1575 

1576 return bool( 

1577 contains_points(polygon, num.asarray(point)[num.newaxis, :])[0])