wolfhece 2.1.117__py3-none-any.whl → 2.1.119__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.
@@ -5,7 +5,7 @@ from PIL import Image
5
5
  from os.path import exists,join
6
6
  import matplotlib.pyplot as plt
7
7
  from os import stat,remove,listdir,scandir, makedirs
8
- import pathlib
8
+ from pathlib import Path
9
9
  import math
10
10
  from time import sleep
11
11
  from typing import Literal, Union
@@ -22,8 +22,11 @@ from . import viewer
22
22
  from ..PyWMS import getWalonmap
23
23
  from ..PyTranslate import _
24
24
  from ..color_constants import Colors
25
- from ..PyParams import Wolf_Param
26
- from ..matplotlib_fig import Matplotlib_Figure as mplfig
25
+ from ..PyParams import Wolf_Param, Type_Param
26
+ from ..matplotlib_fig import Matplotlib_Figure as MplFig
27
+ from ..drawing_obj import Element_To_Draw
28
+ from ..CpGrid import CpGrid
29
+ from ..PyVertexvectors import vector, Zones, zone, wolfvertex
27
30
 
28
31
  """
29
32
  Importation et visualisation de données LAS et LAZ
@@ -49,6 +52,7 @@ class Classification_LAZ():
49
52
  self.test_wx()
50
53
 
51
54
  self._choose_colors = None
55
+ self._viewer = None
52
56
 
53
57
  def test_wx(self):
54
58
  self.wx_exists = wx.App.Get() is not None
@@ -76,6 +80,7 @@ class Classification_LAZ():
76
80
  9 : ['Eau', 'Points de la surface d\’eau brute mesurés par le scanner 2',Colors.rgb_withalpha_float('royalblue',.3)],
77
81
  10 : ['Ponts', 'Les ponts ont été classés à part pour améliorer la définition du MNT. Ils ont été ouverts grâce',Colors.rgb_withalpha_float('lightyellow1',1.)],
78
82
  11 : ['Mur de berges', 'Mur et muret en berge de la Vesdre dépassant le sol à des vocation de réaliser une modélisation 3D hydraulique avec ces obstacles.',Colors.rgb_withalpha_float('red1',1.)],
83
+ 13 : ['Inconnu', 'A vérifier auSPW', Colors.rgb_withalpha_float('lightslategray',.2)],
79
84
  15 : ['Tranche d\'eau', 'Echo intermédiaire dans l\’eau n\’appartenant ni à la surface d\’eau ni au fond du lit', Colors.rgb_withalpha_float('lightblue',.2)],
80
85
  16 : ['Surface bathymétrique', 'Fond du lit de la Vesdre et de ses affluents et des autres surfaces d\’eau mesurées à partir du scanner 3 FWF discrétisé',Colors.rgb_withalpha_float('sandybrown',1.)],
81
86
  17 : ['Surface bathymétrique incertaine', 'Surface bathymétrique sur les zones peu profondes principalement sous végétation où les intensités des échos sont parfois trop faibles pour avoir la certitude qu\’ils représentent le fond de rivière. La classe 17 est néanmoins plus certaine que la classe 18. Elle est utilisée dans la génération des MNT par défaut.',Colors.rgb_withalpha_float('rosybrown',.5)],
@@ -146,7 +151,7 @@ class xyz_laz():
146
151
  self.data = []
147
152
  return
148
153
 
149
- last_part = pathlib.Path(fn).name
154
+ last_part = Path(fn).name
150
155
  parts = last_part.split('_')
151
156
 
152
157
  # L'origine est codée dans le nom de fichier
@@ -157,7 +162,7 @@ class xyz_laz():
157
162
  dy = 3500.
158
163
 
159
164
  # Récupération du lien vers le fichier GridInfo.txt
160
- gridinfo=join(pathlib.Path(fn).parent,'gridinfo.txt')
165
+ gridinfo=join(Path(fn).parent,'gridinfo.txt')
161
166
 
162
167
  if exists(gridinfo):
163
168
  with open(gridinfo,'r') as f:
@@ -244,8 +249,8 @@ class xyz_laz():
244
249
  if exists(fn+'.zip'):
245
250
  with zipfile.ZipFile(fn+'.zip','r') as zip_file:
246
251
  fn = zip_file.namelist()[0]
247
- zip_file.extract(fn, path=pathlib.Path(fn).parent)
248
- fn = join(pathlib.Path(fn).parent, fn)
252
+ zip_file.extract(fn, path=Path(fn).parent)
253
+ fn = join(Path(fn).parent, fn)
249
254
  elif not exists(fn):
250
255
  return
251
256
 
@@ -395,7 +400,7 @@ class xyz_laz_grid():
395
400
  xloc = np.linspace(xbounds[0], xbounds[1], gridsize[0]+1)
396
401
  yloc = np.linspace(ybounds[0], ybounds[1], gridsize[1]+1)
397
402
 
398
- dirout = pathlib.Path(fn_out).parent
403
+ dirout = Path(fn_out).parent
399
404
  fn=join(dirout,'gridinfo.txt')
400
405
 
401
406
  if exists(fn):
@@ -405,7 +410,7 @@ class xyz_laz_grid():
405
410
  f.write(str(int(xbounds[0]))+','+str(int(xbounds[1]))+'\n')
406
411
  f.write(str(int(ybounds[0]))+','+str(int(ybounds[1]))+'\n')
407
412
  f.write(str(int(gridsize[0]))+','+str(int(gridsize[1]))+'\n')
408
- f.write(pathlib.Path(fn_out).name+'_'+'x1'+'_'+'y1'+'_xyz.bin'+'\n')
413
+ f.write(Path(fn_out).name+'_'+'x1'+'_'+'y1'+'_xyz.bin'+'\n')
409
414
  if force_format == np.float64:
410
415
  f.write('xy_float64')
411
416
  else:
@@ -481,7 +486,7 @@ class xyz_laz_grid():
481
486
  if force_format == np.float64:
482
487
  fnzip = fn + '.zip'
483
488
  with zipfile.ZipFile(fnzip,'w',zipfile.ZIP_DEFLATED) as zip_file:
484
- zip_file.write(fn, pathlib.Path(fn).name)
489
+ zip_file.write(fn, Path(fn).name)
485
490
  remove(fn)
486
491
 
487
492
 
@@ -632,7 +637,7 @@ class xyz_laz_grids():
632
637
 
633
638
  (up_s, up_z, up_color), (down_s, down_z, down_color) = self.scan_around(xy, length_buffer)
634
639
 
635
- figmpl = mplfig()
640
+ figmpl = MplFig()
636
641
  figmpl.presets()
637
642
  fig = figmpl.fig
638
643
  ax = figmpl.cur_ax
@@ -682,6 +687,873 @@ class xyz_laz_grids():
682
687
  gridsize=[max(int(dx/ds),1), max(int(dy/ds),1)],
683
688
  force_format=force_format))
684
689
 
690
+
691
+ class Wolf_LAZ_Data(Element_To_Draw):
692
+ """ Base class for LAZ data which can be imported in Pydraw.Mapviewer.
693
+ """
694
+
695
+ def __init__(self, idx:str = '', plotted:bool = False, mapviewer = None, need_for_wx:bool = False) -> None:
696
+
697
+ self._filename:Path = Path('') # filename TODO : improve serialization
698
+ self._lazfile:Path = None # Not used
699
+
700
+ # Bounds must be set before calling super().__init__() because
701
+ # xmin, xmax, ymin, ymax properties are used and depend on _bounds
702
+ self._bounds = [[0.,0.],[0.,0.]] # [[xmin,xmax],[ymin,ymax]]
703
+
704
+ super().__init__(idx, plotted, mapviewer, need_for_wx)
705
+
706
+ self._data:np.ndarray = None # Numpy data array -- to be plotted
707
+ self._colors:np.ndarray = None # NumPy array of colors for each point --> see viewer attributes for details
708
+ self.classification = Classification_LAZ() # Classification of LAZ data --> defining colors if codification is used
709
+
710
+ self._associated_color:int = Colors_Lazviewer.CODE_2023.value # Associated color type for LAZ data
711
+
712
+ self.viewer:viewer = None # PPTK viewer
713
+
714
+ self._point_size = .05 # Point size in viewer -- in user units
715
+
716
+ self._flight_memory = [] # Flight position
717
+
718
+ self._myprops = None # Properties based on Wolf_Param class
719
+
720
+ self._bg_color = [int(0.23*255), int(0.23*255), int(.44*255), 255] # Background color
721
+ self._bg_color_top = [0, 0, 0, 255] # Top background color
722
+ self._bg_color_bottom = [int(0.23*255), int(0.23*255), int(.44*255), 255] # Bottom background color
723
+
724
+ self._floor_level = 0. # Floor level -- user units
725
+ self._floor_color = [int(0.3*255), int(0.3*255), int(.3*255), 127] # Floor color
726
+
727
+ self._show_grid = True # Show grid in viewer
728
+ self._show_axis = True # Show axis in viewer
729
+ self._show_info = True # Show info in viewer
730
+
731
+ self._select_only_codes = [] # Codes to be selected -- Defined by user
732
+
733
+ self._xls:CpGrid = None # Excel grid for selected data
734
+ self._xlsFrame:wx.Frame = None # Frame containing the xls grid
735
+
736
+ def serialize(self):
737
+ """ Serialize class : data and attributes """
738
+ return {'bounds':self._bounds, 'data':str(self._filename) + '.npz', 'associated_color':self._associated_color,
739
+ 'point_size':self._point_size, 'bg_color':self._bg_color, 'bg_color_top':self._bg_color_top,
740
+ 'bg_color_bottom':self._bg_color_bottom, 'floor_level':self._floor_level, 'floor_color':self._floor_color,
741
+ 'show_grid':self._show_grid, 'show_axis':self._show_axis, 'show_info':self._show_info}
742
+
743
+ def deserialize(self, data:dict):
744
+ """ Deserialize class : data and attributes """
745
+ self._bounds = data['bounds']
746
+ self._filename = Path(data['data'])
747
+ self._associated_color = data['associated_color']
748
+ self._point_size = data['point_size']
749
+ self._bg_color = data['bg_color']
750
+ self._bg_color_top = data['bg_color_top']
751
+ self._bg_color_bottom = data['bg_color_bottom']
752
+ self._floor_level = data['floor_level']
753
+ self._floor_color = data['floor_color']
754
+ self._show_grid = data['show_grid']
755
+ self._show_axis = data['show_axis']
756
+ self._show_info = data['show_info']
757
+
758
+ self.data = np.load(self._filename)['data']
759
+
760
+ def saveas(self, fn:str):
761
+ """ Save class : data and attributes """
762
+ import pickle
763
+
764
+ self._filename = Path(fn)
765
+
766
+ with open(fn,'wb') as f:
767
+ pickle.dump(self.serialize(),f)
768
+
769
+ # save data by numpy
770
+ np.savez(str(fn) + '.npz', data=self._data)
771
+
772
+ def load(self, fn:str):
773
+ """ Load class : data and attributes """
774
+ import pickle
775
+
776
+ with open(fn,'rb') as f:
777
+ self.deserialize(pickle.load(f))
778
+
779
+ @property
780
+ def associated_color(self):
781
+ return self._associated_color
782
+
783
+ @associated_color.setter
784
+ def associated_color(self, value:int):
785
+ self._associated_color = value
786
+ self.set_colors()
787
+
788
+ def merge(self, other:"Wolf_LAZ_Data"):
789
+ """ Merge two Wolf_LAZ_Data objects """
790
+
791
+ if self._data is None:
792
+ self._data = other._data
793
+ else:
794
+ self._data = np.concatenate((self._data, other._data))
795
+
796
+ self.bounds = [[min(self.bounds[0][0],other.bounds[0][0]),max(self.bounds[0][1],other.bounds[0][1])],
797
+ [min(self.bounds[1][0],other.bounds[1][0]),max(self.bounds[1][1],other.bounds[1][1])]]
798
+
799
+ def filter_data(self, codes:list[int]):
800
+ """ Filter data by codes """
801
+ self._data = self._data[np.isin(self._data[:,3],codes)]
802
+
803
+ def bg_color(self, value):
804
+ if self.viewer is not None:
805
+ return self.viewer.set(bg_color = value)
806
+
807
+ def bg_color_top(self, value):
808
+ if self.viewer is not None:
809
+ return self.viewer.set(bg_color_top = value)
810
+
811
+ def bg_color_bottom(self, value):
812
+ if self.viewer is not None:
813
+ return self.viewer.set(bg_color_bottom = value)
814
+
815
+ def floor_level(self, value):
816
+ if self.viewer is not None:
817
+ return self.viewer.set(floor_level = float(value))
818
+
819
+ def floor_color(self, value):
820
+ if self.viewer is not None:
821
+ return self.viewer.set(floor_color = value)
822
+
823
+ def show_grid(self, value:bool):
824
+ if self.viewer is not None:
825
+ return self.viewer.set(show_grid = bool(value))
826
+
827
+ def show_axis(self, value:bool):
828
+ if self.viewer is not None:
829
+ return self.viewer.set(show_axis = bool(value))
830
+
831
+ def show_info(self, value:bool):
832
+ if self.viewer is not None:
833
+ return self.viewer.set(show_info = bool(value))
834
+
835
+ def force_view(self, x, y, z = -1):
836
+ """ Force lookat position """
837
+ if z == -1:
838
+ curx,cury,curz = self.lookat
839
+ self.lookat = [x,y,curz]
840
+ else:
841
+ self.lookat = [x,y,z]
842
+ # self.eye = self._eye_pos()
843
+
844
+ @property
845
+ def selected(self):
846
+ if self.viewer is None:
847
+ return None
848
+ return self.viewer.get('selected')
849
+
850
+ @property
851
+ def xyz_selected(self) -> np.ndarray:
852
+ """ Extract the selected points from the viewer.
853
+
854
+ Filter the selected points by codes if _select_only_codes is not empty."""
855
+
856
+ if self.viewer is None:
857
+ return None
858
+
859
+ if self.selected.shape[0] == 0:
860
+ return np.ndarray((0,3))
861
+
862
+ if len(self._select_only_codes)>0:
863
+ return self.data[self.selected,:3][np.isin(self.data[self.selected,3],self._select_only_codes)]
864
+ else:
865
+ return self.data[self.selected,:3]
866
+
867
+ @property
868
+ def code_selected(self) -> np.ndarray:
869
+ if self.viewer is None:
870
+ return None
871
+
872
+ if self.selected.shape[0] == 0:
873
+ return np.ndarray((0,1))
874
+
875
+ if len(self._select_only_codes)>0:
876
+ return self.data[self.selected,3][np.isin(self.data[self.selected,3],self._select_only_codes)]
877
+ else:
878
+ return self.data[self.selected,3]
879
+
880
+ @property
881
+ def num_points(self):
882
+ """ Number of points """
883
+
884
+ nb1 = self.data.shape[0]
885
+ if self.viewer is not None:
886
+ nb2 = self.viewer.get('num_points')[0]
887
+ assert nb1 == nb2, _('Incoherent number of points')
888
+
889
+ return nb1
890
+
891
+ @property
892
+ def nb_points(self):
893
+ """ Number of points - alias of num_points """
894
+ return self.num_points
895
+
896
+ @property
897
+ def nb(self):
898
+ """ Number of points - alias of num_points """
899
+ return self.num_points
900
+
901
+ @property
902
+ def right(self):
903
+ """Camera Right vector """
904
+ return self.viewer.get('right')
905
+
906
+ @property
907
+ def mvp(self):
908
+ """ Model View Projection matrix
909
+
910
+ See https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93projection_matrix for details
911
+ """
912
+
913
+ if self.viewer is None:
914
+ return None
915
+ return self.viewer.get('mvp')
916
+
917
+ @property
918
+ def eye(self):
919
+ """ eye/camera position """
920
+ if self.viewer is None:
921
+ return None
922
+ return self.viewer.get('eye')
923
+
924
+ @property
925
+ def lookat(self):
926
+ """ Lookat position """
927
+ if self.viewer is None:
928
+ return None
929
+ return self.viewer.get('lookat')
930
+
931
+ @property
932
+ def phi(self):
933
+ """ Azimuth angle (radians) """
934
+ if self.viewer is None:
935
+ return None
936
+ return self.viewer.get('phi')[0]
937
+
938
+ @property
939
+ def theta(self):
940
+ """ Elevation angle (radians) """
941
+ if self.viewer is None:
942
+ return None
943
+ return self.viewer.get('theta')[0]
944
+
945
+ @property
946
+ def r(self):
947
+ """ Distance from lookat """
948
+ if self.viewer is None:
949
+ return None
950
+ return self.viewer.get('r')[0]
951
+
952
+ @phi.setter
953
+ def phi(self, value):
954
+ if self.viewer is None:
955
+ return None
956
+ self.viewer.set(phi=value)
957
+ self._fill_props()
958
+
959
+ @theta.setter
960
+ def theta(self, value):
961
+ if self.viewer is None:
962
+ return None
963
+ self.viewer.set(theta=value)
964
+ self._fill_props()
965
+
966
+ @r.setter
967
+ def r(self, value):
968
+ if self.viewer is None:
969
+ return None
970
+ self.viewer.set(r=value)
971
+ self._fill_props()
972
+
973
+ @lookat.setter
974
+ def lookat(self, value):
975
+ if self.viewer is None:
976
+ return None
977
+ self.viewer.set(lookat=value)
978
+ # self.viewer.set(lookat=value, phi=self.phi, theta=self.theta, r=self.r)
979
+ self._fill_props()
980
+
981
+ def _eye_pos(self):
982
+ """ Compute eye pos from lookat and r, phi, theta.
983
+
984
+ phi is the azimuth angle (radians)
985
+ theta is the elevation angle (radians)
986
+ r is the distance from lookat
987
+ """
988
+
989
+ lx, ly, lz = self.lookat
990
+ r = self.r
991
+ phi = self.phi
992
+ theta = self.theta
993
+
994
+ x = lx + r*np.sin(phi)*np.cos(theta)
995
+ y = ly + r*np.cos(phi)*np.cos(theta)
996
+ z = lz + r*np.sin(theta)
997
+
998
+ return [x,y,z]
999
+
1000
+ @eye.setter
1001
+ def eye(self, value):
1002
+ if self.viewer is None:
1003
+ return None
1004
+ x,y,z = value
1005
+ lx, ly, lz = self.lookat
1006
+ right = self.right
1007
+
1008
+ # Compute phi, theta, r based on eye and lookat and right vector
1009
+ r = np.sqrt((x-lx)**2 + (y-ly)**2 + (z-lz)**2)
1010
+ self.r =r
1011
+ self.theta = np.arcsin((z-lz)/r)
1012
+ self.phi = -np.arctan2((x-lx),(y-ly))
1013
+ self._fill_props()
1014
+
1015
+ @property
1016
+ def point_size(self):
1017
+ """ Point size in viewer -- user units """
1018
+ return self._point_size
1019
+
1020
+ @point_size.setter
1021
+ def point_size(self, value):
1022
+ self._point_size = value
1023
+ if self.viewer is not None:
1024
+ self.viewer.set(point_size=value)
1025
+ self._fill_props()
1026
+
1027
+ @property
1028
+ def xyz(self):
1029
+ return self.data[:,:3]
1030
+
1031
+ @property
1032
+ def codes(self):
1033
+ return self.data[:,3]
1034
+
1035
+ def codes_unique(self):
1036
+ """ Only unique codes """
1037
+ return list(np.unique(self.codes).astype(int))
1038
+
1039
+ def create_viewer(self, color_code:Colors_Lazviewer = None, classification:Classification_LAZ = None):
1040
+ """ Create a viewer for las data """
1041
+
1042
+ if classification is not None:
1043
+ self.classification = classification
1044
+
1045
+ if color_code is not None:
1046
+ self.associated_color = color_code
1047
+
1048
+ self._colors = get_colors(self.data, self.associated_color, palette_classif= self.classification)
1049
+
1050
+ self.viewer = viewer(self.xyz, self._colors)
1051
+ self.viewer.set(point_size= self._point_size)
1052
+
1053
+ return self.viewer
1054
+
1055
+ def interactive_update_colors(self):
1056
+ """ Create a frame to interactively update colors """
1057
+
1058
+ self.classification.interactive_update_colors()
1059
+
1060
+ self.classification._choose_colors.SetTitle(_('Colors of ') + self.idx)
1061
+
1062
+ def new_callback_colors():
1063
+ self.classification.callback_colors()
1064
+ self.set_colors()
1065
+
1066
+ self.classification._choose_colors.callback = new_callback_colors
1067
+
1068
+ def set_colors(self):
1069
+ """ Set colors in viewer --> using attributes method (not colormap) """
1070
+
1071
+ if self.viewer is not None:
1072
+ self._colors = get_colors(self.data, self.associated_color, palette_classif= self.classification)
1073
+ self.viewer.attributes(self._colors)
1074
+
1075
+ def set_classification(self, classification:str = None):
1076
+ """ Set classification of LAZ data
1077
+
1078
+ TODO : Check if 2020-2022 SPW campaign is the same classification as 2013
1079
+ """
1080
+ if classification is None:
1081
+ logging.warning(_('No classification chosen - Abort !'))
1082
+ elif classification == 'SPW 2013-2014':
1083
+ self.classification.init_2013()
1084
+ else:
1085
+ self.classification.init_2023()
1086
+
1087
+ def find_minmax(self, update=False):
1088
+ """ Find min and max of data """
1089
+ if self.data is not None:
1090
+ self.bounds = [[np.min(self.data[:,0]), np.max(self.data[:,0])],[np.min(self.data[:,1]), np.max(self.data[:,1])]]
1091
+
1092
+ @property
1093
+ def xmin(self):
1094
+ return self.bounds[0][0]
1095
+
1096
+ @xmin.setter
1097
+ def xmin(self, value):
1098
+ self._bounds[0][0] = value
1099
+
1100
+ @property
1101
+ def xmax(self):
1102
+ return self.bounds[0][1]
1103
+
1104
+ @xmax.setter
1105
+ def xmax(self, value):
1106
+ self._bounds[0][1] = value
1107
+
1108
+ @property
1109
+ def ymin(self):
1110
+ return self.bounds[1][0]
1111
+
1112
+ @ymin.setter
1113
+ def ymin(self, value):
1114
+ self._bounds[1][0] = value
1115
+
1116
+ @property
1117
+ def ymax(self):
1118
+ return self.bounds[1][1]
1119
+
1120
+ @ymax.setter
1121
+ def ymax(self, value):
1122
+ self._bounds[1][1] = value
1123
+
1124
+ @property
1125
+ def data(self):
1126
+ """ Full data array (x,y,z,code) """
1127
+ return self._data
1128
+
1129
+ @data.setter
1130
+ def data(self, value):
1131
+ self._data = value
1132
+
1133
+ @property
1134
+ def bounds(self):
1135
+ return self._bounds
1136
+
1137
+ @bounds.setter
1138
+ def bounds(self, value):
1139
+ self._bounds = value
1140
+
1141
+ def from_grid(self, grid:xyz_laz_grid, bounds:Union[tuple[tuple[float,float],tuple[float,float]], list[list[float, float],list[float, float]]]):
1142
+ """ Create data from grid LAZ """
1143
+ self.bounds = bounds
1144
+ self.data = grid.scan(bounds)
1145
+
1146
+ def from_file(self, fn:str):
1147
+ """ Create data from LAZ file """
1148
+ self.data = read_laz(fn)
1149
+ self.bounds = [[np.min(self.data[:,0]), np.max(self.data[:,0])],[np.min(self.data[:,1]), np.max(self.data[:,1])]]
1150
+
1151
+ def descimate(self, step:int):
1152
+ """ Descimate data.
1153
+
1154
+ Conserve only one point every 'step' points.
1155
+
1156
+ :param step: step of descimation
1157
+ """
1158
+ self.data = self.data[::step]
1159
+
1160
+ def get_data_class(self, key:int):
1161
+ """ Get data with a specific code """
1162
+
1163
+ assert isinstance(key, int), _('Key must be an integer')
1164
+
1165
+ return self.data[self.data[:,3] == key]
1166
+
1167
+ def add_pose_in_memory(self, key_time:float = 1.):
1168
+ """ Add current pose in flight memory """
1169
+
1170
+ if self.viewer is not None:
1171
+ lookat = self.lookat
1172
+ new_pose = (lookat[0], lookat[1], lookat[2], self.phi, self.theta, self.r)
1173
+ self._flight_memory.append((new_pose, key_time))
1174
+
1175
+ def play_flight(self, tlim=[-np.inf, np.inf], repeat=False, interp='cubic_natural'):
1176
+ """ Play flight memory """
1177
+ if self.viewer is not None:
1178
+ if len(self._flight_memory)>0:
1179
+ poses = [cur[0] for cur in self._flight_memory]
1180
+ times = [0.]
1181
+ for i in range(1,len(self._flight_memory)):
1182
+ times.append(times[-1]+self._flight_memory[i][1])
1183
+ self.viewer.play(poses, times, tlim, repeat, interp)
1184
+
1185
+ def set_times(self, times:np.ndarray):
1186
+ """ Set times for flight memory """
1187
+ if len(self._flight_memory)>0:
1188
+ self._flight_memory = [(self._flight_memory[i][0], times[i]) for i in range(len(self._flight_memory))]
1189
+
1190
+ def set_times_increment(self, increment:float):
1191
+ if len(self._flight_memory)>0:
1192
+ self._flight_memory = [(self._flight_memory[i][0], increment*i) for i in range(len(self._flight_memory))]
1193
+
1194
+ def get_times(self):
1195
+ return np.asarray([cur[1] for cur in self._flight_memory])
1196
+
1197
+ def record_flight(self, dirout:str, tlim=[-np.inf, np.inf], interp='cubic_natural', fps=24, prefix:str = 'laz_', ext:str = 'png'):
1198
+ """ Record flight memory in multiple images
1199
+
1200
+ FIXME : FREEZE the app --> to debug
1201
+ """
1202
+ if self.viewer is not None:
1203
+ if len(self._flight_memory)>0:
1204
+ poses = [cur[0] for cur in self._flight_memory]
1205
+ times = [0.]
1206
+ for i in range(1,len(self._flight_memory)):
1207
+ times.append(times[-1]+self._flight_memory[i][1])
1208
+ self.viewer.record(dirout, poses, times, tlim, interp, fps=fps, prefix=prefix, ext=ext)
1209
+
1210
+ def save_flight(self, fn:str):
1211
+ """ Write flight memory to file JSON """
1212
+
1213
+ import json
1214
+
1215
+ if len(self._flight_memory)>0:
1216
+ with open(fn,'w') as f:
1217
+ json.dump(self._flight_memory, f, indent=2)
1218
+
1219
+ def load_flight(self, fn:str):
1220
+ """ Load flight memory from file JSON """
1221
+ import json
1222
+
1223
+ if exists(fn):
1224
+ with open(fn,'r') as f:
1225
+ self._flight_memory = json.load(f)
1226
+
1227
+
1228
+ def _callback_props(self):
1229
+
1230
+ self._update_viewer()
1231
+
1232
+ def _callback_destroy_props(self):
1233
+
1234
+ if self._myprops is not None:
1235
+ # self._callback_props()
1236
+ self._myprops.Destroy()
1237
+ self._myprops = None
1238
+
1239
+ def _create_props(self):
1240
+ """ Create properties Wolf_Param for LAZ data """
1241
+
1242
+ if self._myprops is not None:
1243
+ return
1244
+
1245
+ self._myprops = Wolf_Param(None, title=_('Properties of ') + self.idx,
1246
+ to_read=False, force_even_if_same_default= True)
1247
+
1248
+ props = self._myprops
1249
+
1250
+ props.set_callbacks(self._callback_props, self._callback_destroy_props)
1251
+ props.hide_selected_buttons()
1252
+
1253
+
1254
+ ret = props.addparam('Camera', 'X', self.eye[0], Type_Param.Float, 'eye_x')
1255
+ ret = props.addparam('Camera', 'Y', self.eye[1], Type_Param.Float, 'eye_y')
1256
+ ret = props.addparam('Camera', 'Z', self.eye[2], Type_Param.Float, 'eye_z')
1257
+
1258
+ ret = props.addparam('Look at', 'X', self.lookat[0], Type_Param.Float, 'lookat_x')
1259
+ ret = props.addparam('Look at', 'Y', self.lookat[1], Type_Param.Float, 'lookat_y')
1260
+ ret = props.addparam('Look at', 'Z', self.lookat[2], Type_Param.Float, 'lookat_z')
1261
+
1262
+ ret = props.addparam('Relative Position', 'Phi', self.phi, Type_Param.Float, 'azimuthal angle (radians) - (phi, theta, r) are spherical coordinates specifying camera position relative to the look at position.')
1263
+ ret = props.addparam('Relative Position', 'Theta', self.theta, Type_Param.Float, 'elevation angle (radians) - (phi, theta, r) are spherical coordinates specifying camera position relative to the look at position.')
1264
+ ret = props.addparam('Relative Position', 'R', self.r, Type_Param.Float, 'distance to look-at point - (phi, theta, r) are spherical coordinates specifying camera position relative to the look at position.')
1265
+
1266
+ ret = props.addparam('Background', 'Color', self._bg_color, Type_Param.Color, 'Background color')
1267
+ ret = props.addparam('Background', 'Top Color', self._bg_color_top, Type_Param.Color, 'Top Background color')
1268
+ ret = props.addparam('Background', 'Bottom Color', self._bg_color_bottom, Type_Param.Color, 'Bottom Background color')
1269
+
1270
+ ret = props.addparam('Floor', 'Level', self._floor_level, Type_Param.Float, 'Floor level')
1271
+ ret = props.addparam('Floor', 'Color', self._floor_color, Type_Param.Color, 'Floor color')
1272
+
1273
+ ret = props.addparam('Infos', 'Grid', self._show_grid, Type_Param.Logical, 'Show grid')
1274
+ ret = props.addparam('Infos', 'Axis', self._show_axis, Type_Param.Logical, 'Show axis')
1275
+ ret = props.addparam('Infos', 'values', self._show_info, Type_Param.Logical, 'Show info')
1276
+
1277
+ ret = props.addparam('Points', 'Size', self._point_size, Type_Param.Float, 'Point size')
1278
+
1279
+ codes_sel = ''
1280
+ for curcode in self._select_only_codes:
1281
+ codes_sel += str(curcode) + ','
1282
+ ret = props.addparam('Selection', 'Codes', codes_sel, Type_Param.String, 'Codes to select')
1283
+
1284
+ props.Populate()
1285
+
1286
+ updatebutton = wx.Button(props, label=_('Get from viewer'))
1287
+ props.sizerbut.Add(updatebutton,1,wx.EXPAND)
1288
+ updatebutton.Bind(wx.EVT_BUTTON, self._fill_props)
1289
+
1290
+ getselection = wx.Button(props, label=_('Edit selection'))
1291
+ props.sizerbut.Add(getselection,1,wx.EXPAND)
1292
+ getselection.Bind(wx.EVT_BUTTON, self._OnEdit_Selection)
1293
+
1294
+ props.Layout()
1295
+
1296
+ def _set_new_xls(self):
1297
+ """ Create a new Excel grid for selected data """
1298
+
1299
+ self._xlsFrame = wx.Frame(None, wx.ID_ANY, _('Selected points - ') + self.idx)
1300
+ self._xls = CpGrid(self._xlsFrame, wx.ID_ANY, style = wx.WANTS_CHARS)
1301
+
1302
+ sizer = wx.BoxSizer(wx.VERTICAL)
1303
+ sizer.Add(self._xls, 1, wx.EXPAND)
1304
+
1305
+ nbclass = len(self.classification.classification)
1306
+ self._xls.CreateGrid(10, 4)
1307
+
1308
+ # Add a button to plot a histogram
1309
+ but_sizer = wx.BoxSizer(wx.HORIZONTAL)
1310
+ plotbutton = wx.Button(self._xlsFrame, label=_('Plot histogram (All data)'))
1311
+ but_sizer.Add(plotbutton, 1, wx.EXPAND)
1312
+ plotbutton.Bind(wx.EVT_BUTTON, self.OnPlot_histogram)
1313
+
1314
+ plotbutton2 = wx.Button(self._xlsFrame, label=_('Plot histogram (Grid data)'))
1315
+ but_sizer.Add(plotbutton2, 1, wx.EXPAND)
1316
+ plotbutton2.Bind(wx.EVT_BUTTON, self.OnPlot_histogram_grid)
1317
+
1318
+ sizer.Add(but_sizer, 0, wx.EXPAND)
1319
+
1320
+ self._xlsFrame.SetSizer(sizer)
1321
+ self._xlsFrame.Layout()
1322
+ self._xlsFrame.Show()
1323
+
1324
+ icon = wx.Icon()
1325
+ icon_path = Path(__file__).parent.parent / "apps/wolf_logo2.bmp"
1326
+ icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY))
1327
+ self._xlsFrame.SetIcon(icon)
1328
+
1329
+ def OnPlot_histogram(self, event:wx.MouseEvent):
1330
+ """ Plot histogram of selected data """
1331
+ self._plot_histogram()
1332
+
1333
+ def OnPlot_histogram_grid(self, event:wx.MouseEvent):
1334
+ """ Plot histogram of selected data """
1335
+ self._plot_histogram_grid()
1336
+
1337
+ def _plot_histogram_grid(self):
1338
+ """ Histogram ONLY of selected data in grid.
1339
+
1340
+ The data are extracted based on the first column of the grid
1341
+ untile an empty cell is found.
1342
+ """
1343
+
1344
+ xls = self._xls
1345
+
1346
+ if xls is None:
1347
+ logging.warning(_('No Excel grid'))
1348
+ return
1349
+
1350
+ # Find not null cells
1351
+ nbrows = 1
1352
+
1353
+ while xls.GetCellValue(nbrows,0) != '' and nbrows < xls.NumberRows:
1354
+ nbrows += 1
1355
+
1356
+ if nbrows == 1:
1357
+ logging.warning(_('Nt enough points selected'))
1358
+ return
1359
+
1360
+ xyz = np.zeros((nbrows,3))
1361
+ codes = np.zeros(nbrows)
1362
+
1363
+ try:
1364
+ for i in range(nbrows):
1365
+ xyz[i,0] = float(xls.GetCellValue(i,0))
1366
+ xyz[i,1] = float(xls.GetCellValue(i,1))
1367
+ xyz[i,2] = float(xls.GetCellValue(i,2))
1368
+ codes[i] = int(xls.GetCellValue(i,3))
1369
+ except Exception as e:
1370
+ logging.error(e)
1371
+ logging.warning(_('Bad values in grid - Check your input'))
1372
+ return
1373
+
1374
+ fig = plt.figure()
1375
+
1376
+ ax = fig.add_subplot(111)
1377
+
1378
+ ax.hist(xyz[:,2], bins=256)
1379
+
1380
+ fig.show()
1381
+
1382
+ def _plot_histogram(self):
1383
+ """ """
1384
+
1385
+ xyz = self.xyz_selected
1386
+
1387
+ if xyz.shape[0]==0:
1388
+ logging.warning(_('No points selected'))
1389
+ return
1390
+
1391
+ fig = plt.figure()
1392
+
1393
+ ax = fig.add_subplot(111)
1394
+
1395
+ ax.hist(xyz[:,2], bins=256)
1396
+
1397
+ fig.show()
1398
+
1399
+ def _selection2vector(self):
1400
+ """ FIXME: must use RANSAC to compute a segment from the selected points """
1401
+
1402
+ if self.viewer is None:
1403
+ logging.warning(_('No viewer'))
1404
+ return
1405
+
1406
+ xyz = self.xyz_selected
1407
+
1408
+ if xyz.shape[0]==0:
1409
+ logging.warning(_('No points selected'))
1410
+ return
1411
+
1412
+ vect = vector(name = self.idx + '_selection', fromnumpy=xyz)
1413
+
1414
+ return vect
1415
+
1416
+
1417
+ def _OnEdit_Selection(self, event:wx.MouseEvent):
1418
+ """ Get selection from viewer and create a XLS grid """
1419
+ self._edit_selection()
1420
+
1421
+ def _edit_selection(self):
1422
+
1423
+ if self.viewer is None:
1424
+ logging.warning(_('No viewer'))
1425
+ return
1426
+
1427
+ xyz = self.xyz_selected
1428
+
1429
+ if xyz.shape[0]==0:
1430
+ logging.warning(_('No points selected'))
1431
+ return
1432
+
1433
+ if self._xls is None:
1434
+ self._set_new_xls()
1435
+ else:
1436
+ try:
1437
+ self._xls.ClearGrid()
1438
+ except:
1439
+ #Useful if the grid is already destroyed
1440
+ self._set_new_xls()
1441
+
1442
+ nbclass = len(self.classification.classification)
1443
+ min_rows = xyz.shape[0] + nbclass +1
1444
+ if self._xls.NumberRows < min_rows:
1445
+ self._xls.AppendRows(min_rows - self._xls.NumberRows)
1446
+
1447
+ self._xls.SetColLabelValue(0, 'X')
1448
+ self._xls.SetColLabelValue(1, 'Y')
1449
+ self._xls.SetColLabelValue(2, 'Z')
1450
+ self._xls.SetColLabelValue(3, 'Code')
1451
+
1452
+ codes = self.code_selected
1453
+
1454
+ for i in range(xyz.shape[0]):
1455
+ self._xls.SetCellValue(i,0,str(xyz[i,0]))
1456
+ self._xls.SetCellValue(i,1,str(xyz[i,1]))
1457
+ self._xls.SetCellValue(i,2,str(xyz[i,2]))
1458
+ self._xls.SetCellValue(i,3,str(codes[i]))
1459
+
1460
+ # Copy classification under the values
1461
+ for i, (key, val) in enumerate(self.classification.classification.items()):
1462
+ self._xls.SetCellValue(xyz.shape[0]+i+1,0,str(key))
1463
+ self._xls.SetCellValue(xyz.shape[0]+i+1,1,val[0])
1464
+ self._xls.SetCellValue(xyz.shape[0]+i+1,2,val[1])
1465
+ self._xls.SetCellValue(xyz.shape[0]+i+1,3,str(key))
1466
+
1467
+ self._xlsFrame.Show()
1468
+
1469
+ # Mettre la fenêtre au premier plan et centrée
1470
+ self._xlsFrame.Raise()
1471
+ self._xlsFrame.Center()
1472
+
1473
+ def _update_viewer(self):
1474
+ """ Update the viewer with properties """
1475
+
1476
+ if self._myprops is None:
1477
+ return
1478
+
1479
+ if self.viewer is None:
1480
+ return
1481
+
1482
+ props = self._myprops
1483
+
1484
+ self.lookat = (props[('Look at', 'X')], props[('Look at', 'Y')], props[('Look at', 'Z')])
1485
+ self.eye = (props[('Camera', 'X')], props[('Camera', 'Y')], props[('Camera', 'Z')])
1486
+
1487
+ color = np.asarray(props[('Background', 'Color')])
1488
+ self.bg_color(color / 255.)
1489
+ color = np.asarray(props[('Background', 'Top Color')])
1490
+ self.bg_color_top(color / 255.)
1491
+ color = np.asarray(props[('Background', 'Bottom Color')])
1492
+ self.bg_color_bottom(color / 255.)
1493
+
1494
+ self.floor_level(props[('Floor', 'Level')])
1495
+ color = np.asarray(props[('Floor', 'Color')])
1496
+ self.floor_color(color / 255.)
1497
+
1498
+ self.show_grid(props[('Infos', 'Grid')])
1499
+ self.show_axis(props[('Infos', 'Axis')])
1500
+ self.show_info(props[('Infos', 'values')])
1501
+
1502
+ self.point_size = props[('Points', 'Size')]
1503
+
1504
+ codes_sel = props[('Selection', 'Codes')]
1505
+ codes_sel = codes_sel.split(',')
1506
+ try:
1507
+ self._select_only_codes = list(set([int(curcode) for curcode in codes_sel]))
1508
+ except Exception as e:
1509
+ logging.error(e)
1510
+ logging.warning(_('Nullify selection filter - Check your input'))
1511
+ self._select_only_codes = []
1512
+
1513
+ def _fill_props(self, full:bool = False):
1514
+ """ Fill properties from attributes """
1515
+ if self._myprops is None:
1516
+ return
1517
+
1518
+ props = self._myprops
1519
+
1520
+ props[('Look at', 'X')] = self.lookat[0]
1521
+ props[('Look at', 'Y')] = self.lookat[1]
1522
+ props[('Look at', 'Z')] = self.lookat[2]
1523
+
1524
+ props[('Camera', 'X')] = self.eye[0]
1525
+ props[('Camera', 'Y')] = self.eye[1]
1526
+ props[('Camera', 'Z')] = self.eye[2]
1527
+
1528
+ props[('Relative Position', 'Phi')] = self.phi
1529
+ props[('Relative Position', 'Theta')] = self.theta
1530
+ props[('Relative Position', 'R')] = self.r
1531
+
1532
+ if full:
1533
+
1534
+ props[('Background', 'Color')] = self._bg_color
1535
+ props[('Background', 'Top Color')] = self._bg_color_top
1536
+ props[('Background', 'Bottom Color')] = self._bg_color_bottom
1537
+
1538
+ props[('Floor', 'Level')] = self._floor_level
1539
+ props[('Floor', 'Color')] = self._floor_color
1540
+
1541
+ props[('Infos', 'Grid')] = self._show_grid
1542
+ props[('Infos', 'Axis')] = self._show_axis
1543
+ props[('Infos', 'values')] = self._show_info
1544
+
1545
+ props[('Points', 'Size')] = self._point_size
1546
+
1547
+ props.Populate()
1548
+
1549
+ def show_properties(self):
1550
+ """ Surcharged method (see Element_To_Draw) to show properties from MapViewer"""
1551
+ if self.viewer is None:
1552
+ return
1553
+
1554
+ self._create_props()
1555
+ self._myprops.Show()
1556
+
685
1557
  def find_pointsXYZ(xyz:np.ndarray, bounds:Union[tuple[tuple[float,float],tuple[float,float]], list[list[float, float],list[float, float]]]) -> np.ndarray:
686
1558
 
687
1559
  xb=bounds[0]
@@ -1096,7 +1968,7 @@ def myviewer(las:Union[np.ndarray, list[laspy.LasData], laspy.LasData], which_co
1096
1968
 
1097
1969
  colors = get_colors(las, which_colors, fname=fname, palette_classif= palette_classif)
1098
1970
 
1099
- v=viewer(xyz,colors)
1971
+ v = viewer(xyz,colors)
1100
1972
  v.set(point_size=.05)
1101
1973
  return v
1102
1974