wolfhece 2.1.126__py3-none-any.whl → 2.1.128__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,
@@ -1576,6 +1697,14 @@ class CropDialog(wx.Dialog):
1576
1697
  self.ex.SetValue(str(header.origx + header.nbx * header.dx))
1577
1698
  self.ey.SetValue(str(header.origy + header.nby * header.dy))
1578
1699
 
1700
+ def get_crop(self):
1701
+ """ Return the crop values """
1702
+ try:
1703
+ return [[float(self.ox.Value), float(self.oy.Value)], [float(self.ex.Value), float(self.ey.Value)]]
1704
+ except:
1705
+ logging.error(_('Values must be numbers'))
1706
+ return None
1707
+
1579
1708
  import string
1580
1709
  class IntValidator(wx.Validator):
1581
1710
  ''' Validates data as it is entered into the text controls. '''
@@ -1912,6 +2041,8 @@ class Ops_Array(wx.Frame):
1912
2041
 
1913
2042
  self.labelling = wx.Button(self.tools, wx.ID_ANY, _("Labelling"), wx.DefaultPosition,wx.DefaultSize, 0)
1914
2043
 
2044
+ self.statistics = wx.Button(self.tools, wx.ID_ANY, _("Statistics"), wx.DefaultPosition,wx.DefaultSize, 0)
2045
+
1915
2046
  self.clean = wx.Button(self.tools, wx.ID_ANY, _("Clean"), wx.DefaultPosition,wx.DefaultSize, 0)
1916
2047
 
1917
2048
  self.extract_selection = wx.Button(self.tools, wx.ID_ANY, _("Extract selection"), wx.DefaultPosition,wx.DefaultSize, 0)
@@ -1930,6 +2061,7 @@ class Ops_Array(wx.Frame):
1930
2061
  Toolssizer.Add(self.labelling, 1, wx.EXPAND)
1931
2062
  Toolssizer.Add(self.clean, 1, wx.EXPAND)
1932
2063
  Toolssizer.Add(self.extract_selection, 1, wx.EXPAND)
2064
+ Toolssizer.Add(self.statistics, 1, wx.EXPAND)
1933
2065
  Toolssizer.Add(cont_sizer, 1, wx.EXPAND)
1934
2066
 
1935
2067
  self.ApplyTools.SetToolTip(_("Apply Nullvalue into memory/object"))
@@ -1938,6 +2070,7 @@ class Ops_Array(wx.Frame):
1938
2070
  self.labelling.SetToolTip(_("Labelling of contiguous zones using Scipy.label function\n\nReplacing the current values by the labels"))
1939
2071
  self.clean.SetToolTip(_("Clean the array\n\nRemove small isolated patches of data"))
1940
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"))
1941
2074
 
1942
2075
  self.tools.SetSizer(Toolssizer)
1943
2076
  self.tools.Layout()
@@ -2310,6 +2443,7 @@ class Ops_Array(wx.Frame):
2310
2443
  self.filter_zone.Bind(wx.EVT_BUTTON, self.OnFilterZone)
2311
2444
  self.clean.Bind(wx.EVT_BUTTON, self.OnClean)
2312
2445
  self.labelling.Bind(wx.EVT_BUTTON, self.OnLabelling)
2446
+ self.statistics.Bind(wx.EVT_BUTTON, self.OnStatistics)
2313
2447
  self.extract_selection.Bind(wx.EVT_BUTTON, self.OnExtractSelection)
2314
2448
  self._contour_int.Bind(wx.EVT_BUTTON, self.OnContourInt)
2315
2449
  self._contour_list.Bind(wx.EVT_BUTTON, self.OnContourList)
@@ -2784,6 +2918,40 @@ class Ops_Array(wx.Frame):
2784
2918
 
2785
2919
  self.parentarray.labelling()
2786
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
+
2787
2955
  def OnExtractSelection(self, event:wx.MouseEvent):
2788
2956
  """ Extract the current selection """
2789
2957
 
@@ -5122,7 +5290,7 @@ class WolfArray(Element_To_Draw, header_wolf):
5122
5290
  :param fname: filename/filepath - if provided, the file will be read on disk
