From 52cba958c24ea78b4303f43dd499356b4c6bc547 Mon Sep 17 00:00:00 2001 From: "Alan D. Snow" Date: Tue, 27 Dec 2022 15:08:12 -0600 Subject: [PATCH] BUG: Fix Derived Projected CRS support (#1222) --- docs/history.rst | 2 +- pyproj/_crs.pyx | 20 ++++++++++++++-- pyproj/database.pyx | 18 ++++++++++++-- pyproj/enums.py | 1 + pyproj/proj.pxi | 1 + test/crs/test_crs.py | 48 +++++++++++++++++++++++++++++++++++++- test/crs/test_crs_maker.py | 4 ++-- test/test_database.py | 21 +++++++++++++++++ 8 files changed, 107 insertions(+), 8 deletions(-) diff --git a/docs/history.rst b/docs/history.rst index a7e024643..0da2b8e29 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -10,7 +10,7 @@ Latest - PERF: Optimize point transformations (pull #1204) - REF: Raise error when :meth:`.CRS.to_wkt`, :meth:`.CRS.to_json`, or :meth:`.CRS.to_proj4` returns None (issue #1036) - CLN: Remove `AzumuthalEquidistantConversion` & :class:`LambertAzumuthalEqualAreaConversion`. :class:`AzimuthalEquidistantConversion` & :class:`LambertAzimuthalEqualAreaConversion` should be used instead (pull #1219) - +- BUG: Fix Derived Projected CRS support (issue #1182) 3.4.1 ----- diff --git a/pyproj/_crs.pyx b/pyproj/_crs.pyx index 5fbcda83e..13705707d 100644 --- a/pyproj/_crs.pyx +++ b/pyproj/_crs.pyx @@ -2332,6 +2332,8 @@ cdef dict _CRS_TYPE_MAP = { PJ_TYPE_OTHER_CRS: "Other CRS", } +IF (CTE_PROJ_VERSION_MAJOR, CTE_PROJ_VERSION_MINOR) >= (9, 2): + _CRS_TYPE_MAP[PJ_TYPE_DERIVED_PROJECTED_CRS] = "Derived Projected CRS" cdef class _CRS(Base): """ @@ -2382,8 +2384,22 @@ cdef class _CRS(Base): if self._type_name is not None: return self._type_name self._type_name = _CRS_TYPE_MAP[self._type] - if self.is_derived: - self._type_name = f"Derived {self._type_name}" + if not self.is_derived or self._type == PJ_TYPE_PROJECTED_CRS: + # Projected CRS are derived by definition + # https://github.com/OSGeo/PROJ/issues/3525#issuecomment-1365790999 + return self._type_name + + # Handle Derived Projected CRS + # https://github.com/OSGeo/PROJ/issues/3525#issuecomment-1366002289 + IF (CTE_PROJ_VERSION_MAJOR, CTE_PROJ_VERSION_MINOR) < (9, 2): + if self._type == PJ_TYPE_OTHER_CRS: + self._type_name = "Derived Projected CRS" + return self._type_name + ELSE: + if self._type == PJ_TYPE_DERIVED_PROJECTED_CRS: + return self._type_name + + self._type_name = f"Derived {self._type_name}" return self._type_name @property diff --git a/pyproj/database.pyx b/pyproj/database.pyx index 186fb44c7..80c75570c 100644 --- a/pyproj/database.pyx +++ b/pyproj/database.pyx @@ -40,9 +40,23 @@ cdef dict _PJ_TYPE_MAP = { PJType.CONCATENATED_OPERATION: PJ_TYPE_CONCATENATED_OPERATION, PJType.OTHER_COORDINATE_OPERATION: PJ_TYPE_OTHER_COORDINATE_OPERATION, } +IF (CTE_PROJ_VERSION_MAJOR, CTE_PROJ_VERSION_MINOR) >= (9, 2): + _PJ_TYPE_MAP[PJType.DERIVED_PROJECTED_CRS] = PJ_TYPE_DERIVED_PROJECTED_CRS + cdef dict _INV_PJ_TYPE_MAP = {value: key for key, value in _PJ_TYPE_MAP.items()} +cdef PJ_TYPE get_pj_type(pj_type) except *: + if not isinstance(pj_type, PJType): + pj_type = PJType.create(pj_type) + IF (CTE_PROJ_VERSION_MAJOR, CTE_PROJ_VERSION_MINOR) < (9, 2): + if pj_type is PJType.DERIVED_PROJECTED_CRS: + raise NotImplementedError( + "DERIVED_PROJECTED_CRS requires PROJ 9.2+" + ) + return _PJ_TYPE_MAP[pj_type] + + def get_authorities(): """ .. versionadded:: 2.4.0 @@ -92,7 +106,7 @@ def get_codes(str auth_name not None, pj_type not None, bint allow_deprecated=Fa Codes associated with authorities in PROJ database. """ cdef PJ_CONTEXT* context = NULL - cdef PJ_TYPE cpj_type = _PJ_TYPE_MAP[PJType.create(pj_type)] + cdef PJ_TYPE cpj_type = get_pj_type(pj_type) cdef PROJ_STRING_LIST proj_code_list = NULL try: context = pyproj_context_create() @@ -210,7 +224,7 @@ def query_crs_info( pj_type_count * sizeof(PJ_TYPE) ) for iii in range(pj_type_count): - pj_type_list[iii] = _PJ_TYPE_MAP[PJType.create(pj_types[iii])] + pj_type_list[iii] = get_pj_type(pj_types[iii]) context = pyproj_context_create() query_params = proj_get_crs_list_parameters_create() diff --git a/pyproj/enums.py b/pyproj/enums.py index 5090c3bbd..e0cbad945 100644 --- a/pyproj/enums.py +++ b/pyproj/enums.py @@ -140,6 +140,7 @@ class PJType(BaseEnum): GEOGRAPHIC_3D_CRS = "GEOGRAPHIC_3D_CRS" VERTICAL_CRS = "VERTICAL_CRS" PROJECTED_CRS = "PROJECTED_CRS" + DERIVED_PROJECTED_CRS = "DERIVED_PROJECTED_CRS" COMPOUND_CRS = "COMPOUND_CRS" TEMPORAL_CRS = "TEMPORAL_CRS" ENGINEERING_CRS = "ENGINEERING_CRS" diff --git a/pyproj/proj.pxi b/pyproj/proj.pxi index 81b957686..983de4431 100644 --- a/pyproj/proj.pxi +++ b/pyproj/proj.pxi @@ -213,6 +213,7 @@ cdef extern from "proj.h" nogil: PJ_TYPE_TEMPORAL_DATUM PJ_TYPE_ENGINEERING_DATUM PJ_TYPE_PARAMETRIC_DATUM + PJ_TYPE_DERIVED_PROJECTED_CRS PJ_TYPE proj_get_type(const PJ *obj) const char* proj_get_name(const PJ *obj) diff --git a/test/crs/test_crs.py b/test/crs/test_crs.py index 94591a717..cf3acc399 100644 --- a/test/crs/test_crs.py +++ b/test/crs/test_crs.py @@ -1235,7 +1235,7 @@ def test_from_authority__ignf(): def test_ignf_authority_repr(): assert repr(CRS.from_authority("IGNF", "ETRS89UTM28")).startswith( - "" + "" ) @@ -1270,6 +1270,52 @@ def test_crs_is_exact_same__non_crs_input(): assert not CRS(4326).is_exact_same("+init=epsg:4326") +def test_derived_projected_crs(): + wkt = ( + 'DERIVEDPROJCRS["derived projectedCRS",\n' + ' BASEPROJCRS["WGS 84 / UTM zone 31N",\n' + ' BASEGEOGCRS["WGS 84",\n' + ' DATUM["World Geodetic System 1984",\n' + ' ELLIPSOID["WGS 84",6378137,298.257223563,\n' + ' LENGTHUNIT["metre",1]]],\n' + ' PRIMEM["Greenwich",0,\n' + ' ANGLEUNIT["degree",0.0174532925199433]]],\n' + ' CONVERSION["UTM zone 31N",\n' + ' METHOD["Transverse Mercator",\n' + ' ID["EPSG",9807]],\n' + ' PARAMETER["Latitude of natural origin",0,\n' + ' ANGLEUNIT["degree",0.0174532925199433],\n' + ' ID["EPSG",8801]],\n' + ' PARAMETER["Longitude of natural origin",3,\n' + ' ANGLEUNIT["degree",0.0174532925199433],\n' + ' ID["EPSG",8802]],\n' + ' PARAMETER["Scale factor at natural origin",0.9996,\n' + ' SCALEUNIT["unity",1],\n' + ' ID["EPSG",8805]],\n' + ' PARAMETER["False easting",500000,\n' + ' LENGTHUNIT["metre",1],\n' + ' ID["EPSG",8806]],\n' + ' PARAMETER["False northing",0,\n' + ' LENGTHUNIT["metre",1],\n' + ' ID["EPSG",8807]]]],\n' + ' DERIVINGCONVERSION["unnamed",\n' + ' METHOD["PROJ unimplemented"],\n' + ' PARAMETER["foo",1.0,UNIT["metre",1]]],\n' + " CS[Cartesian,2],\n" + ' AXIS["(E)",east,\n' + " ORDER[1],\n" + ' LENGTHUNIT["metre",1,\n' + ' ID["EPSG",9001]]],\n' + ' AXIS["(N)",north,\n' + " ORDER[2],\n" + ' LENGTHUNIT["metre",1,\n' + ' ID["EPSG",9001]]]]' + ) + crs = CRS(wkt) + assert crs.is_derived + assert crs.type_name == "Derived Projected CRS" + + def test_to_string__no_auth(): proj = CRS("+proj=latlong +ellps=GRS80 +towgs84=-199.87,74.79,246.62") assert ( diff --git a/test/crs/test_crs_maker.py b/test/crs/test_crs_maker.py index ff1a8ad35..e46ba9838 100644 --- a/test/crs/test_crs_maker.py +++ b/test/crs/test_crs_maker.py @@ -40,7 +40,7 @@ def test_make_projected_crs(tmp_path): aeaop = AlbersEqualAreaConversion(0, 0) pc = ProjectedCRS(conversion=aeaop, name="Albers") assert pc.name == "Albers" - assert pc.type_name == "Derived Projected CRS" + assert pc.type_name == "Projected CRS" assert pc.coordinate_operation == aeaop assert_can_pickle(pc, tmp_path) @@ -243,7 +243,7 @@ def test_compund_crs(tmp_path): ) assert compcrs.name == "NAD83 / Pennsylvania South + NAVD88 height" assert compcrs.type_name == "Compound CRS" - assert compcrs.sub_crs_list[0].type_name == "Derived Projected CRS" + assert compcrs.sub_crs_list[0].type_name == "Projected CRS" assert compcrs.sub_crs_list[1].type_name == "Vertical CRS" assert_can_pickle(compcrs, tmp_path) diff --git a/test/test_database.py b/test/test_database.py index 19745014d..1eb05ee0e 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -11,6 +11,7 @@ query_utm_crs_info, ) from pyproj.enums import PJType +from test.conftest import PROJ_GTE_92 def test_backwards_compatible_import_paths(): @@ -109,6 +110,16 @@ def test_get_codes__empty(auth, pj_type): assert not get_codes(auth, pj_type) +def test_get_codes__derived_projected_crs(): + if PROJ_GTE_92: + assert not get_codes("EPSG", PJType.DERIVED_PROJECTED_CRS) + else: + with pytest.raises( + NotImplementedError, match="DERIVED_PROJECTED_CRS requires PROJ 9.2+" + ): + get_codes("EPSG", PJType.DERIVED_PROJECTED_CRS) + + def test_get_codes__invalid_auth(): with pytest.raises(TypeError): get_codes(123, PJType.BOUND_CRS) @@ -140,6 +151,16 @@ def test_query_crs_info(auth, pj_type, deprecated): assert not any_deprecated +def test_query_crs_info__derived_projected_crs(): + if PROJ_GTE_92: + assert not query_crs_info(pj_types=PJType.DERIVED_PROJECTED_CRS) + else: + with pytest.raises( + NotImplementedError, match="DERIVED_PROJECTED_CRS requires PROJ 9.2+" + ): + query_crs_info(pj_types=PJType.DERIVED_PROJECTED_CRS) + + @pytest.mark.parametrize( "auth, pj_type", [