Skip to content

API documentation

Fix antimeridian crossings in GeoJSON objects and shapely geometries.

FixWindingWarning

Bases: AntimeridianWarning

The input shape is wound clockwise (instead of counter-clockwise), so this package is reversing the winding order before fixing the shape.

Source code in src/antimeridian/_implementation.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class FixWindingWarning(AntimeridianWarning):
    """The input shape is wound clockwise (instead of counter-clockwise), so
    this package is reversing the winding order before fixing the shape.
    """

    MESSAGE = (
        "The exterior ring of this shape is wound "
        "clockwise. Since this is a common error in real-world "
        "geometries, this package is reversing the exterior coordinates of the "
        "input shape before running its algorithm. If you know that your input "
        "shape is correct (i.e. if your data encompasses both poles), pass "
        "`fix_winding=False`."
    )

    @classmethod
    def warn(cls) -> None:
        warnings.warn(cls.MESSAGE, cls, stacklevel=2)

GeoInterface

Bases: Protocol

A simple protocol for things that have a __geo_interface__ method.

The __geo_interface__ protocol is described here, and is used within shapely to extract geometries from objects.

Source code in src/antimeridian/_implementation.py
58
59
60
61
62
63
64
65
66
67
68
class GeoInterface(Protocol):
    """A simple protocol for things that have a `__geo_interface__` method.

    The `__geo_interface__` protocol is described
    [here](https://gist.github.com/sgillies/2217756>), and is used within
    [shapely](https://shapely.readthedocs.io/en/stable/manual.html) to extract
    geometries from objects.
    """

    @property
    def __geo_interface__(self) -> Dict[str, Any]: ...

bbox

bbox(
    shape: Dict[str, Any] | GeoInterface,
    force_over_antimeridian: bool = False,
) -> List[float]

Calculates a GeoJSON-spec conforming bounding box for a shape.

Per the GeoJSON spec, an antimeridian-spanning bounding box should have its larger longitude as its first bounding box coordinate.

Parameters:

  • shape (Dict[str, Any] | GeoInterface) –

    The polygon or multipolygon for which to calculate the bounding box.

  • force_over_antimeridian (bool, default: False ) –

    Force the bounding box to be over the antimeridian.

Returns:

  • List[float]

    List[float]: The bounding box.

Source code in src/antimeridian/_implementation.py
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
def bbox(
    shape: Dict[str, Any] | GeoInterface, force_over_antimeridian: bool = False
) -> List[float]:
    """Calculates a GeoJSON-spec conforming bounding box for a shape.

    Per the [GeoJSON
    spec](https://datatracker.ietf.org/doc/html/rfc7946#section-5.2), an
    antimeridian-spanning bounding box should have its larger longitude as its
    first bounding box coordinate.

    Args:
        shape: The polygon or multipolygon for which to calculate the bounding box.
        force_over_antimeridian: Force the bounding box to be over the antimeridian.

    Returns:
        List[float]: The bounding box.
    """
    geom = shapely.geometry.shape(shape)
    if geom.geom_type == "Polygon":
        return list(geom.bounds)
    elif geom.geom_type == "MultiPolygon":
        crosses_antimeridian = False
        xmins = list()
        ymin = 90
        xmaxs = list()
        ymax = -90
        for polygon in geom.geoms:
            bounds = polygon.bounds
            xmins.append(bounds[0])
            if bounds[1] < ymin:
                ymin = bounds[1]
            xmaxs.append(bounds[2])
            if bounds[3] > ymax:
                ymax = bounds[3]
            if is_coincident_to_antimeridian(polygon) and not (
                bounds[0] == -180 and bounds[2] == 180
            ):
                crosses_antimeridian = True

        if crosses_antimeridian or force_over_antimeridian:
            return [max(xmins), ymin, min(xmaxs), ymax]
        else:
            return [min(xmins), ymin, max(xmaxs), ymax]
    else:
        raise ValueError(
            f"unsupported geom_type for bbox calculation: {geom.geom_type}"
        )

