2
2
3
3
import warnings
4
4
from collections .abc import Hashable , Mapping , Sequence
5
- from typing import Any , Callable
5
+ from typing import TYPE_CHECKING , Any , Callable , cast
6
6
7
7
import numpy as np
8
8
import pandas as pd
13
13
from .index import GeometryIndex
14
14
from .zonal import _zonal_stats_iterative , _zonal_stats_rasterize
15
15
16
+ if TYPE_CHECKING :
17
+ from geopandas import GeoDataFrame
18
+
16
19
17
20
@xr .register_dataarray_accessor ("xvec" )
18
21
@xr .register_dataset_accessor ("xvec" )
@@ -22,7 +25,7 @@ class XvecAccessor:
22
25
Currently works on coordinates with :class:`xvec.GeometryIndex`.
23
26
"""
24
27
25
- def __init__ (self , xarray_obj : xr .Dataset | xr .DataArray ):
28
+ def __init__ (self , xarray_obj : xr .Dataset | xr .DataArray ) -> None :
26
29
"""xvec init, nothing to be done here."""
27
30
self ._obj = xarray_obj
28
31
self ._geom_coords_all = [
@@ -36,7 +39,9 @@ def __init__(self, xarray_obj: xr.Dataset | xr.DataArray):
36
39
if self .is_geom_variable (name , has_index = True )
37
40
]
38
41
39
- def is_geom_variable (self , name : Hashable , has_index : bool = True ):
42
+ def is_geom_variable (
43
+ self , name : Hashable , has_index : bool = True
44
+ ) -> bool | np .bool_ :
40
45
"""Check if coordinate variable is composed of :class:`shapely.Geometry`.
41
46
42
47
Can return all such variables or only those using :class:`~xvec.GeometryIndex`.
@@ -208,7 +213,7 @@ def to_crs(
208
213
self ,
209
214
variable_crs : Mapping [Any , Any ] | None = None ,
210
215
** variable_crs_kwargs : Any ,
211
- ):
216
+ ) -> xr . DataArray | xr . Dataset :
212
217
"""
213
218
Transform :class:`shapely.Geometry` objects of a variable to a new coordinate
214
219
reference system.
@@ -313,20 +318,15 @@ def to_crs(
313
318
currently wraps :meth:`Dataset.assign_coords <xarray.Dataset.assign_coords>`
314
319
or :meth:`DataArray.assign_coords <xarray.DataArray.assign_coords>`.
315
320
"""
316
- if variable_crs and variable_crs_kwargs :
317
- raise ValueError (
318
- "Cannot specify both keyword and positional arguments to "
319
- "'.xvec.to_crs'."
320
- )
321
+ variable_crs_solved = _resolve_input (
322
+ variable_crs , variable_crs_kwargs , "to_crs"
323
+ )
321
324
322
325
_obj = self ._obj .copy (deep = False )
323
326
324
- if variable_crs_kwargs :
325
- variable_crs = variable_crs_kwargs
326
-
327
327
transformed = {}
328
328
329
- for key , crs in variable_crs .items ():
329
+ for key , crs in variable_crs_solved .items ():
330
330
if not isinstance (self ._obj .xindexes [key ], GeometryIndex ):
331
331
raise ValueError (
332
332
f"The index '{ key } ' is not an xvec.GeometryIndex. "
@@ -335,7 +335,7 @@ def to_crs(
335
335
)
336
336
337
337
data = _obj [key ]
338
- data_crs = self ._obj .xindexes [key ].crs
338
+ data_crs = self ._obj .xindexes [key ].crs # type: ignore
339
339
340
340
# transformation code taken from geopandas (BSD 3-clause license)
341
341
if data_crs is None :
@@ -374,21 +374,21 @@ def to_crs(
374
374
for key , (result , _crs ) in transformed .items ():
375
375
_obj = _obj .assign_coords ({key : result })
376
376
377
- _obj = _obj .drop_indexes (variable_crs .keys ())
377
+ _obj = _obj .drop_indexes (variable_crs_solved .keys ())
378
378
379
- for key , crs in variable_crs .items ():
379
+ for key , crs in variable_crs_solved .items ():
380
380
if crs :
381
381
_obj [key ].attrs ["crs" ] = CRS .from_user_input (crs )
382
- _obj = _obj .set_xindex (key , GeometryIndex , crs = crs )
382
+ _obj = _obj .set_xindex ([ key ] , GeometryIndex , crs = crs )
383
383
384
384
return _obj
385
385
386
386
def set_crs (
387
387
self ,
388
388
variable_crs : Mapping [Any , Any ] | None = None ,
389
- allow_override = False ,
389
+ allow_override : bool = False ,
390
390
** variable_crs_kwargs : Any ,
391
- ):
391
+ ) -> xr . DataArray | xr . Dataset :
392
392
"""Set the Coordinate Reference System (CRS) of coordinates backed by
393
393
:class:`~xvec.GeometryIndex`.
394
394
@@ -480,27 +480,21 @@ def set_crs(
480
480
transform the geometries to a new CRS, use the :meth:`to_crs`
481
481
method.
482
482
"""
483
-
484
- if variable_crs and variable_crs_kwargs :
485
- raise ValueError (
486
- "Cannot specify both keyword and positional arguments to "
487
- ".xvec.set_crs."
488
- )
483
+ variable_crs_solved = _resolve_input (
484
+ variable_crs , variable_crs_kwargs , "set_crs"
485
+ )
489
486
490
487
_obj = self ._obj .copy (deep = False )
491
488
492
- if variable_crs_kwargs :
493
- variable_crs = variable_crs_kwargs
494
-
495
- for key , crs in variable_crs .items ():
489
+ for key , crs in variable_crs_solved .items ():
496
490
if not isinstance (self ._obj .xindexes [key ], GeometryIndex ):
497
491
raise ValueError (
498
492
f"The index '{ key } ' is not an xvec.GeometryIndex. "
499
493
"Set the xvec.GeometryIndex using '.xvec.set_geom_indexes' before "
500
494
"handling projection information."
501
495
)
502
496
503
- data_crs = self ._obj .xindexes [key ].crs
497
+ data_crs = self ._obj .xindexes [key ].crs # type: ignore
504
498
505
499
if not allow_override and data_crs is not None and not data_crs == crs :
506
500
raise ValueError (
@@ -510,23 +504,23 @@ def set_crs(
510
504
"want to transform the geometries, use '.xvec.to_crs' instead."
511
505
)
512
506
513
- _obj = _obj .drop_indexes (variable_crs .keys ())
507
+ _obj = _obj .drop_indexes (variable_crs_solved .keys ())
514
508
515
- for key , crs in variable_crs .items ():
509
+ for key , crs in variable_crs_solved .items ():
516
510
if crs :
517
511
_obj [key ].attrs ["crs" ] = CRS .from_user_input (crs )
518
- _obj = _obj .set_xindex (key , GeometryIndex , crs = crs )
512
+ _obj = _obj .set_xindex ([ key ] , GeometryIndex , crs = crs )
519
513
520
514
return _obj
521
515
522
516
def query (
523
517
self ,
524
518
coord_name : str ,
525
519
geometry : shapely .Geometry | Sequence [shapely .Geometry ],
526
- predicate : str = None ,
527
- distance : float | Sequence [float ] = None ,
528
- unique = False ,
529
- ):
520
+ predicate : str | None = None ,
521
+ distance : float | Sequence [float ] | None = None ,
522
+ unique : bool = False ,
523
+ ) -> xr . DataArray | xr . Dataset :
530
524
"""Return a subset of a DataArray/Dataset filtered using a spatial query on
531
525
:class:`~xvec.GeometryIndex`.
532
526
@@ -619,12 +613,12 @@ def query(
619
613
620
614
"""
621
615
if isinstance (geometry , shapely .Geometry ):
622
- ilocs = self ._obj .xindexes [coord_name ].sindex .query (
616
+ ilocs = self ._obj .xindexes [coord_name ].sindex .query ( # type: ignore
623
617
geometry , predicate = predicate , distance = distance
624
618
)
625
619
626
620
else :
627
- _ , ilocs = self ._obj .xindexes [coord_name ].sindex .query (
621
+ _ , ilocs = self ._obj .xindexes [coord_name ].sindex .query ( # type: ignore
628
622
geometry , predicate = predicate , distance = distance
629
623
)
630
624
if unique :
@@ -634,11 +628,11 @@ def query(
634
628
635
629
def set_geom_indexes (
636
630
self ,
637
- coord_names : str | Sequence [Hashable ],
631
+ coord_names : str | Sequence [str ],
638
632
crs : Any = None ,
639
633
allow_override : bool = False ,
640
- ** kwargs ,
641
- ):
634
+ ** kwargs : dict [ str , Any ] ,
635
+ ) -> xr . DataArray | xr . Dataset :
642
636
"""Set a new :class:`~xvec.GeometryIndex` for one or more existing
643
637
coordinate(s). One :class:`~xvec.GeometryIndex` is set per coordinate. Only
644
638
1-dimensional coordinates are supported.
@@ -691,7 +685,7 @@ def set_geom_indexes(
691
685
692
686
for coord in coord_names :
693
687
if isinstance (self ._obj .xindexes [coord ], GeometryIndex ):
694
- data_crs = self ._obj .xindexes [coord ].crs
688
+ data_crs = self ._obj .xindexes [coord ].crs # type: ignore
695
689
696
690
if not allow_override and data_crs is not None and not data_crs == crs :
697
691
raise ValueError (
@@ -710,7 +704,7 @@ def set_geom_indexes(
710
704
711
705
return _obj
712
706
713
- def to_geopandas (self ):
707
+ def to_geopandas (self ) -> GeoDataFrame | pd . DataFrame :
714
708
"""Convert this array into a GeoPandas :class:`~geopandas.GeoDataFrame`
715
709
716
710
Returns a :class:`~geopandas.GeoDataFrame` with coordinates based on a
@@ -762,11 +756,11 @@ def to_geopandas(self):
762
756
if len (self ._geom_indexes ):
763
757
if self ._obj .ndim == 1 :
764
758
gdf = self ._obj .to_pandas ()
765
- elif self . _obj . ndim == 2 :
759
+ else :
766
760
gdf = self ._obj .to_pandas ()
767
761
if gdf .columns .name == self ._geom_indexes [0 ]:
768
762
gdf = gdf .T
769
- return gdf .reset_index ().set_geometry (
763
+ return gdf .reset_index ().set_geometry ( # type: ignore
770
764
self ._geom_indexes [0 ],
771
765
crs = self ._obj .xindexes [self ._geom_indexes [0 ]].crs ,
772
766
)
@@ -790,7 +784,7 @@ def to_geopandas(self):
790
784
if index_name in self ._geom_coords_all :
791
785
return gdf .reset_index ().set_geometry (
792
786
index_name , crs = self ._obj [index_name ].attrs .get ("crs" , None )
793
- )
787
+ ) # type: ignore
794
788
795
789
warnings .warn (
796
790
"No active geometry column to be set. The resulting object "
@@ -810,7 +804,7 @@ def to_geodataframe(
810
804
dim_order : Sequence [Hashable ] | None = None ,
811
805
geometry : Hashable | None = None ,
812
806
long : bool = True ,
813
- ):
807
+ ) -> GeoDataFrame | pd . DataFrame :
814
808
"""Convert this array and its coordinates into a tidy geopandas.GeoDataFrame.
815
809
816
810
The GeoDataFrame is indexed by the Cartesian product of index coordinates
@@ -884,7 +878,7 @@ def to_geodataframe(
884
878
level
885
879
for level in df .index .names
886
880
if level not in self ._geom_coords_all
887
- ]
881
+ ] # type: ignore
888
882
)
889
883
890
884
if isinstance (df .index , pd .MultiIndex ):
@@ -907,7 +901,7 @@ def to_geodataframe(
907
901
if geometry is not None :
908
902
return df .set_geometry (
909
903
geometry , crs = self ._obj [geometry ].attrs .get ("crs" , None )
910
- )
904
+ ) # type: ignore
911
905
912
906
warnings .warn (
913
907
"No active geometry column to be set. The resulting object "
@@ -926,12 +920,12 @@ def zonal_stats(
926
920
y_coords : Hashable ,
927
921
stats : str | Callable | Sequence [str | Callable | tuple ] = "mean" ,
928
922
name : Hashable = "geometry" ,
929
- index : bool = None ,
923
+ index : bool | None = None ,
930
924
method : str = "rasterize" ,
931
925
all_touched : bool = False ,
932
926
n_jobs : int = - 1 ,
933
- ** kwargs ,
934
- ):
927
+ ** kwargs : dict [ str , Any ] ,
928
+ ) -> xr . DataArray | xr . Dataset :
935
929
"""Extract the values from a dataset indexed by a set of geometries
936
930
937
931
Given an object indexed by x and y coordinates (or latitude and longitude), such
@@ -1121,9 +1115,9 @@ def extract_points(
1121
1115
y_coords : Hashable ,
1122
1116
tolerance : float | None = None ,
1123
1117
name : str = "geometry" ,
1124
- crs : Any = None ,
1125
- index : bool = None ,
1126
- ):
1118
+ crs : Any | None = None ,
1119
+ index : bool | None = None ,
1120
+ ) -> xr . DataArray | xr . Dataset :
1127
1121
"""Extract points from a DataArray or a Dataset indexed by spatial coordinates
1128
1122
1129
1123
Given an object indexed by x and y coordinates (or latitude and longitude), such
@@ -1263,3 +1257,22 @@ def extract_points(
1263
1257
}
1264
1258
)
1265
1259
return result
1260
+
1261
+
1262
+ def _resolve_input (
1263
+ positional : Mapping [Any , Any ] | None ,
1264
+ keyword : Mapping [str , Any ],
1265
+ func_name : str ,
1266
+ ) -> Mapping [Hashable , Any ]:
1267
+ """Resolve combination of positional and keyword arguments.
1268
+
1269
+ Based on xarray's ``either_dict_or_kwargs``.
1270
+ """
1271
+ if positional and keyword :
1272
+ raise ValueError (
1273
+ "Cannot specify both keyword and positional arguments to "
1274
+ f"'.xvec.{ func_name } '."
1275
+ )
1276
+ if positional is None or positional == {}:
1277
+ return cast (Mapping [Hashable , Any ], keyword )
1278
+ return positional
0 commit comments