wolfhece 2.1.127__py3-none-any.whl → 2.1.129__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
@@ -43,6 +43,7 @@ from scipy.ndimage import laplace, label, sum_labels
43
43
  import pygltflib
44
44
  from shapely.geometry import Point, LineString, MultiLineString, Polygon, MultiPolygon, MultiPoint
45
45
  from shapely.ops import linemerge, substring, polygonize_full
46
+ from shapely import contains, contains_properly, contains_xy, touches, prepare, destroy_prepared, is_prepared
46
47
  from os.path import dirname,basename,join
47
48
  import logging
48
49
  from typing import Literal
@@ -379,7 +380,51 @@ class header_wolf():
379
380
 
380
381
  self.head_blocks[_key] = deepcopy(value)
381
382
 
382
- def set_origin(self, x:float, y:float, z:float):
383
+ @property
384
+ def resolution(self):
385
+ return self.get_resolution()
386
+
387
+ @resolution.setter
388
+ def resolution(self, value:tuple[float]):
389
+
390
+ if len(value) == 2:
391
+ self.set_resolution(value[0], value[1])
392
+ elif len(value) == 3:
393
+ self.set_resolution(value[0], value[1], value[2])
394
+
395
+ @property
396
+ def origin(self):
397
+ return self.get_origin()
398
+
399
+ @origin.setter
400
+ def origin(self, value:tuple[float]):
401
+
402
+ if len(value) == 2:
403
+ self.set_origin(value[0], value[1])
404
+ elif len(value) == 3:
405
+ self.set_origin(value[0], value[1], value[2])
406
+
407
+ @property
408
+ def translation(self):
409
+ return self.get_translation()
410
+
411
+ @translation.setter
412
+ def translation(self, value:tuple[float]):
413
+ if len(value) == 2:
414
+ self.set_translation(value[0], value[1])
415
+ elif len(value) == 3:
416
+ self.set_translation(value[0], value[1], value[2])
417
+
418
+ def set_resolution(self, dx:float, dy:float, dz:float= 0.):
419
+ """
420
+ Set resolution
421
+ """
422
+
423
+ self.dx = dx
424
+ self.dy = dy
425
+ self.dz = dz
426
+
427
+ def set_origin(self, x:float, y:float, z:float = 0.):
383
428
  """
384
429
  Set origin
385
430
 
@@ -391,7 +436,28 @@ class header_wolf():
391
436
  self.origy = y
392
437
  self.origz = z
393
438
 
394
- def set_translation(self, tr_x:float, tr_y:float, tr_z:float):
439
+ def get_origin(self):
440
+ """
441
+ Return origin
442
+ """
443
+
444
+ return (self.origx, self.origy, self.origz)
445
+
446
+ def get_resolution(self):
447
+ """
448
+ Return resolution
449
+ """
450
+
451
+ return (self.dx, self.dy, self.dz)
452
+
453
+ def get_translation(self):
454
+ """
455
+ Return translation
456
+ """
457
+
458
+ return (self.translx, self.transly, self.translz)
459
+
460
+ def set_translation(self, tr_x:float, tr_y:float, tr_z:float= 0.):
395
461
  """
396
462
  Set translation
397
463
 
@@ -576,6 +642,44 @@ class header_wolf():
576
642
  else:
577
643
  raise Exception(_("The number of coordinates is not correct"))
578
644
 
645
+ def transform(self):
646
+ """ Return the affine transformation.
647
+
648
+ Similar to rasterio.transform.Affine
649
+
650
+ In WOLF, the convention is :
651
+ - origin is at the lower-left corner
652
+ - the origin is at the corner of the cell dx, dy, so the center of the cell is at dx/2, dy/2
653
+ - X axis is along the rows - i index
654
+ - Y axis is along the columns - j index
655
+
656
+ So, the affine transformation is :
657
+ (dx, 0, origx + translx + dx /2, 0, dy, origy + transly + dy/2)
658
+ """
659
+ from rasterio.transform import Affine
660
+ return Affine(self.dx, 0, self.origx + self.translx + self.dx / 2,
661
+ 0, self.dy, self.origy + self.transly + self.dy / 2)
662
+
663
+ def _transform_gmrio(self):
664
+ """ Return the affine transformation.
665
+
666
+ !! Inverted ij/ji convention !!
667
+
668
+ Similar to rasterio.transform.Affine
669
+
670
+ In WOLF, the convention is :
671
+ - origin is at the lower-left corner
672
+ - the origin is at the corner of the cell dx, dy, so the center of the cell is at dx/2, dy/2
673
+ - X axis is along the rows - i index
674
+ - Y axis is along the columns - j index
675
+
676
+ So, the affine transformation is :
677
+ (dx, 0, origx + translx + dx /2, 0, dy, origy + transly + dy/2)
678
+ """
679
+ from rasterio.transform import Affine
680
+ return Affine(0, self.dy, -(self.origy + self.transly),
681
+ self.dx, 0, -(self.origx + self.transly))
682
+
579
683
  def get_xy_from_ij_array(self, ij:np.ndarray, scale:float=1., aswolf:bool=False, abs:bool=True) -> np.ndarray:
