wolfhece 2.2.43__py3-none-any.whl → 2.2.44__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.
@@ -12,7 +12,7 @@ from numpy import asarray,ndarray,arange,zeros,linspace,concatenate,unique,amin,
12
12
  import math
13
13
  import matplotlib.pyplot as plt
14
14
  from shapely.geometry import LineString,MultiLineString,Point,Polygon,CAP_STYLE,Point
15
- from shapely.prepared import prep
15
+ from shapely import prepare, is_prepared, destroy_prepared
16
16
  from shapely.ops import nearest_points,substring
17
17
  from OpenGL.GL import *
18
18
  import numpy as np
@@ -28,7 +28,7 @@ import wx
28
28
  from typing import Union, Literal
29
29
  from pathlib import Path
30
30
  import pandas as pd
31
-
31
+ from numba import njit
32
32
 
33
33
  from .PyTranslate import _
34
34
  from .drawing_obj import Element_To_Draw
@@ -72,7 +72,8 @@ example_diffsect1="""0 10
72
72
  42 7
73
73
  50 10"""
74
74
 
75
- def INTERSEC(x1,y1,x2,y2,el):
75
+ @njit
76
+ def INTERSEC(x1:float, y1:float, x2:float, y2:float, el:float):
76
77
  """Procédure de calcul de l'abscisse d'intersection d'une altitude donnée el
77
78
  dans un segment défini par ses coordonnées (x1,y1) et (x2,y2)"""
78
79
  xx=1.0e10
@@ -90,14 +91,19 @@ def INTERSEC(x1,y1,x2,y2,el):
90
91
  xx=(el-b)/a
91
92
  return xx
92
93
 
93
- def partranslation(base, x, y ):
94
+ def _shift_np(base:np.ndarray, x:float, y:float):
95
+ """ Copy a Numpy array and translate it by (x,y) in the XY plane """
94
96
 
95
97
  copie = base.copy()
96
98
  copie[:,0] += x
97
99
  copie[:,1] += y
98
- return(copie)
100
+ return copie
101
+
102
+ @njit
103
+ def _find_shift_at_xy(section:np.ndarray, x3:float, y3:float):
104
+ """ Find the Delta X and Delta Y to shift a section
105
+ to be parallel to itself at point (x3,y3) """
99
106
 
100
- def find_xy(section,x3,y3):
101
107
  x1 = section[0,0]
102
108
  y1 = section[0,1]
103
109
  x2 = section[-1,0]
@@ -110,9 +116,9 @@ def find_xy(section,x3,y3):
110
116
  a = (y2-y1)/(x2-x1)
111
117
  b = y1-(a*x1)
112
118
 
113
- vecteur = ([1,a])
114
- normale = ([-a,1])
115
- normale_opposée = ([a,-1])
119
+ # vecteur = ([1,a])
120
+ # normale = ([-a,1])
121
+ # normale_opposée = ([a,-1])
116
122
 
117
123
  c = -1/a
118
124
  d = y3-(c*x3)
@@ -135,19 +141,40 @@ class postype(Enum):
135
141
 
136
142
  class profile(vector):
137
143
  """
138
- Surcharge d'un vecteur en vue de définir un profil de rivière
144
+ Subclass of a vector to define a river profile/cross-section.
145
+
146
+ Some attributes are added to manage specific points of the profile (banks, bed).
147
+ The bankleft, bankright, bed, bankleft_down, bankright_down attributes can be wolfvertex,
148
+ index of vertex or s3D position depending on banksbed_postype.
149
+
150
+ If a profile is part of a croossection object, the parent attribute points to it.
151
+ If the profiles are ordered along a river, the up and down attributes point to the upstream and downstream profiles.
139
152
 
140
- Les coordonnées attendues sont en 3D (x,y,z)
153
+ sdatum and zdatum are offsets to be added to the curvilinear abscissa and altitudes of the profile.
154
+
155
+ The stored coordinates are in 3D (x, y, z). If data_sect is provided at initialization, we expect (X, Z) data.
156
+ The x coordinate is the curvilinear abscissa along the section and z is the altitude (y is set to 0).
157
+
158
+ ATTENTION:
159
+ Normally, a profile is a succession of points in a vertical plane, aligned along a trace.
160
+ In this implementation, the points can be in 3D space, not necessarily aligned.
161
+ Some routines have a 'cumul' parameter to compute the curvilinear abscissa along the section.
162
+ If 'cumul' is True, the real curvilinear abscissa is computed.
163
+ If 'cumul' is False, the distance **between each point and the first point** is computed.
141
164
  """
142
165
 
143
- def __init__(self, name, data_sect='',parent=None) -> None:
166
+ def __init__(self, name:str, data_sect:str | np.ndarray | list | vector = '', parent = None) -> None:
167
+ """ Initialization of a profile
168
+
169
+ :param name: name of the profile
170
+ :param data_sect: section data as a string (x z) separated by spaces and line breaks
171
+ :param parent: parent object (crosssections)
172
+ """
173
+
144
174
  super().__init__(name=name)
145
175
 
146
- if data_sect!='':
147
- for curline in data_sect.splitlines():
148
- values=curline.split(' ')
149
- curvert=wolfvertex(float(values[0]),0.,float(values[1]))
150
- self.add_vertex(curvert)
176
+ if not isinstance(data_sect, (str, np.ndarray, list, vector)):
177
+ raise TypeError(_('data_sect must be a string, numpy array or list'))
151
178
 
152
179
  # Les positions de référence sont intanciées à None
153
180
  # Elles sont accessibles comme "property" de l'objet --> valeur "brute"
@@ -156,9 +183,6 @@ class profile(vector):
156
183
  # - bankleft_s3D, bankright_s3D, bed_s3D qui retournent la position curvi 3D quel que soit le format de stockage
157
184
  # - bankleft_sz, bankright_sz, bed_sz qui retournent un couple (s,z) quel que soit le format de stockage
158
185
 
159
-
160
-
161
-
162
186
  self._bankleft=None
163
187
  self._bankright=None
164
188
  self._bed=None
@@ -174,27 +198,85 @@ class profile(vector):
174
198
  self.up:profile = None
175
199
  self.down:profile = None
176
200
 
177
- self.laz=False
201
+ self.laz = False # if True, points from LAZ file are loaded around the section
178
202
 
179
203
  if parent is not None:
180
204
  assert isinstance(parent,crosssections), _('Bad type of parent object')
205
+
181
206
  self.parent=parent
182
207
 
183
- self.zdatum = 0.
184
- self.add_zdatum=False
208
+ # self.zdatum = 0.
209
+ # self.add_zdatum = False
185
210
 
186
- self.sdatum = 0.
187
- self.add_sdatum=False
211
+ # self.sdatum = 0.
212
+ # self.add_sdatum = False
188
213
 
189
214
  self.orient = None
190
215
 
191
216
  self.sz = None
192
217
  self.sz_bankbed = None
193
218
  self.s3d_bankbed = None
194
- self.prepared=False # if True, one can call self.sz instead of self.get_sz
219
+ self.prepared = False # if True, one can call self.sz instead of self.get_sz
220
+
221
+ self.smin = -99999
222
+ self.smax = -99999
223
+ self.zmin = -99999
224
+ self.zmax = -99999
225
+
226
+ # Uniform discharge
227
+ self.q_slope = None # Based on imposed slope and roughness
228
+ self.q_down = 0. # Based on downstream slope (if oriented sections exist) and roughness
229
+ self.q_up = 0. # Based on upstream slope (if oriented sections exist) and roughness
230
+ self.q_centered = 0. # Based on centered slope (if oriented sections exist) and roughness
231
+
232
+ self.wetarea = None
233
+ self.wetperimeter = None
234
+ self.hydraulicradius = None
235
+ self.waterdepth = None
236
+ self.localwidth = None
237
+ self.criticaldischarge = None
238
+
239
+ self._linestring_sz = None
240
+ self._linestring_s3dz = None
241
+
242
+ if isinstance(data_sect, str):
243
+ if data_sect != '':
244
+ for curline in data_sect.splitlines():
245
+ values=curline.split(' ')
246
+ curvert=wolfvertex(float(values[0]), 0., float(values[1]))
247
+ self.add_vertex(curvert)
248
+ self.prepare()
249
+ else:
250
+ logging.debug(_('Empty data_sect string -- no vertex added'))
251
+ elif isinstance(data_sect, np.ndarray):
252
+ if data_sect.shape[1]==2:
253
+ for cur in data_sect:
254
+ curvert=wolfvertex(float(cur[0]), 0., float(cur[1]))
255
+ self.add_vertex(curvert)
256
+ self.prepare()
257
+ else:
258
+ logging.error(_('Bad shape of input array in data_sect -- no vertex added'))
259
+ elif isinstance(data_sect, list):
260
+ if all(isinstance(item, (list, tuple, np.ndarray)) and len(item)==2 for item in data_sect):
261
+ for cur in data_sect:
262
+ curvert=wolfvertex(float(cur[0]), 0., float(cur[1]))
263
+ self.add_vertex(curvert)
264
+ self.prepare()
265
+ elif all(isinstance(item, wolfvertex) for item in data_sect):
266
+ self.add_vertex(data_sect)
267
+ self.prepare()
268
+ else:
269
+ logging.error(_('Bad shape of input list in data_sect -- no vertex added'))
270
+ elif isinstance(data_sect, vector):
271
+ self.add_vertex(data_sect.myvertices.copy())
272
+ self.prepare()
195
273
 
196
274
  @property
197
275
  def linked_arrays(self):
276
+ """ Return the linked arrays from parent crosssection if any.
277
+ Useful to plot associated data along the section.
278
+ """
279
+
198
280
  if self.parent is not None:
199
281
  return self.parent.get_linked_arrays()
200
282
  else:
@@ -202,46 +284,60 @@ class profile(vector):
202
284
 
203
285
  @property
204
286
  def bankleft(self):
287
+ """ Return the bankleft reference point in its raw format (wolfvertex, index or s3D) """
205
288
  return self._bankleft
206
289
 
207
290
  @bankleft.setter
208
291
  def bankleft(self,value):
209
- self._bankleft=value
292
+ """ Set the bankleft reference point in its raw format (wolfvertex, index or s3D) """
293
+ self._bankleft = value
210
294
 
211
295
  @property
212
296
  def bankright(self):
297
+ """ Return the bankright reference point in its raw format (wolfvertex, index or s3D) """
213
298
  return self._bankright
214
299
 
215
300
  @bankright.setter
216
301
  def bankright(self,value):
217
- self._bankright=value
302
+ """ Set the bankright reference point in its raw format (wolfvertex, index or s3D) """
303
+ self._bankright = value
218
304
 
219
- @ property
305
+ @property
220
306
  def bankleft_down(self):
307
+ """ Return the bankleft_down reference point in its raw format (wolfvertex, index or s3D) """
221
308
  return self._bankleft_down
222
309
 
223
310
  @bankleft_down.setter
224
311
  def bankleft_down(self,value):
225
- self._bankleft_down=value
312
+ """ Set the bankleft_down reference point in its raw format (wolfvertex, index or s3D) """
313
+ self._bankleft_down = value
226
314
 
227
315
  @property
228
316
  def bankright_down(self):
317
+ """ Return the bankright_down reference point in its raw format (wolfvertex, index or s3D) """
229
318
  return self._bankright_down
230
319
 
231
320
  @bankright_down.setter
232
321
  def bankright_down(self,value):
233
- self._bankright_down=value
322
+ """ Set the bankright_down reference point in its raw format (wolfvertex, index or s3D) """
323
+ self._bankright_down = value
234
324
 
235
325
  @property
236
326
  def bed(self):
327
+ """ Return the bed reference point in its raw format (wolfvertex, index or s3D) """
237
328
  return self._bed
238
329
 
239
330
  @bed.setter
240
331
  def bed(self,value):
241
- self._bed=value
332
+ """ Set the bed reference point in its raw format (wolfvertex, index or s3D) """
333
+ self._bed = value
242
334
 
243
335
  @property
244
336
  def bankleft_vertex(self):
337
+ """ Return the bankleft reference point as a wolfvertex whatever the storage format """
338
+ if self._bankleft is None:
339
+ return None
340
+
245
341
  if self.banksbed_postype == postype.BY_VERTEX:
246
342
  return self._bankleft
247
343
  elif self.banksbed_postype == postype.BY_INDEX:
@@ -251,6 +347,10 @@ class profile(vector):
251
347
 
252
348
  @property
253
349
  def bankright_vertex(self):
350
+ """ Return the bankright reference point as a wolfvertex whatever the storage format """
351
+ if self._bankright is None:
352
+ return None
353
+
254
354
  if self.banksbed_postype == postype.BY_VERTEX:
255
355
  return self._bankright
256
356
  elif self.banksbed_postype == postype.BY_INDEX:
@@ -260,6 +360,10 @@ class profile(vector):
260
360
 
261
361
  @property
262
362
  def bankleft_down_vertex(self):
363
+ """ Return the bankleft_down reference point as a wolfvertex whatever the storage format """
364
+ if self._bankleft_down is None:
365
+ return None
366
+
263
367
  if self.banksbed_postype == postype.BY_VERTEX:
264
368
  return self._bankleft_down
265
369
  elif self.banksbed_postype == postype.BY_INDEX:
@@ -269,6 +373,10 @@ class profile(vector):
269
373
 
270
374
  @property
271
375
  def bankright_down_vertex(self):
376
+ """ Return the bankright_down reference point as a wolfvertex whatever the storage format """
377
+ if self._bankright_down is None:
378
+ return None
379
+
272
380
  if self.banksbed_postype == postype.BY_VERTEX:
273
381
  return self._bankright_down
274
382
  elif self.banksbed_postype == postype.BY_INDEX:
@@ -278,6 +386,10 @@ class profile(vector):
278
386
 
279
387
  @property
280
388
  def bed_vertex(self):
389
+ """ Return the bed reference point as a wolfvertex whatever the storage format """
390
+ if self._bed is None:
391
+ return None
392
+
281
393
  if self.banksbed_postype == postype.BY_VERTEX:
282
394
  return self._bed
283
395
  elif self.banksbed_postype == postype.BY_INDEX:
@@ -287,6 +399,7 @@ class profile(vector):
287
399
 
288
400
  @property
289
401
  def bankleft_s3D(self):
402
+ """ Return the bankleft reference point as a s3D position whatever the storage format """
290
403
  if self.banksbed_postype == postype.BY_S3D:
291
404
  return self._bankleft
292
405
  else:
@@ -294,8 +407,8 @@ class profile(vector):
294
407
  # --> projection x,y sur la trace --> récupération de 's'
295
408
  # --> calcul de la distance s3D via shapely LineString sz en projetant '(s,z)'
296
409
  if self.bankleft_vertex is not None:
297
- ls2d = self.asshapely_ls()
298
- lssz = self.asshapely_sz()
410
+ ls2d = self.linestring
411
+ lssz = self.linestring_sz
299
412
  curvert = self.bankleft_vertex
300
413
  s = ls2d.project(Point(curvert.x, curvert.y))
301
414
  s3d = lssz.project(Point(s,curvert.z))
@@ -305,6 +418,7 @@ class profile(vector):
305
418
 
306
419
  @property
307
420
  def bankright_s3D(self):
421
+ """ Return the bankright reference point as a s3D position whatever the storage format """
308
422
  if self.banksbed_postype == postype.BY_S3D:
309
423
  return self._bankright
310
424
  else:
@@ -312,8 +426,8 @@ class profile(vector):
312
426
  # --> projection x,y sur la trace --> récupération de 's'
313
427
  # --> calcul de la distance s3D via shapely LineString sz en projetant '(s,z)'
314
428
  if self.bankright_vertex is not None:
315
- ls2d = self.asshapely_ls()
316
- lssz = self.asshapely_sz()
429
+ ls2d = self.linestring
430
+ lssz = self.linestring_sz
317
431
  curvert = self.bankright_vertex
318
432
  s = ls2d.project(Point(curvert.x, curvert.y))
319
433
  s3d = lssz.project(Point(s,curvert.z))
@@ -323,6 +437,7 @@ class profile(vector):
323
437
 
324
438
  @property
325
439
  def bankleft_down_s3D(self):
440
+ """ Return the bankleft_down reference point as a s3D position whatever the storage format """
326
441
  if self.banksbed_postype == postype.BY_S3D:
327
442
  return self._bankleft_down
328
443
  else:
@@ -330,8 +445,8 @@ class profile(vector):
330
445
  # --> projection x,y sur la trace --> récupération de 's'
331
446
  # --> calcul de la distance s3D via shapely LineString sz en projetant '(s,z)'
332
447
  if self.bankleft_down_vertex is not None:
333
- ls2d = self.asshapely_ls()
334
- lssz = self.asshapely_sz()
448
+ ls2d = self.linestring
449
+ lssz = self.linestring_sz
335
450
  curvert = self.bankleft_down_vertex
336
451
  s = ls2d.project(Point(curvert.x, curvert.y))
337
452
  s3d = lssz.project(Point(s,curvert.z))
@@ -341,6 +456,7 @@ class profile(vector):
341
456
 
342
457
  @property
343
458
  def bankright_down_s3D(self):
459
+ """ Return the bankright_down reference point as a s3D position whatever the storage format """
344
460
  if self.banksbed_postype == postype.BY_S3D:
345
461
  return self._bankright_down
346
462
  else:
@@ -348,8 +464,8 @@ class profile(vector):
348
464
  # --> projection x,y sur la trace --> récupération de 's'
349
465
  # --> calcul de la distance s3D via shapely LineString sz en projetant '(s,z)'
350
466
  if self.bankright_down_vertex is not None:
351
- ls2d = self.asshapely_ls()
352
- lssz = self.asshapely_sz()
467
+ ls2d = self.linestring
468
+ lssz = self.linestring_sz
353
469
  curvert = self.bankright_down_vertex
354
470
  s = ls2d.project(Point(curvert.x, curvert.y))
355
471
  s3d = lssz.project(Point(s,curvert.z))
@@ -359,6 +475,7 @@ class profile(vector):
359
475
 
360
476
  @property
361
477
  def bed_s3D(self):
478
+ """ Return the bed reference point as a s3D position whatever the storage format """
362
479
  if self.banksbed_postype == postype.BY_S3D:
363
480
  return self._bed
364
481
  else:
@@ -366,8 +483,8 @@ class profile(vector):
366
483
  # --> projection x,y sur la trace --> récupération de 's'
367
484
  # --> calcul de la distance s3D via shapely LineString sz en projetant '(s,z)'
368
485
  if self.bed_vertex is not None:
369
- ls2d = self.asshapely_ls()
370
- lssz = self.asshapely_sz()
486
+ ls2d = self.linestring
487
+ lssz = self.linestring_sz
371
488
  curvert = self.bed_vertex
372
489
  s = ls2d.project(Point(curvert.x, curvert.y))
373
490
  s3d = lssz.project(Point(s,curvert.z))
@@ -377,35 +494,40 @@ class profile(vector):
377
494
 
378
495
  @property
379
496
  def bankleft_sz(self):
380
- ls2d = self.asshapely_ls()
497
+ """ Return the bankleft reference point as a (s,z) tuple whatever the storage format """
498
+ ls2d = self.linestring
381
499
  curvert = self.bankleft_vertex
382
500
  s = ls2d.project(Point(curvert.x, curvert.y))
383
501
  return s, curvert.z
384
502
 
385
503
  @property
386
504
  def bankright_sz(self):
387
- ls2d = self.asshapely_ls()
505
+ """ Return the bankright reference point as a (s,z) tuple whatever the storage format """
506
+ ls2d = self.linestring
388
507
  curvert = self.bankright_vertex
389
508
  s = ls2d.project(Point(curvert.x, curvert.y))
390
509
  return s, curvert.z
