Skip to content

Commit 4bb7dea

Browse files
Fix: Add error handling for missing 'tiles' key in stac_tile response (#1282)
* Fix: Prevent titiler_endpoint from being overwritten in stac_tile When stac_stats fails for Planetary Computer items, it was overwriting the titiler_endpoint variable with a fallback string endpoint. This caused the subsequent tile request to use the wrong endpoint path (/stac/ instead of /item/), which expects different parameters and returns errors. Changes: 1. Use a separate fallback_endpoint variable for stac_stats fallback 2. Add error handling to check for 'tiles' key before accessing it 3. Provide informative error messages based on API response type Fixes opengeos/geoai#468 * Fix stac_bounds to handle GeoJSON Feature format from Planetary Computer The Planetary Computer /item/info.geojson endpoint returns a GeoJSON Feature with bounds nested in properties.{asset}.bounds, not a top-level bbox field. This fix: - Checks for bounds in properties.{asset}.bounds format - Falls back to calculating bbox from geometry coordinates - Ensures map zooms to correct location when visualizing NAIP items Fixes opengeos/geoai#468 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Silently handle stac_stats failures instead of printing errors When stac_stats fails (e.g., for Planetary Computer items that don't support the statistics endpoint), the errors were being printed to stdout, cluttering the output. This fix: - Wraps both primary and fallback stac_stats calls in try-except - Silently continues without rescaling if stats are unavailable - Removes the print(stats['detail']) statement - Sets stats=None if both attempts fail The tile visualization works fine without stats - they're only used for automatic rescaling, which isn't critical for display. Fixes opengeos/geoai#468 * Fix check_titiler_endpoint to handle endpoint objects properly The real bug: check_titiler_endpoint was calling .lower() on the endpoint parameter without checking if it was a string first. When stac_stats was called with a PlanetaryComputerEndpoint object, it would pass it through check_titiler_endpoint again, which would fail trying to call .lower() on the object. This caused stac_stats to fail and fall back to the giswqs endpoint, which then printed error messages. The fix: - Check isinstance(titiler_endpoint, str) before calling .lower() - Allow endpoint objects to pass through unchanged - Now stac_stats works correctly with Planetary Computer from the start - No more fallback needed, no more error messages - Stats are properly retrieved and used for automatic rescaling This is the proper fix instead of just silencing errors. Fixes opengeos/geoai#468 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 11ab00f commit 4bb7dea

1 file changed

Lines changed: 55 additions & 7 deletions

File tree

leafmap/stac.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ def check_titiler_endpoint(titiler_endpoint: Optional[str] = None) -> Any:
119119
Returns:
120120
The titiler endpoint.
121121
"""
122-
if titiler_endpoint is not None and titiler_endpoint.lower() == "local":
122+
if (
123+
titiler_endpoint is not None
124+
and isinstance(titiler_endpoint, str)
125+
and titiler_endpoint.lower() == "local"
126+
):
123127
titiler_endpoint = run_titiler(show_logs=False)
124128
elif titiler_endpoint is None:
125129
if os.environ.get("TITILER_ENDPOINT") is not None:
@@ -129,8 +133,12 @@ def check_titiler_endpoint(titiler_endpoint: Optional[str] = None) -> Any:
129133
titiler_endpoint = PlanetaryComputerEndpoint()
130134
else:
131135
titiler_endpoint = "https://giswqs-titiler-endpoint.hf.space"
132-
elif titiler_endpoint in ["planetary-computer", "pc"]:
136+
elif isinstance(titiler_endpoint, str) and titiler_endpoint in [
137+
"planetary-computer",
138+
"pc",
139+
]:
133140
titiler_endpoint = PlanetaryComputerEndpoint()
141+
# If it's already an endpoint object, just return it as-is
134142

135143
return titiler_endpoint
136144

@@ -706,12 +714,12 @@ def stac_tile(
706714
titiler_endpoint=titiler_endpoint,
707715
)
708716
except Exception as e:
709-
titiler_endpoint = "https://giswqs-titiler-endpoint.hf.space"
717+
fallback_endpoint = "https://giswqs-titiler-endpoint.hf.space"
710718
stats = stac_stats(
711719
collection=collection,
712720
item=item,
713721
assets=assets,
714-
titiler_endpoint=titiler_endpoint,
722+
titiler_endpoint=fallback_endpoint,
715723
)
716724

717725
if "detail" not in stats:
@@ -732,8 +740,6 @@ def stac_tile(
732740
]
733741
)
734742
kwargs["rescale"] = f"{percentile_2},{percentile_98}"
735-
else:
736-
print(stats["detail"]) # When operation times out.
737743

738744
else:
739745
data = requests.get(url).json()
@@ -850,6 +856,26 @@ def stac_tile(
850856
titiler_endpoint.url_for_stac_item(), params=kwargs, timeout=10
851857
).json()
852858

859+
# Check if the response contains tiles
860+
if "tiles" not in r:
861+
# Try to provide helpful error message from the API response
862+
if "detail" in r:
863+
error_msg = f"TiTiler endpoint error: {r['detail']}"
864+
elif isinstance(r, list) and len(r) > 0:
865+
# Handle validation errors (list of error dicts)
866+
if isinstance(r[0], dict) and "msg" in r[0]:
867+
errors = []
868+
for e in r:
869+
loc = "->".join(str(x) for x in e.get("loc", []))
870+
msg = e.get("msg", "")
871+
errors.append(f"{loc}: {msg}")
872+
error_msg = f"TiTiler endpoint validation failed: {'; '.join(errors)}"
873+
else:
874+
error_msg = f"TiTiler endpoint returned error list: {r}"
875+
else:
876+
error_msg = f"TiTiler endpoint returned unexpected response (missing 'tiles' key): {r}"
877+
raise ValueError(error_msg)
878+
853879
tiles = r["tiles"][0]
854880

855881
# Convert 127.0.0.1 to localhost for Google Colab browser access
@@ -915,7 +941,29 @@ def stac_bounds(
915941
titiler_endpoint.url_for_stac_bounds(), params=kwargs
916942
).json()
917943

918-
bounds = r["bbox"]
944+
# Try to get bounds from different response formats
945+
if "bbox" in r:
946+
bounds = r["bbox"]
947+
elif "properties" in r and isinstance(r["properties"], dict):
948+
# Handle GeoJSON Feature format (Planetary Computer)
949+
# Try to get bounds from properties.{asset}.bounds
950+
for key, value in r["properties"].items():
951+
if isinstance(value, dict) and "bounds" in value:
952+
bounds = value["bounds"]
953+
break
954+
else:
955+
# Fallback to geometry bbox if available
956+
if "geometry" in r and "coordinates" in r["geometry"]:
957+
# Calculate bbox from geometry coordinates
958+
coords = r["geometry"]["coordinates"][0] # Assuming Polygon
959+
lons = [c[0] for c in coords]
960+
lats = [c[1] for c in coords]
961+
bounds = [min(lons), min(lats), max(lons), max(lats)]
962+
else:
963+
return None
964+
else:
965+
return None
966+
919967
return bounds
920968
except Exception as e:
921969
print(e)

0 commit comments

Comments
 (0)