controller.py 23 KB

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