@@ -440,6 +440,32 @@ def _get_bounds(obj: DataArray | Dataset, key: Hashable) -> list[Hashable]:
440
440
return list (results )
441
441
442
442
443
+ def _parse_grid_mapping_attribute (grid_mapping_attr : str ) -> list [str ]:
444
+ """
445
+ Parse a grid_mapping attribute that may contain multiple grid mappings.
446
+
447
+ The attribute has the format: "grid_mapping_variable_name: optional_coordinate_names_space_separated"
448
+ Multiple sections are separated by colons.
449
+
450
+ Examples:
451
+ - Single: "spatial_ref"
452
+ - Multiple: "spatial_ref: crs_4326: latitude longitude crs_27700: x27700 y27700"
453
+
454
+ Returns a list of grid mapping variable names.
455
+ """
456
+ # Check if there are colons indicating multiple mappings
457
+ if ":" not in grid_mapping_attr :
458
+ return [grid_mapping_attr .strip ()]
459
+
460
+ # Use regex to find grid mapping variable names
461
+ # Pattern matches: word at start OR word that comes after some coordinate names and before ":"
462
+ # This handles cases like "spatial_ref: crs_4326: latitude longitude crs_27700: x27700 y27700"
463
+ pattern = r"(?:^|\s)([a-zA-Z_][a-zA-Z0-9_]*)(?=\s*:)"
464
+ matches = re .findall (pattern , grid_mapping_attr )
465
+
466
+ return matches if matches else [grid_mapping_attr .strip ()]
467
+
468
+
443
469
def _get_grid_mapping_name (obj : DataArray | Dataset , key : str ) -> list [str ]:
444
470
"""
445
471
Translate from grid mapping name attribute to appropriate variable name.
@@ -467,13 +493,17 @@ def _get_grid_mapping_name(obj: DataArray | Dataset, key: str) -> list[str]:
467
493
for var in variables .values ():
468
494
attrs_or_encoding = ChainMap (var .attrs , var .encoding )
469
495
if "grid_mapping" in attrs_or_encoding :
470
- grid_mapping_var_name = attrs_or_encoding ["grid_mapping" ]
471
- if grid_mapping_var_name not in variables :
472
- raise ValueError (
473
- f"{ var } defines non-existing grid_mapping variable { grid_mapping_var_name } ."
474
- )
475
- if key == variables [grid_mapping_var_name ].attrs ["grid_mapping_name" ]:
476
- results .update ([grid_mapping_var_name ])
496
+ grid_mapping_attr = attrs_or_encoding ["grid_mapping" ]
497
+ # Parse potentially multiple grid mappings
498
+ grid_mapping_var_names = _parse_grid_mapping_attribute (grid_mapping_attr )
499
+
500
+ for grid_mapping_var_name in grid_mapping_var_names :
501
+ if grid_mapping_var_name not in variables :
502
+ raise ValueError (
503
+ f"{ var } defines non-existing grid_mapping variable { grid_mapping_var_name } ."
504
+ )
505
+ if key == variables [grid_mapping_var_name ].attrs ["grid_mapping_name" ]:
506
+ results .update ([grid_mapping_var_name ])
477
507
return list (results )
478
508
479
509
@@ -1943,9 +1973,34 @@ def get_associated_variable_names(
1943
1973
if dbounds := self ._obj [dim ].attrs .get ("bounds" , None ):
1944
1974
coords ["bounds" ].append (dbounds )
1945
1975
1946
- for attrname in ["grid" , "grid_mapping" ]:
1947
- if maybe := attrs_or_encoding .get (attrname , None ):
1948
- coords [attrname ] = [maybe ]
1976
+ if grid := attrs_or_encoding .get ("grid" , None ):
1977
+ coords ["grid" ] = [grid ]
1978
+
1979
+ if grid_mapping_attr := attrs_or_encoding .get ("grid_mapping" , None ):
1980
+ # Parse grid mapping variables using the same function
1981
+ grid_mapping_vars = _parse_grid_mapping_attribute (grid_mapping_attr )
1982
+ coords ["grid_mapping" ] = grid_mapping_vars
1983
+
1984
+ # Extract coordinate variables using regex
1985
+ if ":" in grid_mapping_attr :
1986
+ # Pattern to find coordinate variables: words that come after ":" but before next grid mapping variable
1987
+ # This captures coordinate variables between grid mapping sections
1988
+ coord_pattern = r":\s+([^:]+?)(?=\s+[a-zA-Z_][a-zA-Z0-9_]*\s*:|$)"
1989
+ coord_matches = re .findall (coord_pattern , grid_mapping_attr )
1990
+
1991
+ for coord_section in coord_matches :
1992
+ # Split each coordinate section and add valid coordinate names
1993
+ coord_vars = coord_section .split ()
1994
+ # Filter out grid mapping variable names that might have been captured
1995
+ coord_vars = [
1996
+ var
1997
+ for var in coord_vars
1998
+ if not (
1999
+ var .startswith (("crs_" , "spatial_" , "proj_" ))
2000
+ and var in grid_mapping_vars
2001
+ )
2002
+ ]
2003
+ coords ["coordinates" ].extend (coord_vars )
1949
2004
1950
2005
more : Sequence [Hashable ] = ()
1951
2006
if geometry_var := attrs_or_encoding .get ("geometry" , None ):
@@ -2899,6 +2954,60 @@ def formula_terms(self) -> dict[str, str]: # numpydoc ignore=SS06
2899
2954
terms [key ] = value
2900
2955
return terms
2901
2956
2957
+ @property
2958
+ def grid_mapping_names (self ) -> dict [str , list [str ]]:
2959
+ """
2960
+ Mapping the CF grid mapping name to the grid mapping variable name.
2961
+
2962
+ Returns
2963
+ -------
2964
+ dict
2965
+ Dictionary mapping the CF grid mapping name to the variable name containing
2966
+ the grid mapping attributes.
2967
+
2968
+ See Also
2969
+ --------
2970
+ DataArray.cf.grid_mapping_name
2971
+ Dataset.cf.grid_mapping_names
2972
+
2973
+ References
2974
+ ----------
2975
+ https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#appendix-grid-mappings
2976
+
2977
+ Examples
2978
+ --------
2979
+ >>> from cf_xarray.datasets import hrrrds
2980
+ >>> hrrrds.foo.cf.grid_mapping_names
2981
+ {'latitude_longitude': ['crs_4326'], 'lambert_azimuthal_equal_area': ['spatial_ref']}
2982
+ """
2983
+ da = self ._obj
2984
+ attrs_or_encoding = ChainMap (da .attrs , da .encoding )
2985
+ grid_mapping_attr = attrs_or_encoding .get ("grid_mapping" , None )
2986
+
2987
+ if not grid_mapping_attr :
2988
+ return {}
2989
+
2990
+ # Parse potentially multiple grid mappings
2991
+ grid_mapping_var_names = _parse_grid_mapping_attribute (grid_mapping_attr )
2992
+
2993
+ results = {}
2994
+ for grid_mapping_var_name in grid_mapping_var_names :
2995
+ # First check if it's in the DataArray's coords (for multiple grid mappings
2996
+ # that are coordinates of the DataArray)
2997
+ if grid_mapping_var_name in da .coords :
2998
+ grid_mapping_var = da .coords [grid_mapping_var_name ]
2999
+ if "grid_mapping_name" in grid_mapping_var .attrs :
3000
+ gmn = grid_mapping_var .attrs ["grid_mapping_name" ]
3001
+ if gmn not in results :
3002
+ results [gmn ] = [grid_mapping_var_name ]
3003
+ else :
3004
+ results [gmn ].append (grid_mapping_var_name )
3005
+ # For standalone DataArrays, the grid mapping variables may not be available
3006
+ # This is a limitation of the xarray data model - when you extract a DataArray
3007
+ # from a Dataset, it doesn't carry over non-coordinate variables
3008
+
3009
+ return results
3010
+
2902
3011
@property
2903
3012
def grid_mapping_name (self ) -> str :
2904
3013
"""
@@ -2911,6 +3020,7 @@ def grid_mapping_name(self) -> str:
2911
3020
2912
3021
See Also
2913
3022
--------
3023
+ DataArray.cf.grid_mapping_names
2914
3024
Dataset.cf.grid_mapping_names
2915
3025
2916
3026
Examples
@@ -2920,19 +3030,22 @@ def grid_mapping_name(self) -> str:
2920
3030
'rotated_latitude_longitude'
2921
3031
"""
2922
3032
2923
- da = self ._obj
3033
+ # Use grid_mapping_names under the hood
3034
+ grid_mapping_names = self .grid_mapping_names
2924
3035
2925
- attrs_or_encoding = ChainMap (da .attrs , da .encoding )
2926
- grid_mapping = attrs_or_encoding .get ("grid_mapping" , None )
2927
- if not grid_mapping :
3036
+ if not grid_mapping_names :
2928
3037
raise ValueError ("No 'grid_mapping' attribute present." )
2929
3038
2930
- if grid_mapping not in da ._coords :
2931
- raise ValueError (f"Grid Mapping variable { grid_mapping } not present." )
2932
-
2933
- grid_mapping_var = da [grid_mapping ]
3039
+ if len (grid_mapping_names ) > 1 :
3040
+ # Get the variable names for error message
3041
+ all_vars = list (itertools .chain .from_iterable (grid_mapping_names .values ()))
3042
+ raise ValueError (
3043
+ f"Multiple grid mappings found: { all_vars } . "
3044
+ "Please use DataArray.cf.grid_mapping_names instead."
3045
+ )
2934
3046
2935
- return grid_mapping_var .attrs ["grid_mapping_name" ]
3047
+ # Return the single grid mapping name
3048
+ return next (iter (grid_mapping_names .keys ()))
2936
3049
2937
3050
def __getitem__ (self , key : Hashable | Iterable [Hashable ]) -> DataArray :
2938
3051
"""
0 commit comments