"""
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._end_step = self.active_res2d.get_nbresults()
[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._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)
# 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)
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)
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)
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)
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)
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)
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)
hbox6 = wx.BoxSizer(wx.HORIZONTAL)
btn1 = wx.Button(panel, -1, _('Ok'))
hbox6.Add(btn1,1,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
btn2 = wx.Button(panel, -1, _('Cancel'))
hbox6.Add(btn2,1,wx.EXPAND|wx.ALIGN_LEFT|wx.ALL,5)
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.end = sim.get_nbresults(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._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()