5123
5291
  :param mold: initialize from a copy a the mold object --> must be a WolArray if not None
5124
5292
  :param masknull: mask data based on the nullvalue
5125
- :param crop: crop data based on the spatial extent [[xmin, xmax],[ymin,ymax]]
5293
+ :param crop: crop data based on the spatial extent [[xmin, xmax], [ymin,ymax]]
5126
5294
  :param whichtype: type of the numpy array (float32 as default)
5127
5295
  :param preload: True = load data during initialization ; False = waits for the display to be required
5128
5296
  :param create: True = create a new array from wxDialog
@@ -5885,6 +6053,30 @@ class WolfArray(Element_To_Draw, header_wolf):
5885
6053
  if reset_plot:
5886
6054
  self.reset_plot()
5887
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
+
5888
6080
  def export_geotif(self, outdir:str= '', extent:str= '', EPSG:int = 31370):
5889
6081
  """
5890
6082
  Export de la matrice au format Geotiff (Lambert 72 - EPSG:31370)
@@ -5975,7 +6167,15 @@ class WolfArray(Element_To_Draw, header_wolf):
5975
6167
  band.SetNoDataValue(nullvalue)
5976
6168
  band.WriteArray(np.flipud(arr.data.transpose()))
5977
6169
  band.FlushCache()
5978
- 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
+
5979
6179
 
5980
6180
 
5981
6181
  def get_dxdy_min(self):
@@ -6073,12 +6273,16 @@ class WolfArray(Element_To_Draw, header_wolf):
6073
6273
 
6074
6274
  fn_crop = fn + '_crop.tif'
6075
6275
  if type(crop) is np.ndarray:
6076
- pass
6276
+ if crop.shape == (4,):
6277
+ logging.error(_('Crop must be a list or a numpy array with 4 values - [[xmin, xmax], [ymin, ymax]]'))
6278
+ crop = [[crop[0], crop[1]], [crop[2], crop[3]]]
6077
6279
  elif type(crop) is list:
6078
- pass
6280
+ if len(crop) == 4:
6281
+ logging.error(_('Crop must be a list or a numpy array with 4 values - [[xmin, xmax], [ymin, ymax]]'))
6282
+ crop = [[crop[0], crop[1]], [crop[2], crop[3]]]
6079
6283
  else:
6080
6284
  if not self.wx_exists:
6081
- logging.error(_('Crop must be a list or a numpy array with 4 values - xmin, xmax, ymin, ymax'))
6285
+ logging.error(_('WX App is required to display the UI'))
6082
6286
  return
6083
6287
 
6084
6288
  raster_in:gdal.Dataset
@@ -6141,8 +6345,8 @@ class WolfArray(Element_To_Draw, header_wolf):
6141
6345
  newcrop.Destroy()
6142
6346
  return
6143
6347
  else:
6144
- crop = [float(newcrop.ox.Value), float(newcrop.ex.Value),
6145
- float(newcrop.oy.Value), float(newcrop.ey.Value)]
6348
+ crop = [[float(newcrop.ox.Value), float(newcrop.ex.Value)],
6349
+ [float(newcrop.oy.Value), float(newcrop.ey.Value)]]
6146
6350
 
6147
6351
  tmpdx = float(newcrop.dx.Value)
6148
6352
  tmpdy = float(newcrop.dy.Value)
@@ -6153,7 +6357,7 @@ class WolfArray(Element_To_Draw, header_wolf):
6153
6357
 
6154
6358
  newcrop.Destroy()
6155
6359
 
6156
- xmin, xmax, ymin, ymax = crop
6360
+ [xmin, xmax], [ymin, ymax] = crop
6157
6361
 
6158
6362
  gdal.Translate(tmpfile, fn, projWin=[xmin, ymax, xmax, ymin])
6159
6363
  else:
@@ -7800,7 +8004,8 @@ class WolfArray(Element_To_Draw, header_wolf):
7800
8004
 
7801
8005
  return newArray
7802
8006
 
7803
- 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'):
7804
8009
  """
