@@ -534,10 +534,6 @@ def __init__(self, *, west: float, south: float, east: float, north: float, crs:
534534
535535 # TODO: provide west, south, east, north, crs as @properties? Read-only or read-write?
536536
537- def as_polygon (self ) -> shapely .geometry .Polygon :
538- """Convert to a :class:`shapely.geometry.Polygon` representing the bounding box."""
539- return shapely .geometry .box (minx = self ["west" ], miny = self ["south" ], maxx = self ["east" ], maxy = self ["north" ])
540-
541537 @classmethod
542538 def from_any (cls , x : Any , * , crs : Optional [str ] = None ) -> BBoxDict :
543539 if isinstance (x , dict ):
@@ -573,6 +569,56 @@ def from_sequence(cls, seq: Union[list, tuple], crs: Optional[str] = None) -> BB
573569 raise InvalidBBoxException (f"Expected sequence with 4 items, but got { len (seq )} ." )
574570 return cls (west = seq [0 ], south = seq [1 ], east = seq [2 ], north = seq [3 ], crs = crs )
575571
572+ @staticmethod
573+ def normalize_west_east_longitude (west : float , east : float ) -> Tuple [float , float ]:
574+ """
575+ Assuming longitude in degrees (e.g. EPSG:4326):
576+ normalize west to range [-180, 180) and east to range (-180, 180]
577+ which is useful in bounding box contexts.
578+ """
579+ west = ((west + 180 ) % 360 ) - 180
580+ east = 180 - ((180 - east ) % 360 )
581+ return west , east
582+
583+ @staticmethod
584+ def _crs_with_cyclic_x (crs : Union [None , str ]) -> bool :
585+ """
586+ Whether the x coordinate is cyclic (e.g. longitude in EPSG:4326)
587+ which requires some special handling like coordinate normalization and antimeridian crossing handling.
588+ """
589+ return normalize_crs (crs ) == 4326
590+
591+ def as_polygon (self ) -> shapely .geometry .Polygon :
592+ """
593+ Get bounding box as a shapely Polygon.
594+ Simple single polygon, but not ideal for proper handling of antimeridian crossing in EPSG:4326,
595+ which require to split the geometry in two parts: use `as_geometry` instead for that.
596+ """
597+ west , east = self ["west" ], self ["east" ]
598+ if self ._crs_with_cyclic_x (self .get ("crs" )):
599+ west , east = self .normalize_west_east_longitude (west = west , east = east )
600+ if east < west :
601+ # TODO: this assumes "cyclic" implies longitude in degrees (EPSG:4326)
602+ east += 360
603+
604+ return shapely .geometry .box (minx = west , miny = self ["south" ], maxx = east , maxy = self ["north" ])
605+
606+ def as_geometry (self ) -> Union [shapely .geometry .Polygon , shapely .geometry .MultiPolygon ]:
607+ """Get bounding box as a shapely geometry (Polygon or MultiPolygon when crossing antimeridian)"""
608+ west , east = self ["west" ], self ["east" ]
609+ if self ._crs_with_cyclic_x (self .get ("crs" )):
610+ west , east = self .normalize_west_east_longitude (west = west , east = east )
611+ if east < west :
612+ # TODO: this assumes "cyclic" implies longitude in degrees (EPSG:4326), and split is at +/-180
613+ return shapely .geometry .MultiPolygon (
614+ [
615+ shapely .geometry .box (west , self ["south" ], 180 , self ["north" ]),
616+ shapely .geometry .box (- 180 , self ["south" ], east , self ["north" ]),
617+ ]
618+ )
619+
620+ return shapely .geometry .box (minx = west , miny = self ["south" ], maxx = east , maxy = self ["north" ])
621+
576622
577623def to_bbox_dict (x : Any , * , crs : Optional [Union [str , int ]] = None ) -> BBoxDict :
578624 """
0 commit comments