frame.py 21 KB

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