7805
8010
  Mask nodes outside a polygon and set values to nullvalue
7806
8011
 
@@ -7811,7 +8016,7 @@ class WolfArray(Element_To_Draw, header_wolf):
7811
8016
  # (mesh coord, 0-based)
7812
8017
 
7813
8018
  # trouve les indices dans le polygone
7814
- 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)
7815
8020
 
7816
8021
  self.array.mask.fill(True) # Mask everything
7817
8022
 
@@ -7824,7 +8029,8 @@ class WolfArray(Element_To_Draw, header_wolf):
7824
8029
 
7825
8030
  self.count()
7826
8031
 
7827
- 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'):
7828
8034
  """
7829
8035
  Mask nodes inside a polygon and set values to nullvalue
7830
8036
 
@@ -7835,7 +8041,7 @@ class WolfArray(Element_To_Draw, header_wolf):
7835
8041
  # (mesh coord, 0-based)
7836
8042
 
7837
8043
  # trouve les indices dans le polygone
7838
- 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)
7839
8045
 
7840
8046
  if set_nullvalue:
7841
8047
  # annulation des valeurs en dehors du polygone
@@ -7846,12 +8052,47 @@ class WolfArray(Element_To_Draw, header_wolf):
7846
8052
 
7847
8053
  self.count()
7848
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
+
7849
8078
  # *************************************************************************************************************************
7850
8079
  # POSITION and VALUES associated to a vector/polygon/polyline
7851
8080
  # These functions can not be stored in header_wolf, because wa can use the mask of the array to limit the search
7852
8081
  # These functions are also present in WolfResults_2D, but they are not exactly the same due to the structure of the results
7853
8082
  # *************************************************************************************************************************
7854
- 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):
7855
8096
  """
7856
8097
  Return the coordinates inside a polygon
7857
8098
 
@@ -7880,7 +8121,10 @@ class WolfArray(Element_To_Draw, header_wolf):
7880
8121
 
7881
8122
  return mypointsxy
7882
8123
 
7883
- 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):
7884
8128
  """
7885
8129
  Return the coordinates inside a polygon
7886
8130
 
@@ -7889,15 +8133,29 @@ class WolfArray(Element_To_Draw, header_wolf):
7889
8133
  """
7890
8134
 
7891
8135
  if isinstance(myvect, vector):
7892
- # force la mise à jour des min/max
7893
8136
  myvect.find_minmax()
7894
- polygon = myvect.asshapely_pol()
8137
+ polygon = myvect.polygon
7895
8138
  elif isinstance(myvect, Polygon):
7896
8139
  polygon = myvect
7897
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
7898
8146
  mypointsxy, mypointsij = self.get_xy_infootprint_vect(myvect)
7899
8147
 
7900
- 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
7901
8159
 
7902
8160
  mypointsxy = mypointsxy[np.where(inside)]
7903
8161
 
@@ -7908,6 +8166,33 @@ class WolfArray(Element_To_Draw, header_wolf):
7908
8166
 
7909
8167
  return mypointsxy
7910
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
+
7911
8196
  def get_xy_under_polyline(self, myvect: vector, usemask:bool=True):
7912
8197
  """
7913
8198
  Return the coordinates along a polyline
@@ -7921,7 +8206,27 @@ class WolfArray(Element_To_Draw, header_wolf):
7921
8206
 
7922
8207
  return mypoints
7923
8208
 
7924
- 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.):
7925
8230
  """
7926
8231
  Return the indices inside a polygon
7927
8232
 
@@ -7931,7 +8236,8 @@ class WolfArray(Element_To_Draw, header_wolf):
7931
8236
  """
7932
8237
 
7933
8238
  # force la mise à jour des min/max
7934
- myvect.find_minmax()
8239
+ if isinstance(myvect, vector):
8240
+ myvect.find_minmax()
7935
8241
 
7936
8242
  mypointsxy, mypointsij = self.get_xy_infootprint_vect(myvect, eps=eps)
7937
8243
 
@@ -7949,32 +8255,201 @@ class WolfArray(Element_To_Draw, header_wolf):
7949
8255
 
7950
8256
  return mypointsij
7951
8257
 
7952
- 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:
7953
8348
  """ Return True if the array intersects the polygon
