wolfhece 2.2.34__py3-none-any.whl → 2.2.35__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.
wolfhece/wolf_array.py CHANGED
@@ -756,7 +756,7 @@ class header_wolf():
756
756
  def ij2xy_np(self, ij:np.ndarray, scale:float=1., aswolf:bool=False, abs:bool=True) -> np.ndarray:
757
757
  """ alias for get_xy_from_ij_array
758
758
 
759
- :param ij: numpy array containing (i, j, [k]) indices
759
+ :param ij: numpy array containing (i, j, [k]) indices - like np.argwhere() - shape (n, 2) or (n, 3)
760
760
  :param scale: scaling of the spatial resolution (dx,dy,[dz])
761
761
  :param aswolf: if True, input is one-based (as Wolf VB6 or Fortran), otherwise 0-based (default Python standard)
762
762
  :param abs: if True, add translation to results (x, y, [z]) (coordinate to global space)
@@ -770,6 +770,13 @@ class header_wolf():
770
770
  """
771
771
  return self.get_xy_from_ij_array(ij, scale, aswolf, abs)
772
772
 
773
+ def ij2xy_np_ij_from_npwhere(self, ij:np.ndarray, scale:float=1., aswolf:bool=False, abs:bool=True) -> np.ndarray:
774
+ """ alias for get_xy_from_ij_array but with ij as np.where result
775
+ """
776
+
777
+ ij = np.vstack((ij[0], ij[1])).T
778
+ return self.get_xy_from_ij_array(ij, scale, aswolf, abs)
779
+
773
780
  def xy2ij(self, x:float, y:float, z:float=0., scale:float=1., aswolf:bool=False, abs:bool=True, forcedims2:bool=False) -> Union[tuple[np.int32,np.int32], tuple[np.int32,np.int32,np.int32]]:
774
781
  """ alias for get_ij_from_xy """
775
782
  return self.get_ij_from_xy(x, y, z, scale, aswolf, abs, forcedims2)
@@ -1874,6 +1881,11 @@ class Ops_Array(wx.Frame):
1874
1881
  if self.wx_exists:
1875
1882
  self.set_GUI()
1876
1883
 
1884
+ @property
1885
+ def usemask(self):
1886
+ """ Return the usemask Value """
1887
+ return self.selectrestricttomask.GetValue()
1888
+
1877
1889
  @property
1878
1890
  def idx(self):
1879
1891
  """ Return the idx of the parentarray """
@@ -2192,9 +2204,15 @@ class Ops_Array(wx.Frame):
2192
2204
  bSizer16_2 = wx.BoxSizer(wx.VERTICAL)
2193
2205
  bSizer16_3 = wx.BoxSizer(wx.VERTICAL)
2194
2206
 
2195
- selectmethodChoices = [_("by clicks"), _("inside active vector"), _("inside active zone"),
2196
- _("inside temporary vector"), _("along active vector"), _("along active zone"),
2197
- _("along temporary vector")]
2207
+ selectmethodChoices = [_("by clicks"),
2208
+ _("inside active vector"),
2209
+ _("inside active zone"),
2210
+ _("inside temporary vector"),
2211
+ _("along active vector"),
2212
+ _("along active zone"),
2213
+ _("along temporary vector"),
2214
+ # _("outside active vector")
2215
+ ]
2198
2216
  self.selectmethod = wx.RadioBox(self.selection, wx.ID_ANY, _("How to select nodes?"), wx.DefaultPosition,
2199
2217
  wx.DefaultSize, selectmethodChoices, 1, wx.RA_SPECIFY_COLS)
2200
2218
  self.selectmethod.SetSelection(0)
@@ -2445,21 +2463,65 @@ class Ops_Array(wx.Frame):
2445
2463
  sizermask.Add(maskdata, 1, wx.EXPAND)
2446
2464
  maskdata.Bind(wx.EVT_BUTTON, self.Onmask)
2447
2465
 
2466
+ sizer_unmask = wx.BoxSizer(wx.HORIZONTAL)
2448
2467
  unmaskall = wx.Button(self.mask, wx.ID_ANY, _("Unmask all"), wx.DefaultPosition, wx.DefaultSize, 0)
2449
- sizermask.Add(unmaskall, 1, wx.EXPAND)
2468
+ sizer_unmask.Add(unmaskall, 1, wx.EXPAND)
2450
2469
  unmaskall.Bind(wx.EVT_BUTTON, self.Unmaskall)
2451
2470
  unmaskall.SetToolTip(_("Unmask all values in the current array"))
2452
2471
 
2453
2472
  unmasksel = wx.Button(self.mask, wx.ID_ANY, _("Unmask selection"), wx.DefaultPosition, wx.DefaultSize, 0)
2454
- sizermask.Add(unmasksel, 1, wx.EXPAND)
2455
- unmasksel.Bind(wx.EVT_BUTTON, self.Unmasksel)
2473
+ sizer_unmask.Add(unmasksel, 1, wx.EXPAND)
2474
+ unmasksel.Bind(wx.EVT_BUTTON, self.Unmask_selection)
2456
2475
  unmasksel.SetToolTip(_("Unmask all values in the current selection \n If you wish to unmask some of the currently masked data, you have to first select the desired nodes by unchecking the 'Use mask to retrict' on the 'Selection' panel, otherwise it is impossible to select these nodes"))
2457
2476
 
2477
+ sizermask.Add(sizer_unmask, 1, wx.EXPAND)
2478
+
2458
2479
  invertmask = wx.Button(self.mask, wx.ID_ANY, _("Invert mask"), wx.DefaultPosition, wx.DefaultSize, 0)
2459
2480
  sizermask.Add(invertmask, 1, wx.EXPAND)
2460
2481
  invertmask.Bind(wx.EVT_BUTTON, self.InvertMask)
2461
2482
  invertmask.SetToolTip(_("Logical operation on mask -- mask = ~mask"))
2462
2483
 
2484
+ sizer_maskunmask_poly = wx.BoxSizer(wx.HORIZONTAL)
2485
+
2486
+ mask_in_poly = wx.Button(self.mask, wx.ID_ANY, _("Mask inside active vector (+NoData)"), wx.DefaultPosition, wx.DefaultSize, 0)
2487
+ sizer_maskunmask_poly.Add(mask_in_poly, 1, wx.EXPAND)
2488
+ mask_in_poly.Bind(wx.EVT_BUTTON, self.Mask_inside_active_vector)
2489
+ mask_in_poly.SetToolTip(_("Mask all values inside the active vector and set NoData to masked values"))
2490
+
2491
+ mask_out_poly = wx.Button(self.mask, wx.ID_ANY, _("Mask outside active vector (+NoData)"), wx.DefaultPosition, wx.DefaultSize, 0)
2492
+ sizer_maskunmask_poly.Add(mask_out_poly, 1, wx.EXPAND)
2493
+ mask_out_poly.Bind(wx.EVT_BUTTON, self.Mask_outside_active_vector)
2494
+ mask_out_poly.SetToolTip(_("Mask all values outside the active vector and set NoData to masked values"))
2495
+
2496
+ sizermask.Add(sizer_maskunmask_poly, 1, wx.EXPAND)
2497
+
2498
+ sizer_maskunmask_poly_wonodata = wx.BoxSizer(wx.HORIZONTAL)
2499
+
2500
+ mask_in_poly_nodata = wx.Button(self.mask, wx.ID_ANY, _("Mask inside active vector"), wx.DefaultPosition, wx.DefaultSize, 0)
2501
+ sizer_maskunmask_poly_wonodata.Add(mask_in_poly_nodata, 1, wx.EXPAND)
2502
+ mask_in_poly_nodata.Bind(wx.EVT_BUTTON, self.Mask_inside_active_vector_wo_nodata)
2503
+ mask_in_poly_nodata.SetToolTip(_("Mask all values inside the active vector"))
2504
+
2505
+ mask_out_poly_nodata = wx.Button(self.mask, wx.ID_ANY, _("Mask outside active vector"), wx.DefaultPosition, wx.DefaultSize, 0)
2506
+ sizer_maskunmask_poly_wonodata.Add(mask_out_poly_nodata, 1, wx.EXPAND)
2507
+ mask_out_poly_nodata.Bind(wx.EVT_BUTTON, self.Mask_outside_active_vector_wo_nodata)
2508
+ mask_out_poly_nodata.SetToolTip(_("Mask all values outside the active vector"))
2509
+
2510
+ sizermask.Add(sizer_maskunmask_poly_wonodata, 1, wx.EXPAND)
2511
+
2512
+ mask_inside_polys =wx.BoxSizer(wx.HORIZONTAL)
2513
+ mask_in_polys = wx.Button(self.mask, wx.ID_ANY, _("Mask inside active zone (+NoData)"), wx.DefaultPosition, wx.DefaultSize, 0)
2514
+ mask_inside_polys.Add(mask_in_polys, 1, wx.EXPAND)
2515
+ mask_in_polys.Bind(wx.EVT_BUTTON, self.Mask_inside_active_zone)
2516
+ mask_in_polys.SetToolTip(_("Mask all values inside the active zone and set NoData to masked values"))
2517
+
2518
+ mask_inside_polys_nodata = wx.Button(self.mask, wx.ID_ANY, _("Mask inside active zone"), wx.DefaultPosition, wx.DefaultSize, 0)
2519
+ mask_inside_polys.Add(mask_inside_polys_nodata, 1, wx.EXPAND)
2520
+ mask_inside_polys_nodata.Bind(wx.EVT_BUTTON, self.Mask_inside_active_zone_wo_nodata)
2521
+ mask_inside_polys_nodata.SetToolTip(_("Mask all values inside the active zone"))
2522
+
2523
+ sizermask.Add(mask_inside_polys, 1, wx.EXPAND)
2524
+
2463
2525
  self.mask.Layout()
2464
2526
  sizermask.Fit(self.mask)
2465
2527
 
@@ -2630,13 +2692,13 @@ class Ops_Array(wx.Frame):
2630
2692
  self.parentarray.mask_reset()
2631
2693
  self.refresh_array()
2632
2694
 
2633
- def Unmasksel(self, event:wx.MouseEvent):
2695
+ def Unmask_selection(self, event:wx.MouseEvent):
2634
2696
  """
2635
2697
  Enlève le masque des éléments sélectionnés
2636
2698
  @author Pierre Archambeau
2637
2699
  """
2638
2700
 
2639
- self.parentarray.SelectionData.Unmasksel()
2701
+ self.parentarray.SelectionData.unmask_selection()
2640
2702
 
2641
2703
 
2642
2704
  def InvertMask(self, event: wx.MouseEvent):
@@ -2645,6 +2707,42 @@ class Ops_Array(wx.Frame):
2645
2707
  self.parentarray.mask_invert()
2646
2708
  self.refresh_array()
2647
2709
 
2710
+ def Mask_inside_active_vector(self, event: wx.MouseEvent):
2711
+ """ Mask inside the active vector """
2712
+ if self.active_vector is not None:
2713
+ self.parentarray.mask_insidepoly(self.active_vector,)
2714
+ self.refresh_array()
2715
+
2716
+ def Mask_inside_active_vector_wo_nodata(self, event: wx.MouseEvent):
2717
+ """ Mask inside the active vector without setting NoData """
2718
+ if self.active_vector is not None:
2719
+ self.parentarray.mask_insidepoly(self.active_vector, set_nullvalue=False)
2720
+ self.refresh_array()
2721
+
2722
+ def Mask_outside_active_vector(self, event: wx.MouseEvent):
2723
+ """ Mask outside the active vector """
2724
+ if self.active_vector is not None:
2725
+ self.parentarray.mask_outsidepoly(self.active_vector)
2726
+ self.refresh_array()
2727
+
2728
+ def Mask_inside_active_zone(self, event: wx.MouseEvent):
2729
+ """ Mask inside the active zone """
2730
+ if self.active_zone is not None:
2731
+ self.parentarray.mask_insidepolys(self.active_zone.myvectors)
2732
+ self.refresh_array()
2733
+
2734
+ def Mask_inside_active_zone_wo_nodata(self, event: wx.MouseEvent):
2735
+ """ Mask inside the active zone without setting NoData """
2736
+ if self.active_zone is not None:
2737
+ self.parentarray.mask_insidepolys(self.active_zone.myvectors, set_nullvalue=False)
2738
+ self.refresh_array()
2739
+
2740
+ def Mask_outside_active_vector_wo_nodata(self, event: wx.MouseEvent):
2741
+ """ Mask outside the active vector without setting NoData """
2742
+ if self.active_vector is not None:
2743
+ self.parentarray.mask_outsidepoly(self.active_vector, set_nullvalue=False)
2744
+ self.refresh_array()
2745
+
2648
2746
  def interp2Dpolygons(self, event: wx.MouseEvent):
2649
2747
  """
2650
2748
  Bouton d'interpolation sous tous les polygones d'une zone
@@ -2685,48 +2783,6 @@ class Ops_Array(wx.Frame):
2685
2783
 
2686
2784
  self.parentarray.SelectionData.volumesurface()
2687
2785
 
2688
- # def _volumesurface(self, show=True):
2689
- # """
2690
- # Evaluation of stage-storage-surface relation
2691
- # """
2692
-
2693
- # if self.mapviewer is not None:
2694
- # if self.mapviewer.linked:
2695
- # array1 = self.mapviewer.linkedList[0].active_array
2696
- # array2 = self.mapviewer.linkedList[1].active_array
2697
-
2698
- # # transfert des mailles sélectionnées dans l'autre matrice
2699
- # if array1 is self.parentarray:
2700
- # array2.mngselection.myselection = array1.mngselection.myselection.copy()
2701
- # if array2 is self.parentarray:
2702
- # array1.mngselection.myselection = array2.mngselection.myselection.copy()
2703
-
2704
- # if len(self.parentarray.mngselection.myselection) == 0 or self.parentarray.mngselection.myselection == 'all':
2705
- # myarray = array1
2706
- # axs = myarray.volume_estimation()
2707
- # myarray = array2
2708
- # axs = myarray.volume_estimation(axs)
2709
- # else:
2710
- # myarray = array1.mngselection.get_newarray()
2711
- # axs = myarray.volume_estimation()
2712
- # myarray = array2.mngselection.get_newarray()
2713
- # axs = myarray.volume_estimation(axs)
2714
- # else:
2715
- # if len(self.parentarray.mngselection.myselection) == 0 or self.parentarray.mngselection.myselection == 'all':
2716
- # myarray = self.parentarray
2717
- # else:
2718
- # myarray = self.parentarray.mngselection.get_newarray()
2719
- # myarray.volume_estimation()
2720
- # else:
2721
- # if len(self.parentarray.mngselection.myselection) == 0 or self.parentarray.mngselection.myselection == 'all':
2722
- # myarray = self.parentarray
2723
- # else:
2724
- # myarray = self.parentarray.mngselection.get_newarray()
2725
- # myarray.volume_estimation()
2726
-
2727
- # if show:
2728
- # plt.show()
2729
-
2730
2786
  def OnAllSelect(self, event):
