Skip to content

Commit f241681

Browse files
committed
definite error check
1 parent b6fd37f commit f241681

File tree

2 files changed

+139
-69
lines changed

2 files changed

+139
-69
lines changed

stac_check/lint.py

Lines changed: 99 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -633,31 +633,27 @@ def check_catalog_file_name(self) -> bool:
633633
else:
634634
return True
635635

636-
def check_geometry_coordinates_order(self) -> bool:
637-
"""Checks if the coordinates in a geometry may be in the incorrect order.
636+
def check_geometry_coordinates_definite_errors(self) -> bool:
637+
"""Checks if the coordinates in a geometry contain definite errors.
638638
639-
This function attempts to detect cases where coordinates might not follow the GeoJSON
640-
specification where positions should be in [longitude, latitude] order. It uses several
641-
heuristics to identify potentially problematic coordinates:
639+
This function checks for coordinates that definitely violate the GeoJSON specification:
642640
643-
1. Checks if latitude values (second element) exceed ±90 degrees
644-
2. Checks if longitude values (first element) exceed ±180 degrees
645-
3. Uses a heuristic to detect when coordinates are likely reversed
646-
(when first value > 90, second value < 90, and first value > second value*2)
641+
1. Latitude values (second element) exceed ±90 degrees
642+
2. Longitude values (first element) exceed ±180 degrees
647643
648-
Note that this check can never definitively determine if coordinates are reversed
649-
or simply contain errors, it can only flag suspicious patterns.
644+
This check focuses on definite errors rather than potential/likely errors.
645+
For checking potential errors (likely reversed coordinates), use check_geometry_coordinates_order().
650646
651647
Returns:
652-
bool: True if coordinates appear to be in the expected order, False if they may be reversed.
648+
bool: True if coordinates are within valid ranges, False if they contain definite errors.
653649
"""
654-
if "geometry" not in self.data or self.data["geometry"] is None:
650+
if "geometry" not in self.data or self.data.get("geometry") is None:
655651
return True
656652

657-
geometry = self.data["geometry"]
653+
geometry = self.data.get("geometry")
658654

659-
# Function to check a single coordinate pair
660-
def is_valid_coordinate(coord):
655+
# Function to check a single coordinate pair for definite errors
656+
def is_within_valid_ranges(coord):
661657
if len(coord) < 2:
662658
return True # Not enough elements to check
663659

@@ -671,9 +667,49 @@ def is_valid_coordinate(coord):
671667
if abs(lon) > 180:
672668
return False
673669

674-
# Additional heuristic for likely reversed coordinates
675-
# If the first value (supposed longitude) is > 90, second value (supposed latitude) is < 90,
676-
# and first value is significantly larger than second value, they may be reversed
670+
return True
671+
672+
# Function to recursively check all coordinates in a geometry
673+
def check_coordinates(coords):
674+
if isinstance(coords, list):
675+
if coords and isinstance(coords[0], (int, float)):
676+
# This is a single coordinate
677+
return is_within_valid_ranges(coords)
678+
else:
679+
# This is a list of coordinates or a list of lists of coordinates
680+
return all(check_coordinates(coord) for coord in coords)
681+
return True
682+
683+
return check_coordinates(geometry.get("coordinates", []))
684+
685+
def check_geometry_coordinates_order(self) -> bool:
686+
"""Checks if the coordinates in a geometry may be in the incorrect order.
687+
688+
This function uses a heuristic to detect coordinates that are likely in the wrong order
689+
(latitude, longitude instead of longitude, latitude). It looks for cases where:
690+
- The first value (supposed to be longitude) is > 90 degrees
691+
- The second value (supposed to be latitude) is < 90 degrees
692+
- The first value is more than twice the second value
693+
694+
For checking definite errors (values outside valid ranges), use check_geometry_coordinates_definite_errors().
695+
696+
Returns:
697+
bool: True if coordinates appear to be in the correct order, False if they may be reversed.
698+
"""
699+
if "geometry" not in self.data or self.data.get("geometry") is None:
700+
return True
701+
702+
geometry = self.data.get("geometry")
703+
704+
# Function to check if a single coordinate pair is likely in the correct order
705+
def is_likely_correct_order(coord):
706+
if len(coord) < 2:
707+
return True # Not enough elements to check
708+
709+
lon, lat = coord[0], coord[1]
710+
711+
# Heuristic: If the supposed longitude is > 90 and the supposed latitude is < 90,
712+
# and the longitude is more than twice the latitude, it's likely in the correct order
677713
if abs(lon) > 90 and abs(lat) < 90 and abs(lon) > abs(lat) * 2:
678714
return False
679715

@@ -684,14 +720,48 @@ def check_coordinates(coords):
684720
if isinstance(coords, list):
685721
if coords and isinstance(coords[0], (int, float)):
686722
# This is a single coordinate
687-
return is_valid_coordinate(coords)
723+
return is_likely_correct_order(coords)
688724
else:
689725
# This is a list of coordinates or a list of lists of coordinates
690726
return all(check_coordinates(coord) for coord in coords)
691727
return True
692728

693729
return check_coordinates(geometry.get("coordinates", []))
694730

731+
def check_bbox_antimeridian(self) -> bool:
732+
"""
733+
Checks if a bbox that crosses the antimeridian is correctly formatted.
734+
735+
According to the GeoJSON spec, when a bbox crosses the antimeridian (180°/-180° longitude),
736+
the minimum longitude (bbox[0]) should be greater than the maximum longitude (bbox[2]).
737+
This method checks if this convention is followed correctly.
738+
739+
Returns:
740+
bool: True if the bbox is valid (either doesn't cross antimeridian or crosses it correctly),
741+
False if it incorrectly crosses the antimeridian.
742+
"""
743+
if "bbox" not in self.data:
744+
return True
745+
746+
bbox = self.data.get("bbox")
747+
748+
# Extract the 2D part of the bbox (ignoring elevation if present)
749+
if len(bbox) == 4: # 2D bbox [west, south, east, north]
750+
west, _, east, _ = bbox
751+
elif len(bbox) == 6: # 3D bbox [west, south, min_elev, east, north, max_elev]
752+
west, _, _, east, _, _ = bbox
753+
754+
# Check if the bbox appears to cross the antimeridian
755+
# This is the case when west > east in a valid bbox that crosses the antimeridian
756+
# For example: [170, -10, -170, 10] crosses the antimeridian correctly
757+
# But [-170, -10, 170, 10] is incorrectly belting the globe
758+
759+
# Invalid if bbox "belts the globe" (too wide)
760+
if west < east and (east - west) > 180:
761+
return False
762+
# Otherwise, valid (normal or valid antimeridian crossing)
763+
return True
764+
695765
def create_best_practices_dict(self) -> Dict:
696766
"""Creates a dictionary of best practices violations for the current STAC object. The violations are determined
697767
by a set of configurable linting rules specified in the config file.
@@ -857,9 +927,17 @@ def create_best_practices_dict(self) -> Dict:
857927
not self.check_geometry_coordinates_order()
858928
and config["geometry_coordinates_order"] == True
859929
):
860-
msg_1 = "Geometry coordinates may be reversed or contain errors (expected order: longitude, latitude)"
930+
msg_1 = "Geometry coordinates may be in the wrong order (required order: longitude, latitude)"
861931
best_practices_dict["geometry_coordinates_order"] = [msg_1]
862932

