From 4c27643e0fb919abeb80d0f71c6a88fe4d6d19ac Mon Sep 17 00:00:00 2001 From: snowman2 Date: Sat, 14 Dec 2019 21:30:48 -0600 Subject: [PATCH] ENH: Added support for custom CRS --- .gitignore | 2 + .travis.yml | 10 +- Makefile | 2 + docs/api/crs.rst | 76 -- docs/api/crs/coordinate_operation.rst | 131 ++ docs/api/crs/coordinate_system.rst | 38 + docs/api/crs/crs.rst | 77 ++ docs/api/crs/datum.rst | 37 + docs/api/crs/ellipsoid.rst | 15 + docs/api/crs/enums.rst | 25 + docs/api/crs/index.rst | 13 + docs/api/enums.rst | 2 +- docs/api/index.rst | 2 +- docs/build_crs.rst | 203 +++ docs/history.rst | 1 + docs/index.rst | 1 + pyproj/_crs.pxd | 18 +- pyproj/_crs.pyx | 84 +- pyproj/crs/__init__.py | 27 + pyproj/{cf1x8.py => crs/_cf1x8.py} | 0 pyproj/crs/coordinate_operation.py | 1413 +++++++++++++++++++++ pyproj/crs/coordinate_system.py | 357 ++++++ pyproj/{ => crs}/crs.py | 329 ++++- pyproj/crs/datum.py | 29 + pyproj/crs/ellipsoid.py | 33 + pyproj/crs/enums.py | 143 +++ pyproj/enums.py | 43 - pyproj/exceptions.py | 2 +- pyproj/geod.py | 8 +- pyproj/proj.py | 2 +- pyproj/transformer.py | 2 +- test/{ => crs}/test_crs.py | 3 +- test/{ => crs}/test_crs_cf.py | 25 - test/crs/test_crs_coordinate_operation.py | 608 +++++++++ test/crs/test_crs_coordinate_system.py | 165 +++ test/crs/test_crs_datum.py | 16 + test/crs/test_crs_ellipsoid.py | 22 + test/{ => crs}/test_crs_json.py | 0 test/crs/test_crs_maker.py | 159 +++ 39 files changed, 3838 insertions(+), 285 deletions(-) delete mode 100644 docs/api/crs.rst create mode 100644 docs/api/crs/coordinate_operation.rst create mode 100644 docs/api/crs/coordinate_system.rst create mode 100644 docs/api/crs/crs.rst create mode 100644 docs/api/crs/datum.rst create mode 100644 docs/api/crs/ellipsoid.rst create mode 100644 docs/api/crs/enums.rst create mode 100644 docs/api/crs/index.rst create mode 100644 docs/build_crs.rst create mode 100644 pyproj/crs/__init__.py rename pyproj/{cf1x8.py => crs/_cf1x8.py} (100%) create mode 100644 pyproj/crs/coordinate_operation.py create mode 100644 pyproj/crs/coordinate_system.py rename pyproj/{ => crs}/crs.py (75%) create mode 100644 pyproj/crs/datum.py create mode 100644 pyproj/crs/ellipsoid.py create mode 100644 pyproj/crs/enums.py rename test/{ => crs}/test_crs.py (99%) rename test/{ => crs}/test_crs_cf.py (94%) create mode 100644 test/crs/test_crs_coordinate_operation.py create mode 100644 test/crs/test_crs_coordinate_system.py create mode 100644 test/crs/test_crs_datum.py create mode 100644 test/crs/test_crs_ellipsoid.py rename test/{ => crs}/test_crs_json.py (100%) create mode 100644 test/crs/test_crs_maker.py diff --git a/.gitignore b/.gitignore index effb20ab2..cb73ebd68 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ pyproj/proj_dir/ pyproj/*.c +pyproj/*/*.c +pyproj/*/*.html pyproj/*.html proj-*/ diff --git a/.travis.yml b/.travis.yml index 1c8fd8ab1..07670ea84 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,12 +38,18 @@ matrix: env: - DOC=true - python: 3.8 - - python: "nightly" + - python: 3.8 env: - PROJSOURCE=git + # - python: "nightly" + # env: + # - PROJSOURCE=git allow_failures: - - python: "nightly" + # - python: "nightly" + # env: + # - PROJSOURCE=git + - python: 3.8 env: - PROJSOURCE=git diff --git a/Makefile b/Makefile index 077ab2dfd..bd6ea012f 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,8 @@ clean-setup: ## run python setup.py clean clean-cython: ## clean the cython files rm -f pyproj/*.so + rm -f pyproj/*/*.so + rm -f pyproj/*/*.c rm -f pyproj/*.c lint: ## check style with flake8 diff --git a/docs/api/crs.rst b/docs/api/crs.rst deleted file mode 100644 index 753faaf8d..000000000 --- a/docs/api/crs.rst +++ /dev/null @@ -1,76 +0,0 @@ -CRS -=== - -pyproj.CRS ----------- - -.. autoclass:: pyproj.crs.CRS - :members: - :inherited-members: - :special-members: __init__ - - -pyproj.crs.is_wkt ------------------ - -.. autofunction:: pyproj.crs.is_wkt - - -pyproj.crs.is_proj ------------------- - -.. autofunction:: pyproj.crs.is_proj - - -Area Of Use ------------ - -.. autoclass:: pyproj._crs.AreaOfUse - :members: - :inherited-members: - -Coordinate System ------------------ - -.. autoclass:: pyproj.crs.CoordinateSystem - :members: - :inherited-members: - -.. autoclass:: pyproj._crs.Axis - :members: - :inherited-members: - -Coordinate Operation --------------------- - -.. autoclass:: pyproj.crs.CoordinateOperation - :members: - :inherited-members: - -.. autoclass:: pyproj._crs.Param - :members: - -.. autoclass:: pyproj._crs.Grid - :members: - -Datum ------ - -.. autoclass:: pyproj.crs.Datum - :members: - :inherited-members: - -Ellipsoid ---------- - -.. autoclass:: pyproj.crs.Ellipsoid - :members: - :inherited-members: - - -Prime Meridian --------------- - -.. autoclass:: pyproj.crs.PrimeMeridian - :members: - :inherited-members: diff --git a/docs/api/crs/coordinate_operation.rst b/docs/api/crs/coordinate_operation.rst new file mode 100644 index 000000000..202b5e614 --- /dev/null +++ b/docs/api/crs/coordinate_operation.rst @@ -0,0 +1,131 @@ +.. _coordinate_operation: + +Coordinate Operations +===================== + + +.. autoclass:: pyproj.crs.CoordinateOperation + :members: + :inherited-members: + + +.. autoclass:: pyproj._crs.Param + :members: + + +.. autoclass:: pyproj._crs.Grid + :members: + + +.. autoclass:: pyproj.crs.coordinate_operation.AlbersEqualAreaOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.AzumuthalEquidistantOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.GeostationarySatelliteOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.LambertAzumuthalEqualAreaOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.LambertConformalConic1SPOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.LambertConformalConic2SPOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.LambertCylindricalEqualAreaOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.MercatorAOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.MercatorBOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.HotineObliqueMercatorBOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.OrthographicOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.PolarStereographicAOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.PolarStereographicBOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.SinusoidalOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.UTMOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.TransverseMercatorOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.VerticalPerspectiveOperation + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.RotatedLatitudeLongitude + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_operation.ToWGS84Transformation + :members: + :show-inheritance: + :special-members: __new__ diff --git a/docs/api/crs/coordinate_system.rst b/docs/api/crs/coordinate_system.rst new file mode 100644 index 000000000..6f8f33c3c --- /dev/null +++ b/docs/api/crs/coordinate_system.rst @@ -0,0 +1,38 @@ +.. _coordinate_system: + +Coordinate Systems +================== + + +.. autoclass:: pyproj.crs.CoordinateSystem + :members: + :inherited-members: + + +.. autoclass:: pyproj._crs.Axis + :members: + :inherited-members: + + +.. autoclass:: pyproj.crs.coordinate_system.Ellipsoidal2DCS + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_system.Ellipsoidal3DCS + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_system.Cartesian2DCS + :members: + :show-inheritance: + :special-members: __new__ + + +.. autoclass:: pyproj.crs.coordinate_system.VerticalCS + :members: + :show-inheritance: + :special-members: __new__ diff --git a/docs/api/crs/crs.rst b/docs/api/crs/crs.rst new file mode 100644 index 000000000..25b8a75af --- /dev/null +++ b/docs/api/crs/crs.rst @@ -0,0 +1,77 @@ +.. _crs: + +CRS +=== + +pyproj.CRS +---------- + +.. autoclass:: pyproj.crs.CRS + :members: + :inherited-members: + :special-members: __init__ + + +pyproj.crs.GeographicCRS +------------------------ + +.. autoclass:: pyproj.crs.GeographicCRS + :members: + :show-inheritance: + :special-members: __init__ + + +pyproj.crs.ProjectedCRS +----------------------- + +.. autoclass:: pyproj.crs.ProjectedCRS + :members: + :show-inheritance: + :special-members: __init__ + + +pyproj.crs.VerticalCRS +----------------------- + +.. autoclass:: pyproj.crs.VerticalCRS + :members: + :show-inheritance: + :special-members: __init__ + + +pyproj.crs.BoundCRS +----------------------- + +.. autoclass:: pyproj.crs.BoundCRS + :members: + :show-inheritance: + :special-members: __init__ + + +pyproj.crs.CompoundCRS +----------------------- + +.. autoclass:: pyproj.crs.CompoundCRS + :members: + :show-inheritance: + :special-members: __init__ + + +pyproj.crs.is_wkt +----------------- + +.. autofunction:: pyproj.crs.is_wkt + + +pyproj.crs.is_proj +------------------ + +.. autofunction:: pyproj.crs.is_proj + + +Area Of Use +----------- + +.. autoclass:: pyproj._crs.AreaOfUse + :members: + :inherited-members: diff --git a/docs/api/crs/datum.rst b/docs/api/crs/datum.rst new file mode 100644 index 000000000..97cceeeba --- /dev/null +++ b/docs/api/crs/datum.rst @@ -0,0 +1,37 @@ +.. _datum: + +Datum +===== + + +.. note:: PROJ >= 7.0.0 will have better support for aliases for datum names. + Until then, you will need to use the full name of the datum. There is support + currently for the old PROJ names for datums such as WGS84 and NAD83. + + + +.. autoclass:: pyproj.crs.Datum + :members: + :inherited-members: + + +.. autoclass:: pyproj.crs.datum.CustomDatum + :members: + :show-inheritance: + :special-members: __new__ + + +Ellipsoid +--------- + +- :class:`pyproj.crs.Ellipsoid` +- :class:`pyproj.crs.ellipsoid.CustomEllipsoid` + + +Prime Meridian +-------------- + +.. autoclass:: pyproj.crs.PrimeMeridian + :members: + :inherited-members: + diff --git a/docs/api/crs/ellipsoid.rst b/docs/api/crs/ellipsoid.rst new file mode 100644 index 000000000..ad6c9706c --- /dev/null +++ b/docs/api/crs/ellipsoid.rst @@ -0,0 +1,15 @@ +.. _ellipsoid: + +Ellipsoid +========= + + +.. autoclass:: pyproj.crs.Ellipsoid + :members: + :inherited-members: + + +.. autoclass:: pyproj.crs.ellipsoid.CustomEllipsoid + :members: + :show-inheritance: + :special-members: __new__ diff --git a/docs/api/crs/enums.rst b/docs/api/crs/enums.rst new file mode 100644 index 000000000..822b75bbe --- /dev/null +++ b/docs/api/crs/enums.rst @@ -0,0 +1,25 @@ +Enumerations +============ + +.. autoclass:: pyproj.crs.enums.DatumType + :members: + + +.. autoclass:: pyproj.crs.enums.CoordinateOperationType + :members: + + +.. autoclass:: pyproj.crs.enums.Cartesian2DCSAxis + :members: + + +.. autoclass:: pyproj.crs.enums.Ellipsoidal2DCSAxis + :members: + + +.. autoclass:: pyproj.crs.enums.Ellipsoidal3DCSAxis + :members: + + +.. autoclass:: pyproj.crs.enums.VerticalCSAxis + :members: diff --git a/docs/api/crs/index.rst b/docs/api/crs/index.rst new file mode 100644 index 000000000..17223d04d --- /dev/null +++ b/docs/api/crs/index.rst @@ -0,0 +1,13 @@ +CRS module API Documentation +============================ + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + crs + coordinate_system + coordinate_operation + datum + ellipsoid + enums \ No newline at end of file diff --git a/docs/api/enums.rst b/docs/api/enums.rst index 268bb8f44..ef80bc94b 100644 --- a/docs/api/enums.rst +++ b/docs/api/enums.rst @@ -14,4 +14,4 @@ Enumerations .. autoclass:: pyproj.enums.PJType - :members: \ No newline at end of file + :members: diff --git a/docs/api/index.rst b/docs/api/index.rst index 1e9a959ed..81b23d892 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -5,7 +5,7 @@ API Documentation :maxdepth: 2 :caption: Contents: - crs + crs/index transformer geod proj diff --git a/docs/build_crs.rst b/docs/build_crs.rst new file mode 100644 index 000000000..eaa60cf40 --- /dev/null +++ b/docs/build_crs.rst @@ -0,0 +1,203 @@ +Building a Coordinate Reference System +====================================== + +.. versionadded:: 2.5.0 + +PROJ strings have the potential to lose much of the information +about a coordinate reference system (CRS). + +More information: https://proj.org/faq.html#what-is-the-best-format-for-describing-coordinate-reference-systems + +However, PROJ strings make it really simple to construct a CRS. +This addition is meant to simplify the process of transitioning +from the PROJ form of the string to the WKT form WKT. The CRS +classes can be used in the :meth:`pyproj.transformer.Transformer.from_crs` +method just like the :class:`pyproj.crs.CRS` class. + +The current set of classes does not cover every possible use case, +but hopefully it is enough to get you started. +If you notice something is missing that you need, feel free to open an issue on GitHub. + + +.. note:: PROJ >= 7.0.0 will have better support for aliases for datum names. + If you have a previous version of PROJ, you will need to use the + full name of the datum. There is support currently for the old PROJ + names for datums such as WGS84 and NAD83. + + +Here are links to the API docs for the pieces you need to get started: + +- :ref:`crs` +- :ref:`coordinate_operation` +- :ref:`datum` +- :ref:`ellipsoid` +- :ref:`coordinate_operation` + + +Geographic CRS +-------------- + +This is a simple example of creating a lonlat projection. + +PROJ string:: + + +proj=longlat +datum=WGS84 +no_defs + +.. code-block:: python + + from pyproj.crs import GeographicCRS + + geog_crs = GeographicCRS() + geog_wkt = geog_crs.to_wkt() + + +This example is meant to show off different intialization methods. +It can be simplified to not use the Ellipsoid or PrimeMeridian objects. + +PROJ string:: + + +proj=longlat +ellps=airy +pm=lisbon +no_defs + +.. code-block:: python + + from pyproj.crs import Ellipsoid, GeographicCRS, PrimeMeridian + from pyproj.crs.datum import CustomDatum + + cd = CustomDatum( + ellipsoid=Ellipsoid.from_epsg(7001), + prime_meridian=PrimeMeridian.from_name("Lisbon"), + ) + geog_crs = GeographicCRS(datum=cd) + geog_wkt = geog_crs.to_wkt() + + +Projected CRS +------------- + +Simple example using defaults. + +PROJ string:: + + +proj=aea +lat_0=0 +lon_0=0 +lat_1=0 +lat_2=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs + +.. code-block:: python + + from pyproj.crs import ProjectedCRS + from pyproj.crs.coordinate_operation import AlbersEqualAreaOperation + + aeaop = AlbersEqualAreaOperation(0, 0) + proj_crs = ProjectedCRS(coordinate_operation=aeaop) + crs_wkt = proj_crs.to_wkt() + + +More complex example with custom parameters. + +PROJ string:: + + +proj=utm +zone=14 +a=6378137 +b=6356752 +pm=lisbon +units=m +no_defs + +.. code-block:: python + + from pyproj.crs import GeographicCRS, ProjectedCRS + from pyproj.crs.coordinate_operation import UTMOperation + from pyproj.crs.datum import CustomDatum + from pyproj.crs.ellipsoid import CustomEllipsoid + + ell = CustomEllipsoid( + semi_major_axis=6378137, + semi_minor_axis=6356752, + ) + cd = CustomDatum( + ellipsoid=ell, + prime_meridian="Lisbon", + ) + proj_crs = ProjectedCRS( + coordinate_operation=UTMOperation(14), + geodetic_crs=GeographicCRS(datum=cd), + ) + crs_wkt = proj_crs.to_wkt() + + +Bound CRS +--------- + +This is an example building a CRS with `towgs84`. + +PROJ string:: + + +proj=tmerc +lat_0=0 +lon_0=15 +k=0.9996 +x_0=2520000 +y_0=0 +ellps=intl +towgs84=-122.74,-34.27,-22.83,-1.884,-3.4,-3.03,-15.62 +units=m +no_defs + +.. code-block:: python + + from pyproj.crs import BoundCRS, Ellipsoid, GeographicCRS, ProjectedCRS + from pyproj.crs.coordinate_operation import TransverseMercatorOperation, ToWGS84Transformation + from pyproj.crs.datum import CustomDatum + + proj_crs = ProjectedCRS( + coordinate_operation=TransverseMercatorOperation( + latitude_natural_origin=0, + longitude_natural_origin=15, + false_easting=2520000, + false_northing=0, + scale_factor_natural_origin=0.9996, + ), + geodetic_crs=GeographicCRS( + datum=CustomDatum( + ellipsoid={ + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Ellipsoid", + "name": "International 1909 (Hayford)", + "semi_major_axis": 6378388, + "inverse_flattening": 297, + } + ) + ), + ) + bound_crs = BoundCRS( + source_crs=proj_crs, + target_crs="WGS 84", + transformation=ToWGS84Transformation( + proj_crs.geodetic_crs, -122.74, -34.27, -22.83, -1.884, -3.4, -3.03, -15.62 + ), + ) + crs_wkt = bound_crs.to_wkt() + + +Compound CRS +------------- + +The PROJ string is quite lossy in this example, so it is not provided. + +.. warning:: geoid_model support only exists in PROJ >= 6.3.0 + +.. code-block:: python + + from pyproj.crs import CompoundCRS, GeographicCRS, ProjectedCRS, VerticalCRS + from pyproj.crs.coordinate_system import Cartesian2DCS, VerticalCS + from pyproj.crs.coordinate_operation import LambertConformalConic2SPOperation + + + vertcrs = VerticalCRS( + name="NAVD88 height", + datum="North American Vertical Datum 1988", + vertical_cs=VerticalCS(), + geoid_model="GEOID12B", + ) + projcrs = ProjectedCRS( + name="NAD83 / Pennsylvania South", + coordinate_operation=LambertConformalConic2SPOperation( + latitude_false_origin=39.3333333333333, + longitude_false_origin=-77.75, + latitude_first_parallel=40.9666666666667, + latitude_second_parallel=39.9333333333333, + easting_false_origin=600000, + northing_false_origin=0, + ), + geodetic_crs=GeographicCRS(datum="North American Datum 1983"), + cartesian_cs=Cartesian2DCS(), + ) + compcrs = CompoundCRS( + name="NAD83 / Pennsylvania South + NAVD88 height", + components=[projcrs, vertcrs], + ) + crs_wkt = compcrs.to_wkt() diff --git a/docs/history.rst b/docs/history.rst index 96daebe97..be288fad9 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -10,6 +10,7 @@ Change Log * ENH: Added :meth:`pyproj.crs.CoordinateSystem.from_user_input`, :meth:`pyproj.crs.CoordinateOperation.from_user_input`, :meth:`pyproj.crs.Datum.from_user_input`, :meth:`pyproj.crs.PrimeMeridian.from_user_input`, :meth:`pyproj.crs.Ellipsoid.from_user_input` (pull #502) * ENH: Added :meth:`pyproj.crs.CoordinateSystem.from_name`, :meth:`pyproj.crs.CoordinateOperation.from_name`, :meth:`pyproj.crs.Datum.from_name`, :meth:`pyproj.crs.PrimeMeridian.from_name`, :meth:`pyproj.crs.Ellipsoid.from_name` (pull #505) * BUG: Fix getting :attr:`pyproj.crs.Ellipsoid.semi_minor_metre` when not computed (issue #457) +* ENH: Added support for custom CRS (issue #389) 2.4.2 ~~~~~ diff --git a/docs/index.rst b/docs/index.rst index 1730be9d7..9b1b35779 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,6 +18,7 @@ GitHub Repository: https://github.com/pyproj4/pyproj examples gotchas advanced_examples + build_crs api/index optimize_transformations history diff --git a/pyproj/_crs.pxd b/pyproj/_crs.pxd index 8c80896f5..509c02a27 100644 --- a/pyproj/_crs.pxd +++ b/pyproj/_crs.pxd @@ -14,7 +14,7 @@ cdef class Axis: cdef readonly object unit_code @staticmethod - cdef create(PJ_CONTEXT* context, PJ* projobj, int index) + cdef Axis create(PJ_CONTEXT* context, PJ* projobj, int index) cdef class AreaOfUse: cdef readonly double west @@ -24,7 +24,7 @@ cdef class AreaOfUse: cdef readonly object name @staticmethod - cdef create(PJ_CONTEXT* context, PJ* projobj) + cdef AreaOfUse create(PJ_CONTEXT* context, PJ* projobj) cdef class Base: @@ -46,7 +46,7 @@ cdef class Ellipsoid(_CRSParts): cdef readonly double inverse_flattening @staticmethod - cdef create(PJ_CONTEXT* context, PJ* ellipsoid_pj) + cdef Ellipsoid create(PJ_CONTEXT* context, PJ* ellipsoid_pj) cdef class PrimeMeridian(_CRSParts): @@ -55,7 +55,7 @@ cdef class PrimeMeridian(_CRSParts): cdef readonly object unit_name @staticmethod - cdef create(PJ_CONTEXT* context, PJ* prime_meridian_pj) + cdef PrimeMeridian create(PJ_CONTEXT* context, PJ* prime_meridian_pj) cdef class Datum(_CRSParts): @@ -64,14 +64,14 @@ cdef class Datum(_CRSParts): cdef readonly object _prime_meridian @staticmethod - cdef create(PJ_CONTEXT* context, PJ* datum_pj) + cdef Datum create(PJ_CONTEXT* context, PJ* datum_pj) cdef class CoordinateSystem(_CRSParts): cdef readonly object _axis_list @staticmethod - cdef create(PJ_CONTEXT* context, PJ* coordinate_system_pj) + cdef CoordinateSystem create(PJ_CONTEXT* context, PJ* coordinate_system_pj) cdef class Param: @@ -86,7 +86,7 @@ cdef class Param: cdef readonly object unit_category @staticmethod - cdef create(PJ_CONTEXT* context, PJ* projobj, int param_idx) + cdef Param create(PJ_CONTEXT* context, PJ* projobj, int param_idx) cdef class Grid: @@ -99,7 +99,7 @@ cdef class Grid: cdef readonly object available @staticmethod - cdef create(PJ_CONTEXT* context, PJ* projobj, int grid_idx) + cdef Grid create(PJ_CONTEXT* context, PJ* projobj, int grid_idx) cdef class CoordinateOperation(_CRSParts): @@ -117,7 +117,7 @@ cdef class CoordinateOperation(_CRSParts): cdef readonly type_name @staticmethod - cdef create(PJ_CONTEXT* context, PJ* coordinate_operation_pj) + cdef CoordinateOperation create(PJ_CONTEXT* context, PJ* coordinate_operation_pj) cdef class _CRS(Base): diff --git a/pyproj/_crs.pyx b/pyproj/_crs.pyx index b536c61fc..a16a0cb13 100644 --- a/pyproj/_crs.pyx +++ b/pyproj/_crs.pyx @@ -3,14 +3,10 @@ import re import warnings from collections import OrderedDict -from pyproj.compat import cstrencode, pystrdecode from pyproj._datadir cimport pyproj_context_initialize -from pyproj.enums import ( - CoordinateOperationType, - DatumType, - ProjVersion, - WktVersion, -) +from pyproj.compat import cstrencode, pystrdecode +from pyproj.crs.enums import CoordinateOperationType, DatumType +from pyproj.enums import ProjVersion, WktVersion from pyproj.exceptions import CRSError @@ -275,7 +271,7 @@ cdef class Axis: ) @staticmethod - cdef create(PJ_CONTEXT* context, PJ* projobj, int index): + cdef Axis create(PJ_CONTEXT* context, PJ* projobj, int index): cdef Axis axis_info = Axis() cdef const char * name = NULL cdef const char * abbrev = NULL @@ -347,7 +343,7 @@ cdef class AreaOfUse: ) @staticmethod - cdef create(PJ_CONTEXT* context, PJ* projobj): + cdef AreaOfUse create(PJ_CONTEXT* context, PJ* projobj): cdef AreaOfUse area_of_use = AreaOfUse() cdef const char * area_name = NULL if not proj_get_area_of_use( @@ -596,7 +592,7 @@ cdef class CoordinateSystem(_CRSParts): raise RuntimeError("CoordinateSystem is not initializable.") @staticmethod - cdef create(PJ_CONTEXT* context, PJ* coord_system_pj): + cdef CoordinateSystem create(PJ_CONTEXT* context, PJ* coord_system_pj): cdef CoordinateSystem coord_system = CoordinateSystem.__new__(CoordinateSystem) coord_system.context = context coord_system.projobj = coord_system_pj @@ -740,7 +736,7 @@ cdef class Ellipsoid(_CRSParts): ) @staticmethod - cdef create(PJ_CONTEXT* context, PJ* ellipsoid_pj): + cdef Ellipsoid create(PJ_CONTEXT* context, PJ* ellipsoid_pj): cdef Ellipsoid ellips = Ellipsoid.__new__(Ellipsoid) ellips.context = context ellips.projobj = ellipsoid_pj @@ -981,7 +977,7 @@ cdef class PrimeMeridian(_CRSParts): ) @staticmethod - cdef create(PJ_CONTEXT* context, PJ* prime_meridian_pj): + cdef PrimeMeridian create(PJ_CONTEXT* context, PJ* prime_meridian_pj): cdef PrimeMeridian prime_meridian = PrimeMeridian.__new__(PrimeMeridian) prime_meridian.context = context prime_meridian.projobj = prime_meridian_pj @@ -1255,7 +1251,7 @@ cdef class Datum(_CRSParts): ) @staticmethod - cdef create(PJ_CONTEXT* context, PJ* datum_pj): + cdef Datum create(PJ_CONTEXT* context, PJ* datum_pj): cdef Datum datum = Datum.__new__(Datum) datum.context = context datum.projobj = datum_pj @@ -1570,7 +1566,7 @@ cdef class Param: self.unit_category = "undefined" @staticmethod - cdef create(PJ_CONTEXT* context, PJ* projobj, int param_idx): + cdef Param create(PJ_CONTEXT* context, PJ* projobj, int param_idx): cdef Param param = Param() cdef char *out_name cdef char *out_auth_name @@ -1661,7 +1657,7 @@ cdef class Grid: self.available = False @staticmethod - cdef create(PJ_CONTEXT* context, PJ* projobj, int grid_idx): + cdef Grid create(PJ_CONTEXT* context, PJ* projobj, int grid_idx): cdef Grid grid = Grid() cdef char *out_short_name cdef char *out_full_name @@ -1769,7 +1765,7 @@ cdef class CoordinateOperation(_CRSParts): ) @staticmethod - cdef create(PJ_CONTEXT* context, PJ* coord_operation_pj): + cdef CoordinateOperation create(PJ_CONTEXT* context, PJ* coord_operation_pj): cdef CoordinateOperation coord_operation = CoordinateOperation.__new__( CoordinateOperation ) @@ -2167,7 +2163,6 @@ _CRS_TYPE_MAP = { } - cdef class _CRS(Base): """ .. versionadded:: 2.0.0 @@ -2239,21 +2234,7 @@ cdef class _CRS(Base): ------- Ellipsoid: The ellipsoid object with associated attributes. """ - if self._ellipsoid is not None: - return None if self._ellipsoid is False else self._ellipsoid - cdef PJ_CONTEXT* context = proj_context_create() - pyproj_context_initialize(context, True) - cdef PJ* ellipsoid_pj = proj_get_ellipsoid( - context, - self.projobj - ) - CRSError.clear() - if ellipsoid_pj == NULL: - proj_context_destroy(context) - self._ellipsoid = False - return None - self._ellipsoid = Ellipsoid.create(context, ellipsoid_pj) - return self._ellipsoid + return self.datum.ellipsoid @property def prime_meridian(self): @@ -2262,23 +2243,9 @@ cdef class _CRS(Base): Returns ------- - PrimeMeridian: The CRS prime meridian object with associated attributes. + PrimeMeridian: The prime meridian object with associated attributes. """ - if self._prime_meridian is not None: - return None if self._prime_meridian is True else self._prime_meridian - cdef PJ_CONTEXT* context = proj_context_create() - pyproj_context_initialize(context, True) - cdef PJ* prime_meridian_pj = proj_get_prime_meridian( - context, - self.projobj, - ) - CRSError.clear() - if prime_meridian_pj == NULL: - proj_context_destroy(context) - self._prime_meridian = False - return None - self._prime_meridian = PrimeMeridian.create(context, prime_meridian_pj) - return self._prime_meridian + return self.datum.prime_meridian @property def datum(self): @@ -2376,7 +2343,7 @@ cdef class _CRS(Base): """ Returns ------- - CRS: The the base CRS of a BoundCRS or a DerivedCRS/ProjectedCRS, + _CRS: The the base CRS of a BoundCRS or a DerivedCRS/ProjectedCRS, or the source CRS of a CoordinateOperation. """ if self._source_crs is not None: @@ -2387,7 +2354,7 @@ cdef class _CRS(Base): self._source_crs = False return None try: - self._source_crs = self.__class__(_to_wkt(self.context, projobj)) + self._source_crs = _CRS(_to_wkt(self.context, projobj)) finally: proj_destroy(projobj) return self._source_crs @@ -2399,7 +2366,7 @@ cdef class _CRS(Base): Returns ------- - CRS: The hub CRS of a BoundCRS or the target CRS of a CoordinateOperation. + _CRS: The hub CRS of a BoundCRS or 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 @@ -2409,7 +2376,7 @@ cdef class _CRS(Base): self._target_crs = False return None try: - self._target_crs = self.__class__(_to_wkt(self.context, projobj)) + self._target_crs = _CRS(_to_wkt(self.context, projobj)) finally: proj_destroy(projobj) return self._target_crs @@ -2421,7 +2388,7 @@ cdef class _CRS(Base): Returns ------- - list[CRS] + list[_CRS] """ if self._sub_crs_list is not None: return self._sub_crs_list @@ -2434,7 +2401,7 @@ cdef class _CRS(Base): self._sub_crs_list = [] while projobj != NULL: try: - self._sub_crs_list.append(self.__class__(_to_wkt(self.context, projobj))) + self._sub_crs_list.append(_CRS(_to_wkt(self.context, projobj))) finally: proj_destroy(projobj) # deallocate temp proj iii += 1 @@ -2453,7 +2420,7 @@ cdef class _CRS(Base): Returns ------- - pyproj.CRS: The the geodeticCRS / geographicCRS from the CRS. + _CRS: The the geodeticCRS / geographicCRS from the CRS. """ if self._geodetic_crs is not None: return self._geodetic_crs if self. _geodetic_crs is not False else None @@ -2463,11 +2430,10 @@ cdef class _CRS(Base): self._geodetic_crs = False return None try: - self._geodetic_crs = self.__class__(_to_wkt(self.context, projobj)) - return self._geodetic_crs + self._geodetic_crs = _CRS(_to_wkt(self.context, projobj)) finally: proj_destroy(projobj) # deallocate temp proj - + return self._geodetic_crs def to_proj4(self, version=ProjVersion.PROJ_4): """ @@ -2494,7 +2460,7 @@ cdef class _CRS(Base): "coordinate-reference-systems" ) return _to_proj4(self.context, self.projobj, version) - + def to_epsg(self, min_confidence=70): """ Return the EPSG code best matching the CRS diff --git a/pyproj/crs/__init__.py b/pyproj/crs/__init__.py new file mode 100644 index 000000000..b3bc5383a --- /dev/null +++ b/pyproj/crs/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +""" +This module interfaces with PROJ to produce a pythonic interface +to the coordinate reference system (CRS) information through the CRS +class. + +Original Author: Alan D. Snow [github.com/snowman2] (2019) +""" + +from pyproj._crs import ( # noqa: F401 + CoordinateOperation, + CoordinateSystem, + Datum, + Ellipsoid, + PrimeMeridian, + is_proj, + is_wkt, +) +from pyproj.crs.crs import ( # noqa: F401 + CRS, + BoundCRS, + CompoundCRS, + GeographicCRS, + ProjectedCRS, + VerticalCRS, +) +from pyproj.exceptions import CRSError # noqa: F401 diff --git a/pyproj/cf1x8.py b/pyproj/crs/_cf1x8.py similarity index 100% rename from pyproj/cf1x8.py rename to pyproj/crs/_cf1x8.py diff --git a/pyproj/crs/coordinate_operation.py b/pyproj/crs/coordinate_operation.py new file mode 100644 index 000000000..b2b3cab13 --- /dev/null +++ b/pyproj/crs/coordinate_operation.py @@ -0,0 +1,1413 @@ +from pyproj._crs import CoordinateOperation +from pyproj.crs import CRS +from pyproj.exceptions import CRSError + + +class AlbersEqualAreaOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Albers Equal Area Operation. + + https://proj.org/operations/projections/aea.html + """ + + def __new__( + cls, + latitude_first_parallel, + latitude_second_parallel, + latitude_false_origin=0.0, + longitude_false_origin=0.0, + easting_false_origin=0.0, + northing_false_origin=0.0, + ): + """ + Parameters + ---------- + latitude_first_parallel: float + First standard parallel (lat_1). + latitude_second_parallel: float + Second standard parallel (lat_2). + latitude_false_origin: float, optional + Latitude of projection center (lat_0). Defaults to 0.0. + longitude_false_origin: float, optional + Longitude of projection center (lon_0). Defaults to 0.0. + easting_false_origin: float, optional + False easting (x_0). Defaults to 0.0. + northing_false_origin: float, optional + False northing (y_0). Defaults to 0.0. + """ + aea_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Albers Equal Area", + "id": {"authority": "EPSG", "code": 9822}, + }, + "parameters": [ + { + "name": "Latitude of false origin", + "value": latitude_false_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8821}, + }, + { + "name": "Longitude of false origin", + "value": longitude_false_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8822}, + }, + { + "name": "Latitude of 1st standard parallel", + "value": latitude_first_parallel, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8823}, + }, + { + "name": "Latitude of 2nd standard parallel", + "value": latitude_second_parallel, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8824}, + }, + { + "name": "Easting at false origin", + "value": easting_false_origin, + "unit": { + "type": "LinearUnit", + "name": "Metre", + "conversion_factor": 1, + }, + "id": {"authority": "EPSG", "code": 8826}, + }, + { + "name": "Northing at false origin", + "value": northing_false_origin, + "unit": { + "type": "LinearUnit", + "name": "Metre", + "conversion_factor": 1, + }, + "id": {"authority": "EPSG", "code": 8827}, + }, + ], + } + return cls.from_json_dict(aea_json) + + +class AzumuthalEquidistantOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Modified Azimuthal Equidistant operation. + + https://proj.org/operations/projections/aeqd.html + """ + + def __new__( + cls, + latitude_natural_origin=0.0, + longitude_natural_origin=0.0, + false_easting=0.0, + false_northing=0.0, + ): + """ + Parameters + ---------- + latitude_natural_origin: float, optional + Latitude of projection center (lat_0). Defaults to 0.0. + longitude_natural_origin: float, optional + Longitude of projection center (lon_0). Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + + """ + aeqd_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Modified Azimuthal Equidistant", + "id": {"authority": "EPSG", "code": 9832}, + }, + "parameters": [ + { + "name": "Latitude of natural origin", + "value": latitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8801}, + }, + { + "name": "Longitude of natural origin", + "value": longitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8802}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(aeqd_json) + + +class GeostationarySatelliteOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Geostationary Satellite operation. + + https://proj.org/operations/projections/geos.html + """ + + def __new__( + cls, + sweep_angle_axis, + satellite_height, + latitude_natural_origin=0.0, + longitude_natural_origin=0.0, + false_easting=0.0, + false_northing=0.0, + ): + """ + Parameters + ---------- + sweep_angle_axis: str + Sweep angle axis of the viewing instrument. Valid options are “X” and “Y”. + satellite_height: float + Satellite height. + latitude_natural_origin: float, optional + Latitude of projection center (lat_0). Defaults to 0.0. + longitude_natural_origin: float, optional + Longitude of projection center (lon_0). Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + + """ + sweep_angle_axis = sweep_angle_axis.strip().upper() + valid_sweep_axis = ("X", "Y") + if sweep_angle_axis not in valid_sweep_axis: + raise CRSError("sweep_angle_axis only supports {}".format(valid_sweep_axis)) + + geos_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Geostationary Satellite (Sweep {})".format(sweep_angle_axis) + }, + "parameters": [ + { + "name": "Satellite height", + "value": satellite_height, + "unit": "metre", + }, + { + "name": "Latitude of natural origin", + "value": latitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8801}, + }, + { + "name": "Longitude of natural origin", + "value": longitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8802}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(geos_json) + + +class LambertAzumuthalEqualAreaOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Lambert Azimuthal Equal Area operation. + + https://proj.org/operations/projections/laea.html + """ + + def __new__( + cls, + latitude_natural_origin=0.0, + longitude_natural_origin=0.0, + false_easting=0.0, + false_northing=0.0, + ): + """ + Parameters + ---------- + latitude_natural_origin: float, optional + Latitude of projection center (lat_0). Defaults to 0.0. + longitude_natural_origin: float, optional + Longitude of projection center (lon_0). Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + + """ + laea_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Lambert Azimuthal Equal Area", + "id": {"authority": "EPSG", "code": 9820}, + }, + "parameters": [ + { + "name": "Latitude of natural origin", + "value": latitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8801}, + }, + { + "name": "Longitude of natural origin", + "value": longitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8802}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(laea_json) + + +class LambertConformalConic2SPOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Lambert Conformal Conic 2SP operation. + + https://proj.org/operations/projections/lcc.html + """ + + def __new__( + cls, + latitude_first_parallel, + latitude_second_parallel, + latitude_false_origin=0.0, + longitude_false_origin=0.0, + easting_false_origin=0.0, + northing_false_origin=0.0, + ): + """ + Parameters + ---------- + latitude_first_parallel: float + Latitude of 1st standard parallel (lat_1). + latitude_second_parallel: float + Latitude of 2nd standard parallel (lat_2). + latitude_false_origin: float, optional + Latitude of projection center (lat_0). Defaults to 0.0. + longitude_false_origin: float, optional + Longitude of projection center (lon_0). Defaults to 0.0. + easting_false_origin: float, optional + False easting (x_0). Defaults to 0.0. + northing_false_origin: float, optional + False northing (y_0). Defaults to 0.0. + + """ + lcc_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Lambert Conic Conformal (2SP)", + "id": {"authority": "EPSG", "code": 9802}, + }, + "parameters": [ + { + "name": "Latitude of 1st standard parallel", + "value": latitude_first_parallel, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8823}, + }, + { + "name": "Latitude of 2nd standard parallel", + "value": latitude_second_parallel, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8824}, + }, + { + "name": "Latitude of false origin", + "value": latitude_false_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8821}, + }, + { + "name": "Longitude of false origin", + "value": longitude_false_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8822}, + }, + { + "name": "Easting at false origin", + "value": easting_false_origin, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8826}, + }, + { + "name": "Northing at false origin", + "value": northing_false_origin, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8827}, + }, + ], + } + return cls.from_json_dict(lcc_json) + + +class LambertConformalConic1SPOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Lambert Conformal Conic 1SP operation. + + https://proj.org/operations/projections/lcc.html + """ + + def __new__( + cls, + latitude_natural_origin=0.0, + longitude_natural_origin=0.0, + false_easting=0.0, + false_northing=0.0, + scale_factor_natural_origin=1.0, + ): + """ + Parameters + ---------- + latitude_natural_origin: float, optional + Latitude of projection center (lat_0). Defaults to 0.0. + longitude_natural_origin: float, optional + Longitude of projection center (lon_0). Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + scale_factor_natural_origin: float, optional + Scale factor at natural origin (k_0). Defaults to 1.0. + + """ + lcc_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Lambert Conic Conformal (1SP)", + "id": {"authority": "EPSG", "code": 9801}, + }, + "parameters": [ + { + "name": "Latitude of natural origin", + "value": latitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8801}, + }, + { + "name": "Longitude of natural origin", + "value": longitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8802}, + }, + { + "name": "Scale factor at natural origin", + "value": scale_factor_natural_origin, + "unit": "unity", + "id": {"authority": "EPSG", "code": 8805}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(lcc_json) + + +class LambertCylindricalEqualAreaOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Lambert Cylindrical Equal Area operation. + + https://proj.org/operations/projections/cea.html + """ + + def __new__( + cls, + latitude_first_parallel=0.0, + longitude_natural_origin=0.0, + false_easting=0.0, + false_northing=0.0, + ): + """ + Parameters + ---------- + latitude_first_parallel: float, optional + Latitude of 1st standard parallel (lat_ts). Defaults to 0.0. + longitude_natural_origin: float, optional + Longitude of projection center (lon_0). Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + + """ + cea_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Lambert Cylindrical Equal Area", + "id": {"authority": "EPSG", "code": 9835}, + }, + "parameters": [ + { + "name": "Latitude of 1st standard parallel", + "value": latitude_first_parallel, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8823}, + }, + { + "name": "Longitude of natural origin", + "value": longitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8802}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(cea_json) + + +class MercatorAOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Mercator (variant A) operation. + + https://proj.org/operations/projections/merc.html + """ + + def __new__( + cls, + latitude_natural_origin=0.0, + longitude_natural_origin=0.0, + false_easting=0.0, + false_northing=0.0, + scale_factor_natural_origin=1.0, + ): + """ + Parameters + ---------- + longitude_natural_origin: float, optional + Latitude of natural origin (lat_0). Defaults to 0.0. + longitude_natural_origin: float, optional + Longitude of natural origin (lon_0). Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + scale_factor_natural_origin: float, optional + Scale factor at natural origin (k or k_0). Defaults to 1.0 + + """ + merc_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Mercator (variant A)", + "id": {"authority": "EPSG", "code": 9804}, + }, + "parameters": [ + { + "name": "Latitude of natural origin", + "value": latitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8801}, + }, + { + "name": "Longitude of natural origin", + "value": longitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8802}, + }, + { + "name": "Scale factor at natural origin", + "value": scale_factor_natural_origin, + "unit": "unity", + "id": {"authority": "EPSG", "code": 8805}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(merc_json) + + +class MercatorBOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Mercator (variant B) operation. + + https://proj.org/operations/projections/merc.html + """ + + def __new__( + cls, + latitude_first_parallel=0.0, + longitude_natural_origin=0.0, + false_easting=0.0, + false_northing=0.0, + ): + """ + Parameters + ---------- + latitude_first_parallel: float, optional + Latitude of 1st standard parallel (lat_ts). Defaults to 0.0. + longitude_natural_origin: float, optional + Longitude of projection center (lon_0). Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + + """ + merc_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Mercator (variant B)", + "id": {"authority": "EPSG", "code": 9805}, + }, + "parameters": [ + { + "name": "Latitude of 1st standard parallel", + "value": latitude_first_parallel, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8823}, + }, + { + "name": "Longitude of natural origin", + "value": longitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8802}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(merc_json) + + +class HotineObliqueMercatorBOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Hotine Oblique Mercator (variant B) operation. + + https://proj.org/operations/projections/omerc.html + """ + + def __new__( + cls, + latitude_projection_centre, + longitude_projection_centre, + azimuth_initial_line, + angle_from_rectified_to_skew_grid, + scale_factor_on_initial_line=1.0, + easting_projection_centre=0.0, + northing_projection_centre=0.0, + ): + """ + Parameters + ---------- + latitude_projection_centre: float + Latitude of projection centre (lat_0). + longitude_projection_centre: float + Longitude of projection centre (lonc). + azimuth_initial_line: float + Azimuth of initial line (azimuth). + angle_from_rectified_to_skew_grid: float + Angle from Rectified to Skew Grid (gamma). + scale_factor_on_initial_line: float, optional + Scale factor on initial line (k or k_0). Default is 1.0. + easting_projection_centre: float, optional + Easting at projection centre (x_0). Default is 0. + northing_projection_centre: float, optional + Northing at projection centre (y_0). Default is 0. + """ + omerc_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Hotine Oblique Mercator (variant B)", + "id": {"authority": "EPSG", "code": 9815}, + }, + "parameters": [ + { + "name": "Latitude of projection centre", + "value": latitude_projection_centre, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8811}, + }, + { + "name": "Longitude of projection centre", + "value": longitude_projection_centre, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8812}, + }, + { + "name": "Azimuth of initial line", + "value": azimuth_initial_line, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8813}, + }, + { + "name": "Angle from Rectified to Skew Grid", + "value": angle_from_rectified_to_skew_grid, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8814}, + }, + { + "name": "Scale factor on initial line", + "value": scale_factor_on_initial_line, + "unit": "unity", + "id": {"authority": "EPSG", "code": 8815}, + }, + { + "name": "Easting at projection centre", + "value": easting_projection_centre, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8816}, + }, + { + "name": "Northing at projection centre", + "value": northing_projection_centre, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8817}, + }, + ], + } + return cls.from_json_dict(omerc_json) + + +class OrthographicOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Orthographic operation. + + https://proj.org/operations/projections/ortho.html + """ + + def __new__( + cls, + latitude_natural_origin=0.0, + longitude_natural_origin=0.0, + false_easting=0.0, + false_northing=0.0, + ): + """ + Parameters + ---------- + latitude_natural_origin: float, optional + Latitude of projection center (lat_0). Defaults to 0.0. + longitude_natural_origin: float, optional + Longitude of projection center (lon_0). Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + + """ + ortho_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Orthographic", + "id": {"authority": "EPSG", "code": 9840}, + }, + "parameters": [ + { + "name": "Latitude of natural origin", + "value": latitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8801}, + }, + { + "name": "Longitude of natural origin", + "value": longitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8802}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(ortho_json) + + +class PolarStereographicAOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Polar Stereographic A operation. + + https://proj.org/operations/projections/stere.html + """ + + def __new__( + cls, + latitude_natural_origin=0.0, + longitude_natural_origin=0.0, + false_easting=0.0, + false_northing=0.0, + scale_factor_natural_origin=1.0, + ): + """ + Parameters + ---------- + longitude_natural_origin: float, optional + Latitude of natural origin (lat_0). Defaults to 0.0. + longitude_natural_origin: float, optional + Longitude of natural origin (lon_0). Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + scale_factor_natural_origin: float, optional + Scale factor at natural origin (k or k_0). Defaults to 1.0 + + """ + + stere_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Polar Stereographic (variant A)", + "id": {"authority": "EPSG", "code": 9810}, + }, + "parameters": [ + { + "name": "Latitude of natural origin", + "value": latitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8801}, + }, + { + "name": "Longitude of natural origin", + "value": longitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8802}, + }, + { + "name": "Scale factor at natural origin", + "value": scale_factor_natural_origin, + "unit": "unity", + "id": {"authority": "EPSG", "code": 8805}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(stere_json) + + +class PolarStereographicBOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Polar Stereographic B operation. + + https://proj.org/operations/projections/stere.html + """ + + def __new__( + cls, + latitude_standard_parallel=0.0, + longitude_origin=0.0, + false_easting=0.0, + false_northing=0.0, + ): + """ + Parameters + ---------- + latitude_standard_parallel: float, optional + Latitude of standard parallel (lat_ts). Defaults to 0.0. + longitude_origin: float, optional + Longitude of origin (lon_0). Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + + """ + stere_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Polar Stereographic (variant B)", + "id": {"authority": "EPSG", "code": 9829}, + }, + "parameters": [ + { + "name": "Latitude of standard parallel", + "value": latitude_standard_parallel, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8832}, + }, + { + "name": "Longitude of origin", + "value": longitude_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8833}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(stere_json) + + +class SinusoidalOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Sinusoidal operation. + + https://proj.org/operations/projections/sinu.html + """ + + def __new__( + cls, longitude_natural_origin=0.0, false_easting=0.0, false_northing=0.0, + ): + """ + Parameters + ---------- + longitude_natural_origin: float, optional + Longitude of projection center (lon_0). Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + + """ + sinu_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": {"name": "Sinusoidal"}, + "parameters": [ + { + "name": "Longitude of natural origin", + "value": longitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8802}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(sinu_json) + + +class UTMOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the UTM operation. + + https://proj.org/operations/projections/utm.html + """ + + def __new__( + cls, zone, hemisphere="N", + ): + """ + Parameters + ---------- + zone: int + UTM Zone between 1-60. + hemisphere: str, optional + Either N for North or S for South. Default is N. + """ + return cls.from_name( + "UTM zone {zone}{hemisphere}".format(zone=zone, hemisphere=hemisphere) + ) + + +class TransverseMercatorOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Transverse Mercator operation. + + https://proj.org/operations/projections/tmerc.html + """ + + def __new__( + cls, + latitude_natural_origin=0.0, + longitude_natural_origin=0.0, + false_easting=0.0, + false_northing=0.0, + scale_factor_natural_origin=1.0, + ): + """ + Parameters + ---------- + latitude_natural_origin: float, optional + Latitude of projection center (lat_0). Defaults to 0.0. + longitude_natural_origin: float, optional + Longitude of projection center (lon_0). Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + scale_factor_natural_origin: float, optional + Scale factor at natural origin (k or k_0). Defaults to 1.0 + + """ + tmerc_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Transverse Mercator", + "id": {"authority": "EPSG", "code": 9807}, + }, + "parameters": [ + { + "name": "Latitude of natural origin", + "value": latitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8801}, + }, + { + "name": "Longitude of natural origin", + "value": longitude_natural_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8802}, + }, + { + "name": "Scale factor at natural origin", + "value": scale_factor_natural_origin, + "unit": "unity", + "id": {"authority": "EPSG", "code": 8805}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(tmerc_json) + + +class VerticalPerspectiveOperation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Vetical Perspective operation. + + https://proj.org/operations/projections/nsper.html + """ + + def __new__( + cls, + viewpoint_height, + latitude_topocentric_origin=0.0, + longitude_topocentric_origin=0.0, + ellipsoidal_height_topocentric_origin=0.0, + false_easting=0.0, + false_northing=0.0, + ): + """ + Parameters + ---------- + viewpoint_height: float + Viewpoint height (h). + latitude_topocentric_origin: float, optional + Latitude of topocentric origin (lat_0). Defaults to 0.0. + longitude_topocentric_origin: float, optional + Longitude of topocentric origin (lon_0). Defaults to 0.0. + ellipsoidal_height_topocentric_origin: float, optional + Ellipsoidal height of topocentric origin. Defaults to 0.0. + false_easting: float, optional + False easting (x_0). Defaults to 0.0. + false_northing: float, optional + False northing (y_0). Defaults to 0.0. + + """ + nsper_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": { + "name": "Vertical Perspective", + "id": {"authority": "EPSG", "code": 9838}, + }, + "parameters": [ + { + "name": "Latitude of topocentric origin", + "value": latitude_topocentric_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8834}, + }, + { + "name": "Longitude of topocentric origin", + "value": longitude_topocentric_origin, + "unit": "degree", + "id": {"authority": "EPSG", "code": 8835}, + }, + { + "name": "Ellipsoidal height of topocentric origin", + "value": ellipsoidal_height_topocentric_origin, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8836}, + }, + { + "name": "Viewpoint height", + "value": viewpoint_height, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8840}, + }, + { + "name": "False easting", + "value": false_easting, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8806}, + }, + { + "name": "False northing", + "value": false_northing, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8807}, + }, + ], + } + return cls.from_json_dict(nsper_json) + + +class RotatedLatitudeLongitude(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the Rotated Latitude Longitude operation. + + https://proj.org/operations/projections/ob_tran.html + """ + + def __new__( + cls, + grid_north_pole_latitude, + grid_north_pole_longitude, + north_pole_grid_longitude=0.0, + ): + """ + Parameters + ---------- + grid_north_pole_latitude: float + Grid north pole latitude (o_lat_p). + grid_north_pole_longitude: + Grid north pole longitude (o_lon_p). + north_pole_grid_longitude: float, optional + North pole grid longitude (lon_0). Defaults to 0.0. + + """ + rot_latlon_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Conversion", + "name": "unknown", + "method": {"name": "PROJ ob_tran o_proj=longlat"}, + "parameters": [ + { + "name": "o_lat_p", + "value": grid_north_pole_latitude, + "unit": "degree", + }, + { + "name": "o_lon_p", + "value": grid_north_pole_longitude, + "unit": "degree", + }, + {"name": "lon_0", "value": north_pole_grid_longitude, "unit": "degree"}, + ], + } + return cls.from_json_dict(rot_latlon_json) + + +class ToWGS84Transformation(CoordinateOperation): + """ + .. versionadded:: 2.5.0 + + Class for constructing the ToWGS84 Transformation. + """ + + def __new__( + cls, + source_crs, + x_axis_translation=0, + y_axis_translation=0, + z_axis_translation=0, + x_axis_rotation=0, + y_axis_rotation=0, + z_axis_rotation=0, + scale_difference=0, + ): + """ + Parameters + ---------- + x_axis_translation: float, optional + X-axis translation. Defaults to 0.0. + y_axis_translation: float, optional + Y-axis translation. Defaults to 0.0. + z_axis_translation: float, optional + Z-axis translation. Defaults to 0.0. + x_axis_rotation: float, optional + X-axis rotation. Defaults to 0.0. + y_axis_rotation: float, optional + Y-axis rotation. Defaults to 0.0. + z_axis_rotation: float, optional + Z-axis rotation. Defaults to 0.0. + scale_difference: float, optional + Scale difference. Defaults to 0.0. + """ + towgs84_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Transformation", + "name": "Transformation from unknown to WGS84", + "source_crs": CRS.from_user_input(source_crs).to_json_dict(), + "target_crs": { + "type": "GeographicCRS", + "name": "WGS 84", + "datum": { + "type": "GeodeticReferenceFrame", + "name": "World Geodetic System 1984", + "ellipsoid": { + "name": "WGS 84", + "semi_major_axis": 6378137, + "inverse_flattening": 298.257223563, + }, + }, + "coordinate_system": { + "subtype": "ellipsoidal", + "axis": [ + { + "name": "Latitude", + "abbreviation": "lat", + "direction": "north", + "unit": "degree", + }, + { + "name": "Longitude", + "abbreviation": "lon", + "direction": "east", + "unit": "degree", + }, + ], + }, + "id": {"authority": "EPSG", "code": 4326}, + }, + "method": { + "name": "Position Vector transformation (geog2D domain)", + "id": {"authority": "EPSG", "code": 9606}, + }, + "parameters": [ + { + "name": "X-axis translation", + "value": x_axis_translation, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8605}, + }, + { + "name": "Y-axis translation", + "value": y_axis_translation, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8606}, + }, + { + "name": "Z-axis translation", + "value": z_axis_translation, + "unit": "metre", + "id": {"authority": "EPSG", "code": 8607}, + }, + { + "name": "X-axis rotation", + "value": x_axis_rotation, + "unit": { + "type": "AngularUnit", + "name": "arc-second", + "conversion_factor": 4.84813681109536e-06, + }, + "id": {"authority": "EPSG", "code": 8608}, + }, + { + "name": "Y-axis rotation", + "value": y_axis_rotation, + "unit": { + "type": "AngularUnit", + "name": "arc-second", + "conversion_factor": 4.84813681109536e-06, + }, + "id": {"authority": "EPSG", "code": 8609}, + }, + { + "name": "Z-axis rotation", + "value": z_axis_rotation, + "unit": { + "type": "AngularUnit", + "name": "arc-second", + "conversion_factor": 4.84813681109536e-06, + }, + "id": {"authority": "EPSG", "code": 8610}, + }, + { + "name": "Scale difference", + "value": scale_difference, + "unit": { + "type": "ScaleUnit", + "name": "parts per million", + "conversion_factor": 1e-06, + }, + "id": {"authority": "EPSG", "code": 8611}, + }, + ], + } + + return cls.from_json_dict(towgs84_json) diff --git a/pyproj/crs/coordinate_system.py b/pyproj/crs/coordinate_system.py new file mode 100644 index 000000000..2ac94ed3b --- /dev/null +++ b/pyproj/crs/coordinate_system.py @@ -0,0 +1,357 @@ +from pyproj._crs import CoordinateSystem +from pyproj.crs.enums import ( + Cartesian2DCSAxis, + Ellipsoidal2DCSAxis, + Ellipsoidal3DCSAxis, + VerticalCSAxis, +) + +# useful constants to use when setting PROJ JSON units +UNIT_METRE = "metre" +UNIT_DEGREE = "degree" +UNIT_FT = {"type": "LinearUnit", "name": "foot", "conversion_factor": 0.3048} +UNIT_US_FT = { + "type": "LinearUnit", + "name": "US survey foot", + "conversion_factor": 0.304800609601219, +} + +_ELLIPSOIDAL_2D_AXIS_MAP = { + Ellipsoidal2DCSAxis.LONGITUDE_LATITUDE: [ + { + "name": "Longitude", + "abbreviation": "lon", + "direction": "east", + "unit": UNIT_DEGREE, + }, + { + "name": "Latitude", + "abbreviation": "lat", + "direction": "north", + "unit": UNIT_DEGREE, + }, + ], + Ellipsoidal2DCSAxis.LATITUDE_LONGITUDE: [ + { + "name": "Latitude", + "abbreviation": "lat", + "direction": "north", + "unit": UNIT_DEGREE, + }, + { + "name": "Longitude", + "abbreviation": "lon", + "direction": "east", + "unit": UNIT_DEGREE, + }, + ], +} + + +class Ellipsoidal2DCS(CoordinateSystem): + """ + .. versionadded:: 2.5.0 + + This generates an Ellipsoidal 2D Coordinate System + """ + + def __new__(cls, axis=Ellipsoidal2DCSAxis.LONGITUDE_LATITUDE): + """ + Parameters + ---------- + axis: :class:`pyproj.crs.enums.Ellipsoidal2DCSAxis` or str, optional + This is the axis order of the coordinate system. + """ + return cls.from_json_dict( + { + "type": "CoordinateSystem", + "subtype": "ellipsoidal", + "axis": _ELLIPSOIDAL_2D_AXIS_MAP[Ellipsoidal2DCSAxis.create(axis)], + } + ) + + +_ELLIPSOIDAL_3D_AXIS_MAP = { + Ellipsoidal3DCSAxis.LONGITUDE_LATITUDE_HEIGHT: [ + { + "name": "Longitude", + "abbreviation": "lon", + "direction": "east", + "unit": UNIT_DEGREE, + }, + { + "name": "Latitude", + "abbreviation": "lat", + "direction": "north", + "unit": UNIT_DEGREE, + }, + { + "name": "Ellipsoidal height", + "abbreviation": "h", + "direction": "up", + "unit": UNIT_METRE, + }, + ], + Ellipsoidal3DCSAxis.LATITUDE_LONGITUDE_HEIGHT: [ + { + "name": "Latitude", + "abbreviation": "lat", + "direction": "north", + "unit": UNIT_DEGREE, + }, + { + "name": "Longitude", + "abbreviation": "lon", + "direction": "east", + "unit": UNIT_DEGREE, + }, + { + "name": "Ellipsoidal height", + "abbreviation": "h", + "direction": "up", + "unit": UNIT_METRE, + }, + ], +} + + +class Ellipsoidal3DCS(CoordinateSystem): + """ + .. versionadded:: 2.5.0 + + This generates an Ellipsoidal 3D Coordinate System + """ + + def __new__(cls, axis=Ellipsoidal3DCSAxis.LONGITUDE_LATITUDE_HEIGHT): + """ + Parameters + ---------- + axis: :class:`pyproj.crs.enums.Ellipsoidal3DCSAxis` or str, optional + This is the axis order of the coordinate system. + """ + return cls.from_json_dict( + { + "type": "CoordinateSystem", + "subtype": "ellipsoidal", + "axis": _ELLIPSOIDAL_3D_AXIS_MAP[Ellipsoidal3DCSAxis.create(axis)], + } + ) + + +_CARTESIAN_2D_AXIS_MAP = { + Cartesian2DCSAxis.EASTING_NORTHING: [ + {"name": "Easting", "abbreviation": "E", "direction": "east", "unit": "metre"}, + { + "name": "Northing", + "abbreviation": "N", + "direction": "north", + "unit": UNIT_METRE, + }, + ], + Cartesian2DCSAxis.NORTHING_EASTING: [ + { + "name": "Northing", + "abbreviation": "N", + "direction": "north", + "unit": UNIT_METRE, + }, + { + "name": "Easting", + "abbreviation": "E", + "direction": "east", + "unit": UNIT_METRE, + }, + ], + Cartesian2DCSAxis.EASTING_NORTHING_FT: [ + {"name": "Easting", "abbreviation": "X", "direction": "east", "unit": UNIT_FT}, + { + "name": "Northing", + "abbreviation": "Y", + "direction": "north", + "unit": UNIT_FT, + }, + ], + Cartesian2DCSAxis.NORTHING_EASTING_FT: [ + { + "name": "Northing", + "abbreviation": "Y", + "direction": "north", + "unit": UNIT_FT, + }, + {"name": "Easting", "abbreviation": "X", "direction": "east", "unit": UNIT_FT}, + ], + Cartesian2DCSAxis.EASTING_NORTHING_US_FT: [ + { + "name": "Easting", + "abbreviation": "X", + "direction": "east", + "unit": UNIT_US_FT, + }, + { + "name": "Northing", + "abbreviation": "Y", + "direction": "north", + "unit": UNIT_US_FT, + }, + ], + Cartesian2DCSAxis.NORTHING_EASTING_US_FT: [ + { + "name": "Northing", + "abbreviation": "Y", + "direction": "north", + "unit": UNIT_US_FT, + }, + { + "name": "Easting", + "abbreviation": "X", + "direction": "east", + "unit": UNIT_US_FT, + }, + ], + Cartesian2DCSAxis.NORTH_POLE_EASTING_SOUTH_NORTHING_SOUTH: [ + { + "name": "Easting", + "abbreviation": "E", + "direction": "south", + "unit": UNIT_METRE, + }, + { + "name": "Northing", + "abbreviation": "N", + "direction": "south", + "unit": UNIT_METRE, + }, + ], + Cartesian2DCSAxis.SOUTH_POLE_EASTING_NORTH_NORTHING_NORTH: [ + { + "name": "Easting", + "abbreviation": "E", + "direction": "north", + "unit": UNIT_METRE, + }, + { + "name": "Northing", + "abbreviation": "N", + "direction": "north", + "unit": UNIT_METRE, + }, + ], + Cartesian2DCSAxis.WESTING_SOUTHING: [ + { + "name": "Easting", + "abbreviation": "Y", + "direction": "west", + "unit": UNIT_METRE, + }, + { + "name": "Northing", + "abbreviation": "X", + "direction": "south", + "unit": UNIT_METRE, + }, + ], +} + + +class Cartesian2DCS(CoordinateSystem): + """ + .. versionadded:: 2.5.0 + + This generates an Cartesian 2D Coordinate System + """ + + def __new__(cls, axis=Cartesian2DCSAxis.EASTING_NORTHING): + """ + Parameters + ---------- + axis: :class:`pyproj.crs.enums.Cartesian2DCSAxis` or str, optional + This is the axis order of the coordinate system. + """ + return cls.from_json_dict( + { + "type": "CoordinateSystem", + "subtype": "Cartesian", + "axis": _CARTESIAN_2D_AXIS_MAP[Cartesian2DCSAxis.create(axis)], + } + ) + + +_VERTICAL_AXIS_MAP = { + VerticalCSAxis.GRAVITY_HEIGHT: { + "name": "Gravity-related height", + "abbreviation": "H", + "direction": "up", + "unit": UNIT_METRE, + }, + VerticalCSAxis.GRAVITY_HEIGHT_US_FT: { + "name": "Gravity-related height", + "abbreviation": "H", + "direction": "up", + "unit": UNIT_US_FT, + }, + VerticalCSAxis.GRAVITY_HEIGHT_FT: { + "name": "Gravity-related height", + "abbreviation": "H", + "direction": "up", + "unit": UNIT_FT, + }, + VerticalCSAxis.DEPTH: { + "name": "Depth", + "abbreviation": "D", + "direction": "down", + "unit": UNIT_METRE, + }, + VerticalCSAxis.DEPTH_US_FT: { + "name": "Depth", + "abbreviation": "D", + "direction": "down", + "unit": UNIT_US_FT, + }, + VerticalCSAxis.DEPTH_FT: { + "name": "Depth", + "abbreviation": "D", + "direction": "down", + "unit": UNIT_FT, + }, + VerticalCSAxis.UP: { + "name": "up", + "abbreviation": "H", + "direction": "up", + "unit": UNIT_METRE, + }, + VerticalCSAxis.UP_FT: { + "name": "up", + "abbreviation": "H", + "direction": "up", + "unit": UNIT_FT, + }, + VerticalCSAxis.UP_US_FT: { + "name": "up", + "abbreviation": "H", + "direction": "up", + "unit": UNIT_US_FT, + }, +} + + +class VerticalCS(CoordinateSystem): + """ + .. versionadded:: 2.5.0 + + This generates an Vertical Coordinate System + """ + + def __new__(cls, axis=VerticalCSAxis.GRAVITY_HEIGHT): + """ + Parameters + ---------- + axis: :class:`pyproj.crs.enums.VerticalCSAxis` or str, optional + This is the axis direction of the coordinate system. + """ + return cls.from_json_dict( + { + "type": "CoordinateSystem", + "subtype": "vertical", + "axis": [_VERTICAL_AXIS_MAP[VerticalCSAxis.create(axis)]], + } + ) diff --git a/pyproj/crs.py b/pyproj/crs/crs.py similarity index 75% rename from pyproj/crs.py rename to pyproj/crs/crs.py index f98641d9b..00486e60b 100644 --- a/pyproj/crs.py +++ b/pyproj/crs/crs.py @@ -1,35 +1,10 @@ # -*- coding: utf-8 -*- """ This module interfaces with PROJ to produce a pythonic interface -to the coordinate reference system (CRS) information through the CRS -class. +to the coordinate reference system (CRS) information. Original Author: Alan D. Snow [github.com/snowman2] (2019) - -Permission to use, copy, modify, and distribute this software -and its documentation for any purpose and without fee is hereby -granted, provided that the above copyright notice appear in all -copies and that both the copyright notice and this permission -notice appear in supporting documentation. THE AUTHOR DISCLAIMS -ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT -SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR -CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, -NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ -__all__ = [ - "CRS", - "CoordinateOperation", - "CoordinateSystem", - "Datum", - "Ellipsoid", - "PrimeMeridian", - "is_wkt", - "is_proj", -] - import json import re import warnings @@ -45,7 +20,7 @@ is_proj, is_wkt, ) -from pyproj.cf1x8 import ( +from pyproj.crs._cf1x8 import ( GRID_MAPPING_NAME_MAP, INVERSE_GRID_MAPPING_NAME_MAP, INVERSE_PROJ_PARAM_MAP, @@ -55,6 +30,7 @@ PARAM_TO_CF_MAP, PROJ_PARAM_MAP, ) +from pyproj.crs.coordinate_system import Cartesian2DCS, Ellipsoidal2DCS, VerticalCS from pyproj.exceptions import CRSError from pyproj.geod import Geod @@ -311,10 +287,10 @@ def __init__(self, projparams=None, **kwargs): else: raise CRSError("Invalid CRS input: {!r}".format(projparams)) - super(CRS, self).__init__(projstring) + super().__init__(projstring) - @classmethod - def from_authority(cls, auth_name, code): + @staticmethod + def from_authority(auth_name, code): """ .. versionadded:: 2.2.0 @@ -331,10 +307,10 @@ def from_authority(cls, auth_name, code): ------- CRS """ - return cls(_prepare_from_authority(auth_name, code)) + return CRS(_prepare_from_authority(auth_name, code)) - @classmethod - def from_epsg(cls, code): + @staticmethod + def from_epsg(code): """Make a CRS from an EPSG code Parameters @@ -346,10 +322,10 @@ def from_epsg(cls, code): ------- CRS """ - return cls(_prepare_from_epsg(code)) + return CRS(_prepare_from_epsg(code)) - @classmethod - def from_proj4(cls, in_proj_string): + @staticmethod + def from_proj4(in_proj_string): """ .. versionadded:: 2.2.0 @@ -366,10 +342,10 @@ def from_proj4(cls, in_proj_string): """ if not is_proj(in_proj_string): raise CRSError("Invalid PROJ string: {}".format(in_proj_string)) - return cls(_prepare_from_string(in_proj_string)) + return CRS(_prepare_from_string(in_proj_string)) - @classmethod - def from_wkt(cls, in_wkt_string): + @staticmethod + def from_wkt(in_wkt_string): """ .. versionadded:: 2.2.0 @@ -386,10 +362,10 @@ def from_wkt(cls, in_wkt_string): """ if not is_wkt(in_wkt_string): raise CRSError("Invalid WKT string: {}".format(in_wkt_string)) - return cls(_prepare_from_string(in_wkt_string)) + return CRS(_prepare_from_string(in_wkt_string)) - @classmethod - def from_string(cls, in_crs_string): + @staticmethod + def from_string(in_crs_string): """ Make a CRS from: @@ -408,7 +384,7 @@ def from_string(cls, in_crs_string): ------- CRS """ - return cls(_prepare_from_string(in_crs_string)) + return CRS(_prepare_from_string(in_crs_string)) def to_string(self): """ @@ -429,8 +405,8 @@ def to_string(self): return ":".join(auth_info) return self.srs - @classmethod - def from_user_input(cls, value): + @staticmethod + def from_user_input(value): """ Initialize a CRS class instance with: - PROJ string @@ -455,7 +431,7 @@ def from_user_input(cls, value): """ if isinstance(value, CRS): return value - return cls(value) + return CRS(value) def get_geod(self): """ @@ -472,8 +448,8 @@ def get_geod(self): } return Geod(**in_kwargs) - @classmethod - def from_dict(cls, proj_dict): + @staticmethod + def from_dict(proj_dict): """ .. versionadded:: 2.2.0 @@ -488,10 +464,10 @@ def from_dict(cls, proj_dict): ------- CRS """ - return cls(_prepare_from_dict(proj_dict)) + return CRS(_prepare_from_dict(proj_dict)) - @classmethod - def from_json(cls, crs_json): + @staticmethod + def from_json(crs_json): """ .. versionadded:: 2.4.0 @@ -506,10 +482,10 @@ def from_json(cls, crs_json): ------- CRS """ - return cls.from_json_dict(_load_proj_json(crs_json)) + return CRS.from_json_dict(_load_proj_json(crs_json)) - @classmethod - def from_json_dict(cls, crs_dict): + @staticmethod + def from_json_dict(crs_dict): """ .. versionadded:: 2.4.0 @@ -524,7 +500,7 @@ def from_json_dict(cls, crs_dict): ------- CRS """ - return cls(json.dumps(crs_dict)) + return CRS(json.dumps(crs_dict)) def to_dict(self): """ @@ -678,8 +654,8 @@ def to_cf(self, wkt_version="WKT2_2018", errcheck=False): ) return cf_dict - @classmethod - def from_cf(cls, in_cf, errcheck=False): + @staticmethod + def from_cf(in_cf, errcheck=False): """ .. versionadded:: 2.2.0 @@ -752,7 +728,7 @@ def from_cf(cls, in_cf, errcheck=False): "CF parameters not mapped to PROJ: {}".format(tuple(skipped_params)) ) - return cls(proj_dict) + return CRS(proj_dict) def is_exact_same(self, other, ignore_axis_order=False): """ @@ -773,7 +749,7 @@ def is_exact_same(self, other, ignore_axis_order=False): other = CRS.from_user_input(other) except CRSError: return False - return super(CRS, self).is_exact_same(other) + return super().is_exact_same(other) def equals(self, other, ignore_axis_order=False): """ @@ -800,7 +776,56 @@ def equals(self, other, ignore_axis_order=False): other = CRS.from_user_input(other) except CRSError: return False - return super(CRS, self).equals(other, ignore_axis_order=ignore_axis_order) + return super().equals(other, ignore_axis_order=ignore_axis_order) + + @property + def geodetic_crs(self): + """ + .. versionadded:: 2.2.0 + + Returns + ------- + CRS: The the geodeticCRS / geographicCRS from the CRS. + """ + if super().geodetic_crs is None: + return None + return CRS(super().geodetic_crs.srs) + + @property + def source_crs(self): + """ + Returns + ------- + CRS: The the base CRS of a BoundCRS or a DerivedCRS/ProjectedCRS, + or the source CRS of a CoordinateOperation. + """ + if super().source_crs is None: + return None + return CRS(super().source_crs.srs) + + @property + def target_crs(self): + """ + .. versionadded:: 2.2.0 + + Returns + ------- + CRS: The hub CRS of a BoundCRS or the target CRS of a CoordinateOperation. + """ + if super().target_crs is None: + return None + return CRS(super().target_crs.srs) + + @property + def sub_crs_list(self): + """ + If the CRS is a compound CRS, it will return a list of sub CRS objects. + + Returns + ------- + list[CRS] + """ + return [CRS(sub_crs.srs) for sub_crs in super().sub_crs_list] def __eq__(self, other): return self.equals(other) @@ -888,3 +913,187 @@ def extent_axis(axis_list): sub_crs_repr=sub_crs_repr, ) return string_repr + + +class GeographicCRS(CRS): + """ + .. versionadded:: 2.5.0 + + This class is for building a Geographic CRS + """ + + def __init__( + self, + name="undefined", + datum="urn:ogc:def:datum:EPSG::6326", + ellipsoidal_cs=Ellipsoidal2DCS(), + ): + """ + Parameters + ---------- + name: str, optional + Name of the CRS. Default is undefined. + datum: Any, optional + Anything accepted by :meth:`pyproj.crs.Datum.from_user_input` or + a :class:`pyproj.crs.datum.CustomDatum`. + ellipsoidal_cs: Any, optional + Input to create an Ellipsoidal Coordinate System. + Anything accepted by :meth:`pyproj.crs.CoordinateSystem.from_user_input` + or an Ellipsoidal Coordinate System created from :ref:`coordinate_system`. + """ + geographic_crs_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "GeographicCRS", + "name": name, + "datum": Datum.from_user_input(datum).to_json_dict(), + "coordinate_system": CoordinateSystem.from_user_input( + ellipsoidal_cs + ).to_json_dict(), + } + super().__init__(geographic_crs_json) + + +class ProjectedCRS(CRS): + """ + .. versionadded:: 2.5.0 + + This class is for building a Projected CRS. + """ + + def __init__( + self, + coordinate_operation, + name="undefined", + cartesian_cs=Cartesian2DCS(), + geodetic_crs=GeographicCRS(), + ): + """ + Parameters + ---------- + coordinate_operation: Any + Anything accepted by :meth:`pyproj.crs.CoordinateSystem.from_user_input` + or a coordinate operation from :ref:`coordinate_operation`. + name: str, optional + The name of the Projected CRS. Default is undefined. + cartesian_cs: Any, optional + Input to create a Cartesian Coordinate System. + Anything accepted by :meth:`pyproj.crs.CoordinateSystem.from_user_input` + or :class:`pyproj.crs.coordinate_system.Cartesian2DCS`. + geodetic_crs: Any, optional + Input to create the Geodetic CRS, a :class:`GeographicCRS` or + anything accepted by :meth:`pyproj.crs.CRS.from_user_input`. + """ + proj_crs_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "ProjectedCRS", + "name": name, + "base_crs": CRS.from_user_input(geodetic_crs).to_json_dict(), + "conversion": CoordinateOperation.from_user_input( + coordinate_operation + ).to_json_dict(), + "coordinate_system": CoordinateSystem.from_user_input( + cartesian_cs + ).to_json_dict(), + } + super().__init__(proj_crs_json) + + +class VerticalCRS(CRS): + """ + .. versionadded:: 2.5.0 + + This class is for building a Vetical CRS. + + .. warning:: geoid_model support only exists in PROJ >= 6.3.0 + + """ + + def __init__(self, name, datum, vertical_cs=VerticalCS(), geoid_model=None): + """ + Parameters + ---------- + name: str + The name of the Vertical CRS (e.g. NAVD88 height). + datum: Any + Anything accepted by :meth:`pyproj.crs.Datum.from_user_input` + vertical_cs: Any, optional + Input to create a Vertical Coordinate System accepted by + :meth:`pyproj.crs.CoordinateSystem.from_user_input` + or :class:`pyproj.crs.coordinate_system.VerticalCS` + geoid_model: str, optional + The name of the GEOID Model (e.g. GEOID12B). + """ + vert_crs_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "VerticalCRS", + "name": name, + "datum": Datum.from_user_input(datum).to_json_dict(), + "coordinate_system": CoordinateSystem.from_user_input( + vertical_cs + ).to_json_dict(), + } + if geoid_model is not None: + vert_crs_json["geoid_model"] = {"name": geoid_model} + + super().__init__(vert_crs_json) + + +class CompoundCRS(CRS): + """ + .. versionadded:: 2.5.0 + + This class is for building a Compound CRS. + """ + + def __init__(self, name, components): + """ + Parameters + ---------- + name: str + The name of the Compound CRS. + components: List[Any], optional + List of CRS to create a Compound Coordinate System. + List of anything accepted by :meth:`pyproj.crs.CRS.from_user_input` + """ + compound_crs_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "CompoundCRS", + "name": name, + "components": [ + CRS.from_user_input(component).to_json_dict() + for component in components + ], + } + + super().__init__(compound_crs_json) + + +class BoundCRS(CRS): + """ + .. versionadded:: 2.5.0 + + This class is for building a Bound CRS. + """ + + def __init__(self, source_crs, target_crs, transformation): + """ + Parameters + ---------- + source_crs: Any + Input to create a source CRS. + target_crs: Any + Input to create the target CRS. + transformation: Any + Input to create the transformation. + """ + bound_crs_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "BoundCRS", + "source_crs": CRS.from_user_input(source_crs).to_json_dict(), + "target_crs": CRS.from_user_input(target_crs).to_json_dict(), + "transformation": CoordinateOperation.from_user_input( + transformation + ).to_json_dict(), + } + + super().__init__(bound_crs_json) diff --git a/pyproj/crs/datum.py b/pyproj/crs/datum.py new file mode 100644 index 000000000..e91fa89ad --- /dev/null +++ b/pyproj/crs/datum.py @@ -0,0 +1,29 @@ +from pyproj._crs import Datum, Ellipsoid, PrimeMeridian + + +class CustomDatum(Datum): + """ + .. versionadded:: 2.5.0 + + Class to build a datum based on an ellipsoid and prime meridian. + """ + + def __new__(cls, ellipsoid="WGS 84", prime_meridian="Greenwich"): + """ + Parameters + ---------- + ellipsoid: Any, optional + Anything accepted by :meth:`pyproj.crs.Ellipsoid.from_user_input` + or a :class:`pyproj.crs.ellipsoid.CustomEllipsoid`. + prime_meridian: Any, optional + Anything accepted by :meth:`pyproj.crs.PrimeMeridian.from_user_input`. + """ + datum_json = { + "type": "GeodeticReferenceFrame", + "name": "unknown", + "ellipsoid": Ellipsoid.from_user_input(ellipsoid).to_json_dict(), + "prime_meridian": PrimeMeridian.from_user_input( + prime_meridian + ).to_json_dict(), + } + return cls.from_json_dict(datum_json) diff --git a/pyproj/crs/ellipsoid.py b/pyproj/crs/ellipsoid.py new file mode 100644 index 000000000..fc533bf78 --- /dev/null +++ b/pyproj/crs/ellipsoid.py @@ -0,0 +1,33 @@ +from pyproj._crs import Ellipsoid + + +class CustomEllipsoid(Ellipsoid): + """ + .. versionadded:: 2.5.0 + + Class to build a custom ellipsoid. + """ + + def __new__(cls, semi_major_axis, inverse_flattening=None, semi_minor_axis=None): + """ + Parameters + ---------- + semi_major_axis: float + The semi major axis in meters. + inverse_flattening: float, optional + The inverse flattening in meters. Required if missing semi_minor_axis. + semi_minor_axis: float, optional + The semi minor axis in meters. Required if missing inverse_flattening. + """ + ellipsoid_json = { + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Ellipsoid", + "name": "undefined", + "semi_major_axis": semi_major_axis, + } + if inverse_flattening is not None: + ellipsoid_json["inverse_flattening"] = inverse_flattening + if semi_minor_axis is not None: + ellipsoid_json["semi_minor_axis"] = semi_minor_axis + + return cls.from_json_dict(ellipsoid_json) diff --git a/pyproj/crs/enums.py b/pyproj/crs/enums.py new file mode 100644 index 000000000..62c73cdde --- /dev/null +++ b/pyproj/crs/enums.py @@ -0,0 +1,143 @@ +""" +This module contains enumerations used in pyproj.crs. +""" +from pyproj.enums import BaseEnum + + +class DatumType(BaseEnum): + """ + .. versionadded:: 2.5.0 + + Datum Types for creating datum with :meth:`pyproj.crs.Datum.from_name` + + Attributes + ---------- + GEODETIC_REFERENCE_FRAME + DYNAMIC_GEODETIC_REFERENCE_FRAME + VERTICAL_REFERENCE_FRAME + DYNAMIC_VERTICAL_REFERENCE_FRAME + DATUM_ENSEMBLE + """ + + GEODETIC_REFERENCE_FRAME = "GEODETIC_REFERENCE_FRAME" + DYNAMIC_GEODETIC_REFERENCE_FRAME = "DYNAMIC_GEODETIC_REFERENCE_FRAME" + VERTICAL_REFERENCE_FRAME = "VERTICAL_REFERENCE_FRAME" + DYNAMIC_VERTICAL_REFERENCE_FRAME = "DYNAMIC_VERTICAL_REFERENCE_FRAME" + DATUM_ENSEMBLE = "DATUM_ENSEMBLE" + + +class CoordinateOperationType(BaseEnum): + """ + .. versionadded:: 2.5.0 + + Coordinate Operation Types for creating operation + with :meth:`pyproj.crs.CoordinateOperation.from_name` + + Attributes + ---------- + CONVERSION + TRANSFORMATION + CONCATENATED_OPERATION + OTHER_COORDINATE_OPERATION + """ + + CONVERSION = "CONVERSION" + TRANSFORMATION = "TRANSFORMATION" + CONCATENATED_OPERATION = "CONCATENATED_OPERATION" + OTHER_COORDINATE_OPERATION = "OTHER_COORDINATE_OPERATION" + + +class Cartesian2DCSAxis(BaseEnum): + """ + .. versionadded:: 2.5.0 + + Cartesian 2D Coordinate System Axis for creating axis with + with :class:`pyproj.crs.coordinate_system.Cartesian2DCS` + + Attributes + ---------- + EASTING_NORTHING + NORTHING_EASTING + EASTING_NORTHING_FT + NORTHING_EASTING_FT + EASTING_NORTHING_US_FT + NORTHING_EASTING_US_FT + NORTH_POLE_EASTING_SOUTH_NORTHING_SOUTH + SOUTH_POLE_EASTING_NORTH_NORTHING_NORTH + WESTING_SOUTHING + """ + + EASTING_NORTHING = "EASTING_NORTHING" + NORTHING_EASTING = "NORTHING_EASTING" + EASTING_NORTHING_FT = "EASTING_NORTHING_FT" + NORTHING_EASTING_FT = "NORTHING_EASTING_FT" + EASTING_NORTHING_US_FT = "EASTING_NORTHING_US_FT" + NORTHING_EASTING_US_FT = "NORTHING_EASTING_US_FT" + NORTH_POLE_EASTING_SOUTH_NORTHING_SOUTH = "NORTH_POLE_EASTING_SOUTH_NORTHING_SOUTH" + SOUTH_POLE_EASTING_NORTH_NORTHING_NORTH = "SOUTH_POLE_EASTING_NORTH_NORTHING_NORTH" + WESTING_SOUTHING = "WESTING_SOUTHING" + + +class Ellipsoidal2DCSAxis(BaseEnum): + """ + .. versionadded:: 2.5.0 + + Ellisoidal 2D Coordinate System Axis for creating axis with + with :class:`pyproj.crs.coordinate_system.Ellipsoidal2DCS` + + Attributes + ---------- + LONGITUDE_LATITUDE + LATITUDE_LONGITUDE + """ + + LONGITUDE_LATITUDE = "LONGITUDE_LATITUDE" + LATITUDE_LONGITUDE = "LATITUDE_LONGITUDE" + + +class Ellipsoidal3DCSAxis(BaseEnum): + """ + .. versionadded:: 2.5.0 + + Ellisoidal 3D Coordinate System Axis for creating axis with + with :class:`pyproj.crs.coordinate_system.Ellipsoidal3DCS` + + Attributes + ---------- + LONGITUDE_LATITUDE_HEIGHT + LATITUDE_LONGITUDE_HEIGHT + """ + + LONGITUDE_LATITUDE_HEIGHT = "LONGITUDE_LATITUDE_HEIGHT" + LATITUDE_LONGITUDE_HEIGHT = "LATITUDE_LONGITUDE_HEIGHT" + + +class VerticalCSAxis(BaseEnum): + """ + .. versionadded:: 2.5.0 + + Vertical Coordinate System Axis for creating axis with + with :class:`pyproj.crs.coordinate_system.VerticalCS` + + Attributes + ---------- + UP + UP_FT + UP_US_FT + DEPTH + DEPTH_FT + DEPTH_US_FT + GRAVITY_HEIGHT + GRAVITY_HEIGHT_FT + GRAVITY_HEIGHT_US_FT + """ + + GRAVITY_HEIGHT = "GRAVITY_HEIGHT" + GRAVITY_HEIGHT_FT = "GRAVITY_HEIGHT_FT" + GRAVITY_HEIGHT_US_FT = "GRAVITY_HEIGHT_US_FT" + DEPTH = "DEPTH" + DEPTH_FT = "DEPTH_FT" + DEPTH_US_FT = "DEPTH_US_FT" + UP = "UP" + UP_FT = "UP_FT" + UP_US_FT = "UP_US_FT" diff --git a/pyproj/enums.py b/pyproj/enums.py index b443fdb70..be7096139 100644 --- a/pyproj/enums.py +++ b/pyproj/enums.py @@ -134,46 +134,3 @@ class PJType(BaseEnum): TRANSFORMATION = "TRANSFORMATION" CONCATENATED_OPERATION = "CONCATENATED_OPERATION" OTHER_COORDINATE_OPERATION = "OTHER_COORDINATE_OPERATION" - - -class DatumType(BaseEnum): - """ - .. versionadded:: 2.5.0 - - Datum Types for creating datum with :meth:`pyproj.crs.Datum.from_name` - - Attributes - ---------- - GEODETIC_REFERENCE_FRAME - DYNAMIC_GEODETIC_REFERENCE_FRAME - VERTICAL_REFERENCE_FRAME - DYNAMIC_VERTICAL_REFERENCE_FRAME - DATUM_ENSEMBLE - """ - - GEODETIC_REFERENCE_FRAME = "GEODETIC_REFERENCE_FRAME" - DYNAMIC_GEODETIC_REFERENCE_FRAME = "DYNAMIC_GEODETIC_REFERENCE_FRAME" - VERTICAL_REFERENCE_FRAME = "VERTICAL_REFERENCE_FRAME" - DYNAMIC_VERTICAL_REFERENCE_FRAME = "DYNAMIC_VERTICAL_REFERENCE_FRAME" - DATUM_ENSEMBLE = "DATUM_ENSEMBLE" - - -class CoordinateOperationType(BaseEnum): - """ - .. versionadded:: 2.5.0 - - Coordinate Operation Types for creating operation - with :meth:`pyproj.crs.CoordinateOperation.from_name` - - Attributes - ---------- - CONVERSION - TRANSFORMATION - CONCATENATED_OPERATION - OTHER_COORDINATE_OPERATION - """ - - CONVERSION = "CONVERSION" - TRANSFORMATION = "TRANSFORMATION" - CONCATENATED_OPERATION = "CONCATENATED_OPERATION" - OTHER_COORDINATE_OPERATION = "OTHER_COORDINATE_OPERATION" diff --git a/pyproj/exceptions.py b/pyproj/exceptions.py index a2f71ad06..89711ba9c 100644 --- a/pyproj/exceptions.py +++ b/pyproj/exceptions.py @@ -18,7 +18,7 @@ def __init__(self, error_message): internal_proj_error=self.internal_proj_error, ) ProjError.clear() - super(ProjError, self).__init__(error_message) + super().__init__(error_message) @staticmethod def clear(): diff --git a/pyproj/geod.py b/pyproj/geod.py index e47c15ab8..53ae207d5 100644 --- a/pyproj/geod.py +++ b/pyproj/geod.py @@ -195,7 +195,7 @@ def __init__(self, initstring=None, **kwargs): if math.fabs(f) < 1.0e-8: sphere = True - super(Geod, self).__init__(a, f, sphere, b, es) + super().__init__(a, f, sphere, b, es) def fwd(self, lons, lats, az, dist, radians=False): """ @@ -302,9 +302,7 @@ def npts(self, lon1, lat1, lon2, lat2, npts, radians=False): '46.805 -114.051' '46.262 -118.924' """ - lons, lats = super(Geod, self)._npts( - lon1, lat1, lon2, lat2, npts, radians=radians - ) + lons, lats = super()._npts(lon1, lat1, lon2, lat2, npts, radians=radians) return list(zip(lons, lats)) def line_length(self, lons, lats, radians=False): @@ -558,7 +556,7 @@ def __repr__(self): ) # no ellipse name found, call super class - return super(Geod, self).__repr__() + return super().__repr__() def __eq__(self, other): """ diff --git a/pyproj/proj.py b/pyproj/proj.py index 9c834d754..061d976bf 100644 --- a/pyproj/proj.py +++ b/pyproj/proj.py @@ -168,7 +168,7 @@ def __init__(self, projparams=None, preserve_units=True, **kwargs): projstring = self.crs.to_proj4() or self.crs.srs projstring = re.sub(r"\s\+?type=crs", "", projstring) - super(Proj, self).__init__(cstrencode(projstring.strip())) + super().__init__(cstrencode(projstring.strip())) def __call__(self, *args, **kw): # ,lon,lat,inverse=False,errcheck=False): diff --git a/pyproj/transformer.py b/pyproj/transformer.py index d07b65f20..1835a7c66 100644 --- a/pyproj/transformer.py +++ b/pyproj/transformer.py @@ -93,7 +93,7 @@ def __init__( - unavailable_operations: 1 """ - super(TransformerGroup, self).__init__( + super().__init__( CRS.from_user_input(crs_from), CRS.from_user_input(crs_to), skip_equivalent=skip_equivalent, diff --git a/test/test_crs.py b/test/crs/test_crs.py similarity index 99% rename from test/test_crs.py rename to test/crs/test_crs.py index 5b13f81a0..b68342cc6 100644 --- a/test/test_crs.py +++ b/test/crs/test_crs.py @@ -11,7 +11,8 @@ Ellipsoid, PrimeMeridian, ) -from pyproj.enums import CoordinateOperationType, DatumType, ProjVersion, WktVersion +from pyproj.crs.enums import CoordinateOperationType, DatumType +from pyproj.enums import ProjVersion, WktVersion from pyproj.exceptions import CRSError diff --git a/test/test_crs_cf.py b/test/crs/test_crs_cf.py similarity index 94% rename from test/test_crs_cf.py rename to test/crs/test_crs_cf.py index 719651038..36b795401 100644 --- a/test/test_crs_cf.py +++ b/test/crs/test_crs_cf.py @@ -200,31 +200,6 @@ def test_cf_rotated_latlon__grid(): assert crs.to_dict() == {} -def test_cf_lambert_conformal_conic(): - crs = CRS.from_cf( - dict( - grid_mapping_name="lambert_conformal_conic", - standard_parallel=25.0, - longitude_of_central_meridian=265.0, - latitude_of_projection_origin=25.0, - ) - ) - with pytest.warns(UserWarning): - cf_dict = crs.to_cf(errcheck=True) - assert cf_dict.pop("crs_wkt").startswith("PROJCRS[") - assert cf_dict == { - "grid_mapping_name": "lambert_conformal_conic", - "longitude_of_central_meridian": 265, - "scale_factor_at_projection_origin": 1, - "standard_parallel": 25, - "latitude_of_projection_origin": 25, - "false_easting": 0, - "false_northing": 0, - "horizontal_datum_name": "WGS84", - "unit": "m", - } - - def test_cf_lambert_conformal_conic_1sp(): crs = CRS.from_cf( dict( diff --git a/test/crs/test_crs_coordinate_operation.py b/test/crs/test_crs_coordinate_operation.py new file mode 100644 index 000000000..dad45c083 --- /dev/null +++ b/test/crs/test_crs_coordinate_operation.py @@ -0,0 +1,608 @@ +import pytest + +from pyproj.crs import GeographicCRS +from pyproj.crs.coordinate_operation import ( + AlbersEqualAreaOperation, + AzumuthalEquidistantOperation, + GeostationarySatelliteOperation, + HotineObliqueMercatorBOperation, + LambertAzumuthalEqualAreaOperation, + LambertConformalConic1SPOperation, + LambertConformalConic2SPOperation, + LambertCylindricalEqualAreaOperation, + MercatorAOperation, + MercatorBOperation, + OrthographicOperation, + PolarStereographicAOperation, + PolarStereographicBOperation, + RotatedLatitudeLongitude, + SinusoidalOperation, + ToWGS84Transformation, + TransverseMercatorOperation, + UTMOperation, + VerticalPerspectiveOperation, +) +from pyproj.exceptions import CRSError + + +def _to_dict(operation): + param_dict = {} + for param in operation.params: + param_dict[param.name] = param.value + return param_dict + + +def test_albers_equal_area_operation__defaults(): + aeaop = AlbersEqualAreaOperation( + latitude_first_parallel=1, latitude_second_parallel=2 + ) + assert aeaop.name == "unknown" + assert aeaop.method_name == "Albers Equal Area" + assert _to_dict(aeaop) == { + "Easting at false origin": 0.0, + "Latitude of 1st standard parallel": 1.0, + "Latitude of 2nd standard parallel": 2.0, + "Latitude of false origin": 0.0, + "Longitude of false origin": 0.0, + "Northing at false origin": 0.0, + } + + +def test_albers_equal_area_operation(): + aeaop = AlbersEqualAreaOperation( + latitude_first_parallel=1, + latitude_second_parallel=2, + latitude_false_origin=3, + longitude_false_origin=4, + easting_false_origin=5, + northing_false_origin=6, + ) + assert aeaop.name == "unknown" + assert aeaop.method_name == "Albers Equal Area" + assert _to_dict(aeaop) == { + "Easting at false origin": 5.0, + "Latitude of 1st standard parallel": 1.0, + "Latitude of 2nd standard parallel": 2.0, + "Latitude of false origin": 3.0, + "Longitude of false origin": 4.0, + "Northing at false origin": 6.0, + } + + +def test_azimuthal_equidistant_operation__defaults(): + aeop = AzumuthalEquidistantOperation() + assert aeop.name == "unknown" + assert aeop.method_name == "Modified Azimuthal Equidistant" + assert _to_dict(aeop) == { + "Latitude of natural origin": 0.0, + "Longitude of natural origin": 0.0, + "False easting": 0.0, + "False northing": 0.0, + } + + +def test_azimuthal_equidistant_operation(): + aeop = AzumuthalEquidistantOperation( + latitude_natural_origin=1, + longitude_natural_origin=2, + false_easting=3, + false_northing=4, + ) + assert aeop.name == "unknown" + assert aeop.method_name == "Modified Azimuthal Equidistant" + assert _to_dict(aeop) == { + "Latitude of natural origin": 1.0, + "Longitude of natural origin": 2.0, + "False easting": 3.0, + "False northing": 4.0, + } + + +def test_geostationary_operation__defaults(): + geop = GeostationarySatelliteOperation(sweep_angle_axis="x", satellite_height=10) + assert geop.name == "unknown" + assert geop.method_name == "Geostationary Satellite (Sweep X)" + assert _to_dict(geop) == { + "Latitude of natural origin": 0.0, + "Longitude of natural origin": 0.0, + "False easting": 0.0, + "False northing": 0.0, + "Satellite height": 10.0, + } + + +def test_geostationary_operation(): + geop = GeostationarySatelliteOperation( + sweep_angle_axis="y", + satellite_height=11, + latitude_natural_origin=1, + longitude_natural_origin=2, + false_easting=3, + false_northing=4, + ) + assert geop.name == "unknown" + assert geop.method_name == "Geostationary Satellite (Sweep Y)" + assert _to_dict(geop) == { + "Latitude of natural origin": 1.0, + "Longitude of natural origin": 2.0, + "False easting": 3.0, + "False northing": 4.0, + "Satellite height": 11.0, + } + + +def test_geostationary_operation__invalid_sweep(): + with pytest.raises(CRSError): + GeostationarySatelliteOperation(sweep_angle_axis="P", satellite_height=10) + + +def test_lambert_azimuthal_equal_area_operation__defaults(): + aeop = LambertAzumuthalEqualAreaOperation() + assert aeop.name == "unknown" + assert aeop.method_name == "Lambert Azimuthal Equal Area" + assert _to_dict(aeop) == { + "Latitude of natural origin": 0.0, + "Longitude of natural origin": 0.0, + "False easting": 0.0, + "False northing": 0.0, + } + + +def test_lambert_azimuthal_equal_area_operation(): + aeop = LambertAzumuthalEqualAreaOperation( + latitude_natural_origin=1, + longitude_natural_origin=2, + false_easting=3, + false_northing=4, + ) + assert aeop.name == "unknown" + assert aeop.method_name == "Lambert Azimuthal Equal Area" + assert _to_dict(aeop) == { + "Latitude of natural origin": 1.0, + "Longitude of natural origin": 2.0, + "False easting": 3.0, + "False northing": 4.0, + } + + +def test_lambert_conformat_conic_2sp_operation__defaults(): + aeaop = LambertConformalConic2SPOperation( + latitude_first_parallel=1, latitude_second_parallel=2 + ) + assert aeaop.name == "unknown" + assert aeaop.method_name == "Lambert Conic Conformal (2SP)" + assert _to_dict(aeaop) == { + "Easting at false origin": 0.0, + "Latitude of 1st standard parallel": 1.0, + "Latitude of 2nd standard parallel": 2.0, + "Latitude of false origin": 0.0, + "Longitude of false origin": 0.0, + "Northing at false origin": 0.0, + } + + +def test_lambert_conformat_conic_2sp_operation(): + aeaop = LambertConformalConic2SPOperation( + latitude_first_parallel=1, + latitude_second_parallel=2, + latitude_false_origin=3, + longitude_false_origin=4, + easting_false_origin=5, + northing_false_origin=6, + ) + assert aeaop.name == "unknown" + assert aeaop.method_name == "Lambert Conic Conformal (2SP)" + assert _to_dict(aeaop) == { + "Easting at false origin": 5.0, + "Latitude of 1st standard parallel": 1.0, + "Latitude of 2nd standard parallel": 2.0, + "Latitude of false origin": 3.0, + "Longitude of false origin": 4.0, + "Northing at false origin": 6.0, + } + + +def test_lambert_conformat_conic_1sp_operation__defaults(): + aeaop = LambertConformalConic1SPOperation() + assert aeaop.name == "unknown" + assert aeaop.method_name == "Lambert Conic Conformal (1SP)" + assert _to_dict(aeaop) == { + "Latitude of natural origin": 0.0, + "Longitude of natural origin": 0.0, + "False easting": 0.0, + "False northing": 0.0, + "Scale factor at natural origin": 1.0, + } + + +def test_lambert_conformat_conic_1sp_operation(): + aeaop = LambertConformalConic1SPOperation( + latitude_natural_origin=1, + longitude_natural_origin=2, + false_easting=3, + false_northing=4, + scale_factor_natural_origin=5, + ) + assert aeaop.name == "unknown" + assert aeaop.method_name == "Lambert Conic Conformal (1SP)" + assert _to_dict(aeaop) == { + "Latitude of natural origin": 1.0, + "Longitude of natural origin": 2.0, + "False easting": 3.0, + "False northing": 4.0, + "Scale factor at natural origin": 5.0, + } + + +def test_lambert_cylindrical_area_operation__defaults(): + lceaop = LambertCylindricalEqualAreaOperation() + assert lceaop.name == "unknown" + assert lceaop.method_name == "Lambert Cylindrical Equal Area" + assert _to_dict(lceaop) == { + "Latitude of 1st standard parallel": 0.0, + "Longitude of natural origin": 0.0, + "False easting": 0.0, + "False northing": 0.0, + } + + +def test_lambert_cylindrical_equal_area_operation(): + lceaop = LambertCylindricalEqualAreaOperation( + latitude_first_parallel=1, + longitude_natural_origin=2, + false_easting=3, + false_northing=4, + ) + assert lceaop.name == "unknown" + assert lceaop.method_name == "Lambert Cylindrical Equal Area" + assert _to_dict(lceaop) == { + "Latitude of 1st standard parallel": 1.0, + "Longitude of natural origin": 2.0, + "False easting": 3.0, + "False northing": 4.0, + } + + +def test_mercator_a_operation__defaults(): + aeaop = MercatorAOperation() + assert aeaop.name == "unknown" + assert aeaop.method_name == "Mercator (variant A)" + assert _to_dict(aeaop) == { + "Latitude of natural origin": 0.0, + "Longitude of natural origin": 0.0, + "False easting": 0.0, + "False northing": 0.0, + "Scale factor at natural origin": 1.0, + } + + +def test_mercator_a_operation(): + aeaop = MercatorAOperation( + latitude_natural_origin=1, + longitude_natural_origin=2, + false_easting=3, + false_northing=4, + scale_factor_natural_origin=5, + ) + assert aeaop.name == "unknown" + assert aeaop.method_name == "Mercator (variant A)" + assert _to_dict(aeaop) == { + "Latitude of natural origin": 1.0, + "Longitude of natural origin": 2.0, + "False easting": 3.0, + "False northing": 4.0, + "Scale factor at natural origin": 5.0, + } + + +def test_mercator_b_operation__defaults(): + lceaop = MercatorBOperation() + assert lceaop.name == "unknown" + assert lceaop.method_name == "Mercator (variant B)" + assert _to_dict(lceaop) == { + "Latitude of 1st standard parallel": 0.0, + "Longitude of natural origin": 0.0, + "False easting": 0.0, + "False northing": 0.0, + } + + +def test_mercator_b_operation(): + lceaop = MercatorBOperation( + latitude_first_parallel=1, + longitude_natural_origin=2, + false_easting=3, + false_northing=4, + ) + assert lceaop.name == "unknown" + assert lceaop.method_name == "Mercator (variant B)" + assert _to_dict(lceaop) == { + "Latitude of 1st standard parallel": 1.0, + "Longitude of natural origin": 2.0, + "False easting": 3.0, + "False northing": 4.0, + } + + +def test_hotline_oblique_mercator_b_operation__defaults(): + hop = HotineObliqueMercatorBOperation( + latitude_projection_centre=0, + longitude_projection_centre=0, + azimuth_initial_line=0, + angle_from_rectified_to_skew_grid=0, + ) + assert hop.name == "unknown" + assert hop.method_name == "Hotine Oblique Mercator (variant B)" + assert _to_dict(hop) == { + "Latitude of projection centre": 0.0, + "Longitude of projection centre": 0.0, + "Azimuth of initial line": 0.0, + "Angle from Rectified to Skew Grid": 0.0, + "Scale factor on initial line": 1.0, + "Easting at projection centre": 0.0, + "Northing at projection centre": 0.0, + } + + +def test_hotline_oblique_mercator_b_operation(): + hop = HotineObliqueMercatorBOperation( + latitude_projection_centre=1, + longitude_projection_centre=2, + azimuth_initial_line=3, + angle_from_rectified_to_skew_grid=4, + scale_factor_on_initial_line=5, + easting_projection_centre=6, + northing_projection_centre=7, + ) + assert hop.name == "unknown" + assert hop.method_name == "Hotine Oblique Mercator (variant B)" + assert _to_dict(hop) == { + "Latitude of projection centre": 1.0, + "Longitude of projection centre": 2.0, + "Azimuth of initial line": 3.0, + "Angle from Rectified to Skew Grid": 4.0, + "Scale factor on initial line": 5.0, + "Easting at projection centre": 6.0, + "Northing at projection centre": 7.0, + } + + +def test_orthographic_operation__defaults(): + aeop = OrthographicOperation() + assert aeop.name == "unknown" + assert aeop.method_name == "Orthographic" + assert _to_dict(aeop) == { + "Latitude of natural origin": 0.0, + "Longitude of natural origin": 0.0, + "False easting": 0.0, + "False northing": 0.0, + } + + +def test_orthographic_operation(): + aeop = OrthographicOperation( + latitude_natural_origin=1, + longitude_natural_origin=2, + false_easting=3, + false_northing=4, + ) + assert aeop.name == "unknown" + assert aeop.method_name == "Orthographic" + assert _to_dict(aeop) == { + "Latitude of natural origin": 1.0, + "Longitude of natural origin": 2.0, + "False easting": 3.0, + "False northing": 4.0, + } + + +def test_polar_stereographic_a_operation__defaults(): + aeaop = PolarStereographicAOperation() + assert aeaop.name == "unknown" + assert aeaop.method_name == "Polar Stereographic (variant A)" + assert _to_dict(aeaop) == { + "Latitude of natural origin": 0.0, + "Longitude of natural origin": 0.0, + "False easting": 0.0, + "False northing": 0.0, + "Scale factor at natural origin": 1.0, + } + + +def test_polar_stereographic_a_operation(): + aeaop = PolarStereographicAOperation( + latitude_natural_origin=1, + longitude_natural_origin=2, + false_easting=3, + false_northing=4, + scale_factor_natural_origin=5, + ) + assert aeaop.name == "unknown" + assert aeaop.method_name == "Polar Stereographic (variant A)" + assert _to_dict(aeaop) == { + "Latitude of natural origin": 1.0, + "Longitude of natural origin": 2.0, + "False easting": 3.0, + "False northing": 4.0, + "Scale factor at natural origin": 5.0, + } + + +def test_polar_stereographic_b_operation__defaults(): + aeop = PolarStereographicBOperation() + assert aeop.name == "unknown" + assert aeop.method_name == "Polar Stereographic (variant B)" + assert _to_dict(aeop) == { + "Latitude of standard parallel": 0.0, + "Longitude of origin": 0.0, + "False easting": 0.0, + "False northing": 0.0, + } + + +def test_polar_stereographic_b_operation(): + aeop = PolarStereographicBOperation( + latitude_standard_parallel=1, + longitude_origin=2, + false_easting=3, + false_northing=4, + ) + assert aeop.name == "unknown" + assert aeop.method_name == "Polar Stereographic (variant B)" + assert _to_dict(aeop) == { + "Latitude of standard parallel": 1.0, + "Longitude of origin": 2.0, + "False easting": 3.0, + "False northing": 4.0, + } + + +def test_sinusoidal_operation__defaults(): + aeop = SinusoidalOperation() + assert aeop.name == "unknown" + assert aeop.method_name == "Sinusoidal" + assert _to_dict(aeop) == { + "Longitude of natural origin": 0.0, + "False easting": 0.0, + "False northing": 0.0, + } + + +def test_sinusoidal_operation(): + aeop = SinusoidalOperation( + longitude_natural_origin=2, false_easting=3, false_northing=4 + ) + assert aeop.name == "unknown" + assert aeop.method_name == "Sinusoidal" + assert _to_dict(aeop) == { + "Longitude of natural origin": 2.0, + "False easting": 3.0, + "False northing": 4.0, + } + + +def test_utm_operation__defaults(): + aeop = UTMOperation(zone=2) + assert aeop.name == "UTM zone 2N" + assert aeop.method_name == "Transverse Mercator" + + +def test_utm_operation(): + aeop = UTMOperation(zone=2, hemisphere="s") + assert aeop.name == "UTM zone 2S" + assert aeop.method_name == "Transverse Mercator" + + +def test_transverse_mercator_operation__defaults(): + aeaop = TransverseMercatorOperation() + assert aeaop.name == "unknown" + assert aeaop.method_name == "Transverse Mercator" + assert _to_dict(aeaop) == { + "Latitude of natural origin": 0.0, + "Longitude of natural origin": 0.0, + "False easting": 0.0, + "False northing": 0.0, + "Scale factor at natural origin": 1.0, + } + + +def test_transverse_mercator_operation(): + aeaop = TransverseMercatorOperation( + latitude_natural_origin=1, + longitude_natural_origin=2, + false_easting=3, + false_northing=4, + scale_factor_natural_origin=5, + ) + assert aeaop.name == "unknown" + assert aeaop.method_name == "Transverse Mercator" + assert _to_dict(aeaop) == { + "Latitude of natural origin": 1.0, + "Longitude of natural origin": 2.0, + "False easting": 3.0, + "False northing": 4.0, + "Scale factor at natural origin": 5.0, + } + + +def test_vertical_perspective_operation__defaults(): + aeaop = VerticalPerspectiveOperation(viewpoint_height=10) + assert aeaop.name == "unknown" + assert aeaop.method_name == "Vertical Perspective" + assert _to_dict(aeaop) == { + "Latitude of topocentric origin": 0.0, + "Longitude of topocentric origin": 0.0, + "Ellipsoidal height of topocentric origin": 0.0, + "Viewpoint height": 10.0, + "False easting": 0.0, + "False northing": 0.0, + } + + +def test_vertical_perspective_operation(): + aeaop = VerticalPerspectiveOperation( + viewpoint_height=10, + latitude_topocentric_origin=1, + longitude_topocentric_origin=2, + false_easting=3, + false_northing=4, + ellipsoidal_height_topocentric_origin=5, + ) + assert aeaop.name == "unknown" + assert aeaop.method_name == "Vertical Perspective" + assert _to_dict(aeaop) == { + "Latitude of topocentric origin": 1.0, + "Longitude of topocentric origin": 2.0, + "Ellipsoidal height of topocentric origin": 5.0, + "Viewpoint height": 10.0, + "False easting": 3.0, + "False northing": 4.0, + } + + +def test_rotated_latitude_longitude_operation__defaults(): + aeaop = RotatedLatitudeLongitude( + grid_north_pole_latitude=1, grid_north_pole_longitude=2 + ) + assert aeaop.name == "unknown" + assert aeaop.method_name == "PROJ ob_tran o_proj=longlat" + assert _to_dict(aeaop) == {"o_lat_p": 1.0, "o_lon_p": 2.0, "lon_0": 0.0} + + +def test_rotated_latitude_longitude_operation(): + aeaop = RotatedLatitudeLongitude( + grid_north_pole_latitude=1, + grid_north_pole_longitude=2, + north_pole_grid_longitude=3, + ) + assert aeaop.name == "unknown" + assert aeaop.method_name == "PROJ ob_tran o_proj=longlat" + assert _to_dict(aeaop) == {"o_lat_p": 1.0, "o_lon_p": 2.0, "lon_0": 3.0} + + +def test_towgs84_transformation(): + transformation = ToWGS84Transformation(GeographicCRS(), 1, 2, 3, 4, 5, 6, 7) + assert transformation.towgs84 == [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0] + assert _to_dict(transformation) == { + "Scale difference": 7.0, + "X-axis rotation": 4.0, + "X-axis translation": 1.0, + "Y-axis rotation": 5.0, + "Y-axis translation": 2.0, + "Z-axis rotation": 6.0, + "Z-axis translation": 3.0, + } + + +def test_towgs84_transformation__defaults(): + transformation = ToWGS84Transformation(GeographicCRS()) + assert transformation.towgs84 == [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + assert _to_dict(transformation) == { + "Scale difference": 0.0, + "X-axis rotation": 0.0, + "X-axis translation": 0.0, + "Y-axis rotation": 0.0, + "Y-axis translation": 0.0, + "Z-axis rotation": 0.0, + "Z-axis translation": 0.0, + } diff --git a/test/crs/test_crs_coordinate_system.py b/test/crs/test_crs_coordinate_system.py new file mode 100644 index 000000000..9ef2ceb4c --- /dev/null +++ b/test/crs/test_crs_coordinate_system.py @@ -0,0 +1,165 @@ +import pytest + +from pyproj.crs.coordinate_system import ( + Cartesian2DCS, + Ellipsoidal2DCS, + Ellipsoidal3DCS, + VerticalCS, +) +from pyproj.crs.enums import ( + Cartesian2DCSAxis, + Ellipsoidal2DCSAxis, + Ellipsoidal3DCSAxis, + VerticalCSAxis, +) + + +@pytest.mark.parametrize( + "axis, direction, unit_name", + [ + ("UP", "up", "metre"), + (VerticalCSAxis.UP, "up", "metre"), + (VerticalCSAxis.UP_US_FT, "up", "US survey foot"), + ("UP_FT", "up", "foot"), + (VerticalCSAxis.DEPTH, "down", "metre"), + (VerticalCSAxis.DEPTH_US_FT, "down", "US survey foot"), + ("DEPTH_FT", "down", "foot"), + (VerticalCSAxis.GRAVITY_HEIGHT_US_FT, "up", "US survey foot"), + ("GRAVITY_HEIGHT_FT", "up", "foot"), + ], +) +def test_vertical_cs(axis, direction, unit_name): + vcs = VerticalCS(axis=axis) + assert len(vcs.axis_list) == 1 + assert vcs.axis_list[0].direction == direction + assert vcs.axis_list[0].unit_name == unit_name + + +@pytest.mark.parametrize( + "axis, name_0, direction_0, name_1, direction_1, unit", + [ + ("EASTING_NORTHING", "Easting", "east", "Northing", "north", "metre"), + ( + Cartesian2DCSAxis.NORTHING_EASTING, + "Northing", + "north", + "Easting", + "east", + "metre", + ), + ("EASTING_NORTHING_FT", "Easting", "east", "Northing", "north", "foot"), + ( + Cartesian2DCSAxis.NORTHING_EASTING_FT, + "Northing", + "north", + "Easting", + "east", + "foot", + ), + ( + "EASTING_NORTHING_US_FT", + "Easting", + "east", + "Northing", + "north", + "US survey foot", + ), + ( + Cartesian2DCSAxis.NORTHING_EASTING_US_FT, + "Northing", + "north", + "Easting", + "east", + "US survey foot", + ), + ( + "NORTH_POLE_EASTING_SOUTH_NORTHING_SOUTH", + "Easting", + "south", + "Northing", + "south", + "metre", + ), + ( + Cartesian2DCSAxis.SOUTH_POLE_EASTING_NORTH_NORTHING_NORTH, + "Easting", + "north", + "Northing", + "north", + "metre", + ), + ("WESTING_SOUTHING", "Easting", "west", "Northing", "south", "metre"), + ], +) +def test_cartesian_2d_cs(axis, name_0, direction_0, name_1, direction_1, unit): + vcs = Cartesian2DCS(axis=axis) + assert len(vcs.axis_list) == 2 + assert vcs.axis_list[0].direction == direction_0 + assert vcs.axis_list[0].name == name_0 + assert vcs.axis_list[0].unit_name == unit + assert vcs.axis_list[1].direction == direction_1 + assert vcs.axis_list[1].name == name_1 + assert vcs.axis_list[1].unit_name == unit + + +@pytest.mark.parametrize( + "axis, name_0, direction_0, name_1, direction_1", + [ + ( + Ellipsoidal2DCSAxis.LONGITUDE_LATITUDE, + "Longitude", + "east", + "Latitude", + "north", + ), + ( + Ellipsoidal2DCSAxis.LATITUDE_LONGITUDE, + "Latitude", + "north", + "Longitude", + "east", + ), + ], +) +def test_ellipsoidal_2d_cs(axis, name_0, direction_0, name_1, direction_1): + vcs = Ellipsoidal2DCS(axis=axis) + assert len(vcs.axis_list) == 2 + assert vcs.axis_list[0].direction == direction_0 + assert vcs.axis_list[0].name == name_0 + assert vcs.axis_list[0].unit_name == "degree" + assert vcs.axis_list[1].direction == direction_1 + assert vcs.axis_list[1].name == name_1 + assert vcs.axis_list[1].unit_name == "degree" + + +@pytest.mark.parametrize( + "axis, name_0, direction_0, name_1, direction_1", + [ + ( + Ellipsoidal3DCSAxis.LONGITUDE_LATITUDE_HEIGHT, + "Longitude", + "east", + "Latitude", + "north", + ), + ( + Ellipsoidal3DCSAxis.LATITUDE_LONGITUDE_HEIGHT, + "Latitude", + "north", + "Longitude", + "east", + ), + ], +) +def test_ellipsoidal_3d_cs(axis, name_0, direction_0, name_1, direction_1): + vcs = Ellipsoidal3DCS(axis=axis) + assert len(vcs.axis_list) == 3 + assert vcs.axis_list[0].direction == direction_0 + assert vcs.axis_list[0].name == name_0 + assert vcs.axis_list[0].unit_name == "degree" + assert vcs.axis_list[1].direction == direction_1 + assert vcs.axis_list[1].name == name_1 + assert vcs.axis_list[1].unit_name == "degree" + assert vcs.axis_list[2].direction == "up" + assert vcs.axis_list[2].name == "Ellipsoidal height" + assert vcs.axis_list[2].unit_name == "metre" diff --git a/test/crs/test_crs_datum.py b/test/crs/test_crs_datum.py new file mode 100644 index 000000000..5d85a0612 --- /dev/null +++ b/test/crs/test_crs_datum.py @@ -0,0 +1,16 @@ +from pyproj.crs.datum import CustomDatum, Ellipsoid, PrimeMeridian + + +def test_custom_datum(): + cd = CustomDatum() + assert cd.ellipsoid.name == "WGS 84" + assert cd.prime_meridian.name == "Greenwich" + + +def test_custom_datum__input(): + cd = CustomDatum( + ellipsoid=Ellipsoid.from_epsg(7001), + prime_meridian=PrimeMeridian.from_name("Lisbon"), + ) + assert cd.ellipsoid.name == "Airy 1830" + assert cd.prime_meridian.name == "Lisbon" diff --git a/test/crs/test_crs_ellipsoid.py b/test/crs/test_crs_ellipsoid.py new file mode 100644 index 000000000..6e7fb24af --- /dev/null +++ b/test/crs/test_crs_ellipsoid.py @@ -0,0 +1,22 @@ +from numpy.testing import assert_almost_equal + +from pyproj.crs.ellipsoid import CustomEllipsoid + + +def test_custom_ellipsoid(): + ce = CustomEllipsoid(semi_major_axis=6378137, inverse_flattening=298.257222101,) + assert ce.name == "undefined" + assert ce.semi_major_metre == 6378137 + assert ce.semi_minor_metre == 6356752.314140356 + assert "semi_minor_axis" not in ce.to_json_dict() + assert_almost_equal(ce.inverse_flattening, 298.257222101) + + +def test_custom_ellipsoid__minor(): + ce = CustomEllipsoid(semi_major_axis=6378137, semi_minor_axis=6356752.314,) + assert ce.name == "undefined" + assert ce.semi_major_metre == 6378137 + assert ce.semi_minor_metre == 6356752.314 + json_dict = ce.to_json_dict() + assert "inverse_flattening" not in json_dict + assert_almost_equal(ce.inverse_flattening, 298.25722014) diff --git a/test/test_crs_json.py b/test/crs/test_crs_json.py similarity index 100% rename from test/test_crs_json.py rename to test/crs/test_crs_json.py diff --git a/test/crs/test_crs_maker.py b/test/crs/test_crs_maker.py new file mode 100644 index 000000000..05efd9c11 --- /dev/null +++ b/test/crs/test_crs_maker.py @@ -0,0 +1,159 @@ +from distutils.version import LooseVersion + +import pytest + +from pyproj import proj_version_str +from pyproj.crs import BoundCRS, CompoundCRS, GeographicCRS, ProjectedCRS, VerticalCRS +from pyproj.crs.coordinate_operation import ( + AlbersEqualAreaOperation, + LambertConformalConic2SPOperation, + ToWGS84Transformation, + TransverseMercatorOperation, + UTMOperation, +) +from pyproj.crs.coordinate_system import Cartesian2DCS, Ellipsoidal3DCS, VerticalCS +from pyproj.crs.datum import CustomDatum +from pyproj.crs.enums import VerticalCSAxis + + +def test_make_projected_crs(): + aeaop = AlbersEqualAreaOperation(0, 0) + pc = ProjectedCRS(coordinate_operation=aeaop, name="Albers") + assert pc.name == "Albers" + assert pc.type_name == "Projected CRS" + assert pc.coordinate_operation == aeaop + + +def test_make_geographic_crs(): + gc = GeographicCRS(name="WGS 84") + assert gc.name == "WGS 84" + assert gc.type_name == "Geographic 2D CRS" + assert gc.to_authority() == ("OGC", "CRS84") + + +def test_make_geographic_3d_crs(): + gcrs = GeographicCRS(ellipsoidal_cs=Ellipsoidal3DCS()) + assert gcrs.type_name == "Geographic 3D CRS" + assert gcrs.to_authority() == ("IGNF", "WGS84GEODD") + + +def test_vertical_crs(): + vc = VerticalCRS( + name="NAVD88 height", + datum="North American Vertical Datum 1988", + geoid_model="GEOID12B", + ) + assert vc.name == "NAVD88 height" + assert vc.type_name == "Vertical CRS" + assert vc.coordinate_system == VerticalCS() + if LooseVersion(proj_version_str) >= LooseVersion("6.3.0"): + assert vc.to_json_dict()["geoid_model"]["name"] == "GEOID12B" + + +@pytest.mark.parametrize( + "axis", + [ + VerticalCSAxis.UP, + VerticalCSAxis.UP_FT, + VerticalCSAxis.DEPTH, + VerticalCSAxis.DEPTH_FT, + VerticalCSAxis.GRAVITY_HEIGHT_FT, + ], +) +def test_vertical_crs__chance_cs_axis(axis): + vc = VerticalCRS( + name="NAVD88 height", + datum="North American Vertical Datum 1988", + vertical_cs=VerticalCS(axis=axis), + ) + assert vc.name == "NAVD88 height" + assert vc.type_name == "Vertical CRS" + assert vc.coordinate_system == VerticalCS(axis=axis) + + +def test_compund_crs(): + vertcrs = VerticalCRS( + name="NAVD88 height", + datum="North American Vertical Datum 1988", + vertical_cs=VerticalCS(), + geoid_model="GEOID12B", + ) + projcrs = ProjectedCRS( + name="NAD83 / Pennsylvania South", + coordinate_operation=LambertConformalConic2SPOperation( + latitude_false_origin=39.3333333333333, + longitude_false_origin=-77.75, + latitude_first_parallel=40.9666666666667, + latitude_second_parallel=39.9333333333333, + easting_false_origin=600000, + northing_false_origin=0, + ), + geodetic_crs=GeographicCRS(datum="North American Datum 1983"), + cartesian_cs=Cartesian2DCS(), + ) + compcrs = CompoundCRS( + name="NAD83 / Pennsylvania South + NAVD88 height", components=[projcrs, vertcrs] + ) + assert compcrs.name == "NAD83 / Pennsylvania South + NAVD88 height" + assert compcrs.type_name == "Compound CRS" + assert compcrs.sub_crs_list[0].type_name == "Projected CRS" + assert compcrs.sub_crs_list[1].type_name == "Vertical CRS" + + +def test_bound_crs(): + proj_crs = ProjectedCRS(coordinate_operation=UTMOperation(12)) + bound_crs = BoundCRS( + source_crs=proj_crs, + target_crs="WGS 84", + transformation=ToWGS84Transformation( + proj_crs.geodetic_crs, 1, 2, 3, 4, 5, 6, 7 + ), + ) + assert bound_crs.type_name == "Bound CRS" + assert bound_crs.source_crs.coordinate_operation.name == "UTM zone 12N" + assert bound_crs.coordinate_operation.towgs84 == [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0] + assert bound_crs.target_crs.name == "WGS 84" + + +def test_bound_crs__example(): + proj_crs = ProjectedCRS( + coordinate_operation=TransverseMercatorOperation( + latitude_natural_origin=0, + longitude_natural_origin=15, + false_easting=2520000, + false_northing=0, + scale_factor_natural_origin=0.9996, + ), + geodetic_crs=GeographicCRS( + datum=CustomDatum( + ellipsoid={ + "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", + "type": "Ellipsoid", + "name": "International 1909 (Hayford)", + "semi_major_axis": 6378388, + "inverse_flattening": 297, + } + ) + ), + ) + bound_crs = BoundCRS( + source_crs=proj_crs, + target_crs="WGS 84", + transformation=ToWGS84Transformation( + proj_crs.geodetic_crs, -122.74, -34.27, -22.83, -1.884, -3.4, -3.03, -15.62 + ), + ) + with pytest.warns(UserWarning): + assert bound_crs.to_dict() == { + "ellps": "intl", + "k": 0.9996, + "lat_0": 0, + "lon_0": 15, + "no_defs": None, + "proj": "tmerc", + "towgs84": [-122.74, -34.27, -22.83, -1.884, -3.4, -3.03, -15.62], + "type": "crs", + "units": "m", + "x_0": 2520000, + "y_0": 0, + }