matplotlib-map-utils 2.0.2__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,952 @@
1
+ ############################################################
2
+ # inset_map.py contains all the main objects and functions
3
+ # for creating the inset map axis and its indicators
4
+ ############################################################
5
+
6
+ ### IMPORTING PACKAGES ###
7
+
8
+ # Default packages
9
+ import warnings
10
+ import copy
11
+ # Math packages
12
+ import numpy
13
+ # Geo packages
14
+ import pyproj
15
+ import shapely
16
+ # Graphical packages
17
+ import matplotlib
18
+ import matplotlib.artist
19
+ import matplotlib.patches
20
+ import matplotlib.colors
21
+ # The types we use in this script
22
+ from typing import Literal
23
+ # The information contained in our helper scripts (validation and defaults)
24
+ from ..defaults import inset_map as imd
25
+ from ..validation import inset_map as imt
26
+ from ..validation import functions as imf
27
+
28
+ ### INITIALIZATION ###
29
+
30
+ # Setting the defaults to the "medium" size, which is roughly optimized for A4/Letter paper
31
+ # Making these as globals is important for the set_size() function to work later
32
+ _DEFAULT_INSET_MAP = imd._DEFAULTS_IM["md"][0]
33
+
34
+ ### CLASSES ###
35
+ # Note these are really just to be convenient when storing the
36
+ # configuration options that are used by the drawing functions instead
37
+
38
+ # The main object model of the inset map
39
+ class InsetMap(matplotlib.artist.Artist):
40
+
41
+ ## INITIALIZATION ##
42
+ def __init__(self,
43
+ location: Literal["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]="lower left",
44
+ size: imt._TYPE_INSET["size"]=None,
45
+ pad: imt._TYPE_INSET["pad"]=None,
46
+ coords: imt._TYPE_INSET["coords"]=None,
47
+ transform=None,
48
+ to_plot=None,
49
+ **kwargs):
50
+ # Starting up the object with the base properties of a matplotlib Artist
51
+ matplotlib.artist.Artist.__init__(self)
52
+
53
+ # Validating each of the passed parameters
54
+ self._location = imf._validate(imt._VALIDATE_INSET, "location", location)
55
+ self._size = imf._validate(imt._VALIDATE_INSET, "size", size)
56
+ self._pad = imf._validate(imt._VALIDATE_INSET, "pad", pad)
57
+ self._coords = imf._validate(imt._VALIDATE_INSET, "coords", coords)
58
+ self._to_plot = imf._validate(imt._VALIDATE_INSET, "to_plot", to_plot)
59
+
60
+ # Checking if we need to override values for size and pad
61
+ if self._size is None:
62
+ self._size = _DEFAULT_INSET_MAP["size"]
63
+ if self._pad is None:
64
+ self._pad = _DEFAULT_INSET_MAP["pad"]
65
+
66
+ self._transform = transform # not validated!
67
+ self._kwargs = kwargs # not validated!
68
+
69
+ # We do set the zorder for our objects individually,
70
+ # but we ALSO set it for the entire artist, here
71
+ # Thank you to matplotlib-scalebar for this tip
72
+ zorder = 99
73
+
74
+ ## INTERNAL PROPERTIES ##
75
+ # This allows for easy-updating of properties
76
+ # Each property will have the same pair of functions
77
+ # 1) calling the property itself returns its value (InsetMap.size will output (width,height))
78
+ # 2) passing a value will update it (InsetMap.size = (width,height) will update it)
79
+
80
+ # location/loc
81
+ @property
82
+ def location(self):
83
+ return self._location
84
+
85
+ @location.setter
86
+ def location(self, val: Literal["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]):
87
+ val = imf._validate(imt._VALIDATE_INSET, "location", val)
88
+ self._location = val
89
+
90
+ @property
91
+ def loc(self):
92
+ return self._location
93
+
94
+ @loc.setter
95
+ def loc(self, val: Literal["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]):
96
+ val = imf._validate(imt._VALIDATE_INSET, "location", val)
97
+ self._location = val
98
+
99
+ # size
100
+ @property
101
+ def size(self):
102
+ return self._size
103
+
104
+ @size.setter
105
+ def size(self, val):
106
+ val = imf._validate(imt._VALIDATE_INSET, "size", val)
107
+ if val is not None:
108
+ self._size = val
109
+ else:
110
+ self._size = _DEFAULT_INSET_MAP["size"]
111
+
112
+ # pad
113
+ @property
114
+ def pad(self):
115
+ return self._pad
116
+
117
+ @pad.setter
118
+ def pad(self, val):
119
+ val = imf._validate(imt._VALIDATE_INSET, "pad", val)
120
+ if val is not None:
121
+ self._pad = val
122
+ else:
123
+ self._pad = _DEFAULT_INSET_MAP["pad"]
124
+
125
+ # coords
126
+ @property
127
+ def coords(self):
128
+ return self._coords
129
+
130
+ @coords.setter
131
+ def coords(self, val):
132
+ val = imf._validate(imt._VALIDATE_INSET, "coords", val)
133
+ self._coords = val
134
+
135
+ # transform
136
+ @property
137
+ def transform(self):
138
+ return self._transform
139
+
140
+ @transform.setter
141
+ def transform(self, val):
142
+ self._transform = val
143
+
144
+ # kwargs
145
+ @property
146
+ def kwargs(self):
147
+ return self._kwargs
148
+
149
+ @kwargs.setter
150
+ def kwargs(self, val):
151
+ if isinstance(val, dict):
152
+ self._kwargs = self._kwargs | val
153
+ else:
154
+ raise ValueError("kwargs expects a dictionary, please try again")
155
+
156
+ # to_plot
157
+ @property
158
+ def to_plot(self):
159
+ return self._to_plot
160
+
161
+ @to_plot.setter
162
+ def to_plot(self, val):
163
+ val = imf._validate(imt._VALIDATE_INSET, "to_plot", val)
164
+ self._to_plot = val
165
+
166
+ ## COPY FUNCTION ##
167
+ # This is solely to get around matplotlib's restrictions around re-using an artist across multiple axes
168
+ # Instead, you can use add_artist() like normal, but with add_artist(na.copy())
169
+ # Thank you to the cartopy team for helping fix a bug with this!
170
+ def copy(self):
171
+ return copy.deepcopy(self)
172
+
173
+ ## CREATE FUNCTION ##
174
+ # Calling InsetMap.create(ax) will create an inset map with the specified parameters on the given axis
175
+ # Note that this is different than the way NorthArrows and ScaleBars are rendered (via draw/add_artist())!
176
+ def create(self, pax, **kwargs):
177
+ # Can re-use the drawing function we already established, but return the object instead
178
+ iax = inset_map(ax=pax, location=self._location, size=self._size,
179
+ pad=self._pad, coords=self._coords, transform=self._transform,
180
+ **self._kwargs, **kwargs)
181
+
182
+ # If data is passed to to_plot, then we plot that on the newly created axis as well
183
+ for d in self._to_plot:
184
+ if d is not None:
185
+ if "kwargs" in d.keys():
186
+ d["data"].plot(ax=iax, **d["kwargs"])
187
+ else:
188
+ d["data"].plot(ax=iax)
189
+ # Instead of "drawing", we have to return the axis, for further manipulation
190
+ return iax
191
+
192
+ ## SIZE FUNCTION ##
193
+ # This function will update the default dictionaries used based on the size of map being created
194
+ # See defaults.py for more information on the dictionaries used here
195
+ def set_size(size: Literal["xs","xsmall","x-small",
196
+ "sm","small",
197
+ "md","medium",
198
+ "lg","large",
199
+ "xl","xlarge","x-large"]):
200
+ # Bringing in our global default values to update them
201
+ global _DEFAULT_INSET_MAP
202
+ # Changing the global default values as required
203
+ if size.lower() in ["xs","xsmall","x-small"]:
204
+ _DEFAULT_INSET_MAP = imd._DEFAULTS_IM["xs"][0]
205
+ elif size.lower() in ["sm","small"]:
206
+ _DEFAULT_INSET_MAP = imd._DEFAULTS_IM["sm"][0]
207
+ elif size.lower() in ["md","medium"]:
208
+ _DEFAULT_INSET_MAP = imd._DEFAULTS_IM["md"][0]
209
+ elif size.lower() in ["lg","large"]:
210
+ _DEFAULT_INSET_MAP = imd._DEFAULTS_IM["lg"][0]
211
+ elif size.lower() in ["xl","xlarge","x-large"]:
212
+ _DEFAULT_INSET_MAP = imd._DEFAULTS_IM["xl"][0]
213
+ else:
214
+ raise ValueError("Invalid value supplied, try one of ['xsmall', 'small', 'medium', 'large', 'xlarge'] instead")
215
+
216
+ # The main object model of the extent indicator
217
+ class ExtentIndicator(matplotlib.artist.Artist):
218
+
219
+ ## INITIALIZATION ##
220
+ def __init__(self,
221
+ to_return: imt._TYPE_EXTENT["to_return"]=None,
222
+ straighten: imt._TYPE_EXTENT["straighten"]=True,
223
+ pad: imt._TYPE_EXTENT["pad"]=0.05,
224
+ plot: imt._TYPE_EXTENT["plot"]=True,
225
+ facecolor: imt._TYPE_EXTENT["facecolor"]="red",
226
+ linecolor: imt._TYPE_EXTENT["linecolor"]="red",
227
+ alpha: imt._TYPE_EXTENT["alpha"]=0.5,
228
+ linewidth: imt._TYPE_EXTENT["linewidth"]=1,
229
+ **kwargs):
230
+ # Starting up the object with the base properties of a matplotlib Artist
231
+ matplotlib.artist.Artist.__init__(self)
232
+
233
+ # Validating each of the passed parameters
234
+ self._to_return = imf._validate(imt._VALIDATE_EXTENT, "to_return", to_return)
235
+ self._straighten = imf._validate(imt._VALIDATE_EXTENT, "straighten", straighten)
236
+ self._pad = imf._validate(imt._VALIDATE_EXTENT, "pad", pad)
237
+ self._plot = imf._validate(imt._VALIDATE_EXTENT, "plot", plot)
238
+ self._facecolor = imf._validate(imt._VALIDATE_EXTENT, "facecolor", facecolor)
239
+ self._linecolor = imf._validate(imt._VALIDATE_EXTENT, "linecolor", linecolor)
240
+ self._alpha = imf._validate(imt._VALIDATE_EXTENT, "alpha", alpha)
241
+ self._linewidth = imf._validate(imt._VALIDATE_EXTENT, "linewidth", linewidth)
242
+
243
+ self._kwargs = kwargs # not validated!
244
+
245
+ # We do set the zorder for our objects individually,
246
+ # but we ALSO set it for the entire artist, here
247
+ # Thank you to matplotlib-scalebar for this tip
248
+ zorder = 99
249
+
250
+ ## INTERNAL PROPERTIES ##
251
+ # This allows for easy-updating of properties
252
+ # Each property will have the same pair of functions
253
+ # 1) calling the property itself returns its value (ExtentIndicator.facecolor will output color)
254
+ # 2) passing a value will update it (ExtentIndicator.facecolor = color will update it)
255
+
256
+ # to_return
257
+ @property
258
+ def to_return(self):
259
+ return self._to_return
260
+
261
+ @to_return.setter
262
+ def to_return(self, val):
263
+ val = imf._validate(imt._VALIDATE_EXTENT, "to_return", val)
264
+ self._to_return = val
265
+
266
+ # straighten
267
+ @property
268
+ def straighten(self):
269
+ return self._straighten
270
+
271
+ @straighten.setter
272
+ def straighten(self, val):
273
+ val = imf._validate(imt._VALIDATE_EXTENT, "straighten", val)
274
+ self._straighten = val
275
+
276
+ # pad
277
+ @property
278
+ def pad(self):
279
+ return self._pad
280
+
281
+ @pad.setter
282
+ def pad(self, val):
283
+ val = imf._validate(imt._VALIDATE_EXTENT, "pad", val)
284
+ self._pad = val
285
+
286
+ # plot
287
+ @property
288
+ def plot(self):
289
+ return self._plot
290
+
291
+ @plot.setter
292
+ def plot(self, val):
293
+ val = imf._validate(imt._VALIDATE_EXTENT, "plot", val)
294
+ self._plot = val
295
+
296
+ # facecolor
297
+ @property
298
+ def facecolor(self):
299
+ return self._facecolor
300
+
301
+ @facecolor.setter
302
+ def facecolor(self, val):
303
+ val = imf._validate(imt._VALIDATE_EXTENT, "facecolor", val)
304
+ self._facecolor = val
305
+
306
+ # linecolor
307
+ @property
308
+ def linecolor(self):
309
+ return self._linecolor
310
+
311
+ @linecolor.setter
312
+ def linecolor(self, val):
313
+ val = imf._validate(imt._VALIDATE_EXTENT, "linecolor", val)
314
+ self._linecolor = val
315
+
316
+ # alpha
317
+ @property
318
+ def alpha(self):
319
+ return self._alpha
320
+
321
+ @alpha.setter
322
+ def alpha(self, val):
323
+ val = imf._validate(imt._VALIDATE_EXTENT, "alpha", val)
324
+ self._alpha = val
325
+
326
+ # linewidth
327
+ @property
328
+ def linewidth(self):
329
+ return self._linewidth
330
+
331
+ @linewidth.setter
332
+ def linewidth(self, val):
333
+ val = imf._validate(imt._VALIDATE_EXTENT, "linewidth", val)
334
+ self._linewidth = val
335
+
336
+ # kwargs
337
+ @property
338
+ def kwargs(self):
339
+ return self._kwargs
340
+
341
+ @kwargs.setter
342
+ def kwargs(self, val):
343
+ if isinstance(val, dict):
344
+ self._kwargs = self._kwargs | val
345
+ else:
346
+ raise ValueError("kwargs expects a dictionary, please try again")
347
+
348
+ ## COPY FUNCTION ##
349
+ # This is solely to get around matplotlib's restrictions around re-using an artist across multiple axes
350
+ # Instead, you can use add_artist() like normal, but with add_artist(na.copy())
351
+ # Thank you to the cartopy team for helping fix a bug with this!
352
+ def copy(self):
353
+ return copy.deepcopy(self)
354
+
355
+ ## CREATE FUNCTION ##
356
+ # Calling ExtentIndicator.create(ax) will create an inset map with the specified parameters on the given axis
357
+ # Note that this is different than the way NorthArrows and ScaleBars are rendered (via draw/add_artist())!
358
+ def create(self,
359
+ pax: imt._TYPE_EXTENT["pax"],
360
+ bax: imt._TYPE_EXTENT["bax"],
361
+ pcrs: imt._TYPE_EXTENT["pcrs"],
362
+ bcrs: imt._TYPE_EXTENT["bcrs"], **kwargs):
363
+
364
+ # Can re-use the drawing function we already established, but return the object instead
365
+ exi = indicate_extent(pax=pax, bax=bax, pcrs=pcrs, bcrs=bcrs,
366
+ to_return=self._to_return, straighten=self._straighten,
367
+ pad=self._pad, plot=self._plot,
368
+ facecolor=self._facecolor, linecolor=self._linecolor,
369
+ alpha=self._alpha, linewidth=self._linewidth,
370
+ **self._kwargs, **kwargs)
371
+
372
+ # The indicator will be drawn automatically if plot is True
373
+ # If we have anything to return from to_return, we will do so here
374
+ if exi is not None:
375
+ return exi
376
+
377
+ # The main object model of the detail indicator
378
+ class DetailIndicator(matplotlib.artist.Artist):
379
+
380
+ ## INITIALIZATION ##
381
+ def __init__(self,
382
+ to_return: imt._TYPE_DETAIL["to_return"]=None,
383
+ straighten: imt._TYPE_EXTENT["straighten"]=True,
384
+ pad: imt._TYPE_EXTENT["pad"]=0.05,
385
+ plot: imt._TYPE_EXTENT["plot"]=True,
386
+ facecolor: imt._TYPE_EXTENT["facecolor"]="none",
387
+ linecolor: imt._TYPE_EXTENT["linecolor"]="black",
388
+ alpha: imt._TYPE_EXTENT["alpha"]=1,
389
+ linewidth: imt._TYPE_EXTENT["linewidth"]=1,
390
+ connector_color: imt._TYPE_DETAIL["connector_color"]="black",
391
+ connector_width: imt._TYPE_DETAIL["connector_width"]=1,
392
+ **kwargs):
393
+ # Starting up the object with the base properties of a matplotlib Artist
394
+ matplotlib.artist.Artist.__init__(self)
395
+
396
+ # Validating each of the passed parameters
397
+ self._straighten = imf._validate(imt._VALIDATE_EXTENT, "straighten", straighten)
398
+ self._pad = imf._validate(imt._VALIDATE_EXTENT, "pad", pad)
399
+ self._plot = imf._validate(imt._VALIDATE_EXTENT, "plot", plot)
400
+ self._facecolor = imf._validate(imt._VALIDATE_EXTENT, "facecolor", facecolor)
401
+ self._linecolor = imf._validate(imt._VALIDATE_EXTENT, "linecolor", linecolor)
402
+ self._alpha = imf._validate(imt._VALIDATE_EXTENT, "alpha", alpha)
403
+ self._linewidth = imf._validate(imt._VALIDATE_EXTENT, "linewidth", linewidth)
404
+ self._to_return = imf._validate(imt._VALIDATE_DETAIL, "to_return", to_return)
405
+ self._connector_color = imf._validate(imt._VALIDATE_DETAIL, "connector_color", connector_color)
406
+ self._connector_width = imf._validate(imt._VALIDATE_DETAIL, "connector_width", connector_width)
407
+
408
+ self._kwargs = kwargs # not validated!
409
+
410
+ # We do set the zorder for our objects individually,
411
+ # but we ALSO set it for the entire artist, here
412
+ # Thank you to matplotlib-scalebar for this tip
413
+ zorder = 99
414
+
415
+ ## INTERNAL PROPERTIES ##
416
+ # This allows for easy-updating of properties
417
+ # Each property will have the same pair of functions
418
+ # 1) calling the property itself returns its value (DetailIndicator.facecolor will output color)
419
+ # 2) passing a value will update it (DetailIndicator.facecolor = color will update it)
420
+
421
+ # to_return
422
+ @property
423
+ def to_return(self):
424
+ return self._to_return
425
+
426
+ @to_return.setter
427
+ def to_return(self, val):
428
+ val = imf._validate(imt._VALIDATE_DETAIL, "to_return", val)
429
+ self._to_return = val
430
+
431
+ # straighten
432
+ @property
433
+ def straighten(self):
434
+ return self._straighten
435
+
436
+ @straighten.setter
437
+ def straighten(self, val):
438
+ val = imf._validate(imt._VALIDATE_EXTENT, "straighten", val)
439
+ self._straighten = val
440
+
441
+ # pad
442
+ @property
443
+ def pad(self):
444
+ return self._pad
445
+
446
+ @pad.setter
447
+ def pad(self, val):
448
+ val = imf._validate(imt._VALIDATE_EXTENT, "pad", val)
449
+ self._pad = val
450
+
451
+ # plot
452
+ @property
453
+ def plot(self):
454
+ return self._plot
455
+
456
+ @plot.setter
457
+ def plot(self, val):
458
+ val = imf._validate(imt._VALIDATE_EXTENT, "plot", val)
459
+ self._plot = val
460
+
461
+ # facecolor
462
+ @property
463
+ def facecolor(self):
464
+ return self._facecolor
465
+
466
+ @facecolor.setter
467
+ def facecolor(self, val):
468
+ val = imf._validate(imt._VALIDATE_EXTENT, "facecolor", val)
469
+ self._facecolor = val
470
+
471
+ # linecolor
472
+ @property
473
+ def linecolor(self):
474
+ return self._linecolor
475
+
476
+ @linecolor.setter
477
+ def linecolor(self, val):
478
+ val = imf._validate(imt._VALIDATE_EXTENT, "linecolor", val)
479
+ self._linecolor = val
480
+
481
+ # alpha
482
+ @property
483
+ def alpha(self):
484
+ return self._alpha
485
+
486
+ @alpha.setter
487
+ def alpha(self, val):
488
+ val = imf._validate(imt._VALIDATE_EXTENT, "alpha", val)
489
+ self._alpha = val
490
+
491
+ # linewidth
492
+ @property
493
+ def linewidth(self):
494
+ return self._linewidth
495
+
496
+ @linewidth.setter
497
+ def linewidth(self, val):
498
+ val = imf._validate(imt._VALIDATE_EXTENT, "linewidth", val)
499
+ self._linewidth = val
500
+
501
+ # connector_color
502
+ @property
503
+ def connector_color(self):
504
+ return self._connector_color
505
+
506
+ @connector_color.setter
507
+ def connector_color(self, val):
508
+ val = imf._validate(imt._VALIDATE_DETAIL, "connector_color", val)
509
+ self._connector_color = val
510
+
511
+ # connector_width
512
+ @property
513
+ def connector_width(self):
514
+ return self._connector_width
515
+
516
+ @connector_width.setter
517
+ def connector_width(self, val):
518
+ val = imf._validate(imt._VALIDATE_DETAIL, "connector_width", val)
519
+ self._connector_width = val
520
+
521
+ # kwargs
522
+ @property
523
+ def kwargs(self):
524
+ return self._kwargs
525
+
526
+ @kwargs.setter
527
+ def kwargs(self, val):
528
+ if isinstance(val, dict):
529
+ self._kwargs = self._kwargs | val
530
+ else:
531
+ raise ValueError("kwargs expects a dictionary, please try again")
532
+
533
+ ## COPY FUNCTION ##
534
+ # This is solely to get around matplotlib's restrictions around re-using an artist across multiple axes
535
+ # Instead, you can use add_artist() like normal, but with add_artist(na.copy())
536
+ # Thank you to the cartopy team for helping fix a bug with this!
537
+ def copy(self):
538
+ return copy.deepcopy(self)
539
+
540
+ ## CREATE FUNCTION ##
541
+ # Calling DetailIndicator.create(ax) will create an inset map with the specified parameters on the given axis
542
+ # Note that this is different than the way NorthArrows and ScaleBars are rendered (via draw/add_artist())!
543
+ def create(self,
544
+ pax: imt._TYPE_EXTENT["pax"],
545
+ iax: imt._TYPE_EXTENT["bax"],
546
+ pcrs: imt._TYPE_EXTENT["pcrs"],
547
+ icrs: imt._TYPE_EXTENT["bcrs"], **kwargs):
548
+
549
+ # Can re-use the drawing function we already established, but return the object instead
550
+ dti = indicate_detail(pax=pax, iax=iax, pcrs=pcrs, icrs=icrs,
551
+ to_return=self._to_return, straighten=self._straighten,
552
+ pad=self._pad, plot=self._plot,
553
+ facecolor=self._facecolor, linecolor=self._linecolor,
554
+ alpha=self._alpha, linewidth=self._linewidth,
555
+ connector_color=self._connector_color,
556
+ connector_width=self._connector_width,
557
+ **self._kwargs, **kwargs)
558
+
559
+ # The indicator will be drawn automatically if plot is True
560
+ # If we have anything to return from to_return, we will do so here
561
+ if dti is not None:
562
+ return dti
563
+
564
+ ### DRAWING FUNCTIONS ###
565
+ # See here for doc: https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.inset_axes.html
566
+ # See here for kwargs: https://matplotlib.org/stable/api/_as_gen/mpl_toolkits.axes_grid1.inset_locator.inset_axes.html#mpl_toolkits.axes_grid1.inset_locator.inset_axes
567
+
568
+ # Function for creating an inset map, independent of the InsetMap object model
569
+ # It is intended to be an easier-to-use API than the default inset_axes
570
+ def inset_map(ax,
571
+ location: Literal["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]="upper right",
572
+ size: imt._TYPE_INSET["size"]=None,
573
+ pad: imt._TYPE_INSET["pad"]=None,
574
+ coords: imt._TYPE_INSET["coords"]=None,
575
+ transform=None,
576
+ **kwargs):
577
+
578
+ ## VALIDATION ##
579
+ location = imf._validate(imt._VALIDATE_INSET, "location", location)
580
+ size = imf._validate(imt._VALIDATE_INSET, "size", size)
581
+ pad = imf._validate(imt._VALIDATE_INSET, "pad", pad)
582
+ coords = imf._validate(imt._VALIDATE_INSET, "coords", coords)
583
+
584
+ if size is None:
585
+ size = _DEFAULT_INSET_MAP["size"]
586
+ if pad is None:
587
+ pad = _DEFAULT_INSET_MAP["pad"]
588
+
589
+ ## SET-UP ##
590
+ # Getting the figure
591
+ fig = ax.get_figure()
592
+
593
+ ## SIZE ##
594
+ # Setting the desired dimensions of the inset map
595
+ # The default inset_axis() function does this as a fraction of the parent axis
596
+ # But the size variable expects dimensions in inches
597
+
598
+ # Casting size to width and height
599
+ if isinstance(size, (tuple, list)):
600
+ inset_width, inset_height = size
601
+ else:
602
+ inset_width = size
603
+ inset_height = size
604
+
605
+ ## PADDING ##
606
+ # Padding is expressed in inches here, unlike traditional matplotlib
607
+ # which expresses it as a fraction of the font size
608
+ if isinstance(pad, (tuple, list)):
609
+ pad_x, pad_y = pad
610
+ else:
611
+ pad_x = pad
612
+ pad_y = pad
613
+
614
+ ## RESIZING ##
615
+ # Getting the current dimensions of the parent axis in inches (ignoring ticks and labels - just the axis itself)
616
+ parent_axis_bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
617
+
618
+ # Expressing the desired height and width of the inset map as a fraction of the parent
619
+ # We do this because, later on, we're assuming everything is in ax.transAxes
620
+ # which ranges from 0 to 1, as a fraction of the parent axis
621
+ inset_width = inset_width / parent_axis_bbox.width
622
+ inset_height = inset_height / parent_axis_bbox.height
623
+ # and doing the same for the padding
624
+ pad_x = pad_x / parent_axis_bbox.width
625
+ pad_y = pad_y / parent_axis_bbox.height
626
+
627
+ ## PLACEMENT ##
628
+ # Calculating the start coordinate (which is always the bottom-left corner) of the inset map
629
+ # based on the desired location, padding (inches) from the side, and the height and width of the inset map
630
+ if coords is None:
631
+ # First, the x coordinate
632
+ if location in ["upper left", "center left", "lower left"]:
633
+ x = pad_x
634
+ elif location in ["upper center", "center", "lower center"]:
635
+ x = (1 - (inset_width + pad_x)) / 2
636
+ elif location in ["upper right", "center right", "lower right"]:
637
+ x = 1 - (inset_width + pad_x)
638
+ # Then the y coordinate
639
+ if location in ["upper left", "upper center", "upper right"]:
640
+ y = 1 - (inset_height + pad_y)
641
+ elif location in ["center left", "center", "center right"]:
642
+ y = (1 - (inset_height + pad_y)) / 2
643
+ elif location in ["lower left", "lower center", "lower right"]:
644
+ y = pad_y
645
+ # If coordinates are passed, calculate references with respect to that
646
+ # NOTE: in this case, padding is ignored!
647
+ else:
648
+ # Transforming the passed coordinates to transAxes coordinates
649
+ if transform is not None and transform != ax.transAxes:
650
+ # Coords needs to be x,y
651
+ coords = transform.transform(coords) # Transforming the coordinates to display units
652
+ coords = ax.transAxes.inverted().transform(coords) # Converting back to ax.transAxes
653
+ # Now coords can be treated as basically an offset value
654
+ # Here, we use inset_width and inset_height, because we are expressing everything in relative axis units
655
+ # That's what the transformations were for!
656
+ # First, the x coordinate
657
+ if location in ["upper left", "center left", "lower left"]:
658
+ x = coords[0]
659
+ elif location in ["upper center", "center", "lower center"]:
660
+ x = coords[0] - (inset_width / 2)
661
+ elif location in ["upper right", "center right", "lower right"]:
662
+ x = coords[0] - inset_width
663
+ # Then the y coordinate
664
+ if location in ["upper left", "upper center", "upper right"]:
665
+ y = coords[1] - inset_height
666
+ elif location in ["center left", "center", "center right"]:
667
+ y = coords[1] - (inset_height / 2)
668
+ elif location in ["lower left", "lower center", "lower right"]:
669
+ y = coords[1]
670
+
671
+ ## DRAWING ##
672
+ # Creating the new inset map with the specified height, width, and location
673
+ # by default, inset_axes requires everything to be in ax.transAxes coordinates
674
+ iax = ax.inset_axes([x, y, inset_width, inset_height], **kwargs)
675
+
676
+ # We also set the anchor here, such that it stays fixed when any resizing takes place
677
+ loc_anchors = {
678
+ "upper left": "NW", "upper center": "N", "upper right": "NE",
679
+ "center left": "W", "center": "C", "center right": "E",
680
+ "lower left": "SW", "lower center": "S", "lower right": "SE"
681
+ }
682
+ iax.set_anchor(anchor=loc_anchors[location])
683
+
684
+ # The new inset axis is returned
685
+ return iax
686
+
687
+ # This function will display the extent/bounds of one axis on the other
688
+ # This is can be used on an inset map to show where the bounds of the parent axis lay
689
+ # or, it is called by detail_indicator to show where the bounds of an inset axis lay on the parent
690
+ # here, PAX means "plotting axis" (where the indicator is plotted)
691
+ # and BAX means "bounds axis" (where the extent is derived from)
692
+ def indicate_extent(pax: imt._TYPE_EXTENT["pax"],
693
+ bax: imt._TYPE_EXTENT["bax"],
694
+ pcrs: imt._TYPE_EXTENT["pcrs"],
695
+ bcrs: imt._TYPE_EXTENT["bcrs"],
696
+ to_return: imt._TYPE_EXTENT["to_return"]=None,
697
+ straighten: imt._TYPE_EXTENT["straighten"]=True,
698
+ pad: imt._TYPE_EXTENT["pad"]=0.05,
699
+ plot: imt._TYPE_EXTENT["plot"]=True,
700
+ facecolor: imt._TYPE_EXTENT["facecolor"]="red",
701
+ linecolor: imt._TYPE_EXTENT["linecolor"]="red",
702
+ alpha: imt._TYPE_EXTENT["alpha"]=0.5,
703
+ linewidth: imt._TYPE_EXTENT["linewidth"]=1,
704
+ **kwargs):
705
+
706
+ ## VALIDATION ##
707
+ pax = imf._validate(imt._VALIDATE_EXTENT, "pax", pax)
708
+ bax = imf._validate(imt._VALIDATE_EXTENT, "bax", bax)
709
+ pcrs = imf._validate(imt._VALIDATE_EXTENT, "pcrs", pcrs)
710
+ bcrs = imf._validate(imt._VALIDATE_EXTENT, "bcrs", bcrs)
711
+ to_return = imf._validate(imt._VALIDATE_EXTENT, "to_return", to_return)
712
+ straighten = imf._validate(imt._VALIDATE_EXTENT, "straighten", straighten)
713
+ pad = imf._validate(imt._VALIDATE_EXTENT, "pad", pad)
714
+ plot = imf._validate(imt._VALIDATE_EXTENT, "plot", plot)
715
+ facecolor = imf._validate(imt._VALIDATE_EXTENT, "facecolor", facecolor)
716
+ linecolor = imf._validate(imt._VALIDATE_EXTENT, "linecolor", linecolor)
717
+ alpha = imf._validate(imt._VALIDATE_EXTENT, "alpha", alpha)
718
+ linewidth = imf._validate(imt._VALIDATE_EXTENT, "linewidth", linewidth)
719
+
720
+ # Make sure the figure layout is calculated
721
+ fig = pax.get_figure()
722
+ fig.draw_without_rendering()
723
+
724
+ # Get the limits of the bounds axis (which will be in its own crs)
725
+ ymin, ymax = bax.get_ylim()
726
+ yrange = abs(ymax-ymin)
727
+ xmin, xmax = bax.get_xlim()
728
+ xrange = abs(xmax-xmin)
729
+
730
+ # Buffering the points, if desired
731
+ # Note that this is treated as a percentage increase/decrease!
732
+ if pad is not None and isinstance(pad, (float, int)):
733
+ pad_data = min(yrange, xrange)*pad
734
+ else:
735
+ pad_data = 0
736
+
737
+ # Converting it into a tuple of coordinates
738
+ # in the order of lower-left, upper-left, upper-right, lower-right
739
+ extent_corners = [(xmin-pad_data, ymin-pad_data), (xmin-pad_data, ymax+pad_data),
740
+ (xmax+pad_data, ymax+pad_data), (xmax+pad_data, ymin-pad_data)]
741
+
742
+ # Converting the points
743
+ # This is now ready to be plotted on the parent axis, if desired
744
+ transform_crs = pyproj.Transformer.from_crs(bcrs, pcrs, always_xy=True)
745
+ extent_points = numpy.array([transform_crs.transform(p[0],p[1]) for p in extent_corners])
746
+ extent_shape = shapely.Polygon(extent_points)
747
+
748
+ # Straightening the points if desired
749
+ if straighten == True:
750
+ extent_shape = shapely.envelope(extent_shape)
751
+
752
+ # return extent_shape
753
+
754
+ # Plotting, if desired
755
+ if plot == True:
756
+ # Note that the alpha ONLY applies to the facecolor!
757
+ extent_patch = matplotlib.patches.Polygon(list(extent_shape.exterior.coords), transform=pax.transData,
758
+ facecolor=matplotlib.colors.to_rgba(facecolor, alpha=alpha),
759
+ edgecolor=linecolor, linewidth=linewidth, **kwargs)
760
+ pax.add_artist(extent_patch)
761
+
762
+ # Deciding what we need to return
763
+ if to_return is None:
764
+ pass
765
+ elif to_return == "shape":
766
+ return extent_shape
767
+ elif to_return == "patch":
768
+ return extent_patch
769
+ elif to_return == "fig":
770
+ return [fig.transFigure.inverted().transform(pax.transData.transform(p)) for p in list(extent_shape.exterior.coords)[::-1]]
771
+ elif to_return == "ax":
772
+ return [pax.transAxes.inverted().transform(pax.transData.transform(p)) for p in list(extent_shape.exterior.coords)[::-1]]
773
+ else:
774
+ pass
775
+
776
+ # Detail indicators are for when the inset map shows a zoomed-in section of the parent map
777
+ # This will also, call extent_indicator too, as the two are linked
778
+ # here, PAX means "parent axis" (where the indicator is plotted)
779
+ # and IAX means "inset axis" (which contains the detail/zoomed-in section)
780
+ def indicate_detail(pax: imt._TYPE_EXTENT["pax"],
781
+ iax: imt._TYPE_EXTENT["bax"],
782
+ pcrs: imt._TYPE_EXTENT["pcrs"],
783
+ icrs: imt._TYPE_EXTENT["bcrs"],
784
+ to_return: imt._TYPE_DETAIL["to_return"]=None,
785
+ straighten: imt._TYPE_EXTENT["straighten"]=True,
786
+ pad: imt._TYPE_EXTENT["pad"]=0.05,
787
+ plot: imt._TYPE_EXTENT["plot"]=True,
788
+ facecolor: imt._TYPE_EXTENT["facecolor"]="none",
789
+ alpha: imt._TYPE_EXTENT["alpha"]=1,
790
+ linecolor: imt._TYPE_EXTENT["linecolor"]="black",
791
+ linewidth: imt._TYPE_EXTENT["linewidth"]=1,
792
+ # connector_color: imt._TYPE_DETAIL["connector_color"]="black",
793
+ # connector_width: imt._TYPE_DETAIL["connector_width"]=1,
794
+ **kwargs):
795
+
796
+ fig = pax.get_figure()
797
+ fig.draw_without_rendering()
798
+
799
+ ## VALIDATION ##
800
+ pax = imf._validate(imt._VALIDATE_EXTENT, "pax", pax)
801
+ iax = imf._validate(imt._VALIDATE_EXTENT, "bax", iax)
802
+ pcrs = imf._validate(imt._VALIDATE_EXTENT, "pcrs", pcrs)
803
+ icrs = imf._validate(imt._VALIDATE_EXTENT, "bcrs", icrs)
804
+ to_return = imf._validate(imt._VALIDATE_DETAIL, "to_return", to_return)
805
+ straighten = imf._validate(imt._VALIDATE_EXTENT, "straighten", straighten)
806
+ pad = imf._validate(imt._VALIDATE_EXTENT, "pad", pad)
807
+ plot = imf._validate(imt._VALIDATE_EXTENT, "plot", plot)
808
+ facecolor = imf._validate(imt._VALIDATE_EXTENT, "facecolor", facecolor)
809
+ alpha = imf._validate(imt._VALIDATE_EXTENT, "alpha", alpha)
810
+ linecolor = imf._validate(imt._VALIDATE_EXTENT, "linecolor", linecolor)
811
+ linewidth = imf._validate(imt._VALIDATE_EXTENT, "linewidth", linewidth)
812
+ # connector_color = imf._validate(imt._VALIDATE_DETAIL, "connector_color", connector_color)
813
+ # connector_width = imf._validate(imt._VALIDATE_DETAIL, "connector_width", connector_width)
814
+
815
+ # Drawing the extent indicator on the main map
816
+ # Setting to_return="ax" gets us the corners of the patch in pax.transAxes coordinates
817
+ # We only need the first 4 points - the fifth is a repeated point to enforce "closure"
818
+ corners_extent = indicate_extent(pax=pax, bax=iax, pcrs=pcrs, bcrs=icrs,
819
+ straighten=straighten, pad=pad, plot=plot,
820
+ facecolor=facecolor, linecolor=linecolor,
821
+ alpha=alpha, linewidth=linewidth*1.25,
822
+ to_return="ax", **kwargs)[:4]
823
+
824
+ # Getting the inset axis points and transforming them to the parent axis CRS
825
+ corners_inset = _inset_corners_to_parent(pax, iax)
826
+
827
+ # Getting the center of both the extent and the inset, which we will then use to decide WHICH lines to draw
828
+ center_extent_x = sum([p[0] for p in corners_extent]) / len(corners_extent)
829
+ center_extent_y = sum([p[1] for p in corners_extent]) / len(corners_extent)
830
+ center_inset_x = sum([p[0] for p in corners_inset]) / len(corners_inset)
831
+ center_inset_y = sum([p[1] for p in corners_inset]) / len(corners_inset)
832
+
833
+ ## CONNECTION ##
834
+ # This part is quite tricky, and involves connecting the inset map to its extent indicator
835
+ # To do so, we make an educated guess about which corners we need to connect,
836
+ # based on the relative position of each object
837
+
838
+ # If our extent is horizontally centered with our inset, connect just the left or right edges
839
+ if (abs(center_extent_y - center_inset_y) / abs(center_inset_y)) <= 0.30:
840
+ if center_extent_x > center_inset_x:
841
+ # extent lefts + inset rights
842
+ connections = [[corners_extent[0], corners_inset[3]], [corners_extent[1], corners_inset[2]]]
843
+ else:
844
+ # extent rights + inset lefts
845
+ connections = [[corners_extent[3], corners_inset[0]], [corners_extent[2], corners_inset[1]]]
846
+
847
+ # If instead our extent is vertically centered, connect just the top or bottom edges
848
+ elif (abs(center_extent_x - center_inset_x) / abs(center_inset_x)) <= 0.30:
849
+ if center_extent_y > center_inset_y:
850
+ # extent bottoms + inset tops
851
+ connections = [[corners_extent[0], corners_inset[1]], [corners_extent[3], corners_inset[2]]]
852
+ else:
853
+ # extent tops + inset bottoms
854
+ connections = [[corners_extent[1], corners_inset[0]], [corners_extent[2], corners_inset[3]]]
855
+
856
+ # The most common cases will be when the inset is in a corner...
857
+ elif center_extent_x > center_inset_x and center_extent_y > center_inset_y:
858
+ # top-left and bottom-right corners for each
859
+ connections = [[corners_extent[1], corners_inset[1]], [corners_extent[3], corners_inset[3]]]
860
+ elif center_extent_x <= center_inset_x and center_extent_y > center_inset_y:
861
+ # top-right and bottom-left corners for each
862
+ connections = [[corners_extent[2], corners_inset[2]], [corners_extent[0], corners_inset[0]]]
863
+ elif center_extent_x > center_inset_x and center_extent_y <= center_inset_y:
864
+ # bottom-left and top-right corners for each
865
+ connections = [[corners_extent[0], corners_inset[0]], [corners_extent[2], corners_inset[2]]]
866
+ elif center_extent_x <= center_inset_x and center_extent_y <= center_inset_y:
867
+ # top-right and bottom-left corners for each
868
+ connections = [[corners_extent[2], corners_inset[2]], [corners_extent[0], corners_inset[0]]]
869
+
870
+ ## PLOTTING ##
871
+ if plot == True:
872
+ # A manual plot call, to connect the corners to each other
873
+ for c in connections:
874
+ # This is listed as [extent_x, inset_x], [extent_y, inset_y]
875
+ pax.plot([c[0][0], c[1][0]], [c[0][1], c[1][1]],
876
+ color=linecolor, linewidth=linewidth, transform=pax.transAxes)
877
+
878
+ # Also updating the linewidth and color of the inset map itsef, to match
879
+ for a in ["top","bottom","left","right"]:
880
+ iax.spines[a].set_linewidth(linewidth*1.2) # making this slightly thicker
881
+ iax.spines[a].set_edgecolor(linecolor)
882
+
883
+ # Returning as requested
884
+ if to_return is None:
885
+ pass
886
+ elif to_return == "connectors" or to_return == "lines":
887
+ return connections
888
+ else:
889
+ pass
890
+
891
+ ### HELPING FUNCTIONS ###
892
+
893
+ # This is a top-level helping function
894
+ # that will return an axis with inset maps drawn for Alaska, Hawaii, DC, and/or Puerto Rico
895
+ # NOTE that as of this initial release, it assumes your map is in CRS 3857 for positioning
896
+ def inset_usa(ax, alaska=True, hawaii=True, dc=True, puerto_rico=True, size=None, pad=None, **kwargs):
897
+ # This will return all of the axes we create
898
+ to_return = []
899
+
900
+ # Alaska and Hawaii are positioned relative to each other
901
+ if alaska == True and hawaii == True:
902
+ aax = inset_map(ax, "lower left", size, pad, **kwargs)
903
+ to_return.append(aax)
904
+ # Need to shift over the hawaii axis by the size of the alaska axis
905
+ # Note that we add xmax and xmin together here, to account for the padding (xmin is the amount of padding)
906
+ shift_right = float(aax.get_window_extent().transformed(ax.transAxes.inverted()).xmax) + float(aax.get_window_extent().transformed(ax.transAxes.inverted()).xmin)
907
+ # also need to shift it up, by the amount of the padding (which we can crib from ymin)
908
+ shift_up = float(aax.get_window_extent().transformed(ax.transAxes.inverted()).ymin)
909
+ hax = inset_map(ax, "lower left", size, pad, coords=(shift_right, shift_up), **kwargs)
910
+ to_return.append(hax)
911
+ else:
912
+ if alaska == True:
913
+ aax = inset_map(ax, "lower_left", size, pad, **kwargs)
914
+ to_return.append(aax)
915
+ if hawaii == True:
916
+ hax = inset_map(ax, "lower left", size, pad, **kwargs)
917
+ to_return.append(hax)
918
+
919
+ # Puerto Rico is positioned off the coast of Florida
920
+ if puerto_rico == True:
921
+ pax = inset_map(ax, "lower right", size, pad, **kwargs)
922
+ to_return.append(pax)
923
+
924
+ # DC is off the coast of DC
925
+ if dc == True:
926
+ dax = inset_map(ax, "center right", size, pad, **kwargs)
927
+ to_return.append(dax)
928
+
929
+ # Finally, returning everything
930
+ return to_return
931
+
932
+
933
+ # This retrieves the position of the inset axes (iax)
934
+ # in the coordinates of its parent axis
935
+ def _inset_corners_to_parent(pax, iax):
936
+ # Make sure the figure layout is calculated
937
+ fig = pax.get_figure()
938
+ fig.draw_without_rendering()
939
+
940
+ # Get positions as Bbox objects in figure coordinates (0-1)
941
+ iax_pos = iax.get_position()
942
+
943
+ # Extract corners in figure coordinates
944
+ figure_corners = numpy.array([(iax_pos.x0, iax_pos.y0), (iax_pos.x0, iax_pos.y1),
945
+ (iax_pos.x1, iax_pos.y1), (iax_pos.x1, iax_pos.y0)])
946
+
947
+ # Convert to parent axes coordinates (0-1, as a fraction of each axis)
948
+ parent_corners = pax.transAxes.inverted().transform(
949
+ fig.transFigure.transform(figure_corners)
950
+ )
951
+
952
+ return parent_corners