controller.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. """!
  2. @package animation.controller
  3. @brief Animations management
  4. Classes:
  5. - controller::AnimationController
  6. (C) 2013 by the GRASS Development Team
  7. This program is free software under the GNU General Public License
  8. (>=v2). Read the file COPYING that comes with GRASS for details.
  9. @author Anna Petrasova <kratochanna gmail.com>
  10. """
  11. import os
  12. import wx
  13. from core.gcmd import GException, GError, GMessage
  14. from core.utils import _
  15. from grass.imaging import writeAvi, writeGif, writeIms, writeSwf
  16. from animation.temporal_manager import TemporalManager
  17. from animation.dialogs import InputDialog, EditDialog, ExportDialog
  18. from animation.utils import TemporalMode, Orientation, RenderText, WxImageToPil, \
  19. sampleCmdMatrixAndCreateNames, layerListToCmdsMatrix, HashCmds
  20. from animation.data import AnimationData
  21. class AnimationController(wx.EvtHandler):
  22. def __init__(self, frame, sliders, animations, mapwindows, provider, bitmapPool, mapFilesPool):
  23. wx.EvtHandler.__init__(self)
  24. self.mapwindows = mapwindows
  25. self.frame = frame
  26. self.sliders = sliders
  27. self.slider = self.sliders['temporal']
  28. self.animationToolbar = None
  29. self.temporalMode = None
  30. self.animationData = []
  31. self.timer = wx.Timer(self, id=wx.NewId())
  32. self.animations = animations
  33. self.bitmapPool = bitmapPool
  34. self.mapFilesPool = mapFilesPool
  35. self.bitmapProvider = provider
  36. for anim, win in zip(self.animations, self.mapwindows):
  37. anim.SetCallbackUpdateFrame(
  38. lambda index, dataId, win=win: self.UpdateFrame(index, win, dataId))
  39. anim.SetCallbackEndAnimation(
  40. lambda index, dataId, win=win: self.UpdateFrameEnd(index, win, dataId))
  41. anim.SetCallbackOrientationChanged(self.OrientationChangedInReverseMode)
  42. for slider in self.sliders.values():
  43. slider.SetCallbackSliderChanging(self.SliderChanging)
  44. slider.SetCallbackSliderChanged(self.SliderChanged)
  45. slider.SetCallbackFrameIndexChanged(self.ChangeFrame)
  46. self.runAfterReleasingSlider = None
  47. self.temporalManager = TemporalManager()
  48. self.Bind(wx.EVT_TIMER, self.OnTimerTick, self.timer)
  49. self.timeTick = 200
  50. self._dialogs = {}
  51. def SetAnimationToolbar(self, toolbar):
  52. self.animationToolbar = toolbar
  53. def GetTimeTick(self):
  54. return self._timeTick
  55. def SetTimeTick(self, value):
  56. self._timeTick = value
  57. if self.timer.IsRunning():
  58. self.timer.Stop()
  59. self.timer.Start(self._timeTick)
  60. self.DisableSliderIfNeeded()
  61. timeTick = property(fget=GetTimeTick, fset=SetTimeTick)
  62. def OnTimerTick(self, event):
  63. for anim in self.animations:
  64. anim.Update()
  65. def StartAnimation(self):
  66. # if self.timer.IsRunning():
  67. # self.timer.Stop()
  68. for anim in self.animations:
  69. if self.timer.IsRunning():
  70. anim.NextFrameIndex()
  71. anim.Start()
  72. if not self.timer.IsRunning():
  73. self.timer.Start(self.timeTick)
  74. self.DisableSliderIfNeeded()
  75. def PauseAnimation(self, paused):
  76. if paused:
  77. if self.timer.IsRunning():
  78. self.timer.Stop()
  79. self.DisableSliderIfNeeded()
  80. else:
  81. if not self.timer.IsRunning():
  82. self.timer.Start(self.timeTick)
  83. self.DisableSliderIfNeeded()
  84. for anim in self.animations:
  85. anim.Pause(paused)
  86. def EndAnimation(self):
  87. if self.timer.IsRunning():
  88. self.timer.Stop()
  89. self.DisableSliderIfNeeded()
  90. for anim in self.animations:
  91. anim.Stop()
  92. def UpdateFrameEnd(self, index, win, dataId):
  93. if self.timer.IsRunning():
  94. self.timer.Stop()
  95. self.DisableSliderIfNeeded()
  96. self.animationToolbar.Stop()
  97. self.UpdateFrame(index, win, dataId)
  98. def UpdateFrame(self, index, win, dataId):
  99. bitmap = self.bitmapProvider.GetBitmap(dataId)
  100. if dataId is None:
  101. dataId = ''
  102. win.DrawBitmap(bitmap)
  103. # self.frame.SetStatusText(dataId)
  104. self.slider.UpdateFrame(index)
  105. def SliderChanging(self, index):
  106. if self.runAfterReleasingSlider is None:
  107. self.runAfterReleasingSlider = self.timer.IsRunning()
  108. self.PauseAnimation(True)
  109. self.ChangeFrame(index)
  110. def SliderChanged(self):
  111. if self.runAfterReleasingSlider:
  112. self.PauseAnimation(False)
  113. self.runAfterReleasingSlider = None
  114. def ChangeFrame(self, index):
  115. for anim in self.animations:
  116. anim.FrameChangedFromOutside(index)
  117. def DisableSliderIfNeeded(self):
  118. if self.timer.IsRunning() and self._timeTick < 100:
  119. self.slider.EnableSlider(False)
  120. else:
  121. self.slider.EnableSlider(True)
  122. def OrientationChangedInReverseMode(self, mode):
  123. if mode == Orientation.FORWARD:
  124. self.animationToolbar.PlayForward()
  125. elif mode == Orientation.BACKWARD:
  126. self.animationToolbar.PlayBack()
  127. def SetReplayMode(self, mode):
  128. for anim in self.animations:
  129. anim.replayMode = mode
  130. def SetOrientation(self, mode):
  131. for anim in self.animations:
  132. anim.orientation = mode
  133. def SetTemporalMode(self, mode):
  134. self._temporalMode = mode
  135. def GetTemporalMode(self):
  136. return self._temporalMode
  137. temporalMode = property(fget=GetTemporalMode, fset=SetTemporalMode)
  138. def GetTimeGranularity(self):
  139. if self.temporalMode == TemporalMode.TEMPORAL:
  140. return self.temporalManager.GetGranularity()
  141. return None
  142. def EditAnimations(self):
  143. # running = False
  144. # if self.timer.IsRunning():
  145. # running = True
  146. self.EndAnimation()
  147. dlg = EditDialog(parent=self.frame, evalFunction=self.EvaluateInput,
  148. animationData=self.animationData, maxAnimations=len(self.animations))
  149. if dlg.ShowModal() == wx.ID_CANCEL:
  150. dlg.Destroy()
  151. return
  152. self.animationData, self.temporalMode, self.temporalManager = dlg.GetResult()
  153. dlg.Destroy()
  154. self._setAnimations()
  155. def AddAnimation(self):
  156. # check if we can add more animations
  157. found = False
  158. indices = [anim.windowIndex for anim in self.animationData]
  159. for windowIndex in range(len(self.animations)):
  160. if windowIndex not in indices:
  161. found = True
  162. break
  163. if not found:
  164. GMessage(parent=self.frame,
  165. message=_("Maximum number of animations is %s.") % len(self.animations))
  166. return
  167. # running = False
  168. # if self.timer.IsRunning():
  169. # running = True
  170. self.EndAnimation()
  171. # self.PauseAnimation(True)
  172. animData = AnimationData()
  173. # number of active animations
  174. animationIndex = len([anim for anim in self.animations if anim.IsActive()])
  175. animData.SetDefaultValues(windowIndex, animationIndex)
  176. dlg = InputDialog(parent=self.frame, mode='add', animationData=animData)
  177. if dlg.ShowModal() == wx.ID_CANCEL:
  178. dlg.Destroy()
  179. return
  180. dlg.Destroy()
  181. # check compatibility
  182. if animData.windowIndex in indices:
  183. GMessage(parent=self.frame, message=_("More animations are using one window."
  184. " Please select different window for each animation."))
  185. return
  186. try:
  187. temporalMode, tempManager = self.EvaluateInput(self.animationData + [animData])
  188. except GException, e:
  189. GError(parent=self.frame, message=e.value, showTraceback=False)
  190. return
  191. # if ok, set temporal mode
  192. self.temporalMode = temporalMode
  193. self.temporalManager = tempManager
  194. # add data
  195. windowIndex = animData.windowIndex
  196. self.animationData.append(animData)
  197. self._setAnimations()
  198. def SetAnimations(self, layerLists):
  199. """!Set animation data directly.
  200. @param layerLists list of layerLists
  201. """
  202. try:
  203. animationData = []
  204. for i in range(len(self.animations)):
  205. if layerLists[i]:
  206. anim = AnimationData()
  207. anim.SetDefaultValues(i, i)
  208. anim.SetLayerList(layerLists[i])
  209. animationData.append(anim)
  210. except (GException, ValueError, IOError) as e:
  211. GError(parent=self.frame, message=str(e),
  212. showTraceback=False, caption=_("Invalid input"))
  213. return
  214. try:
  215. temporalMode, tempManager = self.EvaluateInput(animationData)
  216. except GException, e:
  217. GError(parent=self.frame, message=e.value, showTraceback=False)
  218. return
  219. self.animationData = animationData
  220. self.temporalManager = tempManager
  221. self.temporalMode = temporalMode
  222. self._setAnimations()
  223. def _setAnimations(self):
  224. indices = [anim.windowIndex for anim in self.animationData]
  225. self._updateWindows(activeIndices=indices)
  226. if self.temporalMode == TemporalMode.TEMPORAL:
  227. timeLabels, mapNamesDict = self.temporalManager.GetLabelsAndMaps()
  228. else:
  229. timeLabels, mapNamesDict = None, None
  230. for anim in self.animationData:
  231. if anim.viewMode == '2d':
  232. anim.cmdMatrix = layerListToCmdsMatrix(anim.layerList)
  233. else:
  234. anim.cmdMatrix = [(cmd,) for cmd in anim.GetNvizCommands()['commands']]
  235. self._updateSlider(timeLabels=timeLabels)
  236. self._updateAnimations(activeIndices=indices, mapNamesDict=mapNamesDict)
  237. wx.Yield()
  238. self._updateBitmapData()
  239. # if running:
  240. # self.PauseAnimation(False)
  241. # # self.StartAnimation()
  242. # else:
  243. self.EndAnimation()
  244. def _updateSlider(self, timeLabels=None):
  245. if self.temporalMode == TemporalMode.NONTEMPORAL:
  246. self.frame.SetSlider('nontemporal')
  247. self.slider = self.sliders['nontemporal']
  248. frameCount = self.animationData[0].mapCount
  249. self.slider.SetFrames(frameCount)
  250. elif self.temporalMode == TemporalMode.TEMPORAL:
  251. self.frame.SetSlider('temporal')
  252. self.slider = self.sliders['temporal']
  253. self.slider.SetTemporalType(self.temporalManager.temporalType)
  254. self.slider.SetFrames(timeLabels)
  255. else:
  256. self.frame.SetSlider(None)
  257. self.slider = None
  258. def _updateAnimations(self, activeIndices, mapNamesDict=None):
  259. if self.temporalMode == TemporalMode.NONTEMPORAL:
  260. for i in range(len(self.animations)):
  261. if i not in activeIndices:
  262. self.animations[i].SetActive(False)
  263. continue
  264. anim = [anim for anim in self.animationData if anim.windowIndex == i][0]
  265. self.animations[i].SetFrames([HashCmds(cmdList) for cmdList in anim.cmdMatrix])
  266. self.animations[i].SetActive(True)
  267. else:
  268. for i in range(len(self.animations)):
  269. if i not in activeIndices:
  270. self.animations[i].SetActive(False)
  271. continue
  272. anim = [anim for anim in self.animationData if anim.windowIndex == i][0]
  273. identifiers = sampleCmdMatrixAndCreateNames(anim.cmdMatrix,
  274. mapNamesDict[anim.firstStdsNameType[0]])
  275. self.animations[i].SetFrames(identifiers)
  276. self.animations[i].SetActive(True)
  277. def _updateWindows(self, activeIndices):
  278. # add or remove window
  279. for windowIndex in range(len(self.animations)):
  280. if not self.frame.IsWindowShown(windowIndex) and windowIndex in activeIndices:
  281. self.frame.AddWindow(windowIndex)
  282. elif self.frame.IsWindowShown(windowIndex) and windowIndex not in activeIndices:
  283. self.frame.RemoveWindow(windowIndex)
  284. def _updateBitmapData(self):
  285. # unload previous data
  286. self.bitmapProvider.Unload()
  287. # load new data
  288. for animData in self.animationData:
  289. if animData.viewMode == '2d':
  290. self._set2DData(animData)
  291. else:
  292. self._load3DData(animData)
  293. self._loadLegend(animData)
  294. self.bitmapProvider.Load(nprocs=4)
  295. # clear pools
  296. self.bitmapPool.Clear()
  297. self.mapFilesPool.Clear()
  298. def _set2DData(self, animationData):
  299. opacities = [layer.opacity for layer in animationData.layerList if layer.active]
  300. self.bitmapProvider.SetCmds(animationData.cmdMatrix, opacities)
  301. def _load3DData(self, animationData):
  302. nviz = animationData.GetNvizCommands()
  303. self.bitmapProvider.SetCmds3D(nviz['commands'], nviz['region'])
  304. def _loadLegend(self, animationData):
  305. if animationData.legendCmd:
  306. try:
  307. bitmap = self.bitmapProvider.LoadOverlay(animationData.legendCmd)
  308. try:
  309. from PIL import Image
  310. for param in animationData.legendCmd:
  311. if param.startswith('at'):
  312. b, t, l, r = param.split('=')[1].split(',')
  313. x, y = float(l) / 100., 1 - float(t) / 100.
  314. break
  315. except ImportError:
  316. x, y = 0, 0
  317. self.mapwindows[animationData.windowIndex].SetOverlay(bitmap, x, y)
  318. except GException:
  319. GError(message=_("Failed to display legend."))
  320. else:
  321. self.mapwindows[animationData.windowIndex].ClearOverlay()
  322. def EvaluateInput(self, animationData):
  323. stds = 0
  324. maps = 0
  325. mapCount = set()
  326. tempManager = None
  327. windowIndex = []
  328. for anim in animationData:
  329. for layer in anim.layerList:
  330. if layer.active and hasattr(layer, 'maps'):
  331. if layer.mapType in ('strds', 'stvds'):
  332. stds += 1
  333. else:
  334. maps += 1
  335. mapCount.add(len(layer.maps))
  336. windowIndex.append(anim.windowIndex)
  337. if maps and stds:
  338. temporalMode = TemporalMode.NONTEMPORAL
  339. elif maps:
  340. temporalMode = TemporalMode.NONTEMPORAL
  341. elif stds:
  342. temporalMode = TemporalMode.TEMPORAL
  343. else:
  344. temporalMode = None
  345. if temporalMode == TemporalMode.NONTEMPORAL:
  346. if len(mapCount) > 1:
  347. raise GException(_("Inconsistent number of maps, please check input data."))
  348. elif temporalMode == TemporalMode.TEMPORAL:
  349. tempManager = TemporalManager()
  350. # these raise GException:
  351. for anim in animationData:
  352. tempManager.AddTimeSeries(*anim.firstStdsNameType)
  353. message = tempManager.EvaluateInputData()
  354. if message:
  355. GMessage(parent=self.frame, message=message)
  356. return temporalMode, tempManager
  357. def Reload(self):
  358. self.EndAnimation()
  359. self.bitmapProvider.Load(force=True)
  360. self.EndAnimation()
  361. def Export(self):
  362. if not self.animationData:
  363. GMessage(parent=self.frame, message=_("No animation to export."))
  364. return
  365. if 'export' in self._dialogs:
  366. self._dialogs['export'].Show()
  367. self._dialogs['export'].Raise()
  368. else:
  369. dlg = ExportDialog(self.frame, temporal=self.temporalMode,
  370. timeTick=self.timeTick)
  371. dlg.doExport.connect(self._export)
  372. self._dialogs['export'] = dlg
  373. dlg.Show()
  374. def _export(self, exportInfo, decorations):
  375. size = self.frame.animationPanel.GetSize()
  376. if self.temporalMode == TemporalMode.TEMPORAL:
  377. timeLabels, mapNamesDict = self.temporalManager.GetLabelsAndMaps()
  378. frameCount = len(timeLabels)
  379. else:
  380. frameCount = self.animationData[0].mapCount # should be the same for all
  381. animWinSize = []
  382. animWinPos = []
  383. animWinIndex = []
  384. legends = [anim.legendCmd for anim in self.animationData]
  385. # determine position and sizes of bitmaps
  386. for i, (win, anim) in enumerate(zip(self.mapwindows, self.animations)):
  387. if anim.IsActive():
  388. pos = win.GetPosition()
  389. animWinPos.append(pos)
  390. animWinSize.append(win.GetSize())
  391. animWinIndex.append(i)
  392. images = []
  393. busy = wx.BusyInfo(message=_("Preparing export, please wait..."), parent=self.frame)
  394. wx.Yield()
  395. for frameIndex in range(frameCount):
  396. image = wx.EmptyImage(*size)
  397. image.Replace(0, 0, 0, 255, 255, 255)
  398. # collect bitmaps of all windows and paste them into the one
  399. for i in animWinIndex:
  400. frameId = self.animations[i].GetFrame(frameIndex)
  401. bitmap = self.bitmapProvider.GetBitmap(frameId)
  402. im = wx.ImageFromBitmap(bitmap)
  403. # add legend if used
  404. legend = legends[i]
  405. if legend:
  406. legendBitmap = self.bitmapProvider.LoadOverlay(legend)
  407. x, y = self.mapwindows[i].GetOverlayPos()
  408. legImage = wx.ImageFromBitmap(legendBitmap)
  409. # not so nice result, can we handle the transparency otherwise?
  410. legImage.ConvertAlphaToMask()
  411. im.Paste(legImage, x, y)
  412. if im.GetSize() != animWinSize[i]:
  413. im.Rescale(*animWinSize[i])
  414. image.Paste(im, *animWinPos[i])
  415. # paste decorations
  416. for decoration in decorations:
  417. # add image
  418. x = decoration['pos'][0] / 100. * size[0]
  419. y = decoration['pos'][1] / 100. * size[1]
  420. if decoration['name'] == 'image':
  421. decImage = wx.Image(decoration['file'])
  422. elif decoration['name'] == 'time':
  423. timeLabel = timeLabels[frameIndex]
  424. if timeLabel[1]:
  425. text = _("%(from)s %(dash)s %(to)s") % \
  426. {'from': timeLabel[0], 'dash': u"\u2013", 'to': timeLabel[1]}
  427. else:
  428. text = _("%(start)s %(unit)s") % \
  429. {'start': timeLabel[0], 'unit': timeLabel[2]}
  430. decImage = RenderText(text, decoration['font']).ConvertToImage()
  431. elif decoration['name'] == 'text':
  432. text = decoration['text']
  433. decImage = RenderText(text, decoration['font']).ConvertToImage()
  434. image.Paste(decImage, x, y)
  435. images.append(image)
  436. del busy
  437. # export
  438. pilImages = [WxImageToPil(image) for image in images]
  439. busy = wx.BusyInfo(message=_("Exporting animation, please wait..."),
  440. parent=self.frame)
  441. wx.Yield()
  442. try:
  443. if exportInfo['method'] == 'sequence':
  444. filename = os.path.join(exportInfo['directory'],
  445. exportInfo['prefix'] + '.' + exportInfo['format'].lower())
  446. writeIms(filename=filename, images=pilImages)
  447. elif exportInfo['method'] == 'gif':
  448. writeGif(filename=exportInfo['file'], images=pilImages,
  449. duration=self.timeTick / float(1000), repeat=True)
  450. elif exportInfo['method'] == 'swf':
  451. writeSwf(filename=exportInfo['file'], images=pilImages,
  452. duration=self.timeTick / float(1000), repeat=True)
  453. elif exportInfo['method'] == 'avi':
  454. writeAvi(filename=exportInfo['file'], images=pilImages,
  455. duration=self.timeTick / float(1000),
  456. encoding=exportInfo['encoding'],
  457. inputOptions=exportInfo['options'])
  458. except Exception, e:
  459. del busy
  460. GError(parent=self.frame, message=str(e))
  461. return
  462. del busy