Source code for pyproj.crs.crs

"""
This module interfaces with PROJ to produce a pythonic interface
to the coordinate reference system (CRS) information.
"""
import json
import re
import threading
import warnings
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union

from pyproj._crs import (  # noqa
    _CRS,
    AreaOfUse,
    Axis,
    CoordinateOperation,
    CoordinateSystem,
    Datum,
    Ellipsoid,
    PrimeMeridian,
    _load_proj_json,
    is_proj,
    is_wkt,
)
from pyproj.crs._cf1x8 import (
    _GEOGRAPHIC_GRID_MAPPING_NAME_MAP,
    _GRID_MAPPING_NAME_MAP,
    _INVERSE_GEOGRAPHIC_GRID_MAPPING_NAME_MAP,
    _INVERSE_GRID_MAPPING_NAME_MAP,
    _horizontal_datum_from_params,
    _try_list_if_string,
)
from pyproj.crs.coordinate_operation import ToWGS84Transformation
from pyproj.crs.coordinate_system import Cartesian2DCS, Ellipsoidal2DCS, VerticalCS
from pyproj.enums import ProjVersion, WktVersion
from pyproj.exceptions import CRSError
from pyproj.geod import Geod


class CRSLocal(threading.local):
    """
    Threading local instance for cython CRS class.

    For more details, see:
    https://github.com/pyproj4/pyproj/issues/782
    """

    def __init__(self):
        self.crs = None  # Initialises in each thread


def _prepare_from_dict(projparams: dict, allow_json: bool = True) -> str:
    # check if it is a PROJ JSON dict
    if "proj" not in projparams and "init" not in projparams and allow_json:
        return json.dumps(projparams)
    # convert a dict to a proj string.
    pjargs = []
    for key, value in projparams.items():
        # the towgs84 as list
        if isinstance(value, (list, tuple)):
            value = ",".join([str(val) for val in value])
        # issue 183 (+ no_rot)
        if value is None or str(value) == "True":
            pjargs.append(f"+{key}")
        elif str(value) == "False":
            pass
        else:
            pjargs.append(f"+{key}={value}")
    return _prepare_from_string(" ".join(pjargs))


def _prepare_from_string(in_crs_string: str) -> str:
    if not in_crs_string:
        raise CRSError(f"CRS is empty or invalid: {in_crs_string!r}")
    elif "{" in in_crs_string:
        # may be json, try to decode it
        try:
            crs_dict = json.loads(in_crs_string, strict=False)
        except ValueError:
            raise CRSError("CRS appears to be JSON but is not valid")

        if not crs_dict:
            raise CRSError("CRS is empty JSON")
        return _prepare_from_dict(crs_dict)
    elif is_proj(in_crs_string):
        in_crs_string = re.sub(r"[\s+]?=[\s+]?", "=", in_crs_string.lstrip())
        # make sure the projection starts with +proj or +init
        starting_params = ("+init", "+proj", "init", "proj")
        if not in_crs_string.startswith(starting_params):
            kvpairs = []  # type: List[str]
            first_item_inserted = False
            for kvpair in in_crs_string.split():
                if not first_item_inserted and (kvpair.startswith(starting_params)):
                    kvpairs.insert(0, kvpair)
                    first_item_inserted = True
                else:
                    kvpairs.append(kvpair)
            in_crs_string = " ".join(kvpairs)

        # make sure it is the CRS type
        if "type=crs" not in in_crs_string:
            if "+" in in_crs_string:
                in_crs_string += " +type=crs"
            else:
                in_crs_string += " type=crs"

        # look for EPSG, replace with epsg (EPSG only works
        # on case-insensitive filesystems).
        in_crs_string = in_crs_string.replace("+init=EPSG", "+init=epsg").strip()
        if in_crs_string.startswith(("+init", "init")):
            warnings.warn(
                "'+init=<authority>:<code>' syntax is deprecated. "
                "'<authority>:<code>' is the preferred initialization method. "
                "When making the change, be mindful of axis order changes: "
                "https://pyproj4.github.io/pyproj/stable/gotchas.html"
                "#axis-order-changes-in-proj-6",
                FutureWarning,
                stacklevel=2,
            )
    return in_crs_string


def _prepare_from_authority(auth_name: str, auth_code: Union[str, int]):
    return f"{auth_name}:{auth_code}"


def _prepare_from_epsg(auth_code: Union[str, int]):
    return _prepare_from_authority("epsg", auth_code)