580
684
  """
581
685
  Converts array coordinates (numpy cells) to this array's world coodinates.
@@ -1028,6 +1132,23 @@ class header_wolf():
1028
1132
 
1029
1133
  self.head_blocks[getkeyblock(i)] = curhead
1030
1134
 
1135
+ @classmethod
1136
+ def read_header(cls, filename:str) -> "header_wolf":
1137
+ """
1138
+ alias for read_txt_header
1139
+ """
1140
+ newhead = cls()
1141
+ newhead.read_txt_header(filename)
1142
+ return newhead
1143
+
1144
+ def write_header(self, filename:str,
1145
+ wolftype:int,
1146
+ forceupdate:bool=False):
1147
+ """
1148
+ alias for write_txt_header
1149
+ """
1150
+ self.write_txt_header(filename, wolftype, forceupdate)
1151
+
1031
1152
  def write_txt_header(self,
1032
1153
  filename:str,
1033
1154
  wolftype:int,
@@ -1920,6 +2041,8 @@ class Ops_Array(wx.Frame):
1920
2041
 
1921
2042
  self.labelling = wx.Button(self.tools, wx.ID_ANY, _("Labelling"), wx.DefaultPosition,wx.DefaultSize, 0)
1922
2043
 
2044
+ self.statistics = wx.Button(self.tools, wx.ID_ANY, _("Statistics"), wx.DefaultPosition,wx.DefaultSize, 0)
2045
+
1923
2046
  self.clean = wx.Button(self.tools, wx.ID_ANY, _("Clean"), wx.DefaultPosition,wx.DefaultSize, 0)
1924
2047
 
1925
2048
  self.extract_selection = wx.Button(self.tools, wx.ID_ANY, _("Extract selection"), wx.DefaultPosition,wx.DefaultSize, 0)
@@ -1938,6 +2061,7 @@ class Ops_Array(wx.Frame):
1938
2061
  Toolssizer.Add(self.labelling, 1, wx.EXPAND)
1939
2062
  Toolssizer.Add(self.clean, 1, wx.EXPAND)
1940
2063
  Toolssizer.Add(self.extract_selection, 1, wx.EXPAND)
2064
+ Toolssizer.Add(self.statistics, 1, wx.EXPAND)
1941
2065
  Toolssizer.Add(cont_sizer, 1, wx.EXPAND)
1942
2066
 
1943
2067
  self.ApplyTools.SetToolTip(_("Apply Nullvalue into memory/object"))
@@ -1946,6 +2070,7 @@ class Ops_Array(wx.Frame):
1946
2070
  self.labelling.SetToolTip(_("Labelling of contiguous zones using Scipy.label function\n\nReplacing the current values by the labels"))
1947
2071
  self.clean.SetToolTip(_("Clean the array\n\nRemove small isolated patches of data"))
1948
2072
  self.extract_selection.SetToolTip(_("Extract the current selection"))
2073
+ self.statistics.SetToolTip(_("Compute statistics on the array\n\nResults are displayed in a dialog box"))
1949
2074
 
1950
2075
  self.tools.SetSizer(Toolssizer)
1951
2076
  self.tools.Layout()
@@ -2318,6 +2443,7 @@ class Ops_Array(wx.Frame):
2318
2443
  self.filter_zone.Bind(wx.EVT_BUTTON, self.OnFilterZone)
2319
2444
  self.clean.Bind(wx.EVT_BUTTON, self.OnClean)
2320
2445
  self.labelling.Bind(wx.EVT_BUTTON, self.OnLabelling)
2446
+ self.statistics.Bind(wx.EVT_BUTTON, self.OnStatistics)
2321
2447
  self.extract_selection.Bind(wx.EVT_BUTTON, self.OnExtractSelection)
2322
2448
  self._contour_int.Bind(wx.EVT_BUTTON, self.OnContourInt)
2323
2449
  self._contour_list.Bind(wx.EVT_BUTTON, self.OnContourList)
@@ -2792,6 +2918,40 @@ class Ops_Array(wx.Frame):
2792
2918
 
2793
2919
  self.parentarray.labelling()
2794
2920
 
2921
+ def OnStatistics(self, event:wx.MouseEvent):
2922
+ """ Statistics on the array """
2923
+
2924
+ ret = self.parentarray.statistics()
2925
+
2926
+ ret_frame = wx.Frame(None, -1, _('Statistics of {} on selected values').format(self.parentarray.idx), size = (300, 200))
2927
+
2928
+ icon = wx.Icon()
2929
+ icon_path = Path(__file__).parent / "apps/wolf_logo2.bmp"
2930
+ icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY))
2931
+ ret_frame.SetIcon(icon)
2932
+
2933
+ ret_panel = wx.Panel(ret_frame, -1)
2934
+
2935
+ sizer = wx.BoxSizer(wx.VERTICAL)
2936
+ # Add a cpGrid and put statistics in it
2937
+ cp = CpGrid(ret_panel, wx.ID_ANY, style=wx.WANTS_CHARS | wx.TE_CENTER)
2938
+ cp.CreateGrid(len(ret), 2)
2939
+
2940
+ sizer.Add(cp, 1, wx.EXPAND)
2941
+
2942
+ for i, (key,val) in enumerate(ret.items()):
2943
+ if i != len(ret)-1:
2944
+ cp.SetCellValue(i, 0, key)
2945
+ cp.SetCellValue(i, 1, str(val))
2946
+
2947
+ ret_panel.SetSizer(sizer)
2948
+ ret_panel.Layout()
2949
+
2950
+ ret_frame.SetSize((300, 200))
2951
+ ret_frame.Centre()
2952
+
2953
+ ret_frame.Show()
2954
+
2795
2955
  def OnExtractSelection(self, event:wx.MouseEvent):
2796
2956
  """ Extract the current selection """
2797
2957
 
@@ -5893,6 +6053,30 @@ class WolfArray(Element_To_Draw, header_wolf):
5893
6053
  if reset_plot:
5894
6054
  self.reset_plot()
5895
6055
 
6056
+ def statistics(self, inside_polygon:vector | Polygon = None):
6057
+ """
6058
+ Statistics on Selected data or the whole array if no selection
6059
+
6060
+ :param inside_polygon: vector or Polygon to select data inside the polygon
6061
+ :return: mean, std, median, sum, values
6062
+ """
6063
+
6064
+ if inside_polygon is not None:
6065
+ ij = self.get_ij_inside_polygon(inside_polygon)
6066
+ vals = self.array[ij]
6067
+ elif self.SelectionData.nb == 0 or self.SelectionData.myselection == 'all':
6068
+ logging.info(_('No selection -- statistics on the whole array'))
6069
+ vals = self.array[~self.array.mask] # all values
6070
+ else:
6071
+ vals = self.SelectionData.get_values_sel()
6072
+
6073
+ mean = np.mean(vals)
6074
+ std = np.std(vals)
6075
+ median = np.median(vals)
6076
+ sum = np.sum(vals)
6077
+
6078
+ return {_('Mean'): mean, _('Std'): std, _('Median'): median, _('Sum'): sum, _('Values'): vals}
6079
+
5896
6080
  def export_geotif(self, outdir:str= '', extent:str= '', EPSG:int = 31370):
5897
6081
  """
5898
6082
  Export de la matrice au format Geotiff (Lambert 72 - EPSG:31370)
@@ -5983,7 +6167,15 @@ class WolfArray(Element_To_Draw, header_wolf):
5983
6167
  band.SetNoDataValue(nullvalue)
5984
6168
  band.WriteArray(np.flipud(arr.data.transpose()))
5985
6169
  band.FlushCache()
5986
- band.ComputeStatistics(True)
6170
+ try:
6171
+ band.ComputeStatistics(True)
6172
+ except RuntimeError as e:
6173
+ # If the array is mainly composed of null values, ComputeStatistics(True) will fail because ComputeStatistics(True) takes a subset of the data to compute statistics (and it may only find null values resulting to an error while in the array, it is not the case)
6174
+ try:
6175
+ band.ComputeStatistics(0) # Using 0 for exact stats instead of True (which implies approximate)
6176
+ except RuntimeError as e:
6177
+ print("Warning: ComputeStatistics failed:", e)
6178
+
5987
6179
 
5988
6180
 
5989
6181
  def get_dxdy_min(self):
@@ -7812,7 +8004,8 @@ class WolfArray(Element_To_Draw, header_wolf):
7812
8004
 
7813
8005
  return newArray
7814
8006
 
7815
- def mask_outsidepoly(self, myvect: vector, eps:float = 0., set_nullvalue:bool=True):
8007
+ def mask_outsidepoly(self, myvect: vector, eps:float = 0.,
8008
+ set_nullvalue:bool=True, method:Literal['mpl', 'shapely_strict', 'shapely_wboundary', 'rasterio']='shapely_strict'):
7816
8009
  """
7817
8010
  Mask nodes outside a polygon and set values to nullvalue
7818
8011
 
@@ -7823,7 +8016,7 @@ class WolfArray(Element_To_Draw, header_wolf):
7823
8016
  # (mesh coord, 0-based)
7824
8017
 
7825
8018
  # trouve les indices dans le polygone
7826
- myij = self.get_ij_inside_polygon(myvect, usemask=True, eps=eps)
8019
+ myij = self.get_ij_inside_polygon(myvect, usemask=True, eps=eps, method=method)
7827
8020
 
7828
8021
  self.array.mask.fill(True) # Mask everything
7829
8022
 
@@ -7836,7 +8029,8 @@ class WolfArray(Element_To_Draw, header_wolf):
7836
8029
 
7837
8030
  self.count()
7838
8031
 
7839
- def mask_insidepoly(self, myvect: vector, eps:float = 0., set_nullvalue:bool=True):
8032
+ def mask_insidepoly(self, myvect: vector, eps:float = 0.,
8033
+ set_nullvalue:bool=True, method:Literal['mpl', 'shapely_strict', 'shapely_wboundary', 'rasterio']='mpl'):
7840
8034
  """
7841
8035
  Mask nodes inside a polygon and set values to nullvalue
7842
8036
 
@@ -7847,7 +8041,7 @@ class WolfArray(Element_To_Draw, header_wolf):
7847
8041
  # (mesh coord, 0-based)
7848
8042
 
7849
8043
  # trouve les indices dans le polygone
7850
- myij = self.get_ij_inside_polygon(myvect, usemask=False, eps=eps)
8044
+ myij = self.get_ij_inside_polygon(myvect, usemask=False, eps=eps, method=method)
7851
8045
 
7852
8046
  if set_nullvalue:
7853
8047
  # annulation des valeurs en dehors du polygone
@@ -7858,12 +8052,47 @@ class WolfArray(Element_To_Draw, header_wolf):
7858
8052
 
7859
8053
  self.count()
7860
8054
 
