ras-commander 0.44.0__py3-none-any.whl → 0.46.0__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.
ras_commander/HdfXsec.py CHANGED
@@ -13,12 +13,16 @@ import h5py
13
13
  import numpy as np
14
14
  import pandas as pd
15
15
  from geopandas import GeoDataFrame
16
+ import geopandas as gpd
16
17
  from shapely.geometry import LineString, MultiLineString
17
18
  from typing import List # Import List to avoid NameError
18
19
  from .Decorators import standardize_input, log_call
19
20
  from .HdfBase import HdfBase
20
21
  from .HdfUtils import HdfUtils
21
22
  from .LoggingConfig import get_logger
23
+ import logging
24
+
25
+
22
26
 
23
27
  logger = get_logger(__name__)
24
28
 
@@ -37,141 +41,384 @@ class HdfXsec:
37
41
  Note:
38
42
  This class is designed to work with HEC-RAS geometry HDF files and requires them to have
39
43
  a specific structure and naming convention for the data groups and attributes.
40
- """
44
+ """
45
+ @staticmethod
46
+ @log_call
47
+ def cross_sections(hdf_path: str, datetime_to_str: bool = True, ras_object=None) -> gpd.GeoDataFrame:
48
+ """
49
+ Extract cross sections from HDF geometry file and return as GeoDataFrame
50
+
51
+ Parameters
52
+ ----------
53
+ hdf_path : str
54
+ Path to HDF file
55
+ datetime_to_str : bool, optional
56
+ Convert datetime objects to strings, by default True
57
+ ras_object : RasPrj, optional
58
+ RAS project object, by default None
59
+
60
+ Returns
61
+ -------
62
+ gpd.GeoDataFrame
63
+ GeoDataFrame containing cross section geometries and attributes including:
64
+ - River name
65
+ - Reach name
66
+ - River station
67
+ - Name
68
+ - Description
69
+ - Left/Channel/Right lengths
70
+ - Bank stations
71
+ - Friction mode
72
+ - Contraction/Expansion coefficients
73
+ - Levee information
74
+ - Hydraulic property parameters
75
+ - Block modes
76
+ - Last edited timestamp
77
+ - Manning's n values and stations
78
+ - Ineffective blocks data
79
+ """
80
+ try:
81
+ with h5py.File(hdf_path, 'r') as hdf:
82
+ # Extract datasets
83
+ poly_info = hdf['/Geometry/Cross Sections/Polyline Info'][:]
84
+ poly_parts = hdf['/Geometry/Cross Sections/Polyline Parts'][:]
85
+ poly_points = hdf['/Geometry/Cross Sections/Polyline Points'][:]
86
+
87
+ station_info = hdf['/Geometry/Cross Sections/Station Elevation Info'][:]
88
+ station_values = hdf['/Geometry/Cross Sections/Station Elevation Values'][:]
89
+
90
+ # Get attributes for cross sections
91
+ xs_attrs = hdf['/Geometry/Cross Sections/Attributes'][:]
92
+
93
+ # Get Manning's n data
94
+ mann_info = hdf["/Geometry/Cross Sections/Manning's n Info"][:]
95
+ mann_values = hdf["/Geometry/Cross Sections/Manning's n Values"][:]
96
+
97
+ # Get ineffective blocks data
98
+ ineff_blocks = hdf['/Geometry/Cross Sections/Ineffective Blocks'][:]
99
+ ineff_info = hdf['/Geometry/Cross Sections/Ineffective Info'][:]
100
+
101
+ # Initialize lists to store data
102
+ geometries = []
103
+ station_elevations = []
104
+ mannings_n = []
105
+ ineffective_blocks = []
106
+
107
+ # Process each cross section
108
+ for i in range(len(poly_info)):
109
+ # Extract polyline info
110
+ point_start_idx = poly_info[i][0]
111
+ point_count = poly_info[i][1]
112
+ part_start_idx = poly_info[i][2]
113
+ part_count = poly_info[i][3]
114
+
115
+ # Extract parts for current polyline
116
+ parts = poly_parts[part_start_idx:part_start_idx + part_count]
117
+
118
+ # Collect all points for this cross section
119
+ xs_points = []
120
+ for part in parts:
121
+ part_point_start = point_start_idx + part[0]
122
+ part_point_count = part[1]
123
+ points = poly_points[part_point_start:part_point_start + part_point_count]
124
+ xs_points.extend(points)
125
+
126
+ # Create LineString geometry
127
+ if len(xs_points) >= 2:
128
+ geometry = LineString(xs_points)
129
+ geometries.append(geometry)
130
+
131
+ # Extract station-elevation data
132
+ start_idx = station_info[i][0]
133
+ count = station_info[i][1]
134
+ station_elev = station_values[start_idx:start_idx + count]
135
+ station_elevations.append(station_elev)
136
+
137
+ # Extract Manning's n data
138
+ mann_start_idx = mann_info[i][0]
139
+ mann_count = mann_info[i][1]
140
+ mann_n_section = mann_values[mann_start_idx:mann_start_idx + mann_count]
141
+ mann_n_dict = {
142
+ 'Station': mann_n_section[:, 0].tolist(),
143
+ 'Mann n': mann_n_section[:, 1].tolist()
144
+ }
145
+ mannings_n.append(mann_n_dict)
146
+
147
+ # Extract ineffective blocks data
148
+ ineff_start_idx = ineff_info[i][0]
149
+ ineff_count = ineff_info[i][1]
150
+ if ineff_count > 0:
151
+ blocks = ineff_blocks[ineff_start_idx:ineff_start_idx + ineff_count]
152
+ blocks_list = []
153
+ for block in blocks:
154
+ block_dict = {
155
+ 'Left Sta': float(block['Left Sta']),
156
+ 'Right Sta': float(block['Right Sta']),
157
+ 'Elevation': float(block['Elevation']),
158
+ 'Permanent': bool(block['Permanent'])
159
+ }
160
+ blocks_list.append(block_dict)
161
+ ineffective_blocks.append(blocks_list)
162
+ else:
163
+ ineffective_blocks.append([])
164
+
165
+ # Create GeoDataFrame
166
+ if geometries:
167
+ # Create DataFrame from attributes
168
+ data = {
169
+ 'geometry': geometries,
170
+ 'station_elevation': station_elevations,
171
+ 'mannings_n': mannings_n,
172
+ 'ineffective_blocks': ineffective_blocks,
173
+ 'River': [x['River'].decode('utf-8').strip() for x in xs_attrs],
174
+ 'Reach': [x['Reach'].decode('utf-8').strip() for x in xs_attrs],
175
+ 'RS': [x['RS'].decode('utf-8').strip() for x in xs_attrs],
176
+ 'Name': [x['Name'].decode('utf-8').strip() for x in xs_attrs],
177
+ 'Description': [x['Description'].decode('utf-8').strip() for x in xs_attrs],
178
+ 'Len Left': xs_attrs['Len Left'],
179
+ 'Len Channel': xs_attrs['Len Channel'],
180
+ 'Len Right': xs_attrs['Len Right'],
181
+ 'Left Bank': xs_attrs['Left Bank'],
182
+ 'Right Bank': xs_attrs['Right Bank'],
183
+ 'Friction Mode': [x['Friction Mode'].decode('utf-8').strip() for x in xs_attrs],
184
+ 'Contr': xs_attrs['Contr'],
185
+ 'Expan': xs_attrs['Expan'],
186
+ 'Left Levee Sta': xs_attrs['Left Levee Sta'],
187
+ 'Left Levee Elev': xs_attrs['Left Levee Elev'],
188
+ 'Right Levee Sta': xs_attrs['Right Levee Sta'],
189
+ 'Right Levee Elev': xs_attrs['Right Levee Elev'],
190
+ 'HP Count': xs_attrs['HP Count'],
191
+ 'HP Start Elev': xs_attrs['HP Start Elev'],
192
+ 'HP Vert Incr': xs_attrs['HP Vert Incr'],
193
+ 'HP LOB Slices': xs_attrs['HP LOB Slices'],
194
+ 'HP Chan Slices': xs_attrs['HP Chan Slices'],
195
+ 'HP ROB Slices': xs_attrs['HP ROB Slices'],
196
+ 'Ineff Block Mode': xs_attrs['Ineff Block Mode'],
197
+ 'Obstr Block Mode': xs_attrs['Obstr Block Mode'],
198
+ 'Default Centerline': xs_attrs['Default Centerline'],
199
+ 'Last Edited': [x['Last Edited'].decode('utf-8').strip() for x in xs_attrs]
200
+ }
201
+
202
+ gdf = gpd.GeoDataFrame(data)
203
+
204
+ # Set CRS if available
205
+ if 'Projection' in hdf['/Geometry'].attrs:
206
+ proj = hdf['/Geometry'].attrs['Projection']
207
+ if isinstance(proj, bytes):
208
+ proj = proj.decode('utf-8')
209
+ gdf.set_crs(proj, allow_override=True)
210
+
211
+ return gdf
212
+
213
+ return gpd.GeoDataFrame()
214
+
215
+ except Exception as e:
216
+ logging.error(f"Error processing cross-section data: {str(e)}")
217
+ return gpd.GeoDataFrame()
218
+
41
219
 
