wolfhece 2.1.118__py3-none-any.whl → 2.1.120__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.
@@ -23,9 +23,10 @@ from ..PyWMS import getWalonmap
23
23
  from ..PyTranslate import _
24
24
  from ..color_constants import Colors
25
25
  from ..PyParams import Wolf_Param, Type_Param
26
- from ..matplotlib_fig import Matplotlib_Figure as mplfig
26
+ from ..matplotlib_fig import Matplotlib_Figure as MplFig, PRESET_LAYOUTS
27
27
  from ..drawing_obj import Element_To_Draw
28
28
  from ..CpGrid import CpGrid
29
+ from ..PyVertexvectors import vector, Zones, zone, wolfvertex
29
30
 
30
31
  """
31
32
  Importation et visualisation de données LAS et LAZ
@@ -374,6 +375,35 @@ class xyz_laz_grid():
374
375
 
375
376
  return retdata
376
377
 
378
+ def find_files_in_bounds(self, bounds:Union[tuple[tuple[float,float],tuple[float,float]], list[list[float, float],list[float, float]]]):
379
+ """ Find all files in bounds """
380
+
381
+ x1=bounds[0][0]
382
+ x2=bounds[0][1]
383
+ y1=bounds[1][0]
384
+ y2=bounds[1][1]
385
+
386
+ file1= self.genfile.split('x1')[0]
387
+ file2= self.genfile.split('y1')[-1]
388
+
389
+ files=[]
390
+
391
+ for x in range(int(self.origx), int(self.endx), int(self.dx)):
392
+ for y in range(int(self.origy), int(self.endy), int(self.dy)):
393
+
394
+ locbounds=np.float64(((x,x+self.dx),(y,y+self.dy)))
395
+
396
+ test = not(x2 < locbounds[0][0] or x1 > locbounds[0][1] or y2 < locbounds[1][0] or y1 > locbounds[1][1])
397
+
398
+ if test:
399
+ fxyz = file1+str(int(x))+'_'+str(int(y))+file2
400
+
401
+ fn = join(self.mydir,fxyz)
402
+ if exists(fn):
403
+ files.append(fn)
404
+
405
+ return files
406
+
377
407
  def _split_xyz(self, dirout:str, nbparts:int = 10):
378
408
  """ Split XYZ file into 'nb' parts along X and Y """
379
409
  for entry in scandir(self.mydir):
@@ -493,7 +523,7 @@ class xyz_laz_grids():
493
523
  """ Ensemble de grids """
494
524
 
495
525
  def __init__(self, dir_grids:str, create:bool=False) -> None:
496
- self.grids = []
526
+ self.grids:list[xyz_laz_grid] = []
497
527
  self.colors = Classification_LAZ()
498
528
  self.colors.init_2023()
499
529
 
@@ -526,6 +556,36 @@ class xyz_laz_grids():
526
556
  logging.info(_('Data found -- {} points'.format(ret.shape[0])))
527
557
  return ret
528
558
 
559
+ def find_files_in_bounds(self, bounds:Union[tuple[tuple[float,float],tuple[float,float]], list[list[float, float],list[float, float]]]):
560
+
561
+ ret = [(cur.mydir, cur.find_files_in_bounds(bounds)) for cur in self.grids]
562
+ ret = [cur for cur in ret if len(cur[1])>0]
563
+
564
+ if len(ret)==0:
565
+ logging.info(_('No data found'))
566
+ return []
567
+ else:
568
+ logging.info(_('Data found -- {} files'.format(len(ret))))
569
+ return ret
570
+
571
+ def copy_files_in_bounds(self, bounds:Union[tuple[tuple[float,float],tuple[float,float]], list[list[float, float],list[float, float]]], dirout:str):
572
+
573
+ import shutil
574
+
575
+ out = Path(dirout)
576
+ out.mkdir(exist_ok=True)
577
+
578
+ files = self.find_files_in_bounds(bounds)
579
+
580
+ for curdir, curfiles in files:
581
+ locdir = dirout / Path(curdir).parent.name
582
+ locdir.mkdir(exist_ok=True)
583
+ for curfile in curfiles:
584
+ shutil.copy(curfile, locdir / Path(curfile).name)
585
+
586
+ # copy gridinfo.txt
587
+ shutil.copy(join(curdir,'gridinfo.txt'), locdir / 'gridinfo.txt')
588
+
529
589
  def read_dir(self, dir_grids):
530
590
  dirs = listdir(dir_grids)
531
591
 
@@ -636,14 +696,11 @@ class xyz_laz_grids():
636
696
 
637
697
  (up_s, up_z, up_color), (down_s, down_z, down_color) = self.scan_around(xy, length_buffer)
638
698
 
639
- figmpl = mplfig()
640
- figmpl.presets()
641
- fig = figmpl.fig
642
- ax = figmpl.cur_ax
699
+ figmpl = MplFig(PRESET_LAYOUTS.DEFAULT)
643
700
 
644
701
  logging.info(_('Plotting'))
645
- ax.scatter(up_s, up_z, c=up_color ,marker='.')
646
- ax.scatter(down_s, down_z,c=down_color,marker='+')
702
+ figmpl.plot(up_s, up_z, c=up_color ,marker='.')
703
+ figmpl.plot(down_s, down_z,c=down_color,marker='+')
647
704
 
648
705
  if show:
649
706
  figmpl.Show()
@@ -687,49 +744,116 @@ class xyz_laz_grids():
687
744
  force_format=force_format))
688
745
 
689
746
 
690
- class Base_LAZ_Data(Element_To_Draw):
747
+ class Wolf_LAZ_Data(Element_To_Draw):
748
+ """ Base class for LAZ data which can be imported in Pydraw.Mapviewer.
749
+ """
691
750
 
692
751
  def __init__(self, idx:str = '', plotted:bool = False, mapviewer = None, need_for_wx:bool = False) -> None:
693
752
 
753
+ self._filename:Path = Path('') # filename TODO : improve serialization
754
+ self._lazfile:Path = None # Not used
755
+
756
+ # Bounds must be set before calling super().__init__() because
757
+ # xmin, xmax, ymin, ymax properties are used and depend on _bounds
694
758
  self._bounds = [[0.,0.],[0.,0.]] # [[xmin,xmax],[ymin,ymax]]
759
+
695
760
  super().__init__(idx, plotted, mapviewer, need_for_wx)
696
761
 
697
- self.lazfile:Path = None
698
- self._data:np.ndarray = None
699
- self.classification = Classification_LAZ()
762
+ self._data:np.ndarray = None # Numpy data array -- to be plotted
763
+ self._colors:np.ndarray = None # NumPy array of colors for each point --> see viewer attributes for details
764
+ self.classification = Classification_LAZ() # Classification of LAZ data --> defining colors if codification is used
700
765
 
701
- self.associated_color:int = Colors_Lazviewer.CODE_2023.value
766
+ self._associated_color:int = Colors_Lazviewer.CODE_2023.value # Associated color type for LAZ data
702
767
 
703
- self.viewer:viewer = None
704
- self._colors:np.ndarray = None
768
+ self.viewer:viewer = None # PPTK viewer
705
769
 
706
- self._point_size = .05
770
+ self._point_size = .05 # Point size in viewer -- in user units
707
771
 
708
- self._memory = []
772
+ self._flight_memory = [] # Flight position
709
773
 
710
- self._myprops = None
774
+ self._myprops = None # Properties based on Wolf_Param class
711
775
 
712
- self._bg_color = [0,0,0,255]
713
- self._bg_color_top = [0,0,0,255]
714
- self._bg_color_bottom = [0,0,0,255]
776
+ self._bg_color = [int(0.23*255), int(0.23*255), int(.44*255), 255] # Background color
777
+ self._bg_color_top = [0, 0, 0, 255] # Top background color
778
+ self._bg_color_bottom = [int(0.23*255), int(0.23*255), int(.44*255), 255] # Bottom background color
715
779
 
716
- self._floor_level = 0.
717
- self._floor_color = [0,0,0,255]
780
+ self._floor_level = 0. # Floor level -- user units
781
+ self._floor_color = [int(0.3*255), int(0.3*255), int(.3*255), 127] # Floor color
718
782
 
719
- self._show_grid = True
720
- self._show_axis = True
721
- self._show_info = True
783
+ self._show_grid = True # Show grid in viewer
784
+ self._show_axis = True # Show axis in viewer
785
+ self._show_info = True # Show info in viewer
722
786
 
723
- self._select_only_codes = []
787
+ self._select_only_codes = [] # Codes to be selected -- Defined by user
724
788
 
725
- self._xls:CpGrid = None
726
- self._xlsFrame:wx.Frame = None
789
+ self._xls:CpGrid = None # Excel grid for selected data
790
+ self._xlsFrame:wx.Frame = None # Frame containing the xls grid
727
791
 
728
- # self._auto_update_properties = False
729
- # self._auto_update_interval = 100 # ms
730
- # self._timer = None
792
+ def serialize(self):
793
+ """ Serialize class : data and attributes """
794
+ return {'bounds':self._bounds, 'data':str(self._filename) + '.npz', 'associated_color':self._associated_color,
795
+ 'point_size':self._point_size, 'bg_color':self._bg_color, 'bg_color_top':self._bg_color_top,
796
+ 'bg_color_bottom':self._bg_color_bottom, 'floor_level':self._floor_level, 'floor_color':self._floor_color,
797
+ 'show_grid':self._show_grid, 'show_axis':self._show_axis, 'show_info':self._show_info}
798
+
799
+ def deserialize(self, data:dict):
800
+ """ Deserialize class : data and attributes """
801
+ self._bounds = data['bounds']
802
+ self._filename = Path(data['data'])
803
+ self._associated_color = data['associated_color']
804
+ self._point_size = data['point_size']
805
+ self._bg_color = data['bg_color']
806
+ self._bg_color_top = data['bg_color_top']
807
+ self._bg_color_bottom = data['bg_color_bottom']
808
+ self._floor_level = data['floor_level']
809
+ self._floor_color = data['floor_color']
810
+ self._show_grid = data['show_grid']
811
+ self._show_axis = data['show_axis']
812
+ self._show_info = data['show_info']
813
+
814
+ self.data = np.load(self._filename)['data']
815
+
816
+ def saveas(self, fn:str):
817
+ """ Save class : data and attributes """
818
+ import pickle
819
+
820
+ self._filename = Path(fn)
821
+
822
+ with open(fn,'wb') as f:
823
+ pickle.dump(self.serialize(),f)
824
+
825
+ # save data by numpy
826
+ np.savez(str(fn) + '.npz', data=self._data)
827
+
828
+ def load(self, fn:str):
829
+ """ Load class : data and attributes """
830
+ import pickle
831
+
832
+ with open(fn,'rb') as f:
833
+ self.deserialize(pickle.load(f))
834
+
835
+ @property
836
+ def associated_color(self):
837
+ return self._associated_color
838
+
839
+ @associated_color.setter
840
+ def associated_color(self, value:int):
841
+ self._associated_color = value
842
+ self.set_colors()
843
+
844
+ def merge(self, other:"Wolf_LAZ_Data"):
845
+ """ Merge two Wolf_LAZ_Data objects """
846
+
847
+ if self._data is None:
848
+ self._data = other._data
849
+ else:
850
+ self._data = np.concatenate((self._data, other._data))
851
+
852
+ self.bounds = [[min(self.bounds[0][0],other.bounds[0][0]),max(self.bounds[0][1],other.bounds[0][1])],
853
+ [min(self.bounds[1][0],other.bounds[1][0]),max(self.bounds[1][1],other.bounds[1][1])]]
731
854
 
732
855
  def filter_data(self, codes:list[int]):
856
+ """ Filter data by codes """
733
857
  self._data = self._data[np.isin(self._data[:,3],codes)]
734
858
 
735
859
  def bg_color(self, value):
@@ -746,7 +870,7 @@ class Base_LAZ_Data(Element_To_Draw):
746
870
 
747
871
  def floor_level(self, value):
748
872
  if self.viewer is not None:
749
- return self.viewer.set(floor_level = value)
873
+ return self.viewer.set(floor_level = float(value))
750
874
 
751
875
  def floor_color(self, value):
752
876
  if self.viewer is not None:
@@ -754,15 +878,24 @@ class Base_LAZ_Data(Element_To_Draw):
754
878
 
755
879
  def show_grid(self, value:bool):
756
880
  if self.viewer is not None:
757
- return self.viewer.set(show_grid = value)
881
+ return self.viewer.set(show_grid = bool(value))
758
882
 
759
883
  def show_axis(self, value:bool):
760
884
  if self.viewer is not None:
761
- return self.viewer.set(show_axis = value)
885
+ return self.viewer.set(show_axis = bool(value))
762
886
 
763
887
  def show_info(self, value:bool):
764
888
  if self.viewer is not None:
765
- return self.viewer.set(show_info = value)
889
+ return self.viewer.set(show_info = bool(value))
890
+
891
+ def force_view(self, x, y, z = -1):
892
+ """ Force lookat position """
893
+ if z == -1:
894
+ curx,cury,curz = self.lookat
895
+ self.lookat = [x,y,curz]
896
+ else:
897
+ self.lookat = [x,y,z]
898
+ # self.eye = self._eye_pos()
766
899
 
767
900
  @property
768
901
  def selected(self):
@@ -772,6 +905,10 @@ class Base_LAZ_Data(Element_To_Draw):
772
905
 
773
906
  @property
774
907
  def xyz_selected(self) -> np.ndarray:
908
+ """ Extract the selected points from the viewer.
909
+
910
+ Filter the selected points by codes if _select_only_codes is not empty."""
911
+
775
912
  if self.viewer is None:
776
913
  return None
777
914
 
@@ -799,21 +936,25 @@ class Base_LAZ_Data(Element_To_Draw):
799
936
  @property
800
937
  def num_points(self):
801
938
  """ Number of points """
939
+
802
940
  nb1 = self.data.shape[0]
803
941
  if self.viewer is not None:
804
- nb2 = self.viewer.get('num_points')[0]
805
- assert nb1 == nb2, _('Incoherent number of points')
806
-
942
+ try:
943
+ nb2 = self.viewer.get('num_points')[0]
944
+ assert nb1 == nb2, _('Incoherent number of points')
945
+ except:
946
+ # viewer is not initialized or Destroyed
947
+ self.viewer = None
807
948
  return nb1
808
949
 
809
950
  @property
810
951
  def nb_points(self):
811
- """ Number of points """
952
+ """ Number of points - alias of num_points """
812
953
  return self.num_points
813
954
 
814
955
  @property
815
956
  def nb(self):
816
- """ Number of points """
957
+ """ Number of points - alias of num_points """
817
958
  return self.num_points
818
959
 
819
960
  @property
@@ -823,36 +964,46 @@ class Base_LAZ_Data(Element_To_Draw):
823
964
 
824
965
  @property
825
966
  def mvp(self):
967
+ """ Model View Projection matrix
968
+
969
+ See https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93projection_matrix for details
970
+ """
971
+
826
972
  if self.viewer is None:
827
973
  return None
828
974
  return self.viewer.get('mvp')
829
975
 
830
976
  @property
831
977
  def eye(self):
978
+ """ eye/camera position """
832
979
  if self.viewer is None:
833
980
  return None
834
981
  return self.viewer.get('eye')
835
982
 
836
983
  @property
837
984
  def lookat(self):
985
+ """ Lookat position """
838
986
  if self.viewer is None:
839
987
  return None
840
988
  return self.viewer.get('lookat')
841
989
 
842
990
  @property
843
991
  def phi(self):
992
+ """ Azimuth angle (radians) """
844
993
  if self.viewer is None:
845
994
  return None
846
995
  return self.viewer.get('phi')[0]
847
996
 
848
997
  @property
849
998
  def theta(self):
999
+ """ Elevation angle (radians) """
850
1000
  if self.viewer is None:
851
1001
  return None
852
1002
  return self.viewer.get('theta')[0]
853
1003
 
854
1004
  @property
855
1005
  def r(self):
1006
+ """ Distance from lookat """
856
1007
  if self.viewer is None:
857
1008
  return None
858
1009
  return self.viewer.get('r')[0]
@@ -862,24 +1013,48 @@ class Base_LAZ_Data(Element_To_Draw):
862
1013
  if self.viewer is None:
863
1014
  return None
864
1015
  self.viewer.set(phi=value)
1016
+ self._fill_props()
865
1017
 
866
1018
  @theta.setter
867
1019
  def theta(self, value):
868
1020
  if self.viewer is None:
869
1021
  return None
870
1022
  self.viewer.set(theta=value)
1023
+ self._fill_props()
871
1024
 
872
1025
  @r.setter
873
1026
  def r(self, value):
874
1027
  if self.viewer is None:
875
1028
  return None
876
1029
  self.viewer.set(r=value)
1030
+ self._fill_props()
877
1031
 
878
1032
  @lookat.setter
879
1033
  def lookat(self, value):
880
1034
  if self.viewer is None:
881
1035
  return None
882
1036
  self.viewer.set(lookat=value)
1037
+ # self.viewer.set(lookat=value, phi=self.phi, theta=self.theta, r=self.r)
1038
+ self._fill_props()
1039
+
1040
+ def _eye_pos(self):
1041
+ """ Compute eye pos from lookat and r, phi, theta.
1042
+
1043
+ phi is the azimuth angle (radians)
1044
+ theta is the elevation angle (radians)
1045
+ r is the distance from lookat
1046
+ """
1047
+
1048
+ lx, ly, lz = self.lookat
1049
+ r = self.r
1050
+ phi = self.phi
1051
+ theta = self.theta
1052
+
1053
+ x = lx + r*np.sin(phi)*np.cos(theta)
1054
+ y = ly + r*np.cos(phi)*np.cos(theta)
1055
+ z = lz + r*np.sin(theta)
1056
+
1057
+ return [x,y,z]
883
1058
 
884
1059
  @eye.setter
885
1060
  def eye(self, value):
@@ -892,11 +1067,13 @@ class Base_LAZ_Data(Element_To_Draw):
892
1067
  # Compute phi, theta, r based on eye and lookat and right vector
893
1068
  r = np.sqrt((x-lx)**2 + (y-ly)**2 + (z-lz)**2)
894
1069
  self.r =r
895
- self.phi = np.arccos((z-lz)/r)
896
- self.theta = np.arctan2((y-ly),(x-lx))
1070
+ self.theta = np.arcsin((z-lz)/r)
1071
+ self.phi = -np.arctan2((x-lx),(y-ly))
1072
+ self._fill_props()
897
1073
 
898
1074
  @property
899
1075
  def point_size(self):
1076
+ """ Point size in viewer -- user units """
900
1077
  return self._point_size
901
1078
 
902
1079
  @point_size.setter
@@ -904,6 +1081,7 @@ class Base_LAZ_Data(Element_To_Draw):
904
1081
  self._point_size = value
905
1082
  if self.viewer is not None:
906
1083
  self.viewer.set(point_size=value)
1084
+ self._fill_props()
907
1085
 
908
1086
  @property
909
1087
  def xyz(self):
@@ -914,17 +1092,18 @@ class Base_LAZ_Data(Element_To_Draw):
914
1092
  return self.data[:,3]
915
1093
 
916
1094
  def codes_unique(self):
1095
+ """ Only unique codes """
917
1096
  return list(np.unique(self.codes).astype(int))
918
1097
 
919
1098
  def create_viewer(self, color_code:Colors_Lazviewer = None, classification:Classification_LAZ = None):
920
1099
  """ Create a viewer for las data """
921
1100
 
922
- if color_code is not None:
923
- self.associated_color = color_code
924
-
925
1101
  if classification is not None:
926
1102
  self.classification = classification
927
1103
 
1104
+ if color_code is not None:
1105
+ self.associated_color = color_code
1106
+
928
1107
  self._colors = get_colors(self.data, self.associated_color, palette_classif= self.classification)
929
1108
 
930
1109
  self.viewer = viewer(self.xyz, self._colors)
@@ -933,6 +1112,7 @@ class Base_LAZ_Data(Element_To_Draw):
933
1112
  return self.viewer
934
1113
 
935
1114
  def interactive_update_colors(self):
1115
+ """ Create a frame to interactively update colors """
936
1116
 
937
1117
  self.classification.interactive_update_colors()
938
1118
 
@@ -945,12 +1125,17 @@ class Base_LAZ_Data(Element_To_Draw):
945
1125
  self.classification._choose_colors.callback = new_callback_colors
946
1126
 
947
1127
  def set_colors(self):
1128
+ """ Set colors in viewer --> using attributes method (not colormap) """
1129
+
948
1130
  if self.viewer is not None:
949
1131
  self._colors = get_colors(self.data, self.associated_color, palette_classif= self.classification)
950
1132
  self.viewer.attributes(self._colors)
951
1133
 
952
1134
  def set_classification(self, classification:str = None):
1135
+ """ Set classification of LAZ data
953
1136
 