7954
8349
 
7955
8350
  :param myvect: target vector
7956
8351
  :param usemask: limit potential nodes to unmaksed nodes
7957
8352
  """
7958
8353
 
7959
- 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
7960
8359
 
7961
- 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:
7962
8364
  """ Return True if the array intersects the polygon
7963
8365
 
7964
8366
  :param myvect: target vector
7965
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]:
8381
+ """
8382
+ Element-wise intersection with a list of polygons
7966
8383
  """
7967
- return self.get_xy_inside_polygon_shapely(myvect, usemask).shape[0] > 0
7968
8384
 
7969
- def get_ij_under_polyline(self, myvect: vector, usemask:bool=True):
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))
8394
+
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.):
7970
8434
  """
7971
8435
  Return the indices along a polyline
7972
8436
 
7973
8437
  :param myvect: target vector
7974
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)
7975
8440
  """
7976
8441
 
7977
- 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)
7978
8453
  pts = myvect._refine2D(ds)
7979
8454
 
7980
8455
  allij = np.asarray([self.get_ij_from_xy(curpt.x, curpt.y) for curpt in pts])
@@ -7990,7 +8465,7 @@ class WolfArray(Element_To_Draw, header_wolf):
7990
8465
 
7991
8466
  return allij
7992
8467
 
7993
- 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]:
7994
8469
  """
7995
8470
  Récupération des valeurs contenues dans un polygone
7996
8471
 
@@ -8005,6 +8480,44 @@ class WolfArray(Element_To_Draw, header_wolf):
8005
8480
  else:
8006
8481
  return myvalues, None
8007
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
+
8008
8521
  def get_values_underpoly(self, myvect: vector, usemask:bool=True, getxy:bool=False):
8009
8522
  """
8010
8523
  Récupération des valeurs contenues sous une polyligne
@@ -8047,6 +8560,63 @@ class WolfArray(Element_To_Draw, header_wolf):
8047
8560
 
8048
8561
  return self.get_values_underpoly(myvect, usemask, getxy)
8049
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
+
8050
8620
  # *************************************************************************************************************************
8051
8621
  # END POSITION and VALUES associated to a vector/polygon/polyline
8052
8622
  # *************************************************************************************************************************
@@ -9073,7 +9643,7 @@ class WolfArray(Element_To_Draw, header_wolf):
9073
9643
  else:
9074
9644
  self.mypal.isopop(self.get_working_array(), self.nbnotnull)
9075
9645
 
9076
- if VERSION_RGB==1 :
9646
+ if VERSION_RGB==1 or which == 1:
9077
9647
  if self.nbx * self.nby > 1_000_000 : logging.info(_('Computing colors'))
9078
9648
  if self.wolftype not in [WOLF_ARRAY_FULL_SINGLE, WOLF_ARRAY_FULL_INTEGER8, WOLF_ARRAY_FULL_UINTEGER8]:
9079
9649
  # FIXME: Currently, only some types are supported in Cython/OpenGL library
@@ -9098,7 +9668,7 @@ class WolfArray(Element_To_Draw, header_wolf):
9098
9668
  self._tmp_float32 = None
9099
9669
 
9100
9670
 
9101
- if VERSION_RGB==1 :
9671
+ if VERSION_RGB==1 or which == 1:
9102
9672
  if self.shading:
9103
9673
  pond = (self.shaded.array-.5)*2.
9104
9674
  pmin = (1. - self.shaded.alpha) * self.rgb
@@ -9107,7 +9677,7 @@ class WolfArray(Element_To_Draw, header_wolf):
9107
9677
  self.rgb[pond<0,i] = self.rgb[pond<0,i] * (1.+pond[pond<0]) - pmin[pond<0,i] * pond[pond<0]
9108
9678
  self.rgb[pond>0,i] = self.rgb[pond>0,i] * (1.-pond[pond>0]) + pmax[pond>0,i] * pond[pond>0]