2731
2787
  """
2732
2788
  Select all --> just put "all" in "myselection"
@@ -2784,9 +2840,7 @@ class Ops_Array(wx.Frame):
2784
2840
  else:
2785
2841
  structure = np.ones((3, 3))
2786
2842
 
2787
- usemask = self.selectrestricttomask.GetValue()
2788
-
2789
- self.parentarray.SelectionData.erode_selection(nb, usemask, structure)
2843
+ self.parentarray.SelectionData.erode_selection(nb, self.usemask, structure)
2790
2844
  self.refresh_array()
2791
2845
 
2792
2846
  def OnExpandSelection(self, event):
@@ -2797,9 +2851,7 @@ class Ops_Array(wx.Frame):
2797
2851
  else:
2798
2852
  structure = np.ones((3, 3))
2799
2853
 
2800
- usemask = self.selectrestricttomask.GetValue()
2801
-
2802
- self.parentarray.SelectionData.dilate_selection(nb, usemask, structure)
2854
+ self.parentarray.SelectionData.dilate_selection(nb, self.usemask, structure)
2803
2855
  self.refresh_array()
2804
2856
 
2805
2857
  def OnExpandUnselectInterior(self, event):
@@ -2811,9 +2863,7 @@ class Ops_Array(wx.Frame):
2811
2863
  else:
2812
2864
  structure = np.ones((3, 3))
2813
2865
 
2814
- usemask = self.selectrestricttomask.GetValue()
2815
-
2816
- self.parentarray.SelectionData.dilate_contour_selection(nb, usemask, structure)
2866
+ self.parentarray.SelectionData.dilate_contour_selection(nb, self.usemask, structure)
2817
2867
  self.refresh_array()
2818
2868
 
2819
2869
  def OnUnselectInterior(self, event):
@@ -2947,7 +2997,7 @@ class Ops_Array(wx.Frame):
2947
2997
  # condition value
2948
2998
  curcondvalue = float(self.condvalue.GetValue())
2949
2999
 
2950
- self.parentarray.SelectionData.condition_select(curcond, curcondvalue)
3000
+ self.parentarray.SelectionData.condition_select(curcond, curcondvalue, usemask=self.usemask)
2951
3001
 
2952
3002
  self.refresh_array()
2953
3003
 
@@ -3470,6 +3520,21 @@ class Ops_Array(wx.Frame):
3470
3520
  self._select_vector_inside_manager(self.active_vector)
3471
3521
  self.refresh_array()
3472
3522
 
3523
+ def select_vector_outside_manager(self):
3524
+ """ Select nodes outside the active vector (manager) """
3525
+ if self.active_vector is None:
3526
+ logging.warning(_('Please activate a vector !'))
3527
+ return
3528
+
3529
+ if self.active_vector.nbvertices == 0:
3530
+ logging.warning(_('Please add points to vector or select another !'))
3531
+ return
3532
+
3533
+ logging.info(_('Select nodes outside the active polygon/vector...'))
3534
+ self._select_vector_outside_manager(self.active_vector)
3535
+ self.refresh_array()
3536
+
3537
+
3473
3538
  def _select_vector_inside_manager(self, vect: vector):
3474
3539
  """ Select nodes inside a vector or set action to add vertices to a vector by clicks"""
3475
3540
 
@@ -3488,6 +3553,24 @@ class Ops_Array(wx.Frame):
3488
3553
  firstvert = wolfvertex(0., 0.)
3489
3554
  self.vectmp.add_vertex(firstvert)
3490
3555
 
3556
+ def _select_vector_outside_manager(self, vect: vector):
3557
+ """ Select nodes outside a vector or set action to add vertices to a vector by clicks"""
3558
+
3559
+ if vect.nbvertices > 2:
3560
+ self.parentarray.SelectionData.select_outsidepoly(vect)
3561
+
3562
+ elif self.mapviewer is not None:
3563
+ if vect.nbvertices < 3:
3564
+ logging.info(_('Please add points to vector !'))
3565
+
3566
+ self.mapviewer.start_action('select by vector outside', _('Please draw a polygon...'))
3567
+ self.mapviewer.active_array = self.parentarray
3568
+ self.mapviewer.set_label_selecteditem(self.parentarray.idx)
3569
+ self.Active_vector(vect)
3570
+
3571
+ firstvert = wolfvertex(0., 0.)
3572
+ self.vectmp.add_vertex(firstvert)
3573
+
3491
3574
  def select_zone_under_manager(self):
3492
3575
  """ Select nodes along the active zone (manager) """
3493
3576
 
@@ -3597,6 +3680,9 @@ class Ops_Array(wx.Frame):
3597
3680
  logging.info(_(' Choose vector by clicks...'))
3598
3681
  logging.info(_(''))
3599
3682
  self.select_vector_under_tmp()
3683
+ # elif id == 7:
3684
+ # logging.info(_('Node selection outside active vector (manager)'))
3685
+ # self.select_vector_outside_manager()
3600
3686
 
3601
3687
  def onclose(self, event:wx.MouseEvent):
3602
3688
  """ Hide the window """
@@ -3819,40 +3905,370 @@ class Ops_Array(wx.Frame):
3819
3905
  dlg.Destroy()
3820
3906
 
3821
3907
 
3908
+ ALL_SELECTED = 'all'
3909
+
3910
+ class StorageMode(Enum):
3911
+ """ Storage mode for selections in WolfArray """
3912
+ LIST = 0
3913
+ ARRAY = 1
3914
+
3822
3915
  class SelectionData():
3823
3916
  """
3824
3917
  User-selected data in a WolfArray
3825
3918
 
3826
3919
  Contains two storage elements :
3827
- - myselection (list): Current selection which will be lost in the event of a reset
3920
+ - myselection
3828
3921
  - selections( dict): Stored selection(s) to be used, for example, in a spatial interpolation operation.
3829
3922
  These selections are only lost in the event of a general reset.
3830
3923
 
3831
3924
  The selected nodes are stored using their "world" spatial coordinates so that they can be easily transferred to other objects.
