123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656 |
- """
- @package animation.frame
- @brief Animation frame and different types of sliders
- Classes:
- - frame::AnimationFrame
- - frame::AnimationsPanel
- - frame::AnimationSliderBase
- - frame::SimpleAnimationSlider
- - frame::TimeAnimationSlider
- (C) 2013 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.
- @author Anna Petrasova <kratochanna gmail.com>
- """
- import os
- import wx
- import wx.aui
- import six
- import grass.script as gcore
- 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
- MAX_COUNT = 4
- TMP_DIR = None
- gcore.set_raise_on_error(True)
- class AnimationFrame(wx.Frame):
- def __init__(
- self, parent, giface, title=_("Animation Tool"), rasters=None, timeseries=None
- ):
- 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_map.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 = gcore.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,
- )
- 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 = dict()
- self.dialogs["speed"] = None
- self.dialogs["preferences"] = None
- 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 six.iteritems(self.animationSliders):
- 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):
- if event.IsChecked():
- mode = ReplayMode.REPEAT
- else:
- mode = ReplayMode.ONESHOT
- self.controller.SetReplayMode(mode)
- def OnBothDirectionReplay(self, event):
- if event.IsChecked():
- mode = ReplayMode.REVERSE
- else:
- mode = 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")
- def OnCloseWindow(self, event):
- if self.controller.timer.IsRunning():
- self.controller.timer.Stop()
- CleanUp(TMP_DIR)()
- self._mgr.UnInit()
- self.Destroy()
- 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:
- if self.temporalType == TemporalType.ABSOLUTE:
- label = start
- else:
- label = ""
- self.label2.SetLabel(label)
- if self.temporalType == TemporalType.RELATIVE:
- self.indexField.SetValue(start)
- if len(label) != self.label2Length:
- self.label2Length = len(label)
- self.Layout()
|