9109
9679
 
9110
- 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.]
9111
9681
 
9112
9682
  if self.myops is not None:
9113
9683
  # update the wx
@@ -9254,22 +9824,94 @@ class WolfArray(Element_To_Draw, header_wolf):
9254
9824
  self.mygrid = {}
9255
9825
  self.gridmaxscales = -1
9256
9826
 
9257
- 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'):
9258
9834
  """
9259
9835
  Plot the array - Matplotlib version
9260
9836
 
9261
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
9262
9884
  """
9263
9885
 
9264
9886
  self.mask_data(self.nullvalue)
9265
- self.updatepalette(0)
9887
+ if update_palette:
9888
+ self.updatepalette(0)
9889
+
9266
9890
  if figax is None:
9267
- fig, ax = plt.subplots()
9891
+ fig, ax = plt.subplots(figsize=figsize)
9268
9892
  else:
9269
9893
  fig, ax = figax
9270
9894
 
9271
- im = ax.imshow(self.array.transpose(), origin='lower', cmap=self.mypal,
9272
- 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)
9273
9915
  ax.set_aspect('equal')
9274
9916
 
9275
9917
  if getdata_im:
@@ -9601,7 +10243,8 @@ class WolfArray(Element_To_Draw, header_wolf):
9601
10243
 
9602
10244
  return indicesX,indicesY,contourgen,interior
9603
10245
 
9604
- 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]:
9605
10248
  """
9606
10249
  Create Matplotlib image from WolfArray
9607
10250
  """
@@ -9616,11 +10259,11 @@ class WolfArray(Element_To_Draw, header_wolf):
9616
10259
 
9617
10260
  if cmap is None:
9618
10261
  # update local colors if not already done
9619
- if self[i].rgb is None:
9620
- self[i].updatepalette(0)
10262
+ if self.rgb is None:
10263
+ self.updatepalette(1)
9621
10264
 
9622
10265
  # Pointing RGB
9623
- colors = self[i].rgb
10266
+ colors = self.rgb
9624
10267
  colors[self.array.mask,3] = 0.
9625
10268
  # Plot
9626
10269
  colors = colors.swapaxes(0,1)
@@ -9847,6 +10490,292 @@ class WolfArray(Element_To_Draw, header_wolf):
9847
10490
 
9848
10491
  return zones
9849
10492
 
