Skip to content

Commit ab0e5e4

Browse files
committed
handle 2D, no CRS, and read CRS from S3
1 parent c8e5ed4 commit ab0e5e4

File tree

3 files changed

+95
-27
lines changed

3 files changed

+95
-27
lines changed

external_caller.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
1+
import json
2+
import logging
13
import sys
24
import warnings
35

6+
from botocore.exceptions import NoSuchKey
7+
48
from ras_stac.ras1d.converter import process_in_place_s3
9+
from ras_stac.ras1d.utils.s3_utils import str_from_s3
510

611
warnings.filterwarnings("ignore")
712

813
from papipyplug import parse_input, plugin_logger, print_results
914

1015
PLUGIN_PARAMS = {"required": ["in_prefix", "crs", "out_prefix"], "optional": []}
1116

12-
if __name__ == "__main__":
17+
18+
def owp_wrapper(in_prefix: str, out_prefix: str) -> dict:
19+
"""Temporary wrapper to meet OWP MIP 30% deliverable deadline"""
20+
crs_path = in_prefix.replace("source_models", "source_crs") + "crs_inference.json"
21+
try:
22+
crs_dict = json.loads(str_from_s3(crs_path))
23+
crs = crs_dict["best_crs"]
24+
except NoSuchKey:
25+
crs = None
26+
return process_in_place_s3(in_prefix, crs, out_prefix)
27+
28+
29+
def main():
1330
plugin_logger()
1431

1532
input_params = parse_input(sys.argv, PLUGIN_PARAMS)
@@ -18,8 +35,19 @@
1835
crs = input_params.get("crs")
1936
out_prefix = input_params.get("out_prefix")
2037

38+
# Debugging option
2139
if in_prefix == "TEST":
2240
results = f"{crs} | {out_prefix}"
23-
else:
24-
results = process_in_place_s3(in_prefix, crs, out_prefix)
25-
print_results(results)
41+
print_results(results)
42+
return
43+
44+
# Process
45+
try:
46+
results = owp_wrapper(in_prefix, out_prefix)
47+
print_results(results)
48+
except Exception as e:
49+
logging.warning(e)
50+
51+
52+
if __name__ == "__main__":
53+
main()

ras_stac/ras1d/converter.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import datetime
21
import io
32
import json
3+
import logging
44
import sys
5+
from datetime import datetime
56
from pathlib import Path
67

78
import pandas as pd
@@ -14,6 +15,7 @@
1415
from ras_stac.ras1d.utils.classes import (
1516
GenericAsset,
1617
GeometryAsset,
18+
NullGeometryAsset,
1719
PlanAsset,
1820
SteadyFlowAsset,
1921
ThumbAsset,
@@ -48,6 +50,8 @@ def export_stac(self, output_path: str) -> None:
4850
def export_thumbnail(self, thumb_path: str) -> None:
4951
"""Generate STAC thumbnail, save to S3, and log path."""
5052
gdfs = self.primary_geometry.gdfs
53+
if len(gdfs) == 0 or "null" in gdfs:
54+
return
5155
thumb = make_thumbnail(gdfs)
5256
if file_location(thumb_path) == "local":
5357
thumb.savefig(thumb_path, dpi=80)
@@ -72,15 +76,16 @@ def stac_item(self) -> dict:
7276
)
7377
stor_ext = StorageExtension.ext(stac, add_if_missing=True)
7478
stor_ext.apply(platform="AWS", region="us-east-1")
75-
prj_ext = AssetProjectionExtension.ext(stac, add_if_missing=True)
76-
og_crs = CRS(self.crs)
77-
prj_ext.apply(
78-
epsg=og_crs.to_epsg(),
79-
wkt2=og_crs.to_wkt(),
80-
geometry=self.get_footprint(),
81-
bbox=self.get_bbox(),
82-
centroid=to_geojson(self.get_centroid()),
83-
)
79+
if self.crs:
80+
prj_ext = AssetProjectionExtension.ext(stac, add_if_missing=True)
81+
og_crs = CRS(self.crs)
82+
prj_ext.apply(
83+
epsg=og_crs.to_epsg(),
84+
wkt2=og_crs.to_wkt(),
85+
geometry=self.get_footprint(),
86+
bbox=self.get_bbox(),
87+
centroid=to_geojson(self.get_centroid()),
88+
)
8489
return stac
8590

8691
@property
@@ -139,6 +144,7 @@ def stac_properties(self):
139144
"datetime_source": "processing_time" if self.primary_geometry.last_update is None else "model_geometry",
140145
"assigned_HUC8": self.huc8,
141146
"has_2d": any([a.has_2d for a in self.assets if isinstance(a, GeometryAsset)]),
147+
"has_1d": any([a.has_1d for a in self.assets if isinstance(a, GeometryAsset)]),
142148
}
143149
for p in self.custom_properties:
144150
properties[p] = self.custom_properties[p]
@@ -177,7 +183,16 @@ def primary_plan(self) -> PlanAsset:
177183
@property
178184
def primary_geometry(self) -> GeometryAsset:
179185
"""The geometry file listed in the primary plan"""
180-
return self.extension_dict[self.primary_plan.geometry]
186+
if not self.crs:
187+
return NullGeometryAsset()
188+
try:
189+
geom = self.extension_dict[self.primary_plan.geometry]
190+
except Exception:
191+
return NullGeometryAsset()
192+
if not geom.has_1d:
193+
return NullGeometryAsset()
194+
else:
195+
return geom
181196

