Skip to content

Commit

Permalink
feat: add joss paper
Browse files Browse the repository at this point in the history
  • Loading branch information
gadomski committed Oct 11, 2024
1 parent db15389 commit 5eccea4
Show file tree
Hide file tree
Showing 13 changed files with 148 additions and 15 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
extras:
- "dev"
- "cli,dev"
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12"]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -42,7 +42,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.9"
python-version: "3.10"
- name: Insall uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install with development dependencies
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/joss.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: JOSS paper

on:
pull_request:
paths:
- docs/paper.*
push:
branches:
- main
paths:
- docs/paper.*

jobs:
paper:
runs-on: ubuntu-latest
name: JOSS paper
steps:
- uses: actions/checkout@v4
- uses: openjournals/openjournals-draft-action@master
with:
journal: joss
paper-path: docs/paper.md
- uses: actions/upload-artifact@v4
with:
name: paper
path: docs/paper.pdf
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.12"
- name: Install build
run: pip install build
- name: Build
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- 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.10.1
rev: v1.11.2
hooks:
- id: mypy
additional_dependencies:
- click~=8.1.6
- pytest~=8.0
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.5.1
rev: v0.6.9
hooks:
- id: ruff
types_or: [python, pyi, jupyter]
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
]

templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "paper.*"]

html_theme = "pydata_sphinx_theme"
html_static_path = ["_static"]
Expand Down
Binary file added docs/img/antimeridian.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions docs/paper.bib
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@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},
year = {2016},
publisher = {RFC Editor},
address = {USA},
abstract = {GeoJSON is a geospatial data interchange format based on JavaScript Object Notation (JSON). It defines several types of JSON objects and the manner in which they are combined to represent data about geographic features, their properties, and their spatial extents. GeoJSON uses a geographic coordinate reference system, World Geodetic System 1984, and units of decimal degrees.}
}

@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},
year = {1884},
url = {https://www.gutenberg.org/files/17759/17759-h/17759-h.htm}
}