3925
+
3926
+ The "myselection" can be stored in two modes :
3927
+ - LIST: 'all' or list of (x, y) coordinates or tuple of ('all', np.ndarray) for all nodes and excluded nodes
3928
+ - ARRAY: np.ndarray of selected nodes (boolean array) with shape (nbx, nby) where nbx and nby are the number of nodes in the x and y directions respectively.
3929
+ The boolean array is True if the node is selected, False if not selected.
3930
+
3931
+ The selection can be converted from one mode to another automatically based on the number of nodes in the array.
3932
+ If the number of nodes exceeds a threshold (default is 100,000), the selection is stored as an ARRAY.
3933
+ Otherwise, it is stored as a LIST.
3934
+
3935
+ The selection can be forced to a specific storage mode using the `force_storage_mode` method.
3832
3936
  """
3833
3937
 
3834
- myselection:list[tuple[float, float]]
3938
+ # 'all' or list of (x, y) coordinates or a tuple of ('all', np.ndarray) for all nodes and excluded nodes
3939
+ _myselection:list[tuple[float, float]] | str | tuple[str, np.ndarray]
3940
+
3835
3941
  selections: dict[str:dict['select':list[tuple[float, float]], 'idgllist':int, 'color':list[float]]]
3836
3942
 
3837
- def __init__(self, parent:"WolfArray") -> None:
3943
+ def __init__(self, parent:"WolfArray", threshold_array_mode:int = 100_000) -> None:
3944
+ """ Initialize the SelectionData object.
3945
+
3946
+ :param parent: The parent WolfArray object to which this selection data belongs.
3947
+ :param threshold_array_mode: The threshold number of nodes to switch from LIST to ARRAY storage mode.
3948
+ """
3838
3949
 
3839
3950
  self.parent: WolfArray
3840
3951
  self.parent = parent
3841
3952
 
3842
3953
  self.wx_exists = wx.GetApp() is not None
3843
3954
 
3844
- self.myselection = []
3955
+ self._myselection = []
3845
3956
  self.selections = {}
3846
3957
 
3958
+ self._boolarray: np.ndarray | None = None # boolean array for selection - True if selected, False if not selected
3959
+
3960
+ self._storage_mode = StorageMode.LIST # 0: 'all' or list of (x, y) coordinates or tuple of ('all', np.ndarray) for all nodes and excluded nodes, 1 for np.ndarray of selected nodes
3961
+
3847
3962
  self.update_plot_selection = False # force to update OpenGL list if True
3848
3963
  self.hideselection = False
3849
3964
  self.numlist_select = 0 # OpenGL list index
3965
+ self.threshold_array_mode = threshold_array_mode # Default threshold for switching storage mode
3966
+
3967
+ def _auto_storage_mode(self):
3968
+ """ Choose the storage mode based on the number of nodes in the array """
3969
+
3970
+ if self.nb > self.threshold_array_mode:
3971
+ _storage_mode = StorageMode.ARRAY
3972
+ else:
3973
+ _storage_mode = StorageMode.LIST
3974
+
3975
+ if self._storage_mode != _storage_mode:
3976
+ self._convert_to_storage_mode(_storage_mode)
3977
+
3978
+ def _convert_to_storage_mode(self, new_mode:StorageMode):
3979
+ """ Convert the selection to the new storage mode.
3980
+
3981
+ :param new_mode: The new storage mode to convert to (StorageMode.LIST or StorageMode.ARRAY).
3982
+ """
3983
+
3984
+ logging.info('Switching storage mode to {}'.format(new_mode))
3985
+
3986
+ if self._storage_mode == StorageMode.LIST:
3987
+ # Convert from 'all' or list of (x, y) coordinates or tuple of ('all', np.ndarray) to np.ndarray
3988
+ _bool_array = self._myselection_as_array
3989
+
3990
+ if self._myselection == ALL_SELECTED:
3991
+ _bool_array[:,:] = True
3992
+
3993
+ elif isinstance(self._myselection, list):
3994
+
3995
+ if len(self._myselection) == 0:
3996
+ _bool_array[:,:] = False
3997
+ else:
3998
+ _bool_array[:,:] = False
3999
+ xy = np.asarray(self._myselection)
4000
+ ij = self.parent.xy2ij_np(xy)
4001
+ _bool_array[ij[:, 0], ij[:, 1]] = True
4002
+
4003
+ elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED:
4004
+ _bool_array[:,:] = True
4005
+ _excluded_nodes = self._myselection[1]
4006
+
4007
+ if _excluded_nodes.size > 0:
4008
+ _excluded_nodes = self.parent.xy2ij_np(_excluded_nodes)
4009
+ _bool_array[_excluded_nodes[:, 0], _excluded_nodes[:, 1]] = False
4010
+
4011
+ self._myselection = []
4012
+
4013
+ elif self._storage_mode == StorageMode.ARRAY:
4014
+
4015
+ if self.is_all_selected():
4016
+ self._myselection = ALL_SELECTED
4017
+ else:
4018
+ nb = self.nb
4019
+ if nb == 0:
4020
+ self._myselection = []
4021
+ elif nb / max(self.parent.nbnotnull, 1) > 0.5:
4022
+ ij_not_selected = np.argwhere(~self._myselection_as_array)
4023
+ xy_not_selected = self.parent.ij2xy_np(ij_not_selected)
4024
+ self._myselection = (ALL_SELECTED, xy_not_selected)
4025
+ else:
4026
+ ij_selected = np.argwhere(self._myselection_as_array)
4027
+ xy_selected = self.parent.ij2xy_np(ij_selected)
4028
+ self._myselection = list(map(tuple, xy_selected))
4029
+
4030
+ self._storage_mode = new_mode
4031
+
4032
+ def force_storage_mode(self, new_mode:StorageMode):
4033
+ """ Force the storage mode to the new mode.
4034
+
4035
+ :param new_mode: The new storage mode to force (StorageMode.LIST or StorageMode.ARRAY).
4036
+ """
4037
+
4038
+ if new_mode not in StorageMode:
4039
+ logging.error('Invalid storage mode - must be LIST or ARRAY')
4040
+ return
4041
+ if self._storage_mode != new_mode:
4042
+ self._convert_to_storage_mode(new_mode)
4043
+
4044
+ @property
4045
+ def myselection(self) -> list[tuple[float, float]] | str:
4046
+ """ Current selection of nodes.
4047
+
4048
+ Returns:
4049
+ - 'all' if all nodes are selected
4050
+ - list of (x, y) coordinates if specific nodes are selected
4051
+ """
4052
+
4053
+ if self._storage_mode == StorageMode.LIST:
4054
+ if self._myselection == ALL_SELECTED:
4055
+ return ALL_SELECTED
4056
+ elif isinstance(self._myselection, list):
4057
+ return self._myselection
4058
+ elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED:
4059
+
4060
+ excluded_nodes = self._myselection[1]
4061
+ if excluded_nodes.shape[0] == 0:
4062
+ return ALL_SELECTED
4063
+ else:
4064
+ if self.parent.usemask:
4065
+ # Return all nodes except the excluded ones
4066
+ loc_mask = ~ self.parent.array.mask.copy()
4067
+ else:
4068
+ loc_mask = np.ones((self.parent.nbx, self.parent.nby), dtype=bool)
4069
+
4070
+ ij_excluded = self.parent.xy2ij_np(excluded_nodes)
4071
+ loc_mask[ij_excluded[:, 0], ij_excluded[:, 1]] = False
4072
+ ij = np.argwhere(loc_mask)
4073
+ xy = self.parent.ij2xy_np(ij)
4074
+ return list(map(tuple, xy)) # Convert to list of tuples
4075
+
4076
+ elif self._storage_mode == StorageMode.ARRAY:
4077
+ if self.is_all_selected():
4078
+ return ALL_SELECTED
4079
+ else:
4080
+ nbsel = np.count_nonzero(self._myselection_as_array)
4081
+ if nbsel == 0:
4082
+ return []
4083
+ else:
4084
+ # Convert boolean array to list of (x, y) coordinates
4085
+ ij_selected = np.argwhere(self._myselection_as_array)
4086
+ xy_selected = self.parent.ij2xy_np(ij_selected)
4087
+ return list(map(tuple, xy_selected))
4088
+
4089
+ else:
4090
+ logging.error('Invalid storage mode - must be LIST or ARRAY')
4091
+ return []
4092
+
4093
+ @property
4094
+ def _myselection_as_array(self) -> np.ndarray:
4095
+ """ Current selection of nodes as a numpy array.
4096
+
4097
+ Returns:
4098
+ - np.ndarray of shape (nbx, nby) where nbx and nby are the number of nodes in the x and y directions respectively.
4099
+ - True if the node is selected, False if not selected.
4100
+ """
4101
+
4102
+ if self._storage_mode == StorageMode.LIST:
4103
+
4104
+ if self._boolarray is not None:
4105
+ return self._boolarray
4106
+
4107
+ self._boolarray = np.zeros((self.parent.nbx, self.parent.nby), dtype=bool)
4108
+
4109
+ if self._myselection == ALL_SELECTED:
4110
+ if self.parent.usemask:
4111
+ self._boolarray[:,:] = ~self.parent.array.mask[:,:]
4112
+ else:
4113
+ self._boolarray[:,:] = True
4114
+
4115
+ elif isinstance(self._myselection, list):
4116
+ # Convert list of (x, y) coordinates to boolean array
4117
+ if len(self._myselection) > 0:
4118
+ xy= np.asarray(self._myselection)
4119
+ ij = self.parent.xy2ij_np(xy)
4120
+ self._boolarray[ij[:, 0], ij[:, 1]] = True
4121
+
4122
+ elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED:
4123
+
4124
+ # tuple of ('all', np.ndarray) for all nodes and excluded nodes
4125
+ if self.parent.usemask:
4126
+ self._boolarray[:,:] = ~self.parent.array.mask[:,:]
4127
+ else:
4128
+ self._boolarray[:,:] = True
4129
+
4130
+ # Exclude nodes from the second element of the tuple
4131
+ excluded_nodes = self.myselection[1]
4132
+ if excluded_nodes.size > 0:
4133
+ excluded_ij = self.parent.xy2ij_np(excluded_nodes)
4134
+ self._boolarray[excluded_ij[:, 0], excluded_ij[:, 1]] = False
4135
+
4136
+ elif self._storage_mode == StorageMode.ARRAY:
4137
+
4138
+ if self._boolarray is None:
4139
+ self._boolarray = np.zeros((self.parent.nbx, self.parent.nby), dtype=bool)
4140
+
4141
+ return self._boolarray
4142
+
4143
+ @_myselection_as_array.setter
4144
+ def _myselection_as_array(self, value: np.ndarray):
4145
+ """ Set the current selection of nodes as a numpy array.
4146
+
4147
+ :param value: numpy array of shape (nbx, nby) where nbx and nby are the number of nodes in the x and y directions respectively.
4148
+ True if the node is selected, False if not selected.
4149
+ """
4150
+
4151
+ if not isinstance(value, np.ndarray):
4152
+ logging.error('Invalid value for myselection_as_array - must be a numpy array')
4153
+ if value.shape != (self.parent.nbx, self.parent.nby):
4154
+ logging.error('Invalid shape for myselection_as_array - must be ({}, {})'.format
4155
+ (self.parent.nbx, self.parent.nby))
4156
+
4157
+ if self._boolarray is None:
4158
+ self._boolarray = value.copy()
4159
+ else:
4160
+ self._boolarray[:,:] = value[:,:]
4161
+
4162
+ @property
4163
+ def myselection_npargwhere(self) -> np.ndarray:
4164
+ """ Current selection of nodes as a numpy array using np.argwhere """
4165
+ return np.argwhere(self._myselection_as_array)
4166
+
4167
+ @myselection.setter
4168
+ def myselection(self, value: list[tuple[float, float]] | str | tuple[str, np.ndarray]):
4169
+ """ Set the current selection of nodes.
4170
+
4171
+ :param value: 'all' to select all nodes, a list of (x, y) coordinates to select specific nodes,
4172
+ or a tuple of ('all', np.ndarray) for all nodes and excluded nodes.
4173
+ """
4174
+
4175
+ if self._storage_mode == StorageMode.ARRAY:
4176
+ # Convert to the new storage mode if necessary
4177
+ if isinstance(value, list):
4178
+ if len(value) == 0:
4179
+ self._myselection_as_array[:,:] = False
4180
+ else:
4181
+ ij = self.parent.xy2ij_np(np.array(value))
4182
+ self._myselection_as_array[:,:] = False
4183
+ self._myselection_as_array[ij[:, 0], ij[:, 1]] = True
4184
+
4185
+ elif isinstance(value, str):
4186
+
4187
+ if value == ALL_SELECTED:
4188
+ if self.parent.usemask:
4189
+ self._myselection_as_array[:,:] = ~self.parent.array.mask[:,:]
4190
+ else:
4191
+ self._myselection_as_array[:,:] = True
4192
+ else:
4193
+ logging.error('Invalid selection value - must be "all" or a list of (x, y) coordinates')
4194
+
4195
+ elif isinstance(value, tuple) and len(value) == 2 and value[0] == ALL_SELECTED:
4196
+ # tuple of ('all', np.ndarray) for all nodes and excluded nodes
4197
+ if self.parent.usemask:
4198
+ self._myselection_as_array[:,:] = ~self.parent.array.mask[:,:]
4199
+ else:
4200
+ self._myselection_as_array[:,:] = True
4201
+
4202
+ excluded_nodes = value[1]
4203
+ if excluded_nodes.size > 0:
4204
+ ij_excluded = self.parent.xy2ij_np(excluded_nodes)
4205
+ self._myselection_as_array[ij_excluded[:, 0], ij_excluded[:, 1]] = False
4206
+
4207
+ else:
4208
+ logging.error('Invalid selection value - must be "all" or a list of (x, y) coordinates or a tuple of ("all", np.ndarray)')
4209
+
4210
+ elif self._storage_mode == StorageMode.LIST:
4211
+
4212
+ if isinstance(value, list):
4213
+ if len(value) == 0:
4214
+ self._myselection = []
4215
+ else:
4216
+ self._myselection = value
4217
+
4218
+ elif isinstance(value, str):
4219
+ if value == ALL_SELECTED:
4220
+ self._myselection = ALL_SELECTED
4221
+ else:
4222
+ logging.error('Invalid selection value - must be "all" or a list of (x, y) coordinates')
4223
+
4224
+ elif isinstance(value, tuple) and len(value) == 2 and value[0] == ALL_SELECTED:
4225
+ # tuple of ('all', np.ndarray) for all nodes and excluded nodes
4226
+ self._myselection = value
4227
+
4228
+ else:
4229
+ logging.error('Invalid selection value - must be "all" or a list of (x, y) coordinates or a tuple of ("all", np.ndarray)')
4230
+
4231
+ self.update_nb_nodes_selection()
4232
+
4233
+ def is_all_selected(self) -> bool:
4234
+ """ Check if all nodes are selected.
4235
+
4236
+ :return: True if all nodes are selected, False otherwise
4237
+ """
4238
+
4239
+ if self._storage_mode == StorageMode.LIST:
4240
+
4241
+ if self.myselection == ALL_SELECTED:
4242
+ return True
4243
+ elif isinstance(self.myselection, list):
4244
+ if self.parent.usemask:
4245
+ return len(self.myselection) == self.parent.nbnotnull
4246
+ else :
4247
+ return len(self.myselection) == self.parent.nbx * self.parent.nby
4248
+ elif isinstance(self.myselection, tuple) and len(self.myselection) == 2 and self.myselection[0] == ALL_SELECTED:
4249
+ # tuple of ('all', np.ndarray) for all nodes and excluded nodes
4250
+ if self.myselection[1].size == 0:
4251
+ return True
4252
+ return False
4253
+
4254
+ elif self._storage_mode == StorageMode.ARRAY:
4255
+ if self.nb > 0:
4256
+ # Check if all nodes are selected in the boolean array
4257
+ if self.parent.usemask:
4258
+ return np.all(self._myselection_as_array)
4259
+ else:
4260
+ return np.all(self._myselection_as_array == ~self.parent.array.mask)
4261
+ else:
4262
+ return False
3850
4263
 
3851
4264
  def set_selection_from_list_xy(self, xylist: list[tuple[float, float]]):
3852
- """ Set the current selection from a list of (x, y) coordinates """
4265
+ """ Set the current selection from a list of (x, y) coordinates.
4266
+
4267
+ Alias for myselection setter to set a list of (x, y) coordinates.
4268
+ This will convert the list to the appropriate storage mode if necessary.
4269
+ """
3853
4270
 
3854
4271
  self.myselection = xylist
3855
- self.update_nb_nodes_selection()
3856
4272
 
3857
4273
  @property
3858
4274
  def dx(self) -> float:
@@ -3874,51 +4290,60 @@ class SelectionData():
3874
4290
 
3875
4291
  @property
3876
4292
  def nb(self) -> int:
3877
- """ Number of selected nodes """
4293
+ """ Number of selected nodes. """
3878
4294
 
3879
- if self.myselection == 'all':
3880
- return self.parent.nbnotnull
3881
- else:
3882
- return len(self.myselection)
4295
+ if self._storage_mode == StorageMode.LIST:
3883
4296
 
3884
- def Unmasksel(self, resetplot:bool=True):
3885
- """ Unmask selection """
4297
+ if self._myselection == ALL_SELECTED:
4298
+ if self.parent.usemask:
4299
+ return self.parent.nbnotnull
4300
+ else:
4301
+ return self.parent.nbx * self.parent.nby
3886
4302
 
3887
- curarray: WolfArray
3888
- curarray = self.parent
4303
+ elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED:
4304
+ # tuple of ('all', np.ndarray) for all nodes and excluded nodes
4305
+ if self.parent.usemask:
4306
+ return self.parent.nbnotnull - self._myselection[1].shape[0]
4307
+ else:
4308
+ return self.parent.nbx * self.parent.nby - self._myselection[1].shape[0]
4309
+ else:
4310
+ return len(self._myselection)
3889
4311
 
3890
- if self.nb == 0:
3891
- return
3892
- else:
3893
- destxy = self.myselection
4312
+ elif self._storage_mode == StorageMode.ARRAY:
3894
4313
 
3895
- if len(destxy) == 0:
3896
- logging.error(_('No selection to unmask'))
3897
- return
4314
+ return np.count_nonzero(self._myselection_as_array)
3898
4315
 
3899
- if destxy == 'all':
3900
- curarray.array.mask[:, :] = False
3901
- else:
3902
- destij = np.asarray([list(curarray.get_ij_from_xy(x, y)) for x, y in destxy])
4316
+ def unmask_selection(self, resetplot:bool=True):
4317
+ """ Unmask selection """
4318
+
4319
+ if self.nb == 0:
4320
+ return
3903
4321
 
3904
- curarray.array.mask[destij[:, 0], destij[:, 1]] = False
4322
+ self.parent.array.mask[self.myselection_npargwhere] = False
3905
4323
 
3906
4324
  if resetplot:
3907
- curarray.reset_plot()
4325
+ self.parent.reset_plot()
3908
4326
 
3909
4327
  def reset(self):
3910
4328
  """ Reset the selection """
3911
4329
 
3912
4330
  self.myselection = []
4331
+ self._boolarray = None # Reset the boolean array
4332
+ self._storage_mode = StorageMode.LIST # Reset the storage mode to LIST
3913
4333
 
3914
4334
  def reset_all(self):
3915
4335
  """ Reset the selection """
3916
4336
 
3917
- self.myselection = []
4337
+ self.reset()
3918
4338
  self.selections = {}
3919
4339
 
3920
4340
  def get_string(self, which:str = None, all_memories:bool= False) -> str:
3921
- """ Get string of the current selection or of a stored one """
4341
+ """ Get string of the current selection or of a stored one.
4342
+
4343
+ :param which: id/key of the selection to get the string for. If None, get the current selection.
4344
+ :param all_memories: If True, include all stored selections in the output string.
4345
+ :return: String representation of the selection.
4346
+ """
3922
4347
 
3923
4348
  if which is None:
3924
4349
  curlist = self.myselection
@@ -3937,7 +4362,7 @@ class SelectionData():
3937
4362
  if len(curlist) == 0:
3938
4363
  return ''
3939
4364
 
3940
- if curlist == 'all':
4365
+ if curlist == ALL_SELECTED:
3941
4366
  txt += 'all\n'
3942
4367
  return txt
3943
4368
 
@@ -3956,9 +4381,13 @@ class SelectionData():
3956
4381
  return txt
3957
4382
 
3958
4383
  def get_script(self, which:int = None) -> str:
3959
- """ Get script of the current selection or of a stored one """
4384
+ """ Get script of the current selection or of a stored one.
4385
+
4386
+ :param which: id/key of the selection to get the script for. If None, get the current selection.
4387
+ :return: Script representation of the selection.
4388
+ """
3960
4389
 
3961
- if self.myselection == 'all':
4390
+ if self.myselection == ALL_SELECTED:
3962
4391
  logging.error(_('Cannot create script for "all" selection'))
3963
4392
  return ''
3964
4393
 
@@ -3998,7 +4427,14 @@ class SelectionData():
3998
4427
  return txt
3999
4428
 
4000
4429
  def copy_to_clipboard(self, which:int = None, typestr:Literal['string', 'script'] = 'string'):
4001
- """ Copy current selection to clipboard """
4430
+ """ Copy current selection to clipboard.
4431
+
4432
+ -- ONLY WORKS WITH wxPython --
4433
+
4434
+ :param which: id/key of the selection to copy. If None, copy the current selection.
4435
+ :param typestr: Type of data to copy to clipboard - 'string' for string representation, 'script' for script representation.
4436
+ """
4437
+
4002
4438
  if self.wx_exists:
4003
4439
  if wx.TheClipboard.Open():
4004
4440
  wx.TheClipboard.Clear()
@@ -4010,15 +4446,22 @@ class SelectionData():
4010
4446
  wx.TheClipboard.Close()
4011
4447
  else:
4012
4448
  logging.warning(_('Cannot open the clipboard'))
4449
+ else:
4450
+ logging.warning(_('Clipboard is not available in this environment'))
4013
4451
 
4014
4452
  def reselect_from_memory(self, idx:list[str] = None):
4015
4453
  """
