controller.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. """!
  2. @package animation.controller
  3. @brief Animations management
  4. Classes:
  5. - controller::AnimationController
  6. (C) 2012 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 Kratochvilova <kratochanna gmail.com>
  10. """
  11. import os
  12. import wx
  13. try:
  14. import visvis.vvmovie as vv
  15. hasVisvis = True
  16. except ImportError:
  17. # if visvis.vvmovie is in grass python library
  18. # import grass.visvis as vv
  19. #
  20. # question: if integrate visvis, if integrate visvis.vvmovie or only
  21. # images2swf.py, images2gif.py?
  22. hasVisvis = False
  23. from core.gcmd import GException, GError, GMessage
  24. from core.utils import _
  25. import grass.script as grass
  26. from temporal_manager import TemporalManager
  27. from dialogs import InputDialog, EditDialog, AnimationData, ExportDialog
  28. from utils import TemporalMode, Orientation, RenderText, WxImageToPil
  29. class AnimationController(wx.EvtHandler):
  30. def __init__(self, frame, sliders, animations, mapwindows, providers, bitmapPool):
  31. wx.EvtHandler.__init__(self)
  32. self.mapwindows = mapwindows
  33. self.frame = frame
  34. self.sliders = sliders
  35. self.slider = self.sliders['temporal']
  36. self.animationToolbar = None
  37. self.temporalMode = None
  38. self.animationData = []
  39. self.timer = wx.Timer(self, id = wx.NewId())
  40. self.animations = animations
  41. self.bitmapPool = bitmapPool
  42. self.bitmapProviders = providers
  43. for anim, win, provider in zip(self.animations, self.mapwindows, self.bitmapProviders):
  44. anim.SetCallbackUpdateFrame(lambda index, dataId, win = win, provider = provider : self.UpdateFrame(index, win, provider, dataId))
  45. anim.SetCallbackEndAnimation(lambda index, dataId, win = win, provider = provider: self.UpdateFrameEnd(index, win, provider, dataId))
  46. anim.SetCallbackOrientationChanged(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, provider, dataId):
  98. if self.timer.IsRunning():
  99. self.timer.Stop()
  100. self.DisableSliderIfNeeded()
  101. self.animationToolbar.Stop()
  102. self.UpdateFrame(index, win, provider, dataId)
  103. def UpdateFrame(self, index, win, provider, dataId):
  104. bitmap = provider.GetBitmap(dataId)
  105. if dataId is None:
  106. dataId = ''
  107. win.DrawBitmap(bitmap, dataId)
  108. # self.frame.SetStatusText(dataId)
  109. self.slider.UpdateFrame(index)
  110. def SliderChanging(self, index):
  111. if self.runAfterReleasingSlider is None:
  112. self.runAfterReleasingSlider = self.timer.IsRunning()
  113. self.PauseAnimation(True)
  114. self.ChangeFrame(index)
  115. def SliderChanged(self):
  116. if self.runAfterReleasingSlider:
  117. self.PauseAnimation(False)
  118. self.runAfterReleasingSlider = None
  119. def ChangeFrame(self, index):
  120. for anim in self.animations:
  121. anim.FrameChangedFromOutside(index)
  122. def DisableSliderIfNeeded(self):
  123. if self.timer.IsRunning() and self._timeTick < 100:
  124. self.slider.EnableSlider(False)
  125. else:
  126. self.slider.EnableSlider(True)
  127. def OrientationChangedInReverseMode(self, mode):
  128. if mode == Orientation.FORWARD:
  129. self.animationToolbar.PlayForward()
  130. elif mode == Orientation.BACKWARD:
  131. self.animationToolbar.PlayBack()
  132. def SetReplayMode(self, mode):
  133. for anim in self.animations:
  134. anim.replayMode = mode
  135. def SetOrientation(self, mode):
  136. for anim in self.animations:
  137. anim.orientation = mode
  138. def SetTemporalMode(self, mode):
  139. self._temporalMode = mode
  140. def GetTemporalMode(self):
  141. return self._temporalMode
  142. temporalMode = property(fget = GetTemporalMode, fset = SetTemporalMode)
  143. def GetTimeGranularity(self):
  144. if self.temporalMode == TemporalMode.TEMPORAL:
  145. return self.temporalManager.GetGranularity()
  146. return None
  147. def EditAnimations(self):
  148. # running = False
  149. # if self.timer.IsRunning():
  150. # running = True
  151. self.EndAnimation()
  152. dlg = EditDialog(parent = self.frame, evalFunction = self.EvaluateInput,
  153. animationData = self.animationData, maxAnimations = len(self.animations))
  154. if dlg.ShowModal() == wx.ID_CANCEL:
  155. dlg.Destroy()
  156. return
  157. self.animationData, self.temporalMode, self.temporalManager = dlg.GetResult()
  158. dlg.Destroy()
  159. self._setAnimations()
  160. def AddAnimation(self):
  161. # check if we can add more animations
  162. found = False
  163. indices = [anim.windowIndex for anim in self.animationData]
  164. for windowIndex in range(len(self.animations)):
  165. if windowIndex not in indices:
  166. found = True
  167. break
  168. if not found:
  169. GMessage(parent = self.frame, message = _("Maximum number of animations is %s.") % len(self.animations))
  170. return
  171. # running = False
  172. # if self.timer.IsRunning():
  173. # running = True
  174. self.EndAnimation()
  175. # self.PauseAnimation(True)
  176. animData = AnimationData()
  177. # number of active animations
  178. animationIndex = len([anim for anim in self.animations if anim.IsActive()])
  179. animData.SetDefaultValues(windowIndex, animationIndex)
  180. dlg = InputDialog(parent = self.frame, mode = 'add', animationData = animData)
  181. if dlg.ShowModal() == wx.ID_CANCEL:
  182. dlg.Destroy()
  183. return
  184. dlg.Destroy()
  185. # check compatibility
  186. if animData.windowIndex in indices:
  187. GMessage(parent = self.frame, message = _("More animations are using one window."
  188. " Please select different window for each animation."))
  189. return
  190. try:
  191. temporalMode, tempManager = self.EvaluateInput(self.animationData + [animData])
  192. except GException, e:
  193. GError(parent = self.frame, message = e.value, showTraceback = False)
  194. return
  195. # if ok, set temporal mode
  196. self.temporalMode = temporalMode
  197. self.temporalManager = tempManager
  198. # add data
  199. windowIndex = animData.windowIndex
  200. self.animationData.append(animData)
  201. self._setAnimations()
  202. def SetAnimations(self, inputs=None, dataType=None):
  203. """!Set animation data directly.
  204. @param raster list of lists of raster maps or None
  205. @param strds list of strds or None
  206. @param inputs list of lists of raster maps or vector maps,
  207. or a space time raster or vector dataset
  208. @param dataType The type of the input data must be one of 'rast', 'vect', 'strds' or 'strds'
  209. """
  210. try:
  211. animationData = []
  212. for i in range(len(self.animations)):
  213. if inputs is not None and inputs[i]:
  214. if dataType == 'rast' or dataType == 'vect':
  215. if type(inputs[i]) == list:
  216. anim = AnimationData()
  217. anim.SetDefaultValues(i, i)
  218. anim.inputMapType = dataType
  219. anim.inputData = ','.join(inputs[i])
  220. animationData.append(anim)
  221. elif dataType == 'strds' or dataType == 'stvds':
  222. anim = AnimationData()
  223. anim.SetDefaultValues(i, i)
  224. anim.inputMapType = dataType
  225. anim.inputData = inputs[i]
  226. animationData.append(anim)
  227. except (GException, ValueError, IOError) as e:
  228. GError(parent = self.frame, message = str(e),
  229. showTraceback = False, caption = _("Invalid input"))
  230. return
  231. try:
  232. temporalMode, tempManager = self.EvaluateInput(animationData)
  233. except GException, e:
  234. GError(parent = self.frame, message = e.value, showTraceback = False)
  235. return
  236. self.animationData = animationData
  237. self.temporalManager = tempManager
  238. self.temporalMode = temporalMode
  239. self._setAnimations()
  240. def _setAnimations(self):
  241. indices = [anim.windowIndex for anim in self.animationData]
  242. self._updateWindows(activeIndices = indices)
  243. if self.temporalMode == TemporalMode.TEMPORAL:
  244. timeLabels, mapNamesDict = self.temporalManager.GetLabelsAndMaps()
  245. else:
  246. timeLabels, mapNamesDict = None, None
  247. self._updateSlider(timeLabels = timeLabels)
  248. self._updateAnimations(activeIndices = indices, mapNamesDict = mapNamesDict)
  249. wx.Yield()
  250. self._updateBitmapData()
  251. # if running:
  252. # self.PauseAnimation(False)
  253. # # self.StartAnimation()
  254. # else:
  255. self.EndAnimation()
  256. def _updateSlider(self, timeLabels = None):
  257. if self.temporalMode == TemporalMode.NONTEMPORAL:
  258. self.frame.SetSlider('nontemporal')
  259. self.slider = self.sliders['nontemporal']
  260. frameCount = len(self.animationData[0].mapData) # should be the same for all
  261. self.slider.SetFrames(frameCount)
  262. elif self.temporalMode == TemporalMode.TEMPORAL:
  263. self.frame.SetSlider('temporal')
  264. self.slider = self.sliders['temporal']
  265. self.slider.SetTemporalType(self.temporalManager.temporalType)
  266. self.slider.SetFrames(timeLabels)
  267. else:
  268. self.frame.SetSlider(None)
  269. self.slider = None
  270. def _updateAnimations(self, activeIndices, mapNamesDict = None):
  271. if self.temporalMode == TemporalMode.NONTEMPORAL:
  272. for i in range(len(self.animations)):
  273. if i not in activeIndices:
  274. self.animations[i].SetActive(False)
  275. continue
  276. anim = [anim for anim in self.animationData if anim.windowIndex == i][0]
  277. self.animations[i].SetFrames(anim.mapData)
  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. self.animations[i].SetFrames(mapNamesDict[anim.inputData])
  286. self.animations[i].SetActive(True)
  287. def _updateWindows(self, activeIndices):
  288. # add or remove window
  289. for windowIndex in range(len(self.animations)):
  290. if not self.frame.IsWindowShown(windowIndex) and windowIndex in activeIndices:
  291. self.frame.AddWindow(windowIndex)
  292. elif self.frame.IsWindowShown(windowIndex) and windowIndex not in activeIndices:
  293. self.frame.RemoveWindow(windowIndex)
  294. def _updateBitmapData(self):
  295. # unload data:
  296. for prov in self.bitmapProviders:
  297. prov.Unload()
  298. # load data
  299. for animData in self.animationData:
  300. if animData.viewMode == '2d':
  301. self._load2DData(animData)
  302. else:
  303. self._load3DData(animData)
  304. # clear bitmapPool
  305. usedNames = []
  306. for prov in self.bitmapProviders:
  307. names = prov.GetDataNames()
  308. if names:
  309. usedNames.extend(names)
  310. self.bitmapPool.Clear(usedNames)
  311. def _load2DData(self, animationData):
  312. prov = self.bitmapProviders[animationData.windowIndex]
  313. prov.SetData(datasource = animationData.mapData, dataType=animationData.inputMapType)
  314. prov.Load()
  315. if animationData.legendCmd:
  316. try:
  317. # place legend
  318. x, y = 0.1, 0.1
  319. bitmap = prov.LoadOverlay(animationData.legendCmd)
  320. for param in animationData.legendCmd:
  321. if param.startswith('at'):
  322. b, t, l, r = param.split('=')[1].split(',')
  323. x, y = float(l) / 100., 1 - float(t) / 100.
  324. break
  325. self.mapwindows[animationData.windowIndex].SetOverlay(bitmap, x, y)
  326. except GException:
  327. GError(message=_("Failed to display legend."))
  328. def _load3DData(self, animationData):
  329. prov = self.bitmapProviders[animationData.windowIndex]
  330. nviz = animationData.GetNvizCommands()
  331. prov.SetData(datasource = nviz['commands'],
  332. dataNames = animationData.mapData, dataType = 'nviz',
  333. suffix = animationData.nvizParameter,
  334. nvizRegion = nviz['region'])
  335. self.bitmapProviders[animationData.windowIndex].Load()
  336. def EvaluateInput(self, animationData):
  337. stds = 0
  338. maps = 0
  339. mapCount = set()
  340. tempManager = None
  341. windowIndex = []
  342. for anim in animationData:
  343. mapCount.add(len(anim.mapData))
  344. windowIndex.append(anim.windowIndex)
  345. if anim.inputMapType in ('rast', 'vect'):
  346. maps += 1
  347. elif anim.inputMapType in ('strds', 'stvds'):
  348. stds += 1
  349. if maps and stds:
  350. temporalMode = TemporalMode.NONTEMPORAL
  351. elif maps:
  352. temporalMode = TemporalMode.NONTEMPORAL
  353. elif stds:
  354. temporalMode = TemporalMode.TEMPORAL
  355. else:
  356. temporalMode = None
  357. if temporalMode == TemporalMode.NONTEMPORAL:
  358. if len(mapCount) > 1:
  359. raise GException(_("Inconsistent number of maps, please check input data."))
  360. elif temporalMode == TemporalMode.TEMPORAL:
  361. tempManager = TemporalManager()
  362. # these raise GException:
  363. for anim in animationData:
  364. if anim.inputMapType not in ('strds', 'stvds'):
  365. continue
  366. tempManager.AddTimeSeries(anim.inputData, anim.inputMapType)
  367. message = tempManager.EvaluateInputData()
  368. if message:
  369. GMessage(parent = self.frame, message = message)
  370. return temporalMode, tempManager
  371. def Reload(self):
  372. self.EndAnimation()
  373. activeIndices = [anim.windowIndex for anim in self.animationData]
  374. for index in activeIndices:
  375. self.bitmapProviders[index].Load(force = True)
  376. self.EndAnimation()
  377. def Export(self):
  378. if not self.animationData:
  379. GMessage(parent = self.frame, message = _("No animation to export."))
  380. return
  381. if 'export' in self._dialogs:
  382. self._dialogs['export'].Show()
  383. self._dialogs['export'].Raise()
  384. else:
  385. dlg = ExportDialog(self.frame, temporal=self.temporalMode,
  386. timeTick=self.timeTick, visvis=hasVisvis)
  387. dlg.doExport.connect(self._export)
  388. self._dialogs['export'] = dlg
  389. dlg.Show()
  390. def _export(self, exportInfo, decorations):
  391. size = self.frame.animationPanel.GetSize()
  392. if self.temporalMode == TemporalMode.TEMPORAL:
  393. timeLabels, mapNamesDict = self.temporalManager.GetLabelsAndMaps()
  394. frameCount = len(timeLabels)
  395. else:
  396. frameCount = len(self.animationData[0].mapData) # should be the same for all
  397. animWinSize = []
  398. animWinPos = []
  399. animWinIndex = []
  400. # determine position and sizes of bitmaps
  401. for i, (win, anim) in enumerate(zip(self.mapwindows, self.animations)):
  402. if anim.IsActive():
  403. pos = win.GetPosition()
  404. animWinPos.append(pos)
  405. animWinSize.append(win.GetSize())
  406. animWinIndex.append(i)
  407. images = []
  408. for frameIndex in range(frameCount):
  409. image = wx.EmptyImage(*size)
  410. image.Replace(0, 0, 0, 255, 255, 255)
  411. # collect bitmaps of all windows and paste them into the one
  412. for i in range(len(animWinSize)):
  413. frameId = self.animations[animWinIndex[i]].GetFrame(frameIndex)
  414. bitmap = self.bitmapProviders[animWinIndex[i]].GetBitmap(frameId)
  415. im = wx.ImageFromBitmap(bitmap)
  416. # add legend if used
  417. legend = self.animationData[animWinIndex[i]].legendCmd
  418. if legend:
  419. legendBitmap = self.bitmapProviders[animWinIndex[i]].LoadOverlay(legend)
  420. x, y = self.mapwindows[animWinIndex[i]].GetOverlayPos()
  421. legImage = wx.ImageFromBitmap(legendBitmap)
  422. # not so nice result, can we handle the transparency otherwise?
  423. legImage.ConvertAlphaToMask()
  424. im.Paste(legImage, x, y)
  425. if im.GetSize() != animWinSize[i]:
  426. im.Rescale(*animWinSize[i])
  427. image.Paste(im, *animWinPos[i])
  428. # paste decorations
  429. for decoration in decorations:
  430. # add image
  431. x = decoration['pos'][0] / 100. * size[0]
  432. y = decoration['pos'][1] / 100. * size[1]
  433. if decoration['name'] == 'image':
  434. decImage = wx.Image(decoration['file'])
  435. elif decoration['name'] == 'time':
  436. timeLabel = timeLabels[frameIndex]
  437. if timeLabel[1]:
  438. text = _("%(from)s %(dash)s %(to)s") % \
  439. {'from': timeLabel[0], 'dash': u"\u2013", 'to': timeLabel[1]}
  440. else:
  441. text = _("%(start)s %(unit)s") % \
  442. {'start': timeLabel[0], 'unit': timeLabel[2]}
  443. decImage = RenderText(text, decoration['font']).ConvertToImage()
  444. elif decoration['name'] == 'text':
  445. text = decoration['text']
  446. decImage = RenderText(text, decoration['font']).ConvertToImage()
  447. image.Paste(decImage, x, y)
  448. images.append(image)
  449. # export
  450. if exportInfo['method'] == 'sequence':
  451. busy = wx.BusyInfo(message = _("Exporting images, please wait..."), parent = self.frame)
  452. wx.Yield()
  453. zeroPadding = len(str(len(images)))
  454. for i, image in enumerate(images):
  455. filename = "%s_%s.%s" % (exportInfo['prefix'], str(i + 1).zfill(zeroPadding),
  456. exportInfo['format']['ext'])
  457. image.SaveFile(os.path.join(exportInfo['directory'], filename), exportInfo['format']['type'])
  458. busy.Destroy()
  459. elif exportInfo['method'] in ('gif', 'swf', 'avi'):
  460. pilImages = [WxImageToPil(image) for image in images]
  461. busy = wx.BusyInfo(message = _("Exporting animation, please wait..."), parent = self.frame)
  462. wx.Yield()
  463. try:
  464. if exportInfo['method'] == 'gif':
  465. vv.writeGif(filename = exportInfo['file'], images = pilImages,
  466. duration = self.timeTick / float(1000), repeat = True)
  467. elif exportInfo['method'] == 'swf':
  468. vv.writeSwf(filename = exportInfo['file'], images = pilImages,
  469. duration = self.timeTick / float(1000), repeat = True)
  470. elif exportInfo['method'] == 'avi':
  471. vv.writeAvi(filename = exportInfo['file'], images = pilImages,
  472. duration = self.timeTick / float(1000),
  473. encoding = exportInfo['encoding'],
  474. inputOptions = '-sameq')
  475. except Exception, e:
  476. del busy
  477. GError(parent = self.frame, message = str(e))
  478. return
  479. del busy
  480. # image.SaveFile('/home/anna/testy/grass/export/export_%s.png' % frameIndex, wx.BITMAP_TYPE_PNG)
  481. # for anim in self.animationData
  482. #def test():
  483. # import grass.script as grass
  484. # import wx
  485. # app = wx.PySimpleApp()
  486. # wx.InitAllImageHandlers()
  487. # # app.MainLoop()
  488. #
  489. # bitmaps = {}
  490. # rasters = ['elevation.dem']
  491. # # rasters = ['streams']
  492. # # rasters = grass.read_command("g.mlist", type = 'rast', fs = ',', quiet = True).strip().split(',')
  493. #
  494. # # print nrows, ncols
  495. # size = (300,100)
  496. # newSize, scale = ComputeScale(size)
  497. # # print scale
  498. # LoadRasters(rasters = rasters, bitmaps = bitmaps, scale = scale, size = newSize)
  499. #
  500. # for b in bitmaps.keys():
  501. # bitmaps[b].SaveFile('/home/anna/testy/ctypes/' + b + '.png', wx.BITMAP_TYPE_PNG)
  502. #
  503. #if __name__ == '__main__':
  504. #
  505. # test()