Coverage for pyguymer3/geo/_add_topDown_axis.py: 1%

83 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-08 18:47 +0000

1#!/usr/bin/env python3 

2 

3# Define function ... 

4def _add_topDown_axis( 

5 fg, 

6 lon, 

7 lat, 

8 /, 

9 *, 

10 add_coastlines = True, 

11 add_gridlines = True, 

12 coastlines_edgecolor = "black", 

13 coastlines_facecolor = "none", 

14 coastlines_levels = None, 

15 coastlines_linestyle = "solid", 

16 coastlines_linewidth = 0.5, 

17 coastlines_resolution = "i", 

18 coastlines_zorder = 1.5, 

19 configureAgain = False, 

20 debug = __debug__, 

21 dist = 1.0e99, 

22 eps = 1.0e-12, 

23 gridlines_int = None, 

24 gridlines_linecolor = "black", 

25 gridlines_linestyle = ":", 

26 gridlines_linewidth = 0.5, 

27 gridlines_zorder = 2.0, 

28 gs = None, 

29 index = None, 

30 ncols = None, 

31 nIter = 100, 

32 nrows = None, 

33 onlyValid = False, 

34 prefix = ".", 

35 ramLimit = 1073741824, 

36 repair = False, 

37 satellite_height = False, 

38 tol = 1.0e-10, 

39): 

40 """Add an Orthographic axis centred above a point with optionally a 

41 field-of-view based on a circle around the point on the surface of the Earth 

42 

43 Parameters 

44 ---------- 

45 fg : matplotlib.figure.Figure 

46 the figure to add the axis to 

47 lon : float 

48 the longitude of the point (in degrees) 

49 lat : float 

50 the latitude of the point (in degrees) 

51 add_coastlines : bool, optional 

52 add coastline boundaries 

53 add_gridlines : bool, optional 

54 add gridlines of longitude and latitude 

55 coastlines_edgecolor : str, optional 

56 the colour of the edges of the coastline Polygons 

57 coastlines_facecolor : str, optional 

58 the colour of the faces of the coastline Polygons 

59 coastlines_levels : list of int, optional 

60 the levels of the coastline boundaries (if None then default to 

61 ``[1, 6]``) 

62 coastlines_linestyle : str, optional 

63 the linestyle to draw the coastline boundaries with 

64 coastlines_linewidth : float, optional 

65 the linewidth to draw the coastline boundaries with 

66 coastlines_resolution : str, optional 

67 the resolution of the coastline boundaries 

68 coastlines_zorder : float, optional 

69 the zorder to draw the coastline boundaries with (the default value has 

70 been chosen to match the value that it ends up being if the coastline 

71 boundaries are not drawn with the zorder keyword specified -- obtained 

72 by manual inspection on 5/Dec/2023) 

73 configureAgain : bool, optional 

74 configure the axis a second time (this is a hack to make narrow 

75 field-of-view top-down axes work correctly with OpenStreetMap tiles) 

76 debug : bool, optional 

77 print debug messages and draw the circle on the axis 

78 dist : float, optional 

79 the radius of the circle around the point, if larger than the Earth then 

80 make the axis of global extent (in metres) 

81 eps : float, optional 

82 the tolerance of the Vincenty formula iterations 

83 gridlines_int : int, optional 

84 the interval between gridlines, best results if ``90 % gridlines_int == 0``; 

85 if the axis is of global extent then the default will be 45° else it 

86 will be 1° (in degrees) 

87 gridlines_linecolor : str, optional 

88 the colour of the gridlines 

89 gridlines_linestyle : str, optional 

90 the style of the gridlines 

91 gridlines_linewidth : float, optional 

92 the width of the gridlines 

93 gridlines_zorder : float, optional 

94 the zorder to draw the gridlines with (the default value has been chosen 

95 to match the value that it ends up being if the gridlines are not drawn 

96 with the zorder keyword specified -- obtained by manual inspection on 

97 5/Dec/2023) 

98 gs : matplotlib.gridspec.SubplotSpec, optional 

99 the subset of a gridspec to locate the axis 

100 index : int or tuple of int, optional 

101 the index of the axis in the array of axes 

102 ncols : int, optional 

103 the number of columns in the array of axes 

104 nIter : int, optional 

105 the maximum number of iterations (particularly the Vincenty formula) 

106 nrows : int, optional 

107 the number of rows in the array of axes 

108 onlyValid : bool, optional 

109 only return valid Polygons (checks for validity can take a while, if 

110 being called often) 

111 prefix : str, optional 

112 change the name of the output debugging CSVs 

113 ramLimit : int, optional 

114 the maximum RAM usage of each "large" array (in bytes) 

115 repair : bool, optional 

116 attempt to repair invalid Polygons 

117 satellite_height : bool, optional 

118 if a distance is provided then use a "NearsidePerspective" projection at 

119 an altitude which has the same field-of-view as the distance 

120 tol : float, optional 

121 the Euclidean distance that defines two points as being the same (in 

122 degrees) 

123 

124 Returns 

125 ------- 

126 ax : cartopy.mpl.geoaxes.GeoAxesSubplot 

127 the axis 

128 

129 Notes 

130 ----- 

131 There are two arguments relating to the `Global Self-Consistent Hierarchical 

132 High-Resolution Geography dataset <https://www.ngdc.noaa.gov/mgg/shorelines/>`_ : 

133 

134 * *coastlines_levels*; and 

135 * *coastlines_resolution*. 

136 

137 There are six levels to choose from: 

138 

139 * boundary between land and ocean (1); 

140 * boundary between lake and land (2); 

141 * boundary between island-in-lake and lake (3); 

142 * boundary between pond-in-island and island-in-lake (4); 

143 * boundary between Antarctica ice and ocean (5); and 

144 * boundary between Antarctica grounding-line and ocean (6). 

145 

146 There are five resolutions to choose from: 

147 

148 * crude ("c"); 

149 * low ("l"); 

150 * intermediate ("i"); 

151 * high ("h"); and 

152 * full ("f"). 

153 

154 Copyright 2017 Thomas Guymer [1]_ 

155 

156 References 

157 ---------- 

158 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3 

159 """ 

