From 23c9d9df7e06521e822e8e675432bf59a23691b2 Mon Sep 17 00:00:00 2001 From: snowman2 Date: Sat, 10 Oct 2020 14:56:10 -0500 Subject: [PATCH] ENH: Add support for datum ensemble --- pyproj/_crs.pyx | 27 ++++++++++++++-- pyproj/proj.pxi | 31 ++++++++++++------ setup.py | 35 ++++++++++++-------- test/conftest.py | 8 +++++ test/crs/test_crs.py | 72 ++++++++++++++++++++++++++++++----------- test/crs/test_crs_cf.py | 3 +- 6 files changed, 131 insertions(+), 45 deletions(-) diff --git a/pyproj/_crs.pyx b/pyproj/_crs.pyx index 7a13c5b5b..cefaee2e6 100644 --- a/pyproj/_crs.pyx +++ b/pyproj/_crs.pyx @@ -1288,13 +1288,13 @@ _DATUM_TYPE_MAP = { } _PJ_DATUM_TYPE_MAP = { + DatumType.DATUM_ENSEMBLE: PJ_TYPE_DATUM_ENSEMBLE, DatumType.GEODETIC_REFERENCE_FRAME: PJ_TYPE_GEODETIC_REFERENCE_FRAME, DatumType.DYNAMIC_GEODETIC_REFERENCE_FRAME: PJ_TYPE_DYNAMIC_GEODETIC_REFERENCE_FRAME, DatumType.VERTICAL_REFERENCE_FRAME: PJ_TYPE_VERTICAL_REFERENCE_FRAME, DatumType.DYNAMIC_VERTICAL_REFERENCE_FRAME: PJ_TYPE_DYNAMIC_VERTICAL_REFERENCE_FRAME, - DatumType.DATUM_ENSEMBLE: PJ_TYPE_DATUM_ENSEMBLE, } @@ -1329,7 +1329,7 @@ cdef class Datum(_CRSParts): return datum @staticmethod - def from_authority(auth_name, code): + def _from_authority(auth_name, code, PJ_CATEGORY category): """ Create a Datum from an authority code. @@ -1350,7 +1350,7 @@ cdef class Datum(_CRSParts): context, cstrencode(str(auth_name)), cstrencode(str(code)), - PJ_CATEGORY_DATUM, + category, False, NULL, ) @@ -1361,6 +1361,27 @@ cdef class Datum(_CRSParts): CRSError.clear() return Datum.create(context, datum_pj) + @staticmethod + def from_authority(auth_name, code): + """ + Create a Datum from an authority code. + + Parameters + ---------- + auth_name: str + Name ot the authority. + code: str or int + The code used by the authority. + + Returns + ------- + Datum + """ + try: + return Datum._from_authority(auth_name, code, PJ_CATEGORY_DATUM_ENSEMBLE) + except CRSError: + return Datum._from_authority(auth_name, code, PJ_CATEGORY_DATUM) + @staticmethod def from_epsg(code): """ diff --git a/pyproj/proj.pxi b/pyproj/proj.pxi index 848f5fb20..d1e3855cd 100644 --- a/pyproj/proj.pxi +++ b/pyproj/proj.pxi @@ -1,4 +1,25 @@ -# PROJ.4 API Defnition +# PROJ API Definition + +IF CTE_PROJ_VERSION_MAJOR >= 8: + cdef extern from "proj.h": + ctypedef enum PJ_CATEGORY: + PJ_CATEGORY_ELLIPSOID + PJ_CATEGORY_PRIME_MERIDIAN + PJ_CATEGORY_DATUM + PJ_CATEGORY_CRS + PJ_CATEGORY_COORDINATE_OPERATION + PJ_CATEGORY_DATUM_ENSEMBLE +ELSE: + cdef extern from "proj.h": + ctypedef enum PJ_CATEGORY: + PJ_CATEGORY_ELLIPSOID + PJ_CATEGORY_PRIME_MERIDIAN + PJ_CATEGORY_DATUM + PJ_CATEGORY_CRS + PJ_CATEGORY_COORDINATE_OPERATION + cdef int PJ_CATEGORY_DATUM_ENSEMBLE = PJ_CATEGORY_DATUM + + cdef extern from "proj.h": cdef int PROJ_VERSION_MAJOR cdef int PROJ_VERSION_MINOR @@ -397,20 +418,12 @@ cdef extern from "proj.h": PJ *proj_concatoperation_get_step(PJ_CONTEXT *ctx, const PJ *concatoperation, int i_step) - - ctypedef enum PJ_CATEGORY: - PJ_CATEGORY_ELLIPSOID - PJ_CATEGORY_PRIME_MERIDIAN - PJ_CATEGORY_DATUM - PJ_CATEGORY_CRS - PJ_CATEGORY_COORDINATE_OPERATION PJ *proj_create_from_database(PJ_CONTEXT *ctx, const char *auth_name, const char *code, PJ_CATEGORY category, int usePROJAlternativeGridNames, const char* const *options) - PJ_OBJ_LIST *proj_create_from_name(PJ_CONTEXT *ctx, const char *auth_name, const char *searchedName, diff --git a/setup.py b/setup.py index 3b48cd35f..89b104275 100644 --- a/setup.py +++ b/setup.py @@ -14,23 +14,26 @@ INTERNAL_PROJ_DIR = CURRENT_FILE_PATH / "pyproj" / BASE_INTERNAL_PROJ_DIR -def check_proj_version(proj_dir: Path): - """checks that the PROJ library meets the minimum version""" +def get_proj_version(proj_dir: Path) -> str: + proj_version = os.environ.get("PROJ_VERSION") + if proj_version: + return proj_version proj = proj_dir / "bin" / "proj" - proj_ver_bytes = subprocess.check_output( - str(proj), stderr=subprocess.STDOUT - ).decode("ascii") - proj_ver_bytes = (proj_ver_bytes.split()[1]).strip(",") - proj_version = parse_version(proj_ver_bytes) - if proj_version < PROJ_MIN_VERSION: + proj_ver = subprocess.check_output(str(proj), stderr=subprocess.STDOUT).decode( + "ascii" + ) + return (proj_ver.split()[1]).strip(",") + + +def check_proj_version(proj_version: str) -> None: + """checks that the PROJ library meets the minimum version""" + if parse_version(proj_version) < PROJ_MIN_VERSION: raise SystemExit( f"ERROR: Minimum supported proj version is {PROJ_MIN_VERSION}, installed " f"version is {proj_version}. For more information see: " "https://pyproj4.github.io/pyproj/stable/installation.html" ) - return proj_version - def get_proj_dir() -> Path: """ @@ -58,9 +61,6 @@ def get_proj_dir() -> Path: print("PROJ_DIR is set, using existing proj4 installation..\n") else: raise SystemExit(f"ERROR: Invalid path for PROJ_DIR {proj_dir}") - - # check_proj_version - check_proj_version(proj_dir) return proj_dir @@ -152,6 +152,10 @@ def get_extension_modules(): library_dirs = get_proj_libdirs(proj_dir) include_dirs = get_proj_incdirs(proj_dir) + proj_version = get_proj_version(proj_dir) + check_proj_version(proj_version) + proj_version_major, proj_version_minor, proj_version_patch = proj_version.split(".") + # setup extension options ext_options = { "include_dirs": include_dirs, @@ -175,6 +179,11 @@ def get_extension_modules(): Extension("pyproj._sync", ["pyproj/_sync.pyx"], **ext_options), ], quiet=True, + compile_time_env={ + "CTE_PROJ_VERSION_MAJOR": int(proj_version_major), + "CTE_PROJ_VERSION_MINOR": int(proj_version_minor), + "CTE_PROJ_VERSION_PATCH": int(proj_version_patch), + }, **get_cythonize_options(), ) diff --git a/test/conftest.py b/test/conftest.py index 69106a666..140a22482 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,11 +1,13 @@ import os from contextlib import contextmanager +from distutils.version import LooseVersion from pathlib import Path import pyproj from pyproj.datadir import get_data_dir, get_user_data_dir, set_data_dir _NETWORK_ENABLED = pyproj.network.is_network_enabled() +PROJ_GTE_8 = LooseVersion(pyproj.__proj_version__) >= LooseVersion("8.0") def unset_data_dir(): @@ -70,3 +72,9 @@ def grids_available(*grid_names, check_network=True, check_all=False): if check_all: return all(available) return any(available) + + +def get_wgs84_datum_name(): + if PROJ_GTE_8: + return "World Geodetic System 1984 ensemble" + return "World Geodetic System 1984" diff --git a/test/crs/test_crs.py b/test/crs/test_crs.py index 9d2bbf0fa..767da03a4 100644 --- a/test/crs/test_crs.py +++ b/test/crs/test_crs.py @@ -15,7 +15,7 @@ from pyproj.enums import ProjVersion, WktVersion from pyproj.exceptions import CRSError from pyproj.transformer import TransformerGroup -from test.conftest import grids_available +from test.conftest import PROJ_GTE_8, get_wgs84_datum_name, grids_available class CustomCRS(object): @@ -201,7 +201,7 @@ def test_repr(): "Area of Use:\n" "- name: World.\n" "- bounds: (-180.0, -90.0, 180.0, 90.0)\n" - "Datum: World Geodetic System 1984\n" + f"Datum: {get_wgs84_datum_name()}\n" "- Ellipsoid: WGS 84\n" "- Prime Meridian: Greenwich\n" ) @@ -209,9 +209,12 @@ def test_repr(): def test_repr__long(): with pytest.warns(FutureWarning): + if PROJ_GTE_8: + wkt_str = 'GEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1' + else: + wkt_str = 'GEOGCRS["WGS 84",DATUM["World Geodetic System 1984' assert repr(CRS(CRS({"init": "EPSG:4326"}).to_wkt())) == ( - '\n' + f"\n" "Name: WGS 84\n" "Axis Info [ellipsoidal]:\n" "- lon[east]: Longitude (degree)\n" @@ -219,7 +222,7 @@ def test_repr__long(): "Area of Use:\n" "- name: World.\n" "- bounds: (-180.0, -90.0, 180.0, 90.0)\n" - "Datum: World Geodetic System 1984\n" + f"Datum: {get_wgs84_datum_name()}\n" "- Ellipsoid: WGS 84\n" "- Prime Meridian: Greenwich\n" ) @@ -235,7 +238,7 @@ def test_repr_epsg(): "Area of Use:\n" "- name: World.\n" "- bounds: (-180.0, -90.0, 180.0, 90.0)\n" - "Datum: World Geodetic System 1984\n" + f"Datum: {get_wgs84_datum_name()}\n" "- Ellipsoid: WGS 84\n" "- Prime Meridian: Greenwich\n" ) @@ -309,9 +312,13 @@ def test_epsg(): def test_datum(): datum = CRS.from_epsg(4326).datum - assert repr(datum).startswith('DATUM["World Geodetic System 1984"') assert "\n" in repr(datum) - assert datum.to_wkt().startswith('DATUM["World Geodetic System 1984"') + if PROJ_GTE_8: + datum_wkt = 'ENSEMBLE["World Geodetic System 1984 ensemble"' + else: + datum_wkt = 'DATUM["World Geodetic System 1984"' + assert repr(datum).startswith(datum_wkt) + assert datum.to_wkt().startswith(datum_wkt) assert datum == datum assert datum.is_exact_same(datum) @@ -621,16 +628,31 @@ def test_coordinate_operation__from_authority__empty(): def test_datum__from_epsg(): - assert Datum.from_epsg("6326").to_wkt() == ( - 'DATUM["World Geodetic System 1984",' - 'ELLIPSOID["WGS 84",6378137,298.257223563,' - 'LENGTHUNIT["metre",1]],ID["EPSG",6326]]' - ) + if PROJ_GTE_8: + datum_wkt = ( + 'ENSEMBLE["World Geodetic System 1984 ensemble",' + 'MEMBER["World Geodetic System 1984 (Transit)",' + 'ID["EPSG",1166]],MEMBER["World Geodetic System 1984 (G730)",' + 'ID["EPSG",1152]],MEMBER["World Geodetic System 1984 (G873)",' + 'ID["EPSG",1153]],MEMBER["World Geodetic System 1984 (G1150)",' + 'ID["EPSG",1154]],MEMBER["World Geodetic System 1984 (G1674)",' + 'ID["EPSG",1155]],MEMBER["World Geodetic System 1984 (G1762)",' + 'ID["EPSG",1156]],ELLIPSOID["WGS 84",6378137,298.257223563,' + 'LENGTHUNIT["metre",1],ID["EPSG",7030]],' + 'ENSEMBLEACCURACY[2.0],ID["EPSG",6326]]' + ) + else: + datum_wkt = ( + 'DATUM["World Geodetic System 1984",' + 'ELLIPSOID["WGS 84",6378137,298.257223563,' + 'LENGTHUNIT["metre",1]],ID["EPSG",6326]]' + ) + assert Datum.from_epsg("6326").to_wkt() == datum_wkt def test_datum__from_authority(): dt = Datum.from_authority("EPSG", 6326) - assert dt.name == "World Geodetic System 1984" + assert dt.name == get_wgs84_datum_name() def test_datum__from_epsg__invalid(): @@ -648,7 +670,9 @@ def test_datum__from_authority__invalid(): [ 6326, ("EPSG", 6326), - "urn:ogc:def:datum:EPSG::6326", + "urn:ogc:def:ensemble:EPSG::6326" + if PROJ_GTE_8 + else "urn:ogc:def:datum:EPSG::6326", Datum.from_epsg(6326), Datum.from_epsg(6326).to_json_dict(), "World Geodetic System 1984", @@ -867,12 +891,22 @@ def test_datum_equals(): @pytest.mark.parametrize( - "input_str", ["urn:ogc:def:datum:EPSG::6326", "World Geodetic System 1984"] + "input_str", + [ + "urn:ogc:def:ensemble:EPSG::6326" + if PROJ_GTE_8 + else "urn:ogc:def:datum:EPSG::6326", + "World Geodetic System 1984", + ], ) def test_datum__from_string(input_str): dd = Datum.from_string(input_str) - assert dd.name == "World Geodetic System 1984" - assert dd.type_name == "Geodetic Reference Frame" + if PROJ_GTE_8: + assert dd.name == "World Geodetic System 1984 ensemble" + assert dd.type_name == "Datum Ensemble" + else: + assert dd.name == "World Geodetic System 1984" + assert dd.type_name == "Geodetic Reference Frame" @pytest.mark.parametrize( @@ -897,7 +931,7 @@ def test_datum__from_string__type_name(input_str, type_name): ) def test_datum__from_name(input_name): dd = Datum.from_name(input_name) - assert dd.name == "World Geodetic System 1984" + assert dd.name == get_wgs84_datum_name() @pytest.mark.parametrize("auth_name", [None, "ESRI"]) diff --git a/test/crs/test_crs_cf.py b/test/crs/test_crs_cf.py index 2bc98eaf6..40658863e 100644 --- a/test/crs/test_crs_cf.py +++ b/test/crs/test_crs_cf.py @@ -16,6 +16,7 @@ VerticalPerspectiveConversion, ) from pyproj.exceptions import CRSError +from test.conftest import get_wgs84_datum_name def _to_dict(operation): @@ -240,7 +241,7 @@ def test_cf_from_utm(): "longitude_of_prime_meridian": 0.0, "prime_meridian_name": "Greenwich", "geographic_crs_name": "WGS 84", - "horizontal_datum_name": "World Geodetic System 1984", + "horizontal_datum_name": get_wgs84_datum_name(), "projected_crs_name": "WGS 84 / UTM zone 15N", "grid_mapping_name": "transverse_mercator", "latitude_of_projection_origin": 0.0,