ras-commander 0.76.0__py3-none-any.whl → 0.78.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/Decorators.py +256 -256
- ras_commander/HdfInfiltration.py +1529 -1529
- ras_commander/HdfResultsPlan.py +380 -380
- ras_commander/RasExamples.py +543 -424
- ras_commander/RasGeo.py +3 -3
- ras_commander/RasMap.py +467 -252
- ras_commander/RasPlan.py +1536 -1536
- ras_commander/RasPrj.py +1479 -1474
- ras_commander/__init__.py +1 -1
- {ras_commander-0.76.0.dist-info → ras_commander-0.78.0.dist-info}/METADATA +17 -12
- {ras_commander-0.76.0.dist-info → ras_commander-0.78.0.dist-info}/RECORD +14 -14
- {ras_commander-0.76.0.dist-info → ras_commander-0.78.0.dist-info}/WHEEL +1 -1
- {ras_commander-0.76.0.dist-info → ras_commander-0.78.0.dist-info}/licenses/LICENSE +0 -0
- {ras_commander-0.76.0.dist-info → ras_commander-0.78.0.dist-info}/top_level.txt +0 -0
ras_commander/RasMap.py
CHANGED
@@ -1,252 +1,467 @@
|
|
1
|
-
"""
|
2
|
-
RasMap - Parses HEC-RAS mapper configuration files (.rasmap)
|
3
|
-
|
4
|
-
This module provides functionality to extract and organize information from
|
5
|
-
HEC-RAS mapper configuration files, including paths to terrain, soil, and land cover data.
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
-
|
12
|
-
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
import
|
20
|
-
import
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
from
|
26
|
-
|
27
|
-
from .
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
'
|
66
|
-
'
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
'
|
78
|
-
'
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
#
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
return
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
'
|
195
|
-
'
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
'
|
249
|
-
'
|
250
|
-
|
251
|
-
|
252
|
-
|
1
|
+
"""
|
2
|
+
RasMap - Parses HEC-RAS mapper configuration files (.rasmap)
|
3
|
+
|
4
|
+
This module provides functionality to extract and organize information from
|
5
|
+
HEC-RAS mapper configuration files, including paths to terrain, soil, and land cover data.
|
6
|
+
It also includes functions to automate the post-processing of stored maps.
|
7
|
+
|
8
|
+
This module is part of the ras-commander library and uses a centralized logging configuration.
|
9
|
+
|
10
|
+
Logging Configuration:
|
11
|
+
- The logging is set up in the logging_config.py file.
|
12
|
+
- A @log_call decorator is available to automatically log function calls.
|
13
|
+
- Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
14
|
+
|
15
|
+
Classes:
|
16
|
+
RasMap: Class for parsing and accessing HEC-RAS mapper configuration.
|
17
|
+
"""
|
18
|
+
|
19
|
+
import os
|
20
|
+
import re
|
21
|
+
import xml.etree.ElementTree as ET
|
22
|
+
from pathlib import Path
|
23
|
+
import pandas as pd
|
24
|
+
import shutil
|
25
|
+
from typing import Union, Optional, Dict, List, Any
|
26
|
+
|
27
|
+
from .RasPrj import ras
|
28
|
+
from .RasPlan import RasPlan
|
29
|
+
from .RasCmdr import RasCmdr
|
30
|
+
from .LoggingConfig import get_logger
|
31
|
+
from .Decorators import log_call
|
32
|
+
|
33
|
+
logger = get_logger(__name__)
|
34
|
+
|
35
|
+
class RasMap:
|
36
|
+
"""
|
37
|
+
Class for parsing and accessing information from HEC-RAS mapper configuration files (.rasmap).
|
38
|
+
|
39
|
+
This class provides methods to extract paths to terrain, soil, land cover data,
|
40
|
+
and various project settings from the .rasmap file associated with a HEC-RAS project.
|
41
|
+
It also includes functionality to automate the post-processing of stored maps.
|
42
|
+
"""
|
43
|
+
|
44
|
+
@staticmethod
|
45
|
+
@log_call
|
46
|
+
def parse_rasmap(rasmap_path: Union[str, Path], ras_object=None) -> pd.DataFrame:
|
47
|
+
"""
|
48
|
+
Parse a .rasmap file and extract relevant information.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
rasmap_path (Union[str, Path]): Path to the .rasmap file.
|
52
|
+
ras_object: Optional RAS object instance.
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
pd.DataFrame: DataFrame containing extracted information from the .rasmap file.
|
56
|
+
"""
|
57
|
+
ras_obj = ras_object or ras
|
58
|
+
ras_obj.check_initialized()
|
59
|
+
|
60
|
+
rasmap_path = Path(rasmap_path)
|
61
|
+
if not rasmap_path.exists():
|
62
|
+
logger.error(f"RASMapper file not found: {rasmap_path}")
|
63
|
+
# Create a single row DataFrame with all empty values
|
64
|
+
return pd.DataFrame({
|
65
|
+
'projection_path': [None],
|
66
|
+
'profile_lines_path': [[]],
|
67
|
+
'soil_layer_path': [[]],
|
68
|
+
'infiltration_hdf_path': [[]],
|
69
|
+
'landcover_hdf_path': [[]],
|
70
|
+
'terrain_hdf_path': [[]],
|
71
|
+
'current_settings': [{}]
|
72
|
+
})
|
73
|
+
|
74
|
+
try:
|
75
|
+
# Initialize data for the DataFrame - just one row with lists
|
76
|
+
data = {
|
77
|
+
'projection_path': [None],
|
78
|
+
'profile_lines_path': [[]],
|
79
|
+
'soil_layer_path': [[]],
|
80
|
+
'infiltration_hdf_path': [[]],
|
81
|
+
'landcover_hdf_path': [[]],
|
82
|
+
'terrain_hdf_path': [[]],
|
83
|
+
'current_settings': [{}]
|
84
|
+
}
|
85
|
+
|
86
|
+
# Read the file content
|
87
|
+
with open(rasmap_path, 'r', encoding='utf-8') as f:
|
88
|
+
xml_content = f.read()
|
89
|
+
|
90
|
+
# Check if it's a valid XML file
|
91
|
+
if not xml_content.strip().startswith('<'):
|
92
|
+
logger.error(f"File does not appear to be valid XML: {rasmap_path}")
|
93
|
+
return pd.DataFrame(data)
|
94
|
+
|
95
|
+
# Parse the XML file
|
96
|
+
try:
|
97
|
+
tree = ET.parse(rasmap_path)
|
98
|
+
root = tree.getroot()
|
99
|
+
except ET.ParseError as e:
|
100
|
+
logger.error(f"Error parsing XML in {rasmap_path}: {e}")
|
101
|
+
return pd.DataFrame(data)
|
102
|
+
|
103
|
+
# Helper function to convert relative paths to absolute paths
|
104
|
+
def to_absolute_path(relative_path: str) -> str:
|
105
|
+
if not relative_path:
|
106
|
+
return None
|
107
|
+
# Remove any leading .\ or ./
|
108
|
+
relative_path = relative_path.lstrip('.\\').lstrip('./')
|
109
|
+
# Convert to absolute path relative to project folder
|
110
|
+
return str(ras_obj.project_folder / relative_path)
|
111
|
+
|
112
|
+
# Extract projection path
|
113
|
+
try:
|
114
|
+
projection_elem = root.find(".//RASProjectionFilename")
|
115
|
+
if projection_elem is not None and 'Filename' in projection_elem.attrib:
|
116
|
+
data['projection_path'][0] = to_absolute_path(projection_elem.attrib['Filename'])
|
117
|
+
except Exception as e:
|
118
|
+
logger.warning(f"Error extracting projection path: {e}")
|
119
|
+
|
120
|
+
# Extract profile lines path
|
121
|
+
try:
|
122
|
+
profile_lines_elem = root.find(".//Features/Layer[@Name='Profile Lines']")
|
123
|
+
if profile_lines_elem is not None and 'Filename' in profile_lines_elem.attrib:
|
124
|
+
data['profile_lines_path'][0].append(to_absolute_path(profile_lines_elem.attrib['Filename']))
|
125
|
+
except Exception as e:
|
126
|
+
logger.warning(f"Error extracting profile lines path: {e}")
|
127
|
+
|
128
|
+
# Extract soil layer paths
|
129
|
+
try:
|
130
|
+
soil_layers = root.findall(".//Layer[@Name='Hydrologic Soil Groups']")
|
131
|
+
for layer in soil_layers:
|
132
|
+
if 'Filename' in layer.attrib:
|
133
|
+
data['soil_layer_path'][0].append(to_absolute_path(layer.attrib['Filename']))
|
134
|
+
except Exception as e:
|
135
|
+
logger.warning(f"Error extracting soil layer paths: {e}")
|
136
|
+
|
137
|
+
# Extract infiltration HDF paths
|
138
|
+
try:
|
139
|
+
infiltration_layers = root.findall(".//Layer[@Name='Infiltration']")
|
140
|
+
for layer in infiltration_layers:
|
141
|
+
if 'Filename' in layer.attrib:
|
142
|
+
data['infiltration_hdf_path'][0].append(to_absolute_path(layer.attrib['Filename']))
|
143
|
+
except Exception as e:
|
144
|
+
logger.warning(f"Error extracting infiltration HDF paths: {e}")
|
145
|
+
|
146
|
+
# Extract landcover HDF paths
|
147
|
+
try:
|
148
|
+
landcover_layers = root.findall(".//Layer[@Name='LandCover']")
|
149
|
+
for layer in landcover_layers:
|
150
|
+
if 'Filename' in layer.attrib:
|
151
|
+
data['landcover_hdf_path'][0].append(to_absolute_path(layer.attrib['Filename']))
|
152
|
+
except Exception as e:
|
153
|
+
logger.warning(f"Error extracting landcover HDF paths: {e}")
|
154
|
+
|
155
|
+
# Extract terrain HDF paths
|
156
|
+
try:
|
157
|
+
terrain_layers = root.findall(".//Terrains/Layer")
|
158
|
+
for layer in terrain_layers:
|
159
|
+
if 'Filename' in layer.attrib:
|
160
|
+
data['terrain_hdf_path'][0].append(to_absolute_path(layer.attrib['Filename']))
|
161
|
+
except Exception as e:
|
162
|
+
logger.warning(f"Error extracting terrain HDF paths: {e}")
|
163
|
+
|
164
|
+
# Extract current settings
|
165
|
+
current_settings = {}
|
166
|
+
try:
|
167
|
+
settings_elem = root.find(".//CurrentSettings")
|
168
|
+
if settings_elem is not None:
|
169
|
+
# Extract ProjectSettings
|
170
|
+
project_settings_elem = settings_elem.find("ProjectSettings")
|
171
|
+
if project_settings_elem is not None:
|
172
|
+
for child in project_settings_elem:
|
173
|
+
current_settings[child.tag] = child.text
|
174
|
+
|
175
|
+
# Extract Folders
|
176
|
+
folders_elem = settings_elem.find("Folders")
|
177
|
+
if folders_elem is not None:
|
178
|
+
for child in folders_elem:
|
179
|
+
current_settings[child.tag] = child.text
|
180
|
+
|
181
|
+
data['current_settings'][0] = current_settings
|
182
|
+
except Exception as e:
|
183
|
+
logger.warning(f"Error extracting current settings: {e}")
|
184
|
+
|
185
|
+
# Create DataFrame
|
186
|
+
df = pd.DataFrame(data)
|
187
|
+
logger.info(f"Successfully parsed RASMapper file: {rasmap_path}")
|
188
|
+
return df
|
189
|
+
|
190
|
+
except Exception as e:
|
191
|
+
logger.error(f"Unexpected error processing RASMapper file {rasmap_path}: {e}")
|
192
|
+
# Create a single row DataFrame with all empty values
|
193
|
+
return pd.DataFrame({
|
194
|
+
'projection_path': [None],
|
195
|
+
'profile_lines_path': [[]],
|
196
|
+
'soil_layer_path': [[]],
|
197
|
+
'infiltration_hdf_path': [[]],
|
198
|
+
'landcover_hdf_path': [[]],
|
199
|
+
'terrain_hdf_path': [[]],
|
200
|
+
'current_settings': [{}]
|
201
|
+
})
|
202
|
+
|
203
|
+
@staticmethod
|
204
|
+
@log_call
|
205
|
+
def get_rasmap_path(ras_object=None) -> Optional[Path]:
|
206
|
+
"""
|
207
|
+
Get the path to the .rasmap file based on the current project.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
ras_object: Optional RAS object instance.
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
Optional[Path]: Path to the .rasmap file if found, None otherwise.
|
214
|
+
"""
|
215
|
+
ras_obj = ras_object or ras
|
216
|
+
ras_obj.check_initialized()
|
217
|
+
|
218
|
+
project_name = ras_obj.project_name
|
219
|
+
project_folder = ras_obj.project_folder
|
220
|
+
rasmap_path = project_folder / f"{project_name}.rasmap"
|
221
|
+
|
222
|
+
if not rasmap_path.exists():
|
223
|
+
logger.warning(f"RASMapper file not found: {rasmap_path}")
|
224
|
+
return None
|
225
|
+
|
226
|
+
return rasmap_path
|
227
|
+
|
228
|
+
@staticmethod
|
229
|
+
@log_call
|
230
|
+
def initialize_rasmap_df(ras_object=None) -> pd.DataFrame:
|
231
|
+
"""
|
232
|
+
Initialize the rasmap_df as part of project initialization.
|
233
|
+
|
234
|
+
Args:
|
235
|
+
ras_object: Optional RAS object instance.
|
236
|
+
|
237
|
+
Returns:
|
238
|
+
pd.DataFrame: DataFrame containing information from the .rasmap file.
|
239
|
+
"""
|
240
|
+
ras_obj = ras_object or ras
|
241
|
+
ras_obj.check_initialized()
|
242
|
+
|
243
|
+
rasmap_path = RasMap.get_rasmap_path(ras_obj)
|
244
|
+
if rasmap_path is None:
|
245
|
+
logger.warning("No .rasmap file found for this project. Creating empty rasmap_df.")
|
246
|
+
# Create a single row DataFrame with all empty values
|
247
|
+
return pd.DataFrame({
|
248
|
+
'projection_path': [None],
|
249
|
+
'profile_lines_path': [[]],
|
250
|
+
'soil_layer_path': [[]],
|
251
|
+
'infiltration_hdf_path': [[]],
|
252
|
+
'landcover_hdf_path': [[]],
|
253
|
+
'terrain_hdf_path': [[]],
|
254
|
+
'current_settings': [{}]
|
255
|
+
})
|
256
|
+
|
257
|
+
return RasMap.parse_rasmap(rasmap_path, ras_obj)
|
258
|
+
|
259
|
+
@staticmethod
|
260
|
+
@log_call
|
261
|
+
def get_terrain_names(rasmap_path: Union[str, Path]) -> List[str]:
|
262
|
+
"""
|
263
|
+
Extracts terrain layer names from a given .rasmap file.
|
264
|
+
|
265
|
+
Args:
|
266
|
+
rasmap_path (Union[str, Path]): Path to the .rasmap file.
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
List[str]: A list of terrain names.
|
270
|
+
|
271
|
+
Raises:
|
272
|
+
FileNotFoundError: If the rasmap file does not exist.
|
273
|
+
ValueError: If the file is not a valid XML or lacks a 'Terrains' section.
|
274
|
+
"""
|
275
|
+
rasmap_path = Path(rasmap_path)
|
276
|
+
if not rasmap_path.is_file():
|
277
|
+
raise FileNotFoundError(f"The file '{rasmap_path}' does not exist.")
|
278
|
+
|
279
|
+
try:
|
280
|
+
tree = ET.parse(rasmap_path)
|
281
|
+
root = tree.getroot()
|
282
|
+
except ET.ParseError as e:
|
283
|
+
raise ValueError(f"Failed to parse the RASMAP file. Ensure it is a valid XML file. Error: {e}")
|
284
|
+
|
285
|
+
terrains_element = root.find('Terrains')
|
286
|
+
if terrains_element is None:
|
287
|
+
logger.warning("The RASMAP file does not contain a 'Terrains' section.")
|
288
|
+
return []
|
289
|
+
|
290
|
+
terrain_names = [layer.get('Name') for layer in terrains_element.findall('Layer') if layer.get('Name')]
|
291
|
+
logger.info(f"Extracted terrain names: {terrain_names}")
|
292
|
+
return terrain_names
|
293
|
+
|
294
|
+
|
295
|
+
@staticmethod
|
296
|
+
@log_call
|
297
|
+
def postprocess_stored_maps(
|
298
|
+
plan_number: Union[str, List[str]],
|
299
|
+
specify_terrain: Optional[str] = None,
|
300
|
+
layers: Union[str, List[str]] = None,
|
301
|
+
ras_object: Optional[Any] = None
|
302
|
+
) -> bool:
|
303
|
+
"""
|
304
|
+
Automates the generation of stored floodplain map outputs (e.g., .tif files).
|
305
|
+
|
306
|
+
This function modifies the plan and .rasmap files to generate floodplain maps
|
307
|
+
for one or more plans, then restores the original files.
|
308
|
+
|
309
|
+
Args:
|
310
|
+
plan_number (Union[str, List[str]]): Plan number(s) to generate maps for.
|
311
|
+
specify_terrain (Optional[str]): The name of a specific terrain to use.
|
312
|
+
layers (Union[str, List[str]], optional): A list of map layers to generate.
|
313
|
+
Defaults to ['WSEL', 'Velocity', 'Depth'].
|
314
|
+
ras_object (Optional[Any]): The RAS project object.
|
315
|
+
|
316
|
+
Returns:
|
317
|
+
bool: True if the process completed successfully, False otherwise.
|
318
|
+
"""
|
319
|
+
ras_obj = ras_object or ras
|
320
|
+
ras_obj.check_initialized()
|
321
|
+
|
322
|
+
if layers is None:
|
323
|
+
layers = ['WSEL', 'Velocity', 'Depth']
|
324
|
+
elif isinstance(layers, str):
|
325
|
+
layers = [layers]
|
326
|
+
|
327
|
+
# Convert plan_number to list if it's a string
|
328
|
+
plan_number_list = [plan_number] if isinstance(plan_number, str) else plan_number
|
329
|
+
|
330
|
+
rasmap_path = ras_obj.project_folder / f"{ras_obj.project_name}.rasmap"
|
331
|
+
rasmap_backup_path = rasmap_path.with_suffix(f"{rasmap_path.suffix}.storedmap.bak")
|
332
|
+
|
333
|
+
# Store plan paths and their backups
|
334
|
+
plan_paths = []
|
335
|
+
plan_backup_paths = []
|
336
|
+
for plan_num in plan_number_list:
|
337
|
+
plan_path = Path(RasPlan.get_plan_path(plan_num, ras_obj))
|
338
|
+
plan_backup_path = plan_path.with_suffix(f"{plan_path.suffix}.storedmap.bak")
|
339
|
+
plan_paths.append(plan_path)
|
340
|
+
plan_backup_paths.append(plan_backup_path)
|
341
|
+
|
342
|
+
def _create_map_element(name, map_type, profile_name="Max"):
|
343
|
+
map_params = {
|
344
|
+
"MapType": map_type,
|
345
|
+
"OutputMode": "Stored Current Terrain",
|
346
|
+
"ProfileIndex": "2147483647",
|
347
|
+
"ProfileName": profile_name
|
348
|
+
}
|
349
|
+
if specify_terrain:
|
350
|
+
map_params["Terrain"] = specify_terrain
|
351
|
+
|
352
|
+
layer_elem = ET.Element('Layer', Name=name, Type="RASResultsMap", Checked="True")
|
353
|
+
map_params_elem = ET.SubElement(layer_elem, 'MapParameters')
|
354
|
+
for k, v in map_params.items():
|
355
|
+
map_params_elem.set(k, str(v))
|
356
|
+
return layer_elem
|
357
|
+
|
358
|
+
try:
|
359
|
+
# --- 1. Backup and Modify Plan Files ---
|
360
|
+
for plan_num, plan_path, plan_backup_path in zip(plan_number_list, plan_paths, plan_backup_paths):
|
361
|
+
logger.info(f"Backing up plan file {plan_path} to {plan_backup_path}")
|
362
|
+
shutil.copy2(plan_path, plan_backup_path)
|
363
|
+
|
364
|
+
logger.info(f"Updating plan run flags for floodplain mapping for plan {plan_num}...")
|
365
|
+
RasPlan.update_run_flags(
|
366
|
+
plan_num,
|
367
|
+
geometry_preprocessor=False,
|
368
|
+
unsteady_flow_simulation=False,
|
369
|
+
post_processor=False,
|
370
|
+
floodplain_mapping=True, # Note: True maps to 0, which means "Run"
|
371
|
+
ras_object=ras_obj
|
372
|
+
)
|
373
|
+
|
374
|
+
# --- 2. Backup and Modify RASMAP File ---
|
375
|
+
logger.info(f"Backing up rasmap file {rasmap_path} to {rasmap_backup_path}")
|
376
|
+
shutil.copy2(rasmap_path, rasmap_backup_path)
|
377
|
+
|
378
|
+
tree = ET.parse(rasmap_path)
|
379
|
+
root = tree.getroot()
|
380
|
+
|
381
|
+
results_section = root.find('Results')
|
382
|
+
if results_section is None:
|
383
|
+
raise ValueError(f"No <Results> section found in {rasmap_path}")
|
384
|
+
|
385
|
+
# Process each plan's results layer
|
386
|
+
for plan_num in plan_number_list:
|
387
|
+
plan_hdf_part = f".p{plan_num}.hdf"
|
388
|
+
results_layer = None
|
389
|
+
for layer in results_section.findall("Layer[@Type='RASResults']"):
|
390
|
+
filename = layer.get("Filename")
|
391
|
+
if filename and plan_hdf_part.lower() in filename.lower():
|
392
|
+
results_layer = layer
|
393
|
+
break
|
394
|
+
|
395
|
+
if results_layer is None:
|
396
|
+
logger.warning(f"Could not find RASResults layer for plan ending in '{plan_hdf_part}' in {rasmap_path}")
|
397
|
+
continue
|
398
|
+
|
399
|
+
map_definitions = {"WSEL": "elevation", "Velocity": "velocity", "Depth": "depth"}
|
400
|
+
for layer_name in layers:
|
401
|
+
if layer_name in map_definitions:
|
402
|
+
map_type = map_definitions[layer_name]
|
403
|
+
layername_attr = "Water Surface" if layer_name == "WSEL" else None
|
404
|
+
map_elem = _create_map_element(layer_name, map_type)
|
405
|
+
if layername_attr:
|
406
|
+
map_elem.find("MapParameters").set("LayerName", layername_attr)
|
407
|
+
results_layer.append(map_elem)
|
408
|
+
logger.info(f"Added '{layer_name}' stored map to results layer for plan {plan_num}.")
|
409
|
+
|
410
|
+
if specify_terrain:
|
411
|
+
terrains_elem = root.find('Terrains')
|
412
|
+
if terrains_elem is not None:
|
413
|
+
for layer in list(terrains_elem):
|
414
|
+
if layer.get('Name') != specify_terrain:
|
415
|
+
terrains_elem.remove(layer)
|
416
|
+
logger.info(f"Filtered terrains, keeping only '{specify_terrain}'.")
|
417
|
+
|
418
|
+
tree.write(rasmap_path, encoding='utf-8', xml_declaration=True)
|
419
|
+
|
420
|
+
# --- 3. Execute HEC-RAS ---
|
421
|
+
logger.info("Opening HEC-RAS...")
|
422
|
+
ras_exe = ras_obj.ras_exe_path
|
423
|
+
prj_path = f'"{str(ras_obj.prj_file)}"'
|
424
|
+
command = f"{ras_exe} {prj_path}"
|
425
|
+
|
426
|
+
try:
|
427
|
+
import sys
|
428
|
+
import subprocess
|
429
|
+
if sys.platform == "win32":
|
430
|
+
hecras_process = subprocess.Popen(command)
|
431
|
+
else:
|
432
|
+
hecras_process = subprocess.Popen([ras_exe, prj_path])
|
433
|
+
|
434
|
+
logger.info(f"HEC-RAS opened with Process ID: {hecras_process.pid}")
|
435
|
+
logger.info(f"Please run plan(s) {', '.join(plan_number_list)} using the 'Compute Multiple' window in HEC-RAS to generate floodplain mapping results.")
|
436
|
+
|
437
|
+
# Wait for HEC-RAS to close
|
438
|
+
logger.info("Waiting for HEC-RAS to close...")
|
439
|
+
hecras_process.wait()
|
440
|
+
logger.info("HEC-RAS has closed")
|
441
|
+
|
442
|
+
success = True
|
443
|
+
|
444
|
+
except Exception as e:
|
445
|
+
logger.error(f"Failed to launch HEC-RAS: {e}")
|
446
|
+
success = False
|
447
|
+
|
448
|
+
if not success:
|
449
|
+
logger.error("Floodplain mapping computation failed.")
|
450
|
+
return False
|
451
|
+
|
452
|
+
logger.info("Floodplain mapping computation successful.")
|
453
|
+
return True
|
454
|
+
|
455
|
+
except Exception as e:
|
456
|
+
logger.error(f"Error in postprocess_stored_maps: {e}")
|
457
|
+
return False
|
458
|
+
|
459
|
+
finally:
|
460
|
+
# --- 4. Restore Files ---
|
461
|
+
for plan_path, plan_backup_path in zip(plan_paths, plan_backup_paths):
|
462
|
+
if plan_backup_path.exists():
|
463
|
+
logger.info(f"Restoring original plan file from {plan_backup_path}")
|
464
|
+
shutil.move(plan_backup_path, plan_path)
|
465
|
+
if rasmap_backup_path.exists():
|
466
|
+
logger.info(f"Restoring original rasmap file from {rasmap_backup_path}")
|
467
|
+
shutil.move(rasmap_backup_path, rasmap_path)
|