@@ -450,10 +450,68 @@ def check_geometry_null(self) -> bool:
450450 bool: A boolean indicating whether the geometry property is null (True) or not (False).
451451 """
452452 if "geometry" in self .data :
453- return self .data [ "geometry" ] is None
453+ return self .data . get ( "geometry" ) is None
454454 else :
455455 return False
456456
457+ def check_bbox_matches_geometry (self ) -> bool :
458+ """Checks if the bbox of a STAC item matches its geometry.
459+
460+ This function verifies that the bounding box (bbox) accurately represents
461+ the minimum bounding rectangle of the item's geometry. It only applies to
462+ items with non-null geometry of type Polygon or MultiPolygon.
463+
464+ Returns:
465+ bool: True if the bbox matches the geometry or if the check is not applicable
466+ (e.g., null geometry or non-polygon type). False if there's a mismatch.
467+ """
468+ # Skip check if geometry is null or bbox is not present
469+ if (
470+ "geometry" not in self .data
471+ or self .data .get ("geometry" ) is None
472+ or "bbox" not in self .data
473+ or self .data .get ("bbox" ) is None
474+ ):
475+ return True
476+
477+ geometry = self .data .get ("geometry" )
478+ bbox = self .data .get ("bbox" )
479+
480+ # Only process Polygon and MultiPolygon geometries
481+ geom_type = geometry .get ("type" )
482+ if geom_type not in ["Polygon" , "MultiPolygon" ]:
483+ return True
484+
485+ # Extract coordinates based on geometry type
486+ coordinates = []
487+ if geom_type == "Polygon" :
488+ # For Polygon, use the exterior ring (first element)
489+ if len (geometry .get ("coordinates" , [])) > 0 :
490+ coordinates = geometry .get ("coordinates" )[0 ]
491+ elif geom_type == "MultiPolygon" :
492+ # For MultiPolygon, collect all coordinates from all polygons
493+ for polygon in geometry .get ("coordinates" , []):
494+ if len (polygon ) > 0 :
495+ coordinates .extend (polygon [0 ])
496+
497+ # If no valid coordinates, skip check
498+ if not coordinates :
499+ return True
500+
501+ # Calculate min/max from coordinates
502+ lons = [coord [0 ] for coord in coordinates ]
503+ lats = [coord [1 ] for coord in coordinates ]
504+
505+ calc_bbox = [min (lons ), min (lats ), max (lons ), max (lats )]
506+
507+ # Allow for small floating point differences (epsilon)
508+ epsilon = 1e-8
509+ for i in range (4 ):
510+ if abs (bbox [i ] - calc_bbox [i ]) > epsilon :
511+ return False
512+
513+ return True
514+
457515 def check_searchable_identifiers (self ) -> bool :
458516 """Checks if the identifiers of a STAC item are searchable, i.e.,
459517 they only contain lowercase letters, numbers, hyphens, and underscores.
@@ -616,6 +674,14 @@ def create_best_practices_dict(self) -> Dict:
616674 msg_1 = "All items should have a geometry field. STAC is not meant for non-spatial data"
617675 best_practices_dict ["null_geometry" ] = [msg_1 ]
618676
677+ # best practices - check if bbox matches geometry
678+ if (
679+ not self .check_bbox_matches_geometry ()
680+ and config .get ("check_bbox_geometry_match" , True ) == True
681+ ):
682+ msg_1 = "The bbox field does not match the bounds of the geometry. The bbox should be the minimum bounding rectangle of the geometry."
683+ best_practices_dict ["bbox_geometry_mismatch" ] = [msg_1 ]
684+
619685 # check to see if there are too many links
620686 if (
621687 self .check_bloated_links (max_links = max_links )
0 commit comments