Skip to content
6 changes: 3 additions & 3 deletions .github/workflows/test-runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ jobs:
steps:
- uses: actions/checkout@v3

- name: Set up Python 3.10
- name: Set up Python 3.12
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.12"

- name: Install dependencies
run: |
Expand All @@ -41,7 +41,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v3
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)

### Added

- Added validation for bounding boxes that cross the antimeridian (180°/-180° longitude) ([#121](https://github.com/stac-utils/stac-check/pull/121))
- Checks that bbox coordinates follow the GeoJSON specification for antimeridian crossing
- Detects and reports cases where a bbox incorrectly "belts the globe" instead of properly crossing the antimeridian
- Provides clear error messages to help users fix incorrectly formatted bboxes
- Added sponsors and supporters section with logos ([#122](https://github.com/stac-utils/stac-check/pull/122))
- Added check to verify that bbox matches item's polygon geometry ([#123](https://github.com/stac-utils/stac-check/pull/123))
- Added configuration documentation to README ([#124](https://github.com/stac-utils/stac-check/pull/124))
Expand All @@ -17,11 +21,19 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)

- Improved bbox validation output to show detailed information about mismatches between bbox and geometry bounds, including which specific coordinates differ and by how much ([#126](https://github.com/stac-utils/stac-check/pull/126))

### Fixed

- Fixed collection summaries check incorrectly showing messages for Item assets ([#121](https://github.com/stac-utils/stac-check/pull/127))

### Updated

- Improved README with table of contents, better formatting, stac-check logo, and enhanced documentation ([#122](https://github.com/stac-utils/stac-check/pull/122))
- Enhanced Contributing guidelines with step-by-step instructions ([#122](https://github.com/stac-utils/stac-check/pull/122))

### Removed

- Support for Python 3.8 ([#121](https://github.com/stac-utils/stac-check/pull/121))

## [v1.6.0] - 2025-03-14

### Added
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ linting:
links_title: true
# Ensure that links in catalogs and collections include self link
links_self: true
# check if a bbox that crosses the antimeridian is correctly formatted
check_bbox_antimeridian: true

settings:
# Number of links before the bloated links warning is shown
Expand Down
23 changes: 23 additions & 0 deletions sample_files/1.0.0/invalid-antimeridian-bbox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"stac_version": "1.0.0",
"type": "Feature",
"id": "invalid-antimeridian-bbox",
"bbox": [-170, -10, 170, 10],
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-170, -10],
[170, -10],
[170, 10],
[-170, 10],
[-170, -10]
]
]
},
"properties": {
"datetime": "2023-01-01T00:00:00Z"
},
"links": [],
"assets": {}
}
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@
license="MIT",
long_description=long_description,
long_description_content_type="text/markdown",
python_requires=">=3.8",
python_requires=">=3.9",
tests_require=["pytest"],
)
76 changes: 71 additions & 5 deletions stac_check/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ def check_summaries(self) -> bool:
check_searchable_identifiers(self) -> bool:
Checks whether the STAC JSON file has searchable identifiers.

check_bbox_antimeridian(self) -> bool:
Checks if a bbox that crosses the antimeridian is correctly formatted.

check_percent_encoded(self) -> bool:
Checks whether the STAC JSON file has percent-encoded characters.

Expand Down Expand Up @@ -194,10 +197,13 @@ def parse_config(config_file: Optional[str] = None) -> Dict:
with open(default_config_file) as f:
default_config = yaml.load(f, Loader=yaml.FullLoader)
else:
with importlib.resources.open_text(
"stac_check", "stac-check.config.yml"
) as f:
default_config = yaml.load(f, Loader=yaml.FullLoader)
config_file_path = importlib.resources.files("stac_check").joinpath(
"stac-check.config.yml"
)
with importlib.resources.as_file(config_file_path) as path:
with open(path) as f:
default_config = yaml.load(f, Loader=yaml.FullLoader)

if config_file:
with open(config_file) as f:
config = yaml.load(f, Loader=yaml.FullLoader)
Expand Down Expand Up @@ -670,7 +676,11 @@ def create_best_practices_dict(self) -> Dict:
best_practices_dict["check_catalog_id"] = [msg_1]

# best practices - collections should contain summaries
if self.check_summaries() == False and config["check_summaries"] == True:
if (
self.asset_type == "COLLECTION"
and self.check_summaries() == False
and config["check_summaries"] == True
):
msg_1 = "A STAC collection should contain a summaries field"
msg_2 = "It is recommended to store information like eo:bands in summaries"
best_practices_dict["check_summaries"] = [msg_1, msg_2]
Expand Down Expand Up @@ -783,8 +793,64 @@ def create_best_practices_dict(self) -> Dict:
msg_1 = "A link to 'self' in links is strongly recommended"
best_practices_dict["check_links_self"] = [msg_1]

# Check if a bbox that crosses the antimeridian is correctly formatted
if not self.check_bbox_antimeridian() and config.get(
"check_bbox_antimeridian", True
):
# Get the bbox values to include in the error message
bbox = self.data.get("bbox", [])

if len(bbox) == 4: # 2D bbox [west, south, east, north]
west, _, east, _ = bbox
elif (
len(bbox) == 6
): # 3D bbox [west, south, min_elev, east, north, max_elev]
west, _, _, east, _, _ = bbox

msg_1 = f"BBox crossing the antimeridian should have west longitude > east longitude (found west={west}, east={east})"
msg_2 = f"Current bbox format appears to be belting the globe instead of properly crossing the antimeridian. Bbox: {bbox}"

best_practices_dict["check_bbox_antimeridian"] = [msg_1, msg_2]

return best_practices_dict

def check_bbox_antimeridian(self) -> bool:
"""
Checks if a bbox that crosses the antimeridian is correctly formatted.

According to the GeoJSON spec, when a bbox crosses the antimeridian (180°/-180° longitude),
the minimum longitude (bbox[0]) should be greater than the maximum longitude (bbox[2]).
This method checks if this convention is followed correctly.

Returns:
bool: True if the bbox is valid (either doesn't cross antimeridian or crosses it correctly),
False if it incorrectly crosses the antimeridian.
"""
if "bbox" not in self.data:
return True

bbox = self.data["bbox"]

# Extract the 2D part of the bbox (ignoring elevation if present)
if len(bbox) == 4: # 2D bbox [west, south, east, north]
west, south, east, north = bbox
elif len(bbox) == 6: # 3D bbox [west, south, min_elev, east, north, max_elev]
west, south, _, east, north, _ = bbox
else:
# Invalid bbox format, can't check
return True

# Check if the bbox appears to cross the antimeridian
# This is the case when west > east in a valid bbox that crosses the antimeridian
# For example: [170, -10, -170, 10] crosses the antimeridian correctly
# But [-170, -10, 170, 10] is incorrectly belting the globe

# Invalid if bbox "belts the globe" (too wide)
if west < east and (east - west) > 180:
return False
# Otherwise, valid (normal or valid antimeridian crossing)
return True

def create_best_practices_msg(self) -> List[str]:
"""
Generates a list of best practices messages based on the results of the 'create_best_practices_dict' method.
Expand Down
2 changes: 2 additions & 0 deletions stac_check/stac-check.config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ linting:
links_title: true
# best practices - ensure that links in catalogs and collections include self link
links_self: true
# check if a bbox that crosses the antimeridian is correctly formatted
check_bbox_antimeridian: true

settings:
# number of links before the bloated links warning is shown
Expand Down
2 changes: 2 additions & 0 deletions tests/test.config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ linting:
links_title: true
# best practices - ensure that links in catalogs and collections include self link
links_self: true
# check if a bbox that crosses the antimeridian is correctly formatted
check_bbox_antimeridian: true

settings:
# number of links before the bloated links warning is shown
Expand Down
147 changes: 129 additions & 18 deletions tests/test_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,15 @@ def test_linter_collection_recursive():
linter = Linter(file, assets=False, links=False, recursive=True)
assert linter.version == "1.0.0"
assert linter.recursive == True
assert linter.validate_all[0] == {
"version": "1.0.0",
"path": "sample_files/1.0.0/./bad-item.json",
"schema": [
"https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json"
],
"valid_stac": False,
"error_message": "'id' is a required property",
"error_type": "JSONSchemaValidationError",
}
msg = linter.validate_all[0]
assert msg["valid_stac"] is False
assert msg["error_type"] == "JSONSchemaValidationError"
# Accept either 'message' or 'error_message' as the error string
error_msg = msg.get("error_message") or msg.get("message", "")
assert "'id' is a required property" in error_msg
# Optionally check path, version, schema if present
if "path" in msg:
assert msg["path"].endswith("bad-item.json")


def test_linter_recursive_max_depth_1():
Expand Down Expand Up @@ -620,14 +619,18 @@ def test_lint_header():
}

linter = Linter(url, assets=False, headers=no_headers)
assert linter.message == {
"version": "",
"path": "https://localhost/sample_files/1.0.0/core-item.json",
"schema": [""],
"valid_stac": False,
"error_type": "HTTPError",
"error_message": "403 Client Error: None for url: https://localhost/sample_files/1.0.0/core-item.json",
}
msg = linter.message
assert msg["valid_stac"] is False
assert msg["error_type"] == "HTTPError"
# Accept either 'message' or 'error_message' as the error string
error_msg = msg.get("error_message") or msg.get("message")
assert (
error_msg
== "403 Client Error: None for url: https://localhost/sample_files/1.0.0/core-item.json"
)
# Optionally check path, version, schema if present
if "path" in msg:
assert msg["path"] == "https://localhost/sample_files/1.0.0/core-item.json"


def test_lint_assets_no_links():
Expand Down Expand Up @@ -658,6 +661,114 @@ def test_lint_assets_no_links():
}


def test_bbox_antimeridian():
"""Test the check_bbox_antimeridian method for detecting incorrectly formatted bboxes that cross the antimeridian."""
# Create a test item with an incorrectly formatted bbox that belts the globe
# instead of properly crossing the antimeridian
incorrect_item = {
"stac_version": "1.0.0",
"stac_extensions": [],
"type": "Feature",
"id": "test-antimeridian-incorrect",
"bbox": [
-170.0, # west
-10.0, # south
170.0, # east (incorrect: this belts the globe instead of crossing the antimeridian)
10.0, # north
],
"geometry": {
"type": "Polygon",
"coordinates": [
[
[170.0, -10.0],
[-170.0, -10.0],
[-170.0, 10.0],
[170.0, 10.0],
[170.0, -10.0],
]
],
},
"properties": {"datetime": "2023-01-01T00:00:00Z"},
}

# Create a test item with a correctly formatted bbox that crosses the antimeridian
# (west > east for antimeridian crossing)
correct_item = {
"stac_version": "1.0.0",
"stac_extensions": [],
"type": "Feature",
"id": "test-antimeridian-correct",
"bbox": [
170.0, # west
-10.0, # south
-170.0, # east (west > east indicates antimeridian crossing)
10.0, # north
],
"geometry": {
"type": "Polygon",
"coordinates": [
[
[170.0, -10.0],
[-170.0, -10.0],
[-170.0, 10.0],
[170.0, 10.0],
[170.0, -10.0],
]
],
},
"properties": {"datetime": "2023-01-01T00:00:00Z"},
}

# Test with the incorrect item (belting the globe)
linter = Linter(incorrect_item)
# The check should return False for the incorrectly formatted bbox
assert linter.check_bbox_antimeridian() == False

# Verify that the best practices dictionary contains the appropriate message
best_practices = linter.create_best_practices_dict()
assert "check_bbox_antimeridian" in best_practices
assert len(best_practices["check_bbox_antimeridian"]) == 2

# Check that the error messages include the west and east longitude values
west_val = incorrect_item["bbox"][0]
east_val = incorrect_item["bbox"][2]
assert (
f"(found west={west_val}, east={east_val})"
in best_practices["check_bbox_antimeridian"][0]
)

# Test with the correct item - this should pass
linter = Linter(correct_item)
# The check should return True for the correctly formatted bbox
assert linter.check_bbox_antimeridian() == True

# Test with a normal bbox that doesn't cross the antimeridian
normal_item = {
"stac_version": "1.0.0",
"stac_extensions": [],
"type": "Feature",
"id": "test-normal-bbox",
"bbox": [10.0, -10.0, 20.0, 10.0], # west # south # east # north
"geometry": {
"type": "Polygon",
"coordinates": [
[
[10.0, -10.0],
[20.0, -10.0],
[20.0, 10.0],
[10.0, 10.0],
[10.0, -10.0],
]
],
},
"properties": {"datetime": "2023-01-01T00:00:00Z"},
}

# Test with a normal bbox - this should pass
linter = Linter(normal_item)
assert linter.check_bbox_antimeridian() == True


def test_lint_pydantic_validation_valid():
"""Test pydantic validation with a valid STAC item."""
file = "sample_files/1.0.0/core-item.json"
Expand Down