wolfhece 2.2.45__py3-none-any.whl → 2.2.47__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.
@@ -0,0 +1,1453 @@
1
+ import logging
2
+ import math
3
+ from turtle import left, right
4
+ import numpy as np
5
+ import numpy.ma as ma
6
+ from pathlib import Path
7
+ import matplotlib.pyplot as plt
8
+ from enum import Enum
9
+ from scipy.ndimage import label, sum_labels, find_objects
10
+ import pymupdf as pdf
11
+ import wx
12
+ from tqdm import tqdm
13
+ from matplotlib import use, get_backend
14
+ from typing import Literal
15
+
16
+ from .common import A4_rect, rect_cm, list_to_html, list_to_html_aligned, get_rect_from_text, dict_to_html, dataframe_to_html
17
+ from .common import inches2cm, pts2cm, cm2pts, cm2inches, DefaultLayoutA4, NamedTemporaryFile, pt2inches, TemporaryDirectory
18
+ from ..wolf_array import WolfArray, header_wolf, vector, zone, Zones, wolfvertex as wv, wolfpalette
19
+ from ..PyVertexvectors import vector, zone, Zones, wolfvertex as wv
20
+ from ..PyCrosssections import crosssections, profile
21
+ from ..PyTranslate import _
22
+ from .pdf import PDFViewer
23
+
24
+ class CSvsDEM_MainLayout(DefaultLayoutA4):
25
+ """
26
+ Layout for comparing cross-sections, array and Lidar LAZ in a report.
27
+
28
+ 1 cadre pour la zone traitée avec photo de fond ign + contour vectoriel
29
+ 1 cadre avec zoom plus large min 250m
30
+ 1 cadre avec matrice ref + contour vectoriel
31
+ 1 cadre avec matrice à comparer + contour vectoriel
32
+ 1 cadre avec différence
33
+ 1 cadre avec valeurs de synthèse
34
+
35
+ 1 cadre avec histogramme
36
+ 1 cadre avec histogramme des différences
37
+ """
38
+
39
+ def __init__(self, title:str, filename = '', ox = 0, oy = 0, tx = 0, ty = 0, parent=None, is2D=True, idx = '', plotted = True, mapviewer=None, need_for_wx = False, bbox = None, find_minmax = True, shared = False, colors = None):
40
+ super().__init__(title, filename, ox, oy, tx, ty, parent, is2D, idx, plotted, mapviewer, need_for_wx, bbox, find_minmax, shared, colors)
41
+
42
+ useful = self.useful_part
43
+
44
+ width = useful.xmax - useful.xmin
45
+ height = useful.ymax - useful.ymin
46
+
47
+ self._map = self.add_element("Map", width=10., height=10., x = useful.xmin, y=useful.ymax - 10.)
48
+
49
+ self._summaries = self.add_element_repeated("Summary", width= width - 10. - self.padding, height=5. - self.padding/2.,
50
+ first_x=self._map.xmax + self.padding, first_y=self._map.ymin,
51
+ count_x=1, count_y=2, padding=0.5)
52
+
53
+ self._tables = self.add_element_repeated("Tables", width= (width-self.padding) / 2, height= 10.,
54
+ first_x=useful.xmin, first_y=useful.ymin,
55
+ count_x=2, count_y=1, padding=0.5)
56
+
57
+ self._histogram = self.add_element("Histogram", width= width, height= 3.,
58
+ x=useful.xmin, y=self._tables.ymax + self.padding)
59
+
60
+ class CSvsDEM_IndividualLayout(DefaultLayoutA4):
61
+
62
+ def __init__(self, title:str, filename = '', ox = 0, oy = 0, tx = 0, ty = 0, parent=None, is2D=True, idx = '', plotted = True, mapviewer=None, need_for_wx = False, bbox = None, find_minmax = True, shared = False, colors = None):
63
+ super().__init__(title, filename, ox, oy, tx, ty, parent, is2D, idx, plotted, mapviewer, need_for_wx, bbox, find_minmax, shared, colors)
64
+
65
+ useful = self.useful_part
66
+
67
+ width = useful.xmax - useful.xmin
68
+ height = useful.ymax - useful.ymin
69
+
70
+ self._maps = self.add_element_repeated("Maps", width= (width-self.padding) / 2, height=9., count_x=2, count_y=1, first_x=useful.xmin, first_y=useful.ymax - 9.)
71
+
72
+ self._cs = self.add_element("Cross-Sections", width= width, height=9., x=useful.xmin, y=self._maps.ymin - 9. - self.padding)
73
+
74
+ self._dem = self.add_element("DEM", width= (width - self.padding) / 3, height=5., x = useful.xmin, y=useful.ymin)
75
+ self._compare_cs = self.add_element("Comparison", width= (width - self.padding) * 2 / 3, height=5., x = self._dem.xmax + self.padding, y=useful.ymin)
76
+
77
+ class CSvsDEM():
78
+ """
79
+ Class to manage the difference between a unique cross-section and a DEM.
80
+ """
81
+
82
+ def __init__(self, data_group:list, idx:int, dem: WolfArray, title:str = "", index_group:int = 0,index_cs:int = 0, rebinned_dem:WolfArray = None):
83
+
84
+ self._dpi = 600
85
+ self.default_size_hitograms = (12, 6)
86
+ self.default_size_arrays = (10, 10)
87
+ self._fontsize = 6
88
+
89
+ self._data_group = data_group
90
+ self._idx = idx
91
+
92
+ self.dem = dem
93
+ self._rebinned_dem:WolfArray = rebinned_dem
94
+
95
+ if self.dem.nbnotnull > 1_000_000 and self._rebinned_dem is None:
96
+ logging.warning("The DEM has more than 1 million valid cells. Plotting a rebin one.")
97
+ self._rebinned_dem = WolfArray(mold=dem)
98
+ self._rebinned_dem.rebin(10)
99
+ self._rebinned_dem.mypal = dem.mypal
100
+
101
+ self._cs: profile
102
+ self._cs = data_group[idx]['profile']
103
+
104
+ assert isinstance(self.dem, WolfArray), "DEM must be a WolfArray instance."
105
+ assert isinstance(self._cs, profile), "Cross-section must be a profile instance."
106
+
107
+ self.title = title
108
+ self.index_cs = index_cs
109
+ self.index_group = index_group
110
+
111
+ self._background = 'IGN'
112
+
113
+ @property
114
+ def differences(self) -> tuple[float, float]:
115
+ """ Get the difference between the cross-section and the DEM at extremities. """
116
+
117
+ if not isinstance(self.dem, WolfArray):
118
+ raise TypeError("DEM must be an instance of WolfArray")
119
+
120
+ # Get the DEM value at the cross-section location
121
+ dem_value = self.dem.get_value(self._cs[0].x, self._cs[0].y)
122
+
123
+ if dem_value is None or ma.is_masked(dem_value):
124
+ return np.nan
125
+
126
+ # Get the cross-section value (assuming it's a single value at this point)
127
+ cs_value = self._cs[0].z
128
+
129
+ if cs_value is None or ma.is_masked(cs_value):
130
+ return np.nan
131
+
132
+ diff_left = cs_value - dem_value
133
+
134
+ dem_value = self.dem.get_value(self._cs[-1].x, self._cs[-1].y)
135
+ if dem_value is None or ma.is_masked(dem_value):
136
+ return np.nan
137
+ cs_value = self._cs[-1].z
138
+ if cs_value is None or ma.is_masked(cs_value):
139
+ return np.nan
140
+ diff_right = cs_value - dem_value
141
+ return diff_left, diff_right
142
+
143
+ def __str__(self):
144
+
145
+ l_diff, r_diff = self.differences
146
+
147
+ ret = self._label + '\n'
148
+ ret += _("Group : ") + str(self._idx) + '\n'
149
+ ret += _("Section : ") + str(self._cs.myname) + '\n'
150
+ ret += _("Left coordinates (X, Y) : ({:3f},{:3f})").format(self._cs[0].x, self._cs[0].y) + '\n'
151
+ ret += _("Right coordinates (X, Y) : ({:3f},{:3f})").format(self._cs[-1].x, self._cs[-1].y) + '\n'
152
+ ret += _("Left difference : {:3f}").format(l_diff) + '\n'
153
+ ret += _("Right difference : {:3f}").format(r_diff) + '\n'
154
+
155
+ return ret
156
+
157
+ def set_palette_distribute(self, minval:float, maxval:float, step:int=0):
158
+ """
159
+ Set the palette for both arrays.
160
+ """
161
+ self.dem.mypal.distribute_values(minval, maxval, step)
162
+
163
+ def set_palette(self, values:list[float], colors:list[tuple[int, int, int]]):
164
+ """
165
+ Set the palette for both arrays based on specific values.
166
+ """
167
+ self.dem.mypal.set_values_colors(values, colors)
168
+
169
+ def plot_position_grey(self, figax:tuple[plt.Figure, plt.Axes]=None, size_around:float=250) -> tuple[plt.Figure, plt.Axes]:
170
+ """
171
+ Plot the reference array with a background.
172
+ """
173
+ if figax is None:
174
+ figax = plt.subplots()
175
+
176
+ fig, ax = figax
177
+
178
+ h = self.dem.get_header()
179
+ h.origx = (self._cs[0].x + self._cs[-1].x)/2. - size_around
180
+ h.origy = (self._cs[0].y + self._cs[-1].y)/2. - size_around
181
+ h.dx = size_around * 2
182
+ h.dy = size_around * 2
183
+ h.nbx = 1
184
+ h.nby = 1
185
+
186
+ new = WolfArray(srcheader=h)
187
+ new.array.mask[:,:] = True
188
+
189
+ if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
190
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
191
+ first_mask_data=False, with_legend=False,
192
+ update_palette= False,
193
+ Cartoweb= True,
194
+ cat = 'topo_grey',
195
+ )
196
+ else:
197
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
198
+ first_mask_data=False, with_legend=False,
199
+ update_palette= False,
200
+ Cartoweb= False,
201
+ cat = 'topo_grey',
202
+ )
203
+
204
+ copy = self._cs.deepcopy()
205
+ copy.myprop.width = 1
206
+ copy.myprop.color = 0xFF0000
207
+ copy.plot_matplotlib(ax=ax)
208
+ self._cs._plot_extremities(ax, s=10)
209
+
210
+ self.plot_cs_in_group(figax=figax, width=1, color=0x0000FF)
211
+
212
+ return fig, ax
213
+
214
+
215
+ def plot_position(self, figax:tuple[plt.Figure, plt.Axes]=None,
216
+ width:int = 3,
217
+ color:int = 0xFF0000) -> tuple[plt.Figure, plt.Axes]:
218
+ """
219
+ Plot the dem array.
220
+ """
221
+ if figax is None:
222
+ figax = plt.subplots()
223
+
224
+ fig, ax = figax
225
+
226
+ h = self.dem.get_header()
227
+ h.dx = h.nbx * h.dx
228
+ h.dy = h.nby * h.dy
229
+ h.nbx = 1
230
+ h.nby = 1
231
+
232
+ new_array = WolfArray(srcheader=h)
233
+ new_array.array.mask[:,:] = True
234
+
235
+ if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
236
+ new_array.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
237
+ first_mask_data=False, with_legend=False,
238
+ update_palette= False,
239
+ IGN= True,
240
+ cat = 'orthoimage_coverage',
241
+ )
242
+
243
+ elif self._background.upper() == 'WALONMAP':
244
+ new_array.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
245
+ first_mask_data=False, with_legend=False,
246
+ update_palette= False,
247
+ Walonmap= True,
248
+ cat = 'IMAGERIE/ORTHO_2022_ETE',
249
+ )
250
+ else:
251
+ new_array.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
252
+ first_mask_data=False, with_legend=False,
253
+ update_palette= False,
254
+ Walonmap= False,
255
+ )
256
+
257
+
258
+ copy = self._cs.deepcopy()
259
+ copy.myprop.width = width
260
+ copy.myprop.color = color
261
+ copy.plot_matplotlib(ax=ax)
262
+ del copy
263
+
264
+ return fig, ax
265
+
266
+ def plot_position_around(self,
267
+ figax:tuple[plt.Figure, plt.Axes]=None,
268
+ size_around:float = 50.,
269
+ width:int = 3,
270
+ color:int = 0xFF0000,
271
+ s_extremities:int = 50,
272
+ colors_extremities:tuple[str, str] = ('blue', 'green')) -> tuple[plt.Figure, plt.Axes]:
273
+ """
274
+ Plot the dem array.
275
+ """
276
+ if figax is None:
277
+ figax = plt.subplots()
278
+
279
+ fig, ax = figax
280
+
281
+ # search bounds in group
282
+ min_x = min([sect['profile'].xmin for sect in self._data_group])
283
+ max_x = max([sect['profile'].xmax for sect in self._data_group])
284
+ min_y = min([sect['profile'].ymin for sect in self._data_group])
285
+ max_y = max([sect['profile'].ymax for sect in self._data_group])
286
+
287
+ h = self.dem.get_header()
288
+ h.origx = min_x - size_around
289
+ h.origy = min_y - size_around
290
+ h.dx = (max_x - min_x + size_around * 2)
291
+ h.dy = (max_y - min_y + size_around * 2)
292
+ h.nbx = 1
293
+ h.nby = 1
294
+
295
+ new_array = WolfArray(srcheader=h)
296
+ new_array.array.mask[:,:] = True
297
+
298
+ if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
299
+ new_array.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
300
+ first_mask_data=False, with_legend=False,
301
+ update_palette= False,
302
+ IGN= True,
303
+ cat = 'orthoimage_coverage',
304
+ )
305
+
306
+ elif self._background.upper() == 'WALONMAP':
307
+ new_array.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
308
+ first_mask_data=False, with_legend=False,
309
+ update_palette= False,
310
+ Walonmap= True,
311
+ cat = 'IMAGERIE/ORTHO_2022_ETE',
312
+ )
313
+ else:
314
+ new_array.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
315
+ first_mask_data=False, with_legend=False,
316
+ update_palette= False,
317
+ Walonmap= False,
318
+ )
319
+
320
+
321
+ copy = self._cs.deepcopy()
322
+ copy.myprop.width = width
323
+ copy.myprop.color = color
324
+ copy.plot_matplotlib(ax=ax)
325
+ self._cs._plot_extremities(ax, s=s_extremities, colors=colors_extremities)
326
+
327
+ self.plot_cs_in_group(figax=figax, width=2, color=0x0000FF)
328
+
329
+ return fig, ax
330
+
331
+ def plot_cs_in_group(self, figax:tuple[plt.Figure, plt.Axes]=None,
332
+ width:int = 2,
333
+ color:int = 0x0000FF) -> tuple[plt.Figure, plt.Axes]:
334
+ """
335
+ Plot the others cross-sections in the group if exists.
336
+ """
337
+
338
+ if len(self._data_group) <= 1:
339
+ return figax
340
+
341
+ if figax is None:
342
+ figax = plt.subplots()
343
+
344
+ fig, ax = figax
345
+
346
+ for idx, sect in enumerate(self._data_group):
347
+ if idx == self._idx:
348
+ continue
349
+
350
+ cs:profile
351
+ cs = sect['profile']
352
+ copy = cs.deepcopy()
353
+ copy.myprop.width = width
354
+ copy.myprop.color = color
355
+ copy.plot_matplotlib(ax=ax)
356
+ del copy
357
+
358
+ return fig, ax
359
+
360
+ def plot_dem_around(self,
361
+ figax:tuple[plt.Figure, plt.Axes]=None,
362
+ size_around:float = 10.,
363
+ width:int = 3,
364
+ color:int = 0xFF0000,
365
+ s_extremities:int = 50,
366
+ colors_extremities:tuple[str, str] = ('blue', 'green')) -> tuple[plt.Figure, plt.Axes]:
367
+ """
368
+ Plot the dem array.
369
+ """
370
+ if figax is None:
371
+ figax = plt.subplots()
372
+
373
+ fig, ax = figax
374
+
375
+ h = self.dem.get_header()
376
+ h.origx = (self._cs[0].x + self._cs[-1].x)/2. - size_around
377
+ h.origy = (self._cs[0].y + self._cs[-1].y)/2. - size_around
378
+ h.dx = size_around * 2
379
+ h.dy = size_around * 2
380
+ h.nbx = 1
381
+ h.nby = 1
382
+
383
+ new_array = WolfArray(mold = self.dem, crop=h.get_bounds())
384
+ new_array.updatepalette()
385
+ new_array.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
386
+
387
+ copy = self._cs.deepcopy()
388
+ copy.myprop.width = width
389
+ copy.myprop.color = color
390
+ copy.plot_matplotlib(ax=ax)
391
+ self._cs._plot_extremities(ax, s=s_extremities, colors=colors_extremities)
392
+
393
+ ax.legend(fontsize=6)
394
+
395
+ return fig, ax
396
+
397
+
398
+ def plot_position_scaled(self, scale = 4,
399
+ figax:tuple[plt.Figure, plt.Axes]=None,
400
+ width:int = 3,
401
+ color:int = 0xFF0000) -> tuple[plt.Figure, plt.Axes]:
402
+ """
403
+ Plot the reference array.
404
+
405
+ :param scale: Scale factor to apply to the extent of the DEM. For example, scale=1 will double the extent, scale=2 will triple it, etc.
406
+ :param figax: Tuple of (Figure, Axes) to plot on. If None, a new figure and axes will be created.
407
+ """
408
+ if figax is None:
409
+ figax = plt.subplots()
410
+
411
+ fig, ax = figax
412
+
413
+ h = self.dem.get_header()
414
+ a_width = h.nbx * h.dx
415
+ a_height = h.nby * h.dy
416
+
417
+ h.origx += -a_width * scale / 2
418
+ h.origy += -a_height *scale / 2
419
+ h.nbx = 1
420
+ h.nby = 1
421
+ h.dx = a_width *(scale + 1)
422
+ h.dy = a_height *(scale + 1)
423
+
424
+ new = WolfArray(srcheader=h)
425
+ new.array.mask[:,:] = True
426
+
427
+ if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
428
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
429
+ first_mask_data=False, with_legend=False,
430
+ update_palette= False,
431
+ IGN= True,
432
+ cat = 'orthoimage_coverage')
433
+ elif self._background.upper() == 'WALONMAP':
434
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
435
+ first_mask_data=False, with_legend=False,
436
+ update_palette= False,
437
+ Walonmap= True,
438
+ cat = 'IMAGERIE/ORTHO_2022_ETE')
439
+ else:
440
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
441
+ first_mask_data=False, with_legend=False,
442
+ update_palette= False,
443
+ Walonmap= False)
444
+
445
+ copy = self._cs.deepcopy()
446
+ copy.myprop.width = width
447
+ copy.myprop.color = color
448
+ copy.plot_matplotlib(ax=ax)
449
+ del copy
450
+
451
+ return fig, ax
452
+
453
+ def plot_dem(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
454
+ """
455
+ Plot the reference array.
456
+ """
457
+ if figax is None:
458
+ figax = plt.subplots()
459
+
460
+ fig, ax = figax
461
+
462
+ if self._rebinned_dem is not None:
463
+ self._rebinned_dem.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
464
+ else:
465
+ self.dem.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
466
+
467
+ copy = self._cs.deepcopy()
468
+ copy.myprop.width = 5
469
+ copy.myprop.color = 0xFF0000
470
+ copy.plot_matplotlib(ax=ax)
471
+ del copy
472
+
473
+ return fig, ax
474
+
475
+ def plot_cs(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
476
+ """
477
+ Plot the cross section to compare.
478
+ """
479
+ if figax is None:
480
+ figax = plt.subplots()
481
+
482
+ fig, ax = figax
483
+
484
+ old_plotted = self.dem.plotted
485
+ self.dem.plotted = True
486
+ self._cs.plot_cs(fig = fig, ax= ax, linked_arrays={"DEM": self.dem})
487
+ self.dem.plotted = old_plotted
488
+
489
+ return fig, ax
490
+
491
+ def plot_cs_min_at_x0(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
492
+ """
493
+ Plot the cross section to compare.
494
+ """
495
+ if figax is None:
496
+ figax = plt.subplots()
497
+
498
+ fig, ax = figax
499
+
500
+ self._cs._plot_only_cs_min_at_x0(fig = fig, ax= ax, label = 'Cross-Section', style='solid', lw=2)
501
+
502
+ return fig, ax
503
+
504
+ def plot_cs_limited(self, figax:tuple[plt.Figure, plt.Axes]=None, tolerance:float = 1.) -> tuple[plt.Figure, plt.Axes]:
505
+ """
506
+ Plot the cross section to compare.
507
+ """
508
+ if figax is None:
509
+ figax = plt.subplots()
510
+
511
+ fig, ax = figax
512
+
513
+ old_plotted = self.dem.plotted
514
+ self.dem.plotted = True
515
+ self._cs.plot_cs(fig = fig, ax= ax, linked_arrays={"DEM": self.dem}, forceaspect=False)
516
+ self.dem.plotted = old_plotted
517
+
518
+ minz_cs = self._cs.zmin - tolerance
519
+ maxz_cs = self._cs.zmax + tolerance
520
+
521
+ # round to 0 decimals but keep as float
522
+ minz_cs = round(minz_cs, 0)
523
+ maxz_cs = math.ceil(maxz_cs)
524
+
525
+ ax.set_ylim(minz_cs, maxz_cs)
526
+
527
+ return fig, ax
528
+
529
+ def plot_up_down_min_at_x0(self, figax:tuple[plt.Figure, plt.Axes]=None, n_iter = 2) -> tuple[plt.Figure, plt.Axes]:
530
+ """
531
+ Plot the cross section to compare.
532
+ """
533
+ if figax is None:
534
+ figax = plt.subplots()
535
+
536
+ fig, ax = figax
537
+
538
+ self._cs._plot_only_cs_min_at_x0(fig = fig, ax= ax, label = _('Cross-Section'), style='solid', lw=2)
539
+
540
+ n_iter_up = n_iter
541
+ n_iter_down = n_iter
542
+
543
+ cs_up = self._cs.up
544
+ while n_iter_up > 0 and cs_up is not None:
545
+ if cs_up is self._cs:
546
+ break
547
+ cs_up._plot_only_cs_min_at_x0(fig = fig, ax= ax, style='dashed', label = _('Upstream {}').format(n_iter - n_iter_up + 1), col_ax='green', lw = n_iter_up, alpha= 1 - (n_iter - n_iter_up + 1 ) * 0.25)
548
+ cs_up = cs_up.up
549
+ n_iter_up -= 1
550
+
551
+ cs_down = self._cs.down
552
+ while n_iter_down > 0 and cs_down is not None:
553
+ if cs_down is self._cs:
554
+ break
555
+ cs_down._plot_only_cs_min_at_x0(fig = fig, ax= ax, style='dashed', label = _('Downstream {}').format(n_iter - n_iter_down + 1), col_ax='blue', lw = n_iter_down + 1, alpha= 1 - (n_iter - n_iter_down + 1) * 0.25)
556
+ cs_down = cs_down.down
557
+ n_iter_down -= 1
558
+
559
+ ax.legend(fontsize=6)
560
+
561
+ return fig, ax
562
+
563
+ def _complete_report(self, page:CSvsDEM_IndividualLayout):
564
+
565
+ """
566
+ Complete the report with the arrays and histograms.
567
+ """
568
+ useful = page.useful_part
569
+
570
+ # Plot reference array
571
+ key_fig = [('Maps_0-0', self.plot_position_around),
572
+ ('Maps_1-0', self.plot_position_grey),
573
+ ('Cross-Sections', self.plot_cs_limited),
574
+ ('DEM', self.plot_dem_around),
575
+ ('Comparison', self.plot_up_down_min_at_x0),
576
+ ]
577
+
578
+ keys = page.keys
579
+ for key, fig_routine in key_fig:
580
+ if key in keys:
581
+
582
+ rect = page.layout[key]
583
+
584
+ fig, ax = fig_routine()
585
+
586
+ # set size to fit the rectangle
587
+ fig.set_size_inches(pt2inches(rect.width), pt2inches(rect.height))
588
+
589
+ if 'Histogram' in key:
590
+ fig.tight_layout()
591
+
592
+ # convert canvas to PNG and insert it into the PDF
593
+ temp_file = NamedTemporaryFile(delete=False, suffix='.png')
594
+ fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
595
+ page._page.insert_image(page.layout[key], filename = temp_file.name)
596
+ # delete the temporary file
597
+ temp_file.delete = True
598
+ temp_file.close()
599
+
600
+ # Force to delete fig
601
+ plt.close(fig)
602
+ else:
603
+ logging.warning(f"Key {key} not found in layout. Skipping plot.")
604
+
605
+ key = 'Arrays_1-2'
606
+ if key in keys:
607
+ text, css = list_to_html(self._summary_text, font_size='8pt')
608
+ page._page.insert_htmlbox(page.layout[key], text,
609
+ css=css)
610
+
611
+ def create_report(self, output_file: str | Path = None) -> Path:
612
+ """ Create a page report for the array difference. """
613
+
614
+ from time import sleep
615
+ if output_file is None:
616
+ output_file = Path(f"array_difference_{self.index_cs}.pdf")
617
+
618
+ if output_file.exists():
619
+ logging.warning(f"Output file {output_file} already exists. It will be overwritten.")
620
+
621
+ page = CSvsDEM_IndividualLayout(_("Group {} - Index {} - Cross-Section {}").format(self.index_group, self.index_cs, self._cs.myname))
622
+ page.create_report()
623
+ self._complete_report(page)
624
+ page.save_report(output_file)
625
+ sleep(0.2) # Ensure the file is saved before returning
626
+
627
+ return output_file
628
+
629
+
630
+ class CompareMultipleCSvsDEM:
631
+
632
+ def __init__(self, cross_sections:crosssections | Path | str,
633
+ dem: WolfArray | str | Path,
634
+ laz_directory: Path | str = None,
635
+ support: Path | str | vector = None,
636
+ threshold_z: float = 0.5,
637
+ distance_threshold: float = 50.):
638
+ """ Compare multiple cross-sections with a DEM.
639
+
640
+ :param cross_sections: Cross-sections to compare. Can be a crosssections object or a path to a vector file (.vecz).
641
+ :param dem: DEM to compare with. Can be a WolfArray object or a path to a raster file.
642
+ :param laz_directory: Directory where the LAZ files are stored (Numpy-Wolf format).
643
+ :param support: Support vector to sort the cross-sections along. Can be a path to a vector file (first vector in the first zone will be used) or a vector object.
644
+ """
645
+
646
+ if isinstance(support, (str, Path)):
647
+ if not Path(support).exists():
648
+ logging.error(f"The support file {support} does not exist. Centers will be used.")
649
+ self._support = None
650
+ support = Zones(support)
651
+ self._support = support[(0,0)] # get the first zone
652
+ elif isinstance(support, vector):
653
+ self._support = support
654
+ else:
655
+ self._support = None
656
+ logging.warning("The support is not a valid file or vector. Centers will be used.")
657
+
658
+ self._dpi = 600
659
+ self.default_size_arrays = (10, 10)
660
+ self._fontsize = 6
661
+
662
+ if isinstance(dem, (str, Path)):
663
+ dem = WolfArray(dem)
664
+
665
+ assert isinstance(dem, WolfArray), "DEM must be a WolfArray instance."
666
+
667
+ if isinstance(cross_sections, (str, Path)):
668
+ cross_sections = crosssections(cross_sections, format='vecz', dirlaz=laz_directory)
669
+
670
+ assert isinstance(cross_sections, crosssections), "Cross-sections must be a crosssections instance."
671
+
672
+ if self._support is None:
673
+ self._support = cross_sections.create_vector_from_centers()
674
+
675
+ cross_sections.sort_along(self._support, 'support', downfirst=False)
676
+
677
+ assert cross_sections.check_left_right_coherence() == 0, "Cross-sections are not coherent in left/right orientation."
678
+
679
+ self._dirlaz = laz_directory
680
+ self._cs = cross_sections
681
+
682
+ if self._cs.dirlaz is None or self._cs.dirlaz != self._dirlaz:
683
+ logging.info(f"Setting cross-sections directory for LAZ files to {self._dirlaz}")
684
+ self._cs.dirlaz = self._dirlaz
685
+
686
+ self.dem:WolfArray
687
+ self.dem = dem
688
+ self._rebinned_dem:WolfArray = None
689
+
690
+ if self.dem.nbnotnull > 1_000_000:
691
+ logging.warning("The DEM has more than 1 million valid cells. Plotting a rebin one.")
692
+ self._rebinned_dem = WolfArray(mold=dem)
693
+ self._rebinned_dem.rebin(10)
694
+ self._rebinned_dem.mypal = dem.mypal
695
+
696
+ self.subpages:dict[int, CSvsDEM] = {}
697
+
698
+ self._pdf_path = None
699
+
700
+ self._background = 'IGN'
701
+
702
+ self._groups: list[list[dict['section_id':int, "x": float, "y": float, "diff_left": float, "diff_right": float, "s": float]]]
703
+ self._groups = []
704
+
705
+ self._threshold_z = threshold_z
706
+ self._distance_threshold = distance_threshold
707
+
708
+ self.find_differences(tolerance = self._threshold_z, distance_threshold = self._distance_threshold)
709
+
710
+ logging.info(f"Number of groups of differences: {self.count_groups}")
711
+ logging.info(f"Number of groups of differences greater than 3: {self.count_groups_greater_than(3)}")
712
+
713
+ def find_differences(self, tolerance:float = 0.5, distance_threshold:float = 50.):
714
+ """ Find differences between cross-sections and DEM.
715
+
716
+ Store the differences in self._diffs as a list of lists of dictionaries with keys: section_id, x, y, diff_left, diff_right.
717
+
718
+ We need to group the closest cross-sections that have differences.
719
+ So, we start from the upstream cross-section and go downstream, grouping cross-sections which have differences and are close to each other (less than distance_threshold m apart).
720
+
721
+ :param tolerance: Tolerance in meters to consider a difference. If the absolute difference between the cross-section and the DEM is greater than this value, it is considered a difference.
722
+ """
723
+
724
+ all_profiles = []
725
+
726
+ loc_cs = self._cs.get_upstream()
727
+ loc_profile = loc_cs['cs']
728
+
729
+ while loc_profile.down is not loc_profile:
730
+
731
+ diff_left = abs(self.dem.get_value(loc_profile[0].x, loc_profile[0].y) - loc_profile[0].z)
732
+ diff_right = abs(self.dem.get_value(loc_profile[-1].x, loc_profile[-1].y) - loc_profile[-1].z)
733
+
734
+ if diff_left > tolerance or diff_right > tolerance:
735
+ all_profiles.append({'profile': loc_profile, 'diff_left': diff_left, 'diff_right': diff_right})
736
+
737
+ loc_profile = loc_profile.down
738
+
739
+ diff_left = abs(self.dem.get_value(loc_profile[0].x, loc_profile[0].y) - loc_profile[0].z)
740
+ diff_right = abs(self.dem.get_value(loc_profile[-1].x, loc_profile[-1].y) - loc_profile[-1].z)
741
+
742
+ if diff_left > tolerance or diff_right > tolerance:
743
+ all_profiles.append({'profile': loc_profile, 'diff_left': diff_left, 'diff_right': diff_right})
744
+
745
+ # grouped differences
746
+ self._groups = []
747
+
748
+ all_s = np.array([diff['profile'].s for diff in all_profiles])
749
+
750
+ delta_s = all_s[1:] - all_s[:-1]
751
+
752
+ # group are defined by a gap greater than distance_threshold
753
+ group = np.where(delta_s > distance_threshold)[0]
754
+ # add the last index
755
+ group = np.append(group, len(all_profiles)-1)
756
+
757
+ start = 0
758
+ for g in group:
759
+ new_group = all_profiles[start:g+1]
760
+ self._groups.append(new_group)
761
+ start = g+1
762
+
763
+ self._sort_groups_by_inverse_deltaz()
764
+
765
+ logging.info(f"Found {len(self._groups)} groups of differences on the left or right bank.")
766
+
767
+
768
+ @property
769
+ def count_groups(self) -> int:
770
+ """ How many groups of differences are there? """
771
+ return len(self._groups)
772
+
773
+ @property
774
+ def count_differences(self) -> int:
775
+ """ Count total number of differences. """
776
+ count = 0
777
+ for group in self._groups:
778
+ count += len(group)
779
+ return count
780
+
781
+ def count_groups_greater_than(self, threshold: int) -> int:
782
+ """ How many groups of differences are greater than a given threshold? """
783
+ count = 0
784
+ for group in self._groups:
785
+ if len(group) > threshold:
786
+ count += 1
787
+ return count
788
+
789
+ def _diff_to_dict(self):
790
+ """ Compile dict in list of lists to a single dictionary """
791
+
792
+ diff_dict = {}
793
+ for group in self._groups:
794
+ for sect in group:
795
+ prof = sect['profile']
796
+ # copy sect but exclude profile
797
+ diff_dict[prof.myname] = {k: v for k, v in sect.items() if k != 'profile'}
798
+ return diff_dict
799
+
800
+ def _diff_to_dataframe(self):
801
+ """ Compile dict in list of lists to a single pandas DataFrame.
802
+
803
+ Dataframe columns: x, y, diff
804
+ Dataframe index: profile
805
+ """
806
+
807
+ import pandas as pd
808
+
809
+ rows_left = []
810
+ rows_right = []
811
+ for i_group, group in enumerate(self._groups):
812
+ for sect in group:
813
+ prof = sect['profile']
814
+
815
+ diff_left = sect['diff_left']
816
+ diff_right = sect['diff_right']
817
+
818
+ if diff_left > self._threshold_z:
819
+ left_vert = prof[0]
820
+ row = {'profile': prof.myname, 'x': round(left_vert.x,2), 'y': round(left_vert.y,2), 'diff': round(diff_left,2), 'group':i_group +1}
821
+ rows_left.append(row)
822
+ if diff_right > self._threshold_z:
823
+ right_vert = prof[-1]
824
+ row = {'profile': prof.myname, 'x': round(right_vert.x,2), 'y': round(right_vert.y,2), 'diff': round(diff_right,2), 'group':i_group +1}
825
+ rows_right.append(row)
826
+
827
+ # Sort by diff descending
828
+ rows_left = sorted(rows_left, key=lambda x: x['diff'], reverse=True)
829
+ rows_right = sorted(rows_right, key=lambda x: x['diff'], reverse=True)
830
+
831
+ return pd.DataFrame(rows_left[:min(10, len(rows_left))]), pd.DataFrame(rows_right[:min(10, len(rows_right))])
832
+
833
+ @property
834
+ def _all_XY_diff(self):
835
+ """ Get all X and Y coordinates of the differences. """
836
+ all_left = []
837
+ all_right = []
838
+ for group in self._groups:
839
+ for item in group:
840
+ prof = item["profile"]
841
+ left_vert = prof[0]
842
+ right_vert = prof[-1]
843
+
844
+ diff_left = item["diff_left"]
845
+ diff_right = item["diff_right"]
846
+ if diff_left > self._threshold_z:
847
+ all_left.append((left_vert.x, left_vert.y, diff_left))
848
+ if diff_right > self._threshold_z:
849
+ all_right.append((right_vert.x, right_vert.y, diff_right))
850
+
851
+ return all_left, all_right
852
+
853
+ @property
854
+ def _all_differences_as_np(self) -> np.ndarray:
855
+ """ Get all differences as a single array. """
856
+ all_diffs = []
857
+ for group in self._groups:
858
+ for item in group:
859
+ diff_left = item["diff_left"]
860
+ diff_right = item["diff_right"]
861
+ if diff_left > self._threshold_z:
862
+ all_diffs.append(item["diff_left"])
863
+ if diff_right > self._threshold_z:
864
+ all_diffs.append(item["diff_right"])
865
+
866
+ return np.array(all_diffs)
867
+
868
+ @property
869
+ def _all_left_differences_as_np(self) -> np.ndarray:
870
+ """ Get all left differences as a single array. """
871
+ all_diffs = []
872
+ for group in self._groups:
873
+ for item in group:
874
+ diff_left = item["diff_left"]
875
+ if diff_left > self._threshold_z:
876
+ all_diffs.append(item["diff_left"])
877
+
878
+ return np.array(all_diffs)
879
+
880
+ @property
881
+ def _all_right_differences_as_np(self) -> np.ndarray:
882
+ """ Get all right differences as a single array. """
883
+ all_diffs = []
884
+ for group in self._groups:
885
+ for item in group:
886
+ diff_right = item["diff_right"]
887
+ if diff_right > self._threshold_z:
888
+ all_diffs.append(item["diff_right"])
889
+
890
+ return np.array(all_diffs)
891
+
892
+ def plot_histogram_differences(self, figax:tuple[plt.Figure, plt.Axes]=None, density = True, alpha = 0.3, **kwargs) -> tuple[plt.Figure, plt.Axes]:
893
+ """
894
+ Plot histogram of all differences.
895
+ """
896
+ if figax is None:
897
+ figax = plt.subplots(figsize=(8, 3))
898
+
899
+ fig, ax = figax
900
+
901
+ difference_data = self._all_differences_as_np
902
+ ax.hist(difference_data, bins= min(100, int(len(difference_data)/4)), density=density, alpha=alpha, **kwargs)
903
+
904
+ diff_left = self._all_left_differences_as_np
905
+ diff_right = self._all_right_differences_as_np
906
+ ax.hist(diff_left, bins= min(100, int(len(diff_left)/4)), density=density, alpha=alpha, color='green', label='Left bank', **kwargs)
907
+ ax.hist(diff_right, bins= min(100, int(len(diff_right)/4)), density=density, alpha=alpha, color='orange', label='Right bank', **kwargs)
908
+
909
+ # ax.set_xlabel("Value")
910
+ # ax.set_ylabel("Frequency")
911
+
912
+ # set font size of the labels
913
+ ax.tick_params(axis='both', which='major', labelsize=6)
914
+ for label in ax.get_xticklabels():
915
+ label.set_fontsize(self._fontsize)
916
+ for label in ax.get_yticklabels():
917
+ label.set_fontsize(self._fontsize)
918
+ # and gfor the label title
919
+ ax.set_xlabel(ax.get_xlabel(), fontsize=self._fontsize)
920
+ ax.set_ylabel(ax.get_ylabel(), fontsize=self._fontsize)
921
+
922
+ # Add median and mean lines
923
+ mean_val = np.mean(difference_data)
924
+ median_val = np.median(difference_data)
925
+ ax.axvline(mean_val, color='r', linestyle='dashed', linewidth=1, label=f'Mean: {mean_val:.2f}')
926
+ ax.axvline(median_val, color='g', linestyle='dashed', linewidth=1, label=f'Median: {median_val:.2f}')
927
+
928
+ ax.legend(fontsize=self._fontsize)
929
+
930
+ return fig, ax
931
+
932
+ def _read_differences_json(self, differences: Path | str) -> list[list[dict['section_id':int, "x": float, "y": float, "diff": float]]]:
933
+ """ Differences file is a JSON file with the following structure:
934
+
935
+ List of lists with: "section_id", "x", "y", "diff".
936
+
937
+ List of lists because we want to store groups of cross-sections that are close to each other.
938
+ """
939
+
940
+ if not Path(differences).exists():
941
+ logging.error(f"The differences file {differences} does not exist.")
942
+ return []
943
+
944
+ import json
945
+ with open(differences, 'r', encoding='utf-8') as f:
946
+ data = json.load(f)
947
+
948
+ return data
949
+
950
+ def plot_dem_with_background(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
951
+ """
952
+ Plot the reference array with a background.
953
+ """
954
+ if figax is None:
955
+ figax = plt.subplots()
956
+
957
+ fig, ax = figax
958
+
959
+ h = self.dem.get_header()
960
+ width = h.nbx * h.dx
961
+ height = h.nby * h.dy
962
+ h.dx = width
963
+ h.dy = height
964
+ h.nbx = 1
965
+ h.nby = 1
966
+
967
+ new = WolfArray(srcheader=h)
968
+ new.array.mask[:,:] = True
969
+
970
+ if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
971
+
972
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
973
+ first_mask_data=False, with_legend=False,
974
+ update_palette= False,
975
+ IGN= True,
976
+ cat = 'orthoimage_coverage',
977
+ )
978
+ elif self._background.upper() == 'WALONMAP':
979
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
980
+ first_mask_data=False, with_legend=False,
981
+ update_palette= False,
982
+ Walonmap= True,
983
+ cat = 'IMAGERIE/ORTHO_2022_ETE',
984
+ )
985
+ else:
986
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
987
+ first_mask_data=False, with_legend=False,
988
+ update_palette= False,
989
+ Walonmap= False,
990
+ )
991
+ return fig, ax
992
+
993
+ def plot_cartoweb(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
994
+ """
995
+ Plot the reference array with a background.
996
+ """
997
+ if figax is None:
998
+ figax = plt.subplots()
999
+
1000
+ fig, ax = figax
1001
+
1002
+ h = self.dem.get_header()
1003
+ width = h.nbx * h.dx
1004
+ height = h.nby * h.dy
1005
+ h.dx = width
1006
+ h.dy = height
1007
+ h.nbx = 1
1008
+ h.nby = 1
1009
+
1010
+ new = WolfArray(srcheader=h)
1011
+ new.array.mask[:,:] = True
1012
+
1013
+ if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
1014
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
1015
+ first_mask_data=False, with_legend=False,
1016
+ update_palette= False,
1017
+ Cartoweb= True,
1018
+ cat = 'overlay',
1019
+ )
1020
+ else:
1021
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
1022
+ first_mask_data=False, with_legend=False,
1023
+ update_palette= False,
1024
+ Cartoweb= False,
1025
+ cat = 'overlay',
1026
+ )
1027
+ return fig, ax
1028
+
1029
+ def plot_background_grey(self, figax:tuple[plt.Figure, plt.Axes]=None) -> tuple[plt.Figure, plt.Axes]:
1030
+ """
1031
+ Plot the reference array with a background.
1032
+ """
1033
+ if figax is None:
1034
+ figax = plt.subplots()
1035
+
1036
+ fig, ax = figax
1037
+
1038
+ h = self.dem.get_header()
1039
+ width = h.nbx * h.dx
1040
+ height = h.nby * h.dy
1041
+ h.dx = width
1042
+ h.dy = height
1043
+ h.nbx = 1
1044
+ h.nby = 1
1045
+
1046
+ new = WolfArray(srcheader=h)
1047
+ new.array.mask[:,:] = True
1048
+
1049
+ if self._background.upper() == 'IGN' or self._background.upper() == 'NGI':
1050
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
1051
+ first_mask_data=False, with_legend=False,
1052
+ update_palette= False,
1053
+ Cartoweb= True,
1054
+ cat = 'topo_grey',
1055
+ )
1056
+ else:
1057
+ new.plot_matplotlib(figax=figax, figsize = self.default_size_arrays,
1058
+ first_mask_data=False, with_legend=False,
1059
+ update_palette= False,
1060
+ Cartoweb= False,
1061
+ cat = 'topo_grey',
1062
+ )
1063
+ return fig, ax
1064
+
1065
+ def plot_dem(self, figax:tuple[plt.Figure, plt.Axes]=None, use_rebin_if_exists:bool=True) -> tuple[plt.Figure, plt.Axes]:
1066
+ """
1067
+ Plot the reference array.
1068
+ """
1069
+ if figax is None:
1070
+ figax = plt.subplots()
1071
+
1072
+ fig, ax = figax
1073
+
1074
+ if use_rebin_if_exists and self._rebinned_dem is not None:
1075
+ self._rebinned_dem.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
1076
+ else:
1077
+ self.dem.plot_matplotlib(figax=figax, figsize = self.default_size_arrays, first_mask_data=False, with_legend=True, update_palette= False)
1078
+
1079
+ return fig, ax
1080
+
1081
+ def plot_XY(self, figax:tuple[plt.Figure, plt.Axes]=None,
1082
+ s:float=10, alpha:float=0.5,
1083
+ colorized_diff:bool=False,
1084
+ default_color=('blue', 'red'),
1085
+ which_ones:Literal['left', 'right', 'all'] = 'all') -> tuple[plt.Figure, plt.Axes]:
1086
+ """
1087
+ Plot the XY of the differences.
1088
+
1089
+ :param figax: Tuple of (Figure, Axes) to plot on. If None, a new figure and axes will be created.
1090
+ :param s: Size of the points.
1091
+ :param alpha: Alpha of the points.
1092
+ :param colorized_diff: If True, the points will be colored based on the difference value.
1093
+ :param default_color: If colorized_diff is False, the points will be colored with these colors for left and right.
1094
+ :param which_ones: Which points to plot. Can be 'left', 'right', or 'all'.
1095
+ """
1096
+
1097
+ if figax is None:
1098
+ figax = plt.subplots()
1099
+
1100
+ fig, ax = figax
1101
+
1102
+ lefts, rights = self._all_XY_diff
1103
+
1104
+ if colorized_diff:
1105
+ if which_ones == 'left' or which_ones == 'all':
1106
+ x,y,diff = zip(*lefts)
1107
+ ax.scatter(x, y, c=diff, cmap='bwr', s=s, alpha=alpha)
1108
+
1109
+ if which_ones == 'right' or which_ones == 'all':
1110
+ x,y,diff = zip(*rights)
1111
+ ax.scatter(x, y, c=diff, cmap='bwr', s=s, alpha=alpha)
1112
+ elif default_color is not None:
1113
+ left_color, right_color = default_color
1114
+
1115
+ if which_ones == 'left' or which_ones == 'all':
1116
+ x,y,_ = zip(*lefts)
1117
+ ax.scatter(x, y, color=left_color, s=s, alpha=alpha)
1118
+ if which_ones == 'right' or which_ones == 'all':
1119
+ x,y,_ = zip(*rights)
1120
+ ax.scatter(x, y, color=right_color, s=s, alpha=alpha)
1121
+ else:
1122
+ if which_ones == 'left' or which_ones == 'all':
1123
+ x,y,_ = zip(*lefts)
1124
+ ax.scatter(x, y, color='blue', s=s, alpha=alpha)
1125
+ if which_ones == 'right' or which_ones == 'all':
1126
+ x,y,_ = zip(*rights)
1127
+ ax.scatter(x, y, color='red', s=s, alpha=alpha)
1128
+
1129
+ ax.set_aspect('equal', 'box')
1130
+ fig.tight_layout()
1131
+
1132
+ return fig, ax
1133
+
1134
+ def plot_mainpage_map(self) -> tuple[plt.Figure, plt.Axes]:
1135
+ """ Plot the main page map with all differences. """
1136
+
1137
+ fig, ax = self.plot_background_grey()
1138
+
1139
+ self.plot_XY(figax=(fig, ax), colorized_diff=True)
1140
+
1141
+ return fig, ax
1142
+
1143
+ def _summary_dem(self) -> list:
1144
+ """ Summary of the DEM. """
1145
+
1146
+ return [
1147
+ f"Number of cells: {self.dem.nbnotnull}",
1148
+ f"Resolution (m): {self.dem.dx} x {self.dem.dy}",
1149
+ f"Extent: ({self.dem.origx}, {self.dem.origy}) - ({self.dem.origx + self.dem.nbx * self.dem.dx}, {self.dem.origy + self.dem.nby * self.dem.dy})",
1150
+ f"Width x Height (m): {self.dem.nbx * self.dem.dx} x {self.dem.nby * self.dem.dy}",
1151
+ ]
1152
+
1153
+ def _summary_differences(self) -> list:
1154
+ """ Summary of the differences. """
1155
+
1156
+ all_diff = self._all_differences_as_np
1157
+ all_diff_left = self._all_left_differences_as_np
1158
+ all_diff_right = self._all_right_differences_as_np
1159
+ return [
1160
+ f"Number of groups: {self.count_groups}",
1161
+ f"Number of cross-sections: {self.count_differences}",
1162
+ f"Median difference (m): {np.median(all_diff):.3f}",
1163
+ f"Min difference (m): {np.min(all_diff):.3f}",
1164
+ f"Max difference (m): {np.max(all_diff):.3f}",
1165
+ f"Left - Median difference (m): {np.median(all_diff_left):.3f}",
1166
+ f"Left - Min difference (m): {np.min(all_diff_left):.3f}",
1167
+ f"Left - Max difference (m): {np.max(all_diff_left):.3f}",
1168
+ f"Right - Median difference (m): {np.median(all_diff_right):.3f}",
1169
+ f"Right - Min difference (m): {np.min(all_diff_right):.3f}",
1170
+ f"Right - Max difference (m): {np.max(all_diff_right):.3f}",
1171
+ ]
1172
+
1173
+ def __str__(self):
1174
+
1175
+
1176
+ ret = [_("Array information :")]
1177
+ ret.extend(self._summary_dem())
1178
+ ret.append("")
1179
+ ret.append(_("Differences information :"))
1180
+ ret.extend(self._summary_differences())
1181
+
1182
+ return "\n".join(ret)
1183
+
1184
+ def get_group_info(self, i_group:int) -> str:
1185
+ """ Get information about a specific group of differences. """
1186
+ if i_group < 0 or i_group >= self.count_groups:
1187
+ raise IndexError(f"Group index {i_group} out of range. There are {self.count_groups} groups.")
1188
+
1189
+ group = self._groups[i_group]
1190
+
1191
+ ret = [f"Group {i_group + 1} - Number of cross-sections: {len(group)}"]
1192
+ for sect in group:
1193
+ prof = sect['profile']
1194
+ diff_left = sect['diff_left']
1195
+ diff_right = sect['diff_right']
1196
+ ret.append(f" - Cross-section {prof.myname}: Left difference = {diff_left:.3f} m, Right difference = {diff_right:.3f} m")
1197
+
1198
+ return "\n".join(ret)
1199
+
1200
+ def print_left_differences(self) -> str:
1201
+ """ Print all left differences. """
1202
+
1203
+ ret = [_("Left bank differences:")]
1204
+
1205
+ # Sort differences
1206
+ left_diffs = []
1207
+ for group in self._groups:
1208
+ for sect in group:
1209
+ prof = sect['profile']
1210
+ diff_left = sect['diff_left']
1211
+ if diff_left > self._threshold_z:
1212
+ left_diffs.append((prof.myname, diff_left))
1213
+ left_diffs.sort(key=lambda x: x[1], reverse=True)
1214
+ for name, diff in left_diffs:
1215
+ ret.append(f" - Cross-section {name}: {diff:.3f} m")
1216
+
1217
+ return "\n".join(ret)
1218
+
1219
+ def print_right_differences(self) -> str:
1220
+ """ Print all right differences. """
1221
+
1222
+ ret = [_("Right bank differences:")]
1223
+
1224
+ # Sort differences
1225
+ right_diffs = []
1226
+ for group in self._groups:
1227
+ for sect in group:
1228
+ prof = sect['profile']
1229
+ diff_right = sect['diff_right']
1230
+ if diff_right > self._threshold_z:
1231
+ right_diffs.append((prof.myname, diff_right))
1232
+ right_diffs.sort(key=lambda x: x[1], reverse=True)
1233
+ for name, diff in right_diffs:
1234
+ ret.append(f" - Cross-section {name}: {diff:.3f} m")
1235
+
1236
+ return "\n".join(ret)
1237
+
1238
+ def _complete_report_mainpage(self, page:CSvsDEM_MainLayout):
1239
+ """ Complete the report with the global summary and individual differences. """
1240
+
1241
+ key_fig = [('Map', self.plot_mainpage_map),
1242
+ ('Histogram', self.plot_histogram_differences),
1243
+ ]
1244
+
1245
+ key_list = [('Summary_0-0', self._summary_dem),
1246
+ ('Summary_0-1', self._summary_differences),
1247
+ ]
1248
+
1249
+ df_left, df_right = self._diff_to_dataframe()
1250
+ key_table = [('Tables_0-0', df_left),
1251
+ ('Tables_1-0', df_right),
1252
+ ]
1253
+
1254
+ keys = page.keys
1255
+ for key, fig_routine in key_fig:
1256
+ if key in keys:
1257
+
1258
+ rect = page.layout[key]
1259
+
1260
+ fig, ax = fig_routine()
1261
+
1262
+ # set size to fit the rectangle
1263
+ fig.set_size_inches(pt2inches(rect.width), pt2inches(rect.height))
1264
+
1265
+ # convert canvas to PNG and insert it into the PDF
1266
+ temp_file = NamedTemporaryFile(delete=False, suffix='.png')
1267
+ fig.savefig(temp_file, format='png', bbox_inches='tight', dpi=self._dpi)
1268
+ page._page.insert_image(page.layout[key], filename=temp_file.name)
1269
+ # delete the temporary file
1270
+ temp_file.delete = True
1271
+ temp_file.close()
1272
+
1273
+ # Force to delete fig
1274
+ plt.close(fig)
1275
+ else:
1276
+ logging.warning(f"Key {key} not found in layout. Skipping plot.")
1277
+
1278
+ for key, txt_routine in key_list:
1279
+ if key in keys:
1280
+ rect = page.layout[key]
1281
+ text, css = list_to_html(txt_routine(), font_size='8pt')
1282
+ page._page.insert_htmlbox(rect, text, css=css)
1283
+ else:
1284
+ logging.warning(f"Key {key} not found in layout. Skipping text.")
1285
+
1286
+ for key, df in key_table:
1287
+ if key in keys:
1288
+ rect = page.layout[key]
1289
+ text, css = dataframe_to_html(df, font_size='8pt')
1290
+ page._page.insert_htmlbox(rect, text, css=css)
1291
+ else:
1292
+ logging.warning(f"Key {key} not found in layout. Skipping text.")
1293
+
1294
+ def _sort_groups_by_inverse_deltaz(self):
1295
+ """ Sort the groups by the maximum difference in descending order. """
1296
+ self._groups.sort(key=lambda group: max(max(item['diff_left'], item['diff_right']) for item in group), reverse=True)
1297
+
1298
+ def _create_subpages(self):
1299
+ """ Complete the report with the individual sections. """
1300
+
1301
+ for i_group, group in enumerate(self._groups):
1302
+ for i_sect, sect in enumerate(group):
1303
+ self.subpages[(i_group, i_sect)] = CSvsDEM(group,
1304
+ i_sect,
1305
+ self.dem,
1306
+ title = _('Group n° {} - Section n° {}').format(i_group+1, sect['profile'].myname),
1307
+ index_group= i_group + 1,
1308
+ index_cs = i_sect + 1,
1309
+ rebinned_dem=self._rebinned_dem)
1310
+
1311
+ def create_report(self, output_file: str | Path = None,
1312
+ append_subpages: bool = True,
1313
+ nb_max_pages:int = -1) -> None:
1314
+ """ Create a page report for the array comparison. """
1315
+
1316
+ if output_file is None:
1317
+ output_file = Path(f"compare_cs_dem_report.pdf")
1318
+
1319
+ if output_file.exists():
1320
+ logging.warning(f"Output file {output_file} already exists. It will be overwritten.")
1321
+
1322
+ mainpage = CSvsDEM_MainLayout(_("Comparison Report - Cross-Sections vs DEM"))
1323
+ mainpage.create_report()
1324
+ self._complete_report_mainpage(mainpage)
1325
+
1326
+ if append_subpages:
1327
+
1328
+ if nb_max_pages < 0:
1329
+ nb_max_pages = self.count_differences
1330
+ elif nb_max_pages > self.count_differences:
1331
+ logging.warning(f"Requested {nb_max_pages} differences, but only {self.count_differences} are available. Using all available differences.")
1332
+ elif nb_max_pages < self.count_differences:
1333
+ logging.info(f"Limiting to {nb_max_pages} differences.")
1334
+
1335
+ # features_to_treat = [feature for feature in self._groups[:nb_max_pages]]
1336
+
1337
+
1338
+ self._create_subpages()
1339
+
1340
+ with TemporaryDirectory() as temp_dir:
1341
+
1342
+ all_pdfs = []
1343
+
1344
+ nbpages = 0
1345
+ for idx, onepage in tqdm(self.subpages.items(), desc="Preparing individual difference reports", total=nb_max_pages):
1346
+ if nbpages >= nb_max_pages:
1347
+ break
1348
+
1349
+ all_pdfs.extend([onepage.create_report(Path(temp_dir) / f"temp_report_{idx[0]}_{idx[1]}.pdf")])
1350
+ nbpages += 1
1351
+
1352
+ for pdf_file in tqdm(all_pdfs, desc="Compiling PDFs"):
1353
+ mainpage._doc.insert_file(pdf_file)
1354
+
1355
+ # create a TOC
1356
+ mainpage._doc.set_toc(mainpage._doc.get_toc())
1357
+
1358
+ mainpage.save_report(output_file)
1359
+ self._pdf_path = output_file
1360
+
1361
+ @property
1362
+ def pdf_path(self) -> Path:
1363
+ """ Return the path to the generated PDF report. """
1364
+ if hasattr(self, '_pdf_path'):
1365
+ return self._pdf_path
1366
+ else:
1367
+ raise AttributeError("PDF path not set. Please create the report first.")
1368
+
1369
+ def __getitem__(self, index: int | tuple):
1370
+ """ Get the group or a specific difference.
1371
+ """
1372
+
1373
+ if isinstance(index, tuple):
1374
+ if len(index) != 2:
1375
+ raise IndexError("Tuple index must have exactly two elements: (group_index, difference_index).")
1376
+ group_index, diff_index = index
1377
+ if group_index < 0 or group_index >= self.count_groups:
1378
+ raise IndexError("Group index out of range.")
1379
+ group = self._groups[group_index]
1380
+ if diff_index < 0 or diff_index >= len(group):
1381
+ raise IndexError("Difference index out of range.")
1382
+ return group[diff_index]
1383
+ else:
1384
+ if index < 0 or index >= self.count_groups:
1385
+ raise IndexError("Group index out of range.")
1386
+ return self._groups[index]
1387
+
1388
+ class CompareCSvsDEM_wx(PDFViewer):
1389
+
1390
+ def __init__(self, cross_sections: crosssections | Path | str,
1391
+ dem: WolfArray | str | Path,
1392
+ laz_directory: Path | str = None,
1393
+ support: Path | str | vector = None,
1394
+ threshold_z: float = 0.5,
1395
+ distance_threshold: float = 50.0,
1396
+ dpi:int=150,
1397
+ nb_max_groups:int=-1,
1398
+ **kwargs):
1399
+ """ Initialize the Report Viewer for comparison """
1400
+
1401
+ super(CompareCSvsDEM_wx, self).__init__(None, **kwargs)
1402
+
1403
+ use('agg')
1404
+
1405
+ if isinstance(cross_sections, (str, Path)):
1406
+ if not Path(cross_sections).exists():
1407
+ logging.error(f"The cross-sections file {cross_sections} does not exist.")
1408
+ dlg = wx.MessageDialog(self,
1409
+ _("The cross-sections file does not exist."),
1410
+ _("Warning"),
1411
+ wx.OK | wx.ICON_WARNING)
1412
+ dlg.ShowModal()
1413
+ dlg.Destroy()
1414
+ return
1415
+
1416
+ self._report = CompareMultipleCSvsDEM(cross_sections=cross_sections,
1417
+ dem=dem,
1418
+ laz_directory=laz_directory,
1419
+ support=support,
1420
+ threshold_z=threshold_z,
1421
+ distance_threshold=distance_threshold)
1422
+
1423
+ pgbar = wx.ProgressDialog(_("Generating Report"),
1424
+ _("Please wait while the report is being generated..."),
1425
+ maximum=100,
1426
+ parent=self,
1427
+ style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME | wx.PD_REMAINING_TIME)
1428
+
1429
+ self._report._dpi = dpi
1430
+ self._report.create_report(nb_max_pages = nb_max_groups)
1431
+
1432
+ pgbar.Update(100, _("Report generation completed."))
1433
+ pgbar.Destroy()
1434
+
1435
+ # Load the PDF into the viewer
1436
+ if self._report.pdf_path is None:
1437
+ logging.error("No report created. Cannot load PDF.")
1438
+ return
1439
+
1440
+ self.load_pdf(self._report.pdf_path)
1441
+ self.viewer.SetZoom(-1) # Fit to width
1442
+
1443
+ # Set the title of the frame
1444
+ self.SetTitle("Comparison of cross-sections and DEM - Report")
1445
+
1446
+ self.Bind(wx.EVT_CLOSE, self.on_close)
1447
+
1448
+ use('wxagg')
1449
+
1450
+ def on_close(self, event):
1451
+ """ Handle the close event to clean up resources """
1452
+ self.viewer.pdfdoc.pdfdoc.close()
1453
+ self.Destroy()