fimeval 0.1.57__py3-none-any.whl → 0.1.58__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fimeval/BenchFIMQuery/__init__.py +1 -1
- fimeval/BenchFIMQuery/access_benchfim.py +150 -87
- fimeval/BenchFIMQuery/utilis.py +72 -7
- fimeval/BuildingFootprint/__init__.py +2 -1
- fimeval/BuildingFootprint/arcgis_API.py +195 -0
- fimeval/BuildingFootprint/evaluationwithBF.py +21 -63
- fimeval/ContingencyMap/__init__.py +2 -2
- fimeval/ContingencyMap/evaluationFIM.py +45 -24
- fimeval/ContingencyMap/printcontingency.py +3 -1
- fimeval/ContingencyMap/water_bodies.py +175 -0
- fimeval/__init__.py +7 -2
- fimeval/setup_benchFIM.py +2 -0
- fimeval/utilis.py +6 -8
- {fimeval-0.1.57.dist-info → fimeval-0.1.58.dist-info}/METADATA +14 -8
- fimeval-0.1.58.dist-info/RECORD +21 -0
- {fimeval-0.1.57.dist-info → fimeval-0.1.58.dist-info}/WHEEL +1 -1
- fimeval/BuildingFootprint/microsoftBF.py +0 -134
- fimeval/ContingencyMap/PWBs3.py +0 -42
- fimeval-0.1.57.dist-info/RECORD +0 -21
- {fimeval-0.1.57.dist-info → fimeval-0.1.58.dist-info}/licenses/LICENSE.txt +0 -0
- {fimeval-0.1.57.dist-info → fimeval-0.1.58.dist-info}/top_level.txt +0 -0
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
from typing import Optional, Dict, Any, List, Tuple
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
import os
|
|
11
|
-
import json
|
|
11
|
+
import json
|
|
12
12
|
|
|
13
13
|
import rasterio
|
|
14
14
|
from rasterio.warp import transform_bounds
|
|
@@ -24,13 +24,15 @@ from .utilis import (
|
|
|
24
24
|
_to_hour_or_none,
|
|
25
25
|
_record_day,
|
|
26
26
|
_record_hour_or_none,
|
|
27
|
-
_pretty_date_for_print,
|
|
27
|
+
_pretty_date_for_print,
|
|
28
|
+
_ensure_local_gpkg,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
# Preferred area CRSs for area calculations
|
|
31
|
-
AREA_CRS_US = "EPSG:5070"
|
|
32
|
+
AREA_CRS_US = "EPSG:5070" # for CONUS
|
|
32
33
|
AREA_CRS_GLOBAL = "EPSG:6933" # WGS 84 / NSIDC EASE-Grid 2.0 Global (equal-area) for rest of world: in future if the data is added into the catalog.
|
|
33
34
|
|
|
35
|
+
|
|
34
36
|
# Helper: pretty-print container so that print(response) shows the structured text.
|
|
35
37
|
# If "printable" is empty (e.g., download=True), nothing is printed.
|
|
36
38
|
class PrettyDict(dict):
|
|
@@ -40,6 +42,7 @@ class PrettyDict(dict):
|
|
|
40
42
|
return txt
|
|
41
43
|
# Empty string when we do not want anything printed (e.g., download=True)
|
|
42
44
|
return ""
|
|
45
|
+
|
|
43
46
|
__repr__ = __str__
|
|
44
47
|
|
|
45
48
|
|
|
@@ -106,7 +109,8 @@ def _record_bbox_polygon(rec: Dict[str, Any]) -> Polygon:
|
|
|
106
109
|
minx, miny, maxx, maxy = _get_record_bbox_xy(rec)
|
|
107
110
|
return box(minx, miny, maxx, maxy)
|
|
108
111
|
|
|
109
|
-
|
|
112
|
+
|
|
113
|
+
# Return AOI polygon in WGS84 from a raster file --> this will be useful when user have model predicted raster and looking for the benchmrk FIM into the database
|
|
110
114
|
def _raster_aoi_polygon_wgs84(path: str) -> Polygon:
|
|
111
115
|
with rasterio.open(path) as ds:
|
|
112
116
|
if ds.crs is None:
|
|
@@ -117,24 +121,29 @@ def _raster_aoi_polygon_wgs84(path: str) -> Polygon:
|
|
|
117
121
|
)
|
|
118
122
|
return box(minx, miny, maxx, maxy)
|
|
119
123
|
|
|
120
|
-
|
|
124
|
+
|
|
125
|
+
# Return AOI polygon in WGS84 from a vector file --> this will be useful when user have model predicted vector and looking for the benchmrk FIM into the database
|
|
121
126
|
def _vector_aoi_polygon_wgs84(path: str) -> Polygon:
|
|
122
127
|
gdf = gpd.read_file(path)
|
|
123
128
|
if gdf.empty:
|
|
124
129
|
raise ValueError(f"Vector file {path} contains no features.")
|
|
125
130
|
if gdf.crs is None:
|
|
126
|
-
raise ValueError(
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"Vector file {path} has no CRS; cannot derive WGS84 geometry."
|
|
133
|
+
)
|
|
127
134
|
gdf = gdf.to_crs("EPSG:4326")
|
|
128
135
|
geom = unary_union(gdf.geometry)
|
|
129
136
|
if geom.is_empty:
|
|
130
137
|
raise ValueError(f"Vector file {path} has empty geometry after union.")
|
|
131
138
|
return geom
|
|
132
139
|
|
|
140
|
+
|
|
133
141
|
def _ensure_dir(path: str | Path) -> Path:
|
|
134
142
|
p = Path(path)
|
|
135
143
|
p.mkdir(parents=True, exist_ok=True)
|
|
136
144
|
return p
|
|
137
145
|
|
|
146
|
+
|
|
138
147
|
# benchmark FIM filtering by date
|
|
139
148
|
def _filter_by_date_exact(
|
|
140
149
|
records: List[Dict[str, Any]],
|
|
@@ -167,7 +176,8 @@ def _filter_by_date_exact(
|
|
|
167
176
|
out.append(r)
|
|
168
177
|
return out
|
|
169
178
|
|
|
170
|
-
|
|
179
|
+
|
|
180
|
+
# Filter available benchmark FIMs by date range
|
|
171
181
|
def _filter_by_date_range(
|
|
172
182
|
records: List[Dict[str, Any]],
|
|
173
183
|
start_date: Optional[str],
|
|
@@ -191,6 +201,7 @@ def _filter_by_date_range(
|
|
|
191
201
|
out.append(r)
|
|
192
202
|
return out
|
|
193
203
|
|
|
204
|
+
|
|
194
205
|
# Dynamic area CRS selection and overlap stats
|
|
195
206
|
def _pick_area_crs_for_bounds(bounds: Tuple[float, float, float, float]) -> str:
|
|
196
207
|
"""
|
|
@@ -219,7 +230,8 @@ def _pick_area_crs_for_bounds(bounds: Tuple[float, float, float, float]) -> str:
|
|
|
219
230
|
return AREA_CRS_US
|
|
220
231
|
return AREA_CRS_GLOBAL
|
|
221
232
|
|
|
222
|
-
|
|
233
|
+
|
|
234
|
+
# Compute the area overlap statistics between user passed AOI/raster and benchmark AOI
|
|
223
235
|
def _compute_area_overlap_stats(
|
|
224
236
|
aoi_geom: Polygon,
|
|
225
237
|
benchmark_geom: Polygon,
|
|
@@ -280,6 +292,7 @@ def _aoi_context_str(
|
|
|
280
292
|
return ", ".join(parts)
|
|
281
293
|
# fall back to the non-AOI context from utils
|
|
282
294
|
from .utilis import _context_str as _ctx
|
|
295
|
+
|
|
283
296
|
return _ctx(
|
|
284
297
|
huc8=huc8,
|
|
285
298
|
date_input=date_input,
|
|
@@ -288,15 +301,28 @@ def _aoi_context_str(
|
|
|
288
301
|
end_date=end_date,
|
|
289
302
|
)
|
|
290
303
|
|
|
304
|
+
def _display_raster_name(rec: Dict[str, Any]) -> str:
|
|
305
|
+
tif_url = rec.get("tif_url")
|
|
306
|
+
if isinstance(tif_url, str) and tif_url.strip():
|
|
307
|
+
# drop querystring if any
|
|
308
|
+
tif_url = tif_url.split("?", 1)[0]
|
|
309
|
+
return os.path.basename(tif_url)
|
|
310
|
+
|
|
311
|
+
# fallback: last path token of id
|
|
312
|
+
rid = rec.get("id")
|
|
313
|
+
if isinstance(rid, str) and rid.strip():
|
|
314
|
+
return rid.strip().split("/")[-1] + ".tif"
|
|
315
|
+
|
|
316
|
+
return "NA"
|
|
291
317
|
|
|
292
318
|
# Build a single printable block, optionally with overlap appended
|
|
293
|
-
def _format_block_with_overlap(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
tier = rec.get("tier") or rec.get("quality") or "Unknown"
|
|
319
|
+
def _format_block_with_overlap(
|
|
320
|
+
rec: Dict[str, Any], pct: Optional[float], km2: Optional[float]
|
|
321
|
+
) -> str:
|
|
322
|
+
tier = rec.get("tier") or rec.get("HWM") or rec.get("quality") or "Unknown"
|
|
297
323
|
res = rec.get("resolution_m")
|
|
298
324
|
res_txt = f"{res}m" if res is not None else "NA"
|
|
299
|
-
fname = rec
|
|
325
|
+
fname = _display_raster_name(rec)
|
|
300
326
|
|
|
301
327
|
lines = [f"Data Tier: {tier}"]
|
|
302
328
|
|
|
@@ -307,22 +333,27 @@ def _format_block_with_overlap(rec: Dict[str, Any],
|
|
|
307
333
|
date_str = _pretty_date_for_print(rec)
|
|
308
334
|
lines.append(f"Benchmark FIM date: {date_str}")
|
|
309
335
|
|
|
310
|
-
lines.extend(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
336
|
+
lines.extend(
|
|
337
|
+
[
|
|
338
|
+
f"Spatial Resolution: {res_txt}",
|
|
339
|
+
f"Raster Filename in DB: {fname}",
|
|
340
|
+
]
|
|
341
|
+
)
|
|
314
342
|
|
|
315
343
|
if pct is not None and km2 is not None:
|
|
316
|
-
lines.append(
|
|
344
|
+
lines.append(
|
|
345
|
+
f"Overlap with respect to benchmark FIM: {pct:.1f}% / {km2:.2f} km²"
|
|
346
|
+
)
|
|
317
347
|
|
|
318
348
|
return "\n".join(lines)
|
|
319
349
|
|
|
320
|
-
|
|
350
|
+
|
|
351
|
+
# For Tier-4- adding synthetic event year while reflecting the outcomes
|
|
321
352
|
def _is_synthetic_tier(rec: Dict[str, Any]) -> bool:
|
|
322
353
|
"""Return True when the record is a synthetic (Tier_4) event."""
|
|
323
354
|
tier = str(rec.get("tier") or rec.get("quality") or "").lower()
|
|
324
355
|
return "tier_4" in tier or tier.strip() == "4"
|
|
325
|
-
|
|
356
|
+
|
|
326
357
|
|
|
327
358
|
def _return_period_text(rec: Dict[str, Any]) -> str:
|
|
328
359
|
"""
|
|
@@ -344,13 +375,19 @@ def _return_period_text(rec: Dict[str, Any]) -> str:
|
|
|
344
375
|
except Exception:
|
|
345
376
|
return f"{rp} synthetic flow"
|
|
346
377
|
|
|
347
|
-
|
|
378
|
+
|
|
379
|
+
# helpers to read AOI GPKG geometry directly
|
|
348
380
|
def _storage_options_for_uri(uri: str) -> Optional[Dict[str, Any]]:
|
|
349
381
|
if isinstance(uri, str) and uri.startswith("s3://"):
|
|
350
|
-
anon = str(os.environ.get("AWS_NO_SIGN_REQUEST", "")).upper() in {
|
|
382
|
+
anon = str(os.environ.get("AWS_NO_SIGN_REQUEST", "")).upper() in {
|
|
383
|
+
"YES",
|
|
384
|
+
"TRUE",
|
|
385
|
+
"1",
|
|
386
|
+
}
|
|
351
387
|
return {"anon": anon}
|
|
352
388
|
return None
|
|
353
389
|
|
|
390
|
+
|
|
354
391
|
def _gpkg_urls_from_record(rec: Dict[str, Any]) -> List[str]:
|
|
355
392
|
urls: List[str] = []
|
|
356
393
|
for key in ("aoi_gpkg", "aoi_gpkg_url", "gpkg_url"):
|
|
@@ -382,23 +419,36 @@ def _gpkg_urls_from_record(rec: Dict[str, Any]) -> List[str]:
|
|
|
382
419
|
return out
|
|
383
420
|
|
|
384
421
|
def _read_benchmark_aoi_union_geom(rec: Dict[str, Any]) -> Optional[Polygon]:
|
|
385
|
-
# Read and union AOI geometries referenced by the record
|
|
422
|
+
# Read and union AOI geometries referenced by the record
|
|
386
423
|
urls = _gpkg_urls_from_record(rec)
|
|
387
424
|
if not urls:
|
|
388
425
|
return None
|
|
426
|
+
|
|
389
427
|
geoms: List[Polygon] = []
|
|
390
428
|
for uri in urls:
|
|
391
429
|
try:
|
|
392
430
|
storage_opts = _storage_options_for_uri(uri)
|
|
393
|
-
|
|
431
|
+
|
|
432
|
+
uri_to_read = _ensure_local_gpkg(uri)
|
|
433
|
+
|
|
434
|
+
gdf = (
|
|
435
|
+
gpd.read_file(uri_to_read, storage_options=storage_opts)
|
|
436
|
+
if storage_opts
|
|
437
|
+
else gpd.read_file(uri_to_read)
|
|
438
|
+
)
|
|
394
439
|
if gdf.empty:
|
|
395
440
|
continue
|
|
396
|
-
gdf =
|
|
441
|
+
gdf = (
|
|
442
|
+
gdf.to_crs("EPSG:4326")
|
|
443
|
+
if gdf.crs
|
|
444
|
+
else gdf.set_crs("EPSG:4326", allow_override=True)
|
|
445
|
+
)
|
|
397
446
|
u = unary_union(gdf.geometry)
|
|
398
447
|
if not u.is_empty:
|
|
399
448
|
geoms.append(u)
|
|
400
|
-
except Exception:
|
|
449
|
+
except Exception as e:
|
|
401
450
|
continue
|
|
451
|
+
|
|
402
452
|
if not geoms:
|
|
403
453
|
return None
|
|
404
454
|
uall = unary_union(geoms)
|
|
@@ -419,6 +469,7 @@ class benchFIMquery:
|
|
|
419
469
|
into a local directory
|
|
420
470
|
- or, as a special-case, fetch a specific benchmark by its filename.
|
|
421
471
|
"""
|
|
472
|
+
|
|
422
473
|
def __init__(self, catalog: Optional[Dict[str, Any]] = None) -> None:
|
|
423
474
|
"""
|
|
424
475
|
Create a new service.
|
|
@@ -510,12 +561,14 @@ class benchFIMquery:
|
|
|
510
561
|
)
|
|
511
562
|
|
|
512
563
|
if download and not out_dir:
|
|
513
|
-
return PrettyDict(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
564
|
+
return PrettyDict(
|
|
565
|
+
{
|
|
566
|
+
"status": "error",
|
|
567
|
+
"message": "When download=True, you must provide out_dir.",
|
|
568
|
+
"matches": [],
|
|
569
|
+
"printable": "",
|
|
570
|
+
}
|
|
571
|
+
)
|
|
519
572
|
|
|
520
573
|
# Direct filename-only workflow (no AOI, no dates)
|
|
521
574
|
if (
|
|
@@ -535,9 +588,7 @@ class benchFIMquery:
|
|
|
535
588
|
]
|
|
536
589
|
if not candidates:
|
|
537
590
|
candidates = [
|
|
538
|
-
r
|
|
539
|
-
for r in recs
|
|
540
|
-
if str(r.get("file_name", "")).strip() == fname
|
|
591
|
+
r for r in recs if str(r.get("file_name", "")).strip() == fname
|
|
541
592
|
]
|
|
542
593
|
else:
|
|
543
594
|
candidates = [
|
|
@@ -545,39 +596,41 @@ class benchFIMquery:
|
|
|
545
596
|
]
|
|
546
597
|
|
|
547
598
|
if not candidates:
|
|
548
|
-
return PrettyDict(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
599
|
+
return PrettyDict(
|
|
600
|
+
{
|
|
601
|
+
"status": "not_found",
|
|
602
|
+
"message": f"File name {fname!r} not found in catalog.",
|
|
603
|
+
"matches": [],
|
|
604
|
+
"printable": "",
|
|
605
|
+
}
|
|
606
|
+
)
|
|
554
607
|
|
|
555
608
|
target = candidates[0]
|
|
556
609
|
out_dir_path = _ensure_dir(out_dir)
|
|
557
610
|
dl = download_fim_assets(target, str(out_dir_path))
|
|
558
611
|
|
|
559
|
-
return PrettyDict(
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
612
|
+
return PrettyDict(
|
|
613
|
+
{
|
|
614
|
+
"status": "ok",
|
|
615
|
+
"message": f"Downloaded benchmark FIM '{fname}' to '{out_dir_path}'.",
|
|
616
|
+
"matches": [
|
|
617
|
+
{
|
|
618
|
+
"record": target,
|
|
619
|
+
"bbox_intersects": False,
|
|
620
|
+
"intersection_area_pct": None,
|
|
621
|
+
"intersection_area_km2": None,
|
|
622
|
+
"downloads": dl,
|
|
623
|
+
}
|
|
624
|
+
],
|
|
625
|
+
"printable": "",
|
|
626
|
+
}
|
|
627
|
+
)
|
|
573
628
|
|
|
574
629
|
# AOI-based workflows
|
|
575
630
|
records = self.records
|
|
576
631
|
if huc8:
|
|
577
632
|
huc8_str = str(huc8).strip()
|
|
578
|
-
records = [
|
|
579
|
-
r for r in records if str(r.get("huc8", "")).strip() == huc8_str
|
|
580
|
-
]
|
|
633
|
+
records = [r for r in records if str(r.get("huc8", "")).strip() == huc8_str]
|
|
581
634
|
|
|
582
635
|
# Date filters
|
|
583
636
|
if event_date:
|
|
@@ -586,12 +639,14 @@ class benchFIMquery:
|
|
|
586
639
|
records = _filter_by_date_range(records, start_date, end_date)
|
|
587
640
|
|
|
588
641
|
if not records:
|
|
589
|
-
return PrettyDict(
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
642
|
+
return PrettyDict(
|
|
643
|
+
{
|
|
644
|
+
"status": "not_found",
|
|
645
|
+
"message": "No catalog records match the provided filters.",
|
|
646
|
+
"matches": [],
|
|
647
|
+
"printable": "",
|
|
648
|
+
}
|
|
649
|
+
)
|
|
595
650
|
|
|
596
651
|
# If no AOI is provided at all
|
|
597
652
|
if aoi_geom is None:
|
|
@@ -615,14 +670,14 @@ class benchFIMquery:
|
|
|
615
670
|
end_date=end_date,
|
|
616
671
|
file_name=file_name,
|
|
617
672
|
)
|
|
618
|
-
printable = format_records_for_print(
|
|
673
|
+
printable = format_records_for_print(
|
|
674
|
+
[m["record"] for m in matches], context=ctx
|
|
675
|
+
)
|
|
619
676
|
|
|
620
677
|
if download:
|
|
621
678
|
out_dir_path = _ensure_dir(out_dir)
|
|
622
679
|
for m in matches:
|
|
623
|
-
m["downloads"] = download_fim_assets(
|
|
624
|
-
m["record"], str(out_dir_path)
|
|
625
|
-
)
|
|
680
|
+
m["downloads"] = download_fim_assets(m["record"], str(out_dir_path))
|
|
626
681
|
msg = (
|
|
627
682
|
f"Downloaded {len(matches)} benchmark record(s) "
|
|
628
683
|
f"to '{out_dir_path}'."
|
|
@@ -633,12 +688,14 @@ class benchFIMquery:
|
|
|
633
688
|
f"for the provided filters."
|
|
634
689
|
)
|
|
635
690
|
|
|
636
|
-
return PrettyDict(
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
691
|
+
return PrettyDict(
|
|
692
|
+
{
|
|
693
|
+
"status": "ok",
|
|
694
|
+
"message": msg,
|
|
695
|
+
"matches": matches,
|
|
696
|
+
"printable": "" if download else printable,
|
|
697
|
+
}
|
|
698
|
+
)
|
|
642
699
|
|
|
643
700
|
# AOI is present: intersect with bbox
|
|
644
701
|
intersecting: List[Dict[str, Any]] = []
|
|
@@ -652,12 +709,14 @@ class benchFIMquery:
|
|
|
652
709
|
intersecting.append(r)
|
|
653
710
|
|
|
654
711
|
if not intersecting:
|
|
655
|
-
return PrettyDict(
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
712
|
+
return PrettyDict(
|
|
713
|
+
{
|
|
714
|
+
"status": "not_found",
|
|
715
|
+
"message": "No benchmark FIM bbox intersects the provided AOI.",
|
|
716
|
+
"matches": [],
|
|
717
|
+
"printable": "",
|
|
718
|
+
}
|
|
719
|
+
)
|
|
661
720
|
|
|
662
721
|
out_matches: List[Dict[str, Any]] = []
|
|
663
722
|
out_dir_path = _ensure_dir(out_dir) if (download and out_dir) else None
|
|
@@ -725,12 +784,14 @@ class benchFIMquery:
|
|
|
725
784
|
blocks.append(_format_block_with_overlap(rec, None, None))
|
|
726
785
|
printable = header + "\n\n".join(blocks)
|
|
727
786
|
|
|
728
|
-
return PrettyDict(
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
787
|
+
return PrettyDict(
|
|
788
|
+
{
|
|
789
|
+
"status": "ok",
|
|
790
|
+
"message": msg,
|
|
791
|
+
"matches": out_matches,
|
|
792
|
+
"printable": printable,
|
|
793
|
+
}
|
|
794
|
+
)
|
|
734
795
|
|
|
735
796
|
def __call__(
|
|
736
797
|
self,
|
|
@@ -758,4 +819,6 @@ class benchFIMquery:
|
|
|
758
819
|
download=download,
|
|
759
820
|
out_dir=out_dir,
|
|
760
821
|
)
|
|
761
|
-
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
benchFIMquery = benchFIMquery()
|
fimeval/BenchFIMQuery/utilis.py
CHANGED
|
@@ -38,12 +38,72 @@ _YMDH_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[ T]\d{2}$")
|
|
|
38
38
|
_YMDHMS_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?$")
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
#Support functions to ensute the geopackage file is local
|
|
42
|
+
def _s3_bucket_key_from_http_url(url: str) -> Optional[tuple[str, str]]:
|
|
43
|
+
try:
|
|
44
|
+
u = urllib.parse.urlparse(url)
|
|
45
|
+
host = (u.netloc or "").lower()
|
|
46
|
+
path = (u.path or "").lstrip("/")
|
|
47
|
+
if not host or not path:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
if ".s3." in host or host.endswith(".s3.amazonaws.com"):
|
|
51
|
+
bucket = host.split(".s3", 1)[0]
|
|
52
|
+
key = path
|
|
53
|
+
return bucket, key
|
|
54
|
+
|
|
55
|
+
return None
|
|
56
|
+
except Exception:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ensure_local_gpkg(uri: str) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Ensure we can read a GPKG even when uri is an https S3 URL by caching locally.
|
|
63
|
+
Returns a path usable by geopandas.read_file().
|
|
64
|
+
"""
|
|
65
|
+
if not isinstance(uri, str) or not uri.strip():
|
|
66
|
+
return uri
|
|
67
|
+
|
|
68
|
+
u = uri.strip()
|
|
69
|
+
if os.path.exists(u):
|
|
70
|
+
return u
|
|
71
|
+
|
|
72
|
+
# Cache https S3 gpkg locally
|
|
73
|
+
if u.lower().startswith("http") and ".amazonaws.com/" in u.lower() and u.lower().endswith(".gpkg"):
|
|
74
|
+
parsed = _s3_bucket_key_from_http_url(u.split("?", 1)[0])
|
|
75
|
+
if not parsed:
|
|
76
|
+
return u
|
|
77
|
+
|
|
78
|
+
bucket, key = parsed
|
|
79
|
+
cache_dir = os.path.join(os.path.expanduser("~"), ".fimeval_cache", "aoi_gpkg")
|
|
80
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
81
|
+
|
|
82
|
+
local = os.path.join(cache_dir, os.path.basename(key))
|
|
83
|
+
if not os.path.exists(local):
|
|
84
|
+
_download(bucket, key, local)
|
|
85
|
+
return local
|
|
86
|
+
return u
|
|
87
|
+
|
|
41
88
|
def _normalize_user_dt(s: str) -> str:
|
|
42
89
|
s = s.strip()
|
|
43
90
|
s = s.replace("/", "-")
|
|
44
91
|
s = re.sub(r"\s+", " ", s)
|
|
45
92
|
return s
|
|
46
93
|
|
|
94
|
+
def _display_raster_name(rec: Dict[str, Any]) -> str:
|
|
95
|
+
tif_url = rec.get("tif_url")
|
|
96
|
+
if isinstance(tif_url, str) and tif_url.strip():
|
|
97
|
+
# drop querystring if any
|
|
98
|
+
tif_url = tif_url.split("?", 1)[0]
|
|
99
|
+
return os.path.basename(tif_url)
|
|
100
|
+
|
|
101
|
+
# fallback: last path token of id
|
|
102
|
+
rid = rec.get("id")
|
|
103
|
+
if isinstance(rid, str) and rid.strip():
|
|
104
|
+
return rid.strip().split("/")[-1] + ".tif"
|
|
105
|
+
|
|
106
|
+
return "NA"
|
|
47
107
|
|
|
48
108
|
def _to_date(s: str) -> dt.date:
|
|
49
109
|
s = _normalize_user_dt(s)
|
|
@@ -186,7 +246,7 @@ def format_records_for_print(
|
|
|
186
246
|
tier = r.get("tier") or r.get("quality") or "Unknown"
|
|
187
247
|
res = r.get("resolution_m")
|
|
188
248
|
res_txt = f"{res}m" if res is not None else "NA"
|
|
189
|
-
fname = r
|
|
249
|
+
fname = _display_raster_name(r)
|
|
190
250
|
|
|
191
251
|
# Build lines with Tier-aware event text
|
|
192
252
|
lines = [f"Data Tier: {tier}"]
|
|
@@ -196,14 +256,17 @@ def format_records_for_print(
|
|
|
196
256
|
date_str = _pretty_date_for_print(r)
|
|
197
257
|
lines.append(f"Benchmark FIM date: {date_str}")
|
|
198
258
|
|
|
199
|
-
lines.extend(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
259
|
+
lines.extend(
|
|
260
|
+
[
|
|
261
|
+
f"Spatial Resolution: {res_txt}",
|
|
262
|
+
f"Benchmark FIM raster name in DB: {fname}",
|
|
263
|
+
]
|
|
264
|
+
)
|
|
203
265
|
blocks.append("\n".join(lines))
|
|
204
266
|
|
|
205
267
|
return (header + "\n\n".join(blocks)).strip()
|
|
206
268
|
|
|
269
|
+
|
|
207
270
|
# S3 and json catalog
|
|
208
271
|
def load_catalog_core() -> Dict[str, Any]:
|
|
209
272
|
obj = _S3.get_object(Bucket=BUCKET, Key=CATALOG_KEY)
|
|
@@ -224,6 +287,7 @@ def _download(bucket: str, key: str, dest_path: str) -> str:
|
|
|
224
287
|
_S3.download_file(bucket, key, dest_path)
|
|
225
288
|
return dest_path
|
|
226
289
|
|
|
290
|
+
|
|
227
291
|
# Get the files from s3 bucket
|
|
228
292
|
def _folder_from_record(rec: Dict[str, Any]) -> str:
|
|
229
293
|
s3_key = rec.get("s3_key")
|
|
@@ -241,7 +305,8 @@ def _tif_key_from_record(rec: Dict[str, Any]) -> Optional[str]:
|
|
|
241
305
|
return None
|
|
242
306
|
return _folder_from_record(rec) + fname
|
|
243
307
|
|
|
244
|
-
|
|
308
|
+
|
|
309
|
+
# Download that tif and the boundary file --> need to add building footprint automation as well.
|
|
245
310
|
def download_fim_assets(record: Dict[str, Any], dest_dir: str) -> Dict[str, Any]:
|
|
246
311
|
"""
|
|
247
312
|
Download the .tif (if present) and any .gpkg from the record's folder to dest_dir.
|
|
@@ -266,4 +331,4 @@ def download_fim_assets(record: Dict[str, Any], dest_dir: str) -> Dict[str, Any]
|
|
|
266
331
|
_download(BUCKET, key, local)
|
|
267
332
|
out["gpkg_files"].append(local)
|
|
268
333
|
|
|
269
|
-
return out
|
|
334
|
+
return out
|