@@ -633,31 +633,27 @@ def check_catalog_file_name(self) -> bool:
633
633
else :
634
634
return True
635
635
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 .
638
638
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:
642
640
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
647
643
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() .
650
646
651
647
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 .
653
649
"""
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 :
655
651
return True
656
652
657
- geometry = self .data [ "geometry" ]
653
+ geometry = self .data . get ( "geometry" )
658
654
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 ):
661
657
if len (coord ) < 2 :
662
658
return True # Not enough elements to check
663
659
@@ -671,9 +667,49 @@ def is_valid_coordinate(coord):
671
667
if abs (lon ) > 180 :
672
668
return False
673
669
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
677
713
if abs (lon ) > 90 and abs (lat ) < 90 and abs (lon ) > abs (lat ) * 2 :
678
714
return False
679
715
@@ -684,14 +720,48 @@ def check_coordinates(coords):
684
720
if isinstance (coords , list ):
685
721
if coords and isinstance (coords [0 ], (int , float )):
686
722
# This is a single coordinate
687
- return is_valid_coordinate (coords )
723
+ return is_likely_correct_order (coords )
688
724
else :
689
725
# This is a list of coordinates or a list of lists of coordinates
690
726
return all (check_coordinates (coord ) for coord in coords )
691
727
return True
692
728
693
729
return check_coordinates (geometry .get ("coordinates" , []))
694
730
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
+
695
765
def create_best_practices_dict (self ) -> Dict :
696
766
"""Creates a dictionary of best practices violations for the current STAC object. The violations are determined
697
767
by a set of configurable linting rules specified in the config file.
@@ -857,9 +927,17 @@ def create_best_practices_dict(self) -> Dict:
857
927
not self .check_geometry_coordinates_order ()
858
928
and config ["geometry_coordinates_order" ] == True
859
929
):
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)"
861
931
best_practices_dict ["geometry_coordinates_order" ] = [msg_1 ]
862
932
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
+
863
941
# Check if a bbox that crosses the antimeridian is correctly formatted
864
942
if not self .check_bbox_antimeridian () and config .get (
865
943
"check_bbox_antimeridian" , True
@@ -881,43 +959,6 @@ def create_best_practices_dict(self) -> Dict:
881
959
882
960
return best_practices_dict
883
961
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
-
921
962
def create_best_practices_msg (self ) -> List [str ]:
922
963
"""
923
964
Generates a list of best practices messages based on the results of the 'create_best_practices_dict' method.
0 commit comments