matplotlib-map-utils 1.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.
- matplotlib_map_utils/__init__.py +4 -0
- matplotlib_map_utils/defaults.py +416 -0
- matplotlib_map_utils/north_arrow.py +458 -0
- matplotlib_map_utils/scratch/map_utils.py +412 -0
- matplotlib_map_utils/scratch/north_arrow_old_classes.py +1185 -0
- matplotlib_map_utils/validation.py +332 -0
- matplotlib_map_utils-1.0.0.dist-info/LICENSE +674 -0
- matplotlib_map_utils-1.0.0.dist-info/METADATA +131 -0
- matplotlib_map_utils-1.0.0.dist-info/RECORD +11 -0
- matplotlib_map_utils-1.0.0.dist-info/WHEEL +5 -0
- matplotlib_map_utils-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,332 @@
|
|
1
|
+
############################################################
|
2
|
+
# validation.py contains all the main objects and functions
|
3
|
+
# for checking inputs passed to class definitions
|
4
|
+
############################################################
|
5
|
+
|
6
|
+
### IMPORTING PACKAGES ###
|
7
|
+
|
8
|
+
# Default packages
|
9
|
+
import warnings
|
10
|
+
# Math packages
|
11
|
+
import numpy
|
12
|
+
# Geo packages
|
13
|
+
import pyproj
|
14
|
+
# Graphical packages
|
15
|
+
import matplotlib
|
16
|
+
# matplotlib's useful validation functions
|
17
|
+
import matplotlib.rcsetup
|
18
|
+
# The types we use in this script
|
19
|
+
from typing import Tuple, TypedDict, Literal, get_args
|
20
|
+
|
21
|
+
### TYPE HINTS ###
|
22
|
+
# This section of the code is for defining structured dictionaries and lists
|
23
|
+
# for the inputs necessary for object creation we've created (such as the style dictionaries)
|
24
|
+
# so that intellisense can help with autocompletion
|
25
|
+
|
26
|
+
class _TYPE_BASE(TypedDict, total=False):
|
27
|
+
coords: numpy.array # must be 2D numpy array
|
28
|
+
facecolor: str # any color value for matplotlib
|
29
|
+
edgecolor: str # any color value for matplotlib
|
30
|
+
linewidth: float | int # between 0 and inf
|
31
|
+
zorder: int # any integer
|
32
|
+
|
33
|
+
class _TYPE_FANCY(TypedDict, total=False):
|
34
|
+
coords: numpy.array # must be 2D numpy array
|
35
|
+
facecolor: str # any color value for matplotlib
|
36
|
+
zorder: int # any integer
|
37
|
+
|
38
|
+
class _TYPE_LABEL(TypedDict, total=False):
|
39
|
+
text: str # any string that you want to display ("N" or "North" being the most common)
|
40
|
+
position: Literal["top", "bottom", "left", "right"] # from matplotlib documentation
|
41
|
+
ha: Literal["left", "center", "right"] # from matplotlib documentation
|
42
|
+
va: Literal["baseline", "bottom", "center", "center_baseline", "top"] # from matplotlib documentation
|
43
|
+
fontsize: str | float | int # any fontsize value for matplotlib
|
44
|
+
fontfamily: Literal["serif", "sans-serif", "cursive", "fantasy", "monospace"] # from matplotlib documentation
|
45
|
+
fontstyle: Literal["normal", "italic", "oblique"] # from matplotlib documentation
|
46
|
+
color: str # any color value for matplotlib
|
47
|
+
fontweight: Literal["normal", "bold", "heavy", "light", "ultrabold", "ultralight"] # from matplotlib documentation
|
48
|
+
stroke_width: float | int # between 0 and infinity
|
49
|
+
stroke_color: str # any color value for matplotlib
|
50
|
+
rotation: float | int # between -360 and 360
|
51
|
+
zorder: int # any integer
|
52
|
+
|
53
|
+
class _TYPE_SHADOW(TypedDict, total=False):
|
54
|
+
offset: Tuple[float | int, float | int] # two-length tuple or list of x,y values in points
|
55
|
+
alpha: float | int # between 0 and 1
|
56
|
+
shadow_rgbFace: str # any color vlaue for matplotlib
|
57
|
+
|
58
|
+
class _TYPE_PACK(TypedDict, total=False):
|
59
|
+
sep: float | int # between 0 and inf
|
60
|
+
align: Literal["top", "bottom", "left", "right", "center", "baseline"] # from matplotlib documentation
|
61
|
+
pad: float | int # between 0 and inf
|
62
|
+
width: float | int # between 0 and inf
|
63
|
+
height: float | int # between 0 and inf
|
64
|
+
mode: Literal["fixed", "expand", "equal"] # from matplotlib documentation
|
65
|
+
|
66
|
+
class _TYPE_AOB(TypedDict, total=False):
|
67
|
+
facecolor: str # NON-STANDARD: used to set the facecolor of the offset box (i.e. to white), any color vlaue for matplotlib
|
68
|
+
edgecolor: str # NON-STANDARD: used to set the edge of the offset box (i.e. to black), any color vlaue for matplotlib
|
69
|
+
alpha: float | int # NON-STANDARD: used to set the transparency of the face color of the offset box^, between 0 and 1
|
70
|
+
pad: float | int # between 0 and inf
|
71
|
+
borderpad: float | int # between 0 and inf
|
72
|
+
prop: str | float | int # any fontsize value for matplotlib
|
73
|
+
frameon: bool # any bool
|
74
|
+
# bbox_to_anchor: None # NOTE: currently unvalidated, use at your own risk!
|
75
|
+
# bbox_transform: None
|
76
|
+
|
77
|
+
class _TYPE_ROTATION(TypedDict, total=False):
|
78
|
+
degrees: float | int # anything between -360 and 360, or None for "auto"
|
79
|
+
crs: str | int | pyproj.CRS # only required if degrees is None: should be a valid cartopy or pyproj crs, or a string that can be converted to that
|
80
|
+
reference: Literal["axis", "data", "center"] # only required if degrees is None: should be either "axis" or "data" or "center"
|
81
|
+
coords: Tuple[float | int, float | int] # only required if degrees is None: should be a tuple of coordinates in the relevant reference window
|
82
|
+
|
83
|
+
### VALIDITY CHECKS ###
|
84
|
+
# Functions and variables used for validating inputs for classes
|
85
|
+
# All have a similar form, taking in the name of the property (prop), the value (val)
|
86
|
+
# some parameters to check against (min/max, list, type, etc.),
|
87
|
+
# and whether or not None is acceptable value
|
88
|
+
|
89
|
+
def _validate_list(prop, val, list, none_ok=False):
|
90
|
+
if none_ok==False and val is None:
|
91
|
+
raise ValueError(f"None is not a valid value for {prop}, please provide a value in this list: {list}")
|
92
|
+
elif none_ok==True and val is None:
|
93
|
+
return val
|
94
|
+
elif not val in list:
|
95
|
+
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a value in this list: {list}")
|
96
|
+
return val
|
97
|
+
|
98
|
+
def _validate_range(prop, val, min, max, none_ok=False):
|
99
|
+
if none_ok==False and val is None:
|
100
|
+
raise ValueError(f"None is not a valid value for {prop}, please provide a value between {min} and {max}")
|
101
|
+
elif none_ok==True and val is None:
|
102
|
+
return val
|
103
|
+
elif type(val) != int and type(val) != float:
|
104
|
+
raise ValueError(f"The supplied type is not valid for {prop}, please provide a float or integer between {min} and {max}")
|
105
|
+
elif max is not None:
|
106
|
+
if not val >= min and not val <= max:
|
107
|
+
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a value between {min} and {max}")
|
108
|
+
elif max is None:
|
109
|
+
if not val >= min:
|
110
|
+
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a value greater than {min}")
|
111
|
+
return val
|
112
|
+
|
113
|
+
def _validate_type(prop, val, match, none_ok=False):
|
114
|
+
if none_ok==False and val is None:
|
115
|
+
raise ValueError(f"None is not a valid value for {prop}, please provide an object of type {match}")
|
116
|
+
elif none_ok==True and val is None:
|
117
|
+
return val
|
118
|
+
elif not type(val)==match:
|
119
|
+
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide an object of type {match}")
|
120
|
+
return val
|
121
|
+
|
122
|
+
def _validate_coords(prop, val, numpy_type, dims, none_ok=False):
|
123
|
+
if none_ok==False and val is None:
|
124
|
+
raise ValueError(f"None is not a valid value for {prop}, please provide an object of type {numpy_type}")
|
125
|
+
elif none_ok==True and val is None:
|
126
|
+
return val
|
127
|
+
elif not type(val)==numpy_type:
|
128
|
+
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide an object of type {numpy_type}")
|
129
|
+
elif not val.ndim==dims:
|
130
|
+
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a numpy array with {dims} dimensions")
|
131
|
+
return val
|
132
|
+
|
133
|
+
def _validate_tuple(prop, val, length, types, none_ok=False):
|
134
|
+
if none_ok==False and val is None:
|
135
|
+
raise ValueError(f"None is not a valid value for {prop}, please provide a tuple of length {length} instead")
|
136
|
+
elif none_ok==True and val is None:
|
137
|
+
return val
|
138
|
+
elif type(val)!=tuple:
|
139
|
+
raise ValueError(f"{val} is not a valid value for {prop}, please provide a tuple of length {length} instead")
|
140
|
+
elif len(val)!=length:
|
141
|
+
raise ValueError(f"{val} is not a valid value for {prop}, please provide a tuple of length {length} instead")
|
142
|
+
else:
|
143
|
+
for item in val:
|
144
|
+
if type(item) not in types:
|
145
|
+
raise ValueError(f"{type(item)} is not a valid value for the items in {prop}, please provide a value of one of the following types: {types}")
|
146
|
+
return val
|
147
|
+
|
148
|
+
def _validate_color_or_none(prop, val, none_ok=False):
|
149
|
+
if none_ok==False and val is None:
|
150
|
+
raise ValueError(f"None is not a valid value for {prop}, please provide a color string acceptable to matplotlib instead")
|
151
|
+
elif none_ok==True and val is None:
|
152
|
+
return val
|
153
|
+
else:
|
154
|
+
matplotlib.rcsetup.validate_color(val)
|
155
|
+
return val
|
156
|
+
|
157
|
+
# NOTE: This one is a bit messy, particularly with the rotation module, but I can't think of a better way to do it...
|
158
|
+
def _validate_crs(prop, val, rotation_dict, none_ok=False):
|
159
|
+
degrees = rotation_dict.get("degrees",None)
|
160
|
+
crs = rotation_dict.get("crs",None)
|
161
|
+
reference = rotation_dict.get("reference",None)
|
162
|
+
coords = rotation_dict.get("coords",None)
|
163
|
+
|
164
|
+
if degrees is None:
|
165
|
+
if reference == "center":
|
166
|
+
if crs is None:
|
167
|
+
raise ValueError(f"If degrees is set to None, and reference is 'center', then a valid crs must be supplied")
|
168
|
+
else:
|
169
|
+
if crs is None or reference is None or coords is None:
|
170
|
+
raise ValueError(f"If degrees is set to None, then crs, reference, and coords cannot be None: please provide a valid input for each of these variables instead")
|
171
|
+
elif (type(degrees)==int or type(degrees)==float) and (crs is not None or reference is not None or coords is not None):
|
172
|
+
warnings.warn(f"A value for degrees was supplied; values for crs, reference, and coords will be ignored")
|
173
|
+
return val
|
174
|
+
else:
|
175
|
+
if none_ok==False and val is None:
|
176
|
+
raise ValueError(f"If degrees is set to None, then {prop} cannot be None: please provide a valid CRS input for PyProj instead")
|
177
|
+
elif none_ok==True and val is None:
|
178
|
+
return val
|
179
|
+
# This happens if (a) a value for CRS is supplied and (b) a value for degrees is NOT supplied
|
180
|
+
if type(val)==pyproj.CRS:
|
181
|
+
pass
|
182
|
+
else:
|
183
|
+
try:
|
184
|
+
val = pyproj.CRS.from_user_input(val)
|
185
|
+
except:
|
186
|
+
raise Exception(f"Invalid CRS supplied ({val}), please provide a valid CRS input for PyProj instead")
|
187
|
+
return val
|
188
|
+
|
189
|
+
# This final one is used for keys that are not validated
|
190
|
+
def _skip_validation(val, none_ok=False):
|
191
|
+
return val
|
192
|
+
|
193
|
+
### VALIDITY DICTS ###
|
194
|
+
# These compile the functions above^, as well as matplotlib's built-in validity functions
|
195
|
+
# into dictionaries that can be used to validate all the inputs to a dictionary at once
|
196
|
+
|
197
|
+
_VALIDATE_PRIMARY = {
|
198
|
+
"location":{"func":_validate_list, "kwargs":{"list":["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]}},
|
199
|
+
"scale":{"func":_validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
|
200
|
+
}
|
201
|
+
|
202
|
+
_VALIDATE_BASE = {
|
203
|
+
"coords":{"func":_validate_coords, "kwargs":{"numpy_type":numpy.ndarray, "dims":2}}, # must be 2D numpy array
|
204
|
+
"facecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
205
|
+
"edgecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
206
|
+
"linewidth":{"func":_validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
|
207
|
+
"zorder":{"func":_validate_type, "kwargs":{"match":int}} # any integer
|
208
|
+
}
|
209
|
+
|
210
|
+
_VALIDATE_FANCY = {
|
211
|
+
"coords":{"func":_validate_coords, "kwargs":{"numpy_type":numpy.ndarray, "dims":2}}, # must be 2D numpy array
|
212
|
+
"facecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
213
|
+
"zorder":{"func":_validate_type, "kwargs":{"match":int}} # any integer
|
214
|
+
}
|
215
|
+
|
216
|
+
_VALID_LABEL_POSITION = get_args(_TYPE_LABEL.__annotations__["position"])
|
217
|
+
_VALID_LABEL_HA = get_args(_TYPE_LABEL.__annotations__["ha"])
|
218
|
+
_VALID_LABEL_VA = get_args(_TYPE_LABEL.__annotations__["va"])
|
219
|
+
_VALID_LABEL_FONTFAMILY = get_args(_TYPE_LABEL.__annotations__["fontfamily"])
|
220
|
+
_VALID_LABEL_FONTSTYLE = get_args(_TYPE_LABEL.__annotations__["fontstyle"])
|
221
|
+
_VALID_LABEL_FONTWEIGHT = get_args(_TYPE_LABEL.__annotations__["fontweight"])
|
222
|
+
|
223
|
+
_VALIDATE_LABEL = {
|
224
|
+
"text":{"func":_validate_type, "kwargs":{"match":str}}, # any string
|
225
|
+
"position":{"func":_validate_list, "kwargs":{"list":_VALID_LABEL_POSITION}},
|
226
|
+
"ha":{"func":_validate_list, "kwargs":{"list":_VALID_LABEL_HA}},
|
227
|
+
"va":{"func":_validate_list, "kwargs":{"list":_VALID_LABEL_VA}},
|
228
|
+
"fontsize":{"func":matplotlib.rcsetup.validate_fontsize}, # any fontsize value for matplotlib
|
229
|
+
"fontfamily":{"func":_validate_list, "kwargs":{"list":_VALID_LABEL_FONTFAMILY}},
|
230
|
+
"fontstyle":{"func":_validate_list, "kwargs":{"list":_VALID_LABEL_FONTSTYLE}},
|
231
|
+
"color":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
232
|
+
"fontweight":{"func":matplotlib.rcsetup.validate_fontweight}, # any fontweight value for matplotlib
|
233
|
+
"stroke_width":{"func":_validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
|
234
|
+
"stroke_color":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
235
|
+
"rotation":{"func":_validate_range, "kwargs":{"min":-360, "max":360, "none_ok":True}}, # anything between -360 and 360, or None for "auto"
|
236
|
+
"zorder":{"func":_validate_type, "kwargs":{"match":int}} # any integer
|
237
|
+
}
|
238
|
+
|
239
|
+
_VALIDATE_SHADOW = {
|
240
|
+
"offset":{"func":_validate_tuple, "kwargs":{"length":2, "types":[float, int]}},
|
241
|
+
"alpha":{"func":_validate_range, "kwargs":{"min":0, "max":1, "none_ok":True}}, # any value between 0 and 1
|
242
|
+
"shadow_rgbFace":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
243
|
+
}
|
244
|
+
|
245
|
+
_VALID_PACK_ALIGN = get_args(_TYPE_PACK.__annotations__["align"])
|
246
|
+
_VALID_PACK_MODE = get_args(_TYPE_PACK.__annotations__["mode"])
|
247
|
+
|
248
|
+
_VALIDATE_PACK = {
|
249
|
+
"sep":{"func":_validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
|
250
|
+
"align":{"func":_validate_list, "kwargs":{"list":_VALID_PACK_ALIGN}},
|
251
|
+
"pad":{"func":_validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
|
252
|
+
"width":{"func":_validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
|
253
|
+
"height":{"func":_validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
|
254
|
+
"mode":{"func":_validate_list, "kwargs":{"list":_VALID_PACK_MODE}}
|
255
|
+
}
|
256
|
+
|
257
|
+
_VALIDATE_AOB = {
|
258
|
+
"facecolor":{"func":_validate_color_or_none, "kwargs":{"none_ok":True}}, # any color value for matplotlib OR NONE
|
259
|
+
"edgecolor":{"func":_validate_color_or_none, "kwargs":{"none_ok":True}}, # any color value for matplotlib OR NONE
|
260
|
+
"alpha":{"func":_validate_range, "kwargs":{"min":0, "max":1, "none_ok":True}}, # any value between 0 and 1
|
261
|
+
"pad":{"func":_validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
|
262
|
+
"borderpad":{"func":_validate_range, "kwargs":{"min":0, "max":None}}, # between 0 and inf
|
263
|
+
"prop":{"func":matplotlib.rcsetup.validate_fontsize}, # any fontsize value for matplotlib
|
264
|
+
"frameon":{"func":_validate_type, "kwargs":{"match":bool}}, # any bool
|
265
|
+
"bbox_to_anchor":{"func":_skip_validation}, # TODO: Currently unvalidated! Make sure to remove from _validate_dict once updated!
|
266
|
+
"bbox_transform":{"func":_skip_validation} # TODO: Currently unvalidated! Make sure to remove from _validate_dict once updated!
|
267
|
+
}
|
268
|
+
|
269
|
+
_VALID_ROTATION_REFERENCE = get_args(_TYPE_ROTATION.__annotations__["reference"])
|
270
|
+
|
271
|
+
_VALIDATE_ROTATION = {
|
272
|
+
"degrees":{"func":_validate_range, "kwargs":{"min":-360, "max":360, "none_ok":True}}, # anything between -360 and 360, or None for "auto"
|
273
|
+
"crs":{"func":_validate_crs, "kwargs":{"none_ok":True}}, # see _validate_crs for details on what is accepted
|
274
|
+
"reference":{"func":_validate_list, "kwargs":{"list":_VALID_ROTATION_REFERENCE, "none_ok":True}}, # see _VALID_ROTATION_REFERENCE for accepted values
|
275
|
+
"coords":{"func":_validate_tuple, "kwargs":{"length":2, "types":[float, int], "none_ok":True}} # only required if degrees is None: should be a tuple of coordinates in the relevant reference window
|
276
|
+
}
|
277
|
+
|
278
|
+
### MORE VALIDITY FUNCTIONS ###
|
279
|
+
# These are more customized, and so are separated from the _validate_* functions above
|
280
|
+
# Mainly, they can process the input dictionaries wholesale, as well as the individual functions in it
|
281
|
+
|
282
|
+
def _validate_dict(input_dict, default_dict, functions, to_validate: list=None, return_clean=False, parse_false=True):
|
283
|
+
if input_dict == False:
|
284
|
+
if parse_false == True:
|
285
|
+
return None
|
286
|
+
else:
|
287
|
+
return False
|
288
|
+
elif input_dict is None or input_dict == True:
|
289
|
+
return default_dict
|
290
|
+
elif type(input_dict) != dict:
|
291
|
+
raise ValueError(f"A dictionary (NoneType) must be provided, please double-check your inputs")
|
292
|
+
else:
|
293
|
+
values = default_dict | input_dict
|
294
|
+
# Pre-checking that no invalid keys are passed
|
295
|
+
invalid = [key for key in values.keys() if key not in functions.keys() and key not in ["bbox_to_anchor", "bbox_transform"]]
|
296
|
+
if len(invalid) > 0:
|
297
|
+
print(f"Warning: Invalid keys detected ({invalid}). These will be ignored.")
|
298
|
+
# First, trimming our values to only those we need to validate
|
299
|
+
if to_validate is not None:
|
300
|
+
values = {key: val for key, val in values.items() if key in to_validate}
|
301
|
+
functions = {key: val for key, val in functions.items() if key in values.keys()}
|
302
|
+
else:
|
303
|
+
values = {key: val for key, val in values.items() if key in functions.keys()}
|
304
|
+
functions = {key: val for key, val in functions.items() if key in values.keys()}
|
305
|
+
# Now, running the function with the necessary kwargs
|
306
|
+
for key,val in values.items():
|
307
|
+
fd = functions[key]
|
308
|
+
func = fd["func"]
|
309
|
+
# NOTE: This is messy but the only way to get the rotation value to the crs function
|
310
|
+
if key=="crs":
|
311
|
+
_ = func(prop=key, val=val, rotation_dict=values, **fd["kwargs"])
|
312
|
+
# Our custom functions always have this dictionary key in them, so we know what form they take
|
313
|
+
elif "kwargs" in fd:
|
314
|
+
_ = func(prop=key, val=val, **fd["kwargs"])
|
315
|
+
# The matplotlib built-in functions DON'T have that, and only ever take the one value
|
316
|
+
else:
|
317
|
+
_ = func(val)
|
318
|
+
if return_clean==True:
|
319
|
+
return values
|
320
|
+
|
321
|
+
# This function can process the _VALIDATE dictionaries we established above, but for single variables at a time
|
322
|
+
def _validate(validate_dict, prop, val, return_val=True, kwargs={}):
|
323
|
+
fd = validate_dict[prop]
|
324
|
+
func = fd["func"]
|
325
|
+
# Our custom functions always have this dictionary key in them, so we know what form they take
|
326
|
+
if "kwargs" in fd:
|
327
|
+
val = func(prop=prop, val=val, **(fd["kwargs"] | kwargs))
|
328
|
+
# The matplotlib built-in functions DON'T have that, and only ever take the one value
|
329
|
+
else:
|
330
|
+
val = func(val)
|
331
|
+
if return_val==True:
|
332
|
+
return val
|