diff --git a/docs/history.rst b/docs/history.rst index c7292123b..c3673ce06 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -6,8 +6,9 @@ Latest - DEP: Minimum supported Python version 3.8 (issue #930) - DEP: Minimum PROJ version 8.0 (issue #940) - BUG: Prepend "Derived" to CRS type name if CRS is derived (issue #932) -- BUG: Improved handling of inf values in :attr:`pyproj.transformer.Transformer.transform_bounds` (pull #961) -- ENH: Add support for transforming bounds at the poles in :attr:`pyproj.transformer.Transformer.transform_bounds` (pull #962) +- BUG: Improved handling of inf values in :meth:`pyproj.transformer.Transformer.transform_bounds` (pull #961) +- ENH: Add support for transforming bounds at the poles in :meth:`pyproj.transformer.Transformer.transform_bounds` (pull #962) +- ENH: Added :attr:`pyproj.transformer.Transformer.source_crs` & :attr:`pyproj.transformer.Transformer.target_crs` (pull #976) 3.2.1 ------ diff --git a/pyproj/_crs.pxd b/pyproj/_crs.pxd index 30a6506f6..9b12c97d4 100644 --- a/pyproj/_crs.pxd +++ b/pyproj/_crs.pxd @@ -1,5 +1,9 @@ include "proj.pxi" + +from pyproj.enums import WktVersion + + cdef extern from "proj_experimental.h": PJ *proj_crs_promote_to_3D(PJ_CONTEXT *ctx, const char* crs_3D_name, @@ -13,6 +17,12 @@ cdef _to_proj4( object version, bint pretty, ) +cdef _to_wkt( + PJ_CONTEXT* context, + PJ* projobj, + object version, + bint pretty, +) cdef class Axis: cdef readonly str name diff --git a/pyproj/_crs.pyx b/pyproj/_crs.pyx index cd31baa56..fcc578564 100644 --- a/pyproj/_crs.pyx +++ b/pyproj/_crs.pyx @@ -69,8 +69,8 @@ def is_proj(str proj_string not None): cdef _to_wkt( PJ_CONTEXT* context, PJ* projobj, - object version=WktVersion.WKT2_2019, - bint pretty=False + object version, + bint pretty, ): """ Convert a PJ object to a wkt string. @@ -2557,12 +2557,10 @@ cdef class _CRS(Base): @property def source_crs(self): """ - The base CRS of a BoundCRS or a DerivedCRS/ProjectedCRS, - or the source CRS of a CoordinateOperation. - Returns ------- - _CRS + _CRS: + The base CRS of a BoundCRS or a DerivedCRS/ProjectedCRS. """ if self._source_crs is not None: return None if self._source_crs is False else self._source_crs @@ -2572,7 +2570,12 @@ cdef class _CRS(Base): self._source_crs = False return None try: - self._source_crs = _CRS(_to_wkt(self.context, projobj)) + self._source_crs = _CRS(_to_wkt( + self.context, + projobj, + version=WktVersion.WKT2_2019, + pretty=False, + )) finally: proj_destroy(projobj) return self._source_crs @@ -2585,7 +2588,7 @@ cdef class _CRS(Base): Returns ------- _CRS: - The hub CRS of a BoundCRS or the target CRS of a CoordinateOperation. + The hub CRS of a BoundCRS. """ if self._target_crs is not None: return None if self._target_crs is False else self._target_crs @@ -2595,7 +2598,12 @@ cdef class _CRS(Base): self._target_crs = False return None try: - self._target_crs = _CRS(_to_wkt(self.context, projobj)) + self._target_crs = _CRS(_to_wkt( + self.context, + projobj, + version=WktVersion.WKT2_2019, + pretty=False, + )) finally: proj_destroy(projobj) return self._target_crs @@ -2624,7 +2632,12 @@ cdef class _CRS(Base): self._sub_crs_list = [] while projobj != NULL: try: - self._sub_crs_list.append(_CRS(_to_wkt(self.context, projobj))) + self._sub_crs_list.append(_CRS(_to_wkt( + self.context, + projobj, + version=WktVersion.WKT2_2019, + pretty=False, + ))) finally: proj_destroy(projobj) # deallocate temp proj iii += 1 @@ -2655,7 +2668,12 @@ cdef class _CRS(Base): self._geodetic_crs = False return None try: - self._geodetic_crs = _CRS(_to_wkt(self.context, projobj)) + self._geodetic_crs = _CRS(_to_wkt( + self.context, + projobj, + version=WktVersion.WKT2_2019, + pretty=False, + )) finally: proj_destroy(projobj) # deallocate temp proj return self._geodetic_crs @@ -2905,7 +2923,12 @@ cdef class _CRS(Base): if projobj == NULL: return self try: - crs_3d = _CRS(_to_wkt(self.context, projobj)) + crs_3d = _CRS(_to_wkt( + self.context, + projobj, + version=WktVersion.WKT2_2019, + pretty=False, + )) finally: proj_destroy(projobj) return crs_3d diff --git a/pyproj/_transformer.pxd b/pyproj/_transformer.pxd index 4ff781515..242247d42 100644 --- a/pyproj/_transformer.pxd +++ b/pyproj/_transformer.pxd @@ -1,6 +1,6 @@ include "proj.pxi" -from pyproj._crs cimport Base +from pyproj._crs cimport _CRS, Base cdef class _TransformerGroup: @@ -14,6 +14,8 @@ cdef class _Transformer(Base): cdef readonly _area_of_use cdef readonly str type_name cdef readonly tuple _operations + cdef readonly _CRS _source_crs + cdef readonly _CRS _target_crs @staticmethod cdef _Transformer _from_pj( diff --git a/pyproj/_transformer.pyi b/pyproj/_transformer.pyi index 7dc2ac70e..d6c6da577 100644 --- a/pyproj/_transformer.pyi +++ b/pyproj/_transformer.pyi @@ -56,6 +56,10 @@ class _Transformer(Base): @property def area_of_use(self) -> AreaOfUse: ... @property + def source_crs(self) -> Optional[_CRS]: ... + @property + def target_crs(self) -> Optional[_CRS]: ... + @property def operations(self) -> Union[Tuple[CoordinateOperation], None]: ... @property def is_network_enabled(self) -> bool: ... diff --git a/pyproj/_transformer.pyx b/pyproj/_transformer.pyx index eab3a4867..c4dfa8f99 100644 --- a/pyproj/_transformer.pyx +++ b/pyproj/_transformer.pyx @@ -15,13 +15,14 @@ from pyproj._crs cimport ( CoordinateOperation, _get_concatenated_operations, _to_proj4, + _to_wkt, create_area_of_use, ) from pyproj._datadir cimport pyproj_context_create, pyproj_context_destroy from pyproj._datadir import _LOGGER from pyproj.aoi import AreaOfInterest -from pyproj.enums import ProjVersion, TransformDirection +from pyproj.enums import ProjVersion, TransformDirection, WktVersion from pyproj.exceptions import ProjError # version number string for PROJ @@ -798,6 +799,8 @@ cdef class _Transformer(Base): self._area_of_use = None self.type_name = "Unknown Transformer" self._operations = None + self._source_crs = None + self._target_crs = None def _initialize_from_projobj(self): self.proj_info = proj_pj_info(self.projobj) @@ -841,6 +844,62 @@ cdef class _Transformer(Base): self._area_of_use = create_area_of_use(self.context, self.projobj) return self._area_of_use + @property + def source_crs(self): + """ + .. versionadded:: 3.3.0 + + Returns + ------- + Optional[_CRS]: + The source CRS of a CoordinateOperation. + """ + if self._source_crs is not None: + return None if self._source_crs is False else self._source_crs + cdef PJ * projobj = proj_get_source_crs(self.context, self.projobj) + ProjError.clear() + if projobj == NULL: + self._source_crs = False + return None + try: + self._source_crs = _CRS(_to_wkt( + self.context, + projobj, + version=WktVersion.WKT2_2019, + pretty=False, + )) + finally: + proj_destroy(projobj) + return self._source_crs + + @property + def target_crs(self): + """ + .. versionadded:: 3.3.0 + + Returns + ------- + Optional[_CRS]: + The target CRS of a CoordinateOperation. + """ + if self._target_crs is not None: + return None if self._target_crs is False else self._target_crs + cdef PJ * projobj = proj_get_target_crs(self.context, self.projobj) + ProjError.clear() + if projobj == NULL: + self._target_crs = False + return None + try: + self._target_crs = _CRS(_to_wkt( + self.context, + projobj, + version=WktVersion.WKT2_2019, + pretty=False, + )) + finally: + proj_destroy(projobj) + return self._target_crs + @property def operations(self): """ diff --git a/pyproj/transformer.py b/pyproj/transformer.py index 6c1210efa..630dfbb48 100644 --- a/pyproj/transformer.py +++ b/pyproj/transformer.py @@ -416,6 +416,38 @@ def is_network_enabled(self) -> bool: """ return self._transformer.is_network_enabled + @property + def source_crs(self) -> Optional[CRS]: + """ + .. versionadded:: 3.3.0 + + Returns + ------- + Optional[CRS]: + The source CRS of a CoordinateOperation. + """ + return ( + None + if self._transformer.source_crs is None + else CRS(self._transformer.source_crs) + ) + + @property + def target_crs(self) -> Optional[CRS]: + """ + .. versionadded:: 3.3.0 + + Returns + ------- + Optional[CRS]: + The target CRS of a CoordinateOperation. + """ + return ( + None + if self._transformer.target_crs is None + else CRS(self._transformer.target_crs) + ) + @staticmethod def from_proj( proj_from: Any, diff --git a/test/test_transformer.py b/test/test_transformer.py index 933225430..99d47c115 100644 --- a/test/test_transformer.py +++ b/test/test_transformer.py @@ -1522,3 +1522,15 @@ def test_4d_transform__inplace__numpy__int(): assert zarr[0] == 5264462 assert tarr is not t_tarr assert tarr[0] == 2019 + + +def test_transformer_source_target_crs(): + transformer = Transformer.from_crs("EPSG:4326", "EPSG:4258") + assert transformer.source_crs == "EPSG:4326" + assert transformer.target_crs == "EPSG:4258" + + +def test_transformer_source_target_crs__none(): + transformer = Transformer.from_pipeline("+init=ITRF2008:ITRF2000") + assert transformer.source_crs is None + assert transformer.target_crs is None