391
510
 
392
511
  @property
393
512
  def bed_sz(self):
394
- ls2d = self.asshapely_ls()
513
+ """ Return the bed reference point as a (s,z) tuple whatever the storage format """
514
+ ls2d = self.linestring
395
515
  curvert = self.bed_vertex
396
516
  s = ls2d.project(Point(curvert.x, curvert.y))
397
517
  return s, curvert.z
398
518
 
399
519
  @property
400
520
  def bankleft_down_sz(self):
401
- ls2d = self.asshapely_ls()
521
+ """ Return the bankleft_down reference point as a (s,z) tuple whatever the storage format """
522
+ ls2d = self.linestring
402
523
  curvert = self.bankleft_down_vertex
403
524
  s = ls2d.project(Point(curvert.x, curvert.y))
404
525
  return s, curvert.z
405
526
 
406
527
  @property
407
528
  def bankright_down_sz(self):
408
- ls2d = self.asshapely_ls()
529
+ """ Return the bankright_down reference point as a (s,z) tuple whatever the storage format """
530
+ ls2d = self.linestring
409
531
  curvert = self.bankright_down_vertex
410
532
  s = ls2d.project(Point(curvert.x, curvert.y))
411
533
  return s, curvert.z
@@ -414,17 +536,21 @@ class profile(vector):
414
536
 
415
537
  self.update_lengths()
416
538
 
417
- # for curl in self._lengthparts3D
418
539
 
419
- def triangulation_gltf(self, zmin):
540
+ def triangulation_gltf(self, zmin:float):
420
541
  """
421
542
  Génération d'un info de triangulation pour sortie au format GLTF --> Blender
422
- :zmin : position d'altitude minimale de la triangulation
543
+
544
+ La triangulation est constituée de la section et d'une base plane à l'altitude zmin, a priori sous le lit de la rivière.
545
+
546
+ :param zmin: position d'altitude minimale de la triangulation
423
547
  """
548
+
424
549
  section = self.asnparray3d()
425
550
  base = section.copy()
426
551
  base[:,2] = zmin
427
- points = np.concatenate((section,base),axis=0)
552
+
553
+ points = np.concatenate((section,base), axis=0)
428
554
  triangles=[]
429
555
  nb=self.nbvertices
430
556
  for j in range (nb-1):
@@ -436,15 +562,20 @@ class profile(vector):
436
562
  c = b
437
563
  b = a+1
438
564
 
439
- return points,triangles
565
+ return points, triangles
440
566
 
441
- def triangulation_ponts(self,x,y, zmax):
567
+ def triangulation_ponts(self, x:float ,y:float, zmax:float):
442
568
  """
443
569
  Triangulation d'une section de pont
570
+
571
+ :param x: coordonnée X du point de référence du pont
572
+ :param y: coordonnée Y du point de référence du pont
573
+ :param zmax: altitude du tablier du pont
444
574
  """
575
+
445
576
  section = self.asnparray3d()
446
- x1,y1=find_xy(section,x,y)
447
- parallele = partranslation(section,x1,y1)
577
+ x1,y1=_find_shift_at_xy(section,x,y)
578
+ parallele = _shift_np(section,x1,y1)
448
579
 
449
580
  base1 = section.copy()
450
581
  base1[:,2]= zmax
@@ -490,29 +621,60 @@ class profile(vector):
490
621
  triangles.append([len(section)-1,len(section)*2-1,len(section)*4-1])
491
622
  return(np.asarray([[curpt[0],curpt[2],-curpt[1]] for curpt in points],dtype=np.float32),np.asarray(triangles,dtype=np.uint32))
492
623
 
493
- def set_orient(self):
624
+ def _set_orient(self):
494
625
  """
495
626
  Calcul du vecteur directeur de la section sur base des points extrêmes
496
627
  """
628
+
629
+ if self.nbvertices<2:
630
+ logging.error(_('Not enough vertices to define an orientation'))
631
+ self.orient = np.zeros(2)
632
+ return
633
+
497
634
  self.orient = asarray([self.myvertices[-1].x-self.myvertices[0].x,
498
635
  self.myvertices[-1].y-self.myvertices[0].y])
499
- self.orient = self.orient /np.linalg.norm(self.orient)
500
636
 
501
- def get_xy_from_s(self,s) -> tuple[float, float]:
637
+ dist = np.linalg.norm(self.orient)
638
+ if dist==0.:
639
+ logging.error(_('Bad vertices to define an orientation -- identical points'))
640
+ self.orient = np.zeros(2)
641
+ return
642
+
643
+ self.orient = self.orient / np.linalg.norm(self.orient)
644
+
645
+ @property
646
+ def trace_increments(self):
647
+ """ Return the orientation vector of the section (unit vector in the XY plane)
648
+ """
649
+ if self.orient is None:
650
+ self._set_orient()
651
+ return self.orient
652
+
653
+ def get_xy_from_s(self, s:float) -> tuple[float, float]:
502
654
  """
503
655
  Récupération d'un tuple (x,y) sur base d'une distance 2D 's' orientée dans l'axe de la section
656
+
657
+ :param s: distance curviligne 2D le long de la section supposée plane étant donné l'utilisation de l'orientation
504
658
  """
659
+
505
660
  if self.orient is None:
506
- self.set_orient()
661
+ self._set_orient()
507
662
 
508
663
  return self.myvertices[0].x+self.orient[0]*s,self.myvertices[0].y+self.orient[1]*s
509
664
 
510
- def set_vertices_sz_orient(self, sz:np.ndarray, xy1:np.ndarray, xy2:np.ndarray):
665
+ def set_vertices_sz_orient(self, sz:np.ndarray | tuple | list, xy1:np.ndarray | list | tuple, xy2:np.ndarray | list | tuple):
511
666
  """
512
667
  Ajout de vertices depuis :
513
- - une matrice numpy (s,z) -- shape = (nb_vert,2)
514
- - d'un point source [x,y]_1
515
- - d'un point visé [x,y]_2
668
+ - une matrice numpy (s,z) -- shape = (nb_vert,2) ou une liste ou un tuple convertibles en numpy array
669
+ - un point source [x,y]
670
+ - un point visé [x,y]
671
+
672
+ Le point source correspond au vertex de s=0.
673
+ Le point visé sert uniquement à définir l'orientation de la section.
674
+
675
+ :param sz: matrice numpy (s,z) -- shape = (nb_vert,2)
676
+ :param xy1: point source [x,y]
677
+ :param xy2: point visé [x,y]
516
678
  """
517
679
 
518
680
  if isinstance(sz,list):
@@ -539,19 +701,36 @@ class profile(vector):
539
701
  for cur in sz:
540
702
  x, y = xy1[0] + dx*cur[0], xy1[1] + dy*cur[0]
541
703
  self.add_vertex(wolfvertex(x, y, float(cur[1])))
704
+ else:
705
+ logging.error(_('Bad input points in set_vertices_sz_orient -- identical points'))
706
+ else:
707
+ logging.error(_('Bad shape of input arrays in set_vertices_sz_orient'))
542
708
 
543
- def get_laz_around(self,length_buffer=10.):
709
+ def get_laz_around(self, length_buffer:float = 10.):
544
710
  """
545
711
  Récupération de points LAZ autour de la section
712
+
713
+ :param length_buffer: distance latérale [m] autour de la section pour la récupération des points LAZ
546
714
  """
547
- myls = self.asshapely_ls()
548
- mypoly = myls.buffer(length_buffer,cap_style=CAP_STYLE.square)
549
- mybounds = ((mypoly.bounds[0],mypoly.bounds[2]),(mypoly.bounds[1],mypoly.bounds[3]))
715
+
716
+ self.laz = False
717
+
718
+ if self.parent is None:
719
+ logging.error(_('No parent crosssection object -- cannot get LAZ points'))
720
+ return
721
+
722
+ if self.parent.gridlaz is None:
723
+ logging.error(_('No LAZ grid in parent crosssection object -- cannot get LAZ points'))
724
+ return
725
+
726
+ mypoly = self.linestring.buffer(length_buffer, cap_style=CAP_STYLE.square)
727
+ mybounds = ((mypoly.bounds[0], mypoly.bounds[2]), (mypoly.bounds[1], mypoly.bounds[3]))
550
728
 
551
729
  myxyz = self.parent.gridlaz.scan(mybounds)
552
730
 
553
- prep_poly = prep(mypoly)
731
+ prep_poly = prepare(mypoly)
554
732
  mytests = [prep_poly.contains(Point(cur[:3])) for cur in myxyz]
733
+ destroy_prepared(prep_poly)
555
734
 
556
735
  self.usedlaz = np.asarray(myxyz[mytests])
557
736
 
@@ -586,13 +765,23 @@ class profile(vector):
586
765
 
587
766
  self.laz=True
588
767
 
589
- def plot_laz(self,length_buffer=5.,fig:Figure=None,ax:Axes=None,show=False):
768
+ def plot_laz(self, length_buffer:float = 5., fig:Figure=None, ax:Axes=None, show=False):
590
769
  """
591
770
  Dessin des points LAZ sur le graphique Matplotlib
771
+
772
+ :param length_buffer: distance latérale [m] autour de la section pour la récupération des points LAZ
773
+ :param fig: figure Matplotlib
774
+ :param ax: axes Matplotlib
775
+ :param show: if True, show the figure
592
776
  """
777
+
593
778
  if not self.laz:
594
779
  self.get_laz_around(length_buffer)
595
780
 
781
+ if not self.laz:
782
+ logging.error(_('No LAZ points to plot'))
783
+ return None
784
+
596
785
  if ax is None:
597
786
  fig = plt.figure()
598
787
  ax=fig.add_subplot(111)
@@ -606,14 +795,17 @@ class profile(vector):
606
795
 
607
796
  return np.min(self.usedlaz[:,2]),np.max(self.usedlaz[:,2])
608
797
 
609
- def slide_vertex(self,s):
798
+ def slide_vertex(self, s:float):
610
799
  """
611
800
  Glissement des vertices d'une constante 's'
801
+
802
+ :param s: distance de glissement dans l'axe de la section
612
803
  """
613
804
 
614
805
  if self.orient is None:
615
- self.set_orient()
806
+ self._set_orient()
616
807
 
808
+ # increments de position
617
809
  dx = self.orient[0] *s
618
810
  dy = self.orient[1] *s
619
811
 
@@ -621,11 +813,15 @@ class profile(vector):
621
813
  curv.x +=dx
622
814
  curv.y +=dy
623
815
 