8055
+ def mask_insidepolys(self, myvects: list[vector], eps:float = 0.,
8056
+ set_nullvalue:bool=True, method:Literal['mpl', 'shapely_strict', 'shapely_wboundary', 'rasterio']='mpl'):
8057
+ """
8058
+ Mask nodes inside a polygon and set values to nullvalue
8059
+
8060
+ :param myvect: target vector in global coordinates
8061
+ """
8062
+ # The polygon here is in world coordinates
8063
+ # (coord will be converted back with translation, origin and dx/dy)
8064
+ # (mesh coord, 0-based)
8065
+
8066
+ # trouve les indices dans le polygone
8067
+ for myvect in myvects:
8068
+ myij = self.get_ij_inside_polygon(myvect, usemask=False, eps=eps, method=method)
8069
+ # masquage des mailles contenues
8070
+ self.array.mask[myij[:,0],myij[:,1]] = True
8071
+
8072
+ if set_nullvalue:
8073
+ # annulation des valeurs en dehors du polygone
8074
+ self.array.data[myij[:,0],myij[:,1]] = self.nullvalue
8075
+
8076
+ self.count()
8077
+
7861
8078
  # *************************************************************************************************************************
7862
8079
  # POSITION and VALUES associated to a vector/polygon/polyline
7863
8080
  # These functions can not be stored in header_wolf, because wa can use the mask of the array to limit the search
7864
8081
  # These functions are also present in WolfResults_2D, but they are not exactly the same due to the structure of the results
7865
8082
  # *************************************************************************************************************************
7866
- def get_xy_inside_polygon(self, myvect: vector | Polygon, usemask:bool=True):
8083
+ def get_xy_inside_polygon(self, myvect: vector | Polygon, usemask:bool=True,
8084
+ method:Literal['mpl', 'shapely_strict', 'shapely_wboundary', 'rasterio']='shapely_strict'):
8085
+
8086
+ if method == 'mpl':
8087
+ return self.get_xy_inside_polygon_mpl(myvect, usemask)
8088
+ elif method == 'shapely_strict':
8089
+ return self.get_xy_inside_polygon_shapely(myvect, usemask, strictly=True)
8090
+ elif method == 'shapely_wboundary':
8091
+ return self.get_xy_inside_polygon_shapely(myvect, usemask, strictly=False)
8092
+ elif method == 'rasterio':
8093
+ return self.get_xy_inside_polygon_rasterio(myvect, usemask)
8094
+
8095
+ def get_xy_inside_polygon_mpl(self, myvect: vector | Polygon, usemask:bool=True):
7867
8096
  """
7868
8097
  Return the coordinates inside a polygon
7869
8098
 
@@ -7892,7 +8121,10 @@ class WolfArray(Element_To_Draw, header_wolf):
7892
8121
 
7893
8122
  return mypointsxy
7894
8123
 
7895
- def get_xy_inside_polygon_shapely(self, myvect: vector | Polygon, usemask:bool=True):
8124
+ def get_xy_inside_polygon_shapely(self,
8125
+ myvect: vector | Polygon,
8126
+ usemask:bool=True,
8127
+ strictly:bool=True):
7896
8128
  """
7897
8129
  Return the coordinates inside a polygon
7898
8130
 
@@ -7901,15 +8133,29 @@ class WolfArray(Element_To_Draw, header_wolf):
7901
8133
  """
7902
8134
 
7903
8135
  if isinstance(myvect, vector):
7904
- # force la mise à jour des min/max
7905
8136
  myvect.find_minmax()
7906
- polygon = myvect.asshapely_pol()
8137
+ polygon = myvect.polygon
7907
8138
  elif isinstance(myvect, Polygon):
7908
8139
  polygon = myvect
7909
8140
 
8141
+ if not is_prepared(polygon):
8142
+ prepare(polygon) # Prepare the polygon for **faster** contains check -- VERY IMPORTANT
8143
+ to_destroy = True
8144
+ else:
8145
+ to_destroy = False
7910
8146
  mypointsxy, mypointsij = self.get_xy_infootprint_vect(myvect)
7911
8147
 
7912
- inside = np.asarray([polygon.contains(Point(x,y)) for x,y in mypointsxy])
8148
+ if strictly:
8149
+ inside = contains_xy(polygon, mypointsxy[:,0], mypointsxy[:,1])
8150
+ else:
8151
+ points= np.array([Point(x,y) for x,y in mypointsxy])
8152
+ boundary = polygon.boundary
8153
+ inside = contains(polygon, points)
8154
+ on_border = list(map(lambda point: boundary.contains(point), points))
8155
+ inside = np.logical_or(inside, on_border)
8156
+
8157
+ if to_destroy:
8158
+ destroy_prepared(polygon) # Destroy the prepared polygon
7913
8159
 
7914
8160
  mypointsxy = mypointsxy[np.where(inside)]
7915
8161
 
@@ -7920,6 +8166,33 @@ class WolfArray(Element_To_Draw, header_wolf):
7920
8166
 
7921
8167
  return mypointsxy
7922
8168
 
8169
+ def get_xy_inside_polygon_rasterio(self, myvect: vector | Polygon, usemask:bool=True):
8170
+ """
8171
+ Return the coordinates inside a polygon
8172
+ """
8173
+ # force la mise à jour des min/max
8174
+ myvect.find_minmax()
8175
+
8176
+ mypointsxy, mypointsij = self.get_xy_infootprint_vect(myvect)
8177
+
8178
+ from rasterio.features import geometry_mask
8179
+
8180
+ poly = myvect.polygon
8181
+ mask = geometry_mask([poly], out_shape=(self.nbx, self.nby),
8182
+ transform=self._transform_gmrio(),
8183
+ invert=True,
8184
+ all_touched=False)
8185
+ # filter mypointsxy where mask is True
8186
+ # mypointsij is a vector of (i,j) indices
8187
+ mypointsxy = mypointsxy[mask[mypointsij[:, 0], mypointsij[:, 1]]]
8188
+
8189
+ if usemask:
8190
+ mypointsij = mypointsij[mask[mypointsij[:, 0], mypointsij[:, 1]]]
8191
+ mymask = np.logical_not(self.array.mask[mypointsij[:, 0], mypointsij[:, 1]])
8192
+ mypointsxy = mypointsxy[np.where(mymask)]
8193
+
8194
+ return mypointsxy
8195
+
7923
8196
  def get_xy_under_polyline(self, myvect: vector, usemask:bool=True):
7924
8197
  """
7925
8198
  Return the coordinates along a polyline
@@ -7933,7 +8206,27 @@ class WolfArray(Element_To_Draw, header_wolf):
7933
8206
 
7934
8207
  return mypoints
7935
8208
 
7936
- def get_ij_inside_polygon(self, myvect: vector, usemask:bool=True, eps:float = 0.):
8209
+ def get_ij_inside_polygon(self, myvect: vector | Polygon,
8210
+ usemask:bool=True, eps:float = 0.,
8211
+ method:Literal['mpl', 'shapely_strict', 'shapely_wboundary', 'rasterio']='shapely_strict'):
8212
+ """
8213
+ Return the indices inside a polygon
8214
+
8215
+ :param myvect: target vector
8216
+ :param usemask: limit potential nodes to unmaksed nodes
8217
+ :param eps: epsilon for the intersection
8218
+ """
8219
+
8220
+ if method == 'mpl':
8221
+ return self.get_ij_inside_polygon_mpl(myvect, usemask, eps)
8222
+ elif method == 'shapely_strict':
8223
+ return self.get_ij_inside_polygon_shapely(myvect, usemask, eps, strictly=True)
8224
+ elif method == 'shapely_wboundary':
8225
+ return self.get_ij_inside_polygon_shapely(myvect, usemask, eps, strictly=False)
8226
+ elif method == 'rasterio':
8227
+ return self._get_ij_inside_polygon_rasterio(myvect, usemask, eps)
8228
+
8229
+ def get_ij_inside_polygon_mpl(self, myvect: vector | Polygon, usemask:bool=True, eps:float = 0.):
7937
8230
  """
7938
8231
  Return the indices inside a polygon
7939
8232
 
@@ -7943,7 +8236,8 @@ class WolfArray(Element_To_Draw, header_wolf):
7943
8236
  """
