Source code for wolfhece._sim_panels

"""
Simulation explorer, video-creation, drowning, animation and DEM/DTM
dialog classes extracted from PyDraw.py.

Author: HECE - University of Liege, Pierre Archambeau
Date: 2024

Copyright (c) 2024 University of Liege. All rights reserved.

This script and its content are protected by copyright law. Unauthorized
copying or distribution of this file, via any medium, is strictly prohibited.
"""
from __future__ import annotations

import logging
import numpy as np
import wx
from pathlib import Path
from enum import Enum
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
from matplotlib.axes import Axes


from typing import TYPE_CHECKING

from .PyTranslate import _
from .wolfresults_2D import Wolfresults_2D
from .Results2DGPU import wolfres2DGPU
from .wolf_array import WolfArray, header_wolf
from .drowning_victims.drowning_class import Drowning_victim_Viewer

if TYPE_CHECKING:
    from .PyDraw import WolfMapViewer

__all__ = [
    'Sim_Explorer',
    'Sim_VideoCreation',
    'Drowning_Explorer',
    'Select_Begin_end_interval_step',
    'PrecomputedDEM_DTM',
    'Precomputed_DEM_DTM_Dialog',
    'GlobalAnimationClock',
]

# ---------------------------------------------------------------------------
# The classes below were originally in PyDraw.py (lines 894-2359).
# "WolfMapViewer" is referenced only as a forward-reference string annotation,
# so no circular import occurs.
# ---------------------------------------------------------------------------