4016
4454
  Reselect a stored selection
4017
4455
 
4018
- :param idx: id/key of the selection
4456
+ :param idx: id/key of the selection to reselect. If None, show a dialog to choose from available selections.
4019
4457
  """
4020
4458
 
4021
4459
  if idx is None:
4460
+ if not self.wx_exists:
4461
+ logging.error(_('Cannot reselect from memory - no wxPython available'))
4462
+ logging.error(_('Please use the method reselect_from_memory with a list of keys'))
4463
+ return
4464
+
4022
4465
  keys = list(self.selections.keys())
4023
4466
 
4024
4467
  keys = [cur for cur in keys if len(self.selections[cur]['select']) > 0]
@@ -4041,11 +4484,12 @@ class SelectionData():
4041
4484
 
4042
4485
  for curidx in idx:
4043
4486
  if curidx in self.selections:
4044
- self.myselection += self.selections[curidx]['select']
4487
+ self.add_nodes_to_selection(self.selections[curidx]['select'])
4488
+ # self.myselection += self.selections[curidx]['select']
4045
4489
  else:
4046
4490
  logging.error(_('Selection {} does not exist').format(idx))
4047
4491
 
4048
- self.update_nb_nodes_selection()
4492
+ # self.update_nb_nodes_selection()
4049
4493
  self.parent.reset_plot()
4050
4494
 
4051
4495
  def move_selectionto(self, idx:str, color:list[float], resetplot:bool=True):
@@ -4054,6 +4498,7 @@ class SelectionData():
4054
4498
 
4055
4499
  :param idx: id/key of the selection
4056
4500
  :param color: color of the selection - list of 4 integers between 0 and 255
4501
+ :param resetplot: if True, reset the plot after moving the selection
4057
4502
  """
4058
4503
 
4059
4504
  assert len(color) == 4, "color must be a list of 4 integers between 0 and 255"
@@ -4068,7 +4513,7 @@ class SelectionData():
4068
4513
  curdict['color'] = color
4069
4514
 
4070
4515
  self.myselection = [] # reset current selection
4071
- self.update_nb_nodes_selection()
4516
+ # self.update_nb_nodes_selection()
4072
4517
 
4073
4518
  if resetplot:
4074
4519
  self.parent.reset_plot()
@@ -4083,7 +4528,7 @@ class SelectionData():
4083
4528
  if len(self.selections) > 0:
4084
4529
  # plot stored selections
4085
4530
  for cur in self.selections.values():
4086
- if cur['select'] != 'all':
4531
+ if cur['select'] != ALL_SELECTED:
4087
4532
  self.update_plot_selection = update_select
4088
4533
  col = cur['color']
4089
4534
  cur['idgllist'] = self._plot_selection(cur['select'],
@@ -4092,9 +4537,9 @@ class SelectionData():
4092
4537
  cur['idgllist'])
4093
4538
 
4094
4539
 
4095
- if self.myselection != 'all':
4540
+ if self._myselection != ALL_SELECTED and not isinstance(self._myselection, tuple):
4096
4541
  # plot current selection in RED if not 'all'
4097
- if len(self.myselection) > 0:
4542
+ if self.nb > 0:
4098
4543
  self.update_plot_selection = update_select