centroid

centroid(shape: Dict[str, Any] | GeoInterface) -> Point

Calculates the centroid for a polygon or multipolygon.

Polygons are easy, we just use shapely.centroid. For multi-polygons, the antimeridian is taken into account by calculating the centroid from an identical multi-polygon with coordinates in [0, 360).

Parameters:

  • shape (Dict[str, Any] | GeoInterface) –

    The polygon or multipolygon for which to calculate the centroid.

Returns:

  • Point ( Point ) –

    The centroid.

Source code in src/antimeridian/_implementation.py
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
def centroid(shape: Dict[str, Any] | GeoInterface) -> Point:
    """Calculates the centroid for a polygon or multipolygon.

    Polygons are easy, we just use [shapely.centroid][]. For
    multi-polygons, the antimeridian is taken into account by calculating the
    centroid from an identical multi-polygon with coordinates in [0, 360).

    Args:
        shape: The polygon or multipolygon for which to calculate the centroid.

    Returns:
        Point: The centroid.
    """
    # Inspired by
    # https://github.com/stactools-packages/sentinel2/blob/f90f5fa006459e9bb59bfd327d9199e5259ec4a7/src/stactools/sentinel2/stac.py#L192-L208
    geom = shapely.geometry.shape(shape)
    if geom.geom_type == "Polygon":
        return cast(Point, geom.centroid)
    elif geom.geom_type == "MultiPolygon":
        geoms = list()
        for component in geom.geoms:
            if any(c[0] < 0 for c in component.exterior.coords):
                geoms.append(shapely.affinity.translate(component, xoff=+360))
            else:
                geoms.append(component)
        centroid = cast(
            Point, shapely.validation.make_valid(MultiPolygon(geoms)).centroid
        )
        if centroid.x > 180:
            centroid = Point(centroid.x - 360, centroid.y)
        return centroid
    else:
        raise ValueError(
            f"unsupported geom_type for centroid calculation: {geom.geom_type}"
        )

fix_geojson

fix_geojson(
    geojson: Dict[str, Any],
    *,
    force_north_pole: bool = False,
    force_south_pole: bool = False,
    fix_winding: bool = True,
    great_circle: bool = True,
    reverse: bool = False,
) -> Dict[str, Any]

Fixes a GeoJSON object that crosses the antimeridian.

If the object does not cross the antimeridian, it is returned unchanged.

See antimeridian.fix_polygon for a description of the force_north_pole force_south_pole and fix_winding arguments.

Parameters:

  • geojson (Dict[str, Any]) –

    A GeoJSON object as a dictionary

  • force_north_pole (bool, default: False ) –

    If the polygon crosses the antimeridian, force the joined segments to enclose the north pole.

  • force_south_pole (bool, default: False ) –

    If the polygon crosses the antimeridian, force the joined segments to enclose the south pole.

  • fix_winding (bool, default: True ) –

    If the polygon is wound clockwise, reverse its coordinates before applying the algorithm.

  • great_circle (bool, default: True ) –

    Compute meridian crossings on the sphere rather than using 2D geometry.

  • reverse (bool, default: False ) –

    Reverse the coordinates before fixing.

Return

The same GeoJSON with a fixed geometry or geometries