7944
8237
 
7945
8238
  # force la mise à jour des min/max
7946
- myvect.find_minmax()
8239
+ if isinstance(myvect, vector):
8240
+ myvect.find_minmax()
7947
8241
 
7948
8242
  mypointsxy, mypointsij = self.get_xy_infootprint_vect(myvect, eps=eps)
7949
8243
 
@@ -7961,32 +8255,201 @@ class WolfArray(Element_To_Draw, header_wolf):
7961
8255
 
7962
8256
  return mypointsij
7963
8257
 
7964
- def intersects_polygon(self, myvect: vector | Polygon, usemask:bool=True):
8258
+ def get_ij_inside_polygon_shapely(self, myvect: vector | Polygon,
8259
+ usemask:bool=True,
8260
+ eps:float = 0.,
8261
+ strictly:bool=True):
8262
+ """
8263
+ Return the indices inside a polygon with the contains method of shapely
8264
+
8265
+ :param myvect: target vector
8266
+ :param usemask: limit potential nodes to unmaksed nodes
8267
+ :param eps: epsilon for the intersection
8268
+ :param strictly: if True, the points on the border are not considered inside
8269
+ """
8270
+
8271
+ # force la mise à jour des min/max
8272
+ if isinstance(myvect, vector):
8273
+ myvect.find_minmax()
8274
+ poly = myvect.polygon
8275
+ elif isinstance(myvect, Polygon):
8276
+ poly = myvect
8277
+
8278
+ mypointsxy, mypointsij = self.get_xy_infootprint_vect(myvect, eps=eps)
8279
+
8280
+ if not is_prepared(poly):
8281
+ prepare(poly) # Prepare the polygon for **faster** contains check -- VERY IMPORTANT
8282
+ to_destroy = True
8283
+ else:
8284
+ to_destroy = False
8285
+
8286
+ if strictly:
8287
+ inside = contains_xy(poly, mypointsxy[:,0], mypointsxy[:,1])
8288
+ else:
8289
+ points= np.array([Point(x,y) for x,y in mypointsxy])
8290
+ boundary = poly.boundary
8291
+ inside = contains(poly, points)
8292
+ on_border = list(map(lambda point: boundary.contains(point), points))
8293
+ inside = np.logical_or(inside, on_border)
8294
+
8295
+ if to_destroy:
8296
+ destroy_prepared(poly) # Destroy the prepared polygon
8297
+
8298
+ mypointsij = mypointsij[np.where(inside)]
8299
+
8300
+ if usemask:
8301
+ mymask = np.logical_not(self.array.mask[mypointsij[:, 0], mypointsij[:, 1]])
8302
+ mypointsij = mypointsij[np.where(mymask)]
8303
+
8304
+ return mypointsij
8305
+
8306
+ def _get_ij_inside_polygon_rasterio(self, myvect: vector | Polygon, usemask:bool=True, eps:float = 0.):
8307
+ """
8308
+ Return the indices inside a polygon with the geometry_mask method of rasterio.
8309
+
8310
+ :remark: get_ij_inside_polygon_shapely is more efficient
8311
+
8312
+ FIXME : geometry_mask seems strange -- it does not work as documented -- RatsrIO 1.3.x
8313
+
8314
+ :param myvect: target vector
8315
+ :param usemask: limit potential nodes to unmaksed nodes
8316
+ :param eps: epsilon for the intersection
8317
+ """
8318
+
8319
+ # force la mise à jour des min/max
8320
+ if isinstance(myvect, vector):
8321
+ myvect.find_minmax()
8322
+ poly = myvect.polygon
8323
+ elif isinstance(myvect, Polygon):
8324
+ poly = myvect
8325
+
8326
+ mypointsxy, mypointsij = self.get_xy_infootprint_vect(myvect, eps=eps)
8327
+
8328
+ from rasterio.features import geometry_mask
8329
+
8330
+ mask = geometry_mask([poly], out_shape=(self.nbx, self.nby),
8331
+ transform=self._transform_gmrio(),
8332
+ invert=True,
8333
+ all_touched=False)
8334
+ # filter mypointsij where mask is True
8335
+ # mypointsij is a vector of (i,j) indices
8336
+ mypointsij = mypointsij[mask[mypointsij[:, 0], mypointsij[:, 1]]]
8337
+
8338
+ if usemask:
8339
+ mymask = np.logical_not(self.array.mask[mypointsij[:, 0], mypointsij[:, 1]])
8340
+ mypointsij = mypointsij[np.where(mymask)]
8341
+
8342
+ return mypointsij
8343
+
8344
+ def intersects_polygon(self, myvect: vector | Polygon,
8345
+ usemask:bool=True,
8346
+ method:Literal['mpl', 'shapely_strict', 'shapely_wboundary', 'rasterio']='shapely_strict',
8347
+ buffer:float= 0.) -> bool:
7965
8348
  """ Return True if the array intersects the polygon
7966
8349
 
7967
8350
  :param myvect: target vector
7968
8351
  :param usemask: limit potential nodes to unmaksed nodes
7969
8352
  """
7970
8353
 
7971
- return self.get_xy_inside_polygon(myvect, usemask).shape[0] > 0
8354
+ if buffer > 0.:
8355
+ bufvect = myvect.buffer(buffer, inplace= False)
8356
+ return self.get_xy_inside_polygon(bufvect, usemask, method).shape[0] > 0
8357
+ else:
8358
+ return self.get_xy_inside_polygon(myvect, usemask, method).shape[0] > 0
7972
8359
 
7973
- def intersects_polygon_shapely(self, myvect: vector | Polygon, eps:float = 0., usemask:bool=True):
8360
+ def intersects_polygon_shapely(self, myvect: vector | Polygon,
8361
+ eps:float = 0.,
8362
+ usemask:bool=True,
8363
+ buffer:float= 0.) -> bool:
7974
8364
  """ Return True if the array intersects the polygon
7975
8365
 
7976
8366
  :param myvect: target vector
7977
8367
  :param usemask: limit potential nodes to unmaksed nodes
8368
+ :param eps: epsilon for the intersection
8369
+ :param buffer: buffer for the intersection - using buffer from Shapely [m]
8370
+ """
8371
+ if buffer > 0.:
8372
+ poly = myvect.polygon.buffer(buffer)
8373
+ return self.get_xy_inside_polygon_shapely(poly, usemask).shape[0] > 0
8374
+ else:
8375
+ return self.get_xy_inside_polygon_shapely(myvect, usemask).shape[0] > 0
8376
+
8377
+ def interects_listofpolygons(self, myvects:zone | list[vector],
8378
+ usemask:bool=True,
8379
+ method:Literal['mpl', 'shapely_strict', 'shapely_wboundary', 'rasterio']='shapely_strict',
8380
+ buffer:float= 0.) -> list[bool]:
7978
8381
  """
7979
- return self.get_xy_inside_polygon_shapely(myvect, usemask).shape[0] > 0
8382
+ Element-wise intersection with a list of polygons
8383
+ """
8384
+
8385
+ if isinstance(myvects, zone):
8386
+ myvects = myvects.myvectors
8387
+ elif isinstance(myvects, list):
8388
+ pass
8389
+ else:
8390
+ logging.error("Bad type")
8391
+ return {}
8392
+
8393
+ return list(map(lambda x: self.intersects_polygon(x, usemask, method, buffer), myvects))
7980
8394
 
