Skip to content

Commit

Permalink
ENH: Added force_over kwarg to Transformer.from_crs (#1123)
Browse files Browse the repository at this point in the history
  • Loading branch information
snowman2 committed Aug 25, 2022
1 parent dccefd6 commit f7f4c25
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Latest
- DEP: Minimum PROJ version 8.2 (issue #1011)
- BUG: Fix transformer list for 3D transformations in :class:`.TransformerGroup` (discussion #1072)
- ENH: Added authority, accuracy, and allow_ballpark kwargs to :class:`.TransformerGroup` (pull #1076)
- ENH: Added ``force_over`` kwarg to :meth:`.Transformer.from_crs` (issue #997)
- CLN: Remove deprecated ``skip_equivalent`` kwarg from transformers and ``errcheck`` kwarg from :meth:`.CRS.from_cf` (pull #1077)
- REF: use regex to process PROJ strings in :meth:`.CRS.to_dict` (pull #1086)
- BUG: :class:`.MercatorAConversion` defined only for lat_0 = 0 (issue #1089)
Expand Down
1 change: 1 addition & 0 deletions pyproj/_transformer.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class _Transformer(Base):
authority: Optional[str] = None,
accuracy: Optional[str] = None,
allow_ballpark: Optional[bool] = None,
force_over: bool = False,
) -> "_Transformer": ...
@staticmethod
def from_pipeline(proj_pipeline: bytes) -> "_Transformer": ...
Expand Down
17 changes: 13 additions & 4 deletions pyproj/_transformer.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ cdef PJ* proj_create_crs_to_crs(
str authority,
str accuracy,
allow_ballpark,
):
bint force_over,
) except NULL:
"""
This is the same as proj_create_crs_to_crs in proj.h
with the options added. It is a hack for stabilily
Expand Down Expand Up @@ -272,6 +273,7 @@ cdef PJ* proj_create_crs_to_crs(
options[1] = NULL
options[2] = NULL
options[3] = NULL
options[4] = NULL
if authority is not None:
b_authority = cstrencode(f"AUTHORITY={authority}")
options[options_index] = b_authority
Expand All @@ -283,6 +285,12 @@ cdef PJ* proj_create_crs_to_crs(
if allow_ballpark is not None:
if not allow_ballpark:
options[options_index] = b"ALLOW_BALLPARK=NO"
options_index += 1
if force_over:
IF CTE_PROJ_VERSION_MAJOR >= 9:
options[options_index] = b"FORCE_OVER=YES"
ELSE:
raise NotImplementedError("force_over requires PROJ 9+.")

cdef PJ* transform = proj_create_crs_to_crs_from_pj(
ctx,
Expand All @@ -293,6 +301,8 @@ cdef PJ* proj_create_crs_to_crs(
)
proj_destroy(source_crs)
proj_destroy(target_crs)
if transform == NULL:
raise ProjError("Error creating Transformer from CRS.")
return transform


Expand Down Expand Up @@ -455,6 +465,7 @@ cdef class _Transformer(Base):
str authority=None,
str accuracy=None,
allow_ballpark=None,
bint force_over=False,
):
"""
Create a transformer from CRS objects
Expand Down Expand Up @@ -493,14 +504,12 @@ cdef class _Transformer(Base):
authority=authority,
accuracy=accuracy,
allow_ballpark=allow_ballpark,
force_over=force_over,
)
finally:
if pj_area_of_interest != NULL:
proj_area_destroy(pj_area_of_interest)

if transformer.projobj == NULL:
raise ProjError("Error creating Transformer from CRS.")

transformer._init_from_crs(always_xy)
return transformer

Expand Down
14 changes: 13 additions & 1 deletion pyproj/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,14 @@ def __call__(self) -> _Transformer:


@dataclass(frozen=True)
class TransformerFromCRS(TransformerMaker):
class TransformerFromCRS( # pylint: disable=too-many-instance-attributes
TransformerMaker
):
"""
.. versionadded:: 3.1.0
.. versionadded:: 3.4.0 force_over
Generates a Cython _Transformer class from input CRS data.
"""

Expand All @@ -87,6 +91,7 @@ class TransformerFromCRS(TransformerMaker):
authority: Optional[str]
accuracy: Optional[str]
allow_ballpark: Optional[bool]
force_over: bool = False

def __call__(self) -> _Transformer:
"""
Expand All @@ -102,6 +107,7 @@ def __call__(self) -> _Transformer:
authority=self.authority,
accuracy=self.accuracy,
allow_ballpark=self.allow_ballpark,
force_over=self.force_over,
)


Expand Down Expand Up @@ -520,12 +526,14 @@ def from_crs(
authority: Optional[str] = None,
accuracy: Optional[float] = None,
allow_ballpark: Optional[bool] = None,
force_over: bool = False,
) -> "Transformer":
"""Make a Transformer from a :obj:`pyproj.crs.CRS` or input used to create one.
.. versionadded:: 2.2.0 always_xy
.. versionadded:: 2.3.0 area_of_interest
.. versionadded:: 3.1.0 authority, accuracy, allow_ballpark
.. versionadded:: 3.4.0 force_over
Parameters
----------
Expand Down Expand Up @@ -554,6 +562,9 @@ def from_crs(
allow_ballpark: bool, optional
Set to False to disallow the use of Ballpark transformation
in the candidate coordinate operations. Default is to allow.
force_over: bool, default=False
If True, it will to force the +over flag on the transformation.
Requires PROJ 9+.
Returns
-------
Expand All @@ -569,6 +580,7 @@ def from_crs(
authority=authority,
accuracy=accuracy if accuracy is None else str(accuracy),
allow_ballpark=allow_ballpark,
force_over=force_over,
)
)

Expand Down
1 change: 1 addition & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

_NETWORK_ENABLED = pyproj.network.is_network_enabled()
PROJ_LOOSE_VERSION = version.parse(pyproj.__proj_version__)
PROJ_GTE_9 = PROJ_LOOSE_VERSION >= version.parse("9.0.0")
PROJ_GTE_901 = PROJ_LOOSE_VERSION >= version.parse("9.0.1")
PROJ_GTE_91 = PROJ_LOOSE_VERSION >= version.parse("9.1")

Expand Down
59 changes: 58 additions & 1 deletion test/test_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@
from pyproj.enums import TransformDirection
from pyproj.exceptions import ProjError
from pyproj.transformer import AreaOfInterest, TransformerGroup
from test.conftest import PROJ_GTE_91, grids_available, proj_env, proj_network_env
from test.conftest import (
PROJ_GTE_9,
PROJ_GTE_91,
grids_available,
proj_env,
proj_network_env,
)


def test_tranform_wgs84_to_custom():
Expand Down Expand Up @@ -1519,6 +1525,30 @@ def test_pickle_transformer_from_crs():
assert transformer == pickle.loads(pickle.dumps(transformer))


def test_unpickle_transformer_from_crs_v1_3():
pickled_transformer = (
b"\x80\x04\x95p\x01\x00\x00\x00\x00\x00\x00\x8c\x12"
b"pyproj.transformer\x94\x8c\x0bTransformer\x94\x93\x94)"
b"\x81\x94}\x94\x8c\x12_transformer_maker\x94h\x00\x8c\x12"
b"TransformerFromCRS\x94\x93\x94)\x81\x94}\x94(\x8c\x08"
b"crs_from\x94C\tEPSG:4326\x94\x8c\x06crs_to\x94C\tEPSG:2964"
b"\x94\x8c\talways_xy\x94\x88\x8c\x10area_of_interest\x94\x8c\n"
b"pyproj.aoi\x94\x8c\x0eAreaOfInterest\x94\x93\x94)\x81\x94}\x94"
b"(\x8c\x0fwest_lon_degree\x94G\xc0a\x0e\xb8Q\xeb\x85\x1f\x8c\x10"
b"south_lat_degree\x94G@H\x80\x00\x00\x00\x00\x00\x8c\x0f"
b"east_lon_degree\x94G\xc0N\\(\xf5\xc2\x8f\\\x8c\x10"
b"north_lat_degree\x94G@T\xca\xe1G\xae\x14"
b"{ub\x8c\tauthority\x94N\x8c\x08accuracy\x94N\x8c\x0eallow_ballpark\x94Nubsb."
)
transformer = Transformer.from_crs(
"EPSG:4326",
"EPSG:2964",
always_xy=True,
area_of_interest=AreaOfInterest(-136.46, 49.0, -60.72, 83.17),
)
assert transformer == pickle.loads(pickled_transformer)


def test_transformer_group_accuracy_filter():
group = TransformerGroup("EPSG:4326", "EPSG:4258", accuracy=0.05)
assert not group.transformers
Expand All @@ -1541,3 +1571,30 @@ def test_transformer_group_authority_filter():
group.transformers[0].description
== "Ballpark geographic offset from WGS 84 to ETRS89"
)


def test_transformer_force_over():
if PROJ_GTE_9:
transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", force_over=True)
# Test a point along the equator.
# The same point, but in two different representations.
xxx, yyy = transformer.transform(0, 140)
xxx_over, yyy_over = transformer.transform(0, -220)
# Web Mercator x's between 0 and 180 longitude come out positive.
# But when forcing the over flag, the -220 calculation makes it flip.
assert xxx > 0
assert xxx_over < 0
# check it works in both directions
xxx_inverse, yyy_inverse = transformer.transform(
xxx, yyy, direction=TransformDirection.INVERSE
)
xxx_over_inverse, yyy_over_inverse = transformer.transform(
xxx_over, yyy_over, direction=TransformDirection.INVERSE
)
assert_almost_equal(xxx_inverse, 0)
assert_almost_equal(xxx_over_inverse, 0)
assert_almost_equal(yyy_inverse, 140)
assert_almost_equal(yyy_over_inverse, -220)
else:
with pytest.raises(NotImplementedError, match="force_over requires PROJ 9"):
Transformer.from_crs("EPSG:4326", "EPSG:3857", force_over=True)

0 comments on commit f7f4c25

Please sign in to comment.