@misc{STAC_Contributors_SpatioTemporal_Asset_Catalog_2024,
author = {{STAC Contributors}},
title = {{SpatioTemporal Asset Catalog (STAC) specification}},
url = {https://stacspec.org},
year = {2024}
}

@software{Gillies_Shapely_2024,
author = {Gillies, Sean and van der Wel, Casper and Van den Bossche, Joris and Taves, Mike W. and Arnott, Joshua and Ward, Brendan C. and {others}},
doi = {10.5281/zenodo.5597138},
license = {BSD-3-Clause},
month = aug,
title = {{Shapely}},
url = {https://github.com/shapely/shapely},
version = {2.0.6},
year = {2024}
}

@manual{Cartopy,
author = {{Met Office}},
title = {Cartopy: a cartographic python library with a Matplotlib interface},
year = {2010 - 2015},
address = {Exeter, Devon },
url = {https://scitools.org.uk/cartopy}
}
63 changes: 63 additions & 0 deletions docs/paper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: "antimeridian: A Python package for fixing geometries that cross the 180th meridian"
tags:
- Python
- geospatial
- antimeridian
authors:
- name: Peter Gadomski
orcid: 0000-0003-4877-7217
corresponding: true
affiliation: 1
- name: Preston Hartzell
orcid: 0000-0002-8293-3706
affiliation: 2
affiliations:
- name: Development Seed, USA
index: 1
- name: Element 84, Inc., USA
index: 2
date: 10 October 2024
bibliography: paper.bib
---

## Summary

Locations on and around planet Earth can be represented in a geodetic coordinate system by a latitude, a longitude, and a height.
Longitude, the "horizontal" dimension, covers the domain from -180° to 180° or 0° and 360°.
Where the two domain bounds meet is known as the _180th meridian_ or the _antimeridian_.

![Earth map centered on the Pacific ocean, with the 180th meridian highlighted.](./img/antimeridian.jpg)

The GeoJSON specification [@10.17487/RFC7946] describes how antimeridian-crossing shapes should be represented.
For a variety of reasons, real-world geometries often do not comply with the specification, leading to confusing and unrepresentable geometries.
Our **antimeridan** package provides Python functions for correcting improper geometries, as well as other related utilities.

## Statement of need

Due to a variety of factors, including the relative lack of populated settlements on the other side of the world, the Prime Meridian (0°) runs through Greenwich, England [@alma992356353405961].
Before the advent of satellite imagery, relatively few geospatial products crossed the 180th meridian, and so the problem of antimeridian-crossing geometries was usually avoidable.
The proliferation of satellite remote sensing products, including Earth Observation (EO) imagery, coupled with the ubiquity of interactive online maps, the antimeridian has become a feature that can appear on almost anyone's tablet, web portal, or desktop Geographic Information System (GIS) software.
There is a a need to create and fix antimeridian-crossing geometries at scale, e.g. for large SpatioTemporal Asset Catalog (STAC) [@STAC_Contributors_SpatioTemporal_Asset_Catalog_2024] catalogs that are used to search and discover petabytes of geospatial data.
When creating these catalogs, improper antimeridian-crossing geometries need to be corrected before ingesting to a data store to ensure that queries do not break and visualizations do not go haywire.
This is the problem for which **antimeridian** was designed.

To the best of our knowledge, the [algorithm](https://antimeridian.readthedocs.io/en/stable/the-algorithm.html) underlying **antimeridian** is a novel one.
Briefly, it breaks each polygon into segments and finds where a segments might cross the antimeridian.
It then breaks that segment at that crossing point and closes that segment along the antimeridian to create a new polygon.
This results in a multi polygon split on the antimeridian, as the GeoJSON specification requires.

![A complex shape split at the antimeridian](./img/complex-split.png)

The library also includes utilities for calculating centriods from antimeridian-crossing geometries and generating valid GeoJSON antimeridian-crossing bounding boxes.
It has been ported to Go by another developer at [go-geospatial/antimeridian](https://pkg.go.dev/github.com/go-geospatial/antimeridian).

## Key references

- 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.

# Acknowledgements

We acknowledge the financial and program support of the Planetary Computer team at Microsoft, particularly Rob Emanuele, Tom Augspurger, and Matt McFarland.
We would also like to acknowledge our employers, Development Seed and Element 84, who support open source software through direct funding and developer contribution time.
5 changes: 2 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@ version = "0.3.8"
authors = [{ name = "Pete Gadomski", email = "[email protected]" }]
description = "Fix GeoJSON geometries that cross the antimeridian"
readme = "README.md"
requires-python = ">=3.9"
requires-python = ">=3.10"
keywords = ["geojson", "antimeridian", "shapely"]
license = { text = "Apache-2.0" }
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Development Status :: 4 - Beta",
]
dependencies = ["numpy>=1.20.3", "shapely>=2.0"]
dependencies = ["numpy>=1.22.4", "shapely>=2.0"]

[project.urls]
Documentation = "https://antimeridian.readthedocs.io"
Expand Down
3 changes: 2 additions & 1 deletion src/antimeridian/_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from __future__ import annotations

import copy
import itertools
import warnings
from collections import namedtuple
from typing import Any, Dict, List, Optional, Protocol, Tuple, Union, cast
Expand Down Expand Up @@ -467,7 +468,7 @@ def normalize(coords: List[XY]) -> List[XY]:
def segment(coords: List[XY]) -> List[List[XY]]:
segment = []
segments = []
for start, end in zip(coords, coords[1:]):
for start, end in itertools.pairwise(coords):
segment.append(start)
if (end[0] - start[0] > 180) and (end[0] - start[0] != 360): # left
latitude = crossing_latitude(start, end)
Expand Down
3 changes: 2 additions & 1 deletion tests/test_bbox.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import List

import antimeridian
import pytest

import antimeridian

from .conftest import Reader


Expand Down
3 changes: 2 additions & 1 deletion tests/test_multi_polygon.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import antimeridian
import pytest
import shapely.geometry
from shapely.geometry import MultiPolygon

import antimeridian

from .conftest import Reader


Expand Down
5 changes: 3 additions & 2 deletions tests/test_polygon.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from typing import cast

import antimeridian
import pytest
import shapely.affinity
import shapely.geometry
from antimeridian import FixWindingWarning
from shapely.geometry import MultiPolygon, Point, Polygon

import antimeridian
from antimeridian import FixWindingWarning

from .conftest import Reader


Expand Down

0 comments on commit 5eccea4

Please sign in to comment.