4099
4544
  self.numlist_select = self._plot_selection(self.myselection,
4100
4545
  (1., 0., 0.),
@@ -4163,8 +4608,8 @@ class SelectionData():
4163
4608
  """
4164
4609
  Add one coordinate to the selection
4165
4610
 
4166
- :param x: x coordinate
4167
- :param y: y coordinate
4611
+ :param x: x coordinate - float
4612
+ :param y: y coordinate - float
4168
4613
  :param verif: if True, the coordinates are checked to avoid duplicates
4169
4614
  """
4170
4615
 
@@ -4179,48 +4624,79 @@ class SelectionData():
4179
4624
  else:
4180
4625
  return -1 # useful for MB
4181
4626
 
4182
- def add_nodes_to_selection(self, xy:list[float], verif:bool=True):
4627
+ self.update_nb_nodes_selection()
4628
+
4629
+ def add_nodes_to_selection(self, xy:list[float] | np.ndarray, verif:bool=True):
4183
4630
  """
4184
- Add multiple coordinates to the selection
4631
+ Add multiple coordinates to the selection.
4185
4632
 
4186
- :param xy: list of coordinates
4633
+ :param xy: list of coordinates or numpy array of shape (nb_nodes, 2) where nb_nodes is the number of nodes
4634
+ or a numpy array of shape (nbx, nby) where nbx and nby are the number of nodes in the x and y directions respectively.
4635
+ If a numpy array of shape (nbx, nby) is provided, it is assumed to be a boolean array where True indicates a selected node.
4187
4636
  :param verif: if True, the coordinates are checked to avoid duplicates
4188
4637
  """
4189
4638
 
4190
4639
  # on repasse par les i,j car les coordonnées transférées peuvent venir d'un click souris
4191
4640
  # le but est de ne conserver que les coordonnées des CG de mailles
4192
- ij = [self.parent.get_ij_from_xy(x, y) for x, y in xy]
4193
- self._add_nodes_to_selectionij(ij, verif)
4641
+ if isinstance(xy, np.ndarray):
4642
+ self._add_nodes_to_selection_np(xy, verif)
4643
+ else:
4644
+ ij = [self.parent.get_ij_from_xy(x, y) for x, y in xy]
4645
+ self._add_nodes_to_selectionij(ij, verif)
4194
4646
 
4195
4647
  def _add_node_to_selectionij(self, i:int, j:int, verif=True):
4196
4648
  """
4197
- Add one ij coordinate to the selection
4649
+ Add one ij coordinate to the selection.
4198
4650
 
4199
4651
  :param i: i coordinate
4200
4652
  :param j: j coordinate
4201
4653
  :param verif: if True, the coordinates are checked to avoid duplicates
4202
4654
  """
4203
4655
 
4204
- x1, y1 = self.parent.get_xy_from_ij(i, j)
4205
-
4206
- if isinstance(self.myselection, str):
4207
- self.myselection = []
4208
-
4209
- if verif:
4210
- try:
4211
- ret = self.myselection.index((x1, y1))
4212
- except:
4213
- ret = -1
4214
- if ret >= 0:
4215
- self.myselection.pop(ret)
4656
+ if self._storage_mode == StorageMode.ARRAY:
4216
4657
 
4217
- return 0
4658
+ if verif:
4659
+ self._boolarray[i, j] = not self._boolarray[i, j]
4218
4660
  else:
4219
- self.myselection.append((x1, y1))
4220
- return 0
4221
- else:
4222
- self.myselection.append((x1, y1))
4223
- return 0
4661
+ self._boolarray[i, j] = True
4662
+
4663
+ elif self._storage_mode == StorageMode.LIST:
4664
+ x1, y1 = self.parent.get_xy_from_ij(i, j)
4665
+
4666
+ if self._myselection == ALL_SELECTED:
4667
+ # If all nodes are selected, we need to exclude the current node
4668
+ self._myselection = (ALL_SELECTED, np.array([[x1, y1]])) # Exclude the current node
4669
+ elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED:
4670
+ # check is the current node is in the excluded nodes
4671
+ excluded_nodes = self._myselection[1].tolist()
4672
+ if (x1, y1) in excluded_nodes:
4673
+ # If the current node is in the excluded nodes, we remove it from the excluded nodes
4674
+ ret = excluded_nodes.index((x1, y1))
4675
+ excluded_nodes.pop(ret)
4676
+ if len(excluded_nodes) == 0:
4677
+ self._myselection = ALL_SELECTED # All nodes are selected again
4678
+ else:
4679
+ self._myselection = (ALL_SELECTED, np.array(excluded_nodes))
4680
+ else:
4681
+ # The node is included in the selection, we add it to the excluded nodes
4682
+ excluded_nodes.append((x1, y1))
4683
+ self._myselection = (ALL_SELECTED, np.array(excluded_nodes))
4684
+ else:
4685
+ if verif:
4686
+ try:
4687
+ ret = self._myselection.index((x1, y1))
4688
+ except:
4689
+ ret = -1
4690
+ if ret >= 0:
4691
+ self._myselection.pop(ret)
4692
+
4693
+ return 0
4694
+ else:
4695
+ self._myselection.append((x1, y1))
4696
+ return 0
4697
+ else:
4698
+ self._myselection.append((x1, y1))
4699
+ return 0
4224
4700
 
4225
4701
  def _add_nodes_to_selectionij(self, ij:list[tuple[float, float]], verif:bool=True):
4226
4702
  """
@@ -4230,94 +4706,171 @@ class SelectionData():
4230
4706
  :param verif: if True, the coordinates are checked to avoid duplicates
4231
4707
  """
4232
4708
 
4233
- if isinstance(self.myselection, str):
4234
- self.myselection = []
4235
-
4236
-
4237
4709
  if len(ij)==0:
4238
4710
  logging.info(_('Nothing to do in add_nodes_to_selectionij !'))
4239
4711
  return
4240
4712
 
4241
- nbini = len(self.myselection)
4713
+ nbini = self.nb
4242
4714
 
4243
- xy = [self.parent.get_xy_from_ij(i, j) for i, j in ij]
4715
+ ij = np.asarray(ij)
4244
4716
 
4245
- self.myselection += xy
4246
-
4247
- if nbini != 0:
4717
+ if self._storage_mode == StorageMode.ARRAY:
4248
4718
  if verif:
4249
- # trouve les éléments uniques dans la liste de tuples (--> axis=0) et retourne également le comptage
4250
- selunique, counts = np.unique(self.myselection, return_counts=True, axis=0)
4719
+ self._boolarray[ij[:, 0], ij[:, 1]] = np.logical_xor(self._boolarray[ij[:, 0], ij[:, 1]], True)
4720
+ else:
4721
+ self._boolarray[ij[:, 0], ij[:, 1]] = True
4722
+
4723
+ elif self._storage_mode == StorageMode.LIST:
4724
+
4725
+ xy = self.parent.ij2xy_np(ij)
4251
4726
 
4727
+ if self._myselection == ALL_SELECTED:
4728
+ # If all nodes are selected, we need to exclude the current nodes
4729
+ self._myselection = (ALL_SELECTED, xy) # Exclude the current nodes
4730
+
4731
+ elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED:
4732
+ # check if the current nodes are in the excluded nodes
4733
+ excluded_nodes = self._myselection[1]
4734
+ # extend the excluded nodes with the new ones
4735
+ excluded_nodes = np.vstack((excluded_nodes, xy))
4736
+
4737
+ # Find the duplicates in the excluded nodes
4738
+ selunique, counts = np.unique(excluded_nodes, return_counts=True, axis=0)
4252
4739
  # les éléments énumérés plus d'une fois doivent être enlevés
4253
- # on trie par ordre décroissant
4254
4740
  locsort = sorted(zip(counts.tolist(), selunique.tolist()), reverse=True)
4255
4741
  counts = [x[0] for x in locsort]
4256
4742
  sel = [tuple(x[1]) for x in locsort]
4257
-
4258
4743
  # on recherche le premier 1
4259
4744
  if 1 in counts:
4260
4745
  idx = counts.index(1)
4261
4746
  # on ne conserve que la portion de liste utile
4262
- self.myselection = sel[idx:]
4747
+ self._myselection = (ALL_SELECTED, np.array(sel[idx:]))
4748
+ else:
4749
+ self._myselection = ALL_SELECTED # All nodes are selected again
4750
+ else:
4751
+ # Add the new nodes to the selection
4752
+
4753
+ if nbini != 0:
4754
+ self._myselection += xy.tolist()
4755
+
4756
+ if verif:
4757
+ # trouve les éléments uniques dans la liste de tuples (--> axis=0) et retourne également le comptage
4758
+ selunique, counts = np.unique(self._myselection, return_counts=True, axis=0)
4759
+
4760
+ # les éléments énumérés plus d'une fois doivent être enlevés
4761
+ # on trie par ordre décroissant
4762
+ locsort = sorted(zip(counts.tolist(), selunique.tolist()), reverse=True)
4763
+ counts = [x[0] for x in locsort]
4764
+ sel = [tuple(x[1]) for x in locsort]
4765
+
4766
+ # on recherche le premier 1
4767
+ if 1 in counts:
4768
+ idx = counts.index(1)
4769
+ # on ne conserve que la portion de liste utile
4770
+ self._myselection = sel[idx:]
4771
+ else:
4772
+ self._myselection = []
4773
+ else:
4774
+ self._myselection = np.unique(self.myselection, axis=0).tolist()
4775
+ else:
4776
+ self._myselection = xy.tolist()
4777
+
4778
+ self.update_nb_nodes_selection()
4779
+
4780
+ def _add_nodes_to_selection_np(self, ij:np.ndarray, verif:bool=True):
4781
+ """ Add multiple coordinates to the selection from a numpy array
4782
+
4783
+ :param ij: numpy array of coordinates - same shape as the parent array (nbx, nby) or (nb_nodes, 2)
4784
+ :param verif: if True, the coordinates are checked to avoid duplicates
4785
+ """
4786
+
4787
+ assert ij.shape == (self.parent.nbx, self.parent.nby) or ij.shape[1]==2, _('Invalid shape for ij - must be ({}, {})'.format(self.parent.nbx, self.parent.nby))
4788
+
4789
+ dtype = ij.dtype
4790
+ if dtype not in [np.bool, np.int32, np.int64]:
4791
+ assert ij.shape[1] == 2, _('Invalid shape for ij - must be ({}, {}) or (nb_nodes, 2)'.format(self.parent.nbx, self.parent.nby))
4792
+ ij = self.parent.xy2ij_np(ij)
4793
+
4794
+ if self._storage_mode == StorageMode.ARRAY:
4795
+ if verif:
4796
+ if ij.shape[1] == 2:
4797
+ # using xy from np.where
4798
+ self._myselection_as_array[ij[:,0], ij[:,1]] = np.logical_xor(self._myselection_as_array[ij[:, 0], ij[:, 1]], True)
4799
+ else:
4800
+ self._myselection_as_array[:,:] = np.logical_xor(self._myselection_as_array, ij)
4801
+ else:
4802
+ if ij.shape[1] == 2:
4803
+ # using xy from np.where
4804
+ self._myselection_as_array[ij[:,0], ij[:,1]] = True
4263
4805
  else:
4264
- self.myselection = []
4806
+ self._myselection_as_array[ij] = True
4807
+
4808
+ elif self._storage_mode == StorageMode.LIST:
4809
+ if ij.shape[1] == 2:
4810
+ # using xy from np.where
4811
+ self._add_nodes_to_selectionij(ij, verif)
4265
4812
  else:
4266
- self.myselection = np.unique(self.myselection, axis=0)
4813
+ # Convert the numpy array to a list of tuples
4814
+ ij = np.argwhere(ij)
4815
+ self._add_nodes_to_selectionij(ij, verif)
4267
4816
 
4268
4817
  def select_insidepoly(self, myvect: vector):
4269
- """ Select nodes inside a polygon """
4818
+ """ Select nodes inside a polygon.
4270
4819
 
4271
- nbini = len(self.myselection)
4820
+ :param myvect: vector defining the polygon
4821
+ """
4272
4822
 
4273
- myvect.find_minmax()
4274
- mypoints, _tmpij = self.parent.get_xy_infootprint_vect(myvect)
4275
- path = mpltPath.Path(myvect.asnparray())
4276
- inside = path.contains_points(mypoints)
4823
+ nbini = self.nb
4277
4824
 
4278
- self.hideselection=False
4279
- if self.parent.myops is not None:
4280
- if self.parent.myops.selectrestricttomask.IsChecked():
4281
- self.hideselection=True
4825
+ self.hideselection = self.parent.usemask
4282
4826
 
4283
- self.add_nodes_to_selection(mypoints[np.where(inside)], verif=nbini != 0)
4827
+ inside = self.parent.get_ij_inside_polygon(myvect, usemask=self.hideselection)
4284
4828
 
4285
- if self.parent.myops is not None:
4286
- if len(self.myselection) > 0:
4287
- if self.parent.myops.selectrestricttomask.IsChecked():
4288
- self.condition_select('Mask',0)
4829
+ if inside.shape[0] == 0:
4830
+ logging.info(_('No nodes inside the polygon'))
4831
+ return
4832
+ else:
4833
+ if inside.shape[0] > self.threshold_array_mode:
4834
+ self.force_storage_mode(StorageMode.ARRAY)
4835
+
4836
+ self._add_nodes_to_selectionij(inside, verif= nbini != 0)
4289
4837
 
4290
4838
  self.hideselection=False
4291
- self.update_nb_nodes_selection()
4292
4839
 
4293
4840
  def select_underpoly(self, myvect: vector):
4294
- """ Select nodes along a polyline """
4841
+ """ Select nodes along a polyline
4842
+
4843
+ :param myvect: vector defining the polyline
4844
+ """
4295
4845
 
4296
- nbini = len(self.myselection)
4846
+ nbini = self.nb
4297
4847
 
4298
4848
  myvect.find_minmax()
4299
4849
  mypoints = self.parent.get_ij_under_polyline(myvect)
4300
4850
 
4851
+ if self.parent.usemask:
4852
+ # If the parent uses a mask, we need to check if the points are not masked
4853
+ mypoints = mypoints[~self.parent.array.mask[mypoints[:, 0], mypoints[:, 1]]]
4854
+
4301
4855
  if len(mypoints) == 0:
4302
4856
  logging.info(_('No nodes under the polyline'))
4303
4857
  return
4304
4858
 
4305
- self._add_nodes_to_selectionij(mypoints, verif=nbini != 0)
4306
-
4307
- if self.parent.myops is not None:
4308
- if self.parent.myops.selectrestricttomask.IsChecked():
4309
- self.condition_select('Mask',0)
4310
-
4311
- self.update_nb_nodes_selection()
4859
+ self.add_nodes_to_selection(mypoints, verif = nbini != 0)
4312
4860
 
4313
4861
  def dilate_selection(self, nb_iterations:int, use_mask:bool = True, structure:np.ndarray = None):
4314
- """ Extend the selection """
4862
+ """ Extend the selection.
4863
+
4864
+ :param nb_iterations: Number of iterations to dilate the selection
4865
+ :param use_mask: If True, use the mask of the parent array to limit the dilation
4866
+ :param structure: Structuring element for dilation, default is a cross shape (nodes directly adjacent in the x or y direction)
4867
+ """
4315
4868
 
4316
- if self.myselection == 'all':
4869
+ if self.is_all_selected():
4317
4870
  logging.info(_('Cannot extend selection when all nodes are selected'))
4318
4871
  return
4319
4872
 
4320
- if len(self.myselection) == 0:
4873
+ if self.nb == 0:
4321
4874
  logging.info(_('No nodes selected'))
4322
4875
  return
4323
4876
 
@@ -4331,34 +4884,27 @@ class SelectionData():
4331
4884
 
4332
4885
  from scipy import ndimage
4333
4886
 
4334
- xy = self.myselection
4335
- ij = [self.parent.get_ij_from_xy(x, y) for x, y in xy]
4336
-
4337
- selected = np.zeros(self.parent.array.shape, dtype=bool)
4338
- for i, j in ij:
4339
- selected[i, j] = True
4340
-
4341
- selected = ndimage.binary_dilation(selected,
4887
+ self._myselection_as_array[:,:] = ndimage.binary_dilation(self._myselection_as_array,
4342
4888
  iterations=nb_iterations,
4343
4889
  mask=~self.parent.array.mask if use_mask else None,
4344
4890
  structure=structure)
4345
4891
 
4346
- ij = np.argwhere(selected)
4347
- ij = np.vstack([ij[:, 0], ij[:, 1]]).T
4348
- xy = self.parent.ij2xy_np(ij)
4349
-
4350
- self.myselection = [(cur[0], cur[1]) for cur in xy]
4351
-
4892
+ self._storage_mode = StorageMode.ARRAY # Ensure we are in ARRAY mode
4352
4893
  self.update_nb_nodes_selection()
4353
4894
 
4354
4895
  def erode_selection(self, nb_iterations:int, use_mask:bool = True, structure:np.ndarray = None):
4355
- """ Reduce the selection """
4896
+ """ Reduce the selection.
4897
+
4898
+ :param nb_iterations: Number of iterations to erode the selection
4899
+ :param use_mask: If True, use the mask of the parent array to limit the erosion
4900
+ :param structure: Structuring element for erosion, default is a cross shape (nodes directly adjacent in the x or y direction)
4901
+ """
4356
4902
 
4357
- if self.myselection == 'all':
4903
+ if self.is_all_selected():
4358
4904
  logging.info(_('Cannot reduce selection when all nodes are selected'))
4359
4905
  return
4360
4906
 
4361
- if len(self.myselection) == 0:
4907
+ if self.nb == 0:
4362
4908
  logging.info(_('No nodes selected'))
4363
4909
  return
4364
4910
 
@@ -4372,53 +4918,54 @@ class SelectionData():
4372
4918
 
4373
4919
  from scipy import ndimage
4374
4920
 
4375
- xy = self.myselection
4376
- ij = [self.parent.get_ij_from_xy(x, y) for x, y in xy]
4377
-
4378
- selected = np.zeros(self.parent.array.shape, dtype=bool)
4379
-
4380
- for i, j in ij:
4381
- selected[i, j] = True
4382
-
4383
- selected = ndimage.binary_erosion(selected,
4921
+ self._myselection_as_array[:,:] = ndimage.binary_erosion(self._myselection_as_array,
4384
4922
  iterations=nb_iterations,
4385
4923
  mask=~self.parent.array.mask if use_mask else None,
4386
4924
  structure=structure)
4387
4925
 
4388
- ij = np.argwhere(selected)
4389
- ij = np.vstack([ij[:, 0], ij[:, 1]]).T
4390
- xy = self.parent.ij2xy_np(ij)
4391
-
4392
- self.myselection = [(cur[0], cur[1]) for cur in xy]
4393
-
4926
+ self._storage_mode = StorageMode.ARRAY # Ensure we are in ARRAY mode
4394
4927
  self.update_nb_nodes_selection()
4395
4928
 
4396
4929
  def dilate_contour_selection(self, nbiter:int= 1, use_mask:bool = True, structure:np.ndarray = np.ones((3,3))):
4397
- """ Dilate the contour of the selection """
4930
+ """ Dilate the contour of the selection.
4931
+
4932
+ :param nbiter: Number of iterations to dilate the selection
4933
+ :param use_mask: If True, use the mask of the parent array to limit the dilation
4934
+ :param structure: Structuring element for dilation, default is a 3x3 square
4935
+ """
4398
4936
 
4399
4937
  if self.nb > 0:
4400
- oldsel = self.myselection.copy()
4938
+ oldsel = self._myselection_as_array.copy()
4401
4939
  self.dilate_selection(nbiter, use_mask, structure)
4402
- newsel = self.myselection.copy()
4403
- self.myselection = [cur for cur in newsel if cur not in oldsel]
4940
+ # We keep only the new nodes that were not in the old selection
4941
+ self._myselection_as_array[:,:] = np.logical_and(self._myselection_as_array, ~oldsel)
4942
+ self._storage_mode = StorageMode.ARRAY # Ensure we are in ARRAY mode
4404
4943
  self.update_nb_nodes_selection()
4405
4944
  else:
4406
4945
  logging.info('No selection to expand/dilate')
4407
4946
 
4408
4947
  def erode_contour_selection(self):
4409
- """ Erode the contour of the selection """
4948
+ """ Erode the contour of the selection.
4949
+
4950
+ :param nbiter: Number of iterations to erode the selection
4951
+ :param use_mask: If True, use the mask of the parent array to limit the erosion
4952
+ :param structure: Structuring element for erosion, default is a 3x3 square
4953
+ """
4410
4954
 
4411
4955
  if self.nb > 0:
4412
- oldselect = self.myselection.copy()
4956
+ oldselect = self._myselection_as_array.copy()
4413
4957
  self.erode_selection(1)
4414
- newselect = self.myselection.copy()
4415
- self.myselection = [cur for cur in oldselect if cur not in newselect]
4958
+ self._myselection_as_array[:,:] = np.logical_and(self._myselection_as_array, oldselect)
4959
+ self._storage_mode = StorageMode.ARRAY # Ensure we are in ARRAY mode
4416
4960
  self.update_nb_nodes_selection()
4417
4961
  else:
4418
4962
  logging.info('No selection to contract/erode')
4419
4963
 
4420
4964
  def save_selection(self, filename:str=None):
4421
- """ Save the selection to a file """
4965
+ """ Save the selection to a file.
4966
+
4967
+ :param filename: Name of the file to save the selection to. If None, a dialog will be shown to choose the file.
4968
+ """
4422
4969
 
4423
4970
  if filename is None:
4424
4971
  with wx.FileDialog(None, 'Save selection', wildcard='Text files (*.txt)|*.txt',
@@ -4431,7 +4978,10 @@ class SelectionData():
4431
4978
  f.write(self.get_string(all_memories=True))
4432
4979
 
4433
4980
  def load_selection(self, filename:str=None):
4434
- """ Load a selection from a file """
4981
+ """ Load a selection from a file.
4982
+
4983
+ :param filename: Name of the file to load the selection from. If None, a dialog will be shown to choose the file.
4984
+ """
4435
4985
 
4436
4986
  if filename is None:
4437
4987
  with wx.FileDialog(None, 'Load selection', wildcard='Text files (*.txt)|*.txt',
@@ -4488,13 +5038,35 @@ class SelectionData():
4488
5038
 
4489
5039
  self.parent.reset_plot()
4490
5040
 
4491
- def update_nb_nodes_selection(self):
4492
- """ Update the number of selected nodes """
5041
+ def update_nb_nodes_selection(self, autostoragemode:bool=True) -> tuple[int, float, float, float, float]:
5042
+ """ Update the number of selected nodes.
4493
5043
 
4494
- if self.myselection=='all':
4495
- nb = self.parent.nbnotnull
4496
- else:
4497
- nb = len(self.myselection)
5044
+ :param autostoragemode: If True, automatically switch to ARRAY mode if the number of selected nodes exceeds the threshold.
5045
+ :return: Tuple containing the number of selected nodes, and the bounds of the selection (xmin, xmax, ymin, ymax).
5046
+ """
5047
+
5048
+ if autostoragemode:
5049
+ self._auto_storage_mode()
5050
+
5051
+ if self._storage_mode == StorageMode.ARRAY:
5052
+ nb = np.count_nonzero(self._myselection_as_array)
5053
+ elif self._storage_mode == StorageMode.LIST:
5054
+ if self.is_all_selected():
5055
+ if self.parent.usemask:
5056
+ nb = self.parent.nbnotnull
5057
+ else:
5058
+ nb = self.parent.nbx * self.parent.nby
5059
+ elif isinstance(self._myselection, tuple) and len(self._myselection) == 2 and self._myselection[0] == ALL_SELECTED:
5060
+ # tuple of ('all', np.ndarray) for all nodes and excluded nodes
5061
+
5062
+ if self.parent.usemask:
5063
+ nb = self.parent.nbnotnull
5064
+ else:
5065
+ nb = self.parent.nbx * self.parent.nby
5066
+
5067
+ nb -= len(self._myselection[1])
5068
+ else:
5069
+ nb = len(self._myselection)
4498
5070
 
4499
5071
  self.update_plot_selection = True
4500
5072
  if self.wx_exists:
@@ -4515,13 +5087,24 @@ class SelectionData():
4515
5087
  self.update_plot_selection = True
4516
5088
 
4517
5089
  if nb>0:
4518
- if self.myselection=='all':
5090
+ if self.is_all_selected():
4519
5091
  [xmin, xmax], [ymin, ymax] = self.parent.get_bounds()
4520
5092
  else:
4521
- xmin = np.min(np.asarray(self.myselection)[:, 0])
4522
- ymin = np.min(np.asarray(self.myselection)[:, 1])
4523
- xmax = np.max(np.asarray(self.myselection)[:, 0])
4524
- ymax = np.max(np.asarray(self.myselection)[:, 1])
5093
+ if self._storage_mode == StorageMode.ARRAY:
5094
+ # Get the bounds of the selection from the boolean array
5095
+ sel = np.argwhere(self._myselection_as_array)
5096
+ xmin = np.min(sel[:, 0])
5097
+ ymin = np.min(sel[:, 1])
5098
+ xmax = np.max(sel[:, 0])
5099
+ ymax = np.max(sel[:, 1])
5100
+ xmin, ymin = self.parent.get_xy_from_ij(xmin, ymin)
5101
+ xmax, ymax = self.parent.get_xy_from_ij(xmax, ymax)
5102
+
5103
+ elif self._storage_mode == StorageMode.LIST:
5104
+ xmin = np.min(np.asarray(self.myselection)[:, 0])
5105
+ ymin = np.min(np.asarray(self.myselection)[:, 1])
5106
+ xmax = np.max(np.asarray(self.myselection)[:, 0])
5107
+ ymax = np.max(np.asarray(self.myselection)[:, 1])
4525
5108
  else:
4526
5109
  xmin = -99999.
4527
5110
  ymin = -99999.
@@ -4532,6 +5115,7 @@ class SelectionData():
4532
5115
 
4533
5116
  self.parent.myops.nbselect.SetLabelText(str(nb))
4534
5117
  self.parent.myops.nbselect2.SetLabelText(str(nb))
5118
+
4535
5119
  if nb>0:
4536
5120
 
4537
5121
  self.parent.myops.minx.SetLabelText('{:.3f}'.format(xmin))
@@ -4541,9 +5125,17 @@ class SelectionData():
4541
5125
 
4542
5126
  return nb, xmin, xmax, ymin, ymax
4543
5127
 
4544
- def condition_select(self, cond, condval, condval2=0, usemask=False):
5128
+ def condition_select(self, cond:str | int, condval, condval2 = 0, usemask:bool = False):
5129
+ """ Select nodes based on a condition.
5130
+
5131
+ :param cond: condition to apply (0: '<', 1: '<=', 2: '==', 3: '>=', 4: '>', 5: 'NaN', 6: '>=<=', 7: '><', 8: '<>')
5132
+ :param condval: value to compare with
5133
+ :param condval2: second value to compare with (for ranges)
5134
+ :param usemask: whether to use the mask or not
5135
+ """
5136
+
4545
5137
  array = self.parent.array
4546
- nbini = len(self.myselection)
5138
+ nbini = self.nb
4547
5139
 
4548
5140
  if array.dtype == np.float32:
4549
5141
  condval = np.float32(condval)
@@ -4606,8 +5198,14 @@ class SelectionData():
4606
5198
  return
4607
5199
  else:
4608
5200
  try:
4609
- sel = np.asarray(self.myselection)
4610
- ijall = np.asarray(self.parent.get_ij_from_xy(sel[:, 0], sel[:, 1])).transpose()
5201
+ if self.nb > self.threshold_array_mode:
5202
+ # If the selection is large, we use the boolean array
5203
+ ijall = np.argwhere(self._myselection_as_array)
5204
+ else:
5205
+ # Otherwise, we use the selection list
5206
+ sel = np.asarray(self.myselection)
5207
+ ijall = self.parent.xy2ij_np(sel)
5208
+
4611
5209
  if cond == 0 or cond=='<':
4612
5210
  # <
4613
5211
  ij = np.argwhere((array[ijall[:, 0], ijall[:, 1]] < condval) & (mask[ijall[:, 0], ijall[:, 1]]))
@@ -4684,8 +5282,11 @@ class SelectionData():
4684
5282
  return
4685
5283
  else:
4686
5284
  try:
4687
- sel = np.asarray(self.myselection)
4688
- ijall = np.asarray(self.parent.get_ij_from_xy(sel[:, 0], sel[:, 1])).transpose()
5285
+ if self.nb > self.threshold_array_mode:
5286
+ ijall = np.argwhere(self._myselection_as_array)
5287
+ else:
5288
+ sel = np.asarray(self.myselection)
5289
+ ijall = self.parent.xy2ij_np(sel)
4689
5290
 
4690
5291
  if cond == 0 or cond=='<':
4691
5292
  # <
@@ -4727,9 +5328,16 @@ class SelectionData():
4727
5328
  logging.error(_('Error in condition_select ! -- Please report this bug, specifying the context'))
4728
5329
  return
4729
5330
 
4730
- self.update_nb_nodes_selection()
5331
+ # self.update_nb_nodes_selection()
4731
5332
 
4732
- def treat_select(self, op, cond, opval, condval):
5333
+ def treat_select(self, op:int, cond:int, opval, condval):
5334
+ """ Apply an operation on the selected nodes based on a condition.
5335
+
5336
+ :param op: operation to apply (0: '+', 1: '-', 2: '*', 3: '/', 4: 'replace')
5337
+ :param cond: condition to apply (0: '<', 1: '<=', 2: '==', 3: '>=', 4: '>', 5: 'NaN')
5338
+ :param opval: value to apply the operation with
5339
+ :param condval: value to compare with
5340
+ """
4733
5341
  # operationChoices = [ u"+", u"-", u"*", u"/", u"replace'" ]
4734
5342
  # conditionChoices = [ u"<", u"<=", u"=", u">=", u">",u"isNaN" ]
4735
5343
  def test(val, cond, condval):
@@ -4770,7 +5378,7 @@ class SelectionData():
4770
5378
  logging.error(_('Unknown dtype in treat_select !'))
4771
5379
  return
4772
5380
 
4773
- if self.myselection == 'all':
5381
+ if self.myselection == ALL_SELECTED:
4774
5382
  if op == 0:
4775
5383
  if cond == 0:
4776
5384
  # <
@@ -4933,7 +5541,15 @@ class SelectionData():
4933
5541
 
4934
5542
  self.parent.reset_plot()
4935
5543
 
4936
- def mask_condition(self, op, cond, opval, condval):
5544
+ def mask_condition(self, op:int, cond:int, opval, condval):
5545
+ """ Mask nodes based on a condition.
5546
+
5547
+ :param op: operation to apply (0: '+', 1: '-', 2: '*', 3: '/', 4: 'replace')
5548
+ :param cond: condition to apply (0: '<', 1: '<=', 2: '==', 3: '>=', 4: '>', 5: 'NaN')
5549
+ :param opval: value to apply the operation with
5550
+ :param condval: value to compare with
5551
+ """
5552
+
4937
5553
  # operationChoices = [ u"+", u"-", u"*", u"/", u"replace'" ]
4938
5554
  # conditionChoices = [ u"<", u"<=", u"=", u">=", u">",u"isNaN" ]
4939
5555
  def test(val, cond, condval):
@@ -4974,7 +5590,7 @@ class SelectionData():
4974
5590
  logging.error(_('Unknown dtype in treat_select !'))
4975
5591
  return
4976
5592
 
4977
- if self.myselection == 'all':
5593
+ if self.myselection == ALL_SELECTED:
4978
5594
  if cond == 0:
4979
5595
  # <
4980
5596
  ind = np.argwhere(np.logical_and(array < condval, np.logical_not(array.mask)))
@@ -4997,35 +5613,42 @@ class SelectionData():
4997
5613
  array.mask[ind[:, 0], ind[:, 1]] = True
4998
5614
 
4999
5615
  else:
5000
- ij = [self.parent.get_ij_from_xy(cur[0], cur[1]) for cur in self.myselection]
5616
+ npself = np.asarray(self.myselection)
5617
+ ij = self.parent.xy2ij_np(npself)
5618
+ array.mask[ij[:, 0], ij[:, 1]] = test(array.data[ij[:, 0], ij[:, 1]], cond, condval)
5619
+
5620
+ # ij = [self.parent.get_ij_from_xy(cur[0], cur[1]) for cur in self.myselection]
5001
5621
 
5002
- for i, j in ij:
5003
- if test(array.data[i, j], cond, condval):
5004
- array.mask[i, j] = True
5622
+ # for i, j in ij:
5623
+ # if test(array.data[i, j], cond, condval):
5624
+ # array.mask[i, j] = True
5005
5625
 
5006
5626
  self.parent.nbnotnull = array.count()
5007
5627
  self.parent.updatepalette()
5008
5628
  self.parent.delete_lists()
5009
5629
 
5010
- def get_values_sel(self):
5630
+ def get_values_sel(self) -> np.ndarray:
5631
+ """ Get the values of the selected nodes.
5011
5632
 
5012
- if self.myselection == 'all':
5633
+ :return: numpy array of values of the selected nodes, or -99999 if all nodes are selected.
5634
+ """
5635
+
5636
+ if self.myselection == ALL_SELECTED:
5013
5637
  return -99999
5014
5638
  else:
5015
5639
  sel = np.asarray(self.myselection)
5016
5640
  if len(sel) == 1:
5017
- ijall = np.asarray(self.parent.get_ij_from_xy(sel[0, 0], sel[0, 1])).transpose()
5641
+ ijall = self.parent.xy2ij_np(sel)
5018
5642
  z = self.parent.array[ijall[0], ijall[1]]
5019
5643
  else:
5020
- ijall = np.asarray(self.parent.get_ij_from_xy(sel[:, 0], sel[:, 1])).transpose()
5644
+ ijall = self.parent.xy2ij_np(sel)
5021
5645
  z = self.parent.array[ijall[:, 0], ijall[:, 1]].flatten()
5022
5646
 
5023
5647
  return z
5024
5648
 
5025
- def _get_header(self):
5649
+ def _get_header(self) -> header_wolf | None:
5026
5650
  """ Header corresponding to the selection """
5027
5651
 
5028
- array = self.parent
5029
5652
  sel = np.asarray(self.myselection)
5030
5653
 
5031
5654
  myhead = header_wolf()
@@ -5050,13 +5673,17 @@ class SelectionData():
5050
5673
 
5051
5674
  return myhead
5052
5675
 
5053
- def get_newarray(self):
5054
- """ Create a new array from the selection """
5676
+ def get_newarray(self) -> "WolfArray":
5677
+ """ Create a new array from the selection.
5678
+
5679
+ :return: a new WolfArray object containing the selected nodes, or None if the selection is empty. Null values are set to -99999.
5680
+ If all nodes are selected, returns a WolfArray molded from the parent.
5681
+ """
5055
5682
 
5056
5683
  if self.nb == 0:
5057
5684
  return None
5058
5685
 
5059
- if self.myselection == 'all':
5686
+ if self.myselection == ALL_SELECTED:
5060
5687
  return WolfArray(mold=self.parent)
5061
5688
 
5062
5689
  newarray = WolfArray()
@@ -5069,10 +5696,12 @@ class SelectionData():
5069
5696
 
5070
5697
  sel = np.asarray(self.myselection)
5071
5698
  if len(sel) == 1:
5072
- ijall = np.asarray(self.parent.get_ij_from_xy(sel[0, 0], sel[0, 1])).transpose()
5699
+ # ijall = np.asarray(self.parent.get_ij_from_xy(sel[0, 0], sel[0, 1])).transpose()
5700
+ ijall = self.parent.xy2ij_np(sel)
5073
5701
  z = self.parent.array[ijall[0], ijall[1]]
5074
5702
  else:
5075
5703
  ijall = np.asarray(self.parent.get_ij_from_xy(sel[:, 0], sel[:, 1])).transpose()
5704
+ ijall = self.parent.xy2ij_np(sel)
5076
5705
  z = self.parent.array[ijall[:, 0], ijall[:, 1]].flatten()
5077
5706
 
5078
5707
  newarray.array[:, :] = -99999.
@@ -5083,10 +5712,12 @@ class SelectionData():
5083
5712
  return newarray
5084
5713
 
5085
5714
  def select_all(self):
5086
- """ Select all nodes """
5715
+ """ Select all nodes.
5087
5716
 
5088
- self.myselection = 'all'
5089
- self.update_nb_nodes_selection()
5717
+ It will set the selection to ALL_SELECTED, which is a special value indicating that all nodes are selected.
5718
+ """
5719
+
5720
+ self.myselection = ALL_SELECTED
5090
5721
 
5091
5722
  def interp2Dpolygons(self, working_zone:zone,
5092
5723
  method:Literal["nearest", "linear", "cubic"] = None,
@@ -5095,6 +5726,10 @@ class SelectionData():
5095
5726
  """
5096
5727
  Interpolation sous tous les polygones d'une zone
5097
5728
  cf parent.interp2Dpolygon
5729
+
5730
+ Useful for WX GUI, where the method is chosen by the user.
5731
+ From script, you can call this method directly from the parent array
5732
+ with the desired method and keep parameters.
5098
5733
  """
5099
5734
 
5100
5735
  if method is None:
@@ -5120,6 +5755,10 @@ class SelectionData():
5120
5755
  """
5121
5756
  Interpolation sous un polygone
5122
5757
  cf parent.interp2Dpolygon
5758
+
5759
+ Useful for WX GUI, where the method is chosen by the user.
5760
+ From script, you can call this method directly from the parent array
5761
+ with the desired method and keep parameters.
5123
5762
  """
5124
5763
 
5125
5764
  if method is None:
@@ -5142,6 +5781,10 @@ class SelectionData():
5142
5781
  """
5143
5782
  Interpolation sous toutes les polylignes de la zone
5144
5783
  cf parent.interp2Dpolyline
5784
+
5785
+ Useful for WX GUI, where the method is chosen by the user.
5786
+ From script, you can call this method directly from the parent array
5787
+ with the desired method and keep parameters.
5145
5788
  """
5146
5789
 
5147
5790
  self.parent.interpolate_on_polylines(working_zone)
@@ -5153,6 +5796,10 @@ class SelectionData():
5153
5796
  """
5154
5797
  Interpolation sous la polyligne active
5155
5798
  cf parent.interp2Dpolyline
5799
+
5800
+ Useful for WX GUI, where the method is chosen by the user.
5801
+ From script, you can call this method directly from the parent array
5802
+ with the desired method and keep parameters.
5156
5803
  """
5157
5804
 
5158
5805
  self.parent.interpolate_on_polyline(working_vector)
@@ -5160,14 +5807,31 @@ class SelectionData():
5160
5807
  if resetplot:
5161
5808
  self.parent.reset_plot()
5162
5809
 
5163
-
5164
5810
  def copy(self, source:"SelectionData"):
5811
+ """ Copy the selection data from another SelectionData object.
5812
+
5813
+ :param source: another SelectionData object to copy from.
5814
+ """
5815
+
5816
+ if source._storage_mode == StorageMode.ARRAY:
5817
+ self._storage_mode = StorageMode.ARRAY
5818
+ self._myselection_as_array = source._myselection_as_array.copy()
5165
5819
 
5166
- self.myselection = source.myselection.copy()
5820
+ elif source._storage_mode == StorageMode.LIST:
5821
+ self._storage_mode = StorageMode.LIST
5822
+ self._myselection = source._myselection.copy()
5167
5823
 
5168
5824
  def volumesurface(self, show=True):
5169
5825
  """
5170
- Evaluation of stage-storage-surface relation
5826
+ Evaluation of stage-storage-surface relation from "volume_estimation" routine of the WolfArray.
5827
+
5828
+ If the selection is empty, it will use the whole array.
5829
+ If the selection is ALL_SELECTED, it will use the whole array.
5830
+ If the selection is not empty, it will use the selected nodes only -- see "get_newarray" routine.
5831
+
5832
+ If the parent array is linked to a MapViewer, it will apply the same operation to all linked active arrays.
5833
+
5834
+ :param show: if True, the figure will be shown.
5171
5835
  """
5172
5836
 
5173
5837
  if self.parent.get_mapviewer() is not None:
@@ -5176,38 +5840,36 @@ class SelectionData():
5176
5840
 
5177
5841
  if mapviewer.linked:
5178
5842
 
5179
- array1:WolfArray = mapviewer.linkedList[0].active_array
5180
- array2:WolfArray = mapviewer.linkedList[1].active_array
5181
-
5182
- # transfert des mailles sélectionnées dans l'autre matrice
5183
- if array1 is self.parent:
5184
- array2.SelectionData.copy(array1.SelectionData)
5185
-
5186
- if array2 is self.parent:
5187
- array1.SelectionData.copy(array2.SelectionData)
5188
-
5189
- if self.nb == 0 or self.myselection == 'all':
5190
- myarray = array1
5191
- fig, axs = myarray.volume_estimation()
5192
-
5193
- myarray = array2
5194
- fig, axs = myarray.volume_estimation(axs)
5843
+ # If linked arrays, we copy the selection data to all linked arrays
5844
+ arrays = [cur.active_array for cur in mapviewer.linkedList]
5845
+ for cur in arrays:
5846
+ if cur is not self.parent:
5847
+ cur.SelectionData.copy(self.parent.SelectionData)
5848
+
5849
+ if self.nb == 0 or self.myselection == ALL_SELECTED:
5850
+ for cur in arrays:
5851
+ fig, axs = cur.volume_estimation()
5852
+ if show:
5853
+ fig.show()
5195
5854
  else:
5196
- myarray = array1.mngselection.get_newarray()
5197
- fig, axs = myarray.volume_estimation()
5198
-
5199
- myarray = array2.mngselection.get_newarray()
5200
- fig, axs = myarray.volume_estimation(axs)
5855
+ for cur in arrays:
5856
+ myarray = cur.SelectionData.get_newarray()
5857
+ fig, axs = myarray.volume_estimation()
5858
+ if show:
5859
+ fig.show()
5201
5860
  else:
5202
- if len(self.parent.mngselection.myselection) == 0 or self.parent.mngselection.myselection == 'all':
5861
+ if len(self.parent.SelectionData.myselection) == 0 or self.parent.SelectionData.myselection == ALL_SELECTED:
5203
5862
  myarray = self.parent
5204
5863
  else:
5205
- myarray = self.parent.mngselection.get_newarray()
5206
- myarray.SelectionData.selections = self.parent.mngselection.selections.copy()
5864
+ myarray = self.parent.SelectionData.get_newarray()
5865
+ myarray.SelectionData.selections = self.parent.SelectionData.selections.copy()
5207
5866
 
5208
5867
  fig, axs = myarray.volume_estimation()
5868
+
5869
+ if show:
5870
+ fig.show()
5209
5871
  else:
5210
- if self.nb == 0 or self.myselection == 'all':
5872
+ if self.nb == 0 or self.myselection == ALL_SELECTED:
5211
5873
  myarray = self.parent
5212
5874
  else:
5213
5875
  myarray = self.get_newarray()
@@ -5215,11 +5877,16 @@ class SelectionData():
5215
5877
 
5216
5878
  fig, axs = myarray.volume_estimation()
5217
5879
 
5218
- if show:
5219
- fig.show()
5880
+ if show:
5881
+ fig.show()
5220
5882
 
5221
5883
  class SelectionDataMB(SelectionData):
5222
- """ Extension of SelectionData to manage multiple blocks """
5884
+ """ Extension of SelectionData to manage multiple blocks.
5885
+
5886
+ All routines are not implemented yet, as they depend on the specificities of the blocks.
5887
+
5888
+ For the rest, it is mainly a simple wrapper around the SelectionData of each block.
5889
+ """
5223
5890
 
5224
5891
  def __init__(self, parent:"WolfArrayMB"):
5225
5892
  SelectionData.__init__(self, parent)
@@ -5231,10 +5898,10 @@ class SelectionDataMB(SelectionData):
5231
5898
 
5232
5899
  return np.sum([cur.SelectionData.nb for cur in self.parent.active_blocks])
5233
5900
 
5234
- def Unmasksel(self):
5901
+ def unmask_selection(self):
5235
5902
 
5236
5903
  for curblock in self.parent.active_blocks:
5237
- curblock.SelectionData.Unmasksel(resetplot=False)
5904
+ curblock.SelectionData.unmask_selection(resetplot=False)
5238
5905
 
5239
5906
  self.parent.reset_plot()
5240
5907
 
@@ -5280,7 +5947,7 @@ class SelectionDataMB(SelectionData):
5280
5947
  for curblock in self.parent.active_blocks:
5281
5948
  ret = curblock.SelectionData.add_node_to_selection(x, y, verif)
5282
5949
 
5283
- def add_nodes_to_selection(self, xy:list[float], verif:bool=True):
5950
+ def add_nodes_to_selection(self, xy:list[float] | np.ndarray, verif:bool=True):
5284
5951
  """ Add nodes to the selection """
5285
5952
 
5286
5953
  for curblock in self.parent.active_blocks:
@@ -5686,6 +6353,14 @@ class WolfArray(Element_To_Draw, header_wolf):
5686
6353
 
5687
6354
  self.add_ops_sel() # Ajout d'un gestionnaire de sélection et d'opérations
5688
6355
 
6356
+ @property
6357
+ def usemask(self):
6358
+ """ Check if we want to use the mask in the operations """
6359
+ if self.myops is not None:
6360
+ return self.myops.usemask
6361
+ else:
6362
+ return False
6363
+
5689
6364
  def __getstate__(self):
5690
6365
  """ Récupération de l'état de l'objet pour la sérialisation """
5691
6366
  state = self.__dict__.copy()
@@ -5800,8 +6475,8 @@ class WolfArray(Element_To_Draw, header_wolf):
5800
6475
  """ Get the centers of the cells """
5801
6476
 
5802
6477
  if usenap:
5803
- ij = np.where(self.array.mask==False,)
5804
- xy = self.get_xy_from_ij_array(np.vstack((ij[0], ij[1])).T).copy().flatten()
6478
+ ij = np.argwhere(self.array.mask==False,)
6479
+ xy = self.get_xy_from_ij_array(ij).copy().flatten()
5805
6480
  else:
5806
6481
  ij = np.meshgrid(np.arange(self.nbx), np.arange(self.nby))
5807
6482
  ij = np.asarray([ij[0].flatten(), ij[1].flatten()]).T
@@ -6182,7 +6857,7 @@ class WolfArray(Element_To_Draw, header_wolf):
6182
6857
  logging.info(_('No selection -- no filtering'))
6183
6858
  return
6184
6859
 
6185
- if self.SelectionData.myselection == 'all':
6860
+ if self.SelectionData.myselection == ALL_SELECTED:
6186
6861
  logging.info(_('All nodes selected -- no filtering'))
6187
6862
  return
6188
6863
 
@@ -6282,7 +6957,7 @@ class WolfArray(Element_To_Draw, header_wolf):
6282
6957
 
6283
6958
  vals = self.array[ij[:,0], ij[:,1]]
6284
6959
 
6285
- elif self.SelectionData.nb == 0 or self.SelectionData.myselection == 'all':
6960
+ elif self.SelectionData.nb == 0 or self.SelectionData.myselection == ALL_SELECTED:
6286
6961
  logging.info(_('No selection -- statistics on the whole array'))
6287
6962
  vals = self.array[~self.array.mask].ravel().data # all values
6288
6963
 
@@ -7150,7 +7825,7 @@ class WolfArray(Element_To_Draw, header_wolf):
7150
7825
  See : https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.griddata.html
7151
7826
  """
7152
7827
 
7153
- if self.mngselection.myselection == [] or self.mngselection.myselection == 'all':
7828
+ if self.mngselection.myselection == [] or self.mngselection.myselection == ALL_SELECTED:
7154
7829
 
7155
7830
  decalx = self.origx + self.translx
7156
7831
  decaly = self.origy + self.transly
@@ -7210,7 +7885,7 @@ class WolfArray(Element_To_Draw, header_wolf):
7210
7885
 
7211
7886
  try:
7212
7887
  if self.mngselection is not None:
7213
- if self.mngselection.myselection != [] and self.mngselection.myselection != 'all':
7888
+ if self.mngselection.myselection != [] and self.mngselection.myselection != ALL_SELECTED:
7214
7889
  # interpolation only in the selected cells
7215
7890
 
7216
7891
  # Convert coordinates to indices
@@ -7268,7 +7943,7 @@ class WolfArray(Element_To_Draw, header_wolf):
7268
7943
 
7269
7944
  self.array.data[ij[:, 0], ij[:, 1]] = newvalues
7270
7945
 
7271
- elif self.mngselection.myselection == 'all' and (grid_x is None and grid_y is None):
7946
+ elif self.mngselection.myselection == ALL_SELECTED and (grid_x is None and grid_y is None):
7272
7947
  # interpolation in all the cells
7273
7948
 
7274
7949
  # Creating a grid for all cells
@@ -7918,6 +8593,248 @@ class WolfArray(Element_To_Draw, header_wolf):
7918
8593
 
7919
8594
  return fig, axs
7920
8595
 
8596
+ def surface_volume_estimation_from_elevation(self, axs:plt.Axes= None,
8597
+ desired_zmin:float = None, desired_zmax:float = None,
8598
+ nb:int = 100,
8599
+ method:Literal['all below', 'largest area', 'selected'] = 'largest area',
8600
+ selected_cells:list[tuple[float, float]] = None,
8601
+ dirout:str | Path = None,
8602
+ invert_z:bool = False,
8603
+ array_to_integrate:"WolfArray" = None
8604
+ ):
8605
+ """ Estimation of the surface and volume from an elevation array.
8606
+
8607
+ :param axs: Axes to plot the results
8608
+ :param desired_zmin: Minimum elevation to consider
8609
+ :param desired_zmax: Maximum elevation to consider
8610
+ :param nb: Number of elevation steps
8611
+ :param method: Method to use for the estimation ('all below', 'largest area', 'selected')
8612
+ :param selected_cells: List of kernel cells (if method is 'selected') -- Consider only the areas containing these cells
8613
+ :param dirout: Directory to save the results
8614
+ :return: Figure and Axes with the results
8615
+ :rtype: tuple[plt.Figure, plt.Axes]
8616
+ """
8617
+
8618
+ assert method in ['all below', 'largest area', 'selected'], _('Method must be one of "all below", "largest area", or "selected"')
8619
+ if array_to_integrate is not None:
8620
+ assert isinstance(array_to_integrate, WolfArray), _('array_to_integrate must be a WolfArray instance')
8621
+ assert array_to_integrate.nbx == self.nbx and array_to_integrate.nby == self.nby, _('array_to_integrate must have the same dimensions as the current WolfArray')
8622
+
8623
+ vect = self.array[np.logical_not(self.array.mask)].flatten()
8624
+ zmin = np.amin(vect)
8625
+ zmax = np.amax(vect)
8626
+
8627
+ if desired_zmin is not None:
8628
+ zmin = desired_zmin
8629
+
8630
+ if desired_zmax is not None:
8631
+ zmax = desired_zmax
8632
+
8633
+ deltaz = (zmax - zmin) / nb
8634
+
8635
+ curz = zmin
8636
+ labeled_areas = []
8637
+ stockage = []
8638
+ z = []
8639
+
8640
+ if method == 'all below':
8641
+ labeled = False
8642
+ else:
8643
+ labeled = True
8644
+ if method == 'largest area':
8645
+ use_memory = False
8646
+ else:
8647
+ xy_key = np.asarray(selected_cells, dtype=np.float64)
8648
+ if xy_key is None:
8649
+ logging.error(_('Empty selection'))
8650
+ return
8651
+
8652
+ if len(xy_key) == 0:
8653
+ logging.error(_('Empty selection'))
8654
+ return
8655
+
8656
+ ij_key = self.xy2ij_np(xy_key)
8657
+
8658
+ use_memory = True
8659
+
8660
+ extensionmax = WolfArray(mold=self)
8661
+ extensionmax.array[:, :] = 0.
8662
+
8663
+ if labeled:
8664
+ for i in tqdm(range(nb + 1)):
8665
+ z.append(curz)
8666
+
8667
+ # Compute difference between the array and the current elevation
8668
+ if i == 0:
8669
+ diff = self.array - (curz + 1.e-3)
8670
+ else:
8671
+ diff = self.array - curz
8672
+
8673
+ # Keep only the negative values
8674
+ diff[diff > 0] = 0.
8675
+ diff.data[diff.mask] = 0.
8676
+
8677
+ if np.count_nonzero(diff < 0.) > 1:
8678
+ # More than one cell below the elevation
8679
+
8680
+ # Labeling of the cells below the elevation
8681
+ labeled_array, num_features = label(diff.data)
8682
+ # Applying the same mask as the original array
8683
+ labeled_array = ma.asarray(labeled_array)
8684
+ labeled_array.mask = self.array.mask
8685
+
8686
+ if use_memory:
8687
+ # Use only the labeled areas containing the cells stored in selected_cells
8688
+ labeled_areas = []
8689
+ for curij in ij_key:
8690
+ labeled_areas.append(labeled_array[curij[0], curij[1]])
8691
+
8692
+ # Remove masked value
8693
+ labeled_areas = [x for x in labeled_areas if x is not ma.masked]
8694
+ # Remove duplicates
8695
+ labeled_areas = list(set(labeled_areas))
8696
+
8697
+ for curarea in labeled_areas:
8698
+ if curarea == 0:
8699
+ volume = 0.
8700
+ surface = 0.
8701
+ continue
8702
+
8703
+ # Search
8704
+ mask = labeled_array == curarea
8705
+ area = np.argwhere(mask)
8706
+
8707
+ if array_to_integrate is None:
8708
+ volume = -self.dx * self.dy * np.ma.sum(diff[area])
8709
+ else:
8710
+ volume = self.dx * self.dy * np.ma.sum(array_to_integrate.array[area[:,0], area[:,1]])
8711
+
8712
+ surface = self.dx * self.dy * area.shape[0]
8713
+ extensionmax.array[np.logical_and(mask, extensionmax.array == 0.)] = float(i + 1)
8714
+
8715
+ else:
8716
+ labeled_areas = list(sum_labels(np.ones(labeled_array.shape, dtype=np.int32), labeled_array, range(1, num_features+1)))
8717
+ labeled_areas = [[x, y] for x, y in zip(labeled_areas, range(1, num_features+1))]
8718
+ labeled_areas.sort(key=lambda x: x[0], reverse=True)
8719
+ jmax = labeled_areas[0][1]
8720
+ nbmax = labeled_areas[0][0]
8721
+
8722
+ if array_to_integrate is None:
8723
+ volume = -self.dx * self.dy * np.ma.sum(diff[labeled_array == jmax])
8724
+ else:
8725
+ volume = self.dx * self.dy * np.ma.sum(array_to_integrate.array[labeled_array == jmax])
8726
+
8727
+ surface = self.dx * self.dy * nbmax
8728
+ extensionmax.array[np.logical_and(labeled_array == jmax, extensionmax.array == 0.)] = float(i + 1)
8729
+ else:
8730
+ # Only one cell below the elevation
8731
+ if array_to_integrate is None:
8732
+ volume = -self.dx * self.dy * np.ma.sum(diff)
8733
+ else:
8734
+ volume = self.dx * self.dy * np.ma.sum(array_to_integrate.array[diff < 0.])
8735
+
8736
+ surface = self.dx * self.dy * np.count_nonzero(diff<0.)
8737
+ extensionmax.array[np.logical_and(diff[:,:]<0., extensionmax.array[:, :] == 0.)] = float(i + 1)
8738
+
8739
+ stockage.append([abs(volume), abs(surface)])
8740
+ curz += deltaz
8741
+
8742
+ else:
8743
+ for i in tqdm(range(nb + 1)):
8744
+ z.append(curz)
8745
+
8746
+ if i == 0:
8747
+ diff = self.array - (curz + 1.e-3)
8748
+ else:
8749
+ diff = self.array - curz
8750
+
8751
+ diff[diff > 0] = 0.
8752
+ diff.data[diff.mask] = 0.
8753
+
8754
+ if array_to_integrate is None:
8755
+ volume = -self.dx * self.dy * np.ma.sum(diff)
8756
+ surface = self.dx * self.dy * np.count_nonzero(diff<0.)
8757
+ stockage.append([abs(volume), abs(surface)])
8758
+ else:
8759
+ ij = np.argwhere(diff < 0.)
8760
+ volume = self.dx * self.dy * np.ma.sum(array_to_integrate.array[ij[:,0], ij[:,1]])
8761
+ surface = self.dx * self.dy * ij.shape[0]
8762
+ stockage.append([volume, abs(surface)])
8763
+
8764
+ curz += deltaz
8765
+
8766
+ extensionmax.array[np.logical_and(diff[:,:]<0., extensionmax.array[:, :] == 0.)] = float(i + 1)
8767
+
8768
+ if dirout is None:
8769
+ dirout = Path(self.filename).parent / 'surface_volume'
8770
+
8771
+ if isinstance(dirout, str):
8772
+ dirout = Path(dirout)
8773
+ if not dirout.exists():
8774
+ dirout.mkdir(parents=True, exist_ok=True)
8775
+
8776
+ extensionmax.filename = str(dirout / 'surface_volume_extension.tif')
8777
+ extensionmax.write_all()
8778
+
8779
+ if axs is None:
8780
+ fig, axs = plt.subplots(1, 2, tight_layout=True)
8781
+ else:
8782
+ fig = axs[0].get_figure()
8783
+
8784
+ if invert_z:
8785
+ z = [abs(zmin - x) for x in z]
8786
+ labelx = _("Water depth [m]")
8787
+ else:
8788
+ labelx = _("Elevation [m]")
8789
+
8790
+ axs[0].plot(z, [x[0] for x in stockage])
8791
+ axs[0].scatter(z, [x[0] for x in stockage])
8792
+ axs[0].set_xlabel(labelx, size=10)
8793
+ axs[0].set_ylabel(_("Volume [$m^3$]"), size=10)
8794
+ axs[1].step(z, [x[1] for x in stockage], where='post')
8795
+ axs[1].scatter(z, [x[1] for x in stockage])
8796
+ axs[1].set_xlabel(labelx, size=10)
8797
+ axs[1].set_ylabel(_("Surface [$m^2$]"), size=10)
8798
+ fig.suptitle(_("Retention capacity"), fontsize=12)
8799
+
8800
+ fig.savefig(dirout / 'surface_volume.png', dpi=300)
8801
+
8802
+ fn = dirout / 'surface_volume_hvs.txt'
8803
+ with open(fn, 'w') as f:
8804
+ f.write('H [m]\tZ [m DNG]\tVolume [$m^3$]\tSurface [$m^2$]\n')
8805
+ for curz, (curv, curs) in zip(z, stockage):
8806
+ f.write('{}\t{}\t{}\t{}\n'.format(curz - zmin, curz, curv, curs))
8807
+
8808
+ return fig, axs
8809
+
8810
+ def surface_volume_estimation_from_waterdepth(self, axs:plt.Axes=None,
8811
+ desired_zmin:float = None, desired_zmax:float = None,
8812
+ nb:int = 100,
8813
+ method:Literal['all below', 'largest area', 'selected'] = 'largest',
8814
+ selected_cells:list[tuple[float, float]] = None,
8815
+ dirout:str | Path = None):
8816
+ """ Estimation of the surface and volume from a water depth array.
8817
+ :param axs: Axes to plot the results
8818
+ :param desired_zmin: Minimum water depth to consider
8819
+ :param desired_zmax: Maximum water depth to consider
8820
+ :param nb: Number of water depth steps
8821
+ :param method: Method to use for the estimation ('all below', 'largest area', 'selected')
8822
+ :param selected_cells: List of kernel cells (if method is 'selected') -- Consider only the areas containing these cells
8823
+ :param dirout: Directory to save the results
8824
+ :return: Figure and Axes with the results
8825
+ :rtype: tuple[plt.Figure, plt.Axes]
8826
+ """
8827
+
8828
+ neg_array = WolfArray(mold=self)
8829
+ neg_array.array[:,:] = -self.array
8830
+ return neg_array.surface_volume_estimation_from_elevation(desired_zmin=-desired_zmax if desired_zmax is not None else None,
8831
+ desired_zmax=-desired_zmin if desired_zmin is not None else None,
8832
+ nb=nb, method=method,
8833
+ selected_cells=selected_cells,
8834
+ dirout=dirout if dirout is not None else Path(self.filename).parent / 'surface_volume',
8835
+ axs=axs, invert_z=True)
8836
+
8837
+
7921
8838
  def paste_all(self, fromarray:"WolfArray", mask_after:bool=True):
7922
8839
  """ Paste all the values from another WolfArray """
7923
8840
 
@@ -7972,14 +8889,17 @@ class WolfArray(Element_To_Draw, header_wolf):
7972
8889
  z = np.asarray(z)
7973
8890
 
7974
8891
  if len(sel) == 1:
7975
- ijall = np.asarray(self.get_ij_from_xy(sel[0, 0], sel[0, 1])).transpose()
7976
- i = ijall[0]
7977
- j = ijall[1]
8892
+ ijall = self.xy2ij_np(sel)
7978
8893
 
7979
- if i > 0 and i < self.nbx and j > 0 and j < self.nby:
8894
+ i, j = ijall[0, 0], ijall[0, 1]
8895
+ if i < 0 or i >= self.nbx or j < 0 or j >= self.nby:
8896
+ logging.error(_('Selected point is out of bounds'))
8897
+ return
8898
+ else:
7980
8899
  self.array[i, j] = z
7981
8900
  else:
7982
- ijall = np.asarray(self.get_ij_from_xy(sel[:, 0], sel[:, 1])).transpose()
8901
+ # ijall = np.asarray(self.get_ij_from_xy(sel[:, 0], sel[:, 1])).transpose()
8902
+ ijall = self.xy2ij_np(sel)
7983
8903
 
7984
8904
  useful = np.where((ijall[:, 0] >= 0) & (ijall[:, 0] < self.nbx) & (ijall[:, 1] >= 0) & (ijall[:, 1] < self.nby))
7985
8905
 
@@ -8056,7 +8976,7 @@ class WolfArray(Element_To_Draw, header_wolf):
8056
8976
  ij = [self.get_ij_from_xy(cur[0], cur[1]) for cur in curlist]
8057
8977
  z = [self.array.data[curij[0], curij[1]] for curij in ij]
8058
8978
 
8059
- if cursel == 'all':
8979
+ if cursel == ALL_SELECTED:
8060
8980
  xall = np.linspace(self.origx + self.dx / 2., self.origx + (float(self.nbx) - .5) * self.dx,
8061
8981
  self.nbx)
8062
8982
  yall = np.linspace(self.origy + self.dy / 2., self.origy + (float(self.nby) - .5) * self.dy,
@@ -8549,12 +9469,16 @@ class WolfArray(Element_To_Draw, header_wolf):
8549
9469
  # trouve les indices dans le polygone
8550
9470
  for myvect in myvects:
8551
9471
  myij = self.get_ij_inside_polygon(myvect, usemask=False, eps=eps, method=method)
8552
- # masquage des mailles contenues
8553
- self.array.mask[myij[:,0],myij[:,1]] = True
8554
9472
 
8555
- if set_nullvalue:
8556
- # annulation des valeurs en dehors du polygone
8557
- self.array.data[myij[:,0],myij[:,1]] = self.nullvalue
9473
+ if myij.shape[0] == 0:
9474
+ logging.warning(_("No indices found in the polygon {}").format(myvect.myname))
9475
+ else:
9476
+ # masquage des mailles contenues
9477
+ self.array.mask[myij[:,0],myij[:,1]] = True
9478
+
9479
+ if set_nullvalue:
9480
+ # annulation des valeurs en dehors du polygone
9481
+ self.array.data[myij[:,0],myij[:,1]] = self.nullvalue
8558
9482
 
8559
9483
  self.count()
8560
9484
 
@@ -9242,6 +10166,8 @@ class WolfArray(Element_To_Draw, header_wolf):
9242
10166
  if newpath is not None:
9243
10167
  self.filename = newpath
9244
10168
 
10169
+ self.filename = str(self.filename)
10170
+
9245
10171
  if self.filename.endswith('.tif') or self.filename.endswith('.tiff'):
9246
10172
  self.export_geotif(EPSG=EPSG)
9247
10173
  elif self.filename.endswith('.npy'):
@@ -11830,6 +12756,8 @@ class WolfArrayMB(WolfArray):
11830
12756
  """
11831
12757
  Adds a properly configured block this multiblock.
11832
12758
 
12759
+ Do not forget to call "set_header_from_added_blocks" after adding all blocks.
12760
+
11833
12761
  :param arr: The block to add.
11834
12762
  :param force_idx: If True, the index/key will be set on `arr`. If False, the index/key must already be set on `arr`.
11835
12763
  """