Skip to content

Commit 6cb37a4

Browse files
Merge pull request #19 from NewGraphEnvironment/10-cache-geotiff-metadata
Cache GeoTIFF metadata to skip remote reads on rebuild
2 parents 84e2855 + 3ad5ac4 commit 6cb37a4

3 files changed

Lines changed: 237 additions & 64 deletions

File tree

scripts/item_create.py

Lines changed: 79 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
from tqdm import tqdm
2929

3030
from stac_utils import (
31-
check_geotiff_cog,
31+
geotiff_extract_metadata,
32+
item_create_from_cache,
3233
date_extract_from_path,
3334
datetime_parse_item,
3435
encode_url_for_gdal,
@@ -52,6 +53,9 @@ def process_item(path_item: str, collection_id: str, path_local: str,
5253
results_lookup: dict) -> dict | None:
5354
"""Process a single GeoTIFF URL to create a STAC item.
5455
56+
Uses cached metadata when available (no remote read). Falls back to
57+
rio_stac for cache misses (should not happen if validation ran first).
58+
5559
Returns dict with item_id and item object, or None if processing fails.
5660
"""
5761
href_item = fix_url(path_item)
@@ -70,7 +74,6 @@ def process_item(path_item: str, collection_id: str, path_local: str,
7074
if date_str:
7175
item_time = datetime_parse_item(date_str)
7276
else:
73-
# Placeholder for items where date cannot be extracted (e.g. albers10k2m)
7477
item_time = datetime(2000, 1, 1, tzinfo=timezone.utc)
7578
datetime_is_unknown = True
7679

@@ -81,21 +84,35 @@ def process_item(path_item: str, collection_id: str, path_local: str,
8184
)
8285

8386
try:
84-
# Encode for GDAL/vsicurl (spaces → %20), but keep original for asset href
85-
gdal_path = encode_url_for_gdal(path_item)
86-
item = rio_stac.stac.create_stac_item(
87-
gdal_path,
88-
id=item_id,
89-
asset_media_type=media_type,
90-
asset_name='image',
91-
asset_href=href_item,
92-
with_proj=True,
93-
collection=collection_id,
94-
collection_url=PATH_S3_JSON,
95-
asset_roles=["data"]
96-
)
87+
# Cache hit: build from metadata (no remote read)
88+
if check.get("epsg") is not None:
89+
item = item_create_from_cache(
90+
url=path_item,
91+
item_id=item_id,
92+
metadata=check,
93+
collection_id=collection_id,
94+
collection_url=PATH_S3_JSON,
95+
media_type=media_type,
96+
item_datetime=item_time,
97+
)
98+
else:
99+
# Cache miss: fall back to rio_stac (remote read)
100+
logger.info("Cache miss for %s, reading remote file", href_item)
101+
gdal_path = encode_url_for_gdal(path_item)
102+
item = rio_stac.stac.create_stac_item(
103+
gdal_path,
104+
id=item_id,
105+
asset_media_type=media_type,
106+
asset_name='image',
107+
asset_href=href_item,
108+
with_proj=True,
109+
collection=collection_id,
110+
collection_url=PATH_S3_JSON,
111+
asset_roles=["data"]
112+
)
113+
item.assets['image'].href = href_item
114+
97115
item.datetime = item_time
98-
item.assets['image'].href = href_item
99116

100117
if datetime_is_unknown:
101118
item.properties["datetime_unknown"] = True
@@ -114,30 +131,55 @@ def process_item(path_item: str, collection_id: str, path_local: str,
114131
# =============================================================================
115132

116133
def load_validation_cache(urls_to_check: list[str]) -> dict:
117-
"""Load cached validation results and validate new URLs as needed.
134+
"""Load cached metadata and extract metadata for new URLs as needed.
118135
119-
Returns lookup dict: {url: {"is_geotiff": bool, "is_cog": bool}}
136+
Returns lookup dict: {url: {is_geotiff, is_cog, epsg, height, width, transform, bounds}}
137+
138+
Old cache rows (missing spatial columns) trigger re-extraction on cache miss
139+
in process_item via the rio_stac fallback path.
120140
"""
141+
all_columns = ["url", "is_geotiff", "is_cog", "epsg", "height", "width", "transform", "bounds"]
142+
121143
if os.path.exists(PATH_RESULTS_CSV):
122144
df_existing = pd.read_csv(PATH_RESULTS_CSV)
123145
existing_urls = set(df_existing["url"])
124146
logger.info("Loaded %d existing validation results", len(df_existing))
125147
else:
126-
df_existing = pd.DataFrame(columns=["url", "is_geotiff", "is_cog"])
148+
df_existing = pd.DataFrame(columns=all_columns)
127149
existing_urls = set()
128150
logger.info("No existing validation cache found, will validate all URLs")
129151

130-
urls_to_validate = [url for url in urls_to_check if url not in existing_urls]
131-
logger.info("%d URLs need validation (%d already cached)",
152+
# Detect old-format rows missing spatial metadata
153+
has_spatial = set()
154+
needs_upgrade = set()
155+
if "epsg" in df_existing.columns:
156+
for _, row in df_existing.iterrows():
157+
if pd.notna(row.get("epsg")):
158+
has_spatial.add(row["url"])
159+
elif row.get("is_geotiff"):
160+
needs_upgrade.add(row["url"])
161+
else:
162+
needs_upgrade = {row["url"] for _, row in df_existing.iterrows() if row["is_geotiff"]}
163+
164+
urls_to_validate = [url for url in urls_to_check
165+
if url not in existing_urls or url in needs_upgrade]
166+
if needs_upgrade:
167+
# Drop old rows that will be re-extracted with spatial metadata
168+
urls_upgrading = needs_upgrade & set(urls_to_validate)
169+
if urls_upgrading:
170+
df_existing = df_existing[~df_existing["url"].isin(urls_upgrading)]
171+
logger.info("%d cached URLs need spatial metadata upgrade", len(urls_upgrading))
172+
173+
logger.info("%d URLs need metadata extraction (%d already cached with full metadata)",
132174
len(urls_to_validate), len(urls_to_check) - len(urls_to_validate))
133175

134176
if urls_to_validate:
135-
logger.info("Validating %d GeoTIFFs...", len(urls_to_validate))
177+
logger.info("Extracting metadata from %d GeoTIFFs...", len(urls_to_validate))
136178
with concurrent.futures.ThreadPoolExecutor() as executor:
137179
new_results = list(tqdm(
138-
executor.map(check_geotiff_cog, urls_to_validate),
180+
executor.map(geotiff_extract_metadata, urls_to_validate),
139181
total=len(urls_to_validate),
140-
desc="Validating GeoTIFFs"
182+
desc="Extracting GeoTIFF metadata"
141183
))
142184

143185
df_new = pd.DataFrame(new_results)
@@ -146,12 +188,19 @@ def load_validation_cache(urls_to_check: list[str]) -> dict:
146188
logger.info("Saved %d validation results to %s", len(df_all), PATH_RESULTS_CSV)
147189
else:
148190
df_all = df_existing
149-
logger.info("No new URLs to validate, using existing cache")
150-
151-
return {
152-
fix_url(row["url"]): {"is_geotiff": row["is_geotiff"], "is_cog": row["is_cog"]}
153-
for _, row in df_all.iterrows()
154-
}
191+
logger.info("All URLs cached, no remote reads needed")
192+
193+
# Build lookup with full metadata (NaN → None for missing spatial columns)
194+
result = {}
195+
for _, row in df_all.iterrows():
196+
entry = {"is_geotiff": row["is_geotiff"], "is_cog": row["is_cog"]}
197+
for col in ["epsg", "height", "width", "transform", "bounds"]:
198+
if col in row and pd.notna(row[col]):
199+
entry[col] = int(row[col]) if col in ("epsg", "height", "width") else row[col]
200+
else:
201+
entry[col] = None
202+
result[fix_url(row["url"])] = entry
203+
return result
155204

156205

157206
# =============================================================================

scripts/item_reprocess.py

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from datetime import datetime, timezone
2222

2323
from stac_utils import (
24+
item_create_from_cache,
2425
date_extract_from_path,
2526
datetime_parse_item,
2627
encode_url_for_gdal,
@@ -74,22 +75,35 @@ def process_item(path_item: str, collection, results_lookup) -> dict | None:
7475
)
7576

7677
try:
77-
gdal_path = encode_url_for_gdal(path_item)
78-
item = rio_stac.stac.create_stac_item(
79-
gdal_path,
80-
id=item_id,
81-
asset_media_type=media_type,
82-
asset_name='image',
83-
asset_href=href_item,
84-
with_proj=True,
85-
collection=collection.id,
86-
collection_url=PATH_S3_JSON,
87-
asset_roles=["data"]
88-
)
78+
# Cache hit: build from metadata (no remote read)
79+
if check.get("epsg") is not None:
80+
item = item_create_from_cache(
81+
url=path_item,
82+
item_id=item_id,
83+
metadata=check,
84+
collection_id=collection.id,
85+
collection_url=PATH_S3_JSON,
86+
media_type=media_type,
87+
item_datetime=item_time,
88+
)
89+
else:
90+
# Cache miss: fall back to rio_stac (remote read)
91+
gdal_path = encode_url_for_gdal(path_item)
92+
item = rio_stac.stac.create_stac_item(
93+
gdal_path,
94+
id=item_id,
95+
asset_media_type=media_type,
96+
asset_name='image',
97+
asset_href=href_item,
98+
with_proj=True,
99+
collection=collection.id,
100+
collection_url=PATH_S3_JSON,
101+
asset_roles=["data"]
102+
)
103+
item.assets['image'].href = href_item
104+
89105
item.datetime = item_time
90-
item.assets['image'].href = href_item
91106

92-
# Flag items with unknown datetime for future improvement
93107
if datetime_is_unknown:
94108
item.properties["datetime_unknown"] = True
95109

@@ -125,10 +139,15 @@ def main():
125139
return 1
126140

127141
df_all = pd.read_csv(PATH_RESULTS_CSV)
128-
results_lookup = {
129-
fix_url(row["url"]): {"is_geotiff": row["is_geotiff"], "is_cog": row["is_cog"]}
130-
for _, row in df_all.iterrows()
131-
}
142+
results_lookup = {}
143+
for _, row in df_all.iterrows():
144+
entry = {"is_geotiff": row["is_geotiff"], "is_cog": row["is_cog"]}
145+
for col in ["epsg", "height", "width", "transform", "bounds"]:
146+
if col in row and pd.notna(row[col]):
147+
entry[col] = int(row[col]) if col in ("epsg", "height", "width") else row[col]
148+
else:
149+
entry[col] = None
150+
results_lookup[fix_url(row["url"])] = entry
132151
print(f"✓ Loaded {len(results_lookup)} validation results")
133152
print()
134153

0 commit comments

Comments
 (0)