1137
+ TODO : Check if 2020-2022 SPW campaign is the same classification as 2013
1138
+ """
954
1139
  if classification is None:
955
1140
  logging.warning(_('No classification chosen - Abort !'))
956
1141
  elif classification == 'SPW 2013-2014':
@@ -959,6 +1144,7 @@ class Base_LAZ_Data(Element_To_Draw):
959
1144
  self.classification.init_2023()
960
1145
 
961
1146
  def find_minmax(self, update=False):
1147
+ """ Find min and max of data """
962
1148
  if self.data is not None:
963
1149
  self.bounds = [[np.min(self.data[:,0]), np.max(self.data[:,0])],[np.min(self.data[:,1]), np.max(self.data[:,1])]]
964
1150
 
@@ -996,6 +1182,7 @@ class Base_LAZ_Data(Element_To_Draw):
996
1182
 
997
1183
  @property
998
1184
  def data(self):
1185
+ """ Full data array (x,y,z,code) """
999
1186
  return self._data
1000
1187
 
1001
1188
  @data.setter
@@ -1010,87 +1197,106 @@ class Base_LAZ_Data(Element_To_Draw):
1010
1197
  def bounds(self, value):
1011
1198
  self._bounds = value
1012
1199
 
1013
- def from_grid(self, grid:xyz_laz_grid, bounds:Union[tuple[tuple[float,float],tuple[float,float]], list[list[float, float],list[float, float]]]):
1014
-
1200
+ def from_grid(self, grid:xyz_laz_grids, bounds:Union[tuple[tuple[float,float],tuple[float,float]], list[list[float, float],list[float, float]]]):
1201
+ """ Create data from grid LAZ """
1015
1202
  self.bounds = bounds
1016
1203
  self.data = grid.scan(bounds)
1017
1204
 
1018
1205
  def from_file(self, fn:str):
1019
-
1206
+ """ Create data from LAZ file """
1020
1207
  self.data = read_laz(fn)
1021
1208
  self.bounds = [[np.min(self.data[:,0]), np.max(self.data[:,0])],[np.min(self.data[:,1]), np.max(self.data[:,1])]]
1022
1209
 
1023
- def decimate(self, step:int):
1210
+ def descimate(self, step:int):
1211
+ """ Descimate data.
1212
+
1213
+ Conserve only one point every 'step' points.
1214
+
1215
+ :param step: step of descimation
1216
+ """
1024
1217
  self.data = self.data[::step]
1025
1218
 
1026
1219
  def get_data_class(self, key:int):
1220
+ """ Get data with a specific code """
1221
+
1222
+ assert isinstance(key, int), _('Key must be an integer')
1223
+
1027
1224
  return self.data[self.data[:,3] == key]
1028
1225
 
1029
1226
  def add_pose_in_memory(self, key_time:float = 1.):
1227
+ """ Add current pose in flight memory """
1228
+
1030
1229
  if self.viewer is not None:
1031
1230
  lookat = self.lookat
1032
1231
  new_pose = (lookat[0], lookat[1], lookat[2], self.phi, self.theta, self.r)
1033
- self._memory.append((new_pose, key_time))
1232
+ self._flight_memory.append((new_pose, key_time))
1034
1233
 
1035
1234
  def play_flight(self, tlim=[-np.inf, np.inf], repeat=False, interp='cubic_natural'):
1036
-
1235
+ """ Play flight memory """
1037
1236
  if self.viewer is not None:
1038
- if len(self._memory)>0:
1039
- poses = [cur[0] for cur in self._memory]
1237
+ if len(self._flight_memory)>0:
1238
+ poses = [cur[0] for cur in self._flight_memory]
1040
1239
  times = [0.]
1041
- for i in range(1,len(self._memory)):
1042
- times.append(times[-1]+self._memory[i][1])
1240
+ for i in range(1,len(self._flight_memory)):
1241
+ times.append(times[-1]+self._flight_memory[i][1])
1043
1242
  self.viewer.play(poses, times, tlim, repeat, interp)
1044
1243
 
1045
1244
  def set_times(self, times:np.ndarray):
1046
- if len(self._memory)>0:
1047
- self._memory = [(self._memory[i][0], times[i]) for i in range(len(self._memory))]
1245
+ """ Set times for flight memory """
1246
+ if len(self._flight_memory)>0:
1247
+ self._flight_memory = [(self._flight_memory[i][0], times[i]) for i in range(len(self._flight_memory))]
1048
1248
 
1049
1249
  def set_times_increment(self, increment:float):
1050
- if len(self._memory)>0:
1051
- self._memory = [(self._memory[i][0], increment*i) for i in range(len(self._memory))]
1250
+ if len(self._flight_memory)>0:
1251
+ self._flight_memory = [(self._flight_memory[i][0], increment*i) for i in range(len(self._flight_memory))]
1052
1252
 
1053
1253
  def get_times(self):
1054
- return np.asarray([cur[1] for cur in self._memory])
1254
+ return np.asarray([cur[1] for cur in self._flight_memory])
1255
+
1256
+ def record_flight(self, dirout:str, tlim=[-np.inf, np.inf], interp='cubic_natural', fps=24, prefix:str = 'laz_', ext:str = 'png'):
1257
+ """ Record flight memory in multiple images
1055
1258
 
