[Git][debian-gis-team/antimeridian][upstream] New upstream version 0.3.12
Antonio Valentino (@antonio.valentino)
gitlab at salsa.debian.org
Thu Dec 12 07:10:51 GMT 2024
Antonio Valentino pushed to branch upstream at Debian GIS Project / antimeridian
Commits:
15614f0f by Antonio Valentino at 2024-12-12T06:57:35+00:00
New upstream version 0.3.12
- - - - -
30 changed files:
- .github/workflows/ci.yaml
- + .github/workflows/docs.yaml
- .github/workflows/release.yaml
- .pre-commit-config.yaml
- − .readthedocs.yaml
- CHANGELOG.md
- README.md
- − docs/Makefile
- − docs/_static/.gitignore
- + docs/api.md
- − docs/api.rst
- + docs/cli.md
- − docs/cli.rst
- + docs/comparison.ipynb
- − docs/conf.py
- − docs/environment.yaml
- + docs/examples.ipynb
- − docs/examples.md
- + docs/failure-modes.ipynb
- − docs/failure-modes.md
- docs/img/complex-split.png
- docs/index.rst → docs/index.md
- − docs/make.bat
- docs/paper.bib
- docs/paper.md
- docs/the-algorithm.rst → docs/the-algorithm.md
- + mkdocs.yml
- pyproject.toml
- src/antimeridian/_implementation.py
- uv.lock
Changes:
=====================================
.github/workflows/ci.yaml
=====================================
@@ -12,6 +12,7 @@ on:
jobs:
test:
+ name: Test
runs-on: ubuntu-latest
strategy:
fail-fast: true
@@ -25,7 +26,7 @@ jobs:
- uses: actions/setup-python at v5
with:
python-version: ${{ matrix.python-version }}
- - uses: astral-sh/setup-uv at v3
+ - uses: astral-sh/setup-uv at v4
- name: Sync
run: uv sync
- name: pre-commit
@@ -33,6 +34,7 @@ jobs:
- name: pytest
run: uv run pytest
min-dependencies:
+ name: Minimum dependencies
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -41,7 +43,7 @@ jobs:
uses: actions/setup-python at v5
with:
python-version: "3.10"
- - uses: astral-sh/setup-uv at v3
+ - uses: astral-sh/setup-uv at v4
- name: Sync
run: uv sync --resolution lowest-direct
- name: pytest
@@ -50,25 +52,3 @@ jobs:
run: uv sync --resolution lowest-direct --all-extras
- name: pytest
run: uv run pytest
- docs:
- runs-on: ubuntu-latest
- defaults:
- run:
- shell: bash -el {0}
- env:
- PYDEVD_DISABLE_FILE_VALIDATION: 1
- steps:
- - name: Checkout
- uses: actions/checkout at v4
- - name: Cache conda
- uses: actions/cache at v4
- with:
- path: ~/conda_pkgs_dir
- key: ${{ runner.os }}-conda-${{ hashFiles('docs/environment.yaml') }}
- - uses: conda-incubator/setup-miniconda at v3
- with:
- auto-update-conda: true
- environment-file: docs/environment.yaml
- use-mamba: true
- - name: Check docs
- run: sphinx-build -W docs docs/_build/html
=====================================
.github/workflows/docs.yaml
=====================================
@@ -0,0 +1,34 @@
+name: Docs
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ pages: write
+
+concurrency:
+ group: "docs"
+ cancel-in-progress: true
+
+jobs:
+ deploy:
+ name: Deploy
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout at v4
+ with:
+ fetch-depth: 0
+ - uses: astral-sh/setup-uv at v4
+ - name: Sync
+ run: uv sync
+ - name: Deploy
+ env:
+ GIT_COMMITTER_NAME: ci-bot
+ GIT_COMMITTER_EMAIL: ci-bot at example.com
+ run: |
+ VERSION=$(git describe --tags --match="v*" --abbrev=0)
+ uv run mike deploy $VERSION latest --update-aliases --push
=====================================
.github/workflows/release.yaml
=====================================
@@ -12,6 +12,7 @@ concurrency:
jobs:
release:
+ name: Release
runs-on: ubuntu-latest
permissions:
id-token: write
@@ -29,11 +30,6 @@ jobs:
run: pip install build
- name: Build
run: python -m build
- - name: Publish to TestPyPI
- uses: pypa/gh-action-pypi-publish at release/v1
- with:
- repository-url: https://test.pypi.org/legacy/
- skip-existing: true
- name: Publish to PyPI
if: startsWith(github.ref, 'refs/tags/v')
uses: pypa/gh-action-pypi-publish at release/v1
=====================================
.pre-commit-config.yaml
=====================================
@@ -5,16 +5,15 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- - id: check-added-large-files
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.12.0
+ rev: v1.13.0
hooks:
- id: mypy
additional_dependencies:
- click~=8.1.6
- pytest>=8.0
- repo: https://github.com/charliermarsh/ruff-pre-commit
- rev: v0.6.9
+ rev: v0.8.2
hooks:
- id: ruff
types_or: [python, pyi, jupyter]
=====================================
.readthedocs.yaml deleted
=====================================
@@ -1,9 +0,0 @@
-version: 2
-build:
- os: ubuntu-22.04
- tools:
- python: "mambaforge-4.10"
-conda:
- environment: docs/environment.yaml
-sphinx:
- configuration: docs/conf.py
=====================================
CHANGELOG.md
=====================================
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased]
+## [0.3.12] - 2024-12-09
+
+### Changed
+
+- Documentation updates for [JOSS paper](https://github.com/openjournals/joss-reviews/issues/7530).
+
## [0.3.11] - 2024-10-17
### Fixed
@@ -164,7 +170,8 @@ This v0.1.0 release is to indicate that we think that this package is ready to u
Initial release.
-[unreleased]: https://github.com/gadomski/antimeridian/compare/v0.3.11...HEAD
+[unreleased]: https://github.com/gadomski/antimeridian/compare/v0.3.12...HEAD
+[0.3.12]: https://github.com/gadomsk/antimeridian/compare/v0.3.11...v0.3.12
[0.3.11]: https://github.com/gadomsk/antimeridian/compare/v0.3.10...v0.3.11
[0.3.10]: https://github.com/gadomsk/antimeridian/compare/v0.3.9...v0.3.10
[0.3.9]: https://github.com/gadomsk/antimeridian/compare/v0.3.8...v0.3.9
@@ -187,3 +194,5 @@ Initial release.
[0.1.0]: https://github.com/gadomsk/antimeridian/compare/v0.0.2...v0.1.0
[0.0.2]: https://github.com/gadomsk/antimeridian/compare/v0.0.1...v0.0.2
[0.0.1]: https://github.com/gadomski/antimeridian/releases/tag/v0.0.1
+
+<!-- markdownlint-disable-file MD024 -->
=====================================
README.md
=====================================
@@ -1,13 +1,14 @@
# antimeridian
[](https://github.com/gadomski/antimeridian/actions/workflows/ci.yaml)
-[](https://antimeridian.readthedocs.io/en/stable/)
+[](https://www.gadom.ski/antimeridian/)
[](https://pypi.org/project/antimeridian/)
-
[](https://github.com/gadomski/antimeridian/blob/main/LICENSE)
[](https://github.com/gadomski/antimeridian/blob/main/CODE_OF_CONDUCT)
-<img src="https://github.com/gadomski/antimeridian/blob/main/docs/img/complex-split.png?raw=true" style="width: 600px;" alt="Demonstration image" />
+[](https://joss.theoj.org/papers/2a6c626b3774c8310e46c05fdf8d10de)
+
+
Fix shapes that cross the antimeridian.
See [the documentation](https://antimeridian.readthedocs.io) for information about the underlying algorithm.
@@ -34,7 +35,7 @@ fixed = antimeridian.fix_geojson(geojson)
```
We also have some utilities to create [bounding boxes](https://antimeridian.readthedocs.io/en/latest/api.html#antimeridian.bbox) and [centroids](https://antimeridian.readthedocs.io/en/latest/api.html#antimeridian.centroid) from antimeridian-crossing polygons and multipolygons.
-See [the documentation](https://antimeridian.readthedocs.io/) for a complete API reference.
+See [the documentation](https://www.gadom.ski/antimeridian/) for a complete API reference.
### Command line interface
@@ -62,10 +63,10 @@ We use [pytest](https://docs.pytest.org) for tests:
uv run pytest
```
-We use [Sphinx](https://www.sphinx-doc.org) for docs:
+To build and serve the docs locally:
```shell
-make -C docs html
+uv run mkdocs serve
```
## Contributing
=====================================
docs/Makefile deleted
=====================================
@@ -1,20 +0,0 @@
-# Minimal makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line, and also
-# from the environment for the first two.
-SPHINXOPTS ?=
-SPHINXBUILD ?= sphinx-build
-SOURCEDIR = .
-BUILDDIR = _build
-
-# Put it first so that "make" without argument is like "make help".
-help:
- @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-.PHONY: help Makefile
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
- @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
=====================================
docs/_static/.gitignore deleted
=====================================
=====================================
docs/api.md
=====================================
@@ -0,0 +1,3 @@
+# API documentation
+
+::: antimeridian
=====================================
docs/api.rst deleted
=====================================
@@ -1,5 +0,0 @@
-API documentation
-=================
-
-.. automodule:: antimeridian
- :members:
=====================================
docs/cli.md
=====================================
@@ -0,0 +1,18 @@
+# Command line interface
+
+The **antimeridian** CLI (command line interface) takes a GeoJSON file path as
+its input, and outputs a fixed GeoJSON dictionary to standard output.
+
+## Installation
+
+Install with **pip**:
+
+```shell
+python -m pip install 'antimeridian[cli]'
+```
+
+## Usage
+
+::: mkdocs-click
+ :module: antimeridian._cli
+ :command: cli
=====================================
docs/cli.rst deleted
=====================================
@@ -1,21 +0,0 @@
-Command line interface
-======================
-
-The **antimeridian** :abbr:`CLI (command line interface)` takes a GeoJSON file
-path as its input, and outputs a fixed GeoJSON dictionary to standard output.
-
-Installation
-~~~~~~~~~~~~
-
-Install with **pip**:
-
-.. code-block:: shell
-
- pip install antimeridian[cli]
-
-Usage
-~~~~~
-
-.. click:: antimeridian._cli:cli
- :prog: antimeridian
- :nested: full
=====================================
docs/comparison.ipynb
=====================================
The diff for this file was not included because it is too large.
=====================================
docs/conf.py deleted
=====================================
@@ -1,29 +0,0 @@
-import importlib.metadata
-import os
-
-os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
-
-project = "antimeridian"
-copyright = "2023, Pete Gadomski"
-author = "Pete Gadomski"
-version = importlib.metadata.version("antimeridian")
-release = importlib.metadata.version("antimeridian")
-
-extensions = [
- "nbsphinx",
- "sphinx.ext.autodoc",
- "sphinx.ext.intersphinx",
- "sphinx.ext.napoleon",
- "sphinx_click",
-]
-
-templates_path = ["_templates"]
-exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "paper.*"]
-
-html_theme = "pydata_sphinx_theme"
-html_static_path = ["_static"]
-html_theme_options = {"github_url": "https://github.com/gadomski/antimeridian"}
-
-intersphinx_mapping = {"shapely": ("https://shapely.readthedocs.io/en/stable", None)}
-
-nbsphinx_custom_formats = {".md": "jupytext.reads"}
=====================================
docs/environment.yaml deleted
=====================================
@@ -1,17 +0,0 @@
-name: antimeridian-docs
-channels:
- - conda-forge
-dependencies:
- - cartopy~=0.21
- - geos~=3.11
- - ipykernel~=6.22
- - jupytext~=1.14
- - nbsphinx!=0.9
- - pandoc<3
- - pip~=22.3
- - pydata-sphinx-theme>=0.13
- - scipy~=1.10.0 # need to stay below 1.11 due to https://github.com/SciTools/cartopy/issues/2199
- - sphinx~=8.0
- - sphinx-click>=6.0
- - pip:
- - ..
=====================================
docs/examples.ipynb
=====================================
The diff for this file was not included because it is too large.
=====================================
docs/examples.md deleted
=====================================
@@ -1,101 +0,0 @@
----
-jupyter:
- jupytext:
- text_representation:
- extension: .md
- format_name: markdown
----
-
-# Examples
-
-Here's some examples of using the **antimeridian** package on some artificial and real-world data.
-
-## Test cases
-
-Our test suite exercises the antimeridian algorithm in a variety of ways.
-Here, we visualize some our test cases in two projections.
-
-```python
-import warnings
-import json
-from pathlib import Path
-from typing import Any, Dict, Optional
-
-import shapely.geometry
-from cartopy.crs import Mollweide, PlateCarree
-from cartopy.io import DownloadWarning
-from matplotlib import pyplot
-
-import antimeridian
-
-warnings.filterwarnings("ignore", category=DownloadWarning)
-
-
-def read_json(path: Path) -> Dict[str, Any]:
- with open(path) as f:
- return json.load(f)
-
-
-def plot(name: str, fix_winding: bool = True, suptitle: Optional[str] = None) -> None:
- data = read_json(f"../tests/data/input/{name}.json")
- input = shapely.geometry.shape(data)
- output = shapely.geometry.shape(
- antimeridian.fix_geojson(data, fix_winding=fix_winding)
- )
-
- if suptitle is None:
- suptitle = name
-
- figure = pyplot.figure()
- figure.suptitle(suptitle)
-
- axes = figure.add_subplot(2, 2, 1, projection=PlateCarree())
- axes.set_title("Original in PlateCarree")
- axes.stock_img()
- axes.coastlines()
- axes.add_geometries(input, crs=PlateCarree(), color="coral", alpha=0.7)
-
- axes = figure.add_subplot(2, 2, 2, projection=PlateCarree())
- axes.set_title("Fixed in PlateCarree")
- axes.stock_img()
- axes.coastlines()
- axes.add_geometries(output, crs=PlateCarree(), color="coral", alpha=0.7)
-
- axes = figure.add_subplot(2, 2, 3, projection=Mollweide(central_longitude=180))
- axes.set_title("Original in Mollweide")
- axes.stock_img()
- axes.coastlines()
- axes.add_geometries(input, crs=PlateCarree(), color="coral", alpha=0.7)
-
- axes = figure.add_subplot(2, 2, 4, projection=Mollweide(central_longitude=180))
- axes.set_title("Fixed in Mollweide")
- axes.stock_img()
- axes.coastlines()
- axes.add_geometries(output, crs=PlateCarree(), color="coral", alpha=0.7)
-
- pyplot.show()
-
-
-for name in [
- "split",
- "north-pole",
- "complex-split",
- "multi-split",
- "overlap",
-]:
- plot(name)
-```
-
-## Winding order
-
-The exterior ring of a GeoJSON Polygon _should_ be wound counter-clockwise.
-Taken literally, this means that a clockwise-wound polygon would be a hole in a large polygon that encloses both poles.
-However, in practice, it is common for polygons to be wound incorrectly (e.g. [#29](https://github.com/gadomski/antimeridian/issues/29) and [#32](https://github.com/gadomski/antimeridian/issues/32)).
-Our package therefore defaults to correcting the winding order to be counter-clockwise, and only creates these pole-encompassing polygons if explicitly asked.
-
-Most GIS software will automatically fix clockwise exterior polygons, and so it can be hard to know if your Polygon is wound correctly without inspecting the points themselves.
-
-```python
-plot("cw-split", fix_winding=True, suptitle="Fix winding")
-plot("cw-split", fix_winding=False, suptitle="No fix winding")
-```
=====================================
docs/failure-modes.ipynb
=====================================
The diff for this file was not included because it is too large.
=====================================
docs/failure-modes.md deleted
=====================================
@@ -1,135 +0,0 @@
-# Failure modes
-
-Our algorithm doesn't always work.
-It breaks down when a geometry's edge comes very close to a pole, which usually means the GeoJSON geometry is especially strange.
-Here's one real-world example from a [Sentinel 3](https://sentinels.copernicus.eu/web/sentinel/missions/sentinel-3) [STAC](https://stacspec.org/) Item:
-
-```python
-import json
-from pathlib import Path
-from typing import Any, Dict
-
-from cartopy.crs import RotatedPole, PlateCarree
-from matplotlib import pyplot
-
-
-def read_example(name: str) -> Dict[str, Any]:
- with open(Path("examples") / f"{name}.json") as f:
- return json.load(f)
-
-
-data = read_example("S3A_SL_2_LST_20160521T015552_20160521T033651_6059_004_217_____")
-x = list()
-y = list()
-for point in data["geometry"]["coordinates"][0]:
- x.append(point[0])
- y.append(point[1])
-
-rotated_pole = RotatedPole(pole_latitude=37.5, pole_longitude=177.5)
-plate_carree = PlateCarree()
-axes = pyplot.axes(projection=rotated_pole)
-axes.set_title("S3A_SL_2_LST_20160521T015552_20160521T033651_6059_004_217_____")
-axes.coastlines()
-axes.gridlines()
-axes.plot(x, y, marker="o", transform=plate_carree)
-axes.set_xlim(-90, 90)
-pyplot.show()
-```
-
-## What's an algorithm to do?
-
-Our normal algorithm can't handle this strange geometry:
-
-```python
-import antimeridian
-import shapely.geometry
-from cartopy.crs import Mollweide, PlateCarree
-from shapely.geometry import Polygon, MultiPolygon
-
-
-def plot(input: Polygon, output: Polygon | MultiPolygon, name: str) -> None:
- figure = pyplot.figure()
- figure.suptitle(name)
-
- axes = figure.add_subplot(2, 2, 1, projection=PlateCarree())
- axes.set_title("Original in PlateCarree")
- axes.stock_img()
- axes.coastlines()
- axes.add_geometries(input, crs=PlateCarree(), color="coral", alpha=0.7)
-
- axes = figure.add_subplot(2, 2, 2, projection=PlateCarree())
- axes.set_title("Fixed in PlateCarree")
- axes.stock_img()
- axes.coastlines()
- axes.add_geometries(output, crs=PlateCarree(), color="coral", alpha=0.7)
-
- axes = figure.add_subplot(2, 2, 3, projection=Mollweide(central_longitude=180))
- axes.set_title("Original in Mollweide")
- axes.stock_img()
- axes.coastlines()
- axes.add_geometries(input, crs=PlateCarree(), color="coral", alpha=0.7)
-
- axes = figure.add_subplot(2, 2, 4, projection=Mollweide(central_longitude=180))
- axes.set_title("Fixed in Mollweide")
- axes.stock_img()
- axes.coastlines()
- axes.add_geometries(output, crs=PlateCarree(), color="coral", alpha=0.7)
-
- pyplot.show()
-
-
-input = shapely.geometry.shape(data["geometry"])
-output = shapely.geometry.shape(antimeridian.fix_geojson(data["geometry"]))
-plot(input, output, "S3A_SL_2_LST_20160521T015552_20160521T033651_6059_004_217_____")
-```
-
-## Force the geometry over the north pole
-
-In some cases, we can use our priors to help the algorithm out.
-Here, we can force our fixer to extend the geometry over the north pole, which provides a better result:
-
-```python
-output = shapely.geometry.shape(
- antimeridian.fix_geojson(data["geometry"], force_north_pole=True)
-)
-plot(
- input,
- output,
- "S3A_SL_2_LST_20160521T015552_20160521T033651_6059_004_217_____\n(forced north pole)",
-)
-```
-
-## It doesn't always work
-
-Some geometries are so bad that even our north pole hack doesn't produce a valid result:
-
-```python
-data = read_example("S3A_SL_2_LST_20160521T033651_20160521T051750_6059_004_218_____")
-input = shapely.geometry.shape(data["geometry"])
-output = shapely.geometry.shape(
- antimeridian.fix_geojson(data["geometry"], force_north_pole=True)
-).buffer(0)
-plot(
- input,
- output,
- "S3A_SL_2_LST_20160521T033651_20160521T051750_6059_004_218_____\n(forced north pole)",
-)
-```
-
-In this case, you're better off not using the **antimeridian** package at all, but instead using [shapely's buffer(0) fixer](https://shapely.readthedocs.io/en/stable/manual.html#constructive-methods).
-You lose the pole coverage, but at least your geometry is close to what you want:
-
-```python
-input = shapely.geometry.shape(data["geometry"])
-output = input.buffer(0)
-plot(
- input,
- output,
- "S3A_SL_2_LST_20160521T033651_20160521T051750_6059_004_218_____\n(buffer(0))",
-)
-```
-
-## Conclusions
-
-It's a messy business, dealing with these improperly constructed GeoJSON geometries.
-There's probably not going to be one solution that works for every case.
=====================================
docs/img/complex-split.png
=====================================
Binary files a/docs/img/complex-split.png and b/docs/img/complex-split.png differ
=====================================
docs/index.rst → docs/index.md
=====================================
@@ -1,101 +1,79 @@
-antimeridian
-============
+# antimeridian
-A Python module for fixing geometries that cross the `antimeridian <https://en.wikipedia.org/wiki/180th_meridian>`_.
+A Python module for correcting geometries that cross the [antimeridian](https://en.wikipedia.org/wiki/180th_meridian).
-What's the antimeridian?
-~~~~~~~~~~~~~~~~~~~~~~~~
+## What's the antimeridian?
-Also known as the `180th meridian <https://en.wikipedia.org/wiki/180th_meridian>`_, the antimeridian is the line of longitude on the opposite side of the world from the prime meridian.
+Also known as the _180th meridian_, the antimeridian is the line of longitude on the opposite side of the world from the prime meridian.
It can be either 180° east or west.
-.. image:: https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/Earth_map_with_180th_meridian.jpg/640px-Earth_map_with_180th_meridian.jpg
+
-What's the problem?
-~~~~~~~~~~~~~~~~~~~
+## What's the problem?
-The GeoJSON specification `recommends cutting geometries at the antimeridian <https://www.rfc-editor.org/rfc/rfc7946#section-3.1.9>`_.
+The GeoJSON specification recommends [cutting geometries at the antimeridian](https://www.rfc-editor.org/rfc/rfc7946#section-3.1.9).
Many real-world geometries, however, don't follow this recommendation.
It's very common to create a geometry in a projected coordinate system, then reproject that geometry to WGS84 to use it in GeoJSON.
The reprojection process usually does not split the output geometry across the antimeridian, leading to invalid geometries.
-Here's a simple example, taken from a real-world `Landsat <https://landsat.gsfc.nasa.gov/>`_ `STAC <https://stacspec.org>`_ item:
+Here's a simple example, taken from a real-world [Landsat](https://landsat.gsfc.nasa.gov/) [STAC](https://stacspec.org) item:
-.. code-block:: json
-
- {
- "type": "Polygon",
- "coordinates": [
+```json
+{
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ -179.70358951407547,
+ 52.750507455036264
+ ],
+ [
+ 179.96672360880183,
+ 52.00163609753924
+ ],
+ [
+ -177.89334479610974,
+ 50.62805205289558
+ ],
+ [
+ -179.9847165338706,
+ 51.002602948712465
+ ],
[
- [
- -179.70358951407547,
- 52.750507455036264
- ],
- [
- 179.96672360880183,
- 52.00163609753924
- ],
- [
- -177.89334479610974,
- 50.62805205289558
- ],
- [
- -179.9847165338706,
- 51.002602948712465
- ],
- [
- -179.70358951407547,
- 52.750507455036264
- ]
+ -179.70358951407547,
+ 52.750507455036264
]
]
- }
+ ]
+}
+```
As you can see, one corner of the polygon crosses the antimeridian, leading to an invalid item:
-.. image:: img/landsat-problem.png
+
The issue also arises when geometries cross over a pole.
-How do we fix it?
-~~~~~~~~~~~~~~~~~
+## How do we fix it?
We use a relatively simple algorithm that splits the input polygon into segments.
Each segment is defined by jumps of greater than 180° longitude -- it's not a perfect heuristic, but tends to work for most real-world geometries we've encountered.
Segments are then joined along the antimeridian.
Segments that enclose the poles are constructed by adding points at the top of the antimeridian at both the east and the west longitudes.
-For more details, see :doc:`the-algorithm`.
+For more details, see [the-algorithm](./the-algorithm.md).
-Examples
-~~~~~~~~
+## Examples
Here's before and after pictures of some Sentinel 5p data.
These are swath data that enclose both poles.
In the before picture, you can see the strange artifacts created by the invalid geometry:
-.. image:: img/sentinel-5p-before.png
+
After correction, it's more clear that the data covers both poles:
-.. image:: img/sentinel-5p-after.png
+
Our library also handles splitting complex polygons that cross the antimeridian:
-.. image:: img/complex-split.png
-
-.. toctree::
- :maxdepth: 2
- :caption: Contents:
-
- the-algorithm
- examples
- failure-modes
- api
- cli
-
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`search`
+
=====================================
docs/make.bat deleted
=====================================
@@ -1,35 +0,0 @@
- at ECHO OFF
-
-pushd %~dp0
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
- set SPHINXBUILD=sphinx-build
-)
-set SOURCEDIR=.
-set BUILDDIR=_build
-
-%SPHINXBUILD% >NUL 2>NUL
-if errorlevel 9009 (
- echo.
- echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
- echo.installed, then set the SPHINXBUILD environment variable to point
- echo.to the full path of the 'sphinx-build' executable. Alternatively you
- echo.may add the Sphinx directory to PATH.
- echo.
- echo.If you don't have Sphinx installed, grab it from
- echo.https://www.sphinx-doc.org/
- exit /b 1
-)
-
-if "%1" == "" goto help
-
-%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-goto end
-
-:help
-%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
-
-:end
-popd
=====================================
docs/paper.bib
=====================================
@@ -1,6 +1,6 @@
@rfc{10.17487/RFC7946,
author = {Butler, H. and Daly, M. and Doyle, A. and Gillies, S. and Hagen, S. and Schaub, T.},
-title = {RFC 7946: The GeoJSON Format},
+title = {{RFC 7946: The GeoJSON Format}},
year = {2016},
publisher = {RFC Editor},
address = {USA},
@@ -9,7 +9,7 @@ abstract = {GeoJSON is a geospatial data interchange format based on JavaScript
@proceedings{alma992356353405961,
author = {Various},
-title = {International Conference Held at Washington for the Purpose of Fixing a Prime Meridian and a Universal Day. October, 1884. Protocols of the Proceedings},
+title = {{International Conference Held at Washington for the Purpose of Fixing a Prime Meridian and a Universal Day. October, 1884. Protocols of the Proceedings}},
year = {1884},
url = {https://www.gutenberg.org/files/17759/17759-h/17759-h.htm}
}
@@ -34,7 +34,7 @@ year = {2024}
@manual{Cartopy,
author = {{Met Office}},
-title = {Cartopy: a cartographic python library with a Matplotlib interface},
+title = {{Cartopy: a cartographic python library with a Matplotlib interface}},
year = {2010 - 2015},
address = {Exeter, Devon },
url = {https://scitools.org.uk/cartopy}
=====================================
docs/paper.md
=====================================
@@ -63,9 +63,11 @@ In addition to correcting GeoJSON geometries that cross the antimeridian, our li
- The **antimeridian** package relies on Shapely [@Gillies_Shapely_2024] for geometry validation, conversions, and other operations.
- We use Cartopy [@Cartopy] to generate visualizations for our documentation.
- This library has been ported to Go by another developer at [go-geospatial/antimeridian](https://pkg.go.dev/github.com/go-geospatial/antimeridian).
-- GDAL [@Rouault_GDAL_2024] can wrap shapes at the dateline ([`-wrapdateline`](https://gdal.org/en/latest/programs/ogr2ogr.html#cmdoption-ogr2ogr-wrapdateline)) but this functionality is significantly less feature-full than **antimeridian**.
- In many cases, the default usage of `-wrapdateline` does not correct the shape at all.
- We provide [test cases](https://github.com/gadomski/antimeridian/tree/e67e96dd2041575ee7cf481c7dce35b047a4c2e0/tests/data/ogr2ogr) to demonstrate the differences in output.
+- GDAL [@Rouault_GDAL_2024] can wrap shapes at the dateline ([`-wrapdateline`](https://gdal.org/en/latest/programs/ogr2ogr.html#cmdoption-ogr2ogr-wrapdateline)) and can produce similar outputs to **antimeridian** if tuned with the `-datelineoffset` flag.
+ We created a [notebook](https://www.gadom.ski/antimeridian/v0.3.11/comparison/) to compare the two, and found the following:
+ - In general, `antimeridian` and `ogr2ogr` perform the same, provided `ogr2ogr` is correctly tuned with the `-datelineoffset` flag.
+ - `antimeridian` outputs the same geometry type as the input, whereas `ogr2ogr` outputs a `FeatureCollection`.
+ - `antimeridian` has functionality to handle the poles.
## Acknowledgements
=====================================
docs/the-algorithm.rst → docs/the-algorithm.md
=====================================
@@ -1,58 +1,46 @@
-The algorithm
-=============
+# The algorithm
What follows is a walkthrough, with visual aids, of the algorithm underlying this package.
-Background
-~~~~~~~~~~
+## Background
Let's start with a simple GeoJSON geometry that doesn't come anywhere near the antimeridian.
For the sake of example, let's say it runs from 40° longitude west to 40° east, and from 20° latitude south to 20° north.
Its lower left point is at ``-40, -20``, and its upper right point is at ``40, 20``.
-.. figure:: img/box.png
- :scale: 30%
- :align: center
-
- A simple box.
+
Now let's consider the same geometry, but shifted east 180 degrees.
Its lower-left point is ``140, -20``, and its upper-right point is at ``220, 20``.
Here's its representation in GeoJSON:
-.. literalinclude:: examples/crossing.json
+```json
+--8<-- "docs/examples/crossing.json"
+```
And visualized:
-
-.. figure:: img/crossing.png
- :scale: 30%
- :align: center
-
- A box that crosses the antimeridian.
+
So far, good enough.
-However, the `bounds of WGS84 <https://epsg.io/4326>`_, the coordinate system used by GeoJSON, go from -180° to 180° longitude.
+However, the bounds of [WGS84](https://epsg.io/4326), the coordinate system used by GeoJSON, go from -180° to 180° longitude.
This means that the upper-right coordinate of our antimeridian-crossing box should be "wrapped" to ``-140, 20``:
-.. literalinclude:: examples/wrapped.json
- :diff: examples/crossing.json
+```json
+--8<-- "docs/examples/wrapped.json"
+```
Let's consider the four points of our "wrapped" box, and the order in which they're connected:
-.. code-block:: text
+```text
+ 1 2 3 4 1
- 1 2 3 4 1
-
- [140, -20] -> [-140, -20] -> [-140, 20] -> [140, 20] -> [140, 20]
+[140, -20] -> [-140, -20] -> [-140, 20] -> [140, 20] -> [140, 20]
+```
Exterior rings in GeoJSON should be wound counter-clockwise, but you can see that our antimeridian-crossing box appears to be wound clockwise.
-.. figure:: img/winding.png
- :scale: 30%
- :align: center
-
- The four points in our box, with their connections.
+
This leads to a confusing situation — because it's invalid GeoJSON!
You should not have a standalone clockwise ring.
@@ -60,120 +48,69 @@ When rendering this polygon, some systems automatically correct the winding orde
Another interpretation is that the shape is a hole, and the shell is the entire rest of the globe.
This is very rarely what you're actually trying to represent.
-.. figure:: img/winding-repercussions.png
- :scale: 30%
- :align: center
-
- How should we interpret this invalid GeoJSON?
+
Our algorithm can take this invalid GeoJSON as input and produce valid GeoJSON as output.
-Segmentation
-~~~~~~~~~~~~
+## Segmentation
To correct the invalid GeoJSON, we break our polygon into **segments**.
To create segments, we start at the first point of the geometry, and walk through the points until we find a longitude jump of more than 180° degrees.
-.. figure:: img/delta-gt-180.png
- :scale: 30%
- :align: center
-
- A longitude jump greater than 180°.
+
When we find such a jump, we split the polygon into segments by inserting two points, one on each antimeridian.
The first point ends the first segment, and the second point starts the second segment.
-.. figure:: img/segment.png
- :scale: 30%
- :align: center
-
- Creating two segments by adding points on the antimeridian.
+
We then continue walking through the points, applying the same procedure when other jumps of 180° longitude are found.
-.. figure:: img/another-delta.png
- :scale: 30%
- :align: center
-
- Another jump of more than 180° longitude.
+
We then finish walking the points.
-.. figure:: img/finish-segmentation.png
- :scale: 30%
- :align: center
-
- Finishing the segmentation.
+
Because the last point of a GeoJSON Polygon's coordinates is the same as the first point, we join the first and the last segments.
-.. figure:: img/joining-segments.png
- :scale: 30%
- :align: center
-
- Joining the first and last segments.
+
We now use our segments to build new polygons.
Let's consider the segment on the 180° antimeridian.
We take that segment's end point and search up the antimeridian for the first segment start point, joining those points to create a closed shape:
-.. figure:: img/creating-a-polygon.png
- :scale: 30%
- :align: center
-
- Searching up the antimeridian to create a polygon from a segment.
+
We use the same approach for the -180° antimeridian, but search down (towards the south pole) instead of up.
After the segments are joined, the individual Polygon are combined into a MultiPolygon.
-This conforms to the `GeoJSON specification <https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.9>`_, which expects antimeridian-crossing polygons to be split into multipolygons at the antimeridian.
-
-.. figure:: img/polygons.png
- :scale: 30%
- :align: center
+This conforms to the [GeoJSON specification](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.9), which expects antimeridian-crossing polygons to be split into multipolygons at the antimeridian.
- The output MutliPolygon.
+
-Complicated segmentation
-````````````````````````
+## Complicated segmentation
Note that this segmentation approach works even in more complicated cases, e.g. if there's a "hole" (like a donut over the antimeridian):
-.. figure:: img/donut.png
- :scale: 30%
- :align: center
-
- Creating a polygon for a "donut" over the antimeridian.
+
-The poles
-~~~~~~~~~
+## The poles
Geometries that overlap the north or the south pole present a related, but different, problem.
Here's a oval geometry that covers the north pole, as visualized from "above" the earth:
-.. figure:: img/pole.png
- :scale: 30%
- :align: center
-
- A polygon encircling the north pole.
+
That geometry would be represented as a set of points, all with positive latitudes and with longitudes covering more-or-less the entire -180° to 180° extent of our coordinate system.
Because the edge of our geometry does not touch the pole, there's no point that has a 90° latitude.
This means that the geometry never touches the north pole in a cartesian visualization, since the maximum latitude of the geometry is less than 90°.
-.. figure:: img/pole-as-points.png
- :scale: 30%
- :align: center
-
- The same polygon as points, visualized in a cartesian plot.
+
We use the same segmentation algorithm as described above, which in this case produces only a single segment.
-.. figure:: img/pole-segment.png
- :scale: 30%
- :align: center
-
- The single segment of our polar geometry.
+
When we try to close our polygon by searching "up" the 180° antimeridian from our end point, there's no start point to find.
So we create two new points, one on each antimeridian, and then continue the search down the other side.
@@ -181,32 +118,16 @@ Here's that drawn out, step-by-step.
First, we search up the antimeridian and find no points.
-.. figure:: img/pole-search-up.png
- :scale: 30%
- :align: center
-
- No points above the segment end.
+
We add two points, one at the top of each antimeridian, and add them to the segment.
-.. figure:: img/pole-add-points.png
- :scale: 30%
- :align: center
-
- Add two points and continue the segment.
+
Finally, we search down the 180° antimeridian until we find our segment start point.
-.. figure:: img/pole-enclosed.png
- :scale: 30%
- :align: center
-
- Add two points and continue the segment.
+
This produces a single valid GeoJSON polygon that encloses the pole in a cartesian visualization.
-.. figure:: img/pole-enclosed-polygon.png
- :scale: 30%
- :align: center
-
- The pole-enclosing polygon.
+
=====================================
mkdocs.yml
=====================================
@@ -0,0 +1,71 @@
+site_name: antimeridian
+site_url: https://gadomski.github.io/antimeridian/
+site_author: Pete Gadomski
+repo_name: gadomski/antimeridian
+repo_url: https://github.com/gadomski/antimeridian
+edit_uri: edit/main/docs/
+
+extra:
+ social:
+ - icon: "fontawesome/brands/github"
+ link: "https://github.com/gadomski"
+ version:
+ provider: mike
+
+theme:
+ name: material
+ logo: img/complex-split.png
+ favicon: img/complex-split.png
+ icon:
+ repo: fontawesome/brands/github
+ palette:
+ primary: deep orange
+ features:
+ - content.action.edit
+ - content.code.annotate
+ - content.code.copy
+ - navigation.footer
+ - navigation.indexes
+ - navigation.instant
+ - navigation.tracking
+ - search.share
+ - search.suggest
+
+nav:
+ - Home: index.md
+ - the-algorithm.md
+ - examples.ipynb
+ - failure-modes.ipynb
+ - comparison.ipynb
+ - api.md
+ - cli.md
+
+exclude_docs: paper.md
+
+watch:
+ - docs
+ - src
+
+plugins:
+ - search
+ - social
+ - mike
+ - mkdocs-jupyter
+ - mkdocstrings:
+ enable_inventory: true
+ handlers:
+ python:
+ options:
+ docstring_style: google
+ docstring_section_style: list
+ separate_signature: true
+ show_signature_annotations: true
+ show_symbol_type_toc: true
+ signature_crossrefs: true
+ import:
+ - https://shapely.readthedocs.io/en/stable/objects.inv
+
+markdown_extensions:
+ - pymdownx.snippets
+ - pymdownx.superfences
+ - mkdocs-click
=====================================
pyproject.toml
=====================================
@@ -1,6 +1,6 @@
[project]
name = "antimeridian"
-version = "0.3.11"
+version = "0.3.12"
authors = [{ name = "Pete Gadomski", email = "pete.gadomski at gmail.com" }]
description = "Fix GeoJSON geometries that cross the 180th meridian"
readme = "README.md"
@@ -51,12 +51,19 @@ lint.select = ["F", "E", "W", "I", "ERA", "RUF"]
dev-dependencies = [
"cartopy>=0.24.1",
"matplotlib>=3.9.2",
+ "mike>=2.1.3",
+ "mkdocs-click>=0.8.1",
+ "mkdocs-jupyter>=0.25.1",
+ "mkdocs-material[imaging]>=9.5.47",
+ "mkdocstrings[python]>=0.27.0",
"mypy>=1.2",
"packaging>=24.0",
"pre-commit>=4.0",
"pytest-console-scripts>=1.4",
"pytest>=8.0",
"ruff>=0.6.1",
+ "scipy>=1.14.1",
+ "shapely>=2.0.6",
]
[build-system]
=====================================
src/antimeridian/_implementation.py
=====================================
@@ -55,12 +55,12 @@ class FixWindingWarning(AntimeridianWarning):
class GeoInterface(Protocol):
- """A simple protocol for things that have a ``__geo_interface__`` method.
+ """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.
+ 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
@@ -78,8 +78,8 @@ def fix_geojson(
If the object does not cross the antimeridian, it is returned unchanged.
- See :py:func:`fix_polygon` for a description of the ``force_north_pole``,
- ``force_south_pole``, and ``fix_winding`` arguments.
+ 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
@@ -170,13 +170,13 @@ def fix_shape(
) -> Dict[str, Any]:
"""Fixes a shape that crosses the antimeridian.
- See :py:func:`fix_polygon` for a description of the ``force_north_pole``,
- ``force_south_pole``, and ``fix_winding`` arguments.
+ 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 :py:class:`GeoInterface`. Uses
- :py:func:`shapely.geometry.shape` under the hood.
+ 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
@@ -242,10 +242,10 @@ def fix_multi_polygon(
force_south_pole: bool = False,
fix_winding: bool = True,
) -> MultiPolygon:
- """Fixes a :py:class:`shapely.geometry.MultiPolygon`.
+ """Fixes a [shapely.MultiPolygon][].
- See :py:func:`fix_polygon` for a description of the ``force_north_pole``,
- ``force_south_pole``, and ``fix_winding`` arguments.
+ 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
@@ -277,22 +277,22 @@ def fix_polygon(
force_south_pole: bool = False,
fix_winding: bool = True,
) -> Union[Polygon, MultiPolygon]:
- """Fixes a :py:class:`shapely.geometry.Polygon`.
+ """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
+ 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
+ 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``.
+ If either `force_north_pole` or `force_south_pole` is `True`
+ `fix_winding` is set to `False`
Args:
polygon: The input polygon
@@ -329,7 +329,7 @@ def fix_polygon(
def fix_line_string(line_string: LineString) -> Union[LineString, MultiLineString]:
- """Fixes a :py:class:`shapely.geometry.LineString`.
+ """Fixes a [shapely.LineString][].
Args:
line_string: The input line string
@@ -346,7 +346,7 @@ def fix_line_string(line_string: LineString) -> Union[LineString, MultiLineStrin
def fix_multi_line_string(multi_line_string: MultiLineString) -> MultiLineString:
- """Fixes a :py:class:`shapely.geometry.MultiLineString`.
+ """Fixes a [shapely.MultiLineString][].
Args:
multi_line_string: The input multi line string
@@ -668,14 +668,14 @@ def bbox(
) -> 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
+ 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_antimeridan: Force the bounding box to be over the antimeridian.
+ force_over_antimeridian: Force the bounding box to be over the antimeridian.
Returns:
List[float]: The bounding box.
@@ -715,7 +715,7 @@ def bbox(
def centroid(shape: Dict[str, Any] | GeoInterface) -> Point:
"""Calculates the centroid for a polygon or multipolygon.
- Polygons are easy, we just use :py:func:`shapely.centroid`. For
+ 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).
=====================================
uv.lock
=====================================
The diff for this file was not included because it is too large.
View it on GitLab: https://salsa.debian.org/debian-gis-team/antimeridian/-/commit/15614f0fe4e5af3cf9158c7c52221592620d07a3
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/antimeridian/-/commit/15614f0fe4e5af3cf9158c7c52221592620d07a3
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20241212/35ab6578/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list