182197
def check_for_mip(self) -> None:
183198
mip_data = [a for a in self.assets if a.name == "mip_package_geolocation_metadata.json"]
@@ -214,18 +229,24 @@ def ras_to_stac(ras_dir: str, crs: str):
214229

215230
def process_in_place_s3(in_prefix: str, crs: str, out_prefix: str):
216231
"""Convert a HEC-RAS model to a STAC item and save to same directory."""
232+
logging.info(f"Processing model with crs {crs} at prefix {in_prefix}")
233+
logging.info("Discovering model contents")
217234
converter = from_directory(in_prefix, crs)
218235
converter.check_for_mip()
219236
thumb_path = out_prefix + "Thumbnail.png"
237+
logging.info(f"Generating thumbnail at {thumb_path}")
220238
converter.export_thumbnail(thumb_path)
221239
stac_path = out_prefix + f"{converter.idx}.json"
240+
logging.info(f"Generating STAC item at {thumb_path}")
222241
converter.export_stac(stac_path)
223242
return {"in_path": in_prefix, "crs": crs, "thumb_path": thumb_path, "stac_path": stac_path}
224243

225244

226245
if __name__ == "__main__":
227246
ras_dir = sys.argv[1]
228247
crs = sys.argv[2]
248+
if crs == "None":
249+
crs = None
229250
out_dir = sys.argv[3]
230-
process_in_place_s3(ras_dir, crs, out_dir)
231-
# ras_to_stac(ras_dir, crs)
251+
# process_in_place_s3(ras_dir, crs, out_dir)
252+
ras_to_stac(ras_dir, crs)

ras_stac/ras1d/utils/classes.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,9 @@
1010
import pystac
1111
from pyproj import CRS
1212
from shapely import make_valid, union_all
13-
from shapely.geometry import (
14-
LineString,
15-
MultiPolygon,
16-
Point,
17-
Polygon,
18-
)
13+
from shapely.geometry import LineString, MultiPolygon, Point, Polygon, shape
1914

15+
from ras_stac.ras1d.data.us_geom import us_bounds
2016
from ras_stac.ras1d.utils.common import file_location
2117
from ras_stac.ras1d.utils.ras_utils import (
2218
data_pairs_from_text_block,
@@ -47,7 +43,7 @@ def check_crs(func):
4743

4844
def wrapper(self, *args, **kwargs):
4945
if self.crs is None:
50-
raise ValueError("Projection cannot be None")
46+
return None
5147
return func(self, *args, **kwargs)
5248

5349
return wrapper
@@ -221,6 +217,24 @@ def _extra_fields(self):
221217
return ex
222218

223219

220+
class NullGeometryAsset(GenericAsset):
221+
222+
def __init__(self):
223+
features = us_bounds["features"]
224+
geometries = [shape(feature["geometry"]) for feature in features]
225+
properties = [feature["properties"] for feature in features]
226+
gdf = gpd.GeoDataFrame(properties, geometry=geometries, crs="epsg:4326")
227+
self.gdfs = {"null": gdf}
228+
self.concave_hull = gdf
229+
230+
def __getattr__(self, name):
231+
# Return None if the attribute is not found
232+
return None
233+
234+
def get_river_miles(self):
235+
return None
236+
237+
224238
class GeometryAsset(GenericAsset):
225239

226240
def __init__(self, url: str):
@@ -253,7 +267,7 @@ def version(self):
253267
return search_contents(self.contents, "Program Version", expect_one=False)
254268

255269
@property
256-
@check_crs
270+
# @check_crs
257271
def reaches(self) -> dict:
258272
"""A dictionary of the reaches contained in the HEC-RAS geometry file."""
259273
river_reaches = search_contents(self.contents, "River Reach", expect_one=False)
@@ -263,7 +277,7 @@ def reaches(self) -> dict:
263277
return reaches
264278

265279
@property
266-
@check_crs
280+
# @check_crs
267281
def rivers(self) -> dict:
268282
"""A nested river-reach dictionary of the rivers/reaches contained in the HEC-RAS geometry file."""
269283
rivers = {}
@@ -370,7 +384,7 @@ def n_junctions(self):
370384
return len(self.junctions)
371385

372386
@property
373-
@check_crs
387+
# @check_crs
374388
def n_rivers(self):
375389
"""Number of rivers in the HEC-RAS geometry file."""
376390
return len(self.rivers)
@@ -505,6 +519,11 @@ def has_2d(self):
505519
return True
506520
return False
507521

522+
@property
523+
def has_1d(self):
524+
"""Check if RAS geometry has any 1D components"""
525+
return self.n_rivers > 0
526+
508527

509528
class XS:
510529
"""HEC-RAS Cross Section."""

0 commit comments

Comments
 (0)