1056
- def record_flight(self, dirout:str, tlim=[-np.inf, np.inf], interp='cubic_natural', fps=24, prefix:str = 'laz_', ext:str = '.png'):
1259
+ FIXME : FREEZE the app --> to debug
1260
+ """
1057
1261
  if self.viewer is not None:
1058
- if len(self._memory)>0:
1059
- poses = [cur[0] for cur in self._memory]
1262
+ if len(self._flight_memory)>0:
1263
+ poses = [cur[0] for cur in self._flight_memory]
1060
1264
  times = [0.]
1061
- for i in range(1,len(self._memory)):
1062
- times.append(times[-1]+self._memory[i][1])
1265
+ for i in range(1,len(self._flight_memory)):
1266
+ times.append(times[-1]+self._flight_memory[i][1])
1063
1267
  self.viewer.record(dirout, poses, times, tlim, interp, fps=fps, prefix=prefix, ext=ext)
1064
1268
 
1065
1269
  def save_flight(self, fn:str):
1066
- """ Write memory to file JSON """
1270
+ """ Write flight memory to file JSON """
1271
+
1067
1272
  import json
1068
1273
 
1069
- if len(self._memory)>0:
1274
+ if len(self._flight_memory)>0:
1070
1275
  with open(fn,'w') as f:
1071
- json.dump(self._memory, f, indent=2)
1276
+ json.dump(self._flight_memory, f, indent=2)
1072
1277
 
1073
1278
  def load_flight(self, fn:str):
1074
- """ Load memory from file JSON """
1279
+ """ Load flight memory from file JSON """
1075
1280
  import json
1076
1281
 
1077
1282
  if exists(fn):
1078
1283
  with open(fn,'r') as f:
1079
- self._memory = json.load(f)
1284
+ self._flight_memory = json.load(f)
1080
1285
 
1081
1286
 
1082
1287
  def _callback_props(self):
1083
1288
 
1084
- self._update_props()
1289
+ self._update_viewer()
1085
1290
 
1086
1291
  def _callback_destroy_props(self):
1087
1292
 
1088
1293
  if self._myprops is not None:
1089
- self._callback_props()
1294
+ # self._callback_props()
1090
1295
  self._myprops.Destroy()
1091
1296
  self._myprops = None
1092
1297
 
1093
1298
  def _create_props(self):
1299
+ """ Create properties Wolf_Param for LAZ data """
1094
1300
 
1095
1301
  if self._myprops is not None:
1096
1302
  return
@@ -1134,22 +1340,123 @@ class Base_LAZ_Data(Element_To_Draw):
1134
1340
  codes_sel += str(curcode) + ','
1135
1341
  ret = props.addparam('Selection', 'Codes', codes_sel, Type_Param.String, 'Codes to select')
1136
1342
 
1137
- # ret = props.addparam('Auto Update', 'Active', self._auto_update_properties, Type_Param.Logical, 'Auto update properties')
1138
- # ret = props.addparam('Auto Update', 'Interval', self._auto_update_interval, Type_Param.Integer, 'Auto update interval (ms)')
1139
-
1140
1343
  props.Populate()
1141
1344
 
1142
- updatebutton = wx.Button(props, label=_('Update from viewer'))
1345
+ updatebutton = wx.Button(props, label=_('Get from viewer'))
1143
1346
  props.sizerbut.Add(updatebutton,1,wx.EXPAND)
1144
- props.Bind(wx.EVT_BUTTON, self._fill_props, updatebutton)
1347
+ updatebutton.Bind(wx.EVT_BUTTON, self._fill_props)
1145
1348
 
1146
- getselection = wx.Button(props, label=_('Get selection'))
1349
+ getselection = wx.Button(props, label=_('Edit selection'))
1147
1350
  props.sizerbut.Add(getselection,1,wx.EXPAND)
1148
- props.Bind(wx.EVT_BUTTON, self._get_selection, getselection)
1351
+ getselection.Bind(wx.EVT_BUTTON, self._OnEdit_Selection)
1149
1352
 
1150
1353
  props.Layout()
1151
1354
 
1152
- def _get_selection(self, event):
1355
+ def _set_new_xls(self):
1356
+ """ Create a new Excel grid for selected data """
1357
+
1358
+ self._xlsFrame = wx.Frame(None, wx.ID_ANY, _('Selected points - ') + self.idx)
1359
+ self._xls = CpGrid(self._xlsFrame, wx.ID_ANY, style = wx.WANTS_CHARS)
1360
+
1361
+ sizer = wx.BoxSizer(wx.VERTICAL)
1362
+ sizer.Add(self._xls, 1, wx.EXPAND)
1363
+
1364
+ nbclass = len(self.classification.classification)
1365
+ self._xls.CreateGrid(10, 4)
1366
+
1367
+ # Add a button to plot a histogram
1368
+ but_sizer = wx.BoxSizer(wx.HORIZONTAL)
1369
+ plotbutton = wx.Button(self._xlsFrame, label=_('Plot histogram (All data)'))
1370
+ but_sizer.Add(plotbutton, 1, wx.EXPAND)
1371
+ plotbutton.Bind(wx.EVT_BUTTON, self.OnPlot_histogram)
1372
+
1373
+ plotbutton2 = wx.Button(self._xlsFrame, label=_('Plot histogram (Grid data)'))
1374
+ but_sizer.Add(plotbutton2, 1, wx.EXPAND)
1375
+ plotbutton2.Bind(wx.EVT_BUTTON, self.OnPlot_histogram_grid)
1376
+
1377
+ sizer.Add(but_sizer, 0, wx.EXPAND)
1378
+
1379
+ self._xlsFrame.SetSizer(sizer)
1380
+ self._xlsFrame.Layout()
1381
+ self._xlsFrame.Show()
1382
+
1383
+ icon = wx.Icon()
1384
+ icon_path = Path(__file__).parent.parent / "apps/wolf_logo2.bmp"
1385
+ icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY))
1386
+ self._xlsFrame.SetIcon(icon)
1387
+
1388
+ def OnPlot_histogram(self, event:wx.MouseEvent):
1389
+ """ Plot histogram of selected data """
1390
+ self._plot_histogram()
1391
+
1392
+ def OnPlot_histogram_grid(self, event:wx.MouseEvent):
1393
+ """ Plot histogram of selected data """
1394
+ self._plot_histogram_grid()
1395
+
1396
+ def _plot_histogram_grid(self):
1397
+ """ Histogram ONLY of selected data in grid.
1398
+
1399
+ The data are extracted based on the first column of the grid
1400
+ untile an empty cell is found.
1401
+ """
1402
+
1403
+ xls = self._xls
1404
+
1405
+ if xls is None:
1406
+ logging.warning(_('No Excel grid'))
1407
+ return
1408
+
1409
+ # Find not null cells
1410
+ nbrows = 1
1411
+
1412
+ while xls.GetCellValue(nbrows,0) != '' and nbrows < xls.NumberRows:
1413
+ nbrows += 1
1414
+
1415
+ if nbrows == 1:
1416
+ logging.warning(_('Nt enough points selected'))
1417
+ return
1418
+
1419
+ xyz = np.zeros((nbrows,3))
1420
+ codes = np.zeros(nbrows)
1421
+
1422
+ try:
1423
+ for i in range(nbrows):
1424
+ xyz[i,0] = float(xls.GetCellValue(i,0))
1425
+ xyz[i,1] = float(xls.GetCellValue(i,1))
1426
+ xyz[i,2] = float(xls.GetCellValue(i,2))
1427
+ codes[i] = int(xls.GetCellValue(i,3))
1428
+ except Exception as e:
1429
+ logging.error(e)
1430
+ logging.warning(_('Bad values in grid - Check your input'))
1431
+ return
1432
+
1433
+ fig = plt.figure()
1434
+
1435
+ ax = fig.add_subplot(111)
1436
+
1437
+ ax.hist(xyz[:,2], bins=256)
1438
+
1439
+ fig.show()
1440
+
1441
+ def _plot_histogram(self):
1442
+ """ """
1443
+
1444
+ xyz = self.xyz_selected
1445
+
1446
+ if xyz.shape[0]==0:
1447
+ logging.warning(_('No points selected'))
1448
+ return
1449
+
1450
+ fig = plt.figure()
1451
+
1452
+ ax = fig.add_subplot(111)
1453
+
1454
+ ax.hist(xyz[:,2], bins=256)
1455
+
1456
+ fig.show()
1457
+
1458
+ def _selection2vector(self):
1459
+ """ FIXME: must use RANSAC to compute a segment from the selected points """
1153
1460
 
1154
1461
  if self.viewer is None:
1155
1462
  logging.warning(_('No viewer'))
@@ -1161,24 +1468,40 @@ class Base_LAZ_Data(Element_To_Draw):
1161
1468
  logging.warning(_('No points selected'))
1162
1469
  return
1163
1470
 
1164
- if self._xls is None:
1165
- self._xlsFrame = wx.Frame(None, wx.ID_ANY, _('Selected points - ') + self.idx)
1166
- self._xls = CpGrid(self._xlsFrame, wx.ID_ANY, style = wx.WANTS_CHARS)
1471
+ vect = vector(name = self.idx + '_selection', fromnumpy=xyz)
1167
1472
 
1168
- sizer = wx.BoxSizer(wx.VERTICAL)
1169
- sizer.Add(self._xls, 1, wx.EXPAND)
1473
+ return vect
1170
1474
 
1171
- nbclass = len(self.classification.classification)
1172
- self._xls.CreateGrid(xyz.shape[0] + nbclass +1, 4)
1173
1475
 
1174
- self._xlsFrame.SetSizer(sizer)
1175
- self._xlsFrame.Layout()
1176
- self._xlsFrame.Show()
1476
+ def _OnEdit_Selection(self, event:wx.MouseEvent):
1477
+ """ Get selection from viewer and create a XLS grid """
1478
+ self._edit_selection()
1479
+
1480
+ def _edit_selection(self):
1481
+
1482
+ if self.viewer is None:
1483
+ logging.warning(_('No viewer'))
1484
+ return
1177
1485
 
1486
+ xyz = self.xyz_selected
1487
+
1488
+ if xyz.shape[0]==0:
1489
+ logging.warning(_('No points selected'))
1490
+ return
1491
+
1492
+ if self._xls is None:
1493
+ self._set_new_xls()
1178
1494
  else:
1179
- self._xls.ClearGrid()
1180
- if self._xls.NumberRows < xyz.shape[0]:
1181
- self._xls.AppendRows(xyz.shape[0]-self._xls.NumberRows)
1495
+ try:
1496
+ self._xls.ClearGrid()
1497
+ except:
1498
+ #Useful if the grid is already destroyed
1499
+ self._set_new_xls()
1500
+
1501
+ nbclass = len(self.classification.classification)
1502
+ min_rows = xyz.shape[0] + nbclass +1
1503
+ if self._xls.NumberRows < min_rows:
1504
+ self._xls.AppendRows(min_rows - self._xls.NumberRows)
1182
1505
 
1183
1506
  self._xls.SetColLabelValue(0, 'X')
1184
1507
  self._xls.SetColLabelValue(1, 'Y')
@@ -1206,7 +1529,8 @@ class Base_LAZ_Data(Element_To_Draw):
1206
1529
  self._xlsFrame.Raise()
1207
1530
  self._xlsFrame.Center()
1208
1531
 
1209
- def _update_props(self):
1532
+ def _update_viewer(self):
1533
+ """ Update the viewer with properties """
1210
1534
 
1211
1535
  if self._myprops is None:
1212
1536
  return
@@ -1217,7 +1541,7 @@ class Base_LAZ_Data(Element_To_Draw):
1217
1541
  props = self._myprops
1218
1542
 
1219
1543
  self.lookat = (props[('Look at', 'X')], props[('Look at', 'Y')], props[('Look at', 'Z')])
1220
- self.eye = (props[('Camera', 'X')], props[('Camera', 'Y')], props[('Camera', 'Z')])
1544
+ # self.eye = (props[('Camera', 'X')], props[('Camera', 'Y')], props[('Camera', 'Z')])
1221
1545
 
1222
1546
  color = np.asarray(props[('Background', 'Color')])
1223
1547
  self.bg_color(color / 255.)
@@ -1245,17 +1569,8 @@ class Base_LAZ_Data(Element_To_Draw):
1245
1569
  logging.warning(_('Nullify selection filter - Check your input'))
1246
1570
  self._select_only_codes = []
1247
1571
 
1248
- # self._auto_update_interval = props[('Auto Update', 'Interval')]
1249
- # self._auto_update_properties = props[('Auto Update', 'Active')]
1250
-
1251
- # if self._auto_update_properties:
1252
- # self.SetTimer(self._auto_update_interval)
1253
- # else:
1254
- # if self._timer is not None:
1255
- # self._timer.Stop()
1256
-
1257
1572
  def _fill_props(self, full:bool = False):
1258
-
1573
+ """ Fill properties from attributes """
1259
1574
  if self._myprops is None:
1260
1575
  return
1261
1576
 
@@ -1288,31 +1603,10 @@ class Base_LAZ_Data(Element_To_Draw):
1288
1603
 
1289
1604
  props[('Points', 'Size')] = self._point_size
1290
1605
 
1291
- # props[('Auto Update', 'Interval')] = self._auto_update_interval
1292
- # props[('Auto Update', 'Active')] = self._auto_update_properties
1293
-
1294
1606
  props.Populate()
1295
1607
 
1296
- # def _fill_handler(self, event):
1297
-
1298
- # if self.viewer is None:
1299
- # return
1300
-
1301
- # self._fill_props(full = False)
1302
-
1303
- # def SetTimer(self, interval:int):
1304
-
1305
- # if self.viewer is None:
1306
- # return
1307
-
1308
- # if self._timer is None:
1309
- # self._timer = wx.Timer(self._myprops)
1310
- # self._myprops.Bind(wx.EVT_TIMER, self._fill_handler, self._timer)
1311
-
1312
- # self._timer.Start(interval)
1313
-
1314
1608
  def show_properties(self):
1315
-
1609
+ """ Surcharged method (see Element_To_Draw) to show properties from MapViewer"""
1316
1610
  if self.viewer is None:
1317
1611
  return
1318
1612