7981
- def get_ij_under_polyline(self, myvect: vector, usemask:bool=True):
8395
+ def intersects_zones(self, zones:Zones | list[zone],
8396
+ usemask:bool=True,
8397
+ method:Literal['mpl', 'shapely_strict', 'shapely_wboundary', 'rasterio']='shapely_strict',
8398
+ buffer:float = 0.) -> list[list[bool]]:
8399
+ """
8400
+ Element-wise intersection with a list of zones
8401
+ """
8402
+
8403
+ if isinstance(zones, Zones):
8404
+ zones = zones.myzones
8405
+ elif isinstance(zones, list):
8406
+ pass
8407
+ else:
8408
+ logging.error("Bad type")
8409
+ return {}
8410
+
8411
+ return list(map(lambda x: self.interects_listofpolygons(x, usemask, method, buffer), zones))
8412
+
8413
+ def get_total_area_if_intersects(self, myvects:Zones | list[vector],
8414
+ usemask:bool=True,
8415
+ method:Literal['mpl', 'shapely_strict', 'shapely_wboundary', 'rasterio']='shapely_strict',
8416
+ buffer:float= 0.,
8417
+ coefficient:float = 1.) -> float | list[float]:
8418
+ """
8419
+ Return the area of the intersection with a list of polygons
8420
+ """
8421
+
8422
+ if isinstance(myvects, Zones):
8423
+ ret = []
8424
+ for curzone in myvects.myzones:
8425
+ myvects = curzone.myvectors
8426
+ intersects = self.interects_listofpolygons(myvects, usemask, method, buffer)
8427
+ ret.append(sum([x.area for x in myvects if intersects[myvects.index(x)]]) * coefficient)
8428
+ return ret
8429
+ elif isinstance(myvects, list):
8430
+ intersects = self.interects_listofpolygons(myvects, usemask, method, buffer)
8431
+ return sum([x.area for x in myvects if intersects[myvects.index(x)]]) * coefficient
8432
+
8433
+ def get_ij_under_polyline(self, myvect: vector, usemask:bool=True, step_factor:float=1.):
7982
8434
  """
7983
8435
  Return the indices along a polyline
7984
8436
 
7985
8437
  :param myvect: target vector
7986
8438
  :param usedmask: limit potential nodes to unmaksed nodes
8439
+ :param step_factor: step factor for the discretization of myvect (<1 for thinner, >1 for coarser)
7987
8440
  """
7988
8441
 
7989
- ds = min(self.dx, self.dy)
8442
+ if step_factor>=1.0:
8443
+ logging.warning("Step_factor greater than 1.0 may lead to lots of missing (i,j) even if faster")
8444
+ else:
8445
+ def mod_fix(x, y, tol=1e-10):
8446
+ result = np.mod(x, y)
8447
+ return 0 if np.isclose(result, 0, atol=tol) else (y if np.isclose(result, y, atol=tol) else result)
8448
+ assert mod_fix(1.0,step_factor)==0, "1.0 should be a multiple of step_factor"
8449
+
8450
+ # assert step_factor<=1., "Step factor must be <= 1"
8451
+
8452
+ ds = step_factor*min(self.dx, self.dy)
7990
8453
  pts = myvect._refine2D(ds)
7991
8454
 
7992
8455
  allij = np.asarray([self.get_ij_from_xy(curpt.x, curpt.y) for curpt in pts])
@@ -8002,7 +8465,7 @@ class WolfArray(Element_To_Draw, header_wolf):
8002
8465
 
8003
8466
  return allij
8004
8467
 
8005
- def get_values_insidepoly(self, myvect: vector, usemask:bool=True, getxy:bool=False):
8468
+ def get_values_insidepoly(self, myvect: vector, usemask:bool=True, getxy:bool=False) -> tuple[np.ndarray, np.ndarray | None]:
8006
8469
  """
8007
8470
  Récupération des valeurs contenues dans un polygone
8008
8471
 
@@ -8017,6 +8480,44 @@ class WolfArray(Element_To_Draw, header_wolf):
8017
8480
  else:
8018
8481
  return myvalues, None
8019
8482
 
8483
+ def get_values_inside_listofpolygons(self, myvects:zone | list[vector], usemask:bool=True, getxy:bool=False) -> dict:
8484
+ """
8485
+ Récupération des valeurs contenues dans une instance Zones ou une liste de vecteurs
8486
+ """
8487
+
8488
+ ret = {}
8489
+ if isinstance(myvects, zone):
8490
+ out = ret[myvects.myname] = {}
8491
+ myvects = myvects.myvectors
8492
+ elif isinstance(myvects, list):
8493
+ out = ret
8494
+ else:
8495
+ logging.error("Bad type")
8496
+ return {}
8497
+
8498
+ for vec in myvects:
8499
+ out[vec.myname] = self.get_values_insidepoly(vec, usemask, getxy)
8500
+
8501
+
8502
+ def get_values_inside_zones(self, zones:Zones | list[zone], usemask:bool=True, getxy:bool=False) -> dict:
8503
+ """
8504
+ Récupération des valeurs contenues dans une instance Zones ou une liste de "zone"
8505
+ """
8506
+
8507
+ ret = {}
8508
+ if isinstance(zones, Zones):
8509
+ out = ret[myzones.myname] = {}
8510
+ myzones = zones.myzones
8511
+ elif isinstance(zones, list):
8512
+ myzones = zones
8513
+ out = ret
8514
+ else:
8515
+ logging.error("Bad type")
8516
+ return {}
8517
+
8518
+ for zone in myzones:
8519
+ out[zone.myname] = self.get_values_inside_listofpolygons(zone, usemask, getxy)
8520
+
8020
8521
  def get_values_underpoly(self, myvect: vector, usemask:bool=True, getxy:bool=False):
8021
8522
  """
8022
8523
  Récupération des valeurs contenues sous une polyligne
@@ -8059,6 +8560,63 @@ class WolfArray(Element_To_Draw, header_wolf):
8059
8560
 
8060
8561
  return self.get_values_underpoly(myvect, usemask, getxy)
8061
8562
 
8563
+ def count_insidepoly(self, myvect: vector,
8564
+ usemask:bool=True,
8565
+ method:Literal['mpl', 'shapely_strict', 'shapely_wboundary', 'rasterio']='shapely_strict',
8566
+ coefficient:float = 1.):
8567
+ """
8568
+ Compte le nombre de valeurs contenues dans un polygone
8569
+
8570
+ :param myvect: target vector
8571
+ :param usemask: (optional) restreint les éléments aux éléments non masqués de la matrice
8572
+ :param method: (optional) method to use
8573
+ :param coefficient: (optional) coefficient to apply to the count (default 1.)
8574
+ """
8575
+
8576
+ ij = self.get_ij_inside_polygon(myvect, usemask, method=method)
8577
+ return ij.shape[0] * coefficient
8578
+
8579
+ def count_inside_listofpolygons(self, myvects:zone | list[vector],
8580
+ usemask:bool=True,
8581
+ method:Literal['mpl', 'shapely_strict', 'shapely_wboundary', 'rasterio']='shapely_strict',
8582
+ coefficient:float = 1.):
8583
+ """
8584
+ Compte le nombre de valeurs contenues dans une instance Zones ou une liste de vecteurs
8585
+
8586
+ :param myvects: target vectors or zone
8587
+ :param usemask: (optional) restreint les éléments aux éléments non masqués de la matrice
8588
+ :param method: (optional) method to use
8589
+ :param coefficient: (optional) coefficient to apply to the count (default 1.)
8590
+ """
8591
+
8592
+ if isinstance(myvects, zone):
8593
+ myvects = myvects.myvectors
8594
+ else:
8595
+ logging.error("Bad type")
8596
+ return {}
8597
+
8598
+ return list(map(lambda vec: self.count_insidepoly(vec, usemask, method=method, coefficient=coefficient), myvects))
8599
+
8600
+ def count_inside_zones(self, zones:Zones | list[zone],
8601
+ usemask:bool=True,
8602
+ method:Literal['mpl', 'shapely_strict', 'shapely_wboundary', 'rasterio']='shapely_strict',
8603
+ coefficient:float = 1.):
8604
+ """
8605
+ Compte le nombre de valeurs contenues dans une instance Zones ou une liste de "zone"
8606
+
8607
+ :param zones: target zones or list of zones
8608
+ :param usemask: (optional) restreint les éléments aux éléments non masqués de la matrice
8609
+ :param method: (optional) method to use
8610
+ :param coefficient: (optional) coefficient to apply to the count (default 1.)
8611
+ """
8612
+
8613
+ if isinstance(zones, Zones):
8614
+ zones = zones.myzones
8615
+ else:
8616
+ logging.error("Bad type")
8617
+
8618
+ return list(map(lambda loczone: self.count_inside_listofpolygons(loczone, usemask, method=method, coefficient=coefficient), zones))
8619
+
8062
8620
  # *************************************************************************************************************************