[docs] class Sim_Explorer(wx.Frame): def __init__(self, parent, title, mapviewer:"WolfMapViewer", sim:Wolfresults_2D): super(Sim_Explorer, self).__init__(parent, title=title, size=(150, 250), style = wx.DEFAULT_FRAME_STYLE & ~ (wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX))
[docs] self._panel = wx.Panel(self)
[docs] self.mapviewer = mapviewer
[docs] self.active_res2d:Wolfresults_2D = sim
main_sizer = wx.BoxSizer(wx.HORIZONTAL) left_bar = wx.BoxSizer(wx.VERTICAL) right_bar = wx.BoxSizer(wx.VERTICAL)
[docs] self._all_times_steps = self.active_res2d.get_times_steps()
# Right bar # --------- # Slider
[docs] self._slider_steps = wx.Slider(self._panel, minValue=1, maxValue=sim.get_nbresults(), style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_MIN_MAX_LABELS | wx.SL_LABELS)
self._slider_steps.Bind(wx.EVT_SLIDER, self.OnSliderSteps) right_bar.Add(self._slider_steps, 1, wx.EXPAND | wx.ALL, 2) # Explore by index
[docs] self._label_idx = wx.StaticText(self._panel, label=_('Index'))
right_bar.Add(self._label_idx, 1, wx.EXPAND | wx.ALL, 2)
[docs] self._step_idx = wx.ListBox(self._panel, choices=[str(i) for i in range(1, sim.get_nbresults()+1)], style=wx.LB_SINGLE)
self._step_idx.Bind(wx.EVT_LISTBOX, self.OnSelectIdxStep) right_bar.Add(self._step_idx, 1, wx.EXPAND | wx.ALL, 5) # Explore by time
[docs] self._label_time = wx.StaticText(self._panel, label=_('Time [s]'))
right_bar.Add(self._label_time, 1, wx.EXPAND | wx.ALL, 2) _now = datetime.now()
[docs] self._starting_date = datetime(year=_now.year, month=_now.month, day=_now.day, hour=0, minute=0, second=0)
[docs] self._texttime = wx.TextCtrl(self._panel, value=self._starting_date.strftime('%Y-%m-%d %H:%M:%S'))
right_bar.Add(self._texttime, 1, wx.EXPAND | wx.ALL, 5) self._texttime.Bind(wx.EVT_TEXT, self.OnTextTime)
[docs] self._step_time = wx.ListBox(self._panel, choices=['{:.3f} - {}'.format(i, datetime.strftime(self._starting_date + timedelta(seconds=float(i)), '%Y-%m-%d %H:%M:%S')) for i in self._all_times_steps[0]], style=wx.LB_SINGLE)
self._step_time.Bind(wx.EVT_LISTBOX, self.OnSelectNumStep) right_bar.Add(self._step_time, 1, wx.EXPAND | wx.ALL, 5) # Explore by time step
[docs] self._label_steps = wx.StaticText(self._panel, label=_('Time step [-]'))
right_bar.Add(self._label_steps, 1, wx.EXPAND | wx.ALL, 2)
[docs] self._step_num = wx.ListBox(self._panel, choices=[str(i) for i in self._all_times_steps[1]], style=wx.LB_SINGLE)
self._step_num.Bind(wx.EVT_LISTBOX, self.OnSelectCurTime) right_bar.Add(self._step_num, 1, wx.EXPAND | wx.ALL, 5) # Left bar # -------- # Apply selected step
[docs] self._cmd_apply = wx.Button(self._panel, wx.ID_APPLY, _('Apply'))
self._cmd_apply.SetToolTip(_('Apply the selected parameters to the map')) self._cmd_apply.Bind(wx.EVT_BUTTON, self.OnApply) left_bar.Add(self._cmd_apply, 1, wx.EXPAND | wx.ALL, 5) # Update listbox from files on disk
[docs] self._cmd_update = wx.Button(self._panel, wx.ID_REFRESH, _('Update'))
self._cmd_update.SetToolTip(_('Update the list of available results based on the files on disk')) self._cmd_update.Bind(wx.EVT_BUTTON, self.OnUpdate) left_bar.Add(self._cmd_update, 1, wx.EXPAND | wx.ALL, 5) #Plot
[docs] self._cmd_plot = wx.Button(self._panel, wx.ID_PREVIEW, _('Plot simulation informations'))
self._cmd_plot.SetToolTip(_('Plot synthesis of the simulation (computation time, time step, clock time, mostly dry mesh...)')) self._cmd_plot.Bind(wx.EVT_BUTTON, self.OnPlot) left_bar.Add(self._cmd_plot, 1, wx.EXPAND | wx.ALL, 5) # Next step
[docs] self._cmd_next = wx.Button(self._panel, wx.ID_FORWARD, _('Next'))
self._cmd_next.SetToolTip(_('Go to the next step -- using the selected mode')) self._cmd_next.Bind(wx.EVT_BUTTON, self.OnNext) left_bar.Add(self._cmd_next, 1, wx.EXPAND | wx.ALL, 5) # Previous step
[docs] self._cmd_prev = wx.Button(self._panel, wx.ID_BACKWARD, _('Previous'))
self._cmd_prev.SetToolTip(_('Go to the previous step -- using the selected mode')) self._cmd_prev.Bind(wx.EVT_BUTTON, self.OnPrev) left_bar.Add(self._cmd_prev, 1, wx.EXPAND | wx.ALL, 5) # Check Mode movement
[docs] self._mode = wx.ListBox(self._panel, choices=['by time [s]', 'by time [hour]', 'by index', 'by time step'], style=wx.LB_SINGLE)
self._mode.SetToolTip(_('Select the mode to move through the simulation')) self._mode.SetSelection(2)
[docs] self._interval = wx.TextCtrl(self._panel, value='1', style=wx.ALIGN_CENTER_HORIZONTAL)
self._interval.SetToolTip(_('Interval for the mode selected -- unit depends on the mode')) self._interval.Bind(wx.EVT_TEXT, self.OnInterval) left_bar.Add(self._mode, 1, wx.EXPAND | wx.ALL, 5) left_bar.Add(self._interval, 0, wx.EXPAND | wx.ALL, 5) self.Bind(wx.EVT_CLOSE, self.OnClose) main_sizer.Add(left_bar, 1, wx.EXPAND | wx.ALL, 2) main_sizer.Add(right_bar, 1, wx.EXPAND | wx.ALL, 2) self._panel.SetSizer(main_sizer) self._panel.SetAutoLayout(True)
[docs] self.MinSize = (450, 500)
self.Fit() self.Show() self.SetIcon(wx.Icon(str(Path(__file__).parent / "apps/wolf.ico"))) self._set_all(0)
[docs] def OnPlot(self, event): """ Create a scatter plot of all steps. Major x_axis is time in seconds, Minor X-axis is time by date. Plots: - Computation time step (Dt) - Computation steps (N) - Clock time (s) - Mostly dry mesh (N) """ main_x = self._all_times_steps[0] second_x = [self._starting_date + timedelta(seconds=i) for i in main_x] if isinstance(self.active_res2d, wolfres2DGPU): ax:list[Axes] fig, ax= plt.subplots(5, 1, figsize=(10, 8)) ax[0].plot(main_x, self._all_times_steps[1], 'o-') ax[0].set_ylabel(_('Computation\ntime step (N)'), fontsize=8) ax[0].ticklabel_format(axis='y', style='sci', scilimits=(0,0)) ax[0].grid(which='both') ax[0].set_xticks(main_x) ax[0].set_xticklabels([]) secax:Axes = ax[0].secondary_xaxis('top') secax.set_xlabel(_('Real date\n[Y-M-D H:M:S]'), fontsize=8) secax.set_xticks(main_x) secax.set_xticklabels([datetime.strftime(i, '%Y-%m-%d %H:%M') for i in second_x], fontsize=8) secax.tick_params(axis='x', rotation=30) ax[1].plot(main_x, self.active_res2d.all_dt, 'o-') ax[1].set_ylabel(_(r'$\Delta t$ [s]'), fontsize=8) ax[1].grid(which='both') ax[1].set_xticks(main_x) ax[1].set_xticklabels([]) ax[1].ticklabel_format(axis='y', style='sci', scilimits=(0,0)) ctime = self.active_res2d.all_clock_time ax[2].plot(main_x, self.active_res2d.all_clock_time, 'o-') ax[2].set_ylabel(_('Clock time [s]'), fontsize=8) ax[2].grid(which='both') ax[2].set_xticks([]) ax[2].set_xticks(main_x) ax[2].set_xticklabels([]) ax[2].ticklabel_format(axis='y', style='sci', scilimits=(0,0)) # Fit a line on the (main_x - clock time) plot # This will give a mean acceleration factor # The inverse of the slope of the line is the accelaration factor # The line is y = slope * x + intercept from scipy.stats import linregress slope, intercept, r_value, p_value, std_err = linregress(main_x, ctime) # Plot the info on the ax[2] msg = _('Acceleration factor:') ax[2].text(0.5, 0.5, f'{msg} {1/slope:.2f}', transform=ax[2].transAxes, fontsize=12, verticalalignment='top') ax[3].plot(main_x, self.active_res2d.all_wet_meshes, 'o-', color='blue') ax[3].plot(main_x, self.active_res2d.all_mostly_dry_mesh, 'o-', color='green') ax[3].set_ylabel(_('Wet and Mostly dry\nmeshes [N]'), fontsize=8) ax[3].grid(which='both') ax[3].set_xticks(main_x) ax[3].set_xlabel([]) ax[3].ticklabel_format(axis='y', style='sci', scilimits=(0,0)) ax[4].plot(main_x, [i/j*100 if j>0 else 0 for i, j in zip(self.active_res2d.all_mostly_dry_mesh, self.active_res2d.all_wet_meshes)], 'o-', color='red') ax[4].set_ylabel(_('Wet/Mostly dry\nmeshes [%]'), fontsize=8) ax[4].grid(which='both') ax[4].set_xticks(main_x) ax[4].set_xlabel(_('Simulated time [s]'), fontsize=8) ax[4].ticklabel_format(axis='y', style='sci', scilimits=(0,0)) fig.suptitle('Simulation {}'.format(self.active_res2d.idx), fontsize=10) fig.tight_layout() fig.show()
[docs] def OnInterval(self, event): """ Change the interval """ try: interv = float(self._interval.GetValue()) if interv <= 0: interv = 1. self._interval.SetValue('1') except: interv = 1 self._interval.SetValue('1')
[docs] def _find_next(self, idx:int): """ Find the next step based on the mode and interval """ mode = int(self._mode.GetSelection()) if mode == 0: # By time [s] next_time = self._all_times_steps[0][idx] + float(self._interval.GetValue()) diff = [abs(next_time - i) for i in self._all_times_steps[0][idx:]] next_idx = diff.index(min(diff)) + idx return next_idx elif mode == 1: # By time [hour] next_time = self._all_times_steps[0][idx] + float(self._interval.GetValue())*3600 diff = [abs(next_time - i) for i in self._all_times_steps[0][idx:]] next_idx = diff.index(min(diff)) + idx return next_idx elif mode == 2: # By index next_idx = min(idx + int(self._interval.GetValue()), len(self._all_times_steps[0])-1) return next_idx elif mode == 3: # By time step next_idx = self._all_times_steps[1].index(self._all_times_steps[1][idx] + int(self._interval.GetValue())) diff = [abs(next_idx - i) for i in self._all_times_steps[1][idx:]] next_idx = diff.index(min(diff)) + idx return next_idx
[docs] def _find_prev(self, idx:int): """ Find the previous step based on the mode and interval """ mode = int(self._mode.GetSelection()) if mode == 0: # By time [s] prev_time = self._all_times_steps[0][idx] - float(self._interval.GetValue()) diff = [abs(prev_time - i) for i in self._all_times_steps[0][:idx]] prev_idx = diff.index(min(diff)) return prev_idx elif mode == 1: # By time [hour] prev_time = self._all_times_steps[0][idx] - float(self._interval.GetValue())*3600 diff = [abs(prev_time - i) for i in self._all_times_steps[0][:idx]] prev_idx = diff.index(min(diff)) return prev_idx elif mode == 2: # By index prev_idx = max(idx - int(self._interval.GetValue()), 0) return prev_idx elif mode == 3: # By time step prev_idx = self._all_times_steps[1].index(self._all_times_steps[1][idx] - int(self._interval.GetValue())) diff = [abs(prev_idx - i) for i in self._all_times_steps[1][:idx]] prev_idx = diff.index(min(diff)) return prev_idx
[docs] def OnNext(self, event): """ Go to the next step """ selected_step = self._slider_steps.GetValue()-1 next_idx = self._find_next(selected_step) if next_idx != selected_step: self._set_all(next_idx) self.Refresh(next_idx)
[docs] def OnPrev(self, event): """ Go to the previous step """ selected_step = self._slider_steps.GetValue()-1 prev_idx = self._find_prev(selected_step) if prev_idx != selected_step: self._set_all(prev_idx) self.Refresh(prev_idx)
[docs] def OnTextTime(self, event): try: self._starting_date = datetime.strptime(self._texttime.GetValue(), '%Y-%m-%d %H:%M:%S') self._step_time.Set(['{:.3f} - {}'.format(i, datetime.strftime(self._starting_date + timedelta(seconds=i), '%Y-%m-%d %H:%M:%S')) for i in self._all_times_steps[0]]) except: logging.info('Error while parsing the date') pass
[docs] def OnClose(self, event): """ Close the simulation explorer """ self.mapviewer._pop_sim_explorer(self.active_res2d) self.Destroy()
[docs] def OnUpdate(self, event): self._update()
[docs] def OnApply(self, event): selected_step = self._slider_steps.GetValue()-1 self._cmd_apply.SetBackgroundColour(wx.Colour(255, 0, 0)) # Set button color to red self._cmd_apply.Refresh() # Refresh the button to apply the color change self.Refresh(selected_step) self._cmd_apply.SetBackgroundColour(wx.NullColour) # Reset button color to default self._cmd_apply.Refresh() # Refresh the button to apply the color change
[docs] def _set_all(self, idx:int): # test if idx is in range if idx < 0 : logging.error(_('Index out of range')) return if idx >= len(self._all_times_steps[0]): self._update() if idx >= len(self._all_times_steps[0]): logging.error(_('Index out of range')) return try: self._slider_steps.SetValue(idx+1) self._step_idx.SetSelection(idx) self._step_time.SetSelection(idx) self._step_num.SetSelection(idx) except: logging.error(_('Error while setting the step selection'))
[docs] def Refresh(self, idx:int): self.active_res2d.read_oneresult(idx) self.active_res2d.set_currentview() self.mapviewer.Refresh()
[docs] def OnSliderSteps(self, event): selected_step = self._slider_steps.GetValue() self._set_all(selected_step-1)
[docs] def OnSelectCurTime(self, event): selected_time = self._step_num.GetSelection() self._set_all(selected_time)
[docs] def OnSelectNumStep(self, event): selected_step = self._step_time.GetSelection() self._set_all(selected_step)
[docs] def OnSelectIdxStep(self, event): selected_step = self._step_idx.GetSelection() self._set_all(selected_step)
[docs] def _update(self): nb = self.active_res2d.get_nbresults() self._all_times_steps = self.active_res2d.get_times_steps() self._slider_steps.SetMax(nb) self._step_idx.Set([str(i) for i in range(1,nb+1)]) self._step_time.Set(['{:.3f} - {}'.format(i, datetime.strftime(self._starting_date + timedelta(seconds=float(i)), '%Y-%m-%d %H:%M:%S')) for i in self._all_times_steps[0]]) self._step_num.Set([str(i) for i in self._all_times_steps[1]])
[docs] class Sim_VideoCreation(wx.Dialog): def __init__(self, parent, title, mapviewer:"WolfMapViewer", sim:Wolfresults_2D): super(Sim_VideoCreation, self).__init__(parent, title=title, size=(350, 250), style = wx.DEFAULT_DIALOG_STYLE & ~ (wx.RESIZE_BORDER | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX | wx.CLOSE_BOX))
[docs] self.mapviewer = mapviewer
[docs] self.active_res2d:Wolfresults_2D = sim
[docs] self._framerate = 25
[docs] self._start_step = 1
[docs] self._end_step = self.active_res2d.get_nbresults()
[docs] self._interval = 1
[docs] self._fn = str(Path(self.active_res2d.filename).parent / f'{Path(self.active_res2d.filename).stem}.avi')
[docs] self._tz_ref = timedelta(hours=0)
[docs] self._tz_plot = timedelta(hours=0)
[docs] self._date_ref = datetime(year=2020, month=1, day=1, hour=0, minute=0, second=0)
[docs] self._fontsize = 16
[docs] self._fontcolor = (255, 255, 255, 255)
[docs] self._timeposition = 'top-left' # 'top-right', 'bottom-left', 'bottom-right', 'top-center', 'bottom-center'
panel = wx.Panel(self) vbox = wx.BoxSizer(wx.VERTICAL) hbox1 = wx.BoxSizer(wx.HORIZONTAL) st1 = wx.StaticText(panel, -1, _('File name')) hbox1.Add(st1, 1, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL, 5) tc1 = wx.TextCtrl(panel, -1, self._fn) hbox1.Add(tc1,1,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5) vbox.Add(hbox1,0,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.tc1 = tc1
# Add a button to choose the file name btn_browse = wx.Button(panel, -1, _('Browse')) hbox1.Add(btn_browse, 0, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.btn_browse = btn_browse
self.btn_browse.Bind(wx.EVT_BUTTON, self.OnBrowse) hbox2 = wx.BoxSizer(wx.HORIZONTAL) st2 = wx.StaticText(panel, -1, _('Frame rate [nb_images/second]'), style=wx.ALIGN_CENTER) hbox2.Add(st2, 1, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL, 5) tc2 = wx.TextCtrl(panel, -1, str(self._framerate)) hbox2.Add(tc2,1,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5) vbox.Add(hbox2,0,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.tc2 = tc2
hbox3 = wx.BoxSizer(wx.HORIZONTAL) st3 = wx.StaticText(panel, -1, _('First step'), style=wx.ALIGN_CENTER) hbox3.Add(st3, 1, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL, 5) tc3 = wx.TextCtrl(panel, -1, str(self._start_step)) hbox3.Add(tc3,1,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5) vbox.Add(hbox3,0,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.tc3 = tc3
hbox4 = wx.BoxSizer(wx.HORIZONTAL) st4 = wx.StaticText(panel, -1, _('Final step'), style=wx.ALIGN_CENTER) hbox4.Add(st4, 1, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL, 5) tc4 = wx.TextCtrl(panel, -1, str(self._end_step)) hbox4.Add(tc4,1,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5) vbox.Add(hbox4,0,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.tc4 = tc4
hbox5 = wx.BoxSizer(wx.HORIZONTAL) st5 = wx.StaticText(panel, -1, _('Interval'), style=wx.ALIGN_CENTER) hbox5.Add(st5, 1, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL, 5) tc5 = wx.TextCtrl(panel, -1, str(self._interval)) hbox5.Add(tc5,1,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5) vbox.Add(hbox5,0,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.tc5 = tc5
hbox7 = wx.BoxSizer(wx.HORIZONTAL) st7 = wx.StaticText(panel, -1, _("Font size (for time stamp)"), style=wx.ALIGN_CENTER) hbox7.Add(st7, 1, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL, 5) tc7 = wx.TextCtrl(panel, -1, str(self._fontsize)) hbox7.Add(tc7,1,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5) vbox.Add(hbox7,0,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.tc7 = tc7
hbox8 = wx.BoxSizer(wx.HORIZONTAL) st8 = wx.StaticText(panel, -1, _("Font color (R,G,B,A)"), style=wx.ALIGN_CENTER) hbox8.Add(st8, 1, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL, 5) tc8 = wx.ColourPickerCtrl(panel, -1, wx.Colour(*self._fontcolor)) hbox8.Add(tc8,1,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5) vbox.Add(hbox8,0,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.tc8 = tc8
hbox9 = wx.BoxSizer(wx.HORIZONTAL) st9 = wx.StaticText(panel, -1, _("Time position"), style=wx.ALIGN_CENTER) hbox9.Add(st9, 1, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL, 5) choices = ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top-center', 'bottom-center'] tc9 = wx.Choice(panel, -1, choices=choices) hbox9.Add(tc9, 1, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL, 5) vbox.Add(hbox9, 0, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL, 5)
[docs] self.tc9 = tc9
hbox6 = wx.BoxSizer(wx.HORIZONTAL) btn1 = wx.Button(panel, -1, _('Ok')) hbox6.Add(btn1,1,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.btn1 = btn1
btn2 = wx.Button(panel, -1, _('Cancel')) hbox6.Add(btn2,1,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.btn2 = btn2
vbox.Add(hbox6,0,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5) # add a calendar to pick date and a hour and choose a timezone check_date = wx.CheckBox(panel, -1, _("Set reference date")) vbox.Add(check_date, 0, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.check_date = check_date
calendar = wx.adv.CalendarCtrl(panel, -1, wx.DateTime.Now()) vbox.Add(calendar, 0, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.calendar = calendar
timezone_choices = ['UTC', 'GMT+1', 'GMT+2'] hbox10 = wx.BoxSizer(wx.HORIZONTAL) st10 = wx.StaticText(panel, -1, _("Reference Timezone"), style=wx.ALIGN_CENTER) hbox10.Add(st10, 1, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL, 5) timezone_ref = wx.Choice(panel, -1, choices=timezone_choices) timezone_ref.SetSelection(0) hbox10.Add(timezone_ref, 0, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5) vbox.Add(hbox10, 0, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5) hbox11 = wx.BoxSizer(wx.HORIZONTAL) st11 = wx.StaticText(panel, -1, _("Plot Timezone"), style=wx.ALIGN_CENTER) hbox11.Add(st11, 1, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL, 5) timezone_plot = wx.Choice(panel, -1, choices=timezone_choices) timezone_plot.SetSelection(1) hbox11.Add(timezone_plot, 0, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5) vbox.Add(hbox11, 0, wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
[docs] self.timezone_ref = timezone_ref
[docs] self.timezone_plot = timezone_plot
panel.SetSizer(vbox) vbox.Fit(self) self.btn1.Bind(wx.EVT_BUTTON, self.OnOk) self.btn2.Bind(wx.EVT_BUTTON, self.OnCancel) # Add validation to the text controls self.tc2.Bind(wx.EVT_TEXT, self.OnValidate) self.tc3.Bind(wx.EVT_TEXT, self.OnValidate) self.tc4.Bind(wx.EVT_TEXT, self.OnValidate) self.tc5.Bind(wx.EVT_TEXT, self.OnValidate) self.tc7.Bind(wx.EVT_TEXT, self.OnValidate) self.timezone_plot.Bind(wx.EVT_CHOICE, self.OnValidate) self.timezone_ref.Bind(wx.EVT_CHOICE, self.OnValidate) self.calendar.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.OnValidate) self.check_date.Bind(wx.EVT_CHECKBOX, self.OnValidate) self.tc8.Bind(wx.EVT_COLOURPICKER_CHANGED, self.OnValidate) self.tc9.SetSelection(0) self.tc9.Bind(wx.EVT_CHOICE, self.OnValidate) icon = wx.Icon() icon_path = Path(__file__).parent / "apps/wolf.ico" icon.CopyFromBitmap(wx.Bitmap(str(icon_path), wx.BITMAP_TYPE_ANY)) self.SetIcon(icon) self.CenterOnScreen() self.Show()
[docs] def OnBrowse(self, event): """ Browse a file name to save the video """ with wx.FileDialog(self, _("Save video file"), wildcard="MP4 files (*.mp4)|*.mp4|AVI files (*.avi)|*.avi|All files (*.*)|*.*", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as fileDialog: if fileDialog.ShowModal() == wx.ID_CANCEL: return # the user changed idea... self._fn = fileDialog.GetPath() self.tc1.SetValue(self._fn)
[docs] def get_values(self): """ Return the values set in the dialog """ return self._fn, self._framerate, \ self._start_step, self._end_step, \ self._interval, self._fontsize, \ self._fontcolor, self._timeposition, \ self._date_ref, self._tz_ref, self._tz_plot, \ self._check_date
[docs] def OnValidate(self, event): """ Validate the text controls to be sure that the values are correct """ try: framerate = int(self.tc2.GetValue()) if framerate <= 0: framerate = 25 self.tc2.SetValue(str(framerate)) self._framerate = framerate except: self.tc2.SetValue(str(self._framerate)) try: start_step = int(self.tc3.GetValue()) if start_step < 1: start_step = 1 self.tc3.SetValue(str(start_step)) if start_step > self.active_res2d.get_nbresults(): start_step = self.active_res2d.get_nbresults() self.tc3.SetValue(str(start_step)) self._start_step = start_step except: self.tc3.SetValue(str(self._start_step)) try: end_step = int(self.tc4.GetValue()) if end_step < 1: end_step = 1 self.tc4.SetValue(str(end_step)) if end_step > self.active_res2d.get_nbresults(): end_step = self.active_res2d.get_nbresults() self.tc4.SetValue(str(end_step)) self._end_step = end_step except: self.tc4.SetValue(str(self._end_step)) try: interval = int(self.tc5.GetValue()) if interval < 1: interval = 1 self.tc5.SetValue(str(interval)) self._interval = interval except: self.tc5.SetValue(str(self._interval)) try: fontsize = int(self.tc7.GetValue()) if fontsize < 1: fontsize = 16 self.tc7.SetValue(str(fontsize)) self._fontsize = fontsize except: self.tc7.SetValue(str(self._fontsize)) try: color = self.tc8.GetColour() self._fontcolor = (color.Red(), color.Green(), color.Blue(), color.Alpha()) except: self.tc8.SetColour(wx.Colour(*self._fontcolor)) try: pos_idx = self.tc9.GetSelection() choices = ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top-center', 'bottom-center'] if pos_idx < 0 or pos_idx >= len(choices): pos_idx = 0 self.tc9.SetSelection(pos_idx) self._timeposition = choices[pos_idx] except: self.tc9.SetSelection(0) try: date = self.calendar.GetDate() wx_datetime = wx.DateTime(date.GetDay(), date.GetMonth(), date.GetYear()) self._date_ref = datetime(year=wx_datetime.GetYear(), month=wx_datetime.GetMonth()+1, day=wx_datetime.GetDay()) except: self._date_ref = datetime.now() try: tz_ref_idx = self.timezone_ref.GetSelection() tz_plot_idx = self.timezone_plot.GetSelection() tz_choices = ['UTC', 'GMT+1', 'GMT+2'] if tz_ref_idx < 0 or tz_ref_idx >= len(tz_choices): tz_ref_idx = 0 self._tz_ref = timedelta(hours=int(tz_choices[tz_ref_idx].replace('UTC', '0').replace('GMT+', ''))) self._tz_plot = timedelta(hours=int(tz_choices[tz_plot_idx].replace('UTC', '0').replace('GMT+', ''))) except: self._tz_ref = timedelta(hours=0) self._tz_plot = timedelta(hours=0) self._check_date = self.check_date.GetValue()
[docs] def OnOk(self, event): """ Create the video file """ self.OnValidate(None) self.EndModal(wx.ID_OK)
[docs] def OnCancel(self, event): """ Cancel the video creation """ self.EndModal(wx.ID_CANCEL)
[docs] class Drowning_Explorer(wx.Frame): def __init__(self, parent, title, mapviewer:any, sim:Drowning_victim_Viewer): super().__init__(parent, title=title, size=(150, 250), style = wx.DEFAULT_FRAME_STYLE & ~ (wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX))
[docs] self._panel = wx.Panel(self)
[docs] self.mapviewer = mapviewer
[docs] self.active_drowning:Drowning_victim_Viewer = sim
main_sizer = wx.BoxSizer(wx.HORIZONTAL) left_bar = wx.BoxSizer(wx.VERTICAL) right_bar = wx.BoxSizer(wx.VERTICAL)
[docs] self._all_times_steps = self.active_drowning.wanted_time
# Right bar # --------- # Slider
[docs] self._slider_steps = wx.Slider(self._panel, minValue=1, maxValue=len(self.active_drowning.wanted_time)-1, style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_MIN_MAX_LABELS | wx.SL_LABELS)
self._slider_steps.Bind(wx.EVT_SLIDER, self.OnSliderSteps) right_bar.Add(self._slider_steps, 1, wx.EXPAND | wx.ALL, 2)
[docs] self._time_drowning = wx.TextCtrl(self._panel, value=f"Drowning at 0 days, 0 hours,\n0 minutes and 0 seconds", style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_CENTER)
right_bar.Add(self._time_drowning, 0, wx.EXPAND | wx.ALL, 2) # Explore by time
[docs] self._label_time = wx.StaticText(self._panel, label=_('Time [s]'))
right_bar.Add(self._label_time, 1, wx.EXPAND | wx.ALL, 2) _now = datetime.now()
[docs] self._starting_date = datetime(year=_now.year, month=_now.month, day=_now.day, hour=0, minute=0, second=0)
[docs] self._texttime = wx.TextCtrl(self._panel, value=self._starting_date.strftime('%Y-%m-%d %H:%M:%S'))
right_bar.Add(self._texttime, 1, wx.EXPAND | wx.ALL, 5) self._texttime.Bind(wx.EVT_TEXT, self.OnTextTime)
[docs] self._step_time = wx.ListBox(self._panel, choices=['{:.3f} - {}'.format(i, datetime.strftime(self._starting_date + timedelta(seconds=i), '%Y-%m-%d %H:%M:%S')) for i in self._all_times_steps[:-1]], style=wx.LB_SINGLE)
self._step_time.Bind(wx.EVT_LISTBOX, self.OnSelectNumStep) right_bar.Add(self._step_time, 1, wx.EXPAND | wx.ALL, 5)
[docs] self._step_idx = wx.ListBox(self._panel, choices=[str(i) for i in range(1, len(self.active_drowning.wanted_time)-1+1)], style=wx.LB_SINGLE)
self._step_idx.Bind(wx.EVT_LISTBOX, self.OnSelectIdxStep) right_bar.Add(self._step_idx, 1, wx.EXPAND | wx.ALL, 5) # Left bar # -------- # Apply selected step
[docs] self._cmd_apply = wx.Button(self._panel, wx.ID_APPLY, _('Apply'))
self._cmd_apply.SetToolTip(_('Apply the selected parameters to the map')) self._cmd_apply.Bind(wx.EVT_BUTTON, self.OnApply) left_bar.Add(self._cmd_apply, 1, wx.EXPAND | wx.ALL, 5) # Next step
[docs] self._cmd_next = wx.Button(self._panel, wx.ID_FORWARD, _('Next'))
self._cmd_next.SetToolTip(_('Go to the next step -- using the selected mode')) self._cmd_next.Bind(wx.EVT_BUTTON, self.OnNext) left_bar.Add(self._cmd_next, 1, wx.EXPAND | wx.ALL, 5) # Previous step
[docs] self._cmd_prev = wx.Button(self._panel, wx.ID_BACKWARD, _('Previous'))
self._cmd_prev.SetToolTip(_('Go to the previous step -- using the selected mode')) self._cmd_prev.Bind(wx.EVT_BUTTON, self.OnPrev) left_bar.Add(self._cmd_prev, 1, wx.EXPAND | wx.ALL, 5) self.Bind(wx.EVT_CLOSE, self.OnClose) main_sizer.Add(left_bar, 1, wx.EXPAND | wx.ALL, 2) main_sizer.Add(right_bar, 1, wx.EXPAND | wx.ALL, 2) self._panel.SetSizer(main_sizer) self._panel.SetAutoLayout(True)
[docs] self.MinSize = (450, 500)
self.Fit() self.Show() self.SetIcon(wx.Icon(str(Path(__file__).parent / "apps/wolf.ico"))) self._set_all(0)
[docs] def _find_next(self, idx:int): """ Find the next step based on the mode and interval """ mode = 2 if mode == 0: # By time [s] next_time = self._all_times_steps[idx] + float(self._interval.GetValue()) diff = [abs(next_time - i) for i in self._all_times_steps[idx:]] next_idx = diff.index(min(diff)) + idx return next_idx elif mode == 1: # By time [hour] next_time = self._all_times_steps[idx] + float(self._interval.GetValue())*3600 diff = [abs(next_time - i) for i in self._all_times_steps[idx:]] next_idx = diff.index(min(diff)) + idx return next_idx elif mode == 2: # By index next_idx = min(idx + int(1), len(self._all_times_steps)-1) return next_idx elif mode == 3: # By time step next_idx = self._all_times_steps[1].index(self._all_times_steps[idx] + int(1)) diff = [abs(next_idx - i) for i in self._all_times_steps[idx:]] next_idx = diff.index(min(diff)) + idx return next_idx
[docs] def _find_prev(self, idx:int): """ Find the previous step based on the mode and interval """ mode = 2 if mode == 0: # By time [s] prev_time = self._all_times_steps[idx] - float(1) diff = [abs(prev_time - i) for i in self._all_times_steps[:idx]] prev_idx = diff.index(min(diff)) return prev_idx elif mode == 1: # By time [hour] prev_time = self._all_times_steps[idx] - float(1)*3600 diff = [abs(prev_time - i) for i in self._all_times_steps[:idx]] prev_idx = diff.index(min(diff)) return prev_idx elif mode == 2: # By index prev_idx = max(idx - int(1), 0) return prev_idx elif mode == 3: # By time step prev_idx = self._all_times_steps[1].index(self._all_times_steps[idx] - int(1)) diff = [abs(prev_idx - i) for i in self._all_times_steps[:idx]] prev_idx = diff.index(min(diff)) return prev_idx
[docs] def OnNext(self, event): """ Go to the next step """ selected_step = self._slider_steps.GetValue()+1 next_idx = self._find_next(selected_step) if next_idx != selected_step: self._set_all(next_idx) self.Refresh(next_idx)
[docs] def OnTextTime(self, event): try: self._starting_date = datetime.strptime(self._texttime.GetValue(), '%Y-%m-%d %H:%M:%S') self._step_time.Set(['{:.3f} - {}'.format(int(i/3600/24), datetime.strftime(self._starting_date + timedelta(seconds=i), '%Y-%m-%d %H:%M:%S')) for i in self._all_times_steps[:-1]]) except: pass
[docs] def OnPrev(self, event): """ Go to the previous step """ selected_step = self._slider_steps.GetValue()-1 prev_idx = self._find_prev(selected_step) if prev_idx != selected_step: self._set_all(prev_idx) self.Refresh(prev_idx)
[docs] def OnClose(self, event): """ Close the simulation explorer """ self.mapviewer._pop_sim_explorer(self.active_drowning) self.Destroy()
[docs] def OnUpdate(self, event): self._update()
[docs] def OnApply(self, event): selected_step = self._slider_steps.GetValue()-1 self._cmd_apply.SetBackgroundColour(wx.Colour(255, 0, 0)) # Set button color to red self._cmd_apply.Refresh() # Refresh the button to apply the color change self.Refresh(selected_step) self._cmd_apply.SetBackgroundColour(wx.NullColour) # Reset button color to default self._cmd_apply.Refresh() # Refresh the button to apply the color change
[docs] def _set_all(self, idx:int): self._slider_steps.SetValue(idx+1) self._step_idx.SetSelection(idx)
[docs] def Refresh(self, idx:int): self.active_drowning.read_oneresult(idx) self.mapviewer.Refresh()
[docs] def OnSliderSteps(self, event): selected_step = self._slider_steps.GetValue()-1 self.active_drowning.time_id = selected_step time_id = self._slider_steps.GetValue()-1 time_value = self.active_drowning.wanted_time[time_id] days = np.floor(time_value // 86400) hours = np.floor((time_value % 86400) / 3600) minutes = np.floor(((time_value % 86400) % 3600) / 60) seconds = np.floor(((time_value % 86400) % 3600) % 60) self._time_drowning.SetValue( f"Drowning at {int(days)} days, {int(hours)} hours,\n" f"{int(minutes)} minutes and {int(seconds)} seconds" ) self._set_all(selected_step)
[docs] def OnSelectNumStep(self, event): selected_step = self._step_time.GetSelection() self.active_drowning.time_id = selected_step time_id = selected_step time_value = self.active_drowning.wanted_time[time_id] days = np.floor(time_value // 86400) hours = np.floor((time_value % 86400) / 3600) minutes = np.floor(((time_value % 86400) % 3600) / 60) seconds = np.floor(((time_value % 86400) % 3600) % 60) self._time_drowning.SetValue( f"Drowning at {int(days)} days, {int(hours)} hours,\n" f"{int(minutes)} minutes and {int(seconds)} seconds" ) self._set_all(selected_step)
[docs] def OnSelectIdxStep(self, event): selected_step = self._step_idx.GetSelection() self.active_drowning.time_id = selected_step time_id = selected_step time_value = self.active_drowning.wanted_time[time_id] days = np.floor(time_value // 86400) hours = np.floor((time_value % 86400) / 3600) minutes = np.floor(((time_value % 86400) % 3600) / 60) seconds = np.floor(((time_value % 86400) % 3600) % 60) self._time_drowning.SetValue( f"Drowning at {int(days)} days, {int(hours)} hours,\n" f"{int(minutes)} minutes and {int(seconds)} seconds" ) self._set_all(selected_step)
[docs] def _update(self): nb = len(self.active_drowning.wanted_time) self._all_times_steps = self.active_drowning.wanted_time self._slider_steps.SetMax(nb) self._step_idx.Set([str(i) for i in range(1,nb+1)])
[docs] class Select_Begin_end_interval_step(wx.Dialog): """ wx.frame to select the begin and end of the interval to extract """ def __init__(self, parent, title, sim:Wolfresults_2D, checkbox:bool = False): super(Select_Begin_end_interval_step, self).__init__(parent, title=title, size=(500, 350), style = wx.DEFAULT_FRAME_STYLE & ~ (wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX)) # ajout d'un slider pour choisir le début et la fin de l'intervalle -> selrange # ajout d'un slider pour choisir le pas de l'intervalle # + les mêmes informations mais sous forme de TextCtrl # ajout d'un bouton pour valider # ajout d'un bouton pour annuler
[docs] self._panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
[docs] self.begin = 1
[docs] self.end = sim.get_nbresults(True)
[docs] self.step = 1
[docs] self.check_all = True
[docs] self.check_violin = False
[docs] self._slider_begin = wx.Slider(self._panel, minValue=self.begin, maxValue=self.end, style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_MIN_MAX_LABELS | wx.SL_LABELS)
self._slider_begin.SetToolTip(_('Select the first result to export')) self._slider_begin.SetValue(self.begin) self._slider_begin.Bind(wx.EVT_SLIDER, self.OnSliderBegin) sizer.Add(self._slider_begin, 1, wx.EXPAND | wx.ALL, 2)
[docs] self._slider_end = wx.Slider(self._panel, minValue=self.begin, maxValue=self.end, style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_MIN_MAX_LABELS | wx.SL_LABELS)
self._slider_end.SetToolTip(_('Select the last result to export - If step is > 1, this value will be forced if not already captured')) self._slider_end.SetValue(self.end) self._slider_end.Bind(wx.EVT_SLIDER, self.OnSliderEnd) sizer.Add(self._slider_end, 1, wx.EXPAND | wx.ALL, 2) sizer_txt1 = wx.BoxSizer(wx.HORIZONTAL)
[docs] self._label_range = wx.StaticText(self._panel, label=_('Range'))
[docs] self._text_range = wx.TextCtrl(self._panel, value='1 - {}'.format(sim.get_nbresults(True)))
sizer_txt1.Add(self._label_range, 0, wx.EXPAND | wx.ALL, 2) sizer_txt1.Add(self._text_range, 1, wx.EXPAND | wx.ALL, 2) sizer.Add(sizer_txt1, 0, wx.EXPAND | wx.ALL, 2)
[docs] self._slider_step = wx.Slider(self._panel, minValue=1, maxValue=sim.get_nbresults(True), style=wx.SL_HORIZONTAL | wx.SL_AUTOTICKS | wx.SL_MIN_MAX_LABELS | wx.SL_LABELS)
self._slider_step.SetToolTip(_('Export one result every N steps')) self._slider_step.Bind(wx.EVT_SLIDER, self.OnSliderStep) sizer.Add(self._slider_step, 1, wx.EXPAND | wx.ALL, 2) sizer_txt2 = wx.BoxSizer(wx.HORIZONTAL)
[docs] self._label_step = wx.StaticText(self._panel, label=_('Step'))
[docs] self._text_step = wx.TextCtrl(self._panel, value='1')
sizer_txt2.Add(self._label_step, 0, wx.EXPAND | wx.ALL, 2) sizer_txt2.Add(self._text_step, 0, wx.EXPAND | wx.ALL, 2) sizer.Add(sizer_txt2, 0, wx.EXPAND | wx.ALL, 2) sizer_but = wx.BoxSizer(wx.HORIZONTAL)
[docs] self._cmd_apply = wx.Button(self._panel, wx.ID_APPLY, _('Apply'))
self._cmd_apply.Bind(wx.EVT_BUTTON, self.OnApply)
[docs] self._cmd_ok = wx.Button(self._panel, wx.ID_OK, _('OK'))
self._cmd_ok.Bind(wx.EVT_BUTTON, self.OnOK)
[docs] self._cmd_cancel = wx.Button(self._panel, wx.ID_CANCEL, _('Cancel'))
self._cmd_cancel.Bind(wx.EVT_BUTTON, self.OnCancel) sizer_but.Add(self._cmd_apply, 1, wx.EXPAND | wx.ALL, 2) sizer_but.Add(self._cmd_ok, 1, wx.EXPAND | wx.ALL, 2) sizer_but.Add(self._cmd_cancel, 1, wx.EXPAND | wx.ALL, 2) sizer.Add(sizer_but, 1, wx.EXPAND | wx.ALL, 2) if checkbox: sizer_check = wx.BoxSizer(wx.HORIZONTAL) self._check_all = wx.CheckBox(self._panel, label=_('Statistics and values'), style=wx.CHK_2STATE) self._check_all.SetToolTip(_('If checked, export statistics and all values for each step')) self._check_all.SetValue(True) self._check_all.Bind(wx.EVT_CHECKBOX, self.OnCheckAll) sizer_check.Add(self._check_all, 1, wx.EXPAND | wx.ALL, 2) sizer.Add(sizer_check, 1, wx.EXPAND | wx.ALL, 2) self._check_violin= wx.CheckBox(self._panel, label=_('Violin plot (experimental)'), style=wx.CHK_2STATE) self._check_violin.SetToolTip(_('If checked, create a violin plot for each step')) self._check_violin.SetValue(False) self._check_violin.Bind(wx.EVT_CHECKBOX, self.OnCheckViolin) sizer_check.Add(self._check_violin, 1, wx.EXPAND | wx.ALL, 2) self._panel.SetSizer(sizer) self.CenterOnScreen() self.SetIcon(wx.Icon(str(Path(__file__).parent / "apps/wolf.ico"))) self.Show()
[docs] def OnCheckAll(self, event): self.check_all = self._check_all.IsChecked()
[docs] def OnCheckViolin(self, event): self.check_violin = self._check_violin.IsChecked()
[docs] def OnSliderBegin(self, event): self.begin = min(self._slider_begin.GetValue(), self.end) self._slider_begin.SetValue(self.begin) self._text_range.SetValue('{} - {}'.format(self.begin, self.end))
[docs] def OnSliderEnd(self, event): self.end = max(self._slider_end.GetValue(), self.begin) self._slider_end.SetValue(self.end) self._text_range.SetValue('{} - {}'.format(self.begin, self.end))
[docs] def OnSliderStep(self, event): self.step = self._slider_step.GetValue() self._text_step.SetValue(str(self.step))
[docs] def OnApply(self, event): try: txt_begin, txt_end = self._text_range.GetValue().split('-') except: self._text_range.SetValue('{} - {}'.format(self.begin, self.end)) txt_step = self._text_step.GetValue() try: if self.step != int(txt_step): self._slider_step.SetValue(int(txt_step)) except: logging.error('Error while parsing the step') return try: if int(txt_begin) != self.begin or int(txt_end) != self.end: self._slider_begin.SetRange(int(txt_begin), int(txt_end)) except: logging.error('Error while parsing the range') return
[docs] def OnOK(self, event): self.Hide()
[docs] def OnCancel(self, event): self.begin = -1 self.end = -1 self.step = -1 self.Hide()
[docs] class PrecomputedDEM_DTM(Enum): """ Enum for Precomputed DEM/DTM array """
[docs] DEMDTM_50cm = "AllData.vrt"
[docs] DEMDTM_1m_average = "Combine_1m_average.vrt"
[docs] DEMDTM_1m_min = "Combine_1m_minimum.vrt"
[docs] DEMDTM_1m_max = "Combine_1m_maximum.vrt"
[docs] DEMDTM_2m_average = "Combine_2m_average.vrt"
[docs] DEMDTM_2m_min = "Combine_2m_minimum.vrt"
[docs] DEMDTM_2m_max = "Combine_2m_maximum.vrt"
[docs] DEMDTM_5m_average = "Combine_5m_average.vrt"
[docs] DEMDTM_5m_min = "Combine_5m_minimum.vrt"
[docs] DEMDTM_5m_max = "Combine_5m_maximum.vrt"
[docs] DEMDTM_10m_average = "Combine_10m_average.vrt"
[docs] DEMDTM_10m_min = "Combine_10m_minimum.vrt"
[docs] DEMDTM_10m_max = "Combine_10m_maximum.vrt"
[docs] class Precomputed_DEM_DTM_Dialog(wx.Dialog): """ wx.Dialog to select Precomputed DEM/DTM array Resolutions are 50cm, 1m, 2m, 5m, 10m Operators are average, min, max """ def __init__(self, parent, title, directory:Path | str, mapviewer:"WolfMapViewer"): super(Precomputed_DEM_DTM_Dialog, self).__init__(parent, title=title, size=(500, 350), style = wx.DEFAULT_FRAME_STYLE & ~ (wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX))
[docs] self._dir = Path(directory)
[docs] self._header = None
[docs] self._vrt = None
[docs] self._mapviewer = mapviewer
self.available_vrt()
[docs] self._panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL) # Listbox with all available operators
[docs] self._res = ['50cm', '1m', '2m', '5m', '10m']
[docs] self._ops = ['average', 'minimum', 'maximum']
[docs] self._resolution = wx.ListBox(self._panel, choices=self._res, style=wx.LB_SINGLE)
self._resolution.Bind(wx.EVT_LISTBOX, self.OnSelectResolution)
[docs] self._operations = wx.ListBox(self._panel, choices=[], style=wx.LB_SINGLE)
sizer.Add(self._resolution, 1, wx.EXPAND | wx.ALL, 2) sizer.Add(self._operations, 1, wx.EXPAND | wx.ALL, 2) sizer_btns = wx.BoxSizer(wx.HORIZONTAL)
[docs] self._cmd_sameactive = wx.Button(self._panel, wx.ID_APPLY, _('Same as active array...'))
[docs] self._cmd_sameas = wx.Button(self._panel, wx.ID_APPLY, _('Same as file...'))
[docs] self._cmd_zoom = wx.Button(self._panel, wx.ID_APPLY, _('On current zoom...'))
self._cmd_sameas.Bind(wx.EVT_BUTTON, self.OnSameAs) self._cmd_zoom.Bind(wx.EVT_BUTTON, self.OnZoom) self._cmd_sameactive.Bind(wx.EVT_BUTTON, self.OnSameActive) sizer_btns.Add(self._cmd_sameactive, 1, wx.EXPAND | wx.ALL, 2) sizer_btns.Add(self._cmd_sameas, 1, wx.EXPAND | wx.ALL, 2) sizer_btns.Add(self._cmd_zoom, 1, wx.EXPAND | wx.ALL, 2) sizer.Add(sizer_btns, 1, wx.EXPAND | wx.ALL, 2) self._panel.SetSizer(sizer) self.CenterOnScreen() self.SetIcon(wx.Icon(str(Path(__file__).parent / "apps/wolf.ico"))) self.Show()
[docs] def OnSameAs(self, event): """ Set the Precomputed DEM/DTM array to the same bounds as an existing array """ dlg = wx.FileDialog(self, _('Select a file'), str(self._dir), '', "All supported formats|*.bin;*.tif;*.tiff;*.top;*.flt;*.npy;*.npz;*.vrt|bin (*.bin)|*.bin|Elevation WOLF2D (*.top)|*.top|Geotif (*.tif)|*.tif|Float ESRI (*.flt)|*.flt|Numpy (*.npy)|*.npy|Numpy named arrays(*.npz)|*.npz|all (*.*)|*.*", wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) ret = dlg.ShowModal() if ret == wx.ID_OK: fname = Path(dlg.GetPath()) self._header = header_wolf() self._header.read_txt_header(fname) res = self._resolution.GetStringSelection() if res == '50cm': res = 0.5 elif res in ['1m', '2m', '5m', '10m']: res = float(res[:-1]) else: logging.error('Resolution not found') return if self._header.dx != res or self._header.dy != res: logging.warning(_('Resolution not the same')) logging.warning(_('Forcing resolution to {}m').format(res)) self._header.origx = float(int(self._header.origx / res) * res) self._header.origy = float(int(self._header.origy / res) * res) self._header.nbx = int(np.ceil(float(self._header.nbx) * self._header.dx / res)) self._header.nby = int(np.ceil(float(self._header.nby) * self._header.dy / res)) self._header.dx = res self._header.dy = res logging.info(_('New header:')) logging.info(self._header) self.add_array() self.Hide()
[docs] def OnSameActive(self, event): """ Set the Precomputed DEM/DTM array to the same bounds as the active array """ if self._mapviewer is None: logging.error('No mapviewer to get the active array') return active = self._mapviewer.active_array if active is None: logging.error('No active array to get the bounds') return self._header = active.get_header() res = self._resolution.GetStringSelection() if res == '50cm': res = 0.5 elif res in ['1m', '2m', '5m', '10m']: res = float(res[:-1]) else: logging.error('Resolution not found') return if self._header.dx != res or self._header.dy != res: logging.warning(_('Resolution not the same')) logging.warning(_('Forcing resolution to {}m').format(res)) self._header.origx = float(int(self._header.origx / res) * res) self._header.origy = float(int(self._header.origy / res) * res) self._header.nbx = int(np.ceil(float(self._header.nbx) * self._header.dx / res)) self._header.nby = int(np.ceil(float(self._header.nby) * self._header.dy / res)) self._header.dx = res self._header.dy = res logging.info(_('New header:')) logging.info(self._header) newarray = self.add_array() #copy palette if newarray is not None: newarray.mypal.automatic = False newarray.mypal.values = active.mypal.values self.Hide() self._mapviewer.Refresh()
[docs] def OnZoom(self, event): """ Set the Precomputed DEM/DTM array to the current zoom """ if self._mapviewer is None: logging.error('No mapviewer to get the current zoom') return onzoom = [self._mapviewer.xmin, self._mapviewer.xmax, self._mapviewer.ymin, self._mapviewer.ymax] self._header = header_wolf() # round to the nearest resolution self._header.origx = float(int(onzoom[0])) self._header.origy = float(int(onzoom[2])) res = self._resolution.GetStringSelection() if res == '50cm': res = 0.5 elif res in ['1m', '2m', '5m', '10m']: res = float(res[:-1]) else: logging.error('Resolution not found') return self._header.dx = res self._header.dy = res self._header.nbx = int(float(np.ceil(onzoom[1]) - int(onzoom[0])) / res) self._header.nby = int(float(np.ceil(onzoom[3]) - int(onzoom[2])) / res) self.add_array() self.Hide() self._mapviewer.Refresh()
@property
[docs] def selected_vrt(self): res = self._resolution.GetStringSelection() op = self._operations.GetStringSelection() vrt_names = [cur.name for cur in self._vrt] if res == '50cm': if PrecomputedDEM_DTM.DEMDTM_50cm.value in vrt_names: return self._dir / PrecomputedDEM_DTM.DEMDTM_50cm.value elif res in ['1m', '2m', '5m', '10m']: to_test = 'Combine_{}_{}.vrt'.format(res, op) if to_test in vrt_names: return self._dir / to_test else: logging.error(_('Operator not found - Did you select one?')) return None else: logging.error(_('Resolution not found - Did you select one?')) return None
[docs] def add_array(self): """ Add a new array to the viewer """ if self._mapviewer is None: logging.error(_('No mapviewer to add the array')) return if self._header is None: logging.error(_('No header defined')) return vrt = self.selected_vrt if vrt is None: logging.error(_('No vrt selected')) return newarray = WolfArray(vrt, crop= [[self._header.origx, self._header.origx + self._header.nbx * self._header.dx], [self._header.origy, self._header.origy + self._header.nby * self._header.dy]]) self._mapviewer.add_object(newobj = newarray, id = vrt.stem) return newarray
[docs] def OnSelectResolution(self, event): """ Select the resolution """ res = self._resolution.GetStringSelection() vrt_names = [i.name for i in self._vrt] if res == '50cm': if PrecomputedDEM_DTM.DEMDTM_50cm.value in vrt_names: self._operations.Set(['No operator to choose - 50cm resolution']) elif res in ['1m', '2m', '5m', '10m']: to_test = {i: 'Combine_{}_{}.vrt'.format(res, i) for i in self._ops} self._operations.Set([i for i, val in to_test.items() if val in vrt_names]) else: self._operations.Set([''])
[docs] def available_vrt(self): """ List all available vrt files in the directory """ self._vrt = [i for i in self._dir.iterdir() if i.suffix == '.vrt'] # test if vrt are in PrecomputedDEM_DTM self._vrt = [i for i in self._vrt if i.name in [j.value for j in PrecomputedDEM_DTM]]
[docs] class GlobalAnimationClock: """ Global animation clock manager for all zones with animations. Replaces per-zone wx.Timer instances with a single centralized timer. This avoids flooding the wxPython event queue with redundant refresh events when dozens of zones are animated simultaneously. Features: - Single ~30 fps timer for the entire WolfMapViewer - Zones register/unregister their animation needs - Adaptive throttling based on total animation load - On each tick, updates a shared time base and requests one redraw - Automatically stops the timer when no animations are active """
[docs] _FPS_STEPS = ( (16, 30.0), (48, 20.0), (96, 15.0), (float('inf'), 10.0), )
def __init__(self, mapviewer: 'WolfMapViewer'): """Initialize the global animation clock. :param mapviewer: The WolfMapViewer instance that owns this clock. """
[docs] self.mapviewer = mapviewer
[docs] self.timer: wx.Timer | None = None
[docs] self.subscribed_zones: dict[int, int] = {}
[docs] self.current_time: float = 0.0 # Wall-clock time since animation started
[docs] self._timer_start_time: float | None = None
[docs] self._interval_ms: int | None = None
@property
[docs] def total_load(self) -> int: """Return the total declared animation load across all zones.""" return sum(self.subscribed_zones.values())
[docs] def subscribe(self, zone, load: int = 1): """Register a zone as animated with its estimated rendering load. Automatically starts the timer if this is the first subscription. """ self.subscribed_zones[id(zone)] = max(int(load), 1) self._ensure_timer_state()
[docs] def unsubscribe(self, zone): """Unregister a zone from animation. Automatically stops the timer if this was the last subscription. """ self.subscribed_zones.pop(id(zone), None) self._ensure_timer_state()
[docs] def _get_target_interval_ms(self) -> int: """Return the timer interval based on the current animation load.""" load = self.total_load for max_load, fps in self._FPS_STEPS: if load <= max_load: return max(int(round(1000.0 / fps)), 1) return 100
[docs] def _ensure_timer_state(self): """Start, stop, or retune the timer according to current load.""" if self.total_load <= 0: self._stop_timer() return interval_ms = self._get_target_interval_ms() self._start_timer(interval_ms)
[docs] def _start_timer(self, interval_ms: int): """Start the global animation timer.""" if self.timer is None: self.timer = wx.Timer(self.mapviewer) self.mapviewer.Bind(wx.EVT_TIMER, self._on_timer_tick, self.timer) if self._timer_start_time is None: self._timer_start_time = __import__('time').monotonic() if self._interval_ms != interval_ms or not self.timer.IsRunning(): self._interval_ms = interval_ms self.timer.Start(interval_ms, wx.TIMER_CONTINUOUS)
[docs] def _stop_timer(self): """Stop the global animation timer.""" if self.timer is not None and self.timer.IsRunning(): self.timer.Stop() self._timer_start_time = None self._interval_ms = None self.current_time = 0.0
[docs] def _on_timer_tick(self, event): """Handle timer tick: update time and request canvas refresh.""" import time as time_module if self._timer_start_time is not None: self.current_time = time_module.monotonic() - self._timer_start_time try: if self.mapviewer is not None: self.mapviewer.Refresh() except Exception: pass
[docs] def get_phase(self, anim_speed: float = 1.0) -> float: """Compute animation phase for a given speed. :param anim_speed: Animation speed multiplier (default 1.0). :return: Phase in range [0, 1), cycling at rate = anim_speed. """ if self._timer_start_time is None: return 0.0 phase = (self.current_time * anim_speed) % 1.0 return phase
[docs] def destroy(self): """Clean up the timer and disconnect.""" self._stop_timer() if self.timer is not None: self.timer = None self.subscribed_zones.clear()