Source code in src/antimeridian/_implementation.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def fix_geojson(
    geojson: Dict[str, Any],
    *,
    force_north_pole: bool = False,
    force_south_pole: bool = False,
    fix_winding: bool = True,
    great_circle: bool = True,
    reverse: bool = False,
) -> Dict[str, Any]:
    """Fixes a GeoJSON object that crosses the antimeridian.

    If the object does not cross the antimeridian, it is returned unchanged.

    See [antimeridian.fix_polygon][] for a description of the `force_north_pole`
    `force_south_pole` and `fix_winding` arguments.

    Args:
        geojson: A GeoJSON object as a dictionary
        force_north_pole: If the polygon crosses the antimeridian, force the
            joined segments to enclose the north pole.
        force_south_pole: If the polygon crosses the antimeridian, force the
            joined segments to enclose the south pole.
        fix_winding: If the polygon is wound clockwise, reverse its
            coordinates before applying the algorithm.
        great_circle: Compute meridian crossings on the sphere rather than
            using 2D geometry.
        reverse: Reverse the coordinates before fixing.

    Return:
        The same GeoJSON with a fixed geometry or geometries
    """
    type_ = geojson.get("type", None)
    if type_ is None:
        raise ValueError("no 'type' field found in GeoJSON")
    elif type_ == "Feature":
        geometry = geojson.get("geometry", None)
        if geometry is None:
            raise ValueError("no 'geometry' field found in GeoJSON Feature")
        geojson["geometry"] = fix_shape(
            geometry,
            force_north_pole=force_north_pole,
            force_south_pole=force_south_pole,
            fix_winding=fix_winding,
            great_circle=great_circle,
            reverse=reverse,
        )
        return geojson
    elif type_ == "FeatureCollection":
        features = geojson.get("features", None)
        if features is None:
            raise ValueError("no 'features' field found in GeoJSON FeatureCollection")
        for i, feature in enumerate(features):
            features[i] = fix_geojson(
                feature,
                force_north_pole=force_north_pole,
                force_south_pole=force_south_pole,
                fix_winding=fix_winding,
                great_circle=great_circle,
                reverse=reverse,
            )
        geojson["features"] = features
        return geojson
    else:
        return fix_shape(
            geojson,
            force_north_pole=force_north_pole,
            force_south_pole=force_south_pole,
            fix_winding=fix_winding,
            great_circle=great_circle,
            reverse=reverse,
        )

fix_line_string

fix_line_string(
    line_string: LineString, great_circle: bool
) -> Union[LineString, MultiLineString]

Fixes a shapely.LineString.

Parameters:

  • line_string (LineString) –

    The input line string

  • great_circle (bool) –

    Compute meridian crossings on the sphere rather than using 2D geometry.

Returns:

  • Union[LineString, MultiLineString]

    The fixed line string, either as a single line string or a multi-line

  • Union[LineString, MultiLineString]

    string (if it was split)

Source code in src/antimeridian/_implementation.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
def fix_line_string(
    line_string: LineString, great_circle: bool
) -> Union[LineString, MultiLineString]:
    """Fixes a [shapely.LineString][].

    Args:
        line_string: The input line string
        great_circle: Compute meridian crossings on the sphere rather than
            using 2D geometry.

    Returns:
        The fixed line string, either as a single line string or a multi-line
        string (if it was split)
    """
    segments = segment(list(line_string.coords), great_circle)
    if not segments:
        return line_string
    else:
        return MultiLineString(segments)

fix_multi_line_string

fix_multi_line_string(
    multi_line_string: MultiLineString, great_circle: bool
) -> MultiLineString

Fixes a shapely.MultiLineString.

Parameters:

  • multi_line_string (MultiLineString) –

    The input multi line string

  • great_circle (bool) –

    Compute meridian crossings on the sphere rather than using 2D geometry.

Returns:

  • MultiLineString

    The fixed multi line string

Source code in src/antimeridian/_implementation.py
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
def fix_multi_line_string(
    multi_line_string: MultiLineString, great_circle: bool
) -> MultiLineString:
    """Fixes a [shapely.MultiLineString][].

    Args:
        multi_line_string: The input multi line string
        great_circle: Compute meridian crossings on the sphere rather than
            using 2D geometry.

    Returns:
        The fixed multi line string
    """
    line_strings = list()
    for line_string in multi_line_string.geoms:
        fixed = fix_line_string(line_string, great_circle)
        if isinstance(fixed, LineString):
            line_strings.append(fixed)
        else:
            line_strings.extend(fixed.geoms)
    return MultiLineString(line_strings)

