controller.py 22 KB

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