10493
+ def inpaint(self, mask_array:"WolfArray" = None, test_array:"WolfArray" = None,
10494
+ ignore_last:int = 1, multiprocess:bool = True):
10495
+ """ InPaintaing holes in the array
10496
+
10497
+ :param mask_array: where computation is done
10498
+ :param test_array: used in test -- interpolation is accepted if new value is over test_array
10499
+ :param ignore_last: number of last patches to ignore
10500
+ :param multiprocess: use multiprocess for inpainting
10501
+ """
10502
+
10503
+ from .eikonal import inpaint_array
10504
+
10505
+ if mask_array is None:
10506
+ mask_array = self.array.mask
10507
+ elif isinstance(mask_array, WolfArray):
10508
+ mask_array = mask_array.array.data
10509
+
10510
+ if test_array is None:
10511
+ test_array = np.ones(self.array.data.shape) * self.array.data.min()
10512
+ elif isinstance(test_array, WolfArray):
10513
+ test_array = test_array.array.data
10514
+
10515
+ time, wl_np, wd_np = inpaint_array(self.array.data,
10516
+ mask_array,
10517
+ test_array,
10518
+ ignore_last_patches= ignore_last,
10519
+ dx = self.dx,
10520
+ dy = self.dy,
10521
+ NoDataValue = self.nullvalue,
10522
+ inplace=True,
10523
+ multiprocess= multiprocess)
10524
+
10525
+ wd = WolfArray(mold=self)
10526
+ wd.array[:,:] = wd_np[:,:]
10527
+ wd.mask_data(self.nullvalue)
10528
+
10529
+ wl = self
10530
+
10531
+ self.mask_data(self.nullvalue)
10532
+ self.reset_plot()
10533
+ return time, wl, wd
10534
+
10535
+ def _inpaint_waterlevel_dem_dtm(self, dem:"WolfArray", dtm:"WolfArray",
10536
+ ignore_last:int = 1, use_fortran:bool = False,
10537
+ multiprocess:bool = True):
10538
+ """ InPaintaing waterlevel holes in the array.
10539
+
10540
+ We use DEM and DTM to mask and constraint the inpainting process.
10541
+
10542
+ :param dem: Digital Elevation Model (same as simulation model)
10543
+ :param dtm: Digital Terrain Model
10544
+ :param ignore_last: number of last patches to ignore
10545
+ :param use_fortran: use Fortran inpainting code
10546
+ :param multiprocess: use multiprocess for inpainting
10547
+ """
10548
+
10549
+ if use_fortran:
10550
+ import shutil
10551
+ from tempfile import TemporaryDirectory
10552
+ # check if hoels.exe is available in PATH
10553
+ if shutil.which('holes.exe') is None:
10554
+ logging.error(_('holes.exe not found in PATH'))
10555
+ logging.info(_('We use the Python version of inpainting'))
10556
+ use_fortran = False
10557
+
10558
+ else:
10559
+ with TemporaryDirectory() as tmpdirname:
10560
+ # create mask from DEM, DTM and WL
10561
+
10562
+ mask = self._create_building_holes_dem_dtm(dem, dtm, ignore_last)
10563
+
10564
+ # save mask and dtm to temporary files
10565
+ locdir = Path(tmpdirname)
10566
+ wl_name = locdir / 'array.bin'
10567
+ mask_name = locdir / 'mask.bin'
10568
+ dtm_name = locdir / 'dtm.bin'
10569
+
10570
+ mask.write_all(mask_name)
10571
+ dtm.write_all(dtm_name)
10572
+
10573
+ oldname = self.filename
10574
+ self.write_all(wl_name)
10575
+ self.filename = oldname
10576
+
10577
+ # call holes.exe
10578
+ olddir = os.getcwd()
10579
+
10580
+ # change to temporary directory
10581
+ os.chdir(locdir)
10582
+ shell_command = f'holes.exe inpaint in={wl_name} mask={mask_name} dem={dtm_name} avoid_last={ignore_last} out=temp'
10583
+ logging.info(shell_command)
10584
+ logging.info('Inpainting holes using holes.exe')
10585
+ os.system(shell_command)
10586
+ logging.info('Done - reading inpainted array')
10587
+
10588
+ # read inpainted array
10589
+ wl = WolfArray(locdir / 'temp_combl.tif')
10590
+ wd = WolfArray(locdir / 'temp_h.tif')
10591
+
10592
+ wd.array[wd.array < 0.] = 0.
10593
+ wd.mask_data(0.)
10594
+
10595
+ os.chdir(olddir)
10596
+
10597
+ self.array.data[:,:] = wl.array.data[:,:]
10598
+
10599
+ time = None
10600
+
10601
+ if not use_fortran:
10602
+
10603
+ from .eikonal import inpaint_waterlevel
10604
+
10605
+ time, wl_np, wd_np = inpaint_waterlevel(self.array.data,
10606
+ dem.array.data,
10607
+ dtm.array.data,
10608
+ ignore_last_patches= ignore_last,
10609
+ dx = self.dx,
10610
+ dy = self.dy,
10611
+ NoDataValue = self.nullvalue,
10612
+ inplace=True,
10613
+ multiprocess= multiprocess)
10614
+
10615
+ wd = WolfArray(mold=self)
10616
+ wd.array[:,:] = wd_np[:,:]
10617
+ wd.mask_data(self.nullvalue)
10618
+
10619
+ wl = self
10620
+
10621
+ self.mask_data(self.nullvalue)
10622
+ self.reset_plot()
10623
+ return time, wl, wd
10624
+
10625
+ def count_holes(self, mask:"WolfArray" = None):
10626
+ """ Count holes in the array """
10627
+ from .eikonal import count_holes
10628
+
10629
+ if mask is None:
10630
+ mask = self.array.mask
10631
+ elif isinstance(mask, WolfArray):
10632
+ mask = mask.array.data
10633
+
10634
+ return count_holes(mask)
10635
+
10636
+ def select_holes(self, mask:"WolfArray" = None, ignore_last:int = 1):
10637
+ """ Select holes in the array """
10638
+
10639
+ if mask is None:
10640
+ mask = self.array.mask
10641
+ elif isinstance(mask, WolfArray):
10642
+ mask = mask.array.data
10643
+
10644
+ labels, numfeatures = label(mask)
10645
+
10646
+ # count cells in holes
10647
+ nb_cells = np.bincount(labels.ravel())
10648
+
10649
+ # sort by number of cells
10650
+ idx = np.argsort(nb_cells)
10651
+
10652
+ for i in range(ignore_last):
10653
+ labels[labels == idx[-1-i]] = 0
10654
+
10655
+ # select holes but ignoring last ones
10656
+ ij = np.argwhere(labels)
10657
+
10658
+ # Convert i,j to x,y
10659
+
10660
+ xy = self.ij2xy_np(ij)
10661
+
10662
+ self.SelectionData.myselection = list(xy)
10663
+ self.SelectionData.update_nb_nodes_selection()
10664
+
10665
+ def create_mask_holes(self, ignore_last:int = 1) -> "WolfArray":
10666
+ """ Select holes in the array and create a new aray """
10667
+
10668
+ labels, numfeatures = label(self.array.mask)
10669
+
10670
+ # count cells in holes
10671
+ nb_cells = np.bincount(labels.ravel())
10672
+
10673
+ # sort by number of cells
10674
+ idx = np.argsort(nb_cells)
10675
+
10676
+ for i in range(ignore_last):
10677
+ labels[labels == idx[-1-i]] = 0
10678
+
10679
+ newarray = WolfArray(mold=self)
10680
+ # newarray.allocate_ressources()
10681
+ newarray.array.data[:,:] = labels
10682
+ newarray.array.mask[:,:] = ~labels.astype(bool)
10683
+ newarray.set_nullvalue_in_mask()
10684
+
10685
+ return newarray
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
+
10749
+ def _create_building_holes_dem_dtm(self, dem:"WolfArray", dtm:"WolfArray", ignore_last:int = 1) -> "WolfArray":
10750
+ """ Select holes in the array and create a new aray """
10751
+
10752
+ buildings = dem - dtm
10753
+ buildings.array[buildings.array < 0.] = 0.
10754
+ buildings.array[buildings.array > 0.] = dtm.array[buildings.array > 0.]
10755
+ buildings.array[buildings.array == 0.] = dem.array.max() + 1.
10756
+
10757
+ return buildings
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
+
9850
10779
  class WolfArrayMB(WolfArray):
