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/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