160 

161 # Import standard modules ... 

162 import math 

163 import os 

164 

165 # Import special modules ... 

166 try: 

167 import cartopy 

168 cartopy.config.update( 

169 { 

170 "cache_dir" : os.path.expanduser("~/.local/share/cartopy_cache"), 

171 } 

172 ) 

173 except: 

174 raise Exception("\"cartopy\" is not installed; run \"pip install --user Cartopy\"") from None 

175 try: 

176 import matplotlib 

177 matplotlib.rcParams.update( 

178 { 

179 "backend" : "Agg", # NOTE: See https://matplotlib.org/stable/gallery/user_interfaces/canvasagg.html 

180 "figure.dpi" : 300, 

181 "figure.figsize" : (9.6, 7.2), # NOTE: See https://github.com/Guymer/misc/blob/main/README.md#matplotlib-figure-sizes 

182 "font.size" : 8, 

183 } 

184 ) 

185 import matplotlib.pyplot 

186 except: 

187 raise Exception("\"matplotlib\" is not installed; run \"pip install --user matplotlib\"") from None 

188 try: 

189 import numpy 

190 except: 

191 raise Exception("\"numpy\" is not installed; run \"pip install --user numpy\"") from None 

192 try: 

193 import shapely 

194 import shapely.geometry 

195 except: 

196 raise Exception("\"shapely\" is not installed; run \"pip install --user Shapely\"") from None 

197 

198 # Import sub-functions ... 

199 from ._add_coastlines import _add_coastlines 

200 from ._add_horizontal_gridlines import _add_horizontal_gridlines 

201 from ._add_vertical_gridlines import _add_vertical_gridlines 

202 from .buffer import buffer 

203 from .calc_loc_from_loc_and_bearing_and_dist import calc_loc_from_loc_and_bearing_and_dist 

204 from .clean import clean 

205 from ..consts import CIRCUMFERENCE_OF_EARTH, RADIUS_OF_EARTH 

206 

207 # ************************************************************************** 

208 

209 # Create short-hand ... 

210 globalFieldOfView = bool(dist > 0.25 * CIRCUMFERENCE_OF_EARTH) 

211 

212 # Check inputs ... 

213 if gridlines_int is None: 

214 if globalFieldOfView: 

215 gridlines_int = 45 # [°] 

216 else: 

217 gridlines_int = 1 # [°] 

218 if not globalFieldOfView and satellite_height: 

219 alt = RADIUS_OF_EARTH / math.cos(dist / RADIUS_OF_EARTH) - RADIUS_OF_EARTH # [m] 

220 

221 # Create a Point ... 

222 point1 = shapely.geometry.point.Point(lon, lat) 

223 

224 # Check where the axis should be created ... 

225 # NOTE: See https://scitools.org.uk/cartopy/docs/latest/reference/projections.html 

226 if gs is not None: 