9851
10780
  """
9852
10781
  Matrice multiblocks
@@ -10073,6 +11002,7 @@ class WolfArrayMB(WolfArray):
10073
11002
  arr.isblock = True
10074
11003
  arr.blockindex = len(self.myblocks) - 1
10075
11004
 
11005
+
10076
11006
  def share_palette(self):
10077
11007
  """Partage de la palette de couleurs entre matrices liées"""
10078
11008
  for cur in self.linkedarrays:
@@ -10775,11 +11705,26 @@ class WolfArrayMB(WolfArray):
10775
11705
  self.translx = 0.
10776
11706
  self.transly = 0.
10777
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
+
10778
11722
  def as_WolfArray(self, abs:bool=True, forced_header:header_wolf = None) -> WolfArray:
10779
11723
  """
10780
- Convert to WolfArray
11724
+ Convert to WolfArray -- Rebin blocks if necessary
10781
11725
 
10782
- 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
10783
11728
  """
10784
11729
 
10785
11730
  newArray = WolfArray()
@@ -10889,6 +11834,67 @@ class WolfArrayMB(WolfArray):
10889
11834
 
10890
11835
  return newArray
10891
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
+
10892
11898
 
10893
11899
  class WolfArrayMNAP(WolfArrayMB):
10894
11900
  """