42
220
  @staticmethod
43
221
  @log_call
44
- @standardize_input(file_type='geom_hdf')
45
- def cross_sections(hdf_path: Path, datetime_to_str: bool = False) -> GeoDataFrame:
222
+ def _get_polylines(hdf_path: Path, path: str, info_name: str = "Polyline Info", parts_name: str = "Polyline Parts", points_name: str = "Polyline Points") -> List[LineString]:
46
223
  """
47
- Return the model 1D cross sections.
224
+ Helper method to extract polylines from HDF file.
225
+
226
+ [rest of docstring remains the same]
227
+ """
228
+ try:
229
+ with h5py.File(hdf_path, 'r') as hdf_file:
230
+ polyline_info_path = f"{path}/{info_name}"
231
+ polyline_parts_path = f"{path}/{parts_name}"
232
+ polyline_points_path = f"{path}/{points_name}"
233
+
234
+ polyline_info = hdf_file[polyline_info_path][()]
235
+ polyline_parts = hdf_file[polyline_parts_path][()]
236
+ polyline_points = hdf_file[polyline_points_path][()]
237
+
238
+ geoms = []
239
+ for pnt_start, pnt_cnt, part_start, part_cnt in polyline_info:
240
+ points = polyline_points[pnt_start : pnt_start + pnt_cnt]
241
+ if part_cnt == 1:
242
+ geoms.append(LineString(points))
243
+ else:
244
+ parts = polyline_parts[part_start : part_start + part_cnt]
245
+ geoms.append(
246
+ MultiLineString(
247
+ list(
248
+ points[part_pnt_start : part_pnt_start + part_pnt_cnt]
249
+ for part_pnt_start, part_pnt_cnt in parts
250
+ )
251
+ )
252
+ )
253
+ return geoms
254
+ except Exception as e:
255
+ logger.error(f"Error getting polylines: {str(e)}")
256
+ return []
48
257
 