624
- def movebankbed_index(self, which:Literal['left', 'right', 'bed', 'left_down', 'right_down'],
816
+ def movebankbed_index(self,
817
+ which:Literal['left', 'right', 'bed', 'left_down', 'right_down'],
625
818
  orientation:Literal['left', 'right']) -> None:
626
819
  """
627
- Déplacement des points de référence sur base d'un index
628
- Le cas échéant, adaptation du mode de stockage
820
+ Déplacement des points de référence sur base d'un index.
821
+ Le cas échéant, adaptation du mode de stockage.
822
+
823
+ :param which: point à déplacer ('left', 'right', 'bed', 'left_down', 'right_down')
824
+ :param orientation: direction du déplacement ('left' ou 'right')
629
825
  """
630
826
  if self.banksbed_postype == postype.BY_VERTEX:
631
827
  if which=='left':
@@ -707,9 +903,11 @@ class profile(vector):
707
903
  self.sz_bankbed = self.get_sz_banksbed(force=True)
708
904
  self.s3d_bankbed = self.get_s3d_banksbed(force=True)
709
905
 
710
- def update_sdatum(self, new_sdatum):
906
+ def update_sdatum(self, new_sdatum:float):
711
907
  """
712
- MAJ de la position de la section selon sa trace
908
+ Mise à jour de la position de la section selon sa trace
909
+
910
+ :param new_sdatum: nouvelle valeur de sdatum
713
911
  """
714
912
 
715
913
  sdatum_prev = self.sdatum
@@ -718,14 +916,16 @@ class profile(vector):
718
916
  self.add_sdatum = True
719
917
 
720
918
  if self.prepared:
721
- delta = new_sdatum-sdatum_prev
919
+ delta = new_sdatum - sdatum_prev
722
920
  self.sz[:,0] += delta
723
921
  self.smin += delta
724
922
  self.smax += delta
725
923
 
726
- def update_zdatum(self, new_zdatum):
924
+ def update_zdatum(self, new_zdatum:float):
727
925
  """
728
- MAJ de l'altitude de référence de la section
926
+ Mise à jour de l'altitude de référence de la section
927
+
928
+ :param new_zdatum: nouvelle valeur de zdatum
729
929
  """
730
930
 
731
931
  zdatum_prev = self.zdatum
@@ -734,21 +934,28 @@ class profile(vector):
734
934
  self.add_zdatum = True
735
935
 
736
936
  if self.prepared:
737
- delta = new_zdatum-zdatum_prev
738
- self.sz[:,1] +=new_zdatum-zdatum_prev
937
+ delta = new_zdatum - zdatum_prev
938
+ self.sz[:,1] += delta
739
939
  self.zmin += delta
740
940
  self.zmax += delta
741
941
 
742
- def update_banksbed_from_s3d(self, which, s:float):
942
+ def update_banksbed_from_s3d(self,
943
+ which:Literal['left', 'right', 'bed', 'left_down', 'right_down'],
944
+ s:float):
743
945
  """
744
- MAJ des points de référence depuis une coordonnée curvi 3D
946
+ Mise à jour des points de référence depuis une coordonnée curvi 3D
947
+
948
+ :param which: point à modifier ('left', 'right', 'bed', 'left_down', 'right_down')
949
+ :param s: nouvelle position curvi 3D du point
745
950
  """
746
951
 
747
952
  if self.banksbed_postype != postype.BY_S3D:
748
- s1, s2, s3 = self.get_s3d_banksbed()
953
+ s1, s2, s3, s4, s5 = self.get_s3d_banksbed()
749
954
  self.bankleft = s1
750
- self.bed = s2
751
- self.bankright = s3
955
+ self.bankleft_down = s2
956
+ self.bed = s3
957
+ self.bankright_down = s4
958
+ self.bankright = s5
752
959
 
753
960
  self.banksbed_postype = postype.BY_S3D
754
961
 
@@ -762,20 +969,34 @@ class profile(vector):
762
969
  self.bankright = s
763
970
 
764
971
  if self.prepared:
765
- self.s3d_bankbed[2]=s
972
+ self.s3d_bankbed[4]=s
766
973
 
767
974
  elif which=='bed':
768
975
  self.bed = s
769
976
 
977
+ if self.prepared:
978
+ self.s3d_bankbed[2]=s
979
+
980
+ elif which=='left_down':
981
+ self.bankleft_down = s
982
+
770
983
  if self.prepared:
771
984
  self.s3d_bankbed[1]=s
772
985
 
986
+ elif which=='right_down':
987
+ self.bankright_down = s
988
+
989
+ if self.prepared:
990
+ self.s3d_bankbed[3]=s
991
+
773
992
  if self.prepared:
774
993
  self.sz_bankbed = self.get_sz_banksbed(force=True)
775
994
 
776
- def save(self,f):
995
+ def save(self, f):
777
996
  """
778
997
  Surcharge de l'opération d'écriture
998
+
999
+ :param f: fichier ouvert en écriture
779
1000
  """
780
1001
 
781
1002
  if self.parent.forcesuper:
@@ -789,68 +1010,76 @@ class profile(vector):
789
1010
  which = "LEFT"
790
1011
  elif curvert is self.bankright:
791
1012
  which = "RIGHT"
1013
+ elif curvert is self.bankleft_down:
1014
+ which = "LEFT_DOWN"
1015
+ elif curvert is self.bankright_down:
1016
+ which = "RIGHT_DOWN"
792
1017
 
793
- f.write("{0}\t{1}\t{2}\t{3}\t{4}\n".format(self.myname,curvert.x,curvert.y,which,curvert.z))
1018
+ f.write("{0}\t{1}\t{2}\t{3}\t{4}\n".format(self.myname, curvert.x, curvert.y, which, curvert.z))
794
1019
 
795
- def get_s_from_xy(self, xy:wolfvertex) -> float:
1020
+ def get_s_from_xy(self, xy:wolfvertex, cumul:bool = False) -> float:
796
1021
  """
797
- Retourne la coordonnér curvi sur base d'un vertex
1022
+ Retourne la coordonnée curviligne (ou distance euclidienne selon l'orientation de la section)
1023
+ du point xy donné en tant qu'objet wolfvertex.
1024
+
1025
+ Dans cette routine, 'cumul' est à False par défaut pour être rétro-compatible avec les versions précédentes.
1026
+ ATTENTION: Cette valeur par défaut n'est pas identique à celle utilisée dans 'get_sz'.
1027
+
1028
+ :param xy: vertex dont on veut la coordonnée curvi 2D
1029
+ :param cumul: si True, la coordonnée curvi 2D est cumulée (distance le long de la section)
1030
+ si False, la coordonnée curvi 2D est la distance euclidienne au point de départ
798
1031
  """
799
1032
 
1033
+ if cumul:
1034
+ s = self.linestring.project(Point(xy.x, xy.y))
1035
+ return s
1036
+
800
1037
  x1 = self.myvertices[0].x
801
1038
  y1 = self.myvertices[0].y
802
1039
  length = math.sqrt((xy.x-x1)**2.+(xy.y-y1)**2.)
803
1040
 
804
1041
  return length
805
1042
 
806
- def get_sz(self, cumul=True) -> tuple[np.ndarray, np.ndarray]:
807
- """
808
- Retourne 2 vecteurs avec la position curvi 2D et l'altitude des points
809
- """
1043
+ # def get_sz(self, cumul=True) -> tuple[np.ndarray, np.ndarray]:
1044
+ # """
1045
+ # Surcharge de la méthode get_sz de la classe mère wolfsection pour ajouter les datums s et z.
810
1046
 
811
- if self.prepared:
812
- return self.sz[:,0], self.sz[:,1]
1047
+ # :param cumul: si True, la coordonnée curvi 2D est cumulée (distance le long de la section)
1048
+ # si False, la coordonnée curvi 2D est la distance euclidienne au point de départ
1049
+ # :return: tuple (s,z) avec s la coordonnée curvi 2D
1050
+ # """
813
1051
 
814
- z = asarray([self.myvertices[i].z for i in range(self.nbvertices)])
1052
+ # if self.prepared:
1053
+ # return self.sz[:,0], self.sz[:,1]
815
1054
 
816
- if self.add_zdatum:
817
- z+=self.zdatum
818
-
819
- nb = len(z)
820
- s = zeros(nb)
1055
+ # s, z = super().get_sz(cumul=cumul)
821
1056
 
822
- if cumul:
823
- x1 = self.myvertices[0].x
824
- y1 = self.myvertices[0].y
825
- for i in range(nb-1):
826
- x2 = self.myvertices[i+1].x
827
- y2 = self.myvertices[i+1].y
1057
+ # if self.add_zdatum:
1058
+ # z += self.zdatum
828
1059
 
829
- length = np.sqrt((x2-x1)**2.+(y2-y1)**2.)
830
- s[i+1] = s[i]+length
1060
+ # if self.add_sdatum:
1061
+ # s += self.sdatum
831
1062
 
832
- x1=x2
833
- y1=y2
834
- else:
835
- for i in range(nb):
836
- s[i] = self.myvertices[0].dist2D(self.myvertices[i])
1063
+ # return s,z
837
1064
 
838
- if self.add_sdatum:
839
- s+=self.sdatum
1065
+ def set_sz(self, sz:np.ndarray, trace:list[tuple[float, float]]):
1066
+ """
1067
+ Calcule les positions des vertices sur base d'une matrice sz et d'une trace.
840
1068
 
841
- return s,z
1069
+ :param s: colonne 0
1070
+ :param z: colonne 1
1071
+ :param trace: liste de 2 couples xy -> [[x1,y1], [x2,y2]]
842
1072
 
843
- def set_sz(self, sz, trace):
844
- """
845
- Calcule les positions des vertices sur base d'une matrice sz et d'une trace
846
- s : colonne 0
847
- z : colonne 1
848
- trace : liste de 2 couples xy -> [[x1,y1], [x2,y2]]
1073
+ FIXME: Vérifier la possibilité de fusionner avec set_vertices_sz_orient
849
1074
  """
850
1075
 
851
1076
  orig = trace[0]
852
1077
  end = trace[1]
853
1078
 
1079
+ if np.all(np.asarray(orig)==np.asarray(end)):
1080
+ logging.error(_('Bad input points in set_sz -- identical points'))
1081
+ return
1082
+
854
1083
  vec = np.asarray(end)-np.asarray(orig)
855
1084
  vec = vec/np.linalg.norm(vec)
856
1085
 
@@ -863,7 +1092,11 @@ class profile(vector):
863
1092
  def get_sz_banksbed(self, cumul=True, force:bool=False) -> tuple[float, float, float, float, float, float, float, float, float, float]:
864
1093
  """
865
1094
  Retourne les positions des points de référence mais avec la coordonnée curvi 2D
866
- - (sleft, sbed, sright, zleft, zbed, zright)
1095
+ - (sleft, sbed, sright, zleft, zbed, zright, sbankleft_down, sbankright_down, zbankleft_down, zbankright_down)
1096
+
1097
+ :param cumul: si True, la coordonnée curvi 2D est cumulée (distance le long de la section)
1098
+ si False, la coordonnée curvi 2D est la distance euclidienne au point de départ
1099
+ :param force: si True, force le recalcul même si la section est déjà préparée
867
1100
  """
868
1101
 
869
1102
  if self.prepared and not force:
@@ -952,16 +1185,16 @@ class profile(vector):
952
1185
 
953
1186
  return sleft,sbed,sright,zleft,zbed,zright, sbankleft_down, sbankright_down, zbankleft_down, zbankright_down
954
1187
 
955
- def get_s3d_banksbed(self, force:bool=False)-> tuple[float, float, float]:
1188
+ def get_s3d_banksbed(self, force:bool=False)-> tuple[float, float, float, float, float]:
956
1189
  """
957
1190
  Retourne les coordonnée curvi 3D des points de référence
958
- - (sleft, sbed, sright)
1191
+ - (sleft, sleft_down, sbed, sright_down, sright)
959
1192
  """
960
1193
 
961
1194
  if self.prepared and not force:
962
1195
  return self.s3d_bankbed
963
1196
 
964
- return self.bankleft_s3D, self.bed_s3D, self.bankright_s3D
1197
+ return self.bankleft_s3D, self.bankleft_down_s3D, self.bed_s3D, self.bankright_down_s3D, self.bankright_s3D
965
1198
 
966
1199
  def asshapely_sz(self) -> LineString:
967
1200
  """
@@ -971,6 +1204,19 @@ class profile(vector):
971
1204
  s,z = self.get_sz()
972
1205
  return LineString(np.asarray([[curs, curz] for curs, curz in zip(s,z)]))
973
1206
 
1207
+ @property
1208
+ def linestring_sz(self) -> LineString:
1209
+ """
1210
+ Retroune la section comme objet shapely - polyligne selon la trace avec altitudes
1211
+ """
1212
+
1213
+ if self._linestring_sz is not None:
1214
+ return self._linestring_sz
1215
+
1216
+ self._linestring_sz = self.asshapely_sz()
1217
+ prepare(self._linestring_sz)
1218
+ return self._linestring_sz
1219
+
974
1220
  def asshapely_s3dz(self) -> LineString:
975
1221
  """
976
1222
  Retroune la section comme objet shapely - polyligne selon la trace 3D avec altitudes
@@ -980,9 +1226,32 @@ class profile(vector):
980
1226
  z = [cur.z for cur in self.myvertices]
981
1227
  return LineString(np.asarray([[curs, curz] for curs, curz in zip(s,z)]))
982
1228
 
1229
+ @property
1230
+ def s3dz(self) -> np.ndarray:
1231
+ """
1232
+ Retroune la section comme matrice numpy (s3d,z)
1233
+ """
1234
+
1235
+ s = self.get_s3d()
1236
+ z = [cur.z for cur in self.myvertices]
1237
+ return np.column_stack([s,z])
1238
+
1239
+ @property
1240
+ def linestring_s3dz(self) -> LineString:
1241
+ """
1242
+ Retroune la section comme objet shapely - polyligne selon la trace 3D avec altitudes
1243
+ """
1244
+
1245
+ if self._linestring_s3dz is not None:
1246
+ return self._linestring_s3dz
1247
+
1248
+ self._linestring_s3dz = self.asshapely_s3dz()
1249
+ prepare(self._linestring_s3dz)
1250
+ return self._linestring_s3dz
1251
+
983
1252
  def prepare(self,cumul=True):
984
1253
  """
985
- Pre-Compute sz and sz_banked to avoid multiple computation
1254
+ Pre-Compute sz, sz_banked and shapely objects to avoid multiple computation
986
1255
  """
987
1256
 
988
1257
  self.reset_prepare()
@@ -994,12 +1263,30 @@ class profile(vector):
994
1263
  self.zmin = min(y)
995
1264
  self.zmax = max(y)
996
1265
 
997
- self.prepare_shapely()
1266
+ self.prepare_shapely(linestring=True, polygon=False)
998
1267
  self.sz_bankbed = list(self.get_sz_banksbed(cumul))
999
1268
  self.s3d_bankbed = list(self.get_s3d_banksbed())
1000
1269
 
1001
1270
  self.prepared=True
1002
1271
 
1272
+ def __del__(self):
1273
+
1274
+ self.reset_prepare()
1275
+ super().__del__()
1276
+
1277
+ def _reset_linestring_sz_s3dz(self):
1278
+
1279
+ if self._linestring_sz is not None:
1280
+ if is_prepared(self._linestring_sz):
1281
+ destroy_prepared(self._linestring_sz)
1282
+ self._linestring_sz=None
1283
+
1284
+ if self._linestring_s3dz is not None:
1285
+ if is_prepared(self._linestring_s3dz):
1286
+ destroy_prepared(self._linestring_s3dz)
1287
+ self._linestring_s3dz=None
1288
+
1289
+
1003
1290
  def reset_prepare(self):
1004
1291
  """
1005
1292
  Réinitialisation de la préparation de la section
@@ -1013,173 +1300,35 @@ class profile(vector):
1013
1300
  self.sz_bankbed = None
1014
1301
  self.s3d_bankbed = None
1015
1302
  self.reset_linestring()
1303
+ self._reset_linestring_sz_s3dz()
1016
1304
 
1017
1305
  self.prepared=False
1018
1306
 
1019
- def get_min(self):
1307
+ def get_min(self) -> wolfvertex:
1308
+ """ Return the vertex with minimum elevation """
1020
1309
  return sorted(self.myvertices,key=lambda x:x.z)[0]
1021
1310
 
1022
- def get_max(self):
1311
+ def get_max(self) -> wolfvertex:
1312
+ """ Return the vertex with maximum elevation """
1023
1313
  return sorted(self.myvertices,key=lambda x:x.z)[-1]
1024
1314
 
1025
- def get_minz(self):
1315
+ def get_minz(self) -> float:
1316
+ """ Return the minimum elevation """
1026
1317
  return amin(list(x.z for x in self.myvertices))
1027
1318
 
1028
- def get_maxz(self):
1319
+ def get_maxz(self) -> float:
1320
+ """ Return the maximum elevation """
1029
1321
  return amax(list(x.z for x in self.myvertices))
1030
1322
 
1031
- def plot_linked(self, fig, ax, linked_arrays:dict):
1032
-
1033
- colors=['red','blue','green']
1034
-
1035
- k=0
1036
- for curlabel, curarray in linked_arrays.items():
1037
- if curarray.plotted:
1038
- myls = self.asshapely_ls()
1039
-
1040
- length = myls.length
1041
- ds = min(curarray.dx,curarray.dy)
1042
- nb = int(np.ceil(length/ds*2))
1043
-
1044
- alls = np.linspace(0,int(length),nb)
1045
-
1046
- pts = [myls.interpolate(curs) for curs in alls]
1047
-
1048
- allz = [curarray.get_value(curpt.x,curpt.y) for curpt in pts]
1049
-
1050
- if np.max(allz)>-99999:
1051
- ax.plot(alls,allz,
1052
- color=colors[np.mod(k,3)],
1053
- lw=2.0,
1054
- label=curlabel)
1055
- k+=1
1056
-
1057
- def _plot_only_cs(self,fig:Figure=None,ax:Axes=None,label='',alpha=0.8,lw=1.,style: str ='dashed',centerx=0.,centery=0.,grid=True, col_ax: str = 'black'):
1058
- # plot
1059
- x,y=self.get_sz()
1060
-
1061
- sl,sb,sr,yl,yb,yr, sld, srd, yld, yrd = self.get_sz_banksbed()
1062
-
1063
- if centerx >0. and sb!=-99999.:
1064
- decal = centerx-sb
1065
- x+=decal
1066
- sl+=decal
1067
- sb+=decal
1068
- sr+=decal
1069
-
1070
- if centery >0. and yb!=-99999.:
1071
- decal = centery-yb
1072
- y+=decal
1073
- yl+=decal
1074
- yb+=decal
1075
- yr+=decal
1076
-
1077
- ax.plot(x,y,color=col_ax,
1078
- lw=lw,
1079
- linestyle=style,
1080
- alpha=alpha,
1081
- label=label)
1082
-
1083
- curtick=ax.get_xticks()
1084
- ax.set_xticks(np.arange(min(curtick[0],(x[0]//2)*2),max(curtick[-1],(x[-1]//2)*2),2))
1085
-
1086
- if sl != -99999.:
1087
- ax.plot(sl,yl,'or',alpha=alpha)
1088
- if sb != -99999.:
1089
- ax.plot(sb,yb,'ob',alpha=alpha)
1090
- if sr != -99999.:
1091
- ax.plot(sr,yr,'og',alpha=alpha)
1092
- if sld != -99999.:
1093
- ax.plot(sld,yld,'*r',alpha=alpha)
1094
- if srd != -99999.:
1095
- ax.plot(srd,yrd,'*g',alpha=alpha)
1096
-
1097
- def plot_cs(self, fwl=None, show=False, forceaspect=True, fig:Figure=None, ax:Axes=None, plotlaz=True, clear=True, linked_arrays:dict={}):
1098
- # plot
1099
- x,y=self.get_sz()
1100
-
1101
- sl,sb,sr,yl,yb,yr, sld, srd, yld, yrd = self.get_sz_banksbed()
1102
-
1103
- xmin=x[0]
1104
- xmax=x[-1]
1105
- ymin=self.get_minz()
1106
- ymax=self.get_maxz()
1323
+ def relation_oneh(self, cury:float, x:float = None, y:float = None):
1324
+ """ Compute the hydraulic characteristics for a given water elevation 'cury'
1107
1325
 
1108
- dy=ymax-ymin
1109
- ymin-=dy/4.
1110
- ymax+=dy/4.
1111
-
1112
- if ax is None:
1113
- redraw=False
1114
- fig = plt.figure()
1115
- ax=fig.add_subplot(111)
1116
- else:
1117
- redraw=True
1118
- if clear:
1119
- ax.cla()
1120
-
1121
- if np.min(y) != -99999. and np.max(y) != -99999.:
1122
- ax.plot(x,y,color='black',
1123
- lw=2.0,
1124
- label='Profil')
1125
-
1126
- if self.parent is not None:
1127
- if plotlaz and self.parent.gridlaz is not None:
1128
- minlaz=ymin
1129
- maxlaz=ymax
1130
- minlaz,maxlaz=self.plot_laz(fig=fig,ax=ax)
1131
- if np.min(y) != -99999. and np.max(y) != -99999.:
1132
- ymin = min(ymin,minlaz)
1133
- ymax = max(ymax,maxlaz)
1134
- else:
1135
- ymin = minlaz
1136
- ymax = maxlaz
1137
-
1138
- self.plot_linked(fig,ax,linked_arrays)
1139
-
1140
- if fwl is not None:
1141
- ax.fill_between(x,y,fwl,where=y<=fwl,facecolor='cyan',alpha=0.3,interpolate=True)
1142
-
1143
- if sl != -99999.:
1144
- ax.plot(sl,yl,'or')
1145
- if sb != -99999.:
1146
- ax.plot(sb,yb,'ob')
1147
- if sr != -99999.:
1148
- ax.plot(sr,yr,'og')
1149
- if sld != -99999.:
1150
- ax.plot(sld,yld,'*r')
1151
- if srd != -99999.:
1152
- ax.plot(srd,yrd,'*g')
1153
-
1154
- ax.set_title(self.myname)
1155
- ax.set_xlabel(_('Distance [m]'))
1156
- ax.set_ylabel('Elevation [EL.m]')
1157
- ax.legend()
1158
-
1159
- tol=(xmax-xmin)/10.
1160
- ax.set_xlim(xmin-tol,xmax+tol)
1161
- ax.set_ylim(ymin,ymax)
1162
-
1163
- nbticks = 20
1164
- dzticks = max((((ymax-ymin)/nbticks) // .25) *.25,.25)
1165
-
1166
- ax.set_yticks(np.arange((ymin//.25)*.25,(ymax//.25)*.25,dzticks))
1167
- ax.grid()
1168
-
1169
- if forceaspect:
1170
- aspect=1.0*(ymax-ymin)/(xmax-xmin)*(ax.get_xlim()[1] - ax.get_xlim()[0]) / (ax.get_ylim()[1] - ax.get_ylim()[0])
1171
- ax.set_aspect(aspect)
1172
-
1173
- if show:
1174
- fig.show()
1175
-
1176
- if redraw:
1177
- fig.canvas.draw()
1178
-
1179
- return sl,sb,sr,yl,yb,yr
1180
-
1181
- def relation_oneh(self,cury,x=None,y=None):
1326
+ :param cury: water elevation
1327
+ :param x: optional x coordinates of the section (if None, use the section's coordinates)
1328
+ :param y: optional y coordinates of the section (if None, use the section's coordinates)
1329
+ :return: a,s,w,r (wetted area, wetted perimeter, top width, hydraulic radius)
1182
1330
 
1331
+ """
1183
1332
  if x is None and y is None:
1184
1333
  x,y=self.get_sz()
1185
1334
 
@@ -1224,7 +1373,7 @@ class profile(vector):
1224
1373
  else:
1225
1374
  r=0.
1226
1375
 
1227
- return a,s,w,r
1376
+ return float(a), float(s), float(w), float(r)
1228
1377
 
1229
1378
  def relations(self, discretize: int = 100, plot = True):
1230
1379
  """
@@ -1232,6 +1381,7 @@ class profile(vector):
1232
1381
  of a specific hydraulic characteristic with respect to the water depth in the profile
1233
1382
  (wetted area, wetted perimeter, top width, water detph, hydraulic radius, critical discharge).
1234
1383
  """
1384
+
1235
1385
  x,y=self.get_sz()
1236
1386
 
1237
1387
  ymin=min(y)
@@ -1263,7 +1413,7 @@ class profile(vector):
1263
1413
  if plot: # In order to allow other usages apart from Graphprofile.
1264
1414
  self.wetarea = a
1265
1415
  self.wetperimeter = s
1266
- self.hyrdaulicradius = r
1416
+ self.hydraulicradius = r
1267
1417
  self.waterdepth=h
1268
1418
  self.localwidth=w
1269
1419
  self.criticaldischarge = qcr
@@ -1271,6 +1421,11 @@ class profile(vector):
1271
1421
  return a,s,r,h,w,qcr
1272
1422
 
1273
1423
  def slopes(self):
1424
+ """ Compute the slopes of the section with respect to the upstream and downstream sections """
1425
+
1426
+ if self.up is None or self.down is None:
1427
+ logging.error(_('No upstream or downstream section defined for section {}').format(self.myname))
1428
+ return 0.,0.,0.
1274
1429
 
1275
1430
  slopedown = (self.get_minz() - self.down.get_minz()) / abs(self.down.s - self.s+1.e-10)
1276
1431
  slopeup = (self.up.get_minz() - self.get_minz()) / abs(self.s - self.up.s+1.e-10)
@@ -1278,30 +1433,52 @@ class profile(vector):
1278
1433
 
1279
1434
  return slopeup,slopecentered,slopedown
1280
1435
 
1281
- def ManningStrickler_Q(self,slope=1.e-3,nManning=0.,KStrickler=0.):
1282
- """Procédure générique pour obtenir une relation uniforme Q-H sur base
1283
- - nManning : un coefficient de frottement
1284
- - slope : une pente
1436
+ def ManningStrickler_Q(self, slope:float = 1.e-3, nManning:float = 0., KStrickler:float = 0.):
1437
+ """Procédure générique pour obtenir une relation uniforme Q-H sur base d'une pente et d'un coefficient de frottement.
1438
+
1439
+ :param slope: une pente
1440
+ :param nManning: un coefficient de frottement
1441
+ :param KStrickler: un coefficient de frottement (if nManning > 0.0, KStrickler is ignored)
1285
1442
  """
1286
1443
 
1444
+ if slope <= 0.:
1445
+ logging.error(_('No positive slope provided for section {}').format(self.myname))
1446
+ return
1447
+
1448
+ # Calcul des caractéristiques hydrauliques si pas encore faites
1449
+
1450
+ if self.waterdepth is None:
1451
+ self.relations(plot = True)
1452
+
1287
1453
  if nManning==0. and KStrickler==0.:
1454
+ logging.error(_('No friction coefficient provided for section {}').format(self.myname))
1288
1455
  return
1289
- elif nManning>0.:
1290
- coeff=1./nManning
1456
+ elif nManning > 0.:
1457
+ coeff = 1. / nManning
1291
1458
  elif KStrickler>0.:
1292
1459
  coeff = KStrickler
1293
1460
 
1294
- nn=len(self.waterdepth)
1295
- sqrtslope=math.sqrt(slope)
1461
+ nn = len(self.waterdepth)
1462
+ sqrtslope = math.sqrt(slope)
1463
+
1464
+ self.q_slope = asarray([coeff * self.hydraulicradius[k]**(2./3.)* sqrtslope * self.wetarea[k] for k in range(nn)])
1296
1465
 
1297
- self.q=asarray([coeff * self.hyrdaulicradius[k]**(2./3.)*sqrtslope * self.wetarea[k] for k in range(nn)])
1466
+ return self.q_slope
1298
1467
 
1299
- def ManningStrickler_oneQ(self,slope=1.e-3,nManning=0.,KStrickler=0.,cury=0.):
1468
+ def ManningStrickler_oneQ(self, slope:float = 1.e-3, nManning:float = 0., KStrickler:float = 0., cury:float = 0.):
1300
1469
  """Procédure générique pour obtenir une relation uniforme Q-H sur base
1301
- - nManning : un coefficient de frottement
1302
- - slope : une pente
1470
+
1471
+ :param slope: une pente
1472
+ :param nManning: un coefficient de frottement
1473
+ :param KStrickler: un coefficient de frottement (if nManning > 0.0, KStrickler is ignored)
1474
+ :param cury: une hauteur d'eau
1475
+ :return: le débit Q
1303
1476
  """
1304
1477
 
1478
+ if slope <= 0.:
1479
+ logging.error(_('No positive slope provided for section {}').format(self.myname))
1480
+ return
1481
+
1305
1482
  if nManning==0. and KStrickler==0.:
1306
1483
  return
1307
1484
  elif nManning>0.:
@@ -1317,16 +1494,19 @@ class profile(vector):
1317
1494
 
1318
1495
  return q
1319
1496
 
1320
- # Methods added
1321
- def deepcopy_profile(self, name: str = None):
1497
+ def deepcopy_profile(self, name: str = None, appendstr: str = '_copy'):
1322
1498
  """
1323
1499
  This method returns a deepcopy of the active profile.
1324
1500
  The profile features are individually transferred, therefore,
1325
1501
  only the necessary features are copied.
1502
+
1503
+ :param name: name of the copied profile (if None, the current profile's name + appendstr is used)
1504
+ :param appendstr: string to append to the current profile's name if no new name is given (default: '_copy')
1505
+ :return: the copied profile
1326
1506
  """
1327
1507
  # if a new name is not given, we add _copy to the current profile's name.
1328
1508
  if name is None:
1329
- name = self.myname + "_copy"
1509
+ name = self.myname + appendstr
1330
1510
 
1331
1511
  # We create the new profile (copy).
1332
1512
  copied_profile = profile(name)
@@ -1339,21 +1519,31 @@ class profile(vector):
1339
1519
  copied_profile.bankright = copy.deepcopy(self.bankright)
1340
1520
  copied_profile.bed = copy.deepcopy(self.bed)
1341
1521
  copied_profile.banksbed_postype = copy.deepcopy(self.banksbed_postype)
1522
+ copied_profile.bankleft_down = copy.deepcopy(self.bankleft_down)
1523
+ copied_profile.bankright_down = copy.deepcopy(self.bankright_down)
1342
1524
 
1343
1525
  return copied_profile
1344
1526
 
1345
- def color_active_profile(self, width: float = 3., color: list = [255, 0, 0], plot_opengl = True):
1527
+ def color_active_profile(self, width: float = 3., color: list = [255, 0, 0], plot_opengl:bool = True):
1346
1528
  """
1347
1529
  This method colors and thickens the active profile
1348
1530
  (default width : 3, default color: red).
1531
+
1532
+ :param width: width of the profile
1533
+ :param color: color of the profile as a list of RGB values (0-255)
1534
+ :param plot_opengl: if True, update the OpenGL plot of the parent zone
1349
1535
  """
1536
+
1350
1537
  self.myprop.width = width
1351
1538
  self.myprop.color = getIfromRGB(color)
1539
+
1352
1540
  if plot_opengl:
1353
- self.parentzone.plot(True) # FIXME (Parent zone)
1541
+ if self.parentzone is not None:
1542
+ self.parentzone.plot(True) # FIXME (Parent zone)
1543
+
1544
+ def highlighting(self, width: float = 3., color: list = [255, 0, 0] , plot_opengl:bool = True):
1545
+ """ Alias for color_active_profile """
1354
1546
 
1355
- def highlightning(self, width: float = 3., color: list = [255, 0, 0] , plot_opengl = True):
1356
- """Alias for color_active_profile"""
1357
1547
  self.color_active_profile(width, color)
1358
1548
 
1359
1549
  def uncolor_active_profile(self, plot_opengl = True):
@@ -1364,28 +1554,40 @@ class profile(vector):
1364
1554
  self.myprop.color = getIfromRGB([0, 0, 0])
1365
1555
 
1366
1556
  if plot_opengl:
1367
- self.parentzone.plot(True)
1557
+ if self.parentzone is not None:
1558
+ self.parentzone.plot(True)
1368
1559
 
1369
- def withdrawing(self , plot_opengl = True):
1560
+ def withdrawing(self, plot_opengl:bool = True):
1370
1561
  """Alias for uncolor_active_profile"""
1562
+
1371
1563
  self.uncolor_active_profile(plot_opengl)
1372
1564
 
1373
1565
  def ManningStrickler_profile(self, slope: float =1.e-3, nManning: float =0., KStrickler: float=0.):
1374
1566
  """
1375
- Procédure générique pour obtenir une relation uniforme Q-H d'un profile sur base:
1376
- - nManning ou KStrickler: un coefficient de frottement,
1377
- - slope : une pente fournie (default: 0.001),
1567
+ Procédure générique pour obtenir une relation uniforme Q-H d'un profile
1568
+
1569
+ :param slope: une pente
1570
+ :param nManning: un coefficient de frottement
1571
+ :param KStrickler: un coefficient de frottement (if nManning > 0.0, KStrickler is ignored)
1378
1572
 
1379
1573
  ainsi que les relations correspondant aux pentes aval(slope down), amont(slopeup), et amont-aval (centered).
1574
+
1575
+ :return: le débit Q maximum parmi les 4 relations
1380
1576
  """
1381
1577
  if self.down is not None and self.up is not None:
1382
1578
  slopeup,slopecentered,slopedown = self.slopes()
1579
+ # slopes are not necessarily positive --> subsequent tests
1580
+
1581
+ slopeup = max(slopeup, 0.)
1582
+ slopecentered = max(slopecentered, 0.)
1583
+ slopedown = max(slopedown, 0.)
1383
1584
  else:
1384
- slopecentered = 0
1385
- slopedown = 0
1386
- slopeup = 0
1585
+ slopecentered = 0.
1586
+ slopedown = 0.
1587
+ slopeup = 0.
1387
1588
 
1388
1589
  if nManning==0. and KStrickler==0.:
1590
+ logging.error(_('No friction coefficient provided for section {}').format(self.myname))
1389
1591
  return
1390
1592
  elif nManning>0.:
1391
1593
  coeff=1./nManning
@@ -1396,40 +1598,31 @@ class profile(vector):
1396
1598
 
1397
1599
 
1398
1600
  sqrtslope = math.sqrt(slope)
1399
- if slopedown > 0:
1400
- sqrtslopedown= math.sqrt(slopedown)
1401
- else:
1402
- sqrtslopedown = 0.
1601
+ sqrtslopedown= math.sqrt(slopedown)
1602
+ sqrtslopecentered= math.sqrt(slopecentered)
1603
+ sqrtslopeup= math.sqrt(slopeup)
1403
1604
 
1404
- if slopecentered > 0:
1405
- sqrtslopecentered= math.sqrt(slopecentered)
1406
- else:
1407
- sqrtslopecentered = 0.
1408
- if slopeup > 0:
1409
- sqrtslopeup= math.sqrt(slopeup)
1410
- else:
1411
- sqrtslopeup = 0.
1605
+ self.q_slope = asarray([coeff*(self.hydraulicradius[k]**(2/3))*sqrtslope*self.wetarea[k] for k in range (nn)])
1606
+ self.q_down=asarray([coeff * self.hydraulicradius[k]**(2./3.)*sqrtslopedown * self.wetarea[k] for k in range(nn)])
1607
+ self.q_up=asarray([coeff * self.hydraulicradius[k]**(2./3.)*sqrtslopeup * self.wetarea[k] for k in range(nn)])
1608
+ self.q_centered=asarray([coeff * self.hydraulicradius[k]**(2./3.)*sqrtslopecentered * self.wetarea[k] for k in range(nn)])
1412
1609
 
1413
- self.q_slope = asarray([coeff*(self.hyrdaulicradius[k]**(2/3))*sqrtslope*self.wetarea[k] for k in range (nn)])
1414
- self.q_down=asarray([coeff * self.hyrdaulicradius[k]**(2./3.)*sqrtslopedown * self.wetarea[k] for k in range(nn)])
1415
- self.q_up=asarray([coeff * self.hyrdaulicradius[k]**(2./3.)*sqrtslopeup * self.wetarea[k] for k in range(nn)])
1416
- self.q_centered=asarray([coeff * self.hyrdaulicradius[k]**(2./3.)*sqrtslopecentered * self.wetarea[k] for k in range(nn)])
1417
1610
  return max(max(self.q_slope), max(self.q_down), max(self.q_up), max(self.q_centered), max(self.criticaldischarge))
1418
1611
 
1419
1612
  def plotcs_profile(self,
1420
1613
  fig: Figure = None,
1421
1614
  ax: Axes = None,
1422
- compare = None,
1615
+ compare:"profile" = None,
1423
1616
  vecs : list = [],
1424
1617
  col_structure: str ='none',
1425
1618
  fwl: float = None,
1426
1619
  fwd: float = None,
1427
1620
  simuls:list=None,
1428
- show = False,
1429
- forceaspect= True,
1430
- plotlaz= True,
1431
- clear = True,
1432
- redraw=True ):
1621
+ show:bool = False,
1622
+ forceaspect:bool = True,
1623
+ plotlaz:bool = True,
1624
+ clear:bool = True,
1625
+ redraw:bool = True ):
1433
1626
  """
1434
1627
  This method plots the physical geometry of the current cross section (profile).
1435
1628
 
@@ -1439,6 +1632,21 @@ class profile(vector):
1439
1632
  - idsimul: list of available numerical simulations containing this profile,
1440
1633
  - zsimul: list of water level in the simulations.
1441
1634
  - col_structure colors the structure displayed.
1635
+
1636
+ :param fig: Matplotlib figure
1637
+ :param ax: Matplotlib axes
1638
+ :param compare: reference profile to compare with
1639
+ :param vecs: list of vector objects to plot on the section (e.g. levees, dikes, ...)
1640
+ :param col_structure: color of the structures (default: 'none')
1641
+ :param fwl: water level to plot (if None, no water level is plotted)
1642
+ :param fwd: water depth to plot (if None, no water level is plotted)
1643
+ :param simuls: list of simulations containing the profile (id, name, date, time, water level)
1644
+ :param show: if True, show the figure
1645
+ :param forceaspect: if True, force aspect ratio
1646
+ :param plotlaz: if True, plot LAZ points if available
1647
+ :param clear: if True, clear the axis before plotting
1648
+ :param redraw: if True, redraw the figure after plotting
1649
+
1442
1650
  """
1443
1651
 
1444
1652
  idsimul=None
@@ -1648,10 +1856,11 @@ class profile(vector):
1648
1856
  fwd: float = None,
1649
1857
  fwq: float = None,
1650
1858
  simuls:list=None,
1651
- show = False,
1652
- clear = True,
1653
- labels = True,
1654
- redraw =True):
1859
+ show:bool = False,
1860
+ clear:bool = True,
1861
+ labels:bool = True,
1862
+ redraw:bool =True,
1863
+ force_label_to_right:bool = True):
1655
1864
  """
1656
1865
  This method plots the discharges relationship computed
1657
1866
  with the methods: relations and ManningStrcikler_profile.
@@ -1662,8 +1871,22 @@ class profile(vector):
1662
1871
  - idsimul: list of available numerical models.
1663
1872
  - qsimul: list of discharges in the available numerical models,
1664
1873
  - hsimul: list of water depth in the available numerical models.
1874
+
1875
+ :param fig: Matplotlib figure
1876
+ :param ax: Matplotlib axes
1877
+ :param fwl: water level to plot (if None, no water level is plotted)
1878
+ :param fwd: water depth to plot (if None, no water level is plotted)
1879
+ :param fwq: discharge to plot (if None, no discharge is plotted)
1880
+ :param simuls: list of simulations containing the profile (id, name, date, time, discharge, water level)
1881
+ :param show: if True, show the figure
1882
+ :param clear: if True, clear the axis before plotting
1883
+ :param labels: if True, show the legend
1884
+ :param redraw: if True, redraw the figure after plotting
1665
1885
  """
1666
1886
 
1887
+ if self.q_slope is None:
1888
+ raise Exception(_('Please compute the discharge relationship first using the method "ManningStrickler_profile"'))
1889
+
1667
1890
  hsimul = None
1668
1891
  qsimul = None
1669
1892
  idsimul = None
@@ -1681,12 +1904,18 @@ class profile(vector):
1681
1904
  if fwd is None or fwd <= 0:
1682
1905
  fwd =0
1683
1906
  fwl= self.zmin+ fwd
1907
+ plot_wl = False
1684
1908
  elif fwd > 0 :
1685
1909
  fwl = self.zmin + fwd
1910
+ plot_wl = True
1686
1911
 
1687
1912
  elif fwl is not None:
1688
1913
  fwl = fwl
1689
1914
  fwd = fwl-self.zmin
1915
+ plot_wl = True
1916
+
1917
+ if fig is None:
1918
+ fig, ax = plt.subplots()
1690
1919
 
1691
1920
  # Creation of a new ax for cleaning purposes in the wolfhece.Graphprofile.
1692
1921
  myax2 = ax
@@ -1721,7 +1950,8 @@ class profile(vector):
1721
1950
  myax2.axhline(y=zb, color=_('blue'), alpha=1, lw=1, label =_('Bed'), ls =_('dotted'))
1722
1951
 
1723
1952
  # Desired water depth
1724
- myax2.axhspan(ymin= self.zmin, ymax =fwl,color=_('cyan'), alpha=0.2, lw=2, label =_('Desired water depth'))
1953
+ if plot_wl:
1954
+ myax2.axhspan(ymin= self.zmin, ymax =fwl,color=_('cyan'), alpha=0.2, lw=2, label =_('Desired water depth'))
1725
1955
 
1726
1956
  myax2.set_xlabel(_('Discharge - Q ($m^3/s$)'), size=12)
1727
1957
  myax2.set_ylim(self.zmin, self.zmax+1)
@@ -1729,7 +1959,10 @@ class profile(vector):
1729
1959
  if qsimul is not None:
1730
1960
  myax2.set_xlim(0., max(max(self.q_down),max(self.q_up),max(self.q_centered),max(self.q_slope), max(self.criticaldischarge),max(qsimul)))
1731
1961
  else:
1732
- myax2.set_xlim(0., max(max(self.q_down),max(self.q_up),max(self.q_centered),max(self.q_slope), max(self.criticaldischarge)))
1962
+ if self and self.down and self.up:
1963
+ myax2.set_xlim(0., max(max(self.q_down),max(self.q_up),max(self.q_centered),max(self.q_slope), max(self.criticaldischarge)))
1964
+ else:
1965
+ myax2.set_xlim(0., max(max(self.q_slope), max(self.criticaldischarge)))
1733
1966
 
1734
1967
  # Conversion methods for the second matplotlib ax
1735
1968
  def alt_to_depth(y):
@@ -1737,7 +1970,7 @@ class profile(vector):
1737
1970
  def depth_to_alt(y):
1738
1971
  return y+self.zmin
1739
1972
 
1740
- secax = myax2.secondary_yaxis(_('right'), functions=(alt_to_depth,depth_to_alt))
1973
+ secax = myax2.secondary_yaxis('right', functions=(alt_to_depth,depth_to_alt))
1741
1974
  #secax.set_yticks(y)
1742
1975
  myax2.yaxis.tick_left()
1743
1976
  #myax2.set_yticks(alt_to_depth(self.waterdepth))
@@ -1750,106 +1983,351 @@ class profile(vector):
1750
1983
 
1751
1984
 
1752
1985
  if labels:
1753
- myax2.set_ylabel(_('Water depth - h\n($m$)'), size=12, rotation=270,labelpad=35)
1754
- myax2.yaxis.set_label_position(_('right'))
1755
- secax.set_ylabel(_('Altitude - Z\n($m$)'), size=12, labelpad=10)
1986
+ myax2.set_ylabel(_('Altitude - Z ($m$)'), size=12, rotation=270,labelpad=35)
1987
+ if force_label_to_right:
1988
+ myax2.yaxis.set_label_position('right')
1989
+ secax.set_ylabel(_('Water depth - h ($m$)'), size=12, labelpad=10)
1756
1990
  fig.suptitle('Discharges C.S. - %s'%(self.myname), size=15)
1757
1991
 
1758
1992
 
1759
- if show:
1760
- fig.show()
1993
+ if show:
1994
+ fig.show()
1995
+
1996
+ def plot_discharges_UnCr(self,
1997
+ fig: Figure = None,
1998
+ ax: Axes = None,
1999
+ fwl: float = None,
2000
+ fwd: float = None,
2001
+ fwq: float = None,
2002
+ simuls:list=None,
2003
+ show:bool = False,
2004
+ clear:bool = True,
2005
+ labels:bool = True,
2006
+ redraw:bool =True,
2007
+ force_label_to_right:bool = False):
2008
+
2009
+ self.plotcs_discharges(fig, ax, fwl, fwd, fwq, simuls, show, clear, labels, redraw, force_label_to_right)
2010
+
2011
+ def plotcs_hspw(self,
2012
+ fig:Figure = None, ax:Axes = None,
2013
+ fwl:float = None, fwd:float = None,
2014
+ fwq:float = None,
2015
+ show:bool = False, clear:bool = True,
2016
+ labels:bool = True, redraw:bool =True,
2017
+ force_label_to_right:bool = True):
2018
+
2019
+ """
2020
+ This method plots the hydraulic geometries computed by the relations method
2021
+ (Hydraulic radius, wetted area, wetted perimeter, Top width).
2022
+
2023
+ - fwl: for water level,
2024
+ - fwd: for water depth,
2025
+ - fwq: for water discharge.
2026
+
2027
+ :param fig: Matplotlib figure
2028
+ :param ax: Matplotlib axes
2029
+ :param fwl: water level to plot (if None, no water level is plotted)
2030
+ :param fwd: water depth to plot (if None, no water level is plotted)
2031
+ :param fwq: discharge to plot (if None, no discharge is plotted)
2032
+ :param show: if True, show the figure
2033
+ :param clear: if True, clear the axis before plotting
2034
+ :param labels: if True, show the legend
2035
+ :param redraw: if True, redraw the figure after plotting
2036
+
2037
+ """
2038
+ if self.waterdepth is None:
2039
+ self.relations()
2040
+
2041
+ if fig is None and ax is None:
2042
+ fig, ax = plt.subplots()
2043
+
2044
+ sl,sb,sr,zl,zb,zr, sld, srd, zld, zrd = self.get_sz_banksbed()
2045
+ x,y = self.get_sz()
2046
+
2047
+ if fwl is None:
2048
+ if fwd is None or fwd <= 0:
2049
+ fwd =0
2050
+ fwl= self.zmin+ fwd
2051
+ plot_wl = False
2052
+ elif fwd > 0 :
2053
+ fwl = self.zmin + fwd
2054
+ plot_wl = True
2055
+
2056
+ elif fwl is not None:
2057
+ fwl = fwl
2058
+ fwd = fwl-self.zmin
2059
+ plot_wl = True
2060
+
2061
+ # Creation of a new ax for cleaning purposes in the wolfhece.Graphprofile.
2062
+ myax3 = ax
2063
+ axt3 = ax
2064
+ if redraw:
2065
+ if clear:
2066
+ axt3.clear()
2067
+ # myax3.clear()
2068
+
2069
+ # Plots
2070
+ # FIXME (Clearing issues) a second x axis for the Hydraulic radius to provide more clarity.
2071
+ axt3.plot(self.hydraulicradius,self.waterdepth + self.zmin,color=_('green'),lw=2,label=_('H - Hydraulic radius($m$)'), ls= _('--')) #Plot the evaluation of the hydraulic radius as function of water depth
2072
+ axt3.set_xlim(0, max(self.hydraulicradius))
2073
+
2074
+ myax3.plot(self.wetarea,self.waterdepth + self.zmin,color=_('black'),lw=2.0,label=_('S - Wet area ($m^2$)')) #Plot the wetted area as function of water depth
2075
+ myax3.plot(self.wetperimeter,self.waterdepth + self.zmin,color=_('magenta'),lw=1,label=_('P - Wet perimeter($m$)')) #Plot the wetted perimeter as function of water depth
2076
+ myax3.plot(self.localwidth,self.waterdepth + self.zmin,color=_('red'),lw=1,label=_('W - Top Width ($m$)')) #Plot the evalution of the water table as function of water depth
2077
+
2078
+ # Selection of hydraulic geometries based on their index
2079
+ if fwq is not None and fwq > 0.:
2080
+ # First, We select the closest critical discharge to the user's input.
2081
+ mask = np.absolute(self.criticaldischarge - fwq)
2082
+ index = np.argmin(mask)
2083
+ # Second, since the matrices have the same shapes and their elements are sorted likewise,
2084
+ # we use the index of the selected critical discharge to find the corresponding hydraulic geometries.
2085
+ cr_wetarea = self.wetarea[index]
2086
+ cr_wetperimeter = self.wetperimeter[index]
2087
+ cr_width = self.localwidth[index]
2088
+ cr_radius = self.hydraulicradius[index]
2089
+ cr_h = self.waterdepth[index]
2090
+ myax3.plot(cr_wetarea,cr_h + self.zmin,'ok' )
2091
+ myax3.annotate(_('$Critical$ $characteristics$ \nH: %s $m$ \nS: %s $m^2$ \nP: %s $m$ \nW: %s $m$ \n \n')% (round(cr_radius,2),round(cr_wetarea,2),round(cr_wetperimeter,2),round(cr_width,2)),\
2092
+ (cr_wetarea, cr_h + self.zmin), alpha= 1, fontsize = _('x-small'),color =_('black'))
2093
+
2094
+ #Finally, we plot the critical hydraulic geometries as dots.
2095
+ myax3.plot(cr_wetperimeter,cr_h + self.zmin,_('om') )
2096
+ myax3.plot(cr_width,cr_h + self.zmin,_('or') )
2097
+ myax3.plot(cr_radius,cr_h + self.zmin,_('og') )
2098
+
2099
+ #Displayed water depths and banks
2100
+ if plot_wl:
2101
+ myax3.axhspan(ymin= self.zmin, ymax =fwd + self.zmin,color=_('cyan'), alpha=0.2, lw=2, label =_('Desired water depth'))
2102
+
2103
+ if zl != -99999.:
2104
+ myax3.axhline(y=zl, color=_('magenta'), alpha=1, lw=1, label =_('Left bank'), ls =_('dotted') )
2105
+ if zr != -99999.:
2106
+ myax3.axhline(y=zr, color=_('green'), alpha=1, lw=1, label =_('right bank'), ls =_('dotted') )
2107
+ if zb != -99999.:
2108
+ myax3.axhline(y=zb, color=_('blue'), alpha=1, lw=1, label =_(' bed '), ls =_('dotted'))
2109
+
2110
+ #Limits and labels
2111
+ myax3.set_ylim(self.zmin,self.zmax+1)
2112
+ myax3.set_xlim(0., max(max(self.wetarea), max(self.wetperimeter), max(self.localwidth)))
2113
+ myax3.set_xlabel(_('S - P - W'), size=12)
2114
+ myax3.set_ylabel(_('Altitude - Z ($m$)'), size=12,rotation=270, labelpad=50)
2115
+
2116
+ #Conversion methods for the second y axis
2117
+ def alt_to_depth(y):
2118
+ return y-self.zmin
2119
+ def depth_to_alt(y):
2120
+ return y+self.zmin
2121
+ secax = myax3.secondary_yaxis('right', functions=(alt_to_depth,depth_to_alt))
2122
+
2123
+ myax3.yaxis.tick_left()
2124
+ if force_label_to_right:
2125
+ myax3.yaxis.set_label_position('right')
2126
+ if labels:
2127
+ secax.set_ylabel(_('Water depth - h ($m$)'), size=12, rotation=270, labelpad=20)
2128
+ myax3.grid()
2129
+
2130
+ if show:
2131
+ fig.show()
2132
+
2133
+ def plotcs_relations(self,
2134
+ fig:Figure = None, ax:Axes = None,
2135
+ fwl:float = None, fwd:float = None,
2136
+ fwq:float = None,
2137
+ show:bool = False, clear:bool = True,
2138
+ labels:bool = True, redraw:bool =True,
2139
+ force_label_to_right:bool = False
2140
+ ):
2141
+ """
2142
+ This method plots the cross section relations (geometry, hydraulic geometries)
2143
+
2144
+ It is the same than 'plotcs_hspw' but with (force_label_to_right = False) by default
2145
+ """
2146
+
2147
+ self.plotcs_hspw(fig=fig, ax=ax,
2148
+ fwl=fwl, fwd=fwd,
2149
+ fwq=fwq,
2150
+ show=show, clear=clear,
2151
+ labels=labels, redraw=redraw,
2152
+ force_label_to_right=force_label_to_right)
2153
+
2154
+
2155
+ def plot_linked(self, fig:Figure, ax:Axes, linked_arrays:dict, colors:list=['red','blue','green']):
2156
+ """ Plot linked arrays on the section plot """
2157
+
2158
+ from .wolf_array import WolfArray
2159
+
2160
+ # colors=['red','blue','green']
2161
+ nb_colors = len(colors)
2162
+
2163
+ k=0
2164
+ for curlabel, curarray in linked_arrays.items():
2165
+
2166
+ curarray:WolfArray
2167
+ if curarray.plotted:
2168
+
2169
+ length = self.linestring.length
2170
+ ds = min(curarray.dx,curarray.dy)
2171
+ nb = int(np.ceil(length/ds*2))
2172
+
2173
+ alls = np.linspace(0,int(length),nb)
2174
+
2175
+ pts = [self.linestring.interpolate(curs) for curs in alls]
2176
+
2177
+ allz = [curarray.get_value(curpt.x,curpt.y) for curpt in pts]
2178
+
2179
+ if np.max(allz)>-99999:
2180
+ ax.plot(alls,allz,
2181
+ color=colors[np.mod(k,nb_colors)],
2182
+ lw=2.0,
2183
+ label=curlabel)
2184
+ k+=1
2185
+
2186
+ def _plot_only_cs(self,
2187
+ fig:Figure=None, ax:Axes=None,
2188
+ label='', alpha=0.8, lw=1., style: str ='dashed',
2189
+ centerx=0., centery=0., grid=True, col_ax: str = 'black'):
2190
+ """ Plot only the cross section line on an existing axis """
2191
+
2192
+ x,y=self.get_sz()
2193
+
2194
+ sl,sb,sr,yl,yb,yr, sld, srd, yld, yrd = self.get_sz_banksbed()
2195
+
2196
+ if centerx >0. and sb!=-99999.:
2197
+ decal = centerx-sb
2198
+ x+=decal
2199
+ sl+=decal
2200
+ sb+=decal
2201
+ sr+=decal
2202
+
2203
+ if centery >0. and yb!=-99999.:
2204
+ decal = centery-yb
2205
+ y+=decal
2206
+ yl+=decal
2207
+ yb+=decal
2208
+ yr+=decal
2209
+
2210
+ ax.plot(x,y,color=col_ax,
2211
+ lw=lw,
2212
+ linestyle=style,
2213
+ alpha=alpha,
2214
+ label=label)
2215
+
2216
+ curtick=ax.get_xticks()
2217
+ ax.set_xticks(np.arange(min(curtick[0],(x[0]//2)*2),max(curtick[-1],(x[-1]//2)*2),2))
1761
2218
 
1762
- def plotcs_hspw(self, fig: Figure = None, ax: Axes = None, fwl: float = None, fwd: float = None, fwq: float = None, show = False, clear = True, labels = True,redraw =True):
2219
+ if sl != -99999.:
2220
+ ax.plot(sl,yl,'or',alpha=alpha)
2221
+ if sb != -99999.:
2222
+ ax.plot(sb,yb,'ob',alpha=alpha)
2223
+ if sr != -99999.:
2224
+ ax.plot(sr,yr,'og',alpha=alpha)
2225
+ if sld != -99999.:
2226
+ ax.plot(sld,yld,'*r',alpha=alpha)
2227
+ if srd != -99999.:
2228
+ ax.plot(srd,yrd,'*g',alpha=alpha)
1763
2229
 
2230
+ def plot_cs(self,
2231
+ fwl:float = None, show:bool = False, forceaspect:bool = True,
2232
+ fig:Figure = None, ax:Axes = None,
2233
+ plotlaz:bool = True, clear:bool = True,
2234
+ linked_arrays:dict = {}):
2235
+ """ Plot the cross section with matplotlib - with options
2236
+
2237
+ :param fwl: fill water level - if None, no filling
2238
+ :param show: if True, show the figure
2239
+ :param forceaspect: if True, force aspect ratio
2240
+ :param fig: figure Matplotlib
2241
+ :param ax: axes Matplotlib
2242
+ :param plotlaz: if True, plot LAZ points if available
2243
+ :param clear: if True, clear the axis before plotting
2244
+ :param linked_arrays: dictionary of linked arrays to plot on the section
1764
2245
  """
1765
- This method plots the hydraulic geometries computed by the relations method
1766
- (Hydraulic radius, wetted area, wetted perimeter, Top width).
1767
2246
 
1768
- - fwl: for water level,
1769
- - fwd: for water depth,
1770
- - fwq: for water discharge.
2247
+ if linked_arrays is None:
2248
+ linked_arrays = {}
1771
2249
 
1772
- """
1773
- sl,sb,sr,zl,zb,zr, sld, srd, zld, zrd = self.get_sz_banksbed()
1774
- x,y = self.get_sz()
2250
+ x,y=self.get_sz()
1775
2251
 
1776
- if fwl is None:
1777
- if fwd is None or fwd <= 0:
1778
- fwd =0
1779
- fwl= self.zmin+ fwd
1780
- elif fwd > 0 :
1781
- fwl = self.zmin + fwd
2252
+ sl,sb,sr,yl,yb,yr, sld, srd, yld, yrd = self.get_sz_banksbed()
1782
2253
 
1783
- elif fwl is not None:
1784
- fwl = fwl
1785
- fwd = fwl-self.zmin
2254
+ xmin=x[0]
2255
+ xmax=x[-1]
2256
+ ymin=self.get_minz()
2257
+ ymax=self.get_maxz()
1786
2258
 
1787
- # Creation of a new ax for cleaning purposes in the wolfhece.Graphprofile.
1788
- myax3 = ax
1789
- axt3 = ax
1790
- if redraw:
2259
+ dy=ymax-ymin
2260
+ ymin-=dy/4.
2261
+ ymax+=dy/4.
2262
+
2263
+ if ax is None:
2264
+ redraw=False
2265
+ fig = plt.figure()
2266
+ ax=fig.add_subplot(111)
2267
+ else:
2268
+ redraw=True
1791
2269
  if clear:
1792
- axt3.clear()
1793
- # myax3.clear()
2270
+ ax.cla()
1794
2271
 
1795
- # Plots
1796
- # FIXME (Clearing issues) a second x axis for the Hydraulic radius to provide more clarity.
1797
- axt3.plot(self.hyrdaulicradius,self.waterdepth + self.zmin,color=_('green'),lw=2,label=_('H - Hydraulic radius($m$)'), ls= _('--')) #Plot the evaluation of the hydraulic radius as function of water depth
1798
- axt3.set_xlim(0, max(self.hyrdaulicradius))
2272
+ if np.min(y) != -99999. and np.max(y) != -99999.:
2273
+ ax.plot(x,y,color='black',
2274
+ lw=2.0,
2275
+ label='Profil')
1799
2276
 
1800
- myax3.plot(self.wetarea,self.waterdepth + self.zmin,color=_('black'),lw=2.0,label=_('S - Wet area ($m^2$)')) #Plot the wetted area as function of water depth
1801
- myax3.plot(self.wetperimeter,self.waterdepth + self.zmin,color=_('magenta'),lw=1,label=_('P - Wet perimeter($m$)')) #Plot the wetted perimeter as function of water depth
1802
- myax3.plot(self.localwidth,self.waterdepth + self.zmin,color=_('red'),lw=1,label=_('W - Top Width ($m$)')) #Plot the evalution of the water table as function of water depth
2277
+ if self.parent is not None:
2278
+ if plotlaz and self.parent.gridlaz is not None:
2279
+ minlaz=ymin
2280
+ maxlaz=ymax
2281
+ minlaz,maxlaz=self.plot_laz(fig=fig,ax=ax)
2282
+ if np.min(y) != -99999. and np.max(y) != -99999.:
2283
+ ymin = min(ymin,minlaz)
2284
+ ymax = max(ymax,maxlaz)
2285
+ else:
2286
+ ymin = minlaz
2287
+ ymax = maxlaz
1803
2288
 
1804
- # Selection of hydraulic geometries based on their index
1805
- if fwq is not None and fwq > 0.:
1806
- # First, We select the closest critical discharge to the user's input.
1807
- mask = np.absolute(self.criticaldischarge - fwq)
1808
- index = np.argmin(mask)
1809
- # Second, since the matrices have the same shapes and their elements are sorted likewise,
1810
- # we use the index of the selected critical discharge to find the corresponding hydraulic geometries.
1811
- cr_wetarea = self.wetarea[index]
1812
- cr_wetperimeter = self.wetperimeter[index]
1813
- cr_width = self.localwidth[index]
1814
- cr_radius = self.hyrdaulicradius[index]
1815
- cr_h = self.waterdepth[index]
1816
- myax3.plot(cr_wetarea,cr_h + self.zmin,'ok' )
1817
- myax3.annotate(_('$Critical$ $characteristics$ \nH: %s $m$ \nS: %s $m^2$ \nP: %s $m$ \nW: %s $m$ \n \n')% (round(cr_radius,2),round(cr_wetarea,2),round(cr_wetperimeter,2),round(cr_width,2)),\
1818
- (cr_wetarea, cr_h + self.zmin), alpha= 1, fontsize = _('x-small'),color =_('black'))
2289
+ self.plot_linked(fig,ax,linked_arrays)
1819
2290
 
1820
- #Finally, we plot the critical hydraulic geometries as dots.
1821
- myax3.plot(cr_wetperimeter,cr_h + self.zmin,_('om') )
1822
- myax3.plot(cr_width,cr_h + self.zmin,_('or') )
1823
- myax3.plot(cr_radius,cr_h + self.zmin,_('og') )
2291
+ if fwl is not None:
2292
+ ax.fill_between(x,y,fwl,where=y<=fwl,facecolor='cyan',alpha=0.3,interpolate=True)
1824
2293
 
1825
- #Displayed water depths and banks
1826
- myax3.axhspan(ymin= self.zmin, ymax =fwd + self.zmin,color=_('cyan'), alpha=0.2, lw=2, label =_('Desired water depth'))
1827
- myax3.axhline(y=zl, color=_('magenta'), alpha=1, lw=1, label =_('Left bank'), ls =_('dotted') )
1828
- myax3.axhline(y=zr, color=_('green'), alpha=1, lw=1, label =_('right bank'), ls =_('dotted') )
1829
- myax3.axhline(y=zb, color=_('blue'), alpha=1, lw=1, label =_(' bed '), ls =_('dotted'))
2294
+ if sl != -99999.:
2295
+ ax.plot(sl,yl,'or')
2296
+ if sb != -99999.:
2297
+ ax.plot(sb,yb,'ob')
2298
+ if sr != -99999.:
2299
+ ax.plot(sr,yr,'og')
2300
+ if sld != -99999.:
2301
+ ax.plot(sld,yld,'*r')
2302
+ if srd != -99999.:
2303
+ ax.plot(srd,yrd,'*g')
1830
2304
 
1831
- #Limits and labels
1832
- myax3.set_ylim(self.zmin,self.zmax+1)
1833
- myax3.set_xlim(0., max(max(self.wetarea), max(self.wetperimeter), max(self.localwidth)))
1834
- myax3.set_xlabel(_('S - P - W'), size=12)
1835
- myax3.set_ylabel(_('Water depth - h\n($m$)'), size=12,rotation=270, labelpad=50)
2305
+ ax.set_title(self.myname)
2306
+ ax.set_xlabel(_('Distance [m]'))
2307
+ ax.set_ylabel('Elevation [EL.m]')
2308
+ ax.legend()
1836
2309
 
1837
- #Conversion methods for the second y axis
1838
- def alt_to_depth(y):
1839
- return y-self.zmin
1840
- def depth_to_alt(y):
1841
- return y+self.zmin
1842
- secax = myax3.secondary_yaxis(_('right'), functions=(alt_to_depth,depth_to_alt))
2310
+ tol=(xmax-xmin)/10.
2311
+ ax.set_xlim(xmin-tol,xmax+tol)
2312
+ ax.set_ylim(ymin,ymax)
1843
2313
 
1844
- myax3.yaxis.tick_left()
1845
- myax3.yaxis.set_label_position(_('right'))
1846
- if labels:
1847
- secax.set_ylabel(_('Altitude - Z ($m$)'), size=12, rotation=270, labelpad=20)
1848
- myax3.grid()
2314
+ nbticks = 20
2315
+ dzticks = max((((ymax-ymin)/nbticks) // .25) *.25,.25)
2316
+
2317
+ ax.set_yticks(np.arange((ymin//.25)*.25,(ymax//.25)*.25,dzticks))
2318
+ ax.grid()
2319
+
2320
+ if forceaspect:
2321
+ aspect=1.0*(ymax-ymin)/(xmax-xmin)*(ax.get_xlim()[1] - ax.get_xlim()[0]) / (ax.get_ylim()[1] - ax.get_ylim()[0])
2322
+ ax.set_aspect(aspect)
1849
2323
 
1850
2324
  if show:
1851
2325
  fig.show()
1852
2326
 
2327
+ if redraw:
2328
+ fig.canvas.draw()
2329
+
2330
+ return sl,sb,sr,yl,yb,yr
1853
2331
 
1854
2332
 
1855
2333
  class crosssections(Element_To_Draw):
@@ -1860,6 +2338,7 @@ class crosssections(Element_To_Draw):
1860
2338
  - SPW_2025 --> format ='2025_xlsx'
1861
2339
  - WOLF vecz --> format ='vecz'
1862
2340
  - WOLF sxy --> format ='sxy'
2341
+ - WOLF zones --> format ='zones'
1863
2342
 
1864
2343
  L'objet stocke ses informations dans un dictionnaire : self.myprofiles
1865
2344
  Les clés de chaque entrée sont:
@@ -1884,22 +2363,54 @@ class crosssections(Element_To_Draw):
1884
2363
 
1885
2364
  """
1886
2365
 
1887
- myprofiles:dict
2366
+ myprofiles:dict['cs':profile, 'index':int, 'left':wolfvertex, 'bed':wolfvertex, 'right':wolfvertex, 'left_down':wolfvertex, 'right_down':wolfvertex]
1888
2367
  mygenprofiles:dict
1889
2368
 
1890
2369
  def __init__(self,
1891
- myfile:str = '',
1892
- format:typing.Literal['2000','2022', '2025_xlsx','vecz','sxy']='2022',
2370
+ fn_or_Zones:str | Path | Zones = '',
2371
+ format:typing.Literal['2000','2022', '2025_xlsx','vecz','sxy', 'zones']='2022',
1893
2372
  dirlaz:typing.Union[str, xyz_laz_grids] =r'D:\OneDrive\OneDrive - Universite de Liege\Crues\2021-07 Vesdre\CSC - Convention - ARNE\Data\LAZ_Vesdre\2023',
1894
2373
  mapviewer = None,
1895
2374
  idx='',
1896
- plotted=True) -> None:
2375
+ plotted=True,
2376
+ from_zones:Zones = None,
2377
+ force_unique_name:bool = False) -> None:
2378
+ """
2379
+ Constructor of the crosssections class
2380
+
2381
+ ATTENTION:
2382
+ - If you use "from_zones", the cross-sections will be read from the provided Zones instance, overriding fn_or_Zones and format.
2383
+ - If you use "from_zones", the cross-sections will be added only if "used" in the Zones instance.
2384
+ - If you use "fn_or_Zones" as an instance of Zones, the format must be "zones". In this case, only the first zone will be used and cross-sections will be added even if not "used".
2385
+
2386
+ :param fn_or_Zones: filename (str or Path) or instance of Zones containing the sections
2387
+ :param format: format of the sections file - '2000','2022','2025_xlsx','vecz','sxy', 'zones'
2388
+ :param dirlaz: directory containing LAZ files or instance of xyz_laz_grids
2389
+ :param mapviewer: instance of MapViewer (if None, no mapviewer is used)
2390
+ :param idx: index of the crosssections instance
2391
+ :param plotted: if True, the crosssections will be plotted in the mapviewer (if provided)
2392
+ :param from_zones: if not None, the sections will be read from the provided Zones instance (overrides fn_or_Zones and format)
2393
+ :param force_unique_name: if True, force unique names for each section (useful when reading from Zones)
2394
+ :return: None
1897
2395
 
1898
- assert format in ['2000','2022','2025_xlsx','vecz','sxy'], _('Format %s not supported!')%format
2396
+ """
2397
+
2398
+ assert format in ['2000','2022','2025_xlsx','vecz','sxy', 'zones'], _('Format %s not supported!')%format
1899
2399
 
1900
2400
  super().__init__(idx=idx, plotted= plotted, mapviewer=mapviewer, need_for_wx=False)
1901
2401
 
1902
- self.filename=myfile
2402
+ if from_zones is not None:
2403
+ assert isinstance(from_zones, Zones), _('from_zones must be an instance of Zones!')
2404
+ self.filename = ''
2405
+ format=''
2406
+ elif isinstance(fn_or_Zones, Zones):
2407
+ assert format=='zones', _('If fn_or_Zones is an instance of Zones, format must be "zones"!')
2408
+ self.filename = fn_or_Zones.filename
2409
+ elif isinstance(fn_or_Zones, (str, Path)):
2410
+ self.filename = fn_or_Zones
2411
+ else:
2412
+ raise Exception(_('fn_or_Zones must be a string, Path or an instance of Zones!'))
2413
+
1903
2414
  self.myzones=None
1904
2415
  self.myzone=None
1905
2416
 
@@ -1920,25 +2431,25 @@ class crosssections(Element_To_Draw):
1920
2431
  self.linked_zones=None
1921
2432
 
1922
2433
  if format in ['2000','2022','sxy']:
1923
- self.filename=myfile
1924
- if Path(myfile).exists() and myfile!='':
1925
- f=open(myfile,'r')
2434
+ self.filename=fn_or_Zones
2435
+ if Path(fn_or_Zones).exists() and fn_or_Zones!='':
2436
+ f=open(fn_or_Zones,'r')
1926
2437
  lines=f.read().splitlines()
1927
2438
  f.close()
1928
2439
  elif format=='2025_xlsx':
1929
2440
  # For the 2025_xlsx format, we need to read the file using pandas
1930
- if Path(myfile).exists() and myfile!='':
2441
+ if Path(fn_or_Zones).exists() and fn_or_Zones!='':
1931
2442
  # read the first sheet of the excel file
1932
2443
  # Note: header=1 means that the first row is not the header, but the second row is.
1933
- logging.info(_('Reading cross section data from %s')%myfile)
2444
+ logging.info(_('Reading cross section data from %s')%fn_or_Zones)
1934
2445
  try:
1935
- lines = pd.read_excel(myfile, sheet_name=0, header=1)
1936
- logging.info(_('Cross section data read successfully from %s')%myfile)
2446
+ lines = pd.read_excel(fn_or_Zones, sheet_name=0, header=1)
2447
+ logging.info(_('Cross section data read successfully from %s')%fn_or_Zones)
1937
2448
  except Exception as e:
1938
- logging.error(_('Error reading the file %s: %s')%(myfile, str(e)))
2449
+ logging.error(_('Error reading the file %s: %s')%(fn_or_Zones, str(e)))
1939
2450
  lines = pd.DataFrame()
1940
2451
  else:
1941
- logging.error(_('File %s does not exist!')%myfile)
2452
+ logging.error(_('File %s does not exist!')%fn_or_Zones)
1942
2453
  lines = []
1943
2454
  # For other formats (e.g. vecz)
1944
2455
  else:
@@ -2232,23 +2743,60 @@ class crosssections(Element_To_Draw):
2232
2743
  curdict['right']=cursect.bankright
2233
2744
 
2234
2745
 
2235
- # To make a distinction between cases for vecz
2746
+ elif from_zones is not None:
2747
+
2748
+ for curzone in from_zones.myzones:
2749
+ for curvec in curzone.myvectors:
2750
+ if curvec.used:
2751
+
2752
+ if curvec.myname in self.myprofiles.keys():
2753
+ if force_unique_name:
2754
+ logging.info(_('Profile %s already exists in cross-sections. We change its name to %s.')%(curvec.myname, f"{curvec.myname}_{len(self.myprofiles)}"))
2755
+ curvec.myname = f"{curvec.myname}_{len(self.myprofiles)}"
2756
+ else:
2757
+ logging.warning(_('Profile %s already exists in cross-sections. Previous will be overwritten.')%curvec.myname)
2758
+
2759
+ self.myprofiles[curvec.myname]={}
2760
+ curdict=self.myprofiles[curvec.myname]
2761
+
2762
+ curdict['index']=len(self.myprofiles)
2763
+ curdict['left']=None
2764
+ curdict['bed']=None
2765
+ curdict['right']=None
2766
+ curdict['left_down'] = None
2767
+ curdict['right_down'] = None
2768
+
2769
+ curdict['cs'] = profile(name=curvec.myname, parent=self)
2770
+ cursect:profile
2771
+ cursect=curdict['cs']
2772
+
2773
+ cursect.myvertices = curvec.myvertices.copy()
2774
+
2236
2775
  elif len(lines)==0:
2776
+
2237
2777
  if format=='vecz' or format=='zones':
2238
2778
 
2239
- if isinstance(myfile, Zones):
2240
- self.filename=myfile.filename
2241
- tmpzones=myfile
2242
- elif isinstance(myfile, str):
2243
- self.filename=myfile
2244
- tmpzones=Zones(myfile, find_minmax=False)
2779
+ if isinstance(fn_or_Zones, Zones):
2780
+ self.filename=fn_or_Zones.filename
2781
+ tmpzones = fn_or_Zones
2782
+
2783
+ elif isinstance(fn_or_Zones, (str, Path)):
2784
+ self.filename=fn_or_Zones
2785
+ tmpzones = Zones(fn_or_Zones, find_minmax=False)
2245
2786
 
2246
2787
  curzone:zone
2247
2788
  curvec:vector
2248
- curzone=tmpzones.myzones[0]
2789
+ curzone = tmpzones.myzones[0]
2249
2790
  index=0
2250
2791
  for curvec in curzone.myvectors:
2251
2792
 
2793
+ if curvec.myname in self.myprofiles.keys():
2794
+ if force_unique_name:
2795
+ logging.info(_('Profile %s already exists in cross-sections. We change its name to %s.')%(curvec.myname, f"{curvec.myname}_{index}"))
2796
+ curvec.myname = f"{curvec.myname}_{index}"
2797
+ else:
2798
+ logging.warning(_('Profile %s already exists in cross-sections. Previous will be overwritten.')%curvec.myname)
2799
+
2252
2800
  self.myprofiles[curvec.myname]={}
2253
2801
  curdict=self.myprofiles[curvec.myname]
2254
2802
 
@@ -2256,18 +2804,47 @@ class crosssections(Element_To_Draw):
2256
2804
  curdict['left']=None
2257
2805
  curdict['bed']=None
2258
2806
  curdict['right']=None
2807
+ curdict['left_down'] = None
2808
+ curdict['right_down'] = None
2259
2809
 
2260
2810
  index+=1
2261
- curdict['cs']=profile(name=curvec.myname,parent=self)
2811
+ curdict['cs']=profile(name=curvec.myname, parent=self)
2262
2812
  cursect:profile
2263
2813
  cursect=curdict['cs']
2264
2814
 
2265
- cursect.myvertices = curvec.myvertices
2815
+ cursect.myvertices = curvec.myvertices.copy()
2266
2816
 
2267
2817
  self.verif_bed()
2268
2818
  self.find_minmax(True)
2269
2819
  self.init_cloud()
2270
2820
 
2821
+
2822
+ @property
2823
+ def nb_profiles(self):
2824
+ """ Return the number of profiles in the cross-sections. """
2825
+ return len(self.myprofiles)
2826
+
2827
+ def __getitem__(self, key):
2828
+ """ Get a profile by its name, index or (x, y) coordinates.
2829
+
2830
+ :param key: The name or index of the profile or coordinates where the search is performed.
2831
+ :type key: str | int | tuple | list
2832
+ :return: The profile corresponding to the key.
2833
+ :rtype: profile
2834
+ """
2835
+
2836
+ if isinstance(key, str):
2837
+ return self.myprofiles[key]['cs']
2838
+ elif isinstance(key, int):
2839
+ return self.myprofiles[list(self.myprofiles.keys())[key]]['cs']
2840
+ elif isinstance(key, tuple | list):
2841
+ if len(key) == 2:
2842
+ x, y = key
2843
+ assert isinstance(x, float) and isinstance(y, float), _('Coordinates must be floats.')
2844
+ return self.select_profile(x, y)
2845
+ else:
2846
+ raise KeyError(_('Key must be a string or an integer or a tuple/list of (float, float).'))
2847
+
2271
2848
  def init_cloud(self):
2272
2849
  """ Initialiaze cloud points for cross-sections. """
2273
2850
 
@@ -2435,7 +3012,7 @@ class crosssections(Element_To_Draw):
2435
3012
  curlinkprop = myvec.myname
2436
3013
 
2437
3014
  myvecls = myvec.asshapely_ls()
2438
- prepls=prep(myvecls)
3015
+ prepls=prepare(myvecls)
2439
3016
 
2440
3017
  for cursname in self.myprofiles.values():
2441
3018
  curs:profile
@@ -2464,6 +3041,7 @@ class crosssections(Element_To_Draw):
2464
3041
  curs.refpoints[curlinkprop]=myvert
2465
3042
 
2466
3043
  cursname[curlinkprop]=myvert
3044
+ destroy_prepared(prepls)
2467
3045
 
2468
3046
  self.update_cloud()
2469
3047
 
@@ -2628,12 +3206,12 @@ class crosssections(Element_To_Draw):
2628
3206
  for curprof in self.myprofiles.keys():
2629
3207
  curdict=self.myprofiles[curprof]
2630
3208
  curvec=curdict['cs']
2631
- curvec:vector
3209
+ curvec:profile
2632
3210
  if curvec.used:
2633
3211
  self.myzone.add_vector(curvec, forceparent=True)
2634
3212
 
2635
3213
  if self.plotted:
2636
- self._prep_listogl() #FIXME : Does not work in the context of a 1D model
3214
+ self._prep_listogl()
2637
3215
 
2638
3216
  def showstructure(self, parent=None, forceupdate=False):
2639
3217
  """ Show the structure of the cross-sections in the zones. """
@@ -2641,7 +3219,7 @@ class crosssections(Element_To_Draw):
2641
3219
  self.set_zones()
2642
3220
  self.myzones.showstructure(parent, forceupdate)
2643
3221
 
2644
- def get_upstream(self) -> dict:
3222
+ def get_upstream(self) -> dict['cs':profile, 'index':int, 'left':wolfvertex | None, 'bed':wolfvertex | None, 'right':wolfvertex | None, 'left_down':wolfvertex | None, 'right_down':wolfvertex | None]:
2645
3223
  """ Get the upstream profile of the cross-sections."""
2646
3224
  curprof:profile
2647
3225
  curprof=self.myprofiles[list(self.myprofiles.keys())[0]]['cs']
@@ -2651,7 +3229,7 @@ class crosssections(Element_To_Draw):
2651
3229
 
2652
3230
  return self.myprofiles[curprof.myname]
2653
3231
 
2654
- def get_downstream(self) -> dict:
3232
+ def get_downstream(self) -> dict['cs':profile, 'index':int, 'left':wolfvertex | None, 'bed':wolfvertex | None, 'right':wolfvertex | None, 'left_down':wolfvertex | None, 'right_down':wolfvertex | None]:
2655
3233
  """ Get the downstream profile of the cross-sections. """
2656
3234
  curprof:profile
2657
3235
  curprof=self.myprofiles[list(self.myprofiles.keys())[0]]['cs']
@@ -2860,7 +3438,7 @@ class crosssections(Element_To_Draw):
2860
3438
  mysorted = curdict['sorted'] = []
2861
3439
  length = vecsupport.length
2862
3440
 
2863
- prepsup=prep(vecsupport) #Prepare le vecteur support aux opérations récurrentes
3441
+ prepsup=prepare(vecsupport) #Prepare le vecteur support aux opérations récurrentes
2864
3442
  curvect:profile
2865
3443
  for idx,curv in self.myprofiles.items():
2866
3444
  #bouclage sur les sections
@@ -2878,6 +3456,7 @@ class crosssections(Element_To_Draw):
2878
3456
  curvect.s = length - mydist
2879
3457
  else:
2880
3458
  curvect.s = mydist
3459
+ destroy_prepared(prepsup)
2881
3460
 
2882
3461
  #on trie le résultat en place
2883
3462
  mysorted.sort(key=lambda x:x.s)
@@ -2929,7 +3508,7 @@ class crosssections(Element_To_Draw):
2929
3508
  self.set_zones()
2930
3509
  self.myzones.saveas(filename=filename)
2931
3510
 
2932
- def select_profile(self, x:float, y:float):
3511
+ def select_profile(self, x:float, y:float) -> profile:
2933
3512
  """ Select the profile closest to the given coordinates (x, y).
2934
3513
 
2935
3514
  :param x: X coordinate of the point.
@@ -2965,271 +3544,426 @@ class Interpolator():
2965
3544
 
2966
3545
  """
2967
3546
 
2968
- def __init__(self, vec1:vector, vec2:vector,
2969
- supports:list[vector], ds=1.) -> None:
3547
+ def __init__(self, vec1:vector,
3548
+ vec2:vector,
3549
+ supports:list[vector],
3550
+ ds:float = 1.,
3551
+ epsilon:float = 5e-2) -> None:
3552
+ """
3553
+ Initialize the Interpolator with two vectors and a list of support vectors.
3554
+
3555
+ Not all supports need to intersect both sections.
3556
+ First, we check if each support intersects the sections.
3557
+ If a support does not intersect both sections, it will be ignored.
3558
+
3559
+ Support vectors must have Z coordinates.
3560
+
3561
+ If the Z-coordinates at the intersection points between the supports and the sections are not identical,
3562
+ "alpha" values are computed for interpolation:
3563
+
3564
+ alpha = Z_support / Z_section
3565
+
3566
+ The Z-coordinate of each interpolated point is then:
3567
+
3568
+ Z_interp = Z_section * alpha
3569
+
3570
+ Alpha is linearly interpolated between the two sections according to
3571
+ the curvilinear abscissa. The same applies to the Z-coordinates of the supports and the sections.
3572
+
3573
+ If the sections are not straight lines, a "trace" is defined by the intersection points
3574
+ between the supports and the sections. This trace is used to pre-compute the interpolation points.
3575
+ Then, deltas (in X and Y) are added to "deform" the local sections.
3576
+ These deltas are linearly interpolated between the two sections.
3577
+
3578
+ :param vec1: The first vector representing the first section.
3579
+ :type vec1: vector | profile
3580
+ :param vec2: The second vector representing the second section.
3581
+ :type vec2: vector | profile
3582
+ :param supports: A list of support vectors used for interpolation. This should be a list of vectors, not a zone.
3583
+ :type supports: list[vector]
3584
+ :param ds: The step size for interpolation, default is 1.0 [m].
3585
+ :type ds: float
3586
+ :param epsilon: The tolerance for intersection checks, default is 5e-2 [m] (5 cm).
3587
+ :type epsilon: float
3588
+ """
2970
3589
 
2971
3590
  self.interpolants=[]
2972
3591
 
2973
- sect1={}
2974
- sect2={}
3592
+ sect1 = {} # local dict to store section 1 data
3593
+ sect2 = {} # local dict to store section 2 data
3594
+
3595
+ used_supports:dict[int: dict['vec':vector, int:float]] = {}
2975
3596
 
2976
3597
  #Linestrings shapely des sections 1 et 2
2977
3598
  #ATTENTION, SHAPELY est une librairie géométrique 2D --> des outils spécifiques ont été programmés pour faire l'interpolation 3D
2978
- s1=sect1['ls']=vec1.asshapely_ls()
2979
- s2=sect2['ls']=vec2.asshapely_ls()
3599
+ s1 = sect1['ls'] = vec1.linestring
3600
+ s2 = sect2['ls'] = vec2.linestring
3601
+
3602
+ eps = epsilon # Tolérance pour vérifier l'intersection
3603
+
3604
+ # Check if sections intersect the supports at their endpoints, considering a small epsilon (eps) for precision issues
3605
+ def check_intersection(section_ls:LineString, vec:profile, myls:LineString, eps:float):
3606
+ loc = None
3607
+ # Try intersection at start
3608
+ pt = Point(section_ls.xy[0][0], section_ls.xy[1][0])
3609
+ pt_proj = myls.interpolate(myls.project(pt))
3610
+ length = pt_proj.distance(pt)
3611
+ intersected = length < eps
3612
+ if intersected:
3613
+ vec.myvertices[0].x = pt_proj.xy[0][0]
3614
+ vec.myvertices[0].y = pt_proj.xy[1][0]
3615
+ section_ls = vec.linestring
3616
+ loc = 'first'
3617
+
3618
+ # Try intersection at end if not found at start
3619
+ if not intersected:
3620
+ pt = Point(section_ls.xy[0][-1], section_ls.xy[1][-1])
3621
+ pt_proj = myls.interpolate(myls.project(pt))
3622
+ length = pt_proj.distance(pt)
3623
+ intersected = length < eps
3624
+ if intersected:
3625
+ vec.myvertices[-1].x = pt_proj.xy[0][-1]
3626
+ vec.myvertices[-1].y = pt_proj.xy[1][-1]
3627
+ section_ls = vec.linestring
3628
+ loc = 'last'
3629
+ return intersected, section_ls, loc
3630
+
3631
+ for cur_trace in supports:
3632
+ # RRecherche des distances des intersections des sections sur le support
3633
+
3634
+ # linestring du support courant
3635
+ cur_support_ls = cur_trace.linestring
2980
3636
 
2981
- nb = 0
2982
- supls={}
2983
-
2984
- eps=5.e-2
3637
+ #intersections du vecteur support avec les sections
3638
+ i1 = cur_support_ls.intersects(s1) # check is intersects section 1 --> return True or False -- see https://shapely.readthedocs.io/en/2.1.1/reference/shapely.intersects.html
2985
3639
 
2986
- for curvec in supports:
2987
- #linestring du support courant
2988
- #distances des intersections des sections sur le support
2989
- myls:LineString
2990
- myls=curvec.asshapely_ls()
3640
+ # If the section does not intersect the support, two possibilities:
3641
+ # 1. The first vertex of the section is on/near the support line
3642
+ # 2. The last vertex of the section is on/near the support line
3643
+ # If neither is the case, we ignore this support.
2991
3644
 
2992
- #intersections du vecteur support avec les sections
2993
- i1=myls.intersects(s1)
2994
3645
  if not i1:
2995
- pt=Point(s1.xy[0][0],s1.xy[1][0])
2996
- pt1=myls.interpolate(myls.project(pt))
2997
- length = pt1.distance(pt)
2998
- i1 = length<eps
2999
-
3000
- if i1:
3001
- vec1.myvertices[0].x=pt1.xy[0][0]
3002
- vec1.myvertices[0].y=pt1.xy[1][0]
3003
- s1=vec1.asshapely_ls()
3004
-
3005
- if not i1:
3006
- pt=Point(s1.xy[0][-1],s1.xy[1][-1])
3007
- pt1=myls.interpolate(myls.project(pt))
3008
- length = pt1.distance(pt)
3009
- i1 = length<eps
3010
- if i1:
3011
- vec1.myvertices[-1].x=pt1.xy[0][-1]
3012
- vec1.myvertices[-1].y=pt1.xy[1][-1]
3013
- s1=vec1.asshapely_ls()
3014
-
3015
- i2=myls.intersects(s2)
3646
+ i1, s1, _loc = check_intersection(s1, vec1, cur_support_ls, eps)
3647
+
3648
+ i2 = cur_support_ls.intersects(s2)
3016
3649
  if not i2:
3017
- pt=Point(s2.xy[0][0],s2.xy[1][0])
3018
- pt2=myls.interpolate(myls.project(pt))
3019
- length = pt2.distance(Point(s2.xy[0][0],s2.xy[1][0]))
3020
- i2 = length<eps
3021
-
3022
- if i2:
3023
- vec2.myvertices[0].x=pt2.xy[0][0]
3024
- vec2.myvertices[0].y=pt2.xy[1][0]
3025
- s2=vec2.asshapely_ls()
3026
-
3027
- if not i2:
3028
- pt=Point(s2.xy[0][-1],s2.xy[1][-1])
3029
- pt2=myls.interpolate(myls.project(pt))
3030
- length = pt2.distance(pt)
3031
- i2 = length<eps
3032
- if i2:
3033
- vec2.myvertices[-1].x=pt2.xy[0][-1]
3034
- vec2.myvertices[-1].y=pt2.xy[1][-1]
3035
- s2=vec2.asshapely_ls()
3650
+ i2, s2, _loc = check_intersection(s2, vec2, cur_support_ls, eps)
3036
3651
 
3652
+ # Si le support intersecte les sections, on le conserve
3653
+ # et on le stocke dans le dictionnaire used_supports
3037
3654
  if i1 and i2:
3038
- supls[nb]={}
3039
- supls[nb]['ls']=myls
3040
- supls[nb]['vec']=curvec
3041
- nb+=1
3655
+ loc_sup = used_supports[len(used_supports)] = {}
3656
+ loc_sup['vec'] = cur_trace
3657
+
3658
+
3659
+ # Need to reset the PREPARED linestrings
3660
+ # to ensure they are recalculated with the new coordinates
3661
+ # of the sections after the intersection checks.
3662
+ # This is necessary because the coordinates may have been adjusted
3663
+ # to ensure they are close to the support lines.
3664
+ # Otherwise, the linestrings may not reflect the actual geometry.
3665
+ vec1.reset_linestring()
3666
+ vec2.reset_linestring()
3667
+
3668
+ s1 = vec1.linestring
3669
+ s2 = vec2.linestring
3042
3670
 
3043
3671
  #bouclage sur les vecteurs supports pour trouver les intersections avec les sections
3044
3672
  # - trouver la portion utile entre intersections des supports
3045
3673
  # - trouver les distances sur les sections de ces intersections
3046
- for k in range(nb):
3047
- #linestring du support courant
3048
- #distances des intersections des sections sur le support
3049
- myls:LineString
3050
- myls=supls[k]['ls']
3674
+ for k in range(len(used_supports)):
3051
3675
 
3052
- #intersections du vecteur support avec les sections
3676
+ # distances des intersections des sections sur le support
3677
+ cur_trace:vector
3678
+ cur_trace = used_supports[k]['vec']
3679
+
3680
+ cur_support_ls:LineString
3681
+ cur_support_ls = cur_trace.linestring
3682
+
3683
+ # Coordonnées d'intersection du vecteur support avec les sections
3684
+ # On est certain que cela s'intersecte puisque l'on a vérifié juste avant
3685
+
3686
+ # Section "amont"
3687
+ i1 = cur_support_ls.intersection(s1) # --> return a Point or MultiPoint -- see https://shapely.readthedocs.io/en/stable/reference/shapely.intersection.html
3688
+ if not i1:
3689
+ i1, s1, loc1 = check_intersection(s1, vec1, cur_support_ls, eps)
3690
+ if loc1=='first':
3691
+ i1 = Point(s1.xy[0][0], s1.xy[1][0])
3692
+ elif loc1=='last':
3693
+ i1 = Point(s1.xy[0][-1], s1.xy[1][-1])
3053
3694
 
3054
- #section amont
3055
- i1=myls.intersection(s1)
3056
- if i1.geom_type=='MultiPoint':
3695
+ elif i1.geom_type=='MultiPoint':
3057
3696
  i1=i1.geoms[0]
3058
3697
  logging.debug('MultiPoint -- use first point or debug')
3059
3698
 
3060
- #section aval
3061
- i2=myls.intersection(s2)
3062
- if i2.geom_type=='MultiPoint':
3699
+
3700
+ # Section "aval"
3701
+ i2 = cur_support_ls.intersection(s2)
3702
+ if not i2:
3703
+ i2, s2, loc2 = check_intersection(s2, vec2, cur_support_ls, eps)
3704
+ if loc2=='first':
3705
+ i2 = Point(s2.xy[0][0], s2.xy[1][0])
3706
+ elif loc2=='last':
3707
+ i2 = Point(s2.xy[0][-1], s2.xy[1][-1])
3708
+
3709
+ elif i2.geom_type=='MultiPoint':
3063
3710
  i2=i2.geoms[0]
3064
3711
  logging.debug('MultiPoint -- use first point or debug')
3065
3712
 
3066
- #Les distances, sur les sections, sont calculées en projetant l'intersection du vecteur support et des sections
3067
- sect1[k]=s1.project(i1)
3068
- sect2[k]=s2.project(i2)
3069
-
3070
- #Les distances, sur le support, sont calculées en projetant l'intersection du vecteur support et des sections
3071
- supls[k][1]=myls.project(i1)
3072
- if supls[k][1]==-1.:
3073
- #problème de précision de calcul
3074
- if myls.distance(Point(s1.xy[0][0],s1.xy[1][0]))<eps:
3075
- supls[k][1]=myls.project(Point(s1.xy[0][0],s1.xy[1][0]))
3076
- sect1[k]=s1.project(Point(s1.xy[0][0],s1.xy[1][0]))
3077
- elif myls.distance(Point(s1.xy[0][-1],s1.xy[1][-1]))<eps:
3078
- supls[k][1]=myls.project(Point(s1.xy[0][-1],s1.xy[1][-1]))
3079
- sect1[k]=s1.project(Point(s1.xy[0][-1],s1.xy[1][-1]))
3080
-
3081
- supls[k][2]=myls.project(i2)
3082
- if supls[k][2]==-1.:
3083
- #problème de précision de calcul
3084
- if myls.distance(Point(s2.xy[0][0],s2.xy[1][0]))<eps:
3085
- supls[k][2]=myls.project(Point(s2.xy[0][0],s2.xy[1][0]))
3086
- sect2[k]=s2.project(Point(s2.xy[0][0],s2.xy[1][0]))
3087
- elif myls.distance(Point(s2.xy[0][-1],s2.xy[1][-1]))<eps:
3088
- supls[k][2]=myls.project(Point(s2.xy[0][-1],s2.xy[1][-1]))
3089
- sect2[k]=s2.project(Point(s2.xy[0][-1],s2.xy[1][-1]))
3090
-
3091
- #on ne conserve que la fraction utile entre intersections
3092
- supls[k]['vec']=supls[k]['vec'].substring(supls[k][1],supls[k][2],False,False)
3093
-
3094
- #bouclage sur les intervalles --> nb_supports-1
3095
- for k in range(nb-1):
3713
+ # Les distances curvilignes, sur les sections, sont calculées
3714
+ # en projetant l'intersection du vecteur support et des sections
3715
+ sect1[k] = s1.project(i1)
3716
+ sect2[k] = s2.project(i2)
3717
+
3718
+ # Les distances, sur le support, sont calculées en projetant
3719
+ # l'intersection du vecteur support et des sections
3720
+ used_supports[k][1] = cur_support_ls.project(i1)
3721
+ if used_supports[k][1] == -1.:
3722
+ # Problème de précision de calcul à gérer
3723
+ # Même type de problème que lors de la vérification de l'intersection
3724
+
3725
+ # We test the distance to the first and last points of the section
3726
+ # to ensure we have a valid projection.
3727
+ # We project on the support AND the section and store the result
3728
+ # in the used_supports and sect1/sect2 dictionaries.
3729
+
3730
+ pt_first = Point(vec1[0].x, vec1[0].y)
3731
+ pt_last = Point(vec1[-1].x, vec1[-1].y)
3732
+
3733
+ if cur_support_ls.distance(pt_first) < eps:
3734
+ used_supports[k][1] = cur_support_ls.project(pt_first)
3735
+ sect1[k] = s1.project(pt_first)
3736
+
3737
+ elif cur_support_ls.distance(pt_last) < eps:
3738
+ used_supports[k][1] = cur_support_ls.project(pt_last)
3739
+ sect1[k] = s1.project(pt_last)
3740
+
3741
+ used_supports[k][2] = cur_support_ls.project(i2)
3742
+ if used_supports[k][2] == -1.:
3743
+ # Problème de précision de calcul -- see previous comment
3744
+ pt_first = Point(vec2[0].x, vec2[0].y)
3745
+ pt_last = Point(vec2[-1].x, vec2[-1].y)
3746
+
3747
+ if cur_support_ls.distance(pt_first) < eps:
3748
+ used_supports[k][2] = cur_support_ls.project(pt_first)
3749
+ sect2[k] = s2.project(pt_first)
3750
+
3751
+ elif cur_support_ls.distance(pt_last) < eps:
3752
+ used_supports[k][2] = cur_support_ls.project(pt_last)
3753
+ sect2[k] = s2.project(pt_last)
3754
+
3755
+ # Replace the vector by the portion of the support vector between the two intersections
3756
+ used_supports[k]['vec'] = cur_trace.substring(used_supports[k][1], # curvilinear position of the intersection with section 1
3757
+ used_supports[k][2], # curvilinear position of the intersection with section 2
3758
+ is3D = False, # Compute in 2D, not 3D
3759
+ adim = False # In real coordinates, not adimensional
3760
+ )
3761
+
3762
+
3763
+ # Bouclage sur les intervalles --> nb_supports-1
3764
+ for k in range(len(used_supports)-1):
3765
+
3096
3766
  interpolant=[]
3097
3767
  self.interpolants.append(interpolant)
3098
3768
 
3099
- cursupl:vector
3100
- cursupr:vector
3769
+ cur_support_left:vector
3770
+ cur_support_right:vector
3101
3771
  curvec1:vector
3102
3772
  curvec2:vector
3103
3773
 
3104
- #morceaux de sections entre intersections avec le support
3774
+ # Morceaux de sections entre intersections avec le support.
3775
+ # Comme les distances ont été calculées avec Shapely,
3776
+ # on doit trouver les morceaux en 2D et non 3D.
3105
3777
  curvec1=sect1['sub'+str(k)]=vec1.substring(sect1[k],sect1[k+1],is3D=False,adim=False)
3106
3778
  curvec2=sect2['sub'+str(k)]=vec2.substring(sect2[k],sect2[k+1],is3D=False,adim=False)
3107
3779
 
3108
- #pointeurrs vers les morceaux des supports
3109
- cursupl = supls[k]['vec']
3110
- cursupr = supls[k+1]['vec']
3780
+ # Pointeurs vers les morceaux des supports
3781
+ cur_support_left = used_supports[k]['vec']
3782
+ cur_support_right = used_supports[k+1]['vec']
3111
3783
 
3112
- #MAJ des longueurs 2D et 3D
3113
- cursupl.update_lengths()
3114
- cursupr.update_lengths()
3784
+ # MAJ des longueurs 2D ET 3D
3785
+ cur_support_left.update_lengths()
3786
+ cur_support_right.update_lengths()
3115
3787
  curvec1.update_lengths()
3116
3788
  curvec2.update_lengths()
3117
3789
 
3118
- #Trouve la liste des distances à traiter pour le maillage
3119
- nbi = np.ceil(max(curvec1.length3D,curvec2.length3D)/ds)
3120
- locds = 1./float(nbi)
3121
- dist3d = np.concatenate([np.arange(0.,1.,locds),np.cumsum(curvec1._lengthparts3D)/curvec1.length3D,np.cumsum(curvec2._lengthparts3D)/curvec2.length3D])
3122
- dist3d = np.unique(dist3d)
3123
-
3124
- #nombre de points à traiter sur les supports
3125
- # on divise la longueur 3D des supports par la taille souhaitée et on arrondi à l'entier supérieur
3126
- nbi = int(np.ceil(max(cursupl.length3D,cursupr.length3D)/ds))
3127
- # nouvelle distance de calcul
3128
- locds = 1./float(nbi)
3129
-
3130
- sloc=0.
3131
- pt1l = curvec1.interpolate(0.,True,True)
3132
- pt2l = curvec2.interpolate(0.,True,True)
3133
- pt1r = curvec1.interpolate(1.,True,True)
3134
- pt2r = curvec2.interpolate(1.,True,True)
3135
-
3136
- s1dr = vector(name='sectiondroite1')
3137
- s2dr = vector(name='sectiondroite2')
3138
- s1dr.add_vertex([pt1l,pt1r])
3139
- s2dr.add_vertex([pt2l,pt2r])
3140
-
3141
- s1dr = s1dr.asshapely_ls()
3142
- s2dr = s2dr.asshapely_ls()
3143
-
3144
- # if np.isnan(pt1l.x):
3145
- # a=1
3146
- # if np.isnan(pt2l.x):
3147
- # a=1
3148
- # if np.isnan(pt1r.x):
3149
- # a=1
3150
- # if np.isnan(pt2r.x):
3151
- # a=1
3152
-
3153
- for curalong in range(nbi+1):
3154
- logging.debug(str(curalong))
3155
-
3156
- #interpolation 3D sur les 2 supports
3157
- l1 = cursupl.interpolate(sloc,True,True)
3158
- l2 = cursupr.interpolate(sloc,True,True)
3159
-
3160
- curvec=vector(name='loc')
3161
- curvec.add_vertex([l1,l2])
3162
- val = []
3163
-
3164
- if l1.z!=0.:
3165
- if pt1l.z==0.:
3166
- alpha1l=0.
3167
- else:
3168
- alpha1l = l1.z/pt1l.z
3169
- if pt2l.z==0.:
3170
- alpha2l=0.
3171
- else:
3172
- alpha2l = l1.z/pt2l.z
3790
+ # Trouve la liste des distances à traiter pour le maillage
3791
+
3792
+ # Nombre de points (entier) à évaluer pour s'approcher de la distance ds souhaitée
3793
+ # On divise la longueur 3D maximales des sections par la taille souhaitée
3794
+ # et on arrondit à l'entier supérieur
3795
+ nbi_sect = np.ceil(max(curvec1.length3D, curvec2.length3D) / ds)
3796
+ if nbi_sect == 0:
3797
+ logging.warning(_('No points to interpolate between sections {s1} and {s2} with distance {d} m.').format(s1=curvec1.myname, s2=curvec2.myname, d=ds))
3798
+ else:
3799
+ locds = 1./float(nbi_sect) # nouvelle distance de calcul
3800
+
3801
+ # Create a list of adimensional distances where to interpolate
3802
+ # We use :
3803
+ # - the cumulative length of the sections to create a list of distances
3804
+ # - a range from 0 to 1 with the step size locds
3805
+ dist3d = np.concatenate([np.arange(0.,1.,locds),
3806
+ np.cumsum(curvec1._lengthparts3D) / curvec1.length3D,
3807
+ np.cumsum(curvec2._lengthparts3D) / curvec2.length3D])
3808
+
3809
+ # Remove duplicates AND sort the distances
3810
+ dist3d = np.unique(dist3d)
3811
+
3812
+ # Nombre de points à traiter sur les supports.
3813
+ # On divise la longueur 3D maximale des supports par la taille souhaitée
3814
+ # et on arrondi à l'entier supérieur
3815
+ nbi_support = int(np.ceil(max(cur_support_left.length3D, cur_support_right.length3D) / ds))
3816
+
3817
+ # Nouvelle distance de calcul
3818
+ locds = 1./float(nbi_support)
3819
+
3820
+ # Wolfvertex des points d'intersection des sections avec les supports
3821
+
3822
+ # section amont/support gauche
3823
+ pt1_left = curvec1.interpolate(s= 0., is3D = True, adim = True)
3824
+ # section amont/support droit
3825
+ pt1_right = curvec1.interpolate(s= 1., is3D = True, adim = True)
3826
+
3827
+ # section aval/support gauche
3828
+ pt2_left = curvec2.interpolate(s= 0., is3D = True, adim = True)
3829
+ # section aval/support droit
3830
+ pt2_right = curvec2.interpolate(s= 1., is3D = True, adim = True)
3831
+
3832
+ # Vecteurs temporaires pour les sections.
3833
+ # Ces nouveaux vecteurs sont des segments de droite entre les intersections
3834
+ # des sections avec les supports.
3835
+ # Cela définit ainsi la trace des sections, même si en réalité tous leurs
3836
+ # points ne sont pas alignés.
3837
+
3838
+ # Rappel : Cette procédure a au départ été écrite pour de vraies sections en travers.
3839
+ # Elle a été adaptée pour fonctionner avec des sections 3D de forme quelconque.
3840
+
3841
+ # Passage via vector pour conversion vers LineString
3842
+ # NDLR : on aurait sans doute pu faire plus simple ;-)
3843
+ s1_trace = vector(name= 'temp1')
3844
+ s2_trace = vector(name= 'temp2')
3845
+
3846
+ s1_trace.add_vertex([pt1_left, pt1_right])
3847
+ s2_trace.add_vertex([pt2_left, pt2_right])
3848
+
3849
+ # Convert to LineString for Shapely operations
3850
+ s1_trace = s1_trace.linestring
3851
+ s2_trace = s2_trace.linestring
3852
+
3853
+ sloc = 0. # position adimensionnelle le long du support
3854
+ # On a calculé la distance utile de discrétisation "locds".
3855
+ # On va donc évoluer le long des supports en incrémentant "sloc" progressivement.
3856
+
3857
+ # Compute alpha coefficients for left and right endpoints of the support vectors
3858
+ def safe_div(a, b):
3859
+ return a / b if b != 0. else 0.
3860
+
3861
+ for s_along_sup in range(nbi_support + 1):
3862
+
3863
+ logging.debug(str(s_along_sup))
3864
+
3865
+ # Interpolation 3D sur les 2 supports
3866
+ #
3867
+ # Si le vecteur support est 3D, on souhaite utiliser les positions
3868
+ # relatives des points des sections par rapport aux points du support
3869
+ # pour calculer les positions des points interpolés.
3870
+
3871
+ l1 = cur_support_left.interpolate(s= sloc, is3D= True, adim= True)
3872
+ l2 = cur_support_right.interpolate(s= sloc, is3D= True, adim= True)
3873
+
3874
+ # Trace de la future section interpolée.
3875
+ # Tout comme "s1_trace" et "s2_trace"", c'est un segment de droite
3876
+ # qui servira de position de départ à laquelle des incréments seront ajoutés.
3877
+ # Cela définira les points de la section interpolée.
3878
+ #
3879
+ # On garde ici l'objet de type "vector"
3880
+ cur_trace=vector(name='loc')
3881
+ cur_trace.add_vertex([l1, l2])
3882
+ all_points_interp = []
3883
+
3884
+ if l1.z != 0. and l1.z != -99999.:
3885
+ # Le support est 3D
3886
+ alpha1_left = safe_div(l1.z, pt1_left.z)
3887
+ alpha2_left = safe_div(l1.z, pt2_left.z)
3173
3888
  else:
3174
- alpha1l = 0.
3175
- alpha2l = 0.
3176
- if l2.z!=0.:
3177
- if pt1r.z==0.:
3178
- alpha1r = 0.
3179
- else:
3180
- alpha1r = l2.z/pt1r.z
3889
+ alpha1_left = 0.
3890
+ alpha2_left = 0.
3181
3891
 
3182
- if pt2r.z==0.:
3183
- alpha2r = 0.
3184
- else:
3185
- alpha2r = l2.z/pt2r.z
3892
+ if l2.z != 0. and l2.z != -99999.:
3893
+ # Le support est 3D
3894
+ alpha1_right = safe_div(l2.z, pt1_right.z)
3895
+ alpha2_right = safe_div(l2.z, pt2_right.z)
3186
3896
  else:
3187
- alpha1r=0.
3188
- alpha2r=0.
3897
+ alpha1_right = 0.
3898
+ alpha2_right = 0.
3189
3899
 
3900
+ # Bouclage sur tous les points d'interpolation
3901
+ # sur les sections
3190
3902
  for curdist in dist3d:
3191
3903
 
3192
- #interpolation 3D dans les 2 sections
3193
- cur1 = curvec1.interpolate(curdist,True,True)
3194
- cur2 = curvec2.interpolate(curdist,True,True)
3195
-
3196
- alpha1 = alpha1l*(1.-curdist)+alpha1r*curdist
3197
- alpha2 = alpha2l*(1.-curdist)+alpha2r*curdist
3198
-
3199
- sr1 = s1dr.project(Point(cur1.x,cur1.y))
3200
- sr2 = s2dr.project(Point(cur2.x,cur2.y))
3201
- pr1 = s1dr.interpolate(sr1)
3202
- pr2 = s2dr.interpolate(sr2)
3203
- sr1/=s1dr.length
3204
- sr2/=s2dr.length
3205
-
3206
- dx1 = cur1.x-pr1.x
3207
- dy1 = cur1.y-pr1.y
3208
- dx2 = cur2.x-pr2.x
3209
- dy2 = cur2.y-pr2.y
3210
- dx = dx1*(1.-sloc)+dx2*sloc
3211
- dy = dy1*(1.-sloc)+dy2*sloc
3212
- s = sr1*(1.-sloc)+sr2*sloc
3213
-
3214
- # dist2d1 = cur1.dist2D(curvec1.myvertices[0])/curvec1.myvertices[0].dist2D(curvec1.myvertices[-1]) #curvec1.length2D
3215
- # dist2d2 = cur2.dist2D(curvec2.myvertices[0])/curvec2.myvertices[0].dist2D(curvec2.myvertices[-1]) #curvec2.length2D
3216
- # dist2d = dist2d1*(1.-sloc) + dist2d2*sloc
3217
- # pt = curvec.interpolate(dist2d,False,True)
3218
- pt = curvec.interpolate(s,False,True)
3219
-
3220
- # xloc = cur1.x + sloc * (cur2.x-cur1.x)
3221
- # yloc = cur1.y + sloc * (cur2.y-cur1.y)
3904
+ # Interpolation 3D dans les 2 sections
3905
+ cur1 = curvec1.interpolate(s= curdist, is3D = True, adim = True)
3906
+ cur2 = curvec2.interpolate(s= curdist, is3D = True, adim = True)
3907
+
3908
+ # Pondération linéaire des alphas
3909
+ alpha1 = alpha1_left * (1.-curdist) + alpha1_right*curdist
3910
+ alpha2 = alpha2_left * (1.-curdist) + alpha2_right*curdist
3911
+
3912
+ # Projection 2D des points sur les traces des sections
3913
+ # En fonction de la forme réelle de la section, ces projections
3914
+ # pourraient ne pas être situées selon une abscisse curviligne
3915
+ # strictement croissante le long de la section.
3916
+ s_proj1 = s1_trace.project(Point(cur1.x, cur1.y))
3917
+ s_proj2 = s2_trace.project(Point(cur2.x, cur2.y))
3918
+
3919
+ # Interpolation des points sur les traces des sections
3920
+ proj_1 = s1_trace.interpolate(s_proj1)
3921
+ proj_2 = s2_trace.interpolate(s_proj2)
3922
+
3923
+ s_proj1 /= s1_trace.length # adimensional position along the section 1
3924
+ s_proj2 /= s2_trace.length # adimensional position along the section 2
3925
+
3926
+ # Relative position of the points on the support.
3927
+ # We calculate the difference between the projected points and the original points.
3928
+ dx1 = cur1.x - proj_1.x
3929
+ dy1 = cur1.y - proj_1.y
3930
+ dx2 = cur2.x - proj_2.x
3931
+ dy2 = cur2.y - proj_2.y
3932
+
3933
+ # Linear ponderation of the differences
3934
+ dx = dx1*(1.-sloc) + dx2*sloc
3935
+ dy = dy1*(1.-sloc) + dy2*sloc
3936
+ s = s_proj1*(1.-sloc) + s_proj2*sloc
3937
+
3938
+ # !! Interpolation en 2D !! pour être cohérent avec le "s" des traces.
3939
+ pt = cur_trace.interpolate(s, is3D = False, adim = True)
3940
+
3941
+ # Pondération des altitudes
3222
3942
  zloc = cur1.z*alpha1 + sloc * (cur2.z*alpha2-cur1.z*alpha1)
3223
3943
 
3224
- val.append(wolfvertex(pt.x+dx,pt.y+dy,zloc))
3225
- # val.append(wolfvertex(xloc,yloc,zloc))
3944
+ # Ajout du point
3945
+ all_points_interp.append(wolfvertex(pt.x+dx, pt.y+dy, zloc))
3946
+
3947
+ # ajout de la liste des points interpolés dans la section
3948
+ interpolant.append(all_points_interp)
3949
+
3950
+ # mise à jour de la position le long de la section
3951
+ sloc += locds
3226
3952
 
3227
- interpolant.append(val)
3953
+ # Limite la position à 1.0 pour éviter les débordements.
3954
+ # La sommation des distances ne garantit pas que la position
3955
+ # le long de la section soit strictement inférieure à 1.0.
3956
+ sloc = min(sloc,1.)
3228
3957
 
3229
- sloc+=locds
3230
- sloc=min(sloc,1.)
3231
3958
 
3232
- def get_xyz_for_viewer(self,nbsub=10):
3959
+ def get_xyz_for_viewer(self, nbsub=10):
3960
+ """ Get the XYZ coordinates for the viewer.
3961
+
3962
+ :param nbsub: Number of subdivisions for each segment, default is 10.
3963
+ :type nbsub: int
3964
+ :return: A numpy array of shape [(nb-1)*nbsub, 4] containing the XYZ coordinates.
3965
+ :rtype: np.ndarray
3966
+ """
3233
3967
 
3234
3968
  pts=self.interpolants
3235
3969
 
@@ -3273,7 +4007,12 @@ class Interpolator():
3273
4007
 
3274
4008
  return xyz
3275
4009
 
3276
- def add_triangles_to_zone(self,myzone:zone):
4010
+ def add_triangles_to_zone(self, myzone:zone):
4011
+ """ Add triangles to the specified zone based on the interpolants.
4012
+
4013
+ :param myzone: The zone to which the triangles will be added.
4014
+ :type myzone: zone
4015
+ """
3277
4016
 
3278
4017
  nb=0
3279
4018
  npt=1
@@ -3297,8 +4036,17 @@ class Interpolator():
3297
4036
  npt+=1
3298
4037
  nb+=(len(curpt)-1)*(len(curl)-1)*3+(len(curl)-1)+(len(curpt))
3299
4038
 
3300
- def get_triangles(self,forgltf=True):
4039
+ def get_triangles(self, forgltf=True):
4040
+ """ Get the triangles and points for the interpolants.
3301
4041
 
4042
+ ATTENTION : gltf format coordinates as [x, z, -y] and "forgltf" is True by default.
4043
+ It is not the case in "get_points" where forgltf is False by default.
4044
+
4045
+ :param forgltf: If True, formats the points for glTF export, default is True.
4046
+ :type forgltf: bool
4047
+ :return: A tuple containing the number of points, points array, and triangles array.
4048
+ :rtype: tuple[int, np.ndarray, np.ndarray]
4049
+ """
3302
4050
  points=[]
3303
4051
  triangles=[]
3304
4052
  nbpts=0
@@ -3334,11 +4082,22 @@ class Interpolator():
3334
4082
 
3335
4083
 
3336
4084
  if len(np.argwhere(np.isnan(points)==True))>0:
3337
- a=1
4085
+ logging.error(_('NaN values found in points array.'))
4086
+ raise ValueError(_('NaN values found in points array.'))
3338
4087
 
3339
4088
  return nbpts,points,triangles
3340
4089
 
3341
- def get_points(self,forgltf=False):
4090
+ def get_points(self, forgltf=False):
4091
+ """ Get the points for the interpolants.
4092
+
4093
+ ATTENTION : gltf format coordinates as [x, z, -y] and "forgltf" is False by default.
4094
+ It is not the case in "get_triangles" where forgltf is True by default.
4095
+
4096
+ :param forgltf: If True, formats the points for glTF export, default is False.
4097
+ :type forgltf: bool
4098
+ :return: A tuple containing the number of points and the points array.
4099
+ :rtype: tuple[int, np.ndarray]
4100
+ """
3342
4101
 
3343
4102
  points=[]
3344
4103
  nbpts=0
@@ -3362,7 +4121,19 @@ class Interpolator():
3362
4121
  return nbpts,points
3363
4122
 
3364
4123
  def export_gltf(self, points=None, triangles=None, fn:str | Path = None):
3365
-
4124
+ """ Export the interpolated sections as a glTF file.
4125
+
4126
+ GLTF can be opened in many 3D viewers, including Blender, Cesium, and others.
4127
+
4128
+ :param points: Optional points array, if None, it will be generated from the interpolants.
4129
+ :type points: np.ndarray | None
4130
+ :param triangles: Optional triangles array, if None, it will be generated from the interpolants.
4131
+ :type triangles: np.ndarray | None
4132
+ :param fn: The filename to save the glTF file, if None, a file dialog will be shown.
4133
+ :type fn: str | Path | None
4134
+ :return: None
4135
+ :rtype: None
4136
+ """
3366
4137
  if points is None and triangles is None:
3367
4138
  points,triangles= self.get_triangles()
3368
4139
 
@@ -3441,56 +4212,58 @@ class Interpolator():
3441
4212
  gltf.save(fn)
3442
4213
 
3443
4214
  def get_xy_z_for_griddata(self):
3444
-
4215
+ """
4216
+ Get the XY and Z coordinates for griddata interpolation (Scipy).
4217
+ """
3445
4218
  xy = np.asarray([[curvertex.x,curvertex.y] for curpt in self.interpolants for curl in curpt for curvertex in curl])
3446
4219
  z = np.asarray([curvertex.z for curpt in self.interpolants for curl in curpt for curvertex in curl])
3447
4220
 
3448
- # if len(np.argwhere(np.isnan(z)))>0:
3449
- # test=1
3450
- # if len(np.argwhere(np.isneginf(z)))>0:
3451
- # test=1
3452
- # if len(np.argwhere(np.isposinf(z)))>0:
3453
- # test=1
3454
4221
  return xy,z
3455
4222
 
3456
4223
  class Interpolators():
3457
4224
  """
3458
- Classe de gestion des interpolations sur sections en travers
4225
+ Classe de gestion des interpolations sur sections en travers.
4226
+
4227
+ La préparation de la triangulation est faite sur base de vecteurs supports (instance de type "Zones")
4228
+ et de sections en travers stockées dans une instance de type "crosssections".
3459
4229
  """
3460
4230
 
3461
- def __init__(self, banks:Zones, cs:crosssections, ds=1.) -> None:
4231
+ def __init__(self, banks:Zones, cs:crosssections, ds:float = 1.) -> None:
3462
4232
  """
3463
- Constructeur de la classe Interpolators
4233
+ Constructeur de la classe Interpolators.
3464
4234
 
3465
- :param banks: Zones contenant les vecteurs supports
4235
+ :param banks: Zones contenant les vecteurs supports (pay attention to the used property for zone and vectors)
3466
4236
  :param cs: objet 'crosssections' contenant les sections en travers --> voir PyCrosssections
4237
+ :param ds: distance souhaitée de discrétisation [m]
3467
4238
  """
3468
4239
 
3469
- self.points = None
3470
- self.triangles = None
4240
+ self.points = None # Points for triangulation
4241
+ self.triangles = None # Triangles for triangulation
3471
4242
 
3472
- self.mybanks = [curv for curzone in banks.myzones for curv in curzone.myvectors]
4243
+ # Convert the supports/banks to a list of vectors
4244
+ # All vectors in the zones will be used (independent of the zone name)
4245
+ # If a zone/vector is not used, it will not be included in the list
4246
+ self.mybanks = [curv for curzone in banks.myzones if curzone.used for curv in curzone.myvectors if curv.used]
3473
4247
 
3474
- cs.set_zones()
3475
- self.mycs = cs.myzones
4248
+ cs.set_zones() # Force the cross-sections to be set up as a Zones object
3476
4249
 
3477
- self.myinterp:list[Interpolator]=[]
4250
+ self.myinterp:list[Interpolator]=[] # List of Interpolator objects
3478
4251
 
3479
- zonecs:zone
3480
- zonecs = self.mycs.myzones[0]
4252
+ # self.mycs = cs.myzones # pointer to the zones of the cross-sections
4253
+ zonecs = cs.myzones.myzones[0]
3481
4254
 
3482
4255
  if zonecs.myvectors[0].up is not None:
3483
4256
  # Les sections ont été triées sur base d'un vecteur support
3484
4257
  # On les traite dans l'ordre du tri
3485
4258
  cs1:profile
3486
- cs1=cs.get_upstream()['cs']
4259
+ cs1 = cs.get_upstream()['cs']
3487
4260
 
3488
4261
  while cs1.down is not cs1:
3489
4262
  cs2=cs1.down
3490
4263
 
3491
4264
  logging.info('{} - {}'.format(cs1.myname,cs2.myname))
3492
4265
 
3493
- myinterp=Interpolator(cs1,cs2,self.mybanks,ds)
4266
+ myinterp = Interpolator(cs1, cs2, self.mybanks, ds)
3494
4267
 
3495
4268
  if len(myinterp.interpolants)>0:
3496
4269
  self.myinterp.append(myinterp)
@@ -3499,13 +4272,18 @@ class Interpolators():
3499
4272
 
3500
4273
  cs1=cs2
3501
4274
  else:
3502
- # Les sections n'ont pas été triées --> on les traite dans l'ordre d'énumération
4275
+ # Les sections n'ont pas été triées
4276
+ # --> on les traite dans l'ordre d'énumération
3503
4277
  for i in range(zonecs.nbvectors-1):
3504
- cs1=zonecs.myvectors[i]
3505
- cs2=zonecs.myvectors[i+1]
4278
+ cs1:vector
4279
+ cs2:vector
4280
+ cs1 = zonecs.myvectors[i] # Cross-section 1
4281
+ cs2 = zonecs.myvectors[i+1] # Cross-section 2
3506
4282
 
3507
4283
  logging.info('{} - {}'.format(cs1.myname,cs2.myname))
3508
- myinterp=Interpolator(cs1,cs2,self.mybanks,ds)
4284
+
4285
+ myinterp = Interpolator(cs1, cs2, self.mybanks, ds)
4286
+
3509
4287
  if len(myinterp.interpolants)>0:
3510
4288
  self.myinterp.append(myinterp)
3511
4289
  else:
@@ -3552,6 +4330,7 @@ class Interpolators():
3552
4330
 
3553
4331
 
3554
4332
  def viewer_interpolator(self):
4333
+ """ Display the interpolated sections in a viewer. """
3555
4334
 
3556
4335
  xyz=[]
3557
4336
  for curinterp in self.myinterp:
@@ -3561,9 +4340,34 @@ class Interpolators():
3561
4340
 
3562
4341
  myviewer(xyz,0)
3563
4342
 
4343
+ @property
4344
+ def points_xyz(self):
4345
+ """ Get the XYZ coordinates of the interpolated points.
4346
+
4347
+ Points are stored as [x,z,-y] for glTF compatibility.
4348
+ Convert them to [x,y,z] for other uses.
4349
+ """
4350
+ pts = self.points.reshape([-1,3])[:,[0,2,1]].copy() # Convert from [x,z,-y] to [x,y,z]
4351
+ pts[:,1] = -pts[:,1] # Convert from -y to y
4352
+ return pts
4353
+
3564
4354
  def interp_on_array(self, myarray,
3565
4355
  method:Literal["nearest", "linear", "cubic"],
3566
- use_cloud:bool = True):
4356
+ use_cloud:bool=True,
4357
+ mask_outside_polygons:bool=False):
4358
+ """ Interpolate the sections on a WolfArray.
4359
+
4360
+ :param myarray: The WolfArray to interpolate on.
4361
+ :type myarray: WolfArray
4362
+ :param method: The interpolation method to use, default is "linear".
4363
+ :type method: str
4364
+ :param use_cloud: If True, uses cloud interpolation, otherwise triangulation, default is True.
4365
+ :type use_cloud: bool
4366
+ """
4367
+ from .wolf_array import WolfArray
4368
+ from .PyVertexvectors import Triangulation
4369
+
4370
+ myarray:WolfArray
3567
4371
 
3568
4372
  if use_cloud:
3569
4373
  xy=[]
@@ -3576,7 +4380,40 @@ class Interpolators():
3576
4380
  xy = np.concatenate(xy)
3577
4381
  z = np.concatenate(z)
3578
4382
  myarray.interpolate_on_cloud(xy,z,method)
4383
+
4384
+ if mask_outside_polygons:
4385
+
4386
+ # Mask points outside the polygons defined by the triangulation
4387
+ # Do not solve the problem of points alongside the polygons or on the vertices
4388
+ # observed in some cases if we use triangulation.
4389
+ #
4390
+ # Should be improved by searching the contour of the polygons
4391
+ # like the concatenation of external supports (left + last_section + right_inverted + first_section_inverted)
4392
+
4393
+ tmp_tri = Triangulation(pts = self.points_xyz, tri=self.triangles)
4394
+ ij = myarray.get_ij_inside_listofpolygons(tmp_tri._get_polygons())
4395
+ myarray.array.mask[:,:] = True # Mask all points
4396
+ myarray.array.mask[ij[:,0], ij[:,1]] = False # Unmask
4397
+ myarray.set_nullvalue_in_mask()
3579
4398
  else:
3580
4399
  for interp in self.myinterp:
3581
4400
  n, pts, tri = interp.get_triangles(forgltf=False)
3582
- myarray.interpolate_on_triangulation(pts, tri, interp_method= 'scipy')
4401
+ myarray.interpolate_on_triangulation(pts, tri, interp_method= 'scipy')
4402
+
4403
+ def saveas(self, fn: str | Path):
4404
+ """ Save the interpolators to files.
4405
+
4406
+ Each interpolator will be saved as a separate file with a .tri extension.
4407
+
4408
+ :param fn: The filename to save the interpolators.
4409
+ :type fn: str | Path
4410
+ """
4411
+ from .PyVertexvectors import Triangulation
4412
+
4413
+ if isinstance(fn, str):
4414
+ fn = Path(fn)
4415
+
4416
+ for i, interp in enumerate(self.myinterp):
4417
+ n, pts, tri = interp.get_triangles(forgltf=False)
4418
+ tmp_tri = Triangulation(pts = pts, tri = tri)
4419
+ tmp_tri.saveas(fn.parent / (fn.stem + f'_interp{i+1}.tri'))