227 # Check if a NearsidePerspective axis can be used ... 

228 if not globalFieldOfView and satellite_height: 

229 # Create NearsidePerspective axis ... 

230 ax = fg.add_subplot( 

231 gs, 

232 projection = cartopy.crs.NearsidePerspective( 

233 central_longitude = point1.x, 

234 central_latitude = point1.y, 

235 satellite_height = alt, 

236 ), 

237 ) 

238 else: 

239 # Create Orthographic axis ... 

240 ax = fg.add_subplot( 

241 gs, 

242 projection = cartopy.crs.Orthographic( 

243 central_longitude = point1.x, 

244 central_latitude = point1.y, 

245 ), 

246 ) 

247 elif nrows is not None and ncols is not None and index is not None: 

248 # Check if a NearsidePerspective axis can be used ... 

249 if not globalFieldOfView and satellite_height: 

250 # Create NearsidePerspective axis ... 

251 ax = fg.add_subplot( 

252 nrows, 

253 ncols, 

254 index, 

255 projection = cartopy.crs.NearsidePerspective( 

256 central_longitude = point1.x, 

257 central_latitude = point1.y, 

258 satellite_height = alt, 

259 ), 

260 ) 

261 else: 

262 # Create Orthographic axis ... 

263 ax = fg.add_subplot( 

264 nrows, 

265 ncols, 

266 index, 

267 projection = cartopy.crs.Orthographic( 

268 central_longitude = point1.x, 

269 central_latitude = point1.y, 

270 ), 

271 ) 

272 else: 

273 # Check if a NearsidePerspective axis can be used ... 

274 if not globalFieldOfView and satellite_height: 

275 # Create NearsidePerspective axis ... 

276 ax = fg.add_subplot( 

277 projection = cartopy.crs.NearsidePerspective( 

278 central_longitude = point1.x, 

279 central_latitude = point1.y, 

280 satellite_height = alt, 

281 ), 

282 ) 

283 else: 

284 # Create Orthographic axis ... 

285 ax = fg.add_subplot( 

286 projection = cartopy.crs.Orthographic( 

287 central_longitude = point1.x, 

288 central_latitude = point1.y, 

289 ), 

290 ) 

291 

292 # Check if the field-of-view is too large ... 

293 if globalFieldOfView: 

294 # Configure axis ... 

295 ax.set_global() 

296 elif not satellite_height: 

297 # Buffer the Point ... 

298 polygon1 = buffer( 

299 point1, 

300 dist, 

301 debug = debug, 

302 eps = eps, 

303 fill = +1.0, 

304 fillSpace = "EuclideanSpace", 

305 keepInteriors = False, 

306 nAng = 361, 

307 nIter = nIter, 

308 prefix = prefix, 

309 ramLimit = ramLimit, 

310 simp = -1.0, 

311 tol = tol, 

312 ) 

313 

314 # Calculate Northern extent ... 

315 tmpLon, latMax, _ = calc_loc_from_loc_and_bearing_and_dist( 

316 lon, 

317 lat, 

318 0.0, 

319 dist, 

320 eps = 1.0e-12, 

321 nIter = 100, 

322 ) # [°], [°] 

323 tmpPnt = shapely.geometry.point.Point(tmpLon, latMax) 

324 yMax = ax.projection.project_geometry(tmpPnt).y # [?] 

325 

326 # Calculate Eastern extent ... 

327 lonIter, tmpLat, _ = calc_loc_from_loc_and_bearing_and_dist( 

328 lon, 

329 lat, 

330 90.0, 

331 dist, 

332 eps = 1.0e-12, 

333 nIter = 100, 

334 ) # [°], [°] 

335 tmpPnt = shapely.geometry.point.Point(lonIter, tmpLat) 

336 xMax = ax.projection.project_geometry(tmpPnt).x # [?] 

337 

338 # Calculate Southern extent ... 

339 tmpLon, latMin, _ = calc_loc_from_loc_and_bearing_and_dist( 

340 lon, 

341 lat, 

342 180.0, 

343 dist, 

344 eps = 1.0e-12, 

345 nIter = 100, 

346 ) # [°], [°] 

347 tmpPnt = shapely.geometry.point.Point(tmpLon, latMin) 

348 yMin = ax.projection.project_geometry(tmpPnt).y # [?] 

349 

350 # Calculate Western extent ... 

351 lonMin, tmpLat, _ = calc_loc_from_loc_and_bearing_and_dist( 

352 lon, 

353 lat, 

354 270.0, 

355 dist, 

356 eps = 1.0e-12, 

357 nIter = 100, 

358 ) # [°], [°] 

