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.
@@ -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