[docs]class CRS: """ A pythonic Coordinate Reference System manager. .. versionadded:: 2.0.0 The functionality is based on other fantastic projects: * `rasterio <https://github.com/mapbox/rasterio/blob/c13f0943b95c0eaa36ff3f620bd91107aa67b381/rasterio/_crs.pyx>`_ # noqa: E501 * `opendatacube <https://github.com/opendatacube/datacube-core/blob/83bae20d2a2469a6417097168fd4ede37fd2abe5/datacube/utils/geometry/_base.py>`_ # noqa: E501 Attributes ---------- srs: str The string form of the user input used to create the CRS. """
[docs] def __init__(self, projparams: Any = None, **kwargs) -> None: """ Initialize a CRS class instance with: - PROJ string - Dictionary of PROJ parameters - PROJ keyword arguments for parameters - JSON string with PROJ parameters - CRS WKT string - An authority string [i.e. 'epsg:4326'] - An EPSG integer code [i.e. 4326] - A tuple of ("auth_name": "auth_code") [i.e ('epsg', '4326')] - An object with a `to_wkt` method. - A :class:`pyproj.crs.CRS` class Example usage: >>> from pyproj import CRS >>> crs_utm = CRS.from_user_input(26915) >>> crs_utm <Projected CRS: EPSG:26915> Name: NAD83 / UTM zone 15N Axis Info [cartesian]: - E[east]: Easting (metre) - N[north]: Northing (metre) Area of Use: - name: North America - 96°W to 90°W and NAD83 by country - bounds: (-96.0, 25.61, -90.0, 84.0) Coordinate Operation: - name: UTM zone 15N - method: Transverse Mercator Datum: North American Datum 1983 - Ellipsoid: GRS 1980 - Prime Meridian: Greenwich <BLANKLINE> >>> crs_utm.area_of_use.bounds (-96.0, 25.61, -90.0, 84.0) >>> crs_utm.ellipsoid ELLIPSOID["GRS 1980",6378137,298.257222101, LENGTHUNIT["metre",1], ID["EPSG",7019]] >>> crs_utm.ellipsoid.inverse_flattening 298.257222101 >>> crs_utm.ellipsoid.semi_major_metre 6378137.0 >>> crs_utm.ellipsoid.semi_minor_metre 6356752.314140356 >>> crs_utm.prime_meridian PRIMEM["Greenwich",0, ANGLEUNIT["degree",0.0174532925199433], ID["EPSG",8901]] >>> crs_utm.prime_meridian.unit_name 'degree' >>> crs_utm.prime_meridian.unit_conversion_factor 0.017453292519943295 >>> crs_utm.prime_meridian.longitude 0.0 >>> crs_utm.datum DATUM["North American Datum 1983", ELLIPSOID["GRS 1980",6378137,298.257222101, LENGTHUNIT["metre",1]], ID["EPSG",6269]] >>> crs_utm.coordinate_system CS[Cartesian,2], AXIS["(E)",east, ORDER[1], LENGTHUNIT["metre",1, ID["EPSG",9001]]], AXIS["(N)",north, ORDER[2], LENGTHUNIT["metre",1, ID["EPSG",9001]]] >>> print(crs_utm.coordinate_operation.to_wkt(pretty=True)) CONVERSION["UTM zone 15N", METHOD["Transverse Mercator", ID["EPSG",9807]], PARAMETER["Latitude of natural origin",0, ANGLEUNIT["degree",0.0174532925199433], ID["EPSG",8801]], PARAMETER["Longitude of natural origin",-93, ANGLEUNIT["degree",0.0174532925199433], ID["EPSG",8802]], PARAMETER["Scale factor at natural origin",0.9996, SCALEUNIT["unity",1], ID["EPSG",8805]], PARAMETER["False easting",500000, LENGTHUNIT["metre",1], ID["EPSG",8806]], PARAMETER["False northing",0, LENGTHUNIT["metre",1], ID["EPSG",8807]], ID["EPSG",16015]] >>> crs = CRS(proj='utm', zone=10, ellps='WGS84') >>> print(crs.to_wkt(pretty=True)) PROJCRS["unknown", BASEGEOGCRS["unknown", DATUM["Unknown based on WGS84 ellipsoid", ELLIPSOID["WGS 84",6378137,298.257223563, LENGTHUNIT["metre",1], ID["EPSG",7030]]], PRIMEM["Greenwich",0, ANGLEUNIT["degree",0.0174532925199433], ID["EPSG",8901]]], CONVERSION["UTM zone 10N", METHOD["Transverse Mercator", ID["EPSG",9807]], PARAMETER["Latitude of natural origin",0, ANGLEUNIT["degree",0.0174532925199433], ID["EPSG",8801]], PARAMETER["Longitude of natural origin",-123, ANGLEUNIT["degree",0.0174532925199433], ID["EPSG",8802]], PARAMETER["Scale factor at natural origin",0.9996, SCALEUNIT["unity",1], ID["EPSG",8805]], PARAMETER["False easting",500000, LENGTHUNIT["metre",1], ID["EPSG",8806]], PARAMETER["False northing",0, LENGTHUNIT["metre",1], ID["EPSG",8807]], ID["EPSG",16010]], CS[Cartesian,2], AXIS["(E)",east, ORDER[1], LENGTHUNIT["metre",1, ID["EPSG",9001]]], AXIS["(N)",north, ORDER[2], LENGTHUNIT["metre",1, ID["EPSG",9001]]]] >>> geod = crs.get_geod() >>> f"+a={geod.a:.0f} +f={geod.f:.8f}" '+a=6378137 +f=0.00335281' >>> crs.is_projected True >>> crs.is_geographic False """ projstring = "" if projparams: if isinstance(projparams, _CRS): projstring = projparams.srs elif isinstance(projparams, str): projstring = _prepare_from_string(projparams) elif isinstance(projparams, dict): projstring = _prepare_from_dict(projparams) elif isinstance(projparams, int): projstring = _prepare_from_epsg(projparams) elif isinstance(projparams, (list, tuple)) and len(projparams) == 2: projstring = _prepare_from_authority(*projparams) elif hasattr(projparams, "to_wkt"): projstring = projparams.to_wkt() # type: ignore else: raise CRSError(f"Invalid CRS input: {projparams!r}") if kwargs: projkwargs = _prepare_from_dict(kwargs, allow_json=False) projstring = _prepare_from_string(" ".join((projstring, projkwargs))) self.srs = projstring self._local = CRSLocal() if isinstance(projparams, _CRS): self._local.crs = projparams else: self._local.crs = _CRS(self.srs)
@property def _crs(self): """ Retrieve the Cython based _CRS object for this thread. """ if self._local.crs is None: self._local.crs = _CRS(self.srs) return self._local.crs
[docs] @staticmethod def from_authority(auth_name: str, code: Union[str, int]) -> "CRS": """ .. versionadded:: 2.2.0 Make a CRS from an authority name and authority code Parameters ---------- auth_name: str The name of the authority. code : int or str The code used by the authority. Returns ------- CRS """ return CRS(_prepare_from_authority(auth_name, code))
[docs] @staticmethod def from_epsg(code: Union[str, int]) -> "CRS": """Make a CRS from an EPSG code Parameters ---------- code : int or str An EPSG code. Returns ------- CRS """ return CRS(_prepare_from_epsg(code))
[docs] @staticmethod def from_proj4(in_proj_string: str) -> "CRS": """ .. versionadded:: 2.2.0 Make a CRS from a PROJ string Parameters ---------- in_proj_string : str A PROJ string. Returns ------- CRS """ if not is_proj(in_proj_string): raise CRSError(f"Invalid PROJ string: {in_proj_string}") return CRS(_prepare_from_string(in_proj_string))
[docs] @staticmethod def from_wkt(in_wkt_string: str) -> "CRS": """ .. versionadded:: 2.2.0 Make a CRS from a WKT string Parameters ---------- in_wkt_string : str A WKT string. Returns ------- CRS """ if not is_wkt(in_wkt_string): raise CRSError(f"Invalid WKT string: {in_wkt_string}") return CRS(_prepare_from_string(in_wkt_string))
[docs] @staticmethod def from_string(in_crs_string: str) -> "CRS": """ Make a CRS from: Initialize a CRS class instance with: - PROJ string - JSON string with PROJ parameters - CRS WKT string - An authority string [i.e. 'epsg:4326'] Parameters ---------- in_crs_string : str An EPSG, PROJ, or WKT string. Returns ------- CRS """ return CRS(_prepare_from_string(in_crs_string))
[docs] def to_string(self) -> str: """ .. versionadded:: 2.2.0 Convert the CRS to a string. It attempts to convert it to the authority string. Otherwise, it uses the string format of the user input to create the CRS. Returns ------- str """ auth_info = self.to_authority(min_confidence=100) if auth_info: return ":".join(auth_info) return self.srs
[docs] @staticmethod def from_user_input(value: Any, **kwargs) -> "CRS": """ Initialize a CRS class instance with: - PROJ string - Dictionary of PROJ parameters - PROJ keyword arguments for parameters - JSON string with PROJ parameters - CRS WKT string - An authority string [i.e. 'epsg:4326'] - An EPSG integer code [i.e. 4326] - A tuple of ("auth_name": "auth_code") [i.e ('epsg', '4326')] - An object with a `to_wkt` method. - A :class:`pyproj.crs.CRS` class Parameters ---------- value : obj A Python int, dict, or str. Returns ------- CRS """ if isinstance(value, CRS): return value return CRS(value, **kwargs)
[docs] def get_geod(self) -> Optional[Geod]: """ Returns ------- pyproj.geod.Geod: Geod object based on the ellipsoid. """ if self.ellipsoid is None: return None return Geod( a=self.ellipsoid.semi_major_metre, rf=self.ellipsoid.inverse_flattening, b=self.ellipsoid.semi_minor_metre, )
[docs] @staticmethod def from_dict(proj_dict: dict) -> "CRS": """ .. versionadded:: 2.2.0 Make a CRS from a dictionary of PROJ parameters. Parameters ---------- proj_dict : str PROJ params in dict format. Returns ------- CRS """ return CRS(_prepare_from_dict(proj_dict))
[docs] @staticmethod def from_json(crs_json: str) -> "CRS": """ .. versionadded:: 2.4.0 Create CRS from a CRS JSON string. Parameters ---------- crs_json: str CRS JSON string. Returns ------- CRS """ return CRS.from_json_dict(_load_proj_json(crs_json))
[docs] @staticmethod def from_json_dict(crs_dict: dict) -> "CRS": """ .. versionadded:: 2.4.0 Create CRS from a JSON dictionary. Parameters ---------- crs_dict: dict CRS dictionary. Returns ------- CRS """ return CRS(json.dumps(crs_dict))
[docs] def to_dict(self) -> dict: """ .. versionadded:: 2.2.0 Converts the CRS to dictionary of PROJ parameters. .. warning:: You will likely lose important projection information when converting to a PROJ string from another format. See: https://proj.org/faq.html#what-is-the-best-format-for-describing-coordinate-reference-systems # noqa: E501 Returns ------- dict: PROJ params in dict format. """ def parse(val): if val.lower() == "true": return True elif val.lower() == "false": return False try: return int(val) except ValueError: pass try: return float(val) except ValueError: pass return _try_list_if_string(val) proj_string = self.to_proj4() if proj_string is None: return {} items = map( lambda kv: len(kv) == 2 and (kv[0], parse(kv[1])) or (kv[0], None), (part.lstrip("+").split("=", 1) for part in proj_string.strip().split()), ) return {key: value for key, value in items if value is not False}
[docs] def to_cf( self, wkt_version: Union[WktVersion, str] = WktVersion.WKT2_2019, errcheck: bool = False, ) -> dict: """ .. versionadded:: 2.2.0 This converts a :obj:`pyproj.crs.CRS` object to a Climate and Forecast (CF) Grid Mapping Version 1.8 dict. :ref:`build_crs_cf` Parameters ---------- wkt_version: str or pyproj.enums.WktVersion Version of WKT supported by CRS.to_wkt. Default is :attr:`pyproj.enums.WktVersion.WKT2_2019`. errcheck: bool, optional If True, will warn when parameters are ignored. Defaults to False. Returns ------- dict: CF-1.8 version of the projection. """ cf_dict = {"crs_wkt": self.to_wkt(wkt_version)} # type: Dict[str, Any] # handle bound CRS if ( self.is_bound and self.coordinate_operation and self.coordinate_operation.towgs84 ): sub_cf = self.source_crs.to_cf(errcheck=errcheck) # type: ignore sub_cf.pop("crs_wkt") cf_dict.update(sub_cf) cf_dict["towgs84"] = self.coordinate_operation.towgs84 return cf_dict # handle compound CRS elif self.sub_crs_list: for sub_crs in self.sub_crs_list: sub_cf = sub_crs.to_cf(errcheck=errcheck) sub_cf.pop("crs_wkt") cf_dict.update(sub_cf) return cf_dict # handle vertical CRS elif self.is_vertical: vert_json = self.to_json_dict() if "geoid_model" in vert_json: cf_dict["geoid_name"] = vert_json["geoid_model"]["name"] if self.datum: cf_dict["geopotential_datum_name"] = self.datum.name return cf_dict # write out datum parameters if self.ellipsoid: cf_dict.update( semi_major_axis=self.ellipsoid.semi_major_metre, semi_minor_axis=self.ellipsoid.semi_minor_metre, inverse_flattening=self.ellipsoid.inverse_flattening, ) cf_dict["reference_ellipsoid_name"] = self.ellipsoid.name if self.prime_meridian: cf_dict["longitude_of_prime_meridian"] = self.prime_meridian.longitude cf_dict["prime_meridian_name"] = self.prime_meridian.name # handle geographic CRS if self.geodetic_crs: cf_dict["geographic_crs_name"] = self.geodetic_crs.name if self.is_geographic: if self.coordinate_operation: cf_dict.update( _INVERSE_GEOGRAPHIC_GRID_MAPPING_NAME_MAP[ self.coordinate_operation.method_name.lower() ](self.coordinate_operation) ) if self.datum: cf_dict["horizontal_datum_name"] = self.datum.name else: cf_dict["grid_mapping_name"] = "latitude_longitude" return cf_dict # handle projected CRS if self.is_projected and self.datum: cf_dict["horizontal_datum_name"] = self.datum.name coordinate_operation = None if not self.is_bound and self.is_projected: coordinate_operation = self.coordinate_operation cf_dict["projected_crs_name"] = self.name coordinate_operation_name = ( None if not coordinate_operation else coordinate_operation.method_name.lower().replace(" ", "_") ) if coordinate_operation_name not in _INVERSE_GRID_MAPPING_NAME_MAP: if errcheck: if coordinate_operation: warnings.warn( "Unsupported coordinate operation: " f"{coordinate_operation.method_name}" ) else: warnings.warn("Coordinate operation not found.") return {"crs_wkt": self.to_wkt(wkt_version)} cf_dict.update( _INVERSE_GRID_MAPPING_NAME_MAP[coordinate_operation_name]( coordinate_operation ) ) return cf_dict
[docs] @staticmethod def from_cf( in_cf: dict, ellipsoidal_cs: Any = None, cartesian_cs: Any = None, vertical_cs: Any = None, errcheck=False, ) -> "CRS": """ .. versionadded:: 2.2.0 .. versionadded:: 3.0.0 ellipsoidal_cs, cartesian_cs, vertical_cs This converts a Climate and Forecast (CF) Grid Mapping Version 1.8 dict to a :obj:`pyproj.crs.CRS` object. :ref:`build_crs_cf` Parameters ---------- in_cf: dict CF version of the projection. 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`. 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`. 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` errcheck: bool, optional This parameter is for backwards compatibility with the old version. It currently does nothing when True or False. Returns ------- CRS """ unknown_names = ("unknown", "undefined") if "crs_wkt" in in_cf: return CRS(in_cf["crs_wkt"]) elif "spatial_ref" in in_cf: # for previous supported WKT key return CRS(in_cf["spatial_ref"]) grid_mapping_name = in_cf.get("grid_mapping_name") if grid_mapping_name is None: raise CRSError("CF projection parameters missing 'grid_mapping_name'") # build datum if possible datum = _horizontal_datum_from_params(in_cf) # build geographic CRS try: geographic_conversion_method = _GEOGRAPHIC_GRID_MAPPING_NAME_MAP[ grid_mapping_name ] # type: Optional[Callable] except KeyError: geographic_conversion_method = None geographic_crs_name = in_cf.get("geographic_crs_name") if datum: geographic_crs = GeographicCRS( name=geographic_crs_name or "undefined", datum=datum, ellipsoidal_cs=ellipsoidal_cs, ) # type: CRS elif geographic_crs_name and geographic_crs_name not in unknown_names: geographic_crs = CRS(geographic_crs_name) if ellipsoidal_cs is not None: geographic_crs_json = geographic_crs.to_json_dict() geographic_crs_json[ "coordinate_system" ] = CoordinateSystem.from_user_input(ellipsoidal_cs).to_json_dict() geographic_crs = CRS(geographic_crs_json) else: geographic_crs = GeographicCRS(ellipsoidal_cs=ellipsoidal_cs) if grid_mapping_name == "latitude_longitude": return geographic_crs if geographic_conversion_method is not None: return DerivedGeographicCRS( base_crs=geographic_crs, conversion=geographic_conversion_method(in_cf), ellipsoidal_cs=ellipsoidal_cs, ) # build projected CRS try: conversion_method = _GRID_MAPPING_NAME_MAP[grid_mapping_name] except KeyError: raise CRSError(f"Unsupported grid mapping name: {grid_mapping_name}") projected_crs = ProjectedCRS( name=in_cf.get("projected_crs_name", "undefined"), conversion=conversion_method(in_cf), geodetic_crs=geographic_crs, cartesian_cs=cartesian_cs, ) # build bound CRS if exists bound_crs = None if "towgs84" in in_cf: bound_crs = BoundCRS( source_crs=projected_crs, target_crs="WGS 84", transformation=ToWGS84Transformation( projected_crs.geodetic_crs, *_try_list_if_string(in_cf["towgs84"]) ), ) if "geopotential_datum_name" not in in_cf: return bound_crs or projected_crs # build Vertical CRS vertical_crs = VerticalCRS( name="undefined", datum=in_cf["geopotential_datum_name"], geoid_model=in_cf.get("geoid_name"), vertical_cs=vertical_cs, ) # build compound CRS return CompoundCRS( name="undefined", components=[bound_crs or projected_crs, vertical_crs] )
[docs] def cs_to_cf(self) -> List[dict]: """ .. versionadded:: 3.0.0 This converts all coordinate systems (cs) in the CRS to a list of Climate and Forecast (CF) Version 1.8 dicts. :ref:`build_crs_cf` Returns ------- List[dict]: CF-1.8 version of the coordinate systems. """ cf_axis_list = [] def rotated_pole(crs): try: return ( crs.coordinate_operation and crs.coordinate_operation.method_name.lower() in _INVERSE_GEOGRAPHIC_GRID_MAPPING_NAME_MAP ) except KeyError: return False if self.type_name == "Temporal CRS" and self.datum: datum_json = self.datum.to_json_dict() origin = datum_json.get("time_origin", "1875-05-20").strip().rstrip("zZ") if len(origin) == 4: origin = f"{origin}-01-01" axis = self.axis_info[0] cf_temporal_axis = { "standard_name": "time", "long_name": "time", "calendar": ( datum_json.get("calendar", "proleptic_gregorian") .lower() .replace(" ", "_") ), "axis": "T", } unit_name = axis.unit_name.lower().replace("calendar", "").strip() # no units for TemporalDateTime if unit_name: cf_temporal_axis["units"] = f"{unit_name} since {origin}" cf_axis_list.append(cf_temporal_axis) if self.coordinate_system: cf_axis_list.extend( self.coordinate_system.to_cf(rotated_pole=rotated_pole(self)) ) elif self.is_bound and self.source_crs and self.source_crs.coordinate_system: cf_axis_list.extend( self.source_crs.coordinate_system.to_cf( rotated_pole=rotated_pole(self.source_crs) ) ) else: for sub_crs in self.sub_crs_list: cf_axis_list.extend(sub_crs.cs_to_cf()) return cf_axis_list
[docs] def is_exact_same(self, other: Any, ignore_axis_order: bool = False) -> bool: """ Check if the CRS objects are the exact same. Parameters ---------- other: Any Check if the other CRS is the exact same to this object. If the other object is not a CRS, it will try to create one. On Failure, it will return False. Returns ------- bool """ try: other = CRS.from_user_input(other) except CRSError: return False return self._crs.is_exact_same(other._crs)
[docs] def equals(self, other: Any, ignore_axis_order: bool = False) -> bool: """ .. versionadded:: 2.5.0 Check if the CRS objects are equivalent. Parameters ---------- other: Any Check if the other object is equivalent to this object. If the other object is not a CRS, it will try to create one. On Failure, it will return False. ignore_axis_order: bool, optional If True, it will compare the CRS class and ignore the axis order. Default is False. Returns ------- bool """ try: other = CRS.from_user_input(other) except CRSError: return False return self._crs.equals(other._crs, ignore_axis_order=ignore_axis_order)
@property def geodetic_crs(self) -> Optional["CRS"]: """ .. versionadded:: 2.2.0 Returns ------- CRS: The the geodeticCRS / geographicCRS from the CRS. """ return None if self._crs.geodetic_crs is None else CRS(self._crs.geodetic_crs) @property def source_crs(self) -> Optional["CRS"]: """ The the base CRS of a BoundCRS or a DerivedCRS/ProjectedCRS, or the source CRS of a CoordinateOperation. Returns ------- CRS """ return None if self._crs.source_crs is None else CRS(self._crs.source_crs) @property def target_crs(self) -> Optional["CRS"]: """ .. versionadded:: 2.2.0 Returns ------- CRS: The hub CRS of a BoundCRS or the target CRS of a CoordinateOperation. """ return None if self._crs.target_crs is None else CRS(self._crs.target_crs) @property def sub_crs_list(self) -> List["CRS"]: """ If the CRS is a compound CRS, it will return a list of sub CRS objects. Returns ------- List[CRS] """ return [CRS(sub_crs) for sub_crs in self._crs.sub_crs_list] @property def utm_zone(self) -> Optional[str]: """ .. versionadded:: 2.6.0 Finds the UTM zone in a Projected CRS, Bound CRS, or Compound CRS Returns ------- Optional[str]: The UTM zone number and letter if applicable. """ if self.is_bound and self.source_crs: return self.source_crs.utm_zone elif self.sub_crs_list: for sub_crs in self.sub_crs_list: if sub_crs.utm_zone: return sub_crs.utm_zone elif ( self.coordinate_operation and "UTM ZONE" in self.coordinate_operation.name.upper() ): return self.coordinate_operation.name.upper().split("UTM ZONE ")[-1] return None @property def name(self) -> str: """ Returns ------- str: The name of the CRS (from `proj_get_name <https://proj.org/ development/reference/functions.html#_CPPv313proj_get_namePK2PJ>`_). """ return self._crs.name @property def type_name(self) -> str: """ Returns ------- str: The name of the type of the CRS object. """ return self._crs.type_name @property def axis_info(self) -> List[Axis]: """ Retrieves all relevant axis information in the CRS. If it is a Bound CRS, it gets the axis list from the Source CRS. If it is a Compound CRS, it gets the axis list from the Sub CRS list. Returns ------- List[Axis]: The list of axis information. """ return self._crs.axis_info @property def area_of_use(self) -> Optional[AreaOfUse]: """ Returns ------- AreaOfUse: The area of use object with associated attributes. """ return self._crs.area_of_use @property def ellipsoid(self) -> Optional[Ellipsoid]: """ .. versionadded:: 2.2.0 Returns ------- Ellipsoid: The ellipsoid object with associated attributes. """ return self._crs.ellipsoid @property def prime_meridian(self) -> Optional[PrimeMeridian]: """ .. versionadded:: 2.2.0 Returns ------- PrimeMeridian: The prime meridian object with associated attributes. """ return self._crs.prime_meridian @property def datum(self) -> Optional[Datum]: """ .. versionadded:: 2.2.0 Returns ------- Datum """ return self._crs.datum @property def coordinate_system(self) -> Optional[CoordinateSystem]: """ .. versionadded:: 2.2.0 Returns ------- CoordinateSystem """ return self._crs.coordinate_system @property def coordinate_operation(self) -> Optional[CoordinateOperation]: """ .. versionadded:: 2.2.0 Returns ------- CoordinateOperation """ return self._crs.coordinate_operation @property def remarks(self) -> str: """ .. versionadded:: 2.4.0 Returns ------- str: Remarks about object. """ return self._crs.remarks @property def scope(self) -> str: """ .. versionadded:: 2.4.0 Returns ------- str: Scope of object. """ return self._crs.scope
[docs] def to_wkt( self, version: Union[WktVersion, str] = WktVersion.WKT2_2019, pretty: bool = False, ) -> str: """ Convert the projection to a WKT string. Version options: - WKT2_2015 - WKT2_2015_SIMPLIFIED - WKT2_2019 - WKT2_2019_SIMPLIFIED - WKT1_GDAL - WKT1_ESRI Parameters ---------- version: pyproj.enums.WktVersion The version of the WKT output. Default is :attr:`pyproj.enums.WktVersion.WKT2_2019`. pretty: bool If True, it will set the output to be a multiline string. Defaults to False. Returns ------- str """ return self._crs.to_wkt(version=version, pretty=pretty)
[docs] def to_json(self, pretty: bool = False, indentation: int = 2) -> str: """ .. versionadded:: 2.4.0 Convert the object to a JSON string. Parameters ---------- pretty: bool If True, it will set the output to be a multiline string. Defaults to False. indentation: int If pretty is True, it will set the width of the indentation. Default is 2. Returns ------- str """ return self._crs.to_json(pretty=pretty, indentation=indentation)
[docs] def to_json_dict(self) -> dict: """ .. versionadded:: 2.4.0 Convert the object to a JSON dictionary. Returns ------- dict """ return self._crs.to_json_dict()
[docs] def to_proj4(self, version: Union[ProjVersion, int] = ProjVersion.PROJ_5) -> str: """ Convert the projection to a PROJ string. .. warning:: You will likely lose important projection information when converting to a PROJ string from another format. See: https://proj.org/faq.html#what-is-the-best-format-for-describing-coordinate-reference-systems # noqa: E501 Parameters ---------- version: pyproj.enums.ProjVersion The version of the PROJ string output. Default is :attr:`pyproj.enums.ProjVersion.PROJ_4`. Returns ------- str """ return self._crs.to_proj4(version=version)
[docs] def to_epsg(self, min_confidence: int = 70) -> Optional[int]: """ Return the EPSG code best matching the CRS or None if it a match is not found. Example: >>> from pyproj import CRS >>> ccs = CRS("epsg:4328") >>> ccs.to_epsg() 4328 If the CRS is bound, you can attempt to get an epsg code from the source CRS: >>> from pyproj import CRS >>> ccs = CRS("+proj=geocent +datum=WGS84 +towgs84=0,0,0") >>> ccs.to_epsg() >>> ccs.source_crs.to_epsg() 4978 >>> ccs == CRS.from_epsg(4978) False Parameters ---------- min_confidence: int, optional A value between 0-100 where 100 is the most confident. Default is 70. :ref:`min_confidence` Returns ------- Optional[int]: The best matching EPSG code matching the confidence level. """ return self._crs.to_epsg(min_confidence=min_confidence)
[docs] def to_authority(self, auth_name: Optional[str] = None, min_confidence: int = 70): """ .. versionadded:: 2.2.0 Return the authority name and code best matching the CRS or None if it a match is not found. Example: >>> from pyproj import CRS >>> ccs = CRS("epsg:4328") >>> ccs.to_authority() ('EPSG', '4328') If the CRS is bound, you can get an authority from the source CRS: >>> from pyproj import CRS >>> ccs = CRS("+proj=geocent +datum=WGS84 +towgs84=0,0,0") >>> ccs.to_authority() >>> ccs.source_crs.to_authority() ('EPSG', '4978') >>> ccs == CRS.from_authorty('EPSG', '4978') False Parameters ---------- auth_name: str, optional The name of the authority to filter by. min_confidence: int, optional A value between 0-100 where 100 is the most confident. Default is 70. :ref:`min_confidence` Returns ------- tuple(str, str) or None: The best matching (<auth_name>, <code>) for the confidence level. """ return self._crs.to_authority( auth_name=auth_name, min_confidence=min_confidence )
[docs] def to_3d(self, name: Optional[str] = None) -> "CRS": """ .. versionadded:: 3.1 Convert the current CRS to the 3D version if it makes sense. New vertical axis attributes: - ellipsoidal height - oriented upwards - metre units Parameters ---------- name: str, optional CRS name. Defaults to use the name of the original CRS. Returns ------- CRS """ return CRS(self._crs.to_3d(name=name))
@property def is_geographic(self) -> bool: """ This checks if the CRS is geographic. It will check if it has a geographic CRS in the sub CRS if it is a compount CRS and will check if the source CRS is geographic if it is a bound CRS. Returns ------- bool: True if the CRS is in geographic (lon/lat) coordinates. """ return self._crs.is_geographic @property def is_projected(self) -> bool: """ This checks if the CRS is projected. It will check if it has a projected CRS in the sub CRS if it is a compount CRS and will check if the source CRS is projected if it is a bound CRS. Returns ------- bool: True if CRS is projected. """ return self._crs.is_projected @property def is_vertical(self) -> bool: """ .. versionadded:: 2.2.0 This checks if the CRS is vertical. It will check if it has a vertical CRS in the sub CRS if it is a compount CRS and will check if the source CRS is vertical if it is a bound CRS. Returns ------- bool: True if CRS is vertical. """ return self._crs.is_vertical @property def is_bound(self) -> bool: """ Returns ------- bool: True if CRS is bound. """ return self._crs.is_bound @property def is_compound(self) -> bool: """ .. versionadded:: 3.1.0 Returns ------- bool: True if CRS is compound. """ return self._crs.is_compound @property def is_engineering(self) -> bool: """ .. versionadded:: 2.2.0 Returns ------- bool: True if CRS is local/engineering. """ return self._crs.is_engineering @property def is_geocentric(self) -> bool: """ This checks if the CRS is geocentric and takes into account if the CRS is bound. Returns ------- bool: True if CRS is in geocentric (x/y) coordinates. """ return self._crs.is_geocentric def __eq__(self, other: Any) -> bool: return self.equals(other) def __reduce__(self) -> Tuple[Type["CRS"], Tuple[str]]: """special method that allows CRS instance to be pickled""" return self.__class__, (self.srs,) def __hash__(self) -> int: return hash(self.to_wkt()) def __str__(self) -> str: return self.srs def __repr__(self) -> str: # get axis information axis_info_list = [] # type: List[str] for axis in self.axis_info: axis_info_list.extend(["- ", str(axis), "\n"]) axis_info_str = "".join(axis_info_list) # get coordinate system & sub CRS info source_crs_repr = "" sub_crs_repr = "" if self.coordinate_system and self.coordinate_system.axis_list: coordinate_system_name = str(self.coordinate_system) elif self.is_bound and self.source_crs: coordinate_system_name = str(self.source_crs.coordinate_system) source_crs_repr = f"Source CRS: {self.source_crs.name}\n" else: coordinate_system_names = [] sub_crs_repr_list = ["Sub CRS:\n"] for sub_crs in self.sub_crs_list: coordinate_system_names.append(str(sub_crs.coordinate_system)) sub_crs_repr_list.extend(["- ", sub_crs.name, "\n"]) coordinate_system_name = "|".join(coordinate_system_names) sub_crs_repr = "".join(sub_crs_repr_list) # get coordinate operation repr coordinate_operation = "" if self.coordinate_operation: coordinate_operation = "".join( [ "Coordinate Operation:\n", "- name: ", str(self.coordinate_operation), "\n" "- method: ", str(self.coordinate_operation.method_name), "\n", ] ) # get SRS representation srs_repr = self.to_string() srs_repr = srs_repr if len(srs_repr) <= 50 else " ".join([srs_repr[:50], "..."]) axis_info_str = axis_info_str or "- undefined\n" return ( f"<{self.type_name}: {srs_repr}>\n" f"Name: {self.name}\n" f"Axis Info [{coordinate_system_name or 'undefined'}]:\n" f"{axis_info_str}" "Area of Use:\n" f"{self.area_of_use or '- undefined'}\n" f"{coordinate_operation}" f"Datum: {self.datum}\n" f"- Ellipsoid: {self.ellipsoid or 'undefined'}\n" f"- Prime Meridian: {self.prime_meridian or 'undefined'}\n" f"{source_crs_repr}" f"{sub_crs_repr}" )
[docs]class GeographicCRS(CRS): """ .. versionadded:: 2.5.0 This class is for building a Geographic CRS """
[docs] def __init__( self, name: str = "undefined", datum: Any = "urn:ogc:def:datum:EPSG::6326", ellipsoidal_cs: Any = None, ) -> None: """ 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 or Ellipsoidal2DCS() ).to_json_dict(), } super().__init__(geographic_crs_json)
[docs]class DerivedGeographicCRS(CRS): """ .. versionadded:: 2.5.0 This class is for building a Derived Geographic CRS """
[docs] def __init__( self, base_crs: Any, conversion: Any, ellipsoidal_cs: Any = None, name: str = "undefined", ) -> None: """ Parameters ---------- base_crs: Any Input to create the Geodetic CRS, a :class:`GeographicCRS` or anything accepted by :meth:`pyproj.crs.CRS.from_user_input`. conversion: Any Anything accepted by :meth:`pyproj.crs.CoordinateSystem.from_user_input` or a conversion from :ref:`coordinate_operation`. 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`. name: str, optional Name of the CRS. Default is undefined. """ derived_geographic_crs_json = { "$schema": "https://proj.org/schemas/v0.2/projjson.schema.json", "type": "DerivedGeographicCRS", "name": name, "base_crs": CRS.from_user_input(base_crs).to_json_dict(), "conversion": CoordinateOperation.from_user_input( conversion ).to_json_dict(), "coordinate_system": CoordinateSystem.from_user_input( ellipsoidal_cs or Ellipsoidal2DCS() ).to_json_dict(), } super().__init__(derived_geographic_crs_json)
[docs]class ProjectedCRS(CRS): """ .. versionadded:: 2.5.0 This class is for building a Projected CRS. """
[docs] def __init__( self, conversion: Any, name: str = "undefined", cartesian_cs: Any = None, geodetic_crs: Any = None, ) -> None: """ Parameters ---------- conversion: Any Anything accepted by :meth:`pyproj.crs.CoordinateSystem.from_user_input` or a conversion 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 or GeographicCRS() ).to_json_dict(), "conversion": CoordinateOperation.from_user_input( conversion ).to_json_dict(), "coordinate_system": CoordinateSystem.from_user_input( cartesian_cs or Cartesian2DCS() ).to_json_dict(), } super().__init__(proj_crs_json)
[docs]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 """
[docs] def __init__( self, name: str, datum: Any, vertical_cs: Any = None, geoid_model: Optional[str] = None, ) -> 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 or VerticalCS() ).to_json_dict(), } if geoid_model is not None: vert_crs_json["geoid_model"] = {"name": geoid_model} super().__init__(vert_crs_json)
[docs]class CompoundCRS(CRS): """ .. versionadded:: 2.5.0 This class is for building a Compound CRS. """
[docs] def __init__(self, name: str, components: List[Any]) -> None: """ 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)
[docs]class BoundCRS(CRS): """ .. versionadded:: 2.5.0 This class is for building a Bound CRS. """
[docs] def __init__(self, source_crs: Any, target_crs: Any, transformation: Any) -> None: """ 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)