fix_multi_polygon

fix_multi_polygon(
    multi_polygon: MultiPolygon,
    *,
    force_north_pole: bool = False,
    force_south_pole: bool = False,
    fix_winding: bool = True,
    great_circle: bool = True,
) -> MultiPolygon

Fixes a shapely.MultiPolygon.

See antimeridian.fix_polygon for a description of the force_north_pole force_south_pole and fix_winding arguments.

Parameters:

  • multi_polygon (MultiPolygon) –

    The multi-polygon

  • force_north_pole (bool, default: False ) –

    If the polygon crosses the antimeridian, force the joined segments to enclose the north pole.

  • force_south_pole (bool, default: False ) –

    If the polygon crosses the antimeridian, force the joined segments to enclose the south pole.

  • fix_winding (bool, default: True ) –

    If the polygon is wound clockwise, reverse its coordinates before applying the algorithm.

  • great_circle (bool, default: True ) –

    Compute meridian crossings on the sphere rather than using 2D geometry.

Returns:

  • MultiPolygon

    The fixed multi-polygon

Source code in src/antimeridian/_implementation.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
def fix_multi_polygon(
    multi_polygon: MultiPolygon,
    *,
    force_north_pole: bool = False,
    force_south_pole: bool = False,
    fix_winding: bool = True,
    great_circle: bool = True,
) -> MultiPolygon:
    """Fixes a [shapely.MultiPolygon][].

    See [antimeridian.fix_polygon][] for a description of the `force_north_pole`
    `force_south_pole` and `fix_winding` arguments.

    Args:
        multi_polygon: The multi-polygon
        force_north_pole: If the polygon crosses the antimeridian, force the
            joined segments to enclose the north pole.
        force_south_pole: If the polygon crosses the antimeridian, force the
            joined segments to enclose the south pole.
        fix_winding: If the polygon is wound clockwise, reverse its
            coordinates before applying the algorithm.
        great_circle: Compute meridian crossings on the sphere rather than
            using 2D geometry.

    Returns:
        The fixed multi-polygon
    """
    polygons = list()
    for polygon in multi_polygon.geoms:
        polygons += fix_polygon_to_list(
            polygon,
            force_north_pole=force_north_pole,
            force_south_pole=force_south_pole,
            fix_winding=fix_winding,
            great_circle=great_circle,
        )
    return MultiPolygon(polygons)

fix_polygon

fix_polygon(
    polygon: Polygon,
    *,
    force_north_pole: bool = False,
    force_south_pole: bool = False,
    fix_winding: bool = True,
    great_circle: bool = True,
) -> Union[Polygon, MultiPolygon]

Fixes a shapely.Polygon.

If the input polygon is wound clockwise, it will be fixed to be wound counterclockwise unless fix_winding is False in which case it will be corrected by adding a counterclockwise polygon from (-180, -90) to (180, 90) as its exterior.

In rare cases, the underlying algorithm might need a little help to fix the polygon. For example, a polygon that just barely crosses over a pole might have very few points at high latitudes, leading to ambiguous antimeridian crossing points and invalid geometries. We provide two flags, force_north_pole and force_south_pole for those cases. Most users can ignore these flags.

If either force_north_pole or force_south_pole is True fix_winding is set to False

Parameters:

  • polygon (Polygon) –

    The input polygon

  • force_north_pole (bool, default: False ) –

    If the polygon crosses the antimeridian, force the joined segments to enclose the north pole.

  • force_south_pole (bool, default: False ) –

    If the polygon crosses the antimeridian, force the joined segments to enclose the south pole.

  • fix_winding (bool, default: True ) –

    If the polygon is wound clockwise, reverse its coordinates before applying the algorithm.

  • great_circle (bool, default: True ) –

    Compute meridian crossings on the sphere rather than using 2D geometry.