49
- This method extracts cross-section data from the HEC-RAS geometry HDF file,
50
- including attributes and geometry information.
258
+
259
+ @staticmethod
260
+ @log_call
261
+ @standardize_input(file_type='geom_hdf')
262
+ def river_centerlines(hdf_path: Path, datetime_to_str: bool = False) -> GeoDataFrame:
263
+ """
264
+ Extract river centerlines from HDF geometry file.
51
265
 
52
266
  Parameters
53
267
  ----------
54
268
  hdf_path : Path
55
- Path to the HEC-RAS geometry HDF file.
269
+ Path to the HEC-RAS geometry HDF file
56
270
  datetime_to_str : bool, optional
57
- If True, convert datetime objects to strings. Default is False.
271
+ Convert datetime objects to strings, by default False
58
272
 
59
273
  Returns
60
274
  -------
61
275
  GeoDataFrame
62
- A GeoDataFrame containing the cross sections with their attributes and geometries.
63
-
64
- Raises
65
- ------
66
- KeyError
67
- If the required datasets are not found in the HDF file.
276
+ GeoDataFrame containing river centerline geometries and attributes including:
277
+ - River Name
278
+ - Reach Name
279
+ - Upstream Type and Name
280
+ - Downstream Type and Name
281
+ - Junction distances
282
+ - Geometry
68
283
  """
69
284
  try:
70
285
  with h5py.File(hdf_path, 'r') as hdf_file:
71
- xs_data = hdf_file["Geometry/Cross Sections"]
72
-
73
- if "Attributes" not in xs_data:
74
- logger.warning(f"No 'Attributes' dataset group in {hdf_path}")
286
+ if "Geometry/River Centerlines" not in hdf_file:
287
+ logger.warning("No river centerlines found in geometry file")
75
288
  return GeoDataFrame()
76
289
 
77
- # Convert attribute values
290
+ centerline_data = hdf_file["Geometry/River Centerlines"]
291
+
292
+ # Get attributes
293
+ attrs = centerline_data["Attributes"][()]
78
294
  v_conv_val = np.vectorize(HdfUtils._convert_ras_hdf_value)
79
- xs_attrs = xs_data["Attributes"][()]
80
- xs_dict = {"xs_id": range(xs_attrs.shape[0])}
81
- xs_dict.update(
82
- {name: v_conv_val(xs_attrs[name]) for name in xs_attrs.dtype.names}
295
+
296
+ # Create dictionary of attributes
297
+ centerline_dict = {"centerline_id": range(attrs.shape[0])}
298
+ centerline_dict.update(
299
+ {name: v_conv_val(attrs[name]) for name in attrs.dtype.names}
83
300
  )
84
301
 
85
- xs_df = pd.DataFrame(xs_dict)
86
-
87
- # Create geometry from coordinate pairs
88
- xs_df['geometry'] = xs_df.apply(lambda row: LineString([
89
- (row['XS_X_Coord_1'], row['XS_Y_Coord_1']),
90
- (row['XS_X_Coord_2'], row['XS_Y_Coord_2'])
91
- ]), axis=1)
92
-
93
- # Convert to GeoDataFrame
94
- gdf = GeoDataFrame(xs_df, geometry='geometry', crs=HdfUtils.projection(hdf_path))
95
-
96
- # Convert datetime columns to strings if requested
97
- if datetime_to_str:
98
- gdf = HdfUtils.df_datetimes_to_str(gdf)
99
-
100
- return gdf
302
+ # Get polyline geometries
303
+ geoms = HdfXsec._get_polylines(
304
+ hdf_path,
305
+ "Geometry/River Centerlines",
306
+ info_name="Polyline Info",
307
+ parts_name="Polyline Parts",
308
+ points_name="Polyline Points"
309
+ )
310
+
311
+ # Create GeoDataFrame
312
+ centerline_gdf = GeoDataFrame(
313
+ centerline_dict,
314
+ geometry=geoms,
315
+ crs=HdfUtils.projection(hdf_path)
316
+ )
317
+
318
+ # Decode string columns after creation
319
+ str_columns = ['River Name', 'Reach Name', 'US Type',
320
+ 'US Name', 'DS Type', 'DS Name']
321
+ for col in str_columns:
322
+ if col in centerline_gdf.columns:
323
+ centerline_gdf[col] = centerline_gdf[col].apply(
324
+ lambda x: x.decode('utf-8').strip() if isinstance(x, bytes) else x
325
+ )
326
+
327
+ # Clean up column names and add length calculation
328
+ if not centerline_gdf.empty:
329
+ # Calculate length in project units
330
+ centerline_gdf['length'] = centerline_gdf.geometry.length
331
+
332
+ # Clean string columns
333
+ str_columns = ['River Name', 'Reach Name', 'US Type',
334
+ 'US Name', 'DS Type', 'DS Name']
335
+ for col in str_columns:
336
+ if col in centerline_gdf.columns:
337
+ centerline_gdf[col] = centerline_gdf[col].str.strip()
338
+
339
+ return centerline_gdf
101
340
 
102
- except KeyError as e:
103
- logger.error(f"Error accessing cross-section data in {hdf_path}: {str(e)}")
341
+ except Exception as e:
342
+ logger.error(f"Error reading river centerlines: {str(e)}")
104
343
  return GeoDataFrame()
105
344
 
345
+
346
+
106
347
  @staticmethod
107
348
  @log_call
108
- @standardize_input(file_type='geom_hdf')
109
- def cross_sections_elevations(hdf_path: Path, round_to: int = 2) -> pd.DataFrame:
349
+ def get_river_stationing(centerlines_gdf: GeoDataFrame) -> GeoDataFrame:
110
350
  """
111
- Return the model cross section elevation information.
112
-
113
- This method extracts cross-section elevation data from the HEC-RAS geometry HDF file,
114
- including station-elevation pairs for each cross-section.
351
+ Calculate river stationing along centerlines.
115
352
 
116
353
  Parameters
117
354
  ----------
118
- hdf_path : Path
119
- Path to the HEC-RAS geometry HDF file.
120
- round_to : int, optional
121
- Number of decimal places to round to. Default is 2.
355
+ centerlines_gdf : GeoDataFrame
356
+ GeoDataFrame containing river centerline geometries from river_centerlines()
122
357
 
123
358
  Returns
124
359
  -------
125
- pd.DataFrame
126
- A DataFrame containing the cross section elevation information.
127
-
128
- Raises
129
- ------
130
- KeyError
131
- If the required datasets are not found in the HDF file.
360
+ GeoDataFrame
361
+ Original GeoDataFrame with additional columns:
362
+ - station_start: Starting station for each reach
363
+ - station_end: Ending station for each reach
364
+ - stations: Array of stations along the centerline
365
+ - points: Array of point geometries along the centerline
132
366
  """
133
- try:
134
- with h5py.File(hdf_path, 'r') as hdf_file:
135
- path = "/Geometry/Cross Sections"
136
- if path not in hdf_file:
137
- logger.warning(f"No 'Cross Sections' group found in {hdf_path}")
138
- return pd.DataFrame()
367
+ if centerlines_gdf.empty:
368
+ logger.warning("Empty centerlines GeoDataFrame provided")
369
+ return centerlines_gdf
139
370
 
140
- xselev_data = hdf_file[path]
371
+ try:
372
+ # Create copy to avoid modifying original
373
+ result_gdf = centerlines_gdf.copy()
374
+
375
+ # Initialize new columns
376
+ result_gdf['station_start'] = 0.0
377
+ result_gdf['station_end'] = 0.0
378
+ result_gdf['stations'] = None
379
+ result_gdf['points'] = None
380
+
381
+ # Process each centerline
382
+ for idx, row in result_gdf.iterrows():
383
+ # Get line geometry
384
+ line = row.geometry
385
+
386
+ # Calculate length
387
+ total_length = line.length
388
+
389
+ # Generate points along the line
390
+ distances = np.linspace(0, total_length, num=100) # Adjust num for desired density
391
+ points = [line.interpolate(distance) for distance in distances]
141
392
 
142
- if "Station Elevation Info" not in xselev_data or "Station Elevation Values" not in xselev_data:
143
- logger.warning(f"Required datasets not found in Cross Sections group in {hdf_path}")
144
- return pd.DataFrame()
145
-
146
- # Get cross-section data
147
- xs_df = HdfXsec.cross_sections(hdf_path)
148
- if xs_df.empty:
149
- return pd.DataFrame()
150
-
151
- # Extract elevation data
152
- elevations = []
153
- for part_start, part_cnt in xselev_data["Station Elevation Info"][()]:
154
- xzdata = xselev_data["Station Elevation Values"][()][
155
- part_start : part_start + part_cnt
156
- ]
157
- elevations.append(xzdata)
158
-
159
- # Create DataFrame with elevation info
160
- xs_elev_df = xs_df[
161
- ["xs_id", "River", "Reach", "RS", "Left Bank", "Right Bank"]
162
- ].copy()
163
- xs_elev_df["Left Bank"] = xs_elev_df["Left Bank"].round(round_to).astype(str)
164
- xs_elev_df["Right Bank"] = xs_elev_df["Right Bank"].round(round_to).astype(str)
165
- xs_elev_df["elevation info"] = elevations
166
-
167
- return xs_elev_df
168
-
169
- except KeyError as e:
170
- logger.error(f"Error accessing cross-section elevation data in {hdf_path}: {str(e)}")
171
- return pd.DataFrame()
393
+ # Store results
394
+ result_gdf.at[idx, 'station_start'] = 0.0
395
+ result_gdf.at[idx, 'station_end'] = total_length
396
+ result_gdf.at[idx, 'stations'] = distances
397
+ result_gdf.at[idx, 'points'] = points
398
+
399
+ # Add stationing direction based on upstream/downstream info
400
+ if row['upstream_type'] == 'Junction' and row['downstream_type'] != 'Junction':
401
+ # Reverse stationing if upstream is junction
402
+ result_gdf.at[idx, 'station_start'] = total_length
403
+ result_gdf.at[idx, 'station_end'] = 0.0
404
+ result_gdf.at[idx, 'stations'] = total_length - distances
405
+
406
+ return result_gdf
407
+
172
408
  except Exception as e:
173
- logger.error(f"Unexpected error in cross_sections_elevations: {str(e)}")
174
- return pd.DataFrame()
409
+ logger.error(f"Error calculating river stationing: {str(e)}")
410
+ return centerlines_gdf
411
+
412
+ @staticmethod
413
+ def _interpolate_station(line, distance):
414
+ """Helper method to interpolate station along a line"""
415
+ if distance <= 0:
416
+ return line.coords[0]
417
+ elif distance >= line.length:
418
+ return line.coords[-1]
419
+ return line.interpolate(distance).coords[0]
420
+
421
+
175
422
 
176
423
  @staticmethod
177
424
  @log_call
@@ -225,58 +472,142 @@ class HdfXsec:
225
472
  logger.error(f"Error reading river reaches: {str(e)}")
226
473
  return GeoDataFrame()
227
474
 
475
+
228
476
  @staticmethod
229
- def _get_polylines(hdf_path: Path, path: str, info_name: str = "Polyline Info", parts_name: str = "Polyline Parts", points_name: str = "Polyline Points") -> List[LineString]:
477
+ @log_call
478
+ @standardize_input(file_type='geom_hdf')
479
+ def river_edge_lines(hdf_path: Path, datetime_to_str: bool = False) -> GeoDataFrame:
230
480
  """
231
- Helper method to extract polylines from HDF file.
232
-
233
- This method is used internally to extract polyline geometries for various features
234
- such as river reaches.
481
+ Return the model river edge lines.
235
482
 
236
483
  Parameters
237
484
  ----------
238
485
  hdf_path : Path
239
486
  Path to the HEC-RAS geometry HDF file.
240
- path : str
241
- Path within the HDF file to the polyline data.
242
- info_name : str, optional
243
- Name of the dataset containing polyline info. Default is "Polyline Info".
244
- parts_name : str, optional
245
- Name of the dataset containing polyline parts. Default is "Polyline Parts".
246
- points_name : str, optional
247
- Name of the dataset containing polyline points. Default is "Polyline Points".
487
+ datetime_to_str : bool, optional
488
+ If True, convert datetime objects to strings. Default is False.
248
489
 
249
490
  Returns
250
491
  -------
251
- List[LineString]
252
- A list of LineString geometries representing the polylines.
492
+ GeoDataFrame
493
+ A GeoDataFrame containing river edge lines with their attributes and geometries.
494
+ Each row represents a river bank (left or right) with associated attributes.
253
495
  """
254
496
  try:
255
497
  with h5py.File(hdf_path, 'r') as hdf_file:
256
- polyline_info_path = f"{path}/{info_name}"
257
- polyline_parts_path = f"{path}/{parts_name}"
258
- polyline_points_path = f"{path}/{points_name}"
498
+ if "Geometry/River Edge Lines" not in hdf_file:
499
+ logger.warning("No river edge lines found in geometry file")
500
+ return GeoDataFrame()
259
501
 
260
- polyline_info = hdf_file[polyline_info_path][()]
261
- polyline_parts = hdf_file[polyline_parts_path][()]
262
- polyline_points = hdf_file[polyline_points_path][()]
502
+ edge_data = hdf_file["Geometry/River Edge Lines"]
503
+
504
+ # Get attributes if they exist
505
+ if "Attributes" in edge_data:
506
+ attrs = edge_data["Attributes"][()]
507
+ v_conv_val = np.vectorize(HdfUtils._convert_ras_hdf_value)
508
+
509
+ # Create dictionary of attributes
510
+ edge_dict = {"edge_id": range(attrs.shape[0])}
511
+ edge_dict.update(
512
+ {name: v_conv_val(attrs[name]) for name in attrs.dtype.names}
513
+ )
514
+
515
+ # Add bank side indicator
516
+ if edge_dict["edge_id"].size % 2 == 0: # Ensure even number of edges
517
+ edge_dict["bank_side"] = ["Left", "Right"] * (edge_dict["edge_id"].size // 2)
518
+ else:
519
+ edge_dict = {"edge_id": [], "bank_side": []}
520
+
521
+ # Get polyline geometries
522
+ geoms = HdfXsec._get_polylines(
523
+ hdf_path,
524
+ "Geometry/River Edge Lines",
525
+ info_name="Polyline Info",
526
+ parts_name="Polyline Parts",
527
+ points_name="Polyline Points"
528
+ )
529
+
530
+ # Create GeoDataFrame
531
+ edge_gdf = GeoDataFrame(
532
+ edge_dict,
533
+ geometry=geoms,
534
+ crs=HdfUtils.projection(hdf_path)
535
+ )
536
+
537
+ # Convert datetime objects to strings if requested
538
+ if datetime_to_str and 'Last Edited' in edge_gdf.columns:
539
+ edge_gdf["Last Edited"] = edge_gdf["Last Edited"].apply(
540
+ lambda x: pd.Timestamp.isoformat(x) if pd.notnull(x) else None
541
+ )
542
+
543
+ # Add length calculation in project units
544
+ if not edge_gdf.empty:
545
+ edge_gdf['length'] = edge_gdf.geometry.length
546
+
547
+ return edge_gdf
263
548
 
264
- geoms = []
265
- for pnt_start, pnt_cnt, part_start, part_cnt in polyline_info:
266
- points = polyline_points[pnt_start : pnt_start + pnt_cnt]
267
- if part_cnt == 1:
268
- geoms.append(LineString(points))
269
- else:
270
- parts = polyline_parts[part_start : part_start + part_cnt]
271
- geoms.append(
272
- MultiLineString(
273
- list(
274
- points[part_pnt_start : part_pnt_start + part_pnt_cnt]
275
- for part_pnt_start, part_pnt_cnt in parts
276
- )
277
- )
278
- )
279
- return geoms
280
549
  except Exception as e:
281
- logger.error(f"Error getting polylines: {str(e)}")
282
- return []
550
+ logger.error(f"Error reading river edge lines: {str(e)}")
551
+ return GeoDataFrame()
552
+
553
+ @staticmethod
554
+ @log_call
555
+ @standardize_input(file_type='geom_hdf')
556
+ def river_bank_lines(hdf_path: Path, datetime_to_str: bool = False) -> GeoDataFrame:
557
+ """
558
+ Extract river bank lines from HDF geometry file.
559
+
560
+ Parameters
561
+ ----------
562
+ hdf_path : Path
563
+ Path to the HEC-RAS geometry HDF file
564
+ datetime_to_str : bool, optional
565
+ Convert datetime objects to strings, by default False
566
+
567
+ Returns
568
+ -------
569
+ GeoDataFrame
570
+ GeoDataFrame containing river bank line geometries with attributes:
571
+ - bank_id: Unique identifier for each bank line
572
+ - bank_side: Left or Right bank indicator
573
+ - geometry: LineString geometry of the bank
574
+ - length: Length of the bank line in project units
575
+ """
576
+ try:
577
+ with h5py.File(hdf_path, 'r') as hdf_file:
578
+ if "Geometry/River Bank Lines" not in hdf_file:
579
+ logger.warning("No river bank lines found in geometry file")
580
+ return GeoDataFrame()
581
+
582
+ # Get polyline geometries using existing helper method
583
+ geoms = HdfXsec._get_polylines(
584
+ hdf_path,
585
+ "Geometry/River Bank Lines",
586
+ info_name="Polyline Info",
587
+ parts_name="Polyline Parts",
588
+ points_name="Polyline Points"
589
+ )
590
+
591
+ # Create basic attributes
592
+ bank_dict = {
593
+ "bank_id": range(len(geoms)),
594
+ "bank_side": ["Left", "Right"] * (len(geoms) // 2) # Assuming pairs of left/right banks
595
+ }
596
+
597
+ # Create GeoDataFrame
598
+ bank_gdf = GeoDataFrame(
599
+ bank_dict,
600
+ geometry=geoms,
601
+ crs=HdfUtils.projection(hdf_path)
602
+ )
603
+
604
+ # Add length calculation in project units
605
+ if not bank_gdf.empty:
606
+ bank_gdf['length'] = bank_gdf.geometry.length
607
+
608
+ return bank_gdf
609
+
610
+ except Exception as e:
611
+ logger.error(f"Error reading river bank lines: {str(e)}")
612
+ return GeoDataFrame()
613
+