Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Added force_over kwarg to Transformer.from_crs #1123

Merged
merged 1 commit into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)