Returns:

  • Union[Polygon, MultiPolygon]

    The fixed polygon, either as a single polygon or a multi-polygon (if it was split)

Source code in src/antimeridian/_implementation.py
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
def fix_polygon(
    polygon: Polygon,
    *,
    force_north_pole: bool = False,
    force_south_pole: bool = False,
    fix_winding: bool = True,
    great_circle: bool = True,
) -> Union[Polygon, MultiPolygon]:
    """Fixes a [shapely.Polygon][].

    If the input polygon is wound clockwise, it will be fixed to be wound
    counterclockwise _unless_ `fix_winding` is `False` in which case it
    will be corrected by adding a counterclockwise polygon from (-180, -90) to
    (180, 90) as its exterior.

    In rare cases, the underlying algorithm might need a little help to fix the polygon.
    For example, a polygon that just barely crosses over a pole might have very
    few points at high latitudes, leading to ambiguous antimeridian crossing
    points and invalid geometries. We provide two flags, `force_north_pole`
    and `force_south_pole` for those cases. Most users can ignore these
    flags.

    If either `force_north_pole` or `force_south_pole` is `True`
    `fix_winding` is set to `False`

    Args:
        polygon: The input polygon
        force_north_pole: If the polygon crosses the antimeridian, force the
            joined segments to enclose the north pole.
        force_south_pole: If the polygon crosses the antimeridian, force the
            joined segments to enclose the south pole.
        fix_winding: If the polygon is wound clockwise, reverse its
            coordinates before applying the algorithm.
        great_circle: Compute meridian crossings on the sphere rather than
            using 2D geometry.

    Returns:
        The fixed polygon, either as a single polygon or a multi-polygon (if it
            was split)
    """
    if force_north_pole or force_south_pole:
        fix_winding = False
    polygons = fix_polygon_to_list(
        polygon,
        force_north_pole=force_north_pole,
        force_south_pole=force_south_pole,
        fix_winding=fix_winding,
        great_circle=great_circle,
    )
    if len(polygons) == 1:
        polygon = polygons[0]
        if shapely.is_ccw(polygon.exterior):
            return polygon
        else:
            return Polygon(
                [(-180, 90), (-180, -90), (180, -90), (180, 90)],
                [polygon.exterior.coords],
            )
    else:
        return MultiPolygon(polygons)

fix_shape

fix_shape(
    shape: Dict[str, Any] | GeoInterface,
    *,
    force_north_pole: bool = False,
    force_south_pole: bool = False,
    fix_winding: bool = True,
    great_circle: bool = True,
    reverse: bool = False,
) -> Dict[str, Any]

Fixes a shape that crosses the antimeridian.

See antimeridian.fix_polygon for a description of the force_north_pole force_south_pole and fix_winding arguments.

Parameters:

  • shape (Dict[str, Any] | GeoInterface) –

    A polygon, multi-polygon, line string, or multi-line string, either as a dictionary or as a antimeridian.GeoInterface. Uses shapely.geometry.shape under the hood.

  • force_north_pole (bool, default: False ) –

    If the polygon crosses the antimeridian, force the joined segments to enclose the north pole.

  • force_south_pole (bool, default: False ) –

    If the polygon crosses the antimeridian, force the joined segments to enclose the south pole.

  • fix_winding (bool, default: True ) –

    If the polygon is wound clockwise, reverse its coordinates before applying the algorithm.

  • great_circle (bool, default: True ) –

    Compute meridian crossings on the sphere rather than using 2D geometry.

  • reverse (bool, default: False ) –

    Reverse the coordinates before fixing.

Returns:

  • Dict[str, Any]

    The fixed shape as a dictionary

