diff --git a/gui/wxpython/animation/frame.py b/gui/wxpython/animation/frame.py index 4db0955b942..25a8da54858 100644 --- a/gui/wxpython/animation/frame.py +++ b/gui/wxpython/animation/frame.py @@ -1,14 +1,10 @@ """ @package animation.frame -@brief Animation frame and different types of sliders +@brief Animation window for the standalone Animation Tool Classes: - frame::AnimationFrame - - frame::AnimationsPanel - - frame::AnimationSliderBase - - frame::SimpleAnimationSlider - - frame::TimeAnimationSlider (C) 2013 by the GRASS Development Team @@ -19,626 +15,68 @@ """ import os + import wx -import wx.aui import grass.script as gs -import grass.temporal as tgis -from grass.exceptions import FatalError from core import globalvar -from gui_core.widgets import IntegerValidator -from gui_core.wrap import StaticText, TextCtrl, Slider -from core.gcmd import RunCommand, GWarning - -from animation.mapwindow import AnimationWindow -from animation.provider import BitmapProvider, BitmapPool, MapFilesPool, CleanUp -from animation.controller import AnimationController -from animation.anim import Animation -from animation.toolbars import MainToolbar, AnimationToolbar, MiscToolbar -from animation.dialogs import SpeedDialog, PreferencesDialog -from animation.utils import Orientation, ReplayMode, TemporalType +from animation.panels import AnimationPanel, MAX_COUNT -MAX_COUNT = 4 -TMP_DIR = None +__all__ = ["MAX_COUNT", "AnimationFrame"] gs.set_raise_on_error(True) class AnimationFrame(wx.Frame): + """Window hosting the animation panel. + + Used when the tool runs on its own, that is from g.gui.animation and from + the multi-window layout. The Single-Window layout adds the panel to its + notebook instead, see main_window.frame.MainFrame.OnAnimationTool. + """ + def __init__( self, parent, giface, title=_("Animation Tool"), rasters=None, timeseries=None ): + """ + :param parent: parent window + :param giface: GRASS interface + :param title: window title + :param rasters: raster maps to animate + :param timeseries: space time datasets to animate + """ wx.Frame.__init__( self, parent, title=title, style=wx.DEFAULT_FRAME_STYLE, size=(800, 600) ) - self._giface = giface self.SetClientSize(self.GetSize()) - self.iconsize = (16, 16) - self.SetIcon( wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO) ) - # Make sure the temporal database exists - try: - tgis.init() - except FatalError as e: - GWarning(parent=self, message=str(e)) - - # create temporal directory and ensure it's deleted after programs ends - # (stored in MAPSET/.tmp/) - global TMP_DIR - TMP_DIR = gs.tempdir() - - self.animations = [Animation() for i in range(MAX_COUNT)] - self.windows = [] - self.animationPanel = AnimationsPanel( - self, self.windows, initialCount=MAX_COUNT - ) - bitmapPool = BitmapPool() - mapFilesPool = MapFilesPool() - - self._progressDlg = None - self._progressDlgMax = None - - self.provider = BitmapProvider( - bitmapPool=bitmapPool, mapFilesPool=mapFilesPool, tempDir=TMP_DIR - ) - self.animationSliders = {} - self.animationSliders["nontemporal"] = SimpleAnimationSlider(self) - self.animationSliders["temporal"] = TimeAnimationSlider(self) - self.controller = AnimationController( - frame=self, - sliders=self.animationSliders, - animations=self.animations, - mapwindows=self.windows, - provider=self.provider, - bitmapPool=bitmapPool, - mapFilesPool=mapFilesPool, + self.statusbar = self.CreateStatusBar(number=1, style=0) + self.panel = AnimationPanel( + parent=self, + giface=giface, + statusbar=self.statusbar, + rasters=rasters, + timeseries=timeseries, ) - for win in self.windows: - win.Bind(wx.EVT_SIZE, self.FrameSizeChanged) - self.provider.mapsLoaded.connect(lambda: self.SetStatusText("")) - self.provider.renderingStarted.connect(self._showRenderingProgress) - self.provider.renderingContinues.connect(self._updateProgress) - self.provider.renderingFinished.connect(self._closeProgress) - self.provider.compositionStarted.connect(self._showRenderingProgress) - self.provider.compositionContinues.connect(self._updateProgress) - self.provider.compositionFinished.connect(self._closeProgress) - - self.InitStatusbar() - self._mgr = wx.aui.AuiManager(self) - - # toolbars - self.toolbars = {} - self._addToolbars() - self._addPanes() - self._mgr.Update() - - self.dialogs = {} - self.dialogs["speed"] = None - self.dialogs["preferences"] = None + self.SetName("AnimationFrame") self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) - def InitStatusbar(self): - """Init statusbar.""" - self.CreateStatusBar(number=1, style=0) - - def _addPanes(self): - self._mgr.AddPane( - self.animationPanel, - wx.aui.AuiPaneInfo() - .CentrePane() - .Name("animPanel") - .CentrePane() - .CaptionVisible(False) - .PaneBorder(False) - .Floatable(False) - .BestSize((-1, -1)) - .CloseButton(False) - .DestroyOnClose(True) - .Layer(0), - ) - for name, slider in self.animationSliders.items(): - self._mgr.AddPane( - slider, - wx.aui.AuiPaneInfo() - .PaneBorder(False) - .Name("slider_" + name) - .Layer(1) - .CaptionVisible(False) - .BestSize(slider.GetBestSize()) - .DestroyOnClose(True) - .CloseButton(False) - .Bottom(), - ) - self._mgr.GetPane("slider_" + name).Hide() - - def _addToolbars(self): - """Add toolbars to the window - - Currently known toolbars are: - - 'mainToolbar' - data management - - 'animationToolbar' - animation controls - - 'miscToolbar' - help, close - """ - self.toolbars["mainToolbar"] = MainToolbar(self) - self._mgr.AddPane( - self.toolbars["mainToolbar"], - wx.aui.AuiPaneInfo() - .Name("mainToolbar") - .Caption(_("Main Toolbar")) - .ToolbarPane() - .Top() - .LeftDockable(False) - .RightDockable(False) - .BottomDockable(True) - .TopDockable(True) - .CloseButton(False) - .Layer(2) - .Row(1) - .Position(0) - .BestSize(self.toolbars["mainToolbar"].GetBestSize()), - ) - - self.toolbars["animationToolbar"] = AnimationToolbar(self) - self._mgr.AddPane( - self.toolbars["animationToolbar"], - wx.aui.AuiPaneInfo() - .Name("animationToolbar") - .Caption(_("Animation Toolbar")) - .ToolbarPane() - .Top() - .LeftDockable(False) - .RightDockable(False) - .BottomDockable(True) - .TopDockable(True) - .CloseButton(False) - .Layer(2) - .Row(1) - .Position(1) - .BestSize(self.toolbars["animationToolbar"].GetBestSize()), - ) - self.controller.SetAnimationToolbar(self.toolbars["animationToolbar"]) - - self.toolbars["miscToolbar"] = MiscToolbar(self) - self._mgr.AddPane( - self.toolbars["miscToolbar"], - wx.aui.AuiPaneInfo() - .Name("miscToolbar") - .Caption(_("Misc Toolbar")) - .ToolbarPane() - .Top() - .LeftDockable(False) - .RightDockable(False) - .BottomDockable(True) - .TopDockable(True) - .CloseButton(False) - .Layer(2) - .Row(1) - .Position(2) - .BestSize(self.toolbars["miscToolbar"].GetBestSize()), - ) - def SetAnimations(self, layerLists): """Set animation data :param layerLists: list of layerLists """ - self.controller.SetAnimations(layerLists) - - def OnAddAnimation(self, event): - self.controller.AddAnimation() - - def AddWindow(self, index): - self.animationPanel.AddWindow(index) - - def RemoveWindow(self, index): - self.animationPanel.RemoveWindow(index) - - def IsWindowShown(self, index): - return self.animationPanel.IsWindowShown(index) - - def OnEditAnimation(self, event): - self.controller.EditAnimations() - - def SetSlider(self, name): - if name == "nontemporal": - self._mgr.GetPane("slider_nontemporal").Show() - self._mgr.GetPane("slider_temporal").Hide() - - elif name == "temporal": - self._mgr.GetPane("slider_temporal").Show() - self._mgr.GetPane("slider_nontemporal").Hide() - else: - self._mgr.GetPane("slider_temporal").Hide() - self._mgr.GetPane("slider_nontemporal").Hide() - self._mgr.Update() - - def OnPlayForward(self, event): - self.controller.SetOrientation(Orientation.FORWARD) - self.controller.StartAnimation() - - def OnPlayBack(self, event): - self.controller.SetOrientation(Orientation.BACKWARD) - self.controller.StartAnimation() - - def OnPause(self, event): - self.controller.PauseAnimation(paused=event.IsChecked()) - - def OnStop(self, event): - self.controller.EndAnimation() - - def OnOneDirectionReplay(self, event): - mode = ReplayMode.REPEAT if event.IsChecked() else ReplayMode.ONESHOT - self.controller.SetReplayMode(mode) - - def OnBothDirectionReplay(self, event): - mode = ReplayMode.REVERSE if event.IsChecked() else ReplayMode.ONESHOT - self.controller.SetReplayMode(mode) - - def OnAdjustSpeed(self, event): - win = self.dialogs["speed"] - if win: - win.SetTemporalMode(self.controller.GetTemporalMode()) - win.SetTimeGranularity(self.controller.GetTimeGranularity()) - win.InitTimeSpin(self.controller.GetTimeTick()) - if win.IsShown(): - win.SetFocus() - else: - win.Show() - else: # start - win = SpeedDialog( - self, - temporalMode=self.controller.GetTemporalMode(), - timeGranularity=self.controller.GetTimeGranularity(), - initialSpeed=self.controller.timeTick, - ) - win.CenterOnParent() - self.dialogs["speed"] = win - win.speedChanged.connect(self.ChangeSpeed) - win.Show() - - def ChangeSpeed(self, ms): - self.controller.timeTick = ms - - def Reload(self, event): - self.controller.Reload() - - def _showRenderingProgress(self, count): - # the message is not really visible, it's there for the initial dlg - # size - self._progressDlg = wx.ProgressDialog( - title=_("Loading data"), - message="Loading data started, please be patient.", - maximum=count, - parent=self, - style=wx.PD_CAN_ABORT | wx.PD_APP_MODAL | wx.PD_AUTO_HIDE | wx.PD_SMOOTH, - ) - self._progressDlgMax = count - - def _updateProgress(self, current, text): - text += _(" ({c} out of {p})").format(c=current, p=self._progressDlgMax) - keepGoing, skip = self._progressDlg.Update(current, text) - if not keepGoing: - self.provider.RequestStopRendering() - - def _closeProgress(self): - self._progressDlg.Destroy() - self._progressDlg = None - - def OnExportAnimation(self, event): - self.controller.Export() - - def FrameSizeChanged(self, event): - maxWidth = maxHeight = 0 - for win in self.windows: - w, h = win.GetClientSize() - if w >= maxWidth and h >= maxHeight: - maxWidth, maxHeight = w, h - self.provider.WindowSizeChanged(maxWidth, maxHeight) - event.Skip() - - def OnPreferences(self, event): - if not self.dialogs["preferences"]: - dlg = PreferencesDialog(parent=self, giface=self._giface) - self.dialogs["preferences"] = dlg - dlg.formatChanged.connect(lambda: self.controller.UpdateAnimations()) - dlg.CenterOnParent() - - self.dialogs["preferences"].Show() - - def OnHelp(self, event): - RunCommand("g.manual", quiet=True, entry="wxGUI.animation") + self.panel.SetAnimations(layerLists) def OnCloseWindow(self, event): - if self.controller.timer.IsRunning(): - self.controller.timer.Stop() - CleanUp(TMP_DIR)() - self._mgr.UnInit() - self.Destroy() + """Clean up and close. - def __del__(self): - """It might not be called, therefore we try to clean it all in OnCloseWindow.""" - if hasattr(self, "controller") and hasattr(self.controller, "timer"): - if self.controller.timer.IsRunning(): - self.controller.timer.Stop() - CleanUp(TMP_DIR)() - - -class AnimationsPanel(wx.Panel): - def __init__(self, parent, windows, initialCount=4): - wx.Panel.__init__(self, parent, id=wx.ID_ANY, style=wx.NO_BORDER) - self.shown = [] - self.count = initialCount - self.mainSizer = wx.FlexGridSizer(cols=2, hgap=0, vgap=0) - for i in range(initialCount): - w = AnimationWindow(self) - windows.append(w) - self.mainSizer.Add(w, proportion=1, flag=wx.EXPAND) - - self.mainSizer.AddGrowableCol(0) - self.mainSizer.AddGrowableCol(1) - self.mainSizer.AddGrowableRow(0) - self.mainSizer.AddGrowableRow(1) - self.windows = windows - self.SetSizerAndFit(self.mainSizer) - - for i in range(initialCount): - self.mainSizer.Hide(windows[i]) - self.Layout() - - def AddWindow(self, index): - if len(self.shown) == self.count: - return - self.mainSizer.Show(self.windows[index]) - self.shown.append(index) - self.Layout() - - def RemoveWindow(self, index): - if len(self.shown) == 0: - return - self.mainSizer.Hide(self.windows[index]) - self.shown.remove(index) - self.Layout() - - def IsWindowShown(self, index): - return self.mainSizer.IsShown(self.windows[index]) - - -class AnimationSliderBase(wx.Panel): - def __init__(self, parent): - wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY) - self.label1 = StaticText(self, id=wx.ID_ANY) - self.slider = Slider(self, id=wx.ID_ANY, style=wx.SL_HORIZONTAL) - self.indexField = TextCtrl( - self, - id=wx.ID_ANY, - size=(40, -1), - style=wx.TE_PROCESS_ENTER | wx.TE_RIGHT, - validator=IntegerValidator(), - ) - - self.callbackSliderChanging = None - self.callbackSliderChanged = None - self.callbackFrameIndexChanged = None - - self.framesCount = 0 - - self.enable = True - - self.slider.Bind(wx.EVT_SPIN, self.OnSliderChanging) - self.slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.OnSliderChanged) - self.indexField.Bind(wx.EVT_TEXT_ENTER, self.OnFrameIndexChanged) - - def UpdateFrame(self, index): - self._updateFrameIndex(index) - if not self.enable: - return - - self.slider.SetValue(index) - - def _updateFrameIndex(self, index): - raise NotImplementedError - - def OnFrameIndexChanged(self, event): - self._onFrameIndexChanged(event) - - def SetFrames(self, frames): - self._setFrames(frames) - - def _setFrames(self, frames): - raise NotImplementedError - - def SetCallbackSliderChanging(self, callback): - self.callbackSliderChanging = callback - - def SetCallbackSliderChanged(self, callback): - self.callbackSliderChanged = callback - - def SetCallbackFrameIndexChanged(self, callback): - self.callbackFrameIndexChanged = callback - - def EnableSlider(self, enable=True): - if enable and self.framesCount <= 1: - enable = False # we don't want to enable it - self.enable = enable - self.slider.Enable(enable) - self.indexField.Enable(enable) - - def OnSliderChanging(self, event): - self.callbackSliderChanging(event.GetInt()) - - def OnSliderChanged(self, event): - self.callbackSliderChanged() - - def _onFrameIndexChanged(self, event): - index = self.indexField.GetValue() - index = self._validate(index) - if index is not None: - self.slider.SetValue(index) - self.callbackFrameIndexChanged(index) - - def _validate(self, index): - try: - index = int(index) - except ValueError: - index = self.slider.GetValue() - self.indexField.SetValue(str(index + 1)) - return None - start, end = self.slider.GetRange() - index -= 1 - if index > end: - index = end - self.indexField.SetValue(str(end + 1)) - elif index < start: - index = start - self.indexField.SetValue(str(start + 1)) - - return index - - -class SimpleAnimationSlider(AnimationSliderBase): - def __init__(self, parent): - AnimationSliderBase.__init__(self, parent) - - self._setLabel() - self._doLayout() - - def _doLayout(self): - hbox = wx.BoxSizer(wx.HORIZONTAL) - hbox.Add( - self.indexField, proportion=0, flag=wx.ALIGN_CENTER | wx.LEFT, border=5 - ) - hbox.Add( - self.label1, - proportion=0, - flag=wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, - border=5, - ) - hbox.Add(self.slider, proportion=1, flag=wx.EXPAND, border=0) - self.SetSizerAndFit(hbox) - - def _setFrames(self, count): - self.framesCount = count - if self.framesCount > 1: - self.slider.SetRange(0, self.framesCount - 1) - self.EnableSlider(True) - else: - self.EnableSlider(False) - self._setLabel() - - def _setLabel(self): - label = "/ %(framesCount)s" % {"framesCount": self.framesCount} - self.label1.SetLabel(label) - self.Layout() - - def _updateFrameIndex(self, index): - self.indexField.SetValue(str(index + 1)) - - -class TimeAnimationSlider(AnimationSliderBase): - def __init__(self, parent): - AnimationSliderBase.__init__(self, parent) - self.timeLabels = [] - self.label2 = StaticText(self, id=wx.ID_ANY) - self.label3 = StaticText(self, id=wx.ID_ANY) - self.label2Length = 0 - self.temporalType = TemporalType.RELATIVE - - self._setLabel() - self._doLayout() - - def _doLayout(self): - vbox = wx.BoxSizer(wx.VERTICAL) - hbox = wx.BoxSizer(wx.HORIZONTAL) - hbox.Add(self.label1, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL, border=0) - hbox.AddStretchSpacer() - hbox.Add(self.indexField, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL, border=0) - hbox.Add( - self.label2, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=3 - ) - hbox.AddStretchSpacer() - hbox.Add(self.label3, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL, border=0) - vbox.Add(hbox, proportion=0, flag=wx.EXPAND, border=0) - - hbox = wx.BoxSizer(wx.HORIZONTAL) - hbox.Add(self.slider, proportion=1, flag=wx.EXPAND, border=0) - vbox.Add(hbox, proportion=0, flag=wx.EXPAND, border=0) - - self._setTemporalType() - self.SetSizerAndFit(vbox) - - def _setTemporalType(self): - sizer = self.indexField.GetContainingSizer() - # sizer.Show(self.indexField, False) # TODO: what to do? - sizer.Show(self.indexField, self.temporalType == TemporalType.RELATIVE) - self.Layout() - - def SetTemporalType(self, mode): - self.temporalType = mode - self._setTemporalType() - - def _setFrames(self, timeLabels): - self.timeLabels = timeLabels - self.framesCount = len(timeLabels) - if self.framesCount > 1: - self.slider.SetRange(0, self.framesCount - 1) - self.EnableSlider(True) - else: - self.EnableSlider(False) - self._setLabel() - # TODO: fix setting index values, until then: - self.indexField.Disable() - - def _setLabel(self): - if self.timeLabels: - if self.temporalType == TemporalType.ABSOLUTE: - start = self.timeLabels[0][0] - self.label1.SetLabel(start) - if self.timeLabels[-1][1]: - end = self.timeLabels[-1][1] - else: - end = self.timeLabels[-1][0] - self.label3.SetLabel(end) - else: - unit = self.timeLabels[0][2] - start = self.timeLabels[0][0] - self.label1.SetLabel(start) - if self.timeLabels[-1][1]: - end = self.timeLabels[-1][1] - else: - end = self.timeLabels[-1][0] - end = "%(end)s %(unit)s" % {"end": end, "unit": unit} - self.label3.SetLabel(end) - - self.label2Length = len(start) - self._updateFrameIndex(0) - - else: - self.label1.SetLabel("") - self.label2.SetLabel("") - self.label3.SetLabel("") - self.Layout() - - def _updateFrameIndex(self, index): - start = self.timeLabels[index][0] - if self.timeLabels[index][1]: # interval - if self.temporalType == TemporalType.ABSOLUTE: - label = _("%(from)s %(dash)s %(to)s") % { - "from": start, - "dash": "\u2013", - "to": self.timeLabels[index][1], - } - else: - label = _("to %(to)s") % {"to": self.timeLabels[index][1]} - else: - label = start if self.temporalType == TemporalType.ABSOLUTE else "" - self.label2.SetLabel(label) - if self.temporalType == TemporalType.RELATIVE: - self.indexField.SetValue(start) - if len(label) != self.label2Length: - self.label2Length = len(label) - self.Layout() + The panel releases its resources and then destroys this window, so + this must not destroy it as well. + """ + self.panel.OnCloseWindow(event) diff --git a/gui/wxpython/animation/panels.py b/gui/wxpython/animation/panels.py new file mode 100644 index 00000000000..fe9e9943e34 --- /dev/null +++ b/gui/wxpython/animation/panels.py @@ -0,0 +1,671 @@ +""" +@package animation.panels + +@brief Animation Panel + +Classes: + - panels::AnimationPanel + +(C) 2026 by the GRASS Development Team + +This program is free software under the GNU General Public License +(>=v2). Read the file COPYING that comes with GRASS for details. +""" + +import wx +import wx.aui + +import grass.script as gs +import grass.temporal as tgis +from grass.exceptions import FatalError +from main_window.page import MainPageBase +from core.gcmd import RunCommand, GException, GWarning +from gui_core.widgets import IntegerValidator +from gui_core.wrap import StaticText, TextCtrl, Slider + +from animation.mapwindow import AnimationWindow +from animation.provider import BitmapProvider, BitmapPool, MapFilesPool, CleanUp +from animation.controller import AnimationController +from animation.anim import Animation +from animation.toolbars import MainToolbar, AnimationToolbar, MiscToolbar +from animation.dialogs import SpeedDialog, PreferencesDialog +from animation.utils import Orientation, ReplayMode, TemporalType + +MAX_COUNT = 4 + +gs.set_raise_on_error(True) + + +class AnimationPanel(wx.Panel, MainPageBase): + def __init__( + self, + parent, + giface, + id=wx.ID_ANY, + statusbar=None, + dockable=False, + rasters=None, + timeseries=None, + **kwargs, + ): + self.parent = parent + self._giface = giface + self.statusbar = statusbar + + wx.Panel.__init__(self, parent=parent, id=id, **kwargs) + MainPageBase.__init__(self, dockable) + self.SetName("Animation") + + # Make sure the temporal database exists + try: + tgis.init() + except FatalError as e: + GWarning(parent=self, message=str(e)) + + # Create temporal directory, deleted on close via CleanUp + self.TMP_DIR = gs.tempdir() + + self.animations = [Animation() for _ in range(MAX_COUNT)] + self.windows = [] + self.animationPanel = AnimationsPanel( + self, self.windows, initialCount=MAX_COUNT + ) + bitmapPool = BitmapPool() + mapFilesPool = MapFilesPool() + + self._progressDlg = None + self._progressDlgMax = None + + self.provider = BitmapProvider( + bitmapPool=bitmapPool, mapFilesPool=mapFilesPool, tempDir=self.TMP_DIR + ) + self.animationSliders = {} + self.animationSliders["nontemporal"] = SimpleAnimationSlider(self) + self.animationSliders["temporal"] = TimeAnimationSlider(self) + self.controller = AnimationController( + frame=self, + sliders=self.animationSliders, + animations=self.animations, + mapwindows=self.windows, + provider=self.provider, + bitmapPool=bitmapPool, + mapFilesPool=mapFilesPool, + ) + for win in self.windows: + win.Bind(wx.EVT_SIZE, self.FrameSizeChanged) + + # Connect provider signals once, not per window + self.provider.mapsLoaded.connect(lambda: self.SetStatusText("")) + self.provider.renderingStarted.connect(self._showRenderingProgress) + self.provider.renderingContinues.connect(self._updateProgress) + self.provider.renderingFinished.connect(self._closeProgress) + self.provider.compositionStarted.connect(self._showRenderingProgress) + self.provider.compositionContinues.connect(self._updateProgress) + self.provider.compositionFinished.connect(self._closeProgress) + + self._mgr = wx.aui.AuiManager(self) + + # toolbars + self.toolbars = {} + self._addToolbars() + self._addPanes() + self._mgr.Update() + + self.dialogs = {} + self.dialogs["speed"] = None + self.dialogs["preferences"] = None + + self.Bind(wx.EVT_SIZE, self.OnSize) + + def OnSize(self, event): + """Lay the managed panes out again when the panel is resized. + + Dragging the notebook tab moves and reparents the panel. Without this + the panes keep the layout they had before the drag until some other + event happens to force an update. + """ + self._mgr.Update() + event.Skip() + + def SetStatusText(self, text): + if self.statusbar: + self.statusbar.SetStatusText(text) + elif hasattr(self.parent, "SetStatusText"): + self.parent.SetStatusText(text) + + def _addPanes(self): + self._mgr.AddPane( + self.animationPanel, + wx.aui.AuiPaneInfo() + .CentrePane() + .Name("animPanel") + .CaptionVisible(False) + .PaneBorder(False) + .Floatable(False) + .BestSize((-1, -1)) + .CloseButton(False) + .DestroyOnClose(True) + .Layer(0), + ) + for name, slider in self.animationSliders.items(): + self._mgr.AddPane( + slider, + wx.aui.AuiPaneInfo() + .PaneBorder(False) + .Name("slider_" + name) + .Layer(1) + .CaptionVisible(False) + .BestSize(slider.GetBestSize()) + .DestroyOnClose(True) + .CloseButton(False) + .Bottom(), + ) + self._mgr.GetPane("slider_" + name).Hide() + + def _addToolbars(self): + self.toolbars["mainToolbar"] = MainToolbar(self) + self._mgr.AddPane( + self.toolbars["mainToolbar"], + wx.aui.AuiPaneInfo() + .Name("mainToolbar") + .Caption(_("Main Toolbar")) + .ToolbarPane() + .Top() + .LeftDockable(False) + .RightDockable(False) + .BottomDockable(True) + .TopDockable(True) + .CloseButton(False) + .Layer(2) + .Row(1) + .Position(0) + .BestSize(self.toolbars["mainToolbar"].GetBestSize()), + ) + + self.toolbars["animationToolbar"] = AnimationToolbar(self) + self._mgr.AddPane( + self.toolbars["animationToolbar"], + wx.aui.AuiPaneInfo() + .Name("animationToolbar") + .Caption(_("Animation Toolbar")) + .ToolbarPane() + .Top() + .LeftDockable(False) + .RightDockable(False) + .BottomDockable(True) + .TopDockable(True) + .CloseButton(False) + .Layer(2) + .Row(1) + .Position(1) + .BestSize(self.toolbars["animationToolbar"].GetBestSize()), + ) + self.controller.SetAnimationToolbar(self.toolbars["animationToolbar"]) + + self.toolbars["miscToolbar"] = MiscToolbar(self) + self._mgr.AddPane( + self.toolbars["miscToolbar"], + wx.aui.AuiPaneInfo() + .Name("miscToolbar") + .Caption(_("Misc Toolbar")) + .ToolbarPane() + .Top() + .LeftDockable(False) + .RightDockable(False) + .BottomDockable(True) + .TopDockable(True) + .CloseButton(False) + .Layer(2) + .Row(1) + .Position(2) + .BestSize(self.toolbars["miscToolbar"].GetBestSize()), + ) + + def SetAnimations(self, layerLists): + self.controller.SetAnimations(layerLists) + + def OnAddAnimation(self, event): + self.controller.AddAnimation() + + def AddWindow(self, index): + self.animationPanel.AddWindow(index) + + def RemoveWindow(self, index): + self.animationPanel.RemoveWindow(index) + + def IsWindowShown(self, index): + return self.animationPanel.IsWindowShown(index) + + def OnEditAnimation(self, event): + self.controller.EditAnimations() + + def SetSlider(self, name): + if name == "nontemporal": + self._mgr.GetPane("slider_nontemporal").Show() + self._mgr.GetPane("slider_temporal").Hide() + elif name == "temporal": + self._mgr.GetPane("slider_temporal").Show() + self._mgr.GetPane("slider_nontemporal").Hide() + else: + self._mgr.GetPane("slider_temporal").Hide() + self._mgr.GetPane("slider_nontemporal").Hide() + self._mgr.Update() + + def OnPlayForward(self, event): + self.controller.SetOrientation(Orientation.FORWARD) + self.controller.StartAnimation() + + def OnPlayBack(self, event): + self.controller.SetOrientation(Orientation.BACKWARD) + self.controller.StartAnimation() + + def OnPause(self, event): + self.controller.PauseAnimation(paused=event.IsChecked()) + + def OnStop(self, event): + self.controller.EndAnimation() + + def OnOneDirectionReplay(self, event): + mode = ReplayMode.REPEAT if event.IsChecked() else ReplayMode.ONESHOT + self.controller.SetReplayMode(mode) + + def OnBothDirectionReplay(self, event): + mode = ReplayMode.REVERSE if event.IsChecked() else ReplayMode.ONESHOT + self.controller.SetReplayMode(mode) + + def OnAdjustSpeed(self, event): + win = self.dialogs["speed"] + if win: + win.SetTemporalMode(self.controller.GetTemporalMode()) + win.SetTimeGranularity(self.controller.GetTimeGranularity()) + win.InitTimeSpin(self.controller.GetTimeTick()) + if win.IsShown(): + win.SetFocus() + else: + win.Show() + else: + win = SpeedDialog( + self, + temporalMode=self.controller.GetTemporalMode(), + timeGranularity=self.controller.GetTimeGranularity(), + initialSpeed=self.controller.timeTick, + ) + win.CenterOnParent() + self.dialogs["speed"] = win + win.speedChanged.connect(self.ChangeSpeed) + win.Show() + + def ChangeSpeed(self, ms): + self.controller.timeTick = ms + + def Reload(self, event): + self.controller.Reload() + + def _showRenderingProgress(self, count): + self._progressDlg = wx.ProgressDialog( + title=_("Loading data"), + message="Loading data started, please be patient.", + maximum=count + 1 if count > 0 else 100, + parent=self, + style=wx.PD_CAN_ABORT | wx.PD_APP_MODAL | wx.PD_SMOOTH, + ) + self._progressDlgMax = count + + def _updateProgress(self, current, text): + text += _(" ({c} out of {p})").format(c=current, p=self._progressDlgMax) + keepGoing, skip = self._progressDlg.Update(current, text) + if not keepGoing: + self.provider.RequestStopRendering() + + def _closeProgress(self): + if self._progressDlg: + wx.CallAfter(self._progressDlg.Destroy) + self._progressDlg = None + + def OnExportAnimation(self, event): + self.controller.Export() + + def FrameSizeChanged(self, event): + maxWidth = maxHeight = 0 + for win in self.windows: + w, h = win.GetClientSize() + if w >= maxWidth and h >= maxHeight: + maxWidth, maxHeight = w, h + self.provider.WindowSizeChanged(maxWidth, maxHeight) + event.Skip() + + def OnPreferences(self, event): + if not self.dialogs["preferences"]: + dlg = PreferencesDialog(parent=self, giface=self._giface) + self.dialogs["preferences"] = dlg + dlg.formatChanged.connect(lambda: self.controller.UpdateAnimations()) + dlg.CenterOnParent() + + self.dialogs["preferences"].Show() + + def OnHelp(self, event): + RunCommand("g.manual", quiet=True, entry="wxGUI.animation") + + def OnCloseWindow(self, event=None): + """Clean up resources and close the panel.""" + if hasattr(self, "controller") and hasattr(self.controller, "timer"): + if self.controller.timer.IsRunning(): + self.controller.timer.Stop() + CleanUp(self.TMP_DIR)() + self._mgr.UnInit() + self._onCloseWindow(event) + + def __del__(self): + """Remove the temporary directory if the panel was never closed. + + It might not be called, therefore we try to clean it all in + OnCloseWindow. The attributes are checked because the panel may not + have been fully constructed. + """ + if hasattr(self, "controller") and hasattr(self.controller, "timer"): + if self.controller.timer.IsRunning(): + self.controller.timer.Stop() + if hasattr(self, "TMP_DIR"): + CleanUp(self.TMP_DIR)() + + def LoadDataset(self, stds_type, dataset): + """Load a space time dataset into the first animation slot.""" + if stds_type == "str3ds": + GWarning( + parent=self, + message=_("3D raster animation is not supported."), + ) + return + + from animation.data import AnimLayer + from core.layerlist import LayerList + + layerList = LayerList() + layer = AnimLayer() + layer.mapType = stds_type + try: + layer.name = dataset + except GException as e: + GWarning(parent=self, message=str(e)) + return + + layer.cmd = ["d.rast", "map="] if stds_type == "strds" else ["d.vect", "map="] + layerList.AddLayer(layer) + + self.SetAnimations([layerList] + [None] * (MAX_COUNT - 1)) + + +class AnimationsPanel(wx.Panel): + def __init__(self, parent, windows, initialCount=4): + wx.Panel.__init__(self, parent, id=wx.ID_ANY, style=wx.NO_BORDER) + self.shown = [] + self.count = initialCount + self.mainSizer = wx.FlexGridSizer(cols=2, hgap=0, vgap=0) + for i in range(initialCount): + w = AnimationWindow(self) + windows.append(w) + self.mainSizer.Add(w, proportion=1, flag=wx.EXPAND) + + self.mainSizer.AddGrowableCol(0) + self.mainSizer.AddGrowableCol(1) + self.mainSizer.AddGrowableRow(0) + self.mainSizer.AddGrowableRow(1) + self.windows = windows + self.SetSizerAndFit(self.mainSizer) + + for i in range(initialCount): + self.mainSizer.Hide(windows[i]) + self.Layout() + + def AddWindow(self, index): + if len(self.shown) == self.count: + return + self.mainSizer.Show(self.windows[index]) + self.shown.append(index) + self.Layout() + + def RemoveWindow(self, index): + if len(self.shown) == 0: + return + self.mainSizer.Hide(self.windows[index]) + self.shown.remove(index) + self.Layout() + + def IsWindowShown(self, index): + return self.mainSizer.IsShown(self.windows[index]) + + +class AnimationSliderBase(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY) + self.label1 = StaticText(self, id=wx.ID_ANY) + self.slider = Slider(self, id=wx.ID_ANY, style=wx.SL_HORIZONTAL) + self.indexField = TextCtrl( + self, + id=wx.ID_ANY, + size=(40, -1), + style=wx.TE_PROCESS_ENTER | wx.TE_RIGHT, + validator=IntegerValidator(), + ) + + self.callbackSliderChanging = None + self.callbackSliderChanged = None + self.callbackFrameIndexChanged = None + + self.framesCount = 0 + + self.enable = True + + self.slider.Bind(wx.EVT_SPIN, self.OnSliderChanging) + self.slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.OnSliderChanged) + self.indexField.Bind(wx.EVT_TEXT_ENTER, self.OnFrameIndexChanged) + + def UpdateFrame(self, index): + self._updateFrameIndex(index) + if not self.enable: + return + + self.slider.SetValue(index) + + def _updateFrameIndex(self, index): + raise NotImplementedError + + def OnFrameIndexChanged(self, event): + self._onFrameIndexChanged(event) + + def SetFrames(self, frames): + self._setFrames(frames) + + def _setFrames(self, frames): + raise NotImplementedError + + def SetCallbackSliderChanging(self, callback): + self.callbackSliderChanging = callback + + def SetCallbackSliderChanged(self, callback): + self.callbackSliderChanged = callback + + def SetCallbackFrameIndexChanged(self, callback): + self.callbackFrameIndexChanged = callback + + def EnableSlider(self, enable=True): + if enable and self.framesCount <= 1: + enable = False # we don't want to enable it + self.enable = enable + self.slider.Enable(enable) + self.indexField.Enable(enable) + + def OnSliderChanging(self, event): + self.callbackSliderChanging(event.GetInt()) + + def OnSliderChanged(self, event): + self.callbackSliderChanged() + + def _onFrameIndexChanged(self, event): + index = self.indexField.GetValue() + index = self._validate(index) + if index is not None: + self.slider.SetValue(index) + self.callbackFrameIndexChanged(index) + + def _validate(self, index): + try: + index = int(index) + except ValueError: + index = self.slider.GetValue() + self.indexField.SetValue(str(index + 1)) + return None + start, end = self.slider.GetRange() + index -= 1 + if index > end: + index = end + self.indexField.SetValue(str(end + 1)) + elif index < start: + index = start + self.indexField.SetValue(str(start + 1)) + + return index + + +class SimpleAnimationSlider(AnimationSliderBase): + def __init__(self, parent): + AnimationSliderBase.__init__(self, parent) + + self._setLabel() + self._doLayout() + + def _doLayout(self): + hbox = wx.BoxSizer(wx.HORIZONTAL) + hbox.Add( + self.indexField, proportion=0, flag=wx.ALIGN_CENTER | wx.LEFT, border=5 + ) + hbox.Add( + self.label1, + proportion=0, + flag=wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, + border=5, + ) + hbox.Add(self.slider, proportion=1, flag=wx.EXPAND, border=0) + self.SetSizerAndFit(hbox) + + def _setFrames(self, count): + self.framesCount = count + if self.framesCount > 1: + self.slider.SetRange(0, self.framesCount - 1) + self.EnableSlider(True) + else: + self.EnableSlider(False) + self._setLabel() + + def _setLabel(self): + label = "/ %(framesCount)s" % {"framesCount": self.framesCount} + self.label1.SetLabel(label) + self.Layout() + + def _updateFrameIndex(self, index): + self.indexField.SetValue(str(index + 1)) + + +class TimeAnimationSlider(AnimationSliderBase): + def __init__(self, parent): + AnimationSliderBase.__init__(self, parent) + self.timeLabels = [] + self.label2 = StaticText(self, id=wx.ID_ANY) + self.label3 = StaticText(self, id=wx.ID_ANY) + self.label2Length = 0 + self.temporalType = TemporalType.RELATIVE + + self._setLabel() + self._doLayout() + + def _doLayout(self): + vbox = wx.BoxSizer(wx.VERTICAL) + hbox = wx.BoxSizer(wx.HORIZONTAL) + hbox.Add(self.label1, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL, border=0) + hbox.AddStretchSpacer() + hbox.Add(self.indexField, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL, border=0) + hbox.Add( + self.label2, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=3 + ) + hbox.AddStretchSpacer() + hbox.Add(self.label3, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL, border=0) + vbox.Add(hbox, proportion=0, flag=wx.EXPAND, border=0) + + hbox = wx.BoxSizer(wx.HORIZONTAL) + hbox.Add(self.slider, proportion=1, flag=wx.EXPAND, border=0) + vbox.Add(hbox, proportion=0, flag=wx.EXPAND, border=0) + + self._setTemporalType() + self.SetSizerAndFit(vbox) + + def _setTemporalType(self): + sizer = self.indexField.GetContainingSizer() + # sizer.Show(self.indexField, False) # TODO: what to do? + sizer.Show(self.indexField, self.temporalType == TemporalType.RELATIVE) + self.Layout() + + def SetTemporalType(self, mode): + self.temporalType = mode + self._setTemporalType() + + def _setFrames(self, timeLabels): + self.timeLabels = timeLabels + self.framesCount = len(timeLabels) + if self.framesCount > 1: + self.slider.SetRange(0, self.framesCount - 1) + self.EnableSlider(True) + else: + self.EnableSlider(False) + self._setLabel() + # TODO: fix setting index values, until then: + self.indexField.Disable() + + def _setLabel(self): + if self.timeLabels: + if self.temporalType == TemporalType.ABSOLUTE: + start = self.timeLabels[0][0] + self.label1.SetLabel(start) + if self.timeLabels[-1][1]: + end = self.timeLabels[-1][1] + else: + end = self.timeLabels[-1][0] + self.label3.SetLabel(end) + else: + unit = self.timeLabels[0][2] + start = self.timeLabels[0][0] + self.label1.SetLabel(start) + if self.timeLabels[-1][1]: + end = self.timeLabels[-1][1] + else: + end = self.timeLabels[-1][0] + end = "%(end)s %(unit)s" % {"end": end, "unit": unit} + self.label3.SetLabel(end) + + self.label2Length = len(start) + self._updateFrameIndex(0) + + else: + self.label1.SetLabel("") + self.label2.SetLabel("") + self.label3.SetLabel("") + self.Layout() + + def _updateFrameIndex(self, index): + start = self.timeLabels[index][0] + if self.timeLabels[index][1]: # interval + if self.temporalType == TemporalType.ABSOLUTE: + label = _("%(from)s %(dash)s %(to)s") % { + "from": start, + "dash": "\u2013", + "to": self.timeLabels[index][1], + } + else: + label = _("to %(to)s") % {"to": self.timeLabels[index][1]} + else: + label = start if self.temporalType == TemporalType.ABSOLUTE else "" + self.label2.SetLabel(label) + if self.temporalType == TemporalType.RELATIVE: + self.indexField.SetValue(start) + if len(label) != self.label2Length: + self.label2Length = len(label) + self.Layout() diff --git a/gui/wxpython/main_window/frame.py b/gui/wxpython/main_window/frame.py index 34799f499c8..dfb2c5b7a25 100644 --- a/gui/wxpython/main_window/frame.py +++ b/gui/wxpython/main_window/frame.py @@ -2010,12 +2010,19 @@ def OnIClass(self, event=None, cmd=None): win.Show() def OnAnimationTool(self, event=None, cmd=None): - """Launch Animation tool. See OnIClass documentation.""" - from animation.frame import AnimationFrame + """Launch Animation tool. See OnIClass documentation. - frame = AnimationFrame(parent=self, giface=self._giface) - frame.CentreOnScreen() - frame.Show() + :return: the animation panel, so that a caller can load data into it + """ + from animation.panels import AnimationPanel + + animation_panel = AnimationPanel( + parent=self, giface=self._giface, statusbar=self.statusbar, dockable=True + ) + animation_panel.SetUpPage(self, self.mainnotebook) + + # add animation panel to notebook and make it current + self.mainnotebook.AddPage(animation_panel, _("Animation Tool")) tree = self.GetLayerTree() if tree: @@ -2034,7 +2041,9 @@ def OnAnimationTool(self, event=None, cmd=None): layer.name = ",".join(rasters) layer.cmd = ["d.rast", "map="] layerList.AddLayer(layer) - frame.SetAnimations([layerList, None, None, None]) + animation_panel.SetAnimations([layerList, None, None, None]) + + return animation_panel def OnTimelineTool(self, event=None, cmd=None): """Launch Timeline Tool"""