frame.py 21 KB

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