1- """CRS detection utilities and optional pyproj import guard."""
1+ """CRS detection utilities and optional pyproj import guard.
2+
3+ Uses a two-tier strategy: try the lightweight built-in CRS first,
4+ then fall back to pyproj for codes/formats not in the built-in table.
5+ """
26from __future__ import annotations
37
8+ from xrspatial .reproject ._lite_crs import CRS as LiteCRS
49
5- def _require_pyproj ():
6- """Import and return the pyproj module, raising a clear error if missing."""
10+
11+ def _try_import_pyproj ():
12+ """Try to import pyproj, returning the module or None."""
713 try :
814 import pyproj
915 return pyproj
1016 except ImportError :
17+ return None
18+
19+
20+ def _require_pyproj ():
21+ """Import and return the pyproj module, raising a clear error if missing."""
22+ pyproj = _try_import_pyproj ()
23+ if pyproj is None :
1124 raise ImportError (
1225 "pyproj is required for CRS reprojection. "
1326 "Install it with: pip install pyproj "
1427 "or: pip install xarray-spatial[reproject]"
1528 )
29+ return pyproj
1630
1731
1832def _resolve_crs (crs_input ):
19- """Convert *crs_input* to a ``pyproj.CRS`` object.
20-
21- Accepts anything ``pyproj.CRS()`` accepts: EPSG int, authority string,
22- WKT, proj4 dict, or an existing ``pyproj.CRS`` instance.
23-
24- Returns None if *crs_input* is None.
33+ """Convert *crs_input* to a CRS object.
34+
35+ Resolution order:
36+
37+ 1. ``None`` passes through as ``None``.
38+ 2. An existing ``LiteCRS`` instance passes through unchanged.
39+ 3. An existing ``pyproj.CRS`` instance passes through unchanged
40+ (only checked when pyproj is importable).
41+ 4. Try ``LiteCRS(crs_input)`` -- covers EPSG ints and ``"EPSG:XXXX"``
42+ strings for codes in the built-in table.
43+ 5. Fall back to ``pyproj.CRS(crs_input)`` -- raises ``ImportError``
44+ if pyproj is not installed.
2545 """
2646 if crs_input is None :
2747 return None
28- pyproj = _require_pyproj ()
29- if isinstance (crs_input , pyproj .CRS ):
48+
49+ # Pass through existing LiteCRS
50+ if isinstance (crs_input , LiteCRS ):
51+ return crs_input
52+
53+ # Pass through existing pyproj.CRS (if pyproj available)
54+ pyproj = _try_import_pyproj ()
55+ if pyproj is not None and isinstance (crs_input , pyproj .CRS ):
3056 return crs_input
57+
58+ # Try lite CRS first
59+ try :
60+ return LiteCRS (crs_input )
61+ except (ValueError , TypeError ):
62+ pass
63+
64+ # Fall back to pyproj
65+ pyproj = _require_pyproj ()
3166 return pyproj .CRS (crs_input )
3267
3368
69+ def _crs_from_wkt (wkt ):
70+ """Build a CRS from an OGC WKT string.
71+
72+ Tries ``LiteCRS.from_wkt`` first (extracts the AUTHORITY tag),
73+ then falls back to ``pyproj.CRS.from_wkt``.
74+ """
75+ try :
76+ return LiteCRS .from_wkt (wkt )
77+ except (ValueError , TypeError ):
78+ pass
79+
80+ pyproj = _require_pyproj ()
81+ return pyproj .CRS .from_wkt (wkt )
82+
83+
3484def _detect_source_crs (raster ):
3585 """Auto-detect the CRS of a DataArray.
3686
@@ -47,7 +97,7 @@ def _detect_source_crs(raster):
4797
4898 crs_wkt = raster .attrs .get ('crs_wkt' )
4999 if crs_wkt is not None :
50- return _resolve_crs (crs_wkt )
100+ return _crs_from_wkt (crs_wkt )
51101
52102 # rioxarray fallback
53103 try :
0 commit comments