controller.py 23 KB

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