frame.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. """!
  2. @package animation.frame
  3. @brief Animation frame and different types of sliders
  4. Classes:
  5. - frame::AnimationFrame
  6. - frame::AnimationsPanel
  7. - frame::AnimationSliderBase
  8. - frame::SimpleAnimationSlider
  9. - frame::TimeAnimationSlider
  10. (C) 2013 by the GRASS Development Team
  11. This program is free software under the GNU General Public License
  12. (>=v2). Read the file COPYING that comes with GRASS for details.
  13. @author Anna Petrasova <kratochanna gmail.com>
  14. """
  15. import os
  16. import sys
  17. import wx
  18. import wx.aui
  19. import tempfile
  20. if __name__ == '__main__':
  21. sys.path.append(os.path.join(os.environ['GISBASE'], "etc", "gui", "wxpython"))
  22. import grass.script as gcore
  23. import grass.temporal as tgis
  24. from core import globalvar
  25. from gui_core.widgets import IntegerValidator
  26. from core.gcmd import RunCommand
  27. from core.utils import _
  28. from animation.mapwindow import AnimationWindow
  29. from animation.provider import BitmapProvider, BitmapPool, \
  30. MapFilesPool, CleanUp
  31. from animation.controller import AnimationController
  32. from animation.anim import Animation
  33. from animation.toolbars import MainToolbar, AnimationToolbar, MiscToolbar
  34. from animation.dialogs import SpeedDialog, PreferencesDialog
  35. from animation.utils import Orientation, ReplayMode, TemporalType
  36. MAX_COUNT = 4
  37. TMP_DIR = None
  38. gcore.set_raise_on_error(True)
  39. class AnimationFrame(wx.Frame):
  40. def __init__(self, parent, giface, title=_("Animation tool"),
  41. rasters=None, timeseries=None):
  42. wx.Frame.__init__(self, parent, title=title,
  43. style=wx.DEFAULT_FRAME_STYLE, size=(800, 600))
  44. self._giface = giface
  45. self.SetClientSize(self.GetSize())
  46. self.iconsize = (16, 16)
  47. self.SetIcon(wx.Icon(os.path.join(globalvar.ETCICONDIR, 'grass_map.ico'), wx.BITMAP_TYPE_ICO))
  48. # Make sure the temporal database exists
  49. tgis.init()
  50. global TMP_DIR
  51. TMP_DIR = tempfile.mkdtemp()
  52. self.animations = [Animation() for i in range(MAX_COUNT)]
  53. self.windows = []
  54. self.animationPanel = AnimationsPanel(self, self.windows, initialCount=MAX_COUNT)
  55. bitmapPool = BitmapPool()
  56. mapFilesPool = MapFilesPool()
  57. # create temporal directory and ensure it's deleted after programs ends
  58. # tempDir = tempfile.mkdtemp()
  59. # self.cleanUp = CleanUp(tempDir)
  60. self._progressDlg = None
  61. self._progressDlgMax = None
  62. self.provider = BitmapProvider(bitmapPool=bitmapPool,
  63. mapFilesPool=mapFilesPool, tempDir=TMP_DIR)
  64. self.animationSliders = {}
  65. self.animationSliders['nontemporal'] = SimpleAnimationSlider(self)
  66. self.animationSliders['temporal'] = TimeAnimationSlider(self)
  67. self.controller = AnimationController(frame=self,
  68. sliders=self.animationSliders,
  69. animations=self.animations,
  70. mapwindows=self.windows,
  71. provider=self.provider,
  72. bitmapPool=bitmapPool,
  73. mapFilesPool=mapFilesPool)
  74. for win in self.windows:
  75. win.Bind(wx.EVT_SIZE, self.FrameSizeChanged)
  76. self.provider.mapsLoaded.connect(lambda: self.SetStatusText(''))
  77. self.provider.renderingStarted.connect(self._showRenderingProgress)
  78. self.provider.renderingContinues.connect(self._updateProgress)
  79. self.provider.renderingFinished.connect(self._closeProgress)
  80. self.provider.compositionStarted.connect(self._showRenderingProgress)
  81. self.provider.compositionContinues.connect(self._updateProgress)
  82. self.provider.compositionFinished.connect(self._closeProgress)
  83. self.InitStatusbar()
  84. self._mgr = wx.aui.AuiManager(self)
  85. # toolbars
  86. self.toolbars = {}
  87. tb = ['miscToolbar', 'animationToolbar', 'mainToolbar']
  88. if sys.platform == 'win32':
  89. tb.reverse()
  90. for toolb in tb:
  91. self._addToolbar(toolb)
  92. self._addPanes()
  93. self._mgr.Update()
  94. self.dialogs = dict()
  95. self.dialogs['speed'] = None
  96. self.dialogs['preferences'] = None
  97. self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
  98. def InitStatusbar(self):
  99. """!Init statusbar."""
  100. self.CreateStatusBar(number=1, style=0)
  101. def _addPanes(self):
  102. self._mgr.AddPane(self.animationPanel,
  103. wx.aui.AuiPaneInfo().CentrePane().
  104. Name('animPanel').CentrePane().CaptionVisible(False).PaneBorder(False).
  105. Floatable(False).BestSize((-1, -1)).
  106. CloseButton(False).DestroyOnClose(True).Layer(0))
  107. for name, slider in self.animationSliders.iteritems():
  108. self._mgr.AddPane(slider, wx.aui.AuiPaneInfo().PaneBorder(False).Name('slider_' + name).
  109. Layer(1).CaptionVisible(False).BestSize(slider.GetBestSize()).
  110. DestroyOnClose(True).CloseButton(False).Bottom())
  111. self._mgr.GetPane('slider_' + name).Hide()
  112. def _addToolbar(self, name):
  113. """!Add defined toolbar to the window
  114. Currently known toolbars are:
  115. - 'mainToolbar' - data management
  116. - 'animationToolbar' - animation controls
  117. - 'miscToolbar' - help, close
  118. """
  119. if name == "mainToolbar":
  120. self.toolbars[name] = MainToolbar(self)
  121. self._mgr.AddPane(self.toolbars[name],
  122. wx.aui.AuiPaneInfo().
  123. Name('mainToolbar').Caption(_("Main Toolbar")).
  124. ToolbarPane().Top().
  125. LeftDockable(False).RightDockable(False).
  126. BottomDockable(True).TopDockable(True).
  127. CloseButton(False).Layer(2).Row(1).
  128. BestSize((self.toolbars['mainToolbar'].GetBestSize())))
  129. elif name == 'animationToolbar':
  130. self.toolbars[name] = AnimationToolbar(self)
  131. self._mgr.AddPane(self.toolbars[name],
  132. wx.aui.AuiPaneInfo().
  133. Name('animationToolbar').Caption(_("Animation Toolbar")).
  134. ToolbarPane().Top().
  135. LeftDockable(False).RightDockable(False).
  136. BottomDockable(True).TopDockable(True).
  137. CloseButton(False).Layer(2).Row(1).
  138. BestSize((self.toolbars['animationToolbar'].GetBestSize())))
  139. self.controller.SetAnimationToolbar(self.toolbars['animationToolbar'])
  140. elif name == 'miscToolbar':
  141. self.toolbars[name] = MiscToolbar(self)
  142. self._mgr.AddPane(self.toolbars[name],
  143. wx.aui.AuiPaneInfo().
  144. Name('miscToolbar').Caption(_("Misc Toolbar")).
  145. ToolbarPane().Top().
  146. LeftDockable(False).RightDockable(False).
  147. BottomDockable(True).TopDockable(True).
  148. CloseButton(False).Layer(2).Row(1).
  149. BestSize((self.toolbars['miscToolbar'].GetBestSize())))
  150. def SetAnimations(self, layerLists):
  151. """!Set animation data
  152. @param layerLists list of layerLists
  153. """
  154. self.controller.SetAnimations(layerLists)
  155. def OnAddAnimation(self, event):
  156. self.controller.AddAnimation()
  157. def AddWindow(self, index):
  158. self.animationPanel.AddWindow(index)
  159. def RemoveWindow(self, index):
  160. self.animationPanel.RemoveWindow(index)
  161. def IsWindowShown(self, index):
  162. return self.animationPanel.IsWindowShown(index)
  163. def OnEditAnimation(self, event):
  164. self.controller.EditAnimations()
  165. def SetSlider(self, name):
  166. if name == 'nontemporal':
  167. self._mgr.GetPane('slider_nontemporal').Show()
  168. self._mgr.GetPane('slider_temporal').Hide()
  169. elif name == 'temporal':
  170. self._mgr.GetPane('slider_temporal').Show()
  171. self._mgr.GetPane('slider_nontemporal').Hide()
  172. else:
  173. self._mgr.GetPane('slider_temporal').Hide()
  174. self._mgr.GetPane('slider_nontemporal').Hide()
  175. self._mgr.Update()
  176. def OnPlayForward(self, event):
  177. self.controller.SetOrientation(Orientation.FORWARD)
  178. self.controller.StartAnimation()
  179. def OnPlayBack(self, event):
  180. self.controller.SetOrientation(Orientation.BACKWARD)
  181. self.controller.StartAnimation()
  182. def OnPause(self, event):
  183. self.controller.PauseAnimation(paused=event.IsChecked())
  184. def OnStop(self, event):
  185. self.controller.EndAnimation()
  186. def OnOneDirectionReplay(self, event):
  187. if event.IsChecked():
  188. mode = ReplayMode.REPEAT
  189. else:
  190. mode = ReplayMode.ONESHOT
  191. self.controller.SetReplayMode(mode)
  192. def OnBothDirectionReplay(self, event):
  193. if event.IsChecked():
  194. mode = ReplayMode.REVERSE
  195. else:
  196. mode = ReplayMode.ONESHOT
  197. self.controller.SetReplayMode(mode)
  198. def OnAdjustSpeed(self, event):
  199. win = self.dialogs['speed']
  200. if win:
  201. win.SetTemporalMode(self.controller.GetTemporalMode())
  202. win.SetTimeGranularity(self.controller.GetTimeGranularity())
  203. win.InitTimeSpin(self.controller.GetTimeTick())
  204. if win.IsShown():
  205. win.SetFocus()
  206. else:
  207. win.Show()
  208. else: # start
  209. win = SpeedDialog(self, temporalMode=self.controller.GetTemporalMode(),
  210. timeGranularity=self.controller.GetTimeGranularity(),
  211. initialSpeed=self.controller.timeTick)
  212. win.CenterOnParent()
  213. self.dialogs['speed'] = win
  214. win.speedChanged.connect(self.ChangeSpeed)
  215. win.Show()
  216. def ChangeSpeed(self, ms):
  217. self.controller.timeTick = ms
  218. def Reload(self, event):
  219. self.controller.Reload()
  220. def _showRenderingProgress(self, count):
  221. # the message is not really visible, it's there for the initial dlg size
  222. self._progressDlg = wx.ProgressDialog(title=_("Loading data"),
  223. message="Loading data started, please be patient.",
  224. maximum=count,
  225. parent=self,
  226. style=wx.PD_CAN_ABORT | wx.PD_APP_MODAL |
  227. wx.PD_AUTO_HIDE | wx.PD_SMOOTH)
  228. self._progressDlgMax = count
  229. def _updateProgress(self, current, text):
  230. text += _(" ({} out of {})").format(current, self._progressDlgMax)
  231. keepGoing, skip = self._progressDlg.Update(current, text)
  232. if not keepGoing:
  233. self.provider.RequestStopRendering()
  234. def _closeProgress(self):
  235. self._progressDlg.Destroy()
  236. self._progressDlg = None
  237. def OnExportAnimation(self, event):
  238. self.controller.Export()
  239. def FrameSizeChanged(self, event):
  240. maxWidth = maxHeight = 0
  241. for win in self.windows:
  242. w, h = win.GetClientSize()
  243. if w >= maxWidth and h >= maxHeight:
  244. maxWidth, maxHeight = w, h
  245. self.provider.WindowSizeChanged(maxWidth, maxHeight)
  246. event.Skip()
  247. def OnPreferences(self, event):
  248. if not self.dialogs['preferences']:
  249. dlg = PreferencesDialog(parent=self, giface=self._giface)
  250. self.dialogs['preferences'] = dlg
  251. dlg.CenterOnParent()
  252. self.dialogs['preferences'].ShowModal()
  253. def OnHelp(self, event):
  254. RunCommand('g.manual',
  255. quiet=True,
  256. entry='wxGUI.animation')
  257. def OnCloseWindow(self, event):
  258. if self.controller.timer.IsRunning():
  259. self.controller.timer.Stop()
  260. CleanUp(TMP_DIR)()
  261. self.Destroy()
  262. def __del__(self):
  263. """!It might not be called, therefore we try to clean it all in OnCloseWindow."""
  264. if hasattr(self, 'controller') and hasattr(self.controller, 'timer'):
  265. if self.controller.timer.IsRunning():
  266. self.controller.timer.Stop()
  267. CleanUp(TMP_DIR)()
  268. class AnimationsPanel(wx.Panel):
  269. def __init__(self, parent, windows, initialCount=4):
  270. wx.Panel.__init__(self, parent, id=wx.ID_ANY, style=wx.NO_BORDER)
  271. self.shown = []
  272. self.count = initialCount
  273. self.mainSizer = wx.FlexGridSizer(rows=2, hgap=0, vgap=0)
  274. for i in range(initialCount):
  275. w = AnimationWindow(self)
  276. windows.append(w)
  277. self.mainSizer.Add(item=w, proportion=1, flag=wx.EXPAND)
  278. self.mainSizer.AddGrowableCol(0)
  279. self.mainSizer.AddGrowableCol(1)
  280. self.mainSizer.AddGrowableRow(0)
  281. self.mainSizer.AddGrowableRow(1)
  282. self.windows = windows
  283. self.SetSizerAndFit(self.mainSizer)
  284. for i in range(initialCount):
  285. self.mainSizer.Hide(windows[i])
  286. self.Layout()
  287. def AddWindow(self, index):
  288. if len(self.shown) == self.count:
  289. return
  290. self.mainSizer.Show(self.windows[index])
  291. self.shown.append(index)
  292. self.Layout()
  293. def RemoveWindow(self, index):
  294. if len(self.shown) == 0:
  295. return
  296. self.mainSizer.Hide(self.windows[index])
  297. self.shown.remove(index)
  298. self.Layout()
  299. def IsWindowShown(self, index):
  300. return self.mainSizer.IsShown(self.windows[index])
  301. class AnimationSliderBase(wx.Panel):
  302. def __init__(self, parent):
  303. wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
  304. self.label1 = wx.StaticText(self, id=wx.ID_ANY)
  305. self.slider = wx.Slider(self, id=wx.ID_ANY, style=wx.SL_HORIZONTAL)
  306. self.indexField = wx.TextCtrl(self, id=wx.ID_ANY, size=(40, -1),
  307. style=wx.TE_PROCESS_ENTER | wx.TE_RIGHT,
  308. validator=IntegerValidator())
  309. self.callbackSliderChanging = None
  310. self.callbackSliderChanged = None
  311. self.callbackFrameIndexChanged = None
  312. self.framesCount = 0
  313. self.enable = True
  314. self.slider.Bind(wx.EVT_SPIN, self.OnSliderChanging)
  315. self.slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.OnSliderChanged)
  316. self.indexField.Bind(wx.EVT_TEXT_ENTER, self.OnFrameIndexChanged)
  317. def UpdateFrame(self, index):
  318. self._updateFrameIndex(index)
  319. if not self.enable:
  320. return
  321. self.slider.SetValue(index)
  322. def _updateFrameIndex(self, index):
  323. raise NotImplementedError
  324. def OnFrameIndexChanged(self, event):
  325. self._onFrameIndexChanged(event)
  326. def SetFrames(self, frames):
  327. self._setFrames(frames)
  328. def _setFrames(self, frames):
  329. raise NotImplementedError
  330. def SetCallbackSliderChanging(self, callback):
  331. self.callbackSliderChanging = callback
  332. def SetCallbackSliderChanged(self, callback):
  333. self.callbackSliderChanged = callback
  334. def SetCallbackFrameIndexChanged(self, callback):
  335. self.callbackFrameIndexChanged = callback
  336. def EnableSlider(self, enable=True):
  337. if enable and self.framesCount <= 1:
  338. enable = False # we don't want to enable it
  339. self.enable = enable
  340. self.slider.Enable(enable)
  341. self.indexField.Enable(enable)
  342. def OnSliderChanging(self, event):
  343. self.callbackSliderChanging(event.GetInt())
  344. def OnSliderChanged(self, event):
  345. self.callbackSliderChanged()
  346. def _onFrameIndexChanged(self, event):
  347. index = self.indexField.GetValue()
  348. index = self._validate(index)
  349. if index is not None:
  350. self.slider.SetValue(index)
  351. self.callbackFrameIndexChanged(index)
  352. def _validate(self, index):
  353. try:
  354. index = int(index)
  355. except ValueError:
  356. index = self.slider.GetValue()
  357. self.indexField.SetValue(str(index + 1))
  358. return None
  359. start, end = self.slider.GetRange()
  360. index -= 1
  361. if index > end:
  362. index = end
  363. self.indexField.SetValue(str(end + 1))
  364. elif index < start:
  365. index = start
  366. self.indexField.SetValue(str(start + 1))
  367. return index
  368. class SimpleAnimationSlider(AnimationSliderBase):
  369. def __init__(self, parent):
  370. AnimationSliderBase.__init__(self, parent)
  371. self._setLabel()
  372. self._doLayout()
  373. def _doLayout(self):
  374. hbox = wx.BoxSizer(wx.HORIZONTAL)
  375. hbox.Add(item=self.indexField, proportion=0,
  376. flag=wx.ALIGN_CENTER | wx.LEFT, border=5)
  377. hbox.Add(item=self.label1, proportion=0,
  378. flag=wx.ALIGN_CENTER | wx.LEFT | wx.RIGHT, border=5)
  379. hbox.Add(item=self.slider, proportion=1, flag=wx.ALIGN_CENTER | wx.EXPAND, border=0)
  380. self.SetSizerAndFit(hbox)
  381. def _setFrames(self, count):
  382. self.framesCount = count
  383. if self.framesCount > 1:
  384. self.slider.SetRange(0, self.framesCount - 1)
  385. self.EnableSlider(True)
  386. else:
  387. self.EnableSlider(False)
  388. self._setLabel()
  389. def _setLabel(self):
  390. label = "/ %(framesCount)s" % {'framesCount': self.framesCount}
  391. self.label1.SetLabel(label)
  392. self.Layout()
  393. def _updateFrameIndex(self, index):
  394. self.indexField.SetValue(str(index + 1))
  395. class TimeAnimationSlider(AnimationSliderBase):
  396. def __init__(self, parent):
  397. AnimationSliderBase.__init__(self, parent)
  398. self.timeLabels = []
  399. self.label2 = wx.StaticText(self, id=wx.ID_ANY)
  400. self.label3 = wx.StaticText(self, id=wx.ID_ANY)
  401. self.label2Length = 0
  402. self.temporalType = TemporalType.RELATIVE
  403. self._setLabel()
  404. self._doLayout()
  405. def _doLayout(self):
  406. vbox = wx.BoxSizer(wx.VERTICAL)
  407. hbox = wx.BoxSizer(wx.HORIZONTAL)
  408. hbox.Add(item=self.label1, proportion=0,
  409. flag=wx.ALIGN_CENTER_VERTICAL, border=0)
  410. hbox.AddStretchSpacer()
  411. hbox.Add(item=self.indexField, proportion=0,
  412. flag=wx.ALIGN_CENTER_VERTICAL, border=0)
  413. hbox.Add(item=self.label2, proportion=0,
  414. flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=3)
  415. hbox.AddStretchSpacer()
  416. hbox.Add(item=self.label3, proportion=0,
  417. flag=wx.ALIGN_CENTER_VERTICAL, border=0)
  418. vbox.Add(item=hbox, proportion=0, flag=wx.EXPAND, border=0)
  419. hbox = wx.BoxSizer(wx.HORIZONTAL)
  420. hbox.Add(item=self.slider, proportion=1, flag=wx.ALIGN_CENTER | wx.EXPAND, border=0)
  421. vbox.Add(item=hbox, proportion=0, flag=wx.EXPAND, border=0)
  422. self._setTemporalType()
  423. self.SetSizerAndFit(vbox)
  424. def _setTemporalType(self):
  425. sizer = self.indexField.GetContainingSizer()
  426. # sizer.Show(self.indexField, False) # TODO: what to do?
  427. sizer.Show(self.indexField, self.temporalType == TemporalType.RELATIVE)
  428. self.Layout()
  429. def SetTemporalType(self, mode):
  430. self.temporalType = mode
  431. self._setTemporalType()
  432. def _setFrames(self, timeLabels):
  433. self.timeLabels = timeLabels
  434. self.framesCount = len(timeLabels)
  435. if self.framesCount > 1:
  436. self.slider.SetRange(0, self.framesCount - 1)
  437. self.EnableSlider(True)
  438. else:
  439. self.EnableSlider(False)
  440. self._setLabel()
  441. # TODO: fix setting index values, until then:
  442. self.indexField.Disable()
  443. def _setLabel(self):
  444. if self.timeLabels:
  445. if self.temporalType == TemporalType.ABSOLUTE:
  446. start = self.timeLabels[0][0]
  447. self.label1.SetLabel(start)
  448. if self.timeLabels[-1][1]:
  449. end = self.timeLabels[-1][1]
  450. else:
  451. end = self.timeLabels[-1][0]
  452. self.label3.SetLabel(end)
  453. else:
  454. unit = self.timeLabels[0][2]
  455. start = self.timeLabels[0][0]
  456. self.label1.SetLabel(start)
  457. if self.timeLabels[-1][1]:
  458. end = self.timeLabels[-1][1]
  459. else:
  460. end = self.timeLabels[-1][0]
  461. end = "%(end)s %(unit)s" % {'end': end, 'unit': unit}
  462. self.label3.SetLabel(end)
  463. self.label2Length = len(start)
  464. self._updateFrameIndex(0)
  465. else:
  466. self.label1.SetLabel("")
  467. self.label2.SetLabel("")
  468. self.label3.SetLabel("")
  469. self.Layout()
  470. def _updateFrameIndex(self, index):
  471. start = self.timeLabels[index][0]
  472. if self.timeLabels[index][1]: # interval
  473. if self.temporalType == TemporalType.ABSOLUTE:
  474. label = _("%(from)s %(dash)s %(to)s") % \
  475. {'from': start, 'dash': u"\u2013", 'to': self.timeLabels[index][1]}
  476. else:
  477. label = _("to %(to)s") % {'to': self.timeLabels[index][1]}
  478. else:
  479. if self.temporalType == TemporalType.ABSOLUTE:
  480. label = start
  481. else:
  482. label = ''
  483. self.label2.SetLabel(label)
  484. if self.temporalType == TemporalType.RELATIVE:
  485. self.indexField.SetValue(start)
  486. if len(label) != self.label2Length:
  487. self.label2Length = len(label)
  488. self.Layout()