controller.py 22 KB

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