Source code in src/antimeridian/_implementation.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def fix_shape(
    shape: Dict[str, Any] | GeoInterface,
    *,
    force_north_pole: bool = False,
    force_south_pole: bool = False,
    fix_winding: bool = True,
    great_circle: bool = True,
    reverse: bool = False,
) -> Dict[str, Any]:
    """Fixes a shape that crosses the antimeridian.

    See [antimeridian.fix_polygon][] for a description of the `force_north_pole`
    `force_south_pole` and `fix_winding` arguments.

    Args:
        shape: A polygon, multi-polygon, line string, or multi-line string,
            either as a dictionary or as a [antimeridian.GeoInterface][]. Uses
            [shapely.geometry.shape][] under the hood.
        force_north_pole: If the polygon crosses the antimeridian, force the
            joined segments to enclose the north pole.
        force_south_pole: If the polygon crosses the antimeridian, force the
            joined segments to enclose the south pole.
        fix_winding: If the polygon is wound clockwise, reverse its
            coordinates before applying the algorithm.
        great_circle: Compute meridian crossings on the sphere rather than
            using 2D geometry.
        reverse: Reverse the coordinates before fixing.

    Returns:
        The fixed shape as a dictionary
    """
    geom = shapely.geometry.shape(shape)
    if reverse:
        geom = geom.reverse()
    if geom.geom_type == "Polygon":
        return cast(
            Dict[str, Any],
            shapely.geometry.mapping(
                fix_polygon(
                    geom,
                    force_north_pole=force_north_pole,
                    force_south_pole=force_south_pole,
                    fix_winding=fix_winding,
                    great_circle=great_circle,
                )
            ),
        )
    elif geom.geom_type == "MultiPolygon":
        return cast(
            Dict[str, Any],
            shapely.geometry.mapping(
                fix_multi_polygon(
                    geom,
                    force_north_pole=force_north_pole,
                    force_south_pole=force_south_pole,
                    fix_winding=fix_winding,
                    great_circle=great_circle,
                )
            ),
        )
    elif geom.geom_type == "LineString":
        return cast(
            Dict[str, Any],
            shapely.geometry.mapping(fix_line_string(geom, great_circle)),
        )
    elif geom.geom_type == "MultiLineString":
        return cast(
            Dict[str, Any],
            shapely.geometry.mapping(fix_multi_line_string(geom, great_circle)),
        )
    else:
        raise ValueError(f"unsupported geom_type: {geom.geom_type}")

segment_geojson

segment_geojson(
    geojson: Dict[str, Any], great_circle: bool
) -> MultiLineString

Segments a GeoJSON object into a MultiLineString.

If the object does not cross the antimeridian, its exterior and interior line strings are returned unchanged.

Parameters:

  • geojson (Dict[str, Any]) –

    A GeoJSON object as a dictionary

  • great_circle (bool) –

    Compute meridian crossings on the sphere rather than using 2D geometry.

Return

A MutliLineString of segments.

Source code in src/antimeridian/_implementation.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def segment_geojson(geojson: Dict[str, Any], great_circle: bool) -> MultiLineString:
    """Segments a GeoJSON object into a MultiLineString.

    If the object does not cross the antimeridian, its exterior and interior
    line strings are returned unchanged.

    Args:
        geojson: A GeoJSON object as a dictionary
        great_circle: Compute meridian crossings on the sphere rather than
            using 2D geometry.

    Return:
        A MutliLineString of segments.
    """
    type_ = geojson.get("type", None)
    if type_ is None:
        raise ValueError("no 'type' field found in GeoJSON")
    elif type_ == "Feature":
        geometry = geojson.get("geometry", None)
        if geometry is None:
            raise ValueError("no 'geometry' field found in GeoJSON Feature")
        return MultiLineString(segment_shape(geometry, great_circle))
    elif type_ == "FeatureCollection":
        features = geojson.get("features", None)
        if features is None:
            raise ValueError("no 'features' field found in GeoJSON FeatureCollection")
        segments = list()
        for feature in features:
            segments.extend(segment_geojson(feature, great_circle))
        return MultiLineString(segments)
    else:
        return MultiLineString(segment_shape(geojson, great_circle))