Skip to content

Commit 74feaac

Browse files
committed
BoundingBox: add cyclic_antimeridian_crossing
related to Open-EO/openeo-geopyspark-driver#1568
1 parent 4620527 commit 74feaac

3 files changed

Lines changed: 30 additions & 11 deletions

File tree

openeo_driver/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.139.0a1"
1+
__version__ = "0.139.0a2"

openeo_driver/util/geometry.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -434,16 +434,16 @@ class BoundingBox:
434434
optionally (geo)referenced.
435435
436436
.. note::
437-
About anti-meridian handling:
438-
when working in a cyclic CRS like EPSG:4326 with a bounding box that crosses the anti-meridian,
437+
About antimeridian handling:
438+
when working in a cyclic CRS like EPSG:4326 with a bounding box that crosses the antimeridian,
439439
the "west" coordinate will be larger than the "east" coordinate.
440440
For example::
441441
442-
# Bounding box of 10 degrees wide across the anti-meridian
442+
# Bounding box of 10 degrees wide across the antimeridian
443443
bbox_a = BoundingBox(west=175, east=-175, crs="EPSG:4326", ...)
444444
445445
# bounding box of 350 degrees wide, starting in the west
446-
# and going all the way to the east, but not crossing the anti-meridian.
446+
# and going all the way to the east, but not crossing the antimeridian.
447447
bbox_b = BoundingBox(west=-175, east=175, crs="EPSG:4326", ...)
448448
449449
also see https://datatracker.ietf.org/doc/html/rfc7946#section-5.2
@@ -544,15 +544,25 @@ def assert_crs(self):
544544
def _crs_with_cyclic_x(crs: Union[None, str]) -> bool:
545545
"""
546546
Whether the x coordinate is cyclic (e.g. longitude in EPSG:4326)
547-
which requires some special handling like coordinate normalization and anti-meridian crossing handling.
547+
which requires some special handling like coordinate normalization and antimeridian crossing handling.
548548
"""
549549
return crs == "EPSG:4326"
550550

551551
@staticmethod
552552
def _normalize_longitude(x: float) -> float:
553553
"""Normalize an EPSG:4326 longitude coordinate to the range [-180, 180)"""
554+
# TODO: do this normalization in constructor, instead of each time on the fly?
554555
return (x + 180) % 360 - 180
555556

557+
def cyclic_antimeridian_crossing(self) -> bool:
558+
"""
559+
Whether this bounding box uses cyclic longitude coordinates
560+
and crosses the antimeridian (so that west > east).
561+
"""
562+
return self._crs_with_cyclic_x(self.crs) and (
563+
self._normalize_longitude(self.west) > self._normalize_longitude(self.east)
564+
)
565+
556566
def as_dict(self) -> dict:
557567
return {
558568
"west": self.west,
@@ -571,7 +581,7 @@ def as_wsen_tuple(self) -> Tuple[float, float, float, float]:
571581
def as_polygon(self) -> shapely.geometry.Polygon:
572582
"""
573583
Get bounding box as a shapely Polygon.
574-
Simple single polygon, but not ideal for proper handling of anti-meridian crossing in EPSG:4326,
584+
Simple single polygon, but not ideal for proper handling of antimeridian crossing in EPSG:4326,
575585
which require to split the geometry in two parts: use `as_geometry` instead for that.
576586
"""
577587
west, east = self.west, self.east
@@ -584,7 +594,7 @@ def as_polygon(self) -> shapely.geometry.Polygon:
584594
return shapely.geometry.box(minx=west, miny=self.south, maxx=east, maxy=self.north)
585595

586596
def as_geometry(self) -> Union[shapely.geometry.Polygon, shapely.geometry.MultiPolygon]:
587-
"""Get bounding box as a shapely geometry (Polygon or MultiPolygon when crossing anti-meridian)"""
597+
"""Get bounding box as a shapely geometry (Polygon or MultiPolygon when crossing antimeridian)"""
588598
west, east = self.west, self.east
589599
if self._crs_with_cyclic_x(self.crs):
590600
east = self._normalize_longitude(east)
@@ -614,7 +624,7 @@ def as_geojson(self) -> dict:
614624

615625
def centroid(self) -> Tuple[float, float]:
616626
if self._crs_with_cyclic_x(self.crs):
617-
# Properly handle cyclic longitude coordinates, and anti-meridian crossing
627+
# Properly handle cyclic longitude coordinates, and antimeridian crossing
618628
west = self._normalize_longitude(self.west)
619629
east = self._normalize_longitude(self.east)
620630
if west <= east:
@@ -654,7 +664,7 @@ def contains(self, x: float, y: float) -> bool:
654664
if west <= east:
655665
return west <= x <= east
656666
else:
657-
# Handle anti-meridian crossing
667+
# Handle antimeridian crossing
658668
return not (east < x < west)
659669
else:
660670
return self.west <= x <= self.east
@@ -681,7 +691,7 @@ def reproject(self, crs: Union[str, int]) -> "BoundingBox":
681691
west, south, east, north = reprojected.bounds
682692

683693
if self._crs_with_cyclic_x(crs):
684-
# Handle bounding boxes in EPSG:4326 around the anti-meridian
694+
# Handle bounding boxes in EPSG:4326 around the antimeridian
685695
# use the projection of the centroid to detect coordinate wrapping, and adjust bounds properly
686696
cx, cy = transformer.transform(*self.centroid())
687697
if not (west <= cx <= east):

tests/util/test_geometry.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,15 @@ def test_is_georeferenced(self):
741741
assert BoundingBox(1, 2, 3, 4).is_georeferenced() is False
742742
assert BoundingBox(1, 2, 3, 4, crs=4326).is_georeferenced() is True
743743

744+
def test_cyclic_antimeridian_crossing(self):
745+
assert BoundingBox(1, 2, 3, 4).cyclic_antimeridian_crossing() is False
746+
assert BoundingBox(-170, 2, 170, 4).cyclic_antimeridian_crossing() is False
747+
assert BoundingBox(-170, 2, 170, 4, crs=4326).cyclic_antimeridian_crossing() is False
748+
assert BoundingBox(170, 2, -170, 4).cyclic_antimeridian_crossing() is False
749+
assert BoundingBox(170, 2, -170, 4, crs=4326).cyclic_antimeridian_crossing() is True
750+
assert BoundingBox(-190, 2, -170, 4, crs=4326).cyclic_antimeridian_crossing() is True
751+
assert BoundingBox(170, 2, 190, 4, crs=4326).cyclic_antimeridian_crossing() is True
752+
744753
def test_has_crs(self):
745754
assert BoundingBox(1, 2, 3, 4).has_crs() is False
746755
assert BoundingBox(1, 2, 3, 4, crs=4326).has_crs() is True

0 commit comments

Comments
 (0)