8063
8621
  # END POSITION and VALUES associated to a vector/polygon/polyline
8064
8622
  # *************************************************************************************************************************
@@ -9085,7 +9643,7 @@ class WolfArray(Element_To_Draw, header_wolf):
9085
9643
  else:
9086
9644
  self.mypal.isopop(self.get_working_array(), self.nbnotnull)
9087
9645
 
9088
- if VERSION_RGB==1 :
9646
+ if VERSION_RGB==1 or which == 1:
9089
9647
  if self.nbx * self.nby > 1_000_000 : logging.info(_('Computing colors'))
9090
9648
  if self.wolftype not in [WOLF_ARRAY_FULL_SINGLE, WOLF_ARRAY_FULL_INTEGER8, WOLF_ARRAY_FULL_UINTEGER8]:
9091
9649
  # FIXME: Currently, only some types are supported in Cython/OpenGL library
@@ -9110,7 +9668,7 @@ class WolfArray(Element_To_Draw, header_wolf):
9110
9668
  self._tmp_float32 = None
9111
9669
 
9112
9670
 
9113
- if VERSION_RGB==1 :
9671
+ if VERSION_RGB==1 or which == 1:
9114
9672
  if self.shading:
9115
9673
  pond = (self.shaded.array-.5)*2.
9116
9674
  pmin = (1. - self.shaded.alpha) * self.rgb
@@ -9119,7 +9677,7 @@ class WolfArray(Element_To_Draw, header_wolf):
9119
9677
  self.rgb[pond<0,i] = self.rgb[pond<0,i] * (1.+pond[pond<0]) - pmin[pond<0,i] * pond[pond<0]
9120
9678
  self.rgb[pond>0,i] = self.rgb[pond>0,i] * (1.-pond[pond>0]) + pmax[pond>0,i] * pond[pond>0]
9121
9679
 
9122
- if VERSION_RGB==1 : self.rgb[self.array.mask] = [1., 1., 1., 0.]
9680
+ if VERSION_RGB==1 or which == 1: self.rgb[self.array.mask] = [1., 1., 1., 0.]
9123
9681
 
9124
9682
  if self.myops is not None:
9125
9683
  # update the wx
@@ -9266,22 +9824,94 @@ class WolfArray(Element_To_Draw, header_wolf):
9266
9824
  self.mygrid = {}
9267
9825
  self.gridmaxscales = -1
9268
9826
 
9269
- def plot_matplotlib(self, figax:tuple=None, getdata_im:bool=False):
9827
+ def plot_matplotlib(self, figax:tuple=None,
9828
+ getdata_im:bool=False,
9829
+ update_palette:bool=True,
9830
+ vmin:float=None, vmax:float=None,
9831
+ figsize:tuple=None,
9832
+ Walonmap:bool=False,
9833
+ cat:str='IMAGERIE/ORTHO_2022_ETE'):
9270
9834
  """
9271
9835
  Plot the array - Matplotlib version
9272
9836
 
9273
9837
  Using imshow and RGB array
9838
+
9839
+ Plot the array using Matplotlib.
9840
+ This method visualizes the array data using Matplotlib's `imshow` function.
9841
+ It supports optional overlays, custom palettes, and value range adjustments.
9842
+
9843
+ Notes:
9844
+ ------
9845
+ - The method applies a mask to the data using the `nullvalue` attribute before plotting.
9846
+ - If `Walonmap` is True, the method fetches and overlays a map image using the WalOnMap service.
9847
+ - The aspect ratio of the plot is set to 'equal'.
9848
+
9849
+ :param figax: A tuple containing a Matplotlib figure and axis (fig, ax). If None, a new figure and axis are created.
9850
+ :type figax: tuple, optional (Default value = None)
9851
+ :param getdata_im: If True, returns the image object along with the figure and axis. Default is False, then it only returns (fig, ax).
9852
+ :type getdata_im: bool, optional (Default value = False)
9853
+ :param update_palette: If True, updates the color palette before plotting. Default is True.
9854
+ :type update_palette: bool, optional (Default value = True)
9855
+ :param vmin: Minimum value for color scaling. If None, the minimum value is determined automatically. Default is None.
9856
+ :type vmin: float, optional (Default value = None)
9857
+ :param vmax: Maximum value for color scaling. If None, the maximum value is determined automatically. Default is None.
9858
+ :type vmax: float, optional (Default value = None)
9859
+ :param figsize: Size of the figure in inches (width, height). Only used if `figax` is None. Default is None.
9860
+ :type figsize: tuple, optional (Default value = None)
9861
+ :param Walonmap: If True, overlays a map image using the WalOnMap service. Default is False.
9862
+ :type Walonmap: bool, optional (Default value = False)
9863
+ :param cat: The category of the map to fetch from the WalOnMap service. Default is 'IMAGERIE/ORTHO_2022_ETE'.
9864
+ Available orthos:
9865
+ - `'IMAGERIE/ORTHO_1971'`
9866
+ - `'IMAGERIE/ORTHO_1994_2000'`
9867
+ - `'IMAGERIE/ORTHO_2006_2007'`
9868
+ - `'IMAGERIE/ORTHO_2009_2010'`
9869
+ - `'IMAGERIE/ORTHO_2012_2013'`
9870
+ - `'IMAGERIE/ORTHO_2015'`
9871
+ - `'IMAGERIE/ORTHO_2016'`
9872
+ - `'IMAGERIE/ORTHO_2017'`
9873
+ - `'IMAGERIE/ORTHO_2018'`
9874
+ - `'IMAGERIE/ORTHO_2019'`
9875
+ - `'IMAGERIE/ORTHO_2020'`
9876
+ - `'IMAGERIE/ORTHO_2021'`
9877
+ - `'IMAGERIE/ORTHO_2022_PRINTEMPS'`
9878
+ - `'IMAGERIE/ORTHO_2022_ETE'`
9879
+ - `'IMAGERIE/ORTHO_2023_ETE'`
9880
+ - `'IMAGERIE/ORTHO_LAST'`
9881
+ :type cat: str, optional (Default value = `'IMAGERIE/ORTHO_2022_ETE'`)
9882
+ :return: If `getdata_im` is False, returns (fig, ax), where `fig` is the Matplotlib figure and `ax` is the axis. If `getdata_im` is True, returns (fig, ax, im), where `im` is the image object created by `imshow`.
9883
+ :rtype: tuple
9274
9884
  """
9275
9885
 
9276
9886
  self.mask_data(self.nullvalue)
9277
- self.updatepalette(0)
9887
+ if update_palette:
9888
+ self.updatepalette(0)
9889
+
9278
9890
  if figax is None:
9279
- fig, ax = plt.subplots()
9891
+ fig, ax = plt.subplots(figsize=figsize)
9280
9892
  else:
9281
9893
  fig, ax = figax
9282
9894
 
9283
- im = ax.imshow(self.array.transpose(), origin='lower', cmap=self.mypal,
9284
- extent=(self.origx, self.origx + self.dx * self.nbx, self.origy, self.origy + self.dy * self.nby))
9895
+ if Walonmap:
9896
+ from .PyWMS import getWalonmap
9897
+ from PIL import Image
9898
+
9899
+ try:
9900
+ IO_image = getWalonmap(cat, self.origx, self.origy, self.origx + self.nbx * self.dx, self.origy +self.nby * self.dy, w=1000, tofile=False) # w=self.nbx, h=self.nby
9901
+
9902
+ image = Image.open(IO_image)
9903
+
9904
+ ax.imshow(image.transpose(Image.Transpose.FLIP_TOP_BOTTOM), extent=(self.origx, self.origx + self.dx * self.nbx, self.origy, self.origy + self.dy * self.nby), alpha=1, origin='lower')
9905
+ except Exception as e:
9906
+ logging.error(_('Error while fetching the map image from WalOnMap'))
9907
+ logging.error(e)
9908
+
9909
+ if (vmin is None) and (vmax is None):
9910
+ im = ax.imshow(self.array.transpose(), origin='lower', cmap=self.mypal,
9911
+ extent=(self.origx, self.origx + self.dx * self.nbx, self.origy, self.origy + self.dy * self.nby))
9912
+ else:
9913
+ im = ax.imshow(self.array.transpose(), origin='lower', cmap=self.mypal,
9914
+ extent=(self.origx, self.origx + self.dx * self.nbx, self.origy, self.origy + self.dy * self.nby), vmin=vmin, vmax=vmax)
9285
9915
  ax.set_aspect('equal')