359 tmpPnt = shapely.geometry.point.Point(lonMin, tmpLat) 

360 xMin = ax.projection.project_geometry(tmpPnt).x # [?] 

361 

362 # Project the Point ... 

363 point2 = ax.projection.project_geometry(point1) 

364 

365 # Create a correctly oriented Polygon from scratch that is the Point 

366 # buffered in MatPlotLib space with the same fuzziness as Cartopy does 

367 # internally ... 

368 radius2 = numpy.array( 

369 [ 

370 point2.x - xMin, 

371 xMax - point2.x, 

372 point2.y - yMin, 

373 yMax - point2.y, 

374 ], 

375 dtype = numpy.float64, 

376 ).mean() # [?] 

377 polygon2 = point2.buffer(radius2 * 0.99999) 

378 polygon2 = clean( 

379 polygon2, 

380 debug = debug, 

381 prefix = prefix, 

382 tol = tol, 

383 ) 

384 

385 # Convert the exterior ring of the Polygon to a Path ... 

386 path = matplotlib.path.Path(polygon2.exterior.coords) 

387 

388 # Configure axis ... 

389 # NOTE: The orthographic projection does not have the ability to set 

390 # either the altitude or the field-of-view. I manually do this, 

391 # which involves setting the boundary and the limits for the 

392 # MatPlotLib axis. 

393 ax.set_boundary(path) 

394 ax.set_xlim(xMin, xMax) 

395 ax.set_ylim(yMin, yMax) 

396 

397 # Check if the user wants to configure the axis a second time ... 

398 if configureAgain: 

399 # Configure axis again ... 

400 # NOTE: For some reason, "cartopy.io.img_tiles.OSM()" doesn't work 

401 # unless the first of the following protected members is also 

402 # set. All other interactions with the axis appear to be fine 

403 # without it being set though. Annoyingly, once the first 

404 # protected member is set, all other interactions with the 

405 # axis fail unless the second and third protected members are 

406 # also set. If the second and third protected members are set 

407 # then the resulting gridlines do not extend all the way to 

408 # the edge of the map. Therefore, I have chosen to not set the 

409 # second and third protected members and instead I protect 

410 # setting the first protect member by checking if the 

411 # background is going to be "OSM". 

412 ax.projection._boundary = polygon2.exterior # pylint: disable=W0212 

413 # ax.projection._cw_boundary = polygon2.exterior.reverse() 

414 # ax.projection._ccw_boundary = polygon2.exterior 

415 

416 # Check if the user wants to draw the circle ... 

417 if debug: 

418 # Draw the circle ... 

419 ax.add_geometries( 

420 [polygon1], 

421 cartopy.crs.PlateCarree(), 

422 edgecolor = (0.0, 0.0, 1.0, 1.0), 

423 facecolor = (0.0, 0.0, 1.0, 0.5), 

424 linewidth = 1.0, 

425 ) 

426 

427 # Check if the user wants to add coastline boundaries ... 

428 if add_coastlines: 

429 # Add coastline boundaries ... 

430 _add_coastlines( 

431 ax, 

432 debug = debug, 

433 edgecolor = coastlines_edgecolor, 

434 facecolor = coastlines_facecolor, 

435 levels = coastlines_levels, 

436 linestyle = coastlines_linestyle, 

437 linewidth = coastlines_linewidth, 

438 onlyValid = onlyValid, 

439 repair = repair, 

440 resolution = coastlines_resolution, 

441 zorder = coastlines_zorder, 

442 ) 

443 

444 # Check if the user wants to add gridlines ... 

445 if add_gridlines: 

446 # Add gridlines ... 

447 _add_horizontal_gridlines( 

448 ax, 

449 color = gridlines_linecolor, 

450 linestyle = gridlines_linestyle, 

451 linewidth = gridlines_linewidth, 

452 locs = range( -90, +90 + gridlines_int, gridlines_int), 

453 ngrid = -1, 

454 npoint = 3601, 

455 zorder = gridlines_zorder, 

456 ) 

457 _add_vertical_gridlines( 

458 ax, 

459 color = gridlines_linecolor, 

460 linestyle = gridlines_linestyle, 

461 linewidth = gridlines_linewidth, 

462 locs = range(-180, +180 + gridlines_int, gridlines_int), 

463 ngrid = -1, 

464 npoint = 1801, 

465 zorder = gridlines_zorder, 

466 ) 

467 

468 # Return answer ... 

469 return ax