933+
# best practices - check if geometry coordinates contain definite errors
934+
if (
935+
not self.check_geometry_coordinates_definite_errors()
936+
and config["geometry_coordinates_order"] == True
937+
):
938+
msg_1 = "Geometry coordinates contain invalid values that violate the GeoJSON specification (latitude must be between -90 and 90, longitude between -180 and 180)"
939+
best_practices_dict["geometry_coordinates_definite_errors"] = [msg_1]
940+
863941
# Check if a bbox that crosses the antimeridian is correctly formatted
864942
if not self.check_bbox_antimeridian() and config.get(
865943
"check_bbox_antimeridian", True
@@ -881,43 +959,6 @@ def create_best_practices_dict(self) -> Dict:
881959

882960
return best_practices_dict
883961

884-
def check_bbox_antimeridian(self) -> bool:
885-
"""
886-
Checks if a bbox that crosses the antimeridian is correctly formatted.
887-
888-
According to the GeoJSON spec, when a bbox crosses the antimeridian (180°/-180° longitude),
889-
the minimum longitude (bbox[0]) should be greater than the maximum longitude (bbox[2]).
890-
This method checks if this convention is followed correctly.
891-
892-
Returns:
893-
bool: True if the bbox is valid (either doesn't cross antimeridian or crosses it correctly),
894-
False if it incorrectly crosses the antimeridian.
895-
"""
896-
if "bbox" not in self.data:
897-
return True
898-
899-
bbox = self.data["bbox"]
900-
901-
# Extract the 2D part of the bbox (ignoring elevation if present)
902-
if len(bbox) == 4: # 2D bbox [west, south, east, north]
903-
west, south, east, north = bbox
904-
elif len(bbox) == 6: # 3D bbox [west, south, min_elev, east, north, max_elev]
905-
west, south, _, east, north, _ = bbox
906-
else:
907-
# Invalid bbox format, can't check
908-
return True
909-
910-
# Check if the bbox appears to cross the antimeridian
911-
# This is the case when west > east in a valid bbox that crosses the antimeridian
912-
# For example: [170, -10, -170, 10] crosses the antimeridian correctly
913-
# But [-170, -10, 170, 10] is incorrectly belting the globe
914-
915-
# Invalid if bbox "belts the globe" (too wide)
916-
if west < east and (east - west) > 180:
917-
return False
918-
# Otherwise, valid (normal or valid antimeridian crossing)
919-
return True
920-
921962
def create_best_practices_msg(self) -> List[str]:
922963
"""
923964
Generates a list of best practices messages based on the results of the 'create_best_practices_dict' method.

tests/test_lint.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ def test_lint_assets_no_links():
662662

663663

664664
def test_geometry_coordinates_order():
665-
"""Test the check_geometry_coordinates_order method for detecting incorrectly ordered coordinates."""
665+
"""Test the check_geometry_coordinates_order method for detecting potentially incorrectly ordered coordinates."""
666666
# Create a test item with coordinates in the correct order (longitude, latitude)
667667
correct_item = {
668668
"stac_version": "1.0.0",
@@ -753,31 +753,60 @@ def test_geometry_coordinates_order():
753753
"properties": {"datetime": "2023-01-01T00:00:00Z"},
754754
}
755755

756-
# Test with correct coordinates - this should pass
756+
# Test with correct coordinates - this should pass both checks
757757
linter = Linter(correct_item)
758758
assert linter.check_geometry_coordinates_order() == True
759+
assert linter.check_geometry_coordinates_definite_errors() == True
759760

760761
# Test with reversed coordinates that are within valid ranges
761-
# Current implementation can't detect this case, so the test passes
762+
# Current implementation can't detect this case, so both checks pass
762763
linter = Linter(undetectable_reversed_item)
763764
assert (
764765
linter.check_geometry_coordinates_order() == True
765766
) # Passes because values are within valid ranges
767+
assert (
768+
linter.check_geometry_coordinates_definite_errors() == True
769+
) # Passes because values are within valid ranges
766770

767-
# Test with clearly incorrect coordinates - this should fail
771+
# Test with clearly incorrect coordinates (latitude > 90)
772+
# This should fail the definite errors check but pass the order check (which now only uses heuristic)
768773
linter = Linter(clearly_incorrect_item)
769-
assert linter.check_geometry_coordinates_order() == False
774+
assert (
775+
linter.check_geometry_coordinates_order() == True
776+
) # Now passes because it only checks heuristic
777+
assert (
778+
linter.check_geometry_coordinates_definite_errors() == False
779+
) # Fails because latitude > 90
770780

771-
# Test with coordinates that trigger the heuristic - this should fail
781+
# Test with coordinates that trigger the heuristic
782+
# This should fail the order check but pass the definite errors check
772783
linter = Linter(heuristic_incorrect_item)
773-
assert linter.check_geometry_coordinates_order() == False
784+
assert (
785+
linter.check_geometry_coordinates_order() == False
786+
) # Fails because of heuristic
787+
assert (
788+
linter.check_geometry_coordinates_definite_errors() == True
789+
) # Passes because values are within valid ranges
774790

775-
# Test that the best practices dictionary contains the error message
791+
# Test that the best practices dictionary contains the appropriate error messages
792+
best_practices = linter.create_best_practices_dict()
793+
794+
# For heuristic-based detection
795+
linter = Linter(heuristic_incorrect_item)
776796
best_practices = linter.create_best_practices_dict()
777797
assert "geometry_coordinates_order" in best_practices
778-
assert best_practices["geometry_coordinates_order"] == [
779-
"Geometry coordinates may be reversed or contain errors (expected order: longitude, latitude)"
780-
]
798+
assert (
799+
"may be in the wrong order" in best_practices["geometry_coordinates_order"][0]
800+
)
801+
802+
# For definite errors detection
803+
linter = Linter(clearly_incorrect_item)
804+
best_practices = linter.create_best_practices_dict()
805+
assert "geometry_coordinates_definite_errors" in best_practices
806+
assert (
807+
"contain invalid values"
808+
in best_practices["geometry_coordinates_definite_errors"][0]
809+
)
781810

782811

783812
def test_bbox_antimeridian():

0 commit comments

Comments
 (0)