SW1Dto2D 0.0.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.
- sw1dto2d/__init__.py +35 -0
- sw1dto2d/cartesian.py +32 -0
- sw1dto2d/sw1dto2d.py +753 -0
- sw1dto2d-0.0.0.dist-info/METADATA +560 -0
- sw1dto2d-0.0.0.dist-info/RECORD +13 -0
- sw1dto2d-0.0.0.dist-info/WHEEL +5 -0
- sw1dto2d-0.0.0.dist-info/licenses/AUTHORS +9 -0
- sw1dto2d-0.0.0.dist-info/licenses/LICENSE +516 -0
- sw1dto2d-0.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +30 -0
- tests/test_basic_processes.py +132 -0
- tests/test_brahmaputra.py +144 -0
- tests/test_garonne.py +129 -0
sw1dto2d/sw1dto2d.py
ADDED
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2019-2022 CS GROUP - France, University of Toulouse (France)
|
|
4
|
+
#
|
|
5
|
+
# This file is part of the sw1dto2d Package. sw1dto2d is a package to convert
|
|
6
|
+
# 1D Shallow water results to 2D. It is designed to compute 2D geographic
|
|
7
|
+
# representation of the results of 1D shallow water models.
|
|
8
|
+
#
|
|
9
|
+
# This software is governed by the CeCILL license under French law and abiding
|
|
10
|
+
# by the rules of distribution of free software. You can use, modify and/or
|
|
11
|
+
# redistribute the software under the terms of the CeCILL license as circulated
|
|
12
|
+
# by CEA, CNRS and INRIA at the following URL: "http://www.cecill.info".
|
|
13
|
+
#
|
|
14
|
+
# As a counterpart to the access to the source code and rights to copy, modify
|
|
15
|
+
# and redistribute granted by the license, users are provided only with a
|
|
16
|
+
# limited warranty and the software's author, the holder of the economic
|
|
17
|
+
# rights, and the successive licensors have only limited liability.
|
|
18
|
+
#
|
|
19
|
+
# In this respect, the user's attention is drawn to the risks associated with
|
|
20
|
+
# loading, using, modifying and/or developing or reproducing the software by
|
|
21
|
+
# the user in light of its specific status of free software, that may mean that
|
|
22
|
+
# it is complicated to manipulate, and that also therefore means that it is
|
|
23
|
+
# reserved for developers and experienced professionals having in-depth
|
|
24
|
+
# computer knowledge. Users are therefore encouraged to load and test the
|
|
25
|
+
# software's suitability as regards their requirements in conditions enabling
|
|
26
|
+
# the security of their systems and/or data to be ensured and, more generally,
|
|
27
|
+
# to use and operate it in the same conditions as regards security.
|
|
28
|
+
#
|
|
29
|
+
# The fact that you are presently reading this means that you have had
|
|
30
|
+
# knowledge of the CeCILL license and that you accept its terms.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
"""SW1Dto2D object for river cross-section lines and points reconstruction from centerline."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
import logging
|
|
37
|
+
from typing import Dict, List, Tuple
|
|
38
|
+
|
|
39
|
+
import geopy.distance
|
|
40
|
+
import numpy as np
|
|
41
|
+
import pandas as pd
|
|
42
|
+
import pyproj
|
|
43
|
+
from pyproj.transformer import Transformer
|
|
44
|
+
from shapely.geometry import LineString, Point, Polygon
|
|
45
|
+
import sw1dto2d.cartesian
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SW1Dto2D:
|
|
49
|
+
"""Builds river cross-section data from river centerline data."""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
model_output_1d: pd.DataFrame,
|
|
54
|
+
curvilinear_abscissa_key: str,
|
|
55
|
+
heights_key: str,
|
|
56
|
+
widths_key: str,
|
|
57
|
+
centerline: LineString,
|
|
58
|
+
crs=4326
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Constructor.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
model_output_1d : 1D-Hydraulics output data
|
|
66
|
+
curvilinear_abscissa_key : river center line curvilinear abscissa key in model_output_1d data
|
|
67
|
+
heights_key : heights key in model_output_1d data
|
|
68
|
+
widths_key : widths key in model_output_1d data
|
|
69
|
+
centerline : river center line geometry
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
# Retrieve or create logger
|
|
73
|
+
self._logger = logging.getLogger("sw1dto2d")
|
|
74
|
+
print("logger:", self._logger)
|
|
75
|
+
if self._logger.hasHandlers() is False:
|
|
76
|
+
self._logger.setLevel(logging.INFO)
|
|
77
|
+
handler = logging.StreamHandler()
|
|
78
|
+
handler.setLevel(logging.INFO)
|
|
79
|
+
self._logger.addHandler(handler)
|
|
80
|
+
|
|
81
|
+
# results = pd.read_csv(model_output_1d, sep=";")
|
|
82
|
+
results = model_output_1d
|
|
83
|
+
|
|
84
|
+
self._curv_abscissa_base = results[curvilinear_abscissa_key].unique()
|
|
85
|
+
self._curv_abscissa = results[curvilinear_abscissa_key].unique()
|
|
86
|
+
self._curv_absc_coords = None
|
|
87
|
+
self._curv_absc_normals = None
|
|
88
|
+
|
|
89
|
+
widths = results[widths_key].values
|
|
90
|
+
self._widths = widths.reshape((widths.size // self._curv_abscissa.size, self._curv_abscissa.size))
|
|
91
|
+
|
|
92
|
+
heights = results[heights_key].values
|
|
93
|
+
self._heights = heights.reshape((widths.size // self._curv_abscissa.size, self._curv_abscissa.size))
|
|
94
|
+
|
|
95
|
+
self._centerline = centerline
|
|
96
|
+
if isinstance(crs, int):
|
|
97
|
+
self._crs = pyproj.crs.CRS.from_epsg(crs)
|
|
98
|
+
else:
|
|
99
|
+
self._crs = pyproj.crs.CRS(crs)
|
|
100
|
+
|
|
101
|
+
# Setup distance method from CRS
|
|
102
|
+
if self._crs.to_epsg() == 4326:
|
|
103
|
+
self._distance = geopy.distance.distance
|
|
104
|
+
self._shapely_point = lambda x: Point(x.longitude, x.latitude)
|
|
105
|
+
elif self._crs.axis_info[0].unit_name == "metre" and self._crs.axis_info[1].unit_name == "metre":
|
|
106
|
+
self._distance = sw1dto2d.cartesian.distance
|
|
107
|
+
self._shapely_point = lambda x: x
|
|
108
|
+
else:
|
|
109
|
+
raise RuntimeError("Centerline must have either WGS84 CRS or a CRS with coordinates in meters.")
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def curv_abscissa(self):
|
|
113
|
+
"""Get curv_abscissa parameter."""
|
|
114
|
+
|
|
115
|
+
return self._curv_abscissa
|
|
116
|
+
|
|
117
|
+
def compute_xs_parameters(self, dx: float = None, optimize_normals: bool = True, enforce_length: bool = True, optimize_normals_maxiter: int=1000):
|
|
118
|
+
"""
|
|
119
|
+
Compute cross-sections parameters (coordinates and normals)
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
dx : Resampling spacing (in curvilinear axis)
|
|
124
|
+
optimize_normals : True to optimize normals to prevent cutlines instersections
|
|
125
|
+
enforce_length : True to enforce length of the centerline to the total distance computed from xs
|
|
126
|
+
optimize_normals_maxiter : Maximum number of iteration for normals optimization
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
self._logger.info("-" * 60)
|
|
130
|
+
self._logger.info("Compute cross-sections parameters (coordinates and normals)")
|
|
131
|
+
self._logger.info("-" * 60)
|
|
132
|
+
|
|
133
|
+
# Check centerline is set
|
|
134
|
+
if self._centerline is None:
|
|
135
|
+
raise RuntimeError("centerline is not set yet")
|
|
136
|
+
|
|
137
|
+
if dx is not None:
|
|
138
|
+
L = np.abs(self._curv_abscissa_base[-1] - self._curv_abscissa_base[0])
|
|
139
|
+
n = int(np.round(L / dx))
|
|
140
|
+
self._curv_abscissa = np.linspace(
|
|
141
|
+
self._curv_abscissa_base[0], self._curv_abscissa_base[-1], n + 1, endpoint=True
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Compute alpha (ratio of length) of the cross-sections on the centerline
|
|
145
|
+
self._logger.info("Compute alpha values for the cross-sections")
|
|
146
|
+
xs_alpha = self._compute_xs_alpha(enforce_length)
|
|
147
|
+
|
|
148
|
+
# Compute alpha (ratio of length) of the points on the centerline
|
|
149
|
+
self._logger.info("Compute alpha values for the centerline points")
|
|
150
|
+
cl_alpha = self._compute_cl_alpha()
|
|
151
|
+
|
|
152
|
+
# Compute positions of the cross-sections on the centerline
|
|
153
|
+
self._logger.info("Compute cross-sections centers coordinates")
|
|
154
|
+
xs_coords = self._compute_xs_coords(xs_alpha, cl_alpha)
|
|
155
|
+
|
|
156
|
+
# Compute (raw) normals of the cross-sections on the centerline
|
|
157
|
+
self._logger.info("Compute raw normals")
|
|
158
|
+
xs_normals = self._compute_xs_normals(xs_alpha, cl_alpha)
|
|
159
|
+
self._logger.info("")
|
|
160
|
+
|
|
161
|
+
# Optimize normals
|
|
162
|
+
if optimize_normals is True:
|
|
163
|
+
xs_normals, _ = self._optimize_xs_normals(xs_coords, xs_normals, maxiter=optimize_normals_maxiter)
|
|
164
|
+
# elif optimize_normals == "combine":
|
|
165
|
+
# xs_normals, _ = self._optimize_xs_normals_combine(xs_coords, xs_normals)
|
|
166
|
+
|
|
167
|
+
self._curv_absc_normals = xs_normals
|
|
168
|
+
self._curv_absc_coords = xs_coords
|
|
169
|
+
|
|
170
|
+
def _compute_xs_alpha(self, xs: np.ndarray, enforce_length: bool = True) -> np.ndarray:
|
|
171
|
+
"""
|
|
172
|
+
Compute alpha (ratio of length) of the cross-sections on the centerline
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
xp : array of curvilinear abscissae
|
|
177
|
+
enforce_length : True to enforce length of the centerline to the total distance computed from xs
|
|
178
|
+
|
|
179
|
+
Return
|
|
180
|
+
------
|
|
181
|
+
xs_alpha : array of alpha
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
if enforce_length:
|
|
185
|
+
xs_alpha = (self._curv_abscissa[:] - self._curv_abscissa[0]) / (
|
|
186
|
+
self._curv_abscissa[-1] - self._curv_abscissa[0]
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
raise NotImplementedError("Computation of positions with enforce_length=False is not implemented yet")
|
|
190
|
+
|
|
191
|
+
return xs_alpha
|
|
192
|
+
|
|
193
|
+
def _compute_cl_alpha(self) -> np.ndarray:
|
|
194
|
+
"""
|
|
195
|
+
Compute alpha (ratio of length) of the points of the centerline
|
|
196
|
+
|
|
197
|
+
Return
|
|
198
|
+
------
|
|
199
|
+
cl_alpha : array of alpha values
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
coords = np.array(self._centerline.coords)
|
|
203
|
+
d_x = coords[1:, 0] - coords[0:-1, 0]
|
|
204
|
+
d_y = coords[1:, 1] - coords[0:-1, 1]
|
|
205
|
+
dist = np.cumsum(np.sqrt(d_x**2 + d_y**2))
|
|
206
|
+
dist = np.insert(dist, 0, 0.0)
|
|
207
|
+
cl_alpha = dist / dist[-1]
|
|
208
|
+
|
|
209
|
+
return cl_alpha
|
|
210
|
+
|
|
211
|
+
def _compute_xs_coords(self, xs_alpha: np.ndarray, cl_alpha: np.ndarray) -> np.ndarray:
|
|
212
|
+
"""
|
|
213
|
+
Compute positions of the cross-sections on the centerline
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
xs_alpha : array of alpha (ratio of length) values for the cross-sections
|
|
218
|
+
cl_alpha : array of alpha (ratio of length) values for the points of the centerline
|
|
219
|
+
|
|
220
|
+
Return
|
|
221
|
+
------
|
|
222
|
+
result : array of coordinates
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
coords = np.array(self._centerline.coords)
|
|
226
|
+
x = np.interp(xs_alpha, cl_alpha, coords[:, 0])
|
|
227
|
+
y = np.interp(xs_alpha, cl_alpha, coords[:, 1])
|
|
228
|
+
|
|
229
|
+
return np.stack((x, y), axis=-1)
|
|
230
|
+
|
|
231
|
+
def _compute_xs_normals(self, xs_alpha: np.ndarray, cl_alpha: np.ndarray) -> np.ndarray:
|
|
232
|
+
"""
|
|
233
|
+
Compute normals of the cross-sections on the centerline
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
xs_alpha : array of alpha (ratio of length) values for the cross-sections
|
|
238
|
+
cl_alpha : array of alpha (ratio of length) values for the points of the centerline
|
|
239
|
+
|
|
240
|
+
Return
|
|
241
|
+
------
|
|
242
|
+
result : array of normals
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
coords = np.array(self._centerline.coords)
|
|
246
|
+
dx = coords[2:, 0] - coords[0:-2, 0]
|
|
247
|
+
dy = coords[2:, 1] - coords[0:-2, 1]
|
|
248
|
+
dx = np.insert(dx, 0, coords[1, 0] - coords[0, 0])
|
|
249
|
+
dx = np.append(dx, coords[-1, 0] - coords[-2, 0])
|
|
250
|
+
dy = np.insert(dy, 0, coords[1, 1] - coords[0, 1])
|
|
251
|
+
dy = np.append(dy, coords[-1, 1] - coords[-2, 1])
|
|
252
|
+
norm = np.sqrt(dx**2 + dy**2)
|
|
253
|
+
|
|
254
|
+
cl_nx = dy / norm
|
|
255
|
+
cl_ny = -dx / norm
|
|
256
|
+
|
|
257
|
+
xs_nx = np.interp(xs_alpha, cl_alpha, cl_nx)
|
|
258
|
+
xs_ny = np.interp(xs_alpha, cl_alpha, cl_ny)
|
|
259
|
+
|
|
260
|
+
return np.stack((xs_nx, xs_ny), axis=-1)
|
|
261
|
+
|
|
262
|
+
def _optimize_xs_normals( # noqa: C901
|
|
263
|
+
self, xs_coords: np.ndarray, xs_normals: np.ndarray, maxiter: int=1000
|
|
264
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
265
|
+
"""
|
|
266
|
+
Optimize normals by filtering to prevent intersections
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
xs_coords : array of coordinates of the cross-sections
|
|
271
|
+
xs_normals : array of normals of the cross-sections
|
|
272
|
+
maxiter : Maximum number of iterations
|
|
273
|
+
|
|
274
|
+
Return
|
|
275
|
+
------
|
|
276
|
+
result : tuple (xs_normals, intersect_flag) where xs_normals is a array of normals and intersect_flag an
|
|
277
|
+
array of intersection flag
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
self._logger.info("-" * 60)
|
|
281
|
+
self._logger.info("Optimization of normals to prevent intersections")
|
|
282
|
+
self._logger.info("-" * 60)
|
|
283
|
+
|
|
284
|
+
# Compute normals angles
|
|
285
|
+
angles = np.arctan2(xs_normals[:, 0], xs_normals[:, 1]) * 180.0 / np.pi
|
|
286
|
+
# angles_base = angles.copy()
|
|
287
|
+
|
|
288
|
+
# Compute max widths
|
|
289
|
+
Wmax = np.max(self._widths, axis=0)
|
|
290
|
+
|
|
291
|
+
if self._curv_abscissa_base.size != self._curv_abscissa.size:
|
|
292
|
+
Wmax = np.interp(self._curv_abscissa, self._curv_abscissa_base, Wmax)
|
|
293
|
+
|
|
294
|
+
nintersect = 1
|
|
295
|
+
iter = 0
|
|
296
|
+
while nintersect > 0 and iter < maxiter:
|
|
297
|
+
|
|
298
|
+
iter += 1
|
|
299
|
+
|
|
300
|
+
# Compute cross-sections lines
|
|
301
|
+
xs_lines = []
|
|
302
|
+
|
|
303
|
+
for ix in range(0, xs_coords.shape[0]):
|
|
304
|
+
Wdemi = 0.5 * Wmax[ix]
|
|
305
|
+
bearing1 = angles[ix]
|
|
306
|
+
|
|
307
|
+
# TODO correct bearing !!! 0=North, 90=east, ect :/
|
|
308
|
+
if angles[ix] > 180.0:
|
|
309
|
+
bearing2 = angles[ix] - 180.0
|
|
310
|
+
else:
|
|
311
|
+
bearing2 = angles[ix] + 180.0
|
|
312
|
+
|
|
313
|
+
center = (xs_coords[ix, 1], xs_coords[ix, 0])
|
|
314
|
+
point1 = self._shapely_point(self._distance(meters=Wdemi).destination(center, bearing=bearing1))
|
|
315
|
+
point2 = self._shapely_point(self._distance(meters=Wdemi).destination(center, bearing=bearing2))
|
|
316
|
+
# point1 = geopy.distance.distance(meters=Wdemi).destination(center, bearing=bearing1)
|
|
317
|
+
# point2 = geopy.distance.distance(meters=Wdemi).destination(center, bearing=bearing2)
|
|
318
|
+
|
|
319
|
+
xs_lines.append(LineString([(point1.x, point1.y), (point2.x, point2.y)]))
|
|
320
|
+
|
|
321
|
+
# Flag intersecting lines
|
|
322
|
+
intersect_flag = np.zeros(xs_coords.shape[0], dtype=int)
|
|
323
|
+
|
|
324
|
+
for ix1 in range(0, xs_coords.shape[0]):
|
|
325
|
+
for ix2 in range(ix1 + 1, xs_coords.shape[0]):
|
|
326
|
+
if xs_lines[ix1].intersects(xs_lines[ix2]):
|
|
327
|
+
intersect_flag[ix1] += 1
|
|
328
|
+
intersect_flag[ix2] += 1
|
|
329
|
+
nintersect = np.sum(intersect_flag)
|
|
330
|
+
|
|
331
|
+
self._logger.info(f"Number of intersections: {nintersect}")
|
|
332
|
+
|
|
333
|
+
if nintersect == 0:
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
indices = np.argwhere(intersect_flag > 0).flatten()
|
|
337
|
+
seq = indices[1:] - indices[:-1]
|
|
338
|
+
seq = np.insert(seq, 0, 2)
|
|
339
|
+
seq_start = np.argwhere(seq > 1).flatten()
|
|
340
|
+
|
|
341
|
+
self._logger.info(f"Number of intersecting ranges: {len(seq_start)}")
|
|
342
|
+
|
|
343
|
+
for i in range(0, len(seq_start)):
|
|
344
|
+
start = indices[seq_start[i]]
|
|
345
|
+
|
|
346
|
+
if i < len(seq_start) - 1:
|
|
347
|
+
end = indices[seq_start[i + 1] - 1]
|
|
348
|
+
else:
|
|
349
|
+
end = indices[-1]
|
|
350
|
+
|
|
351
|
+
self._logger.info("Processing intersecting range: %i-%i" % (start, end))
|
|
352
|
+
|
|
353
|
+
nintersect2 = 1
|
|
354
|
+
while nintersect2 > 0:
|
|
355
|
+
new_angles = angles.copy()
|
|
356
|
+
xr = self._curv_abscissa[start : end + 1]
|
|
357
|
+
xb = [self._curv_abscissa[start], self._curv_abscissa[(start + end) // 2], self._curv_abscissa[end]]
|
|
358
|
+
|
|
359
|
+
if xr[-1] < xr[0]:
|
|
360
|
+
xr = xr[0] - xr
|
|
361
|
+
xb = xb[0] - xb
|
|
362
|
+
|
|
363
|
+
anglesb = [angles[start], angles[(start + end) // 2], angles[end]]
|
|
364
|
+
new_angles[start : end + 1] = np.interp(xr, xb, anglesb)
|
|
365
|
+
|
|
366
|
+
# Compute new lines
|
|
367
|
+
new_lines = xs_lines.copy()
|
|
368
|
+
for ix in range(start, end + 1):
|
|
369
|
+
Wdemi = 0.5 * Wmax[ix]
|
|
370
|
+
bearing1 = new_angles[ix]
|
|
371
|
+
|
|
372
|
+
if new_angles[ix] > 180.0:
|
|
373
|
+
bearing2 = new_angles[ix] - 180.0
|
|
374
|
+
else:
|
|
375
|
+
bearing2 = new_angles[ix] + 180.0
|
|
376
|
+
|
|
377
|
+
center = (xs_coords[ix, 1], xs_coords[ix, 0])
|
|
378
|
+
point1 = self._shapely_point(self._distance(meters=Wdemi).destination(center, bearing=bearing1))
|
|
379
|
+
point2 = self._shapely_point(self._distance(meters=Wdemi).destination(center, bearing=bearing2))
|
|
380
|
+
# point1 = geopy.distance.distance(meters=Wdemi).destination(center, bearing=bearing1)
|
|
381
|
+
# point2 = geopy.distance.distance(meters=Wdemi).destination(center, bearing=bearing2)
|
|
382
|
+
new_lines[ix] = LineString([(point1.x, point1.y), (point2.x, point2.y)])
|
|
383
|
+
|
|
384
|
+
# Check for remaining intersections
|
|
385
|
+
nintersect2 = 0
|
|
386
|
+
for ix1 in range(max(0, start - 2), min(end + 3, xs_coords.shape[0])):
|
|
387
|
+
for ix2 in range(ix1 + 1, min(end + 3, xs_coords.shape[0])):
|
|
388
|
+
if new_lines[ix1].intersects(new_lines[ix2]):
|
|
389
|
+
nintersect2 += 1
|
|
390
|
+
|
|
391
|
+
if nintersect2 > 0:
|
|
392
|
+
start = max(0, start - 1)
|
|
393
|
+
end = min(end + 1, xs_coords.shape[0] - 1)
|
|
394
|
+
|
|
395
|
+
self._logger.info("Intersecting range filtered with range[%i-%i]" % (start, end))
|
|
396
|
+
|
|
397
|
+
xs_lines[:] = new_lines[:]
|
|
398
|
+
angles[:] = new_angles[:]
|
|
399
|
+
del new_lines
|
|
400
|
+
|
|
401
|
+
# Recompute normals
|
|
402
|
+
xs_normals[:, 0] = np.sin(angles * np.pi / 180.0)
|
|
403
|
+
xs_normals[:, 1] = np.cos(angles * np.pi / 180.0)
|
|
404
|
+
|
|
405
|
+
if iter >= maxiter:
|
|
406
|
+
logging.error("Optimisation failed: maximum iterations reached.")
|
|
407
|
+
else:
|
|
408
|
+
self._logger.info("Optimisation successful (%i iterations)." % iter)
|
|
409
|
+
self._logger.info("")
|
|
410
|
+
|
|
411
|
+
return xs_normals, intersect_flag
|
|
412
|
+
|
|
413
|
+
def _combine_intersecting_cutlines( # noqa: C901
|
|
414
|
+
self, xs_normals: np.ndarray, xs_lines: list
|
|
415
|
+
) -> list:
|
|
416
|
+
"""
|
|
417
|
+
Optimize normals by filtering to prevent intersections
|
|
418
|
+
|
|
419
|
+
Parameters
|
|
420
|
+
----------
|
|
421
|
+
xs_lines : list of cutlines
|
|
422
|
+
|
|
423
|
+
Return
|
|
424
|
+
------
|
|
425
|
+
result : list
|
|
426
|
+
Corrected cutlines
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
self._logger.info("-" * 60)
|
|
430
|
+
self._logger.info("Correction of cutlines to remove intersections")
|
|
431
|
+
self._logger.info("-" * 60)
|
|
432
|
+
|
|
433
|
+
angles = np.arctan2(xs_normals[:, 0], xs_normals[:, 1]) * 180.0 / np.pi
|
|
434
|
+
Wmax = np.max(self._widths, axis=0)
|
|
435
|
+
if self._curv_abscissa_base.size != self._curv_abscissa.size:
|
|
436
|
+
Wmax = np.interp(self._curv_abscissa, self._curv_abscissa_base, Wmax)
|
|
437
|
+
|
|
438
|
+
# Correct left parts
|
|
439
|
+
left_corrections = 0
|
|
440
|
+
ix1 = 0
|
|
441
|
+
while ix1 < len(xs_lines) - 1:
|
|
442
|
+
xs_part1 = LineString(xs_lines[ix1].coords[:-1])
|
|
443
|
+
ix2 = ix1 + 1
|
|
444
|
+
intersections = []
|
|
445
|
+
while True:
|
|
446
|
+
xs_part2 = LineString(xs_lines[ix2].coords[:-1])
|
|
447
|
+
if xs_part1.intersects(xs_part2):
|
|
448
|
+
intersections.append(xs_part1.intersection(xs_part2))
|
|
449
|
+
ix2 += 1
|
|
450
|
+
if ix2 >= len(xs_lines):
|
|
451
|
+
break
|
|
452
|
+
else:
|
|
453
|
+
break
|
|
454
|
+
|
|
455
|
+
if len(intersections) > 0:
|
|
456
|
+
x = np.mean([point.x for point in intersections])
|
|
457
|
+
y = np.mean([point.y for point in intersections])
|
|
458
|
+
combinedPoint = Point(x, y)
|
|
459
|
+
for ix in range(ix1, ix1+len(intersections)):
|
|
460
|
+
|
|
461
|
+
center = (xs_lines[ix].coords[1][1], xs_lines[ix].coords[1][0])
|
|
462
|
+
left_width = geopy.distance.geodesic(center, (combinedPoint.y, combinedPoint.x)).m
|
|
463
|
+
right_width = Wmax[ix] - left_width
|
|
464
|
+
if angles[ix] > 180.0:
|
|
465
|
+
bearing2 = angles[ix] - 180.0
|
|
466
|
+
else:
|
|
467
|
+
bearing2 = angles[ix] + 180.0
|
|
468
|
+
rightPoint = self._shapely_point(self._distance(meters=right_width).destination(center, bearing=bearing2))
|
|
469
|
+
|
|
470
|
+
xs_lines[ix] = LineString([(combinedPoint.x, combinedPoint.y), (center[1], center[0]), (rightPoint.x, rightPoint.y)])
|
|
471
|
+
|
|
472
|
+
ix1 += len(intersections)
|
|
473
|
+
left_corrections += len(intersections)
|
|
474
|
+
|
|
475
|
+
else:
|
|
476
|
+
|
|
477
|
+
ix1 += 1
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# Correct right parts
|
|
481
|
+
right_corrections = 0
|
|
482
|
+
ix1 = 0
|
|
483
|
+
while ix1 < len(xs_lines) - 1:
|
|
484
|
+
xs_part1 = LineString(xs_lines[ix1].coords[1:])
|
|
485
|
+
ix2 = ix1 + 1
|
|
486
|
+
intersections = []
|
|
487
|
+
while True:
|
|
488
|
+
xs_part2 = LineString(xs_lines[ix2].coords[1:])
|
|
489
|
+
if xs_part1.intersects(xs_part2):
|
|
490
|
+
intersections.append(xs_part1.intersection(xs_part2))
|
|
491
|
+
ix2 += 1
|
|
492
|
+
if ix2 >= len(xs_lines):
|
|
493
|
+
break
|
|
494
|
+
else:
|
|
495
|
+
break
|
|
496
|
+
|
|
497
|
+
if len(intersections) > 0:
|
|
498
|
+
x = np.mean([point.x for point in intersections])
|
|
499
|
+
y = np.mean([point.y for point in intersections])
|
|
500
|
+
combinedPoint = Point(x, y)
|
|
501
|
+
for ix in range(ix1, ix1+len(intersections)):
|
|
502
|
+
|
|
503
|
+
center = (xs_lines[ix].coords[1][1], xs_lines[ix].coords[1][0])
|
|
504
|
+
right_width = geopy.distance.geodesic(center, (combinedPoint.y, combinedPoint.x)).m
|
|
505
|
+
left_width = Wmax[ix] - right_width
|
|
506
|
+
bearing1 = angles[ix]
|
|
507
|
+
leftPoint = self._shapely_point(self._distance(meters=left_width).destination(center, bearing=bearing1))
|
|
508
|
+
xs_lines[ix] = LineString([(leftPoint.x, leftPoint.y), (center[1], center[0]), (combinedPoint.x, combinedPoint.y)])
|
|
509
|
+
|
|
510
|
+
ix1 += len(intersections)
|
|
511
|
+
right_corrections += len(intersections)
|
|
512
|
+
|
|
513
|
+
else:
|
|
514
|
+
|
|
515
|
+
ix1 += 1
|
|
516
|
+
|
|
517
|
+
# if iter >= maxiter:
|
|
518
|
+
# logging.error("Optimisation failed: maximum iterations reached.")
|
|
519
|
+
# else:
|
|
520
|
+
# self._logger.info("Optimisation successful (%i iterations)." % iter)
|
|
521
|
+
self._logger.info("")
|
|
522
|
+
|
|
523
|
+
return xs_lines
|
|
524
|
+
|
|
525
|
+
def compute_xs_cutlines(self, it: int = None, extend: int = None, combine_intersections: bool = True) -> List[LineString]:
|
|
526
|
+
"""
|
|
527
|
+
Compute cutlines of the cross-sections on the centerline
|
|
528
|
+
|
|
529
|
+
Parameters
|
|
530
|
+
----------
|
|
531
|
+
it : index of time occurence (in the H and W arrays)
|
|
532
|
+
extend : extra extension. Final cutline has length W[it] + extend
|
|
533
|
+
|
|
534
|
+
Return
|
|
535
|
+
------
|
|
536
|
+
xs_lines : list of LineStrings objects for the cutlines
|
|
537
|
+
"""
|
|
538
|
+
|
|
539
|
+
xs_coords = self._curv_absc_coords
|
|
540
|
+
xs_normals = self._curv_absc_normals
|
|
541
|
+
angles = np.arctan2(xs_normals[:, 0], xs_normals[:, 1]) * 180.0 / np.pi
|
|
542
|
+
|
|
543
|
+
# Compute widths
|
|
544
|
+
if it is None or it == "max":
|
|
545
|
+
Wit = np.max(self._widths, axis=0)
|
|
546
|
+
elif it == "min":
|
|
547
|
+
Wit = np.min(self._widths, axis=0)
|
|
548
|
+
elif it == "mean":
|
|
549
|
+
Wit = np.mean(self._widths, axis=0)
|
|
550
|
+
else:
|
|
551
|
+
Wit = self._widths[it, :]
|
|
552
|
+
|
|
553
|
+
if self._curv_abscissa_base.size != self._curv_abscissa.size:
|
|
554
|
+
Wit = np.interp(self._curv_abscissa, self._curv_abscissa_base, Wit)
|
|
555
|
+
|
|
556
|
+
if extend is not None:
|
|
557
|
+
Wit = Wit + extend
|
|
558
|
+
|
|
559
|
+
xs_lines = []
|
|
560
|
+
for ix in range(0, xs_coords.shape[0]):
|
|
561
|
+
Wdemi = 0.5 * Wit[ix]
|
|
562
|
+
bearing1 = angles[ix]
|
|
563
|
+
|
|
564
|
+
if angles[ix] > 180.0:
|
|
565
|
+
bearing2 = angles[ix] - 180.0
|
|
566
|
+
else:
|
|
567
|
+
bearing2 = angles[ix] + 180.0
|
|
568
|
+
|
|
569
|
+
center = (xs_coords[ix, 1], xs_coords[ix, 0])
|
|
570
|
+
point1 = self._shapely_point(self._distance(meters=Wdemi).destination(center, bearing=bearing1))
|
|
571
|
+
point2 = self._shapely_point(self._distance(meters=Wdemi).destination(center, bearing=bearing2))
|
|
572
|
+
# point1 = geopy.distance.distance(meters=Wdemi).destination(center, bearing=bearing1)
|
|
573
|
+
# point2 = geopy.distance.distance(meters=Wdemi).destination(center, bearing=bearing2)
|
|
574
|
+
|
|
575
|
+
xs_lines.append(LineString([(point1.x, point1.y), (xs_coords[ix, 0], xs_coords[ix, 1]), (point2.x, point2.y)]))
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
if combine_intersections is True:
|
|
579
|
+
|
|
580
|
+
xs_lines = self._combine_intersecting_cutlines(xs_normals, xs_lines)
|
|
581
|
+
|
|
582
|
+
return xs_lines
|
|
583
|
+
|
|
584
|
+
def compute_water_mask(self, it: int = None):
|
|
585
|
+
"""Compute watermask
|
|
586
|
+
|
|
587
|
+
Parameters
|
|
588
|
+
----------
|
|
589
|
+
it : index of time occurence (in the H and W arrays)
|
|
590
|
+
|
|
591
|
+
Return
|
|
592
|
+
------
|
|
593
|
+
polygon: Polygon object of the water mask
|
|
594
|
+
"""
|
|
595
|
+
|
|
596
|
+
xs_coords = self._curv_absc_coords
|
|
597
|
+
xs_normals = self._curv_absc_normals
|
|
598
|
+
angles = np.arctan2(xs_normals[:, 0], xs_normals[:, 1]) * 180.0 / np.pi
|
|
599
|
+
|
|
600
|
+
# Compute widths
|
|
601
|
+
if it is None or it == "max":
|
|
602
|
+
Wit = np.max(self._widths, axis=0)
|
|
603
|
+
elif it == "min":
|
|
604
|
+
Wit = np.min(self._widths, axis=0)
|
|
605
|
+
elif it == "mean":
|
|
606
|
+
Wit = np.mean(self._widths, axis=0)
|
|
607
|
+
else:
|
|
608
|
+
Wit = self._widths[it, :]
|
|
609
|
+
|
|
610
|
+
if self._curv_abscissa_base.size != self._curv_abscissa.size:
|
|
611
|
+
Wit = np.interp(self._curv_abscissa, self._curv_abscissa_base, Wit)
|
|
612
|
+
|
|
613
|
+
left_points = []
|
|
614
|
+
right_points = []
|
|
615
|
+
for ix in range(0, xs_coords.shape[0]):
|
|
616
|
+
Wdemi = 0.5 * Wit[ix]
|
|
617
|
+
bearing1 = angles[ix]
|
|
618
|
+
|
|
619
|
+
if angles[ix] > 180.0:
|
|
620
|
+
bearing2 = angles[ix] - 180.0
|
|
621
|
+
else:
|
|
622
|
+
bearing2 = angles[ix] + 180.0
|
|
623
|
+
|
|
624
|
+
center = (xs_coords[ix, 1], xs_coords[ix, 0])
|
|
625
|
+
point1 = geopy.distance.distance(meters=Wdemi).destination(center, bearing=bearing1)
|
|
626
|
+
point2 = geopy.distance.distance(meters=Wdemi).destination(center, bearing=bearing2)
|
|
627
|
+
left_points.append((point2.longitude, point2.latitude))
|
|
628
|
+
right_points.insert(0, (point1.longitude, point1.latitude))
|
|
629
|
+
|
|
630
|
+
polygon = Polygon(right_points + left_points)
|
|
631
|
+
|
|
632
|
+
return polygon
|
|
633
|
+
|
|
634
|
+
def compute_xs_points(
|
|
635
|
+
self,
|
|
636
|
+
main_channel: int = None,
|
|
637
|
+
overbanks: int = None,
|
|
638
|
+
extend: int = None,
|
|
639
|
+
overbank_slope: float = 0.0001,
|
|
640
|
+
epsg: int = 4326,
|
|
641
|
+
) -> Tuple[List[Point], List[Dict]]:
|
|
642
|
+
"""
|
|
643
|
+
Compute cutlines of the cross-sections on the centerline
|
|
644
|
+
|
|
645
|
+
Parameters
|
|
646
|
+
----------
|
|
647
|
+
main_channel : number of points in the main channel cross-section part
|
|
648
|
+
overbanks : number of points in each overbank part
|
|
649
|
+
extend : TODO
|
|
650
|
+
overbank_slope : overbank slope
|
|
651
|
+
epsg : EPSG code
|
|
652
|
+
|
|
653
|
+
Return
|
|
654
|
+
------
|
|
655
|
+
result : tuple (points, attributes) with points is a list of LineStrings objects for the cutlines,
|
|
656
|
+
and attributes the corresponding points attributes
|
|
657
|
+
"""
|
|
658
|
+
|
|
659
|
+
xs_coords = self._curv_absc_coords
|
|
660
|
+
xs_normals = self._curv_absc_normals
|
|
661
|
+
angles = np.arctan2(xs_normals[:, 0], xs_normals[:, 1]) * 180.0 / np.pi
|
|
662
|
+
|
|
663
|
+
Wit = np.max(self._widths, axis=0)
|
|
664
|
+
if self._curv_abscissa_base.size != self._curv_abscissa.size:
|
|
665
|
+
Wit = np.interp(self._curv_abscissa, self._curv_abscissa_base, Wit)
|
|
666
|
+
|
|
667
|
+
# Create pyporj transformer
|
|
668
|
+
transformer = Transformer.from_crs(4326, epsg, always_xy=True)
|
|
669
|
+
|
|
670
|
+
points = []
|
|
671
|
+
attributes = []
|
|
672
|
+
for ix in range(0, xs_coords.shape[0]):
|
|
673
|
+
# index0 = len(points)
|
|
674
|
+
|
|
675
|
+
# Compute left overbank points
|
|
676
|
+
Hmax = np.max(self._heights[:, ix])
|
|
677
|
+
alpha_widths = np.linspace(1.0, 0.0, overbanks, endpoint=False)
|
|
678
|
+
for index in range(0, overbanks):
|
|
679
|
+
dist = -(0.5 * Wit[ix] + alpha_widths[index] * extend)
|
|
680
|
+
bearing1 = angles[ix]
|
|
681
|
+
center = (xs_coords[ix, 1], xs_coords[ix, 0])
|
|
682
|
+
point = geopy.distance.distance(meters=dist).destination(center, bearing=bearing1)
|
|
683
|
+
points.append(Point(point.longitude, point.latitude))
|
|
684
|
+
x, y = transformer.transform(point.longitude, point.latitude)
|
|
685
|
+
z = Hmax + alpha_widths[index] * overbank_slope * extend
|
|
686
|
+
attributes.append(
|
|
687
|
+
{
|
|
688
|
+
"xs_id": ix,
|
|
689
|
+
"xsnd_id": index,
|
|
690
|
+
"abs": self._curv_abscissa[ix],
|
|
691
|
+
"loc": "LOB",
|
|
692
|
+
"x": x,
|
|
693
|
+
"y": y,
|
|
694
|
+
"z": z,
|
|
695
|
+
}
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Interpolate (H,W) tuples sorted with increasing H for the main channel
|
|
699
|
+
Hx = self._heights[:, ix]
|
|
700
|
+
Wx = self._widths[:, ix]
|
|
701
|
+
index_sorted = np.argsort(Hx)
|
|
702
|
+
Hx = Hx[index_sorted]
|
|
703
|
+
Wx = Wx[index_sorted]
|
|
704
|
+
|
|
705
|
+
# Compute main channel points
|
|
706
|
+
alpha_widths = np.linspace(-0.5, 0.5, main_channel, endpoint=True)
|
|
707
|
+
Wp = 2.0 * np.abs(alpha_widths * Wit[ix])
|
|
708
|
+
Hp = np.interp(Wp, Wx, Hx)
|
|
709
|
+
for index in range(0, main_channel):
|
|
710
|
+
dist = alpha_widths[index] * Wit[ix]
|
|
711
|
+
bearing1 = angles[ix]
|
|
712
|
+
center = (xs_coords[ix, 1], xs_coords[ix, 0])
|
|
713
|
+
point = geopy.distance.distance(meters=dist).destination(center, bearing=bearing1)
|
|
714
|
+
points.append(Point(point.longitude, point.latitude))
|
|
715
|
+
x, y = transformer.transform(point.longitude, point.latitude)
|
|
716
|
+
z = Hp[index]
|
|
717
|
+
attributes.append(
|
|
718
|
+
{
|
|
719
|
+
"xs_id": ix,
|
|
720
|
+
"xsnd_id": index + overbanks,
|
|
721
|
+
"abs": self._curv_abscissa[ix],
|
|
722
|
+
"loc": "MC",
|
|
723
|
+
"x": x,
|
|
724
|
+
"y": y,
|
|
725
|
+
"z": z,
|
|
726
|
+
}
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
# Compute right overbank points
|
|
730
|
+
Hmax = np.max(self._heights[:, ix])
|
|
731
|
+
alpha_widths = np.linspace(1.0, 0.0, overbanks, endpoint=False)
|
|
732
|
+
alpha_widths = 1.0 - alpha_widths
|
|
733
|
+
for index in range(0, overbanks):
|
|
734
|
+
dist = 0.5 * Wit[ix] + alpha_widths[index] * extend
|
|
735
|
+
bearing1 = angles[ix]
|
|
736
|
+
center = (xs_coords[ix, 1], xs_coords[ix, 0])
|
|
737
|
+
point = geopy.distance.distance(meters=dist).destination(center, bearing=bearing1)
|
|
738
|
+
points.append(Point(point.longitude, point.latitude))
|
|
739
|
+
x, y = transformer.transform(point.longitude, point.latitude)
|
|
740
|
+
z = Hmax + alpha_widths[index] * overbank_slope * extend
|
|
741
|
+
attributes.append(
|
|
742
|
+
{
|
|
743
|
+
"xs_id": ix,
|
|
744
|
+
"xsnd_id": index + main_channel + overbanks,
|
|
745
|
+
"abs": self._curv_abscissa[ix],
|
|
746
|
+
"loc": "ROB",
|
|
747
|
+
"x": x,
|
|
748
|
+
"y": y,
|
|
749
|
+
"z": z,
|
|
750
|
+
}
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
return points, attributes
|