controller.py 22 KB

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