9286
9916
 
9287
9917
  if getdata_im:
@@ -9613,7 +10243,8 @@ class WolfArray(Element_To_Draw, header_wolf):
9613
10243
 
9614
10244
  return indicesX,indicesY,contourgen,interior
9615
10245
 
9616
- def imshow(self, figax:tuple[Figure, Axis] = None, cmap:Colormap = None, step_ticks=100.) -> tuple[Figure, Axis]:
10246
+ def imshow(self, figax:tuple[Figure, Axis] = None,
10247
+ cmap:Colormap = None, step_ticks=100.) -> tuple[Figure, Axis]:
9617
10248
  """
9618
10249
  Create Matplotlib image from WolfArray
9619
10250
  """
@@ -9628,11 +10259,11 @@ class WolfArray(Element_To_Draw, header_wolf):
9628
10259
 
9629
10260
  if cmap is None:
9630
10261
  # update local colors if not already done
9631
- if self[i].rgb is None:
9632
- self[i].updatepalette(0)
10262
+ if self.rgb is None:
10263
+ self.updatepalette(1)
9633
10264
 
9634
10265
  # Pointing RGB
9635
- colors = self[i].rgb
10266
+ colors = self.rgb
9636
10267
  colors[self.array.mask,3] = 0.
9637
10268
  # Plot
9638
10269
  colors = colors.swapaxes(0,1)
@@ -9859,13 +10490,14 @@ class WolfArray(Element_To_Draw, header_wolf):
9859
10490
 
9860
10491
  return zones
9861
10492
 
9862
- def inpaint(self, mask_array:"WolfArray" = None, test_array:"WolfArray" = None, ignore_last:int = 1):
10493
+ def inpaint(self, mask_array:"WolfArray" = None, test_array:"WolfArray" = None,
10494
+ ignore_last:int = 1, multiprocess:bool = True):
9863
10495
  """ InPaintaing holes in the array
9864
10496
 
9865
10497
  :param mask_array: where computation is done
9866
10498
  :param test_array: used in test -- interpolation is accepted if new value is over test_array
9867
10499
  :param ignore_last: number of last patches to ignore
9868
-
10500
+ :param multiprocess: use multiprocess for inpainting
9869
10501
  """
9870
10502
 
9871
10503
  from .eikonal import inpaint_array
@@ -9888,7 +10520,7 @@ class WolfArray(Element_To_Draw, header_wolf):
9888
10520
  dy = self.dy,
9889
10521
  NoDataValue = self.nullvalue,
9890
10522
  inplace=True,
9891
- multiprocess= True)
10523
+ multiprocess= multiprocess)
9892
10524
 
9893
10525
  wd = WolfArray(mold=self)
9894
10526
  wd.array[:,:] = wd_np[:,:]
@@ -9900,7 +10532,9 @@ class WolfArray(Element_To_Draw, header_wolf):
9900
10532
  self.reset_plot()
9901
10533
  return time, wl, wd
9902
10534
 
9903
- def _inpaint_waterlevel_dem_dtm(self, dem:"WolfArray", dtm:"WolfArray", ignore_last:int = 1, use_fortran:bool = False):
10535
+ def _inpaint_waterlevel_dem_dtm(self, dem:"WolfArray", dtm:"WolfArray",
10536
+ ignore_last:int = 1, use_fortran:bool = False,
10537
+ multiprocess:bool = True):
9904
10538
  """ InPaintaing waterlevel holes in the array.
9905
10539
 
9906
10540
  We use DEM and DTM to mask and constraint the inpainting process.
@@ -9909,6 +10543,7 @@ class WolfArray(Element_To_Draw, header_wolf):
9909
10543
  :param dtm: Digital Terrain Model
9910
10544
  :param ignore_last: number of last patches to ignore
9911
10545
  :param use_fortran: use Fortran inpainting code
10546
+ :param multiprocess: use multiprocess for inpainting
9912
10547
  """
9913
10548
 
9914
10549
  if use_fortran:
@@ -9975,7 +10610,7 @@ class WolfArray(Element_To_Draw, header_wolf):
9975
10610
  dy = self.dy,
9976
10611
  NoDataValue = self.nullvalue,
9977
10612
  inplace=True,
9978
- multiprocess= True)
10613
+ multiprocess= multiprocess)
9979
10614
 
9980
10615
  wd = WolfArray(mold=self)
9981
10616
  wd.array[:,:] = wd_np[:,:]
@@ -10049,6 +10684,68 @@ class WolfArray(Element_To_Draw, header_wolf):
10049
10684
 
10050
10685
  return newarray
10051
10686
 
10687
+ def create_binary_mask(self, threshold:float = 0.) -> "WolfArray":
10688
+ """ Create a binary mask from the array """
10689
+
10690
+ newarray = WolfArray(mold=self)
10691
+ newarray.array.data[:,:] = self.array.data > threshold
10692
+ newarray.array.mask[:,:] = ~newarray.array.data.astype(bool)
10693
+ newarray.set_nullvalue_in_mask()
10694
+
10695
+ return newarray
10696
+
10697
+ def fill_holes_with_value(self, value:float, mask:"WolfArray" = None, ignore_last:int = 1):
10698
+ """ Fill holes in the array with a value """
10699
+
10700
+ if mask is None:
10701
+ mask = self.array.mask
10702
+
10703
+ labels, numfeatures = label(mask)
10704
+
10705
+ # count cells in holes
10706
+ nb_cells = np.bincount(labels.ravel())
10707
+
10708
+ # sort by number of cells
10709
+ idx = np.argsort(nb_cells)
10710
+
10711
+ for i in range(ignore_last):
10712
+ labels[labels == idx[-1-i]] = 0
10713
+
10714
+ self.array.data[labels > 0] = value
10715
+ self.array.mask[labels > 0] = False # unmask filled data
10716
+
10717
+ def fill_holes_with_value_if_intersects(self, value:float,
10718
+ vects:list[vector] | Zones,
10719
+ method:Literal['matplotlib','shapely_strict', 'shapely'] = 'shapely_strict',
10720
+ buffer:float = 0.):
10721
+ """ Fill holes in the array with a value if it intersects with the mask """
10722
+
10723
+ if isinstance(vects, Zones):
10724
+ vects = vects.concatenate_all_vectors()
10725
+
10726
+ for vec in vects:
10727
+ self._fill_holes_with_value_if_intersects(value, vec, method, buffer)
10728
+ # list(map(self._fill_holes_with_value_if_intersects, [value]*len(vects), vects, [method]*len(vects), [buffer]*len(vects))) # parallel version
10729
+
10730
+ def _fill_holes_with_value_if_intersects(self, value:float,
10731
+ vect:vector,
10732
+ method:Literal['matplotlib','shapely_strict', 'shapely'] = 'shapely_strict',
10733
+ buffer:float = 0.) -> "WolfArray":
10734
+
10735
+ if buffer > 0.:
10736
+ # As we use a buffer, we check the intersction with the buffer...
10737
+ if self.intersects_polygon(vect, method=method, buffer=buffer):
10738
+ # ...but we fill the original polygon, not the buffered one
10739
+ ij = self.get_ij_inside_polygon(vect, method=method, usemask=False)
10740
+ self.array.data[ij[:,0], ij[:,1]] = value
10741
+ self.array.mask[ij[:,0], ij[:,1]] = False
10742
+ else:
10743
+ ij = self.get_ij_inside_polygon(vect, method=method, buffer=buffer, usemask=False)
10744
+
10745
+ if ij.size[0] > 0:
10746
+ self.array.data[ij[:,0], ij[:,1]] = value
10747
+ self.array.mask[ij[:,0], ij[:,1]] = False
10748
+
10052
10749
  def _create_building_holes_dem_dtm(self, dem:"WolfArray", dtm:"WolfArray", ignore_last:int = 1) -> "WolfArray":
10053
10750
  """ Select holes in the array and create a new aray """
10054
10751
 
@@ -10059,6 +10756,26 @@ class WolfArray(Element_To_Draw, header_wolf):
10059
10756
 
10060
10757
  return buildings
10061
10758
 
10759
+ @classmethod
10760
+ def merge(cls, others:list["WolfArrayMB"], abs:bool=True, copy:bool = True) -> "WolfArray":
10761
+ """
10762
+ Merge several WolfArray into a single WolfArray
10763
+
10764
+ :param others: list of WolfArray to merge
10765
+ :param abs: if True -> Global World Coordinates
10766
+ :param copy: if True -> copy data (**Not necessary** if data header CAN BE modified. It can be save memory)
10767
+ """
10768
+
10769
+ newMBArray = WolfArrayMB()
10770
+
10771
+ for curarray in others:
10772
+ newMBArray.add_block(curarray, force_idx=True, copyarray=copy)
10773
+
10774
+ newMBArray.set_header_from_added_blocks()
10775
+
10776
+ return newMBArray.as_WolfArray(abs=abs)
10777
+
10778
+
10062
10779
  class WolfArrayMB(WolfArray):
10063
10780
  """
10064
10781
  Matrice multiblocks
@@ -10285,6 +11002,7 @@ class WolfArrayMB(WolfArray):
10285
11002
  arr.isblock = True
10286
11003
  arr.blockindex = len(self.myblocks) - 1
10287
11004
 
11005
+
10288
11006
  def share_palette(self):
10289
11007
  """Partage de la palette de couleurs entre matrices liées"""
10290
11008
  for cur in self.linkedarrays:
@@ -10987,11 +11705,26 @@ class WolfArrayMB(WolfArray):
10987
11705
  self.translx = 0.
10988
11706
  self.transly = 0.
10989
11707
 
11708
+ self.head_blocks = {}
11709
+ for curblock in self.myblocks.values():
11710
+
11711
+ new_trx = self.origx + self.translx
11712
+ new_try = self.origy + self.transly
11713
+
11714
+ ox = curblock.origx + curblock.translx
11715
+ oy = curblock.origy + curblock.transly
11716
+
11717
+ curblock.origin = (ox - new_trx, oy - new_try)
11718
+ curblock.translation = (new_trx, new_try)
11719
+
11720
+ self.head_blocks[curblock.idx] = curblock.get_header()
11721
+
10990
11722
  def as_WolfArray(self, abs:bool=True, forced_header:header_wolf = None) -> WolfArray:
10991
11723
  """
10992
- Convert to WolfArray
11724
+ Convert to WolfArray -- Rebin blocks if necessary
10993
11725
 
10994
- Rebin blocks if necessary
11726
+ :param abs: if True, then the translation is taken into account
11727
+ :param forced_header: if not None, then the header is forced to this value
10995
11728
  """
10996
11729
 
10997
11730
  newArray = WolfArray()
@@ -11101,6 +11834,67 @@ class WolfArrayMB(WolfArray):
11101
11834
 
11102
11835
  return newArray
11103
11836
 
11837
+ def plot_matplotlib(self, figax:tuple=None, getdata_im:bool=False, update_palette:bool=True, vmin:float=None, vmax:float=None, figsize:tuple=None, Walonmap:bool=False, cat:str='IMAGERIE/ORTHO_2022_ETE'):
11838
+ """
11839
+ Plot the multi-block (MB) array - Matplotlib version
11840
+
11841
+ Converts the MB array to single/mono-block and,
11842
+ Uses the `plot_matplotlib` method of the WolfArray class.
11843
+
11844
+ Using imshow and RGB array
11845
+
11846
+ Plot the array using Matplotlib.
11847
+ This method visualizes the array data using Matplotlib's `imshow` function.
11848
+ It supports optional overlays, custom palettes, and value range adjustments.
11849
+
11850
+ Notes:
11851
+ ------
11852
+ - The method applies a mask to the data using the `nullvalue` attribute before plotting.
11853
+ - If `Walonmap` is True, the method fetches and overlays a map image using the WalOnMap service.
11854
+ - The aspect ratio of the plot is set to 'equal'.
11855
+
11856
+ :param figax: A tuple containing a Matplotlib figure and axis (fig, ax). If None, a new figure and axis are created.
11857
+ :type figax: tuple, optional (Default value = None)
11858
+ :param getdata_im: If True, returns the image object along with the figure and axis. Default is False, then it only returns (fig, ax).
11859
+ :type getdata_im: bool, optional (Default value = False)
11860
+ :param update_palette: If True, updates the color palette before plotting. Default is True.
11861
+ :type update_palette: bool, optional (Default value = True)
11862
+ :param vmin: Minimum value for color scaling. If None, the minimum value is determined automatically. Default is None.
11863
+ :type vmin: float, optional (Default value = None)
11864
+ :param vmax: Maximum value for color scaling. If None, the maximum value is determined automatically. Default is None.
11865
+ :type vmax: float, optional (Default value = None)
11866
+ :param figsize: Size of the figure in inches (width, height). Only used if `figax` is None. Default is None.
11867
+ :type figsize: tuple, optional (Default value = None)
11868
+ :param Walonmap: If True, overlays a map image using the WalOnMap service. Default is False.
11869
+ :type Walonmap: bool, optional (Default value = False)
11870
+ :param cat: The category of the map to fetch from the WalOnMap service. Default is 'IMAGERIE/ORTHO_2022_ETE'.
11871
+ Available orthos:
11872
+ - `'IMAGERIE/ORTHO_1971'`
11873
+ - `'IMAGERIE/ORTHO_1994_2000'`
11874
+ - `'IMAGERIE/ORTHO_2006_2007'`
11875
+ - `'IMAGERIE/ORTHO_2009_2010'`
11876
+ - `'IMAGERIE/ORTHO_2012_2013'`
11877
+ - `'IMAGERIE/ORTHO_2015'`
11878
+ - `'IMAGERIE/ORTHO_2016'`
11879
+ - `'IMAGERIE/ORTHO_2017'`
11880
+ - `'IMAGERIE/ORTHO_2018'`
11881
+ - `'IMAGERIE/ORTHO_2019'`
11882
+ - `'IMAGERIE/ORTHO_2020'`
11883
+ - `'IMAGERIE/ORTHO_2021'`
11884
+ - `'IMAGERIE/ORTHO_2022_PRINTEMPS'`
11885
+ - `'IMAGERIE/ORTHO_2022_ETE'`
11886
+ - `'IMAGERIE/ORTHO_2023_ETE'`
11887
+ - `'IMAGERIE/ORTHO_LAST'`
11888
+ :type cat: str, optional (Default value = `'IMAGERIE/ORTHO_2022_ETE'`)
11889
+ :return: If `getdata_im` is False, returns (fig, ax), where `fig` is the Matplotlib figure and `ax` is the axis. If `getdata_im` is True, returns (fig, ax, im), where `im` is the image object created by `imshow`.
11890
+ :rtype: tuple
11891
+ """
11892
+
11893
+ # Convert to single block
11894
+ single_block = self.as_WolfArray()
11895
+
11896
+ return single_block.plot_matplotlib(figax=figax, getdata_im=getdata_im, update_palette=update_palette, vmin=vmin, vmax=vmax, figsize=figsize, Walonmap=Walonmap, cat=cat)
11897
+
11104
11898
 
11105
11899
  class WolfArrayMNAP(WolfArrayMB):
11106
11900
  """