123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672 |
- """
- @package animation.controller
- @brief Animations management
- Classes:
- - controller::AnimationController
- (C) 2013 by the GRASS Development Team
- This program is free software under the GNU General Public License
- (>=v2). Read the file COPYING that comes with GRASS for details.
- @author Anna Petrasova <kratochanna gmail.com>
- """
- import os
- import wx
- from core.gcmd import GException, GError, GMessage
- from grass.imaging import writeAvi, writeGif, writeIms, writeSwf
- from core.gthread import gThread
- from core.settings import UserSettings
- from gui_core.wrap import EmptyImage, ImageFromBitmap
- from animation.temporal_manager import TemporalManager
- from animation.dialogs import InputDialog, EditDialog, ExportDialog
- from animation.utils import (
- TemporalMode,
- TemporalType,
- Orientation,
- RenderText,
- WxImageToPil,
- sampleCmdMatrixAndCreateNames,
- layerListToCmdsMatrix,
- HashCmds,
- )
- from animation.data import AnimationData
- class AnimationController(wx.EvtHandler):
- def __init__(
- self, frame, sliders, animations, mapwindows, provider, bitmapPool, mapFilesPool
- ):
- wx.EvtHandler.__init__(self)
- self.mapwindows = mapwindows
- self.frame = frame
- self.sliders = sliders
- self.slider = self.sliders["temporal"]
- self.animationToolbar = None
- self.temporalMode = None
- self.animationData = []
- self.timer = wx.Timer(self, id=wx.ID_ANY)
- self.animations = animations
- self.bitmapPool = bitmapPool
- self.mapFilesPool = mapFilesPool
- self.bitmapProvider = provider
- for anim, win in zip(self.animations, self.mapwindows):
- anim.SetCallbackUpdateFrame(
- lambda index, dataId, win=win: self.UpdateFrame(index, win, dataId)
- )
- anim.SetCallbackEndAnimation(
- lambda index, dataId, win=win: self.UpdateFrameEnd(index, win, dataId)
- )
- anim.SetCallbackOrientationChanged(self.OrientationChangedInReverseMode)
- for slider in self.sliders.values():
- slider.SetCallbackSliderChanging(self.SliderChanging)
- slider.SetCallbackSliderChanged(self.SliderChanged)
- slider.SetCallbackFrameIndexChanged(self.ChangeFrame)
- self.runAfterReleasingSlider = None
- self.temporalManager = TemporalManager()
- self.Bind(wx.EVT_TIMER, self.OnTimerTick, self.timer)
- self.timeTick = 200
- self._dialogs = {}
- def SetAnimationToolbar(self, toolbar):
- self.animationToolbar = toolbar
- def GetTimeTick(self):
- return self._timeTick
- def SetTimeTick(self, value):
- self._timeTick = value
- if self.timer.IsRunning():
- self.timer.Stop()
- self.timer.Start(self._timeTick)
- self.DisableSliderIfNeeded()
- timeTick = property(fget=GetTimeTick, fset=SetTimeTick)
- def OnTimerTick(self, event):
- for anim in self.animations:
- anim.Update()
- def StartAnimation(self):
- # if self.timer.IsRunning():
- # self.timer.Stop()
- for anim in self.animations:
- if self.timer.IsRunning():
- anim.NextFrameIndex()
- anim.Start()
- if not self.timer.IsRunning():
- self.timer.Start(self.timeTick)
- self.DisableSliderIfNeeded()
- def PauseAnimation(self, paused):
- if paused:
- if self.timer.IsRunning():
- self.timer.Stop()
- self.DisableSliderIfNeeded()
- else:
- if not self.timer.IsRunning():
- self.timer.Start(self.timeTick)
- self.DisableSliderIfNeeded()
- for anim in self.animations:
- anim.Pause(paused)
- def EndAnimation(self):
- if self.timer.IsRunning():
- self.timer.Stop()
- self.DisableSliderIfNeeded()
- for anim in self.animations:
- anim.Stop()
- def UpdateFrameEnd(self, index, win, dataId):
- if self.timer.IsRunning():
- self.timer.Stop()
- self.DisableSliderIfNeeded()
- self.animationToolbar.Stop()
- self.UpdateFrame(index, win, dataId)
- def UpdateFrame(self, index, win, dataId):
- bitmap = self.bitmapProvider.GetBitmap(dataId)
- if not UserSettings.Get(
- group="animation", key="temporal", subkey=["nodata", "enable"]
- ):
- if dataId is not None:
- win.DrawBitmap(bitmap)
- else:
- win.DrawBitmap(bitmap)
- self.slider.UpdateFrame(index)
- def SliderChanging(self, index):
- if self.runAfterReleasingSlider is None:
- self.runAfterReleasingSlider = self.timer.IsRunning()
- self.PauseAnimation(True)
- self.ChangeFrame(index)
- def SliderChanged(self):
- if self.runAfterReleasingSlider:
- self.PauseAnimation(False)
- self.runAfterReleasingSlider = None
- def ChangeFrame(self, index):
- for anim in self.animations:
- anim.FrameChangedFromOutside(index)
- def DisableSliderIfNeeded(self):
- if self.timer.IsRunning() and self._timeTick < 100:
- self.slider.EnableSlider(False)
- else:
- self.slider.EnableSlider(True)
- def OrientationChangedInReverseMode(self, mode):
- if mode == Orientation.FORWARD:
- self.animationToolbar.PlayForward()
- elif mode == Orientation.BACKWARD:
- self.animationToolbar.PlayBack()
- def SetReplayMode(self, mode):
- for anim in self.animations:
- anim.replayMode = mode
- def SetOrientation(self, mode):
- for anim in self.animations:
- anim.orientation = mode
- def SetTemporalMode(self, mode):
- self._temporalMode = mode
- def GetTemporalMode(self):
- return self._temporalMode
- temporalMode = property(fget=GetTemporalMode, fset=SetTemporalMode)
- def GetTimeGranularity(self):
- if self.temporalMode == TemporalMode.TEMPORAL:
- return self.temporalManager.GetGranularity()
- return None
- def UpdateAnimations(self):
- """Used sofar for updating slider time labels
- after change of format"""
- self._setAnimations()
- def EditAnimations(self):
- # running = False
- # if self.timer.IsRunning():
- # running = True
- self.EndAnimation()
- dlg = EditDialog(
- parent=self.frame,
- evalFunction=self.EvaluateInput,
- animationData=self.animationData,
- maxAnimations=len(self.animations),
- )
- dlg.CenterOnParent()
- if dlg.ShowModal() == wx.ID_CANCEL:
- dlg.Destroy()
- return
- self.animationData, self.temporalMode, self.temporalManager = dlg.GetResult()
- dlg.Destroy()
- self._setAnimations()
- def AddAnimation(self):
- # check if we can add more animations
- found = False
- indices = [anim.windowIndex for anim in self.animationData]
- for windowIndex in range(len(self.animations)):
- if windowIndex not in indices:
- found = True
- break
- if not found:
- GMessage(
- parent=self.frame,
- message=_("Maximum number of animations is %d.") % len(self.animations),
- )
- return
- # running = False
- # if self.timer.IsRunning():
- # running = True
- self.EndAnimation()
- # self.PauseAnimation(True)
- animData = AnimationData()
- # number of active animations
- animationIndex = len([anim for anim in self.animations if anim.IsActive()])
- animData.SetDefaultValues(windowIndex, animationIndex)
- dlg = InputDialog(parent=self.frame, mode="add", animationData=animData)
- dlg.CenterOnParent()
- if dlg.ShowModal() == wx.ID_CANCEL:
- dlg.UnInit()
- dlg.Destroy()
- return
- dlg.Destroy()
- # check compatibility
- if animData.windowIndex in indices:
- GMessage(
- parent=self.frame,
- message=_(
- "More animations are using one window."
- " Please select different window for each animation."
- ),
- )
- return
- try:
- temporalMode, tempManager = self.EvaluateInput(
- self.animationData + [animData]
- )
- except GException as e:
- GError(parent=self.frame, message=e.value, showTraceback=False)
- return
- # if ok, set temporal mode
- self.temporalMode = temporalMode
- self.temporalManager = tempManager
- # add data
- windowIndex = animData.windowIndex
- self.animationData.append(animData)
- self._setAnimations()
- def SetAnimations(self, layerLists):
- """Set animation data directly.
- :param layerLists: list of layerLists
- """
- try:
- animationData = []
- for i in range(len(self.animations)):
- if layerLists[i]:
- anim = AnimationData()
- anim.SetDefaultValues(i, i)
- anim.SetLayerList(layerLists[i])
- animationData.append(anim)
- except (GException, ValueError, IOError) as e:
- GError(
- parent=self.frame,
- message=str(e),
- showTraceback=False,
- caption=_("Invalid input"),
- )
- return
- try:
- temporalMode, tempManager = self.EvaluateInput(animationData)
- except GException as e:
- GError(parent=self.frame, message=e.value, showTraceback=False)
- return
- self.animationData = animationData
- self.temporalManager = tempManager
- self.temporalMode = temporalMode
- self._setAnimations()
- def _setAnimations(self):
- indices = [anim.windowIndex for anim in self.animationData]
- self._updateWindows(activeIndices=indices)
- if self.temporalMode == TemporalMode.TEMPORAL:
- timeLabels, mapNamesDict = self.temporalManager.GetLabelsAndMaps()
- else:
- timeLabels, mapNamesDict = None, None
- for anim in self.animationData:
- if anim.viewMode == "2d":
- anim.cmdMatrix = layerListToCmdsMatrix(anim.layerList)
- else:
- anim.cmdMatrix = [(cmd,) for cmd in anim.GetNvizCommands()["commands"]]
- self._updateSlider(timeLabels=timeLabels)
- self._updateAnimations(activeIndices=indices, mapNamesDict=mapNamesDict)
- self._updateBitmapData()
- # if running:
- # self.PauseAnimation(False)
- # # self.StartAnimation()
- # else:
- self.EndAnimation()
- def _updateSlider(self, timeLabels=None):
- if self.temporalMode == TemporalMode.NONTEMPORAL:
- self.frame.SetSlider("nontemporal")
- self.slider = self.sliders["nontemporal"]
- frameCount = self.animationData[0].mapCount
- self.slider.SetFrames(frameCount)
- elif self.temporalMode == TemporalMode.TEMPORAL:
- self.frame.SetSlider("temporal")
- self.slider = self.sliders["temporal"]
- self.slider.SetTemporalType(self.temporalManager.temporalType)
- self.slider.SetFrames(timeLabels)
- else:
- self.frame.SetSlider(None)
- self.slider = None
- def _updateAnimations(self, activeIndices, mapNamesDict=None):
- if self.temporalMode == TemporalMode.NONTEMPORAL:
- for i in range(len(self.animations)):
- if i not in activeIndices:
- self.animations[i].SetActive(False)
- continue
- anim = [anim for anim in self.animationData if anim.windowIndex == i][0]
- regions = anim.GetRegions()
- self.animations[i].SetFrames(
- [
- HashCmds(cmdList, region)
- for cmdList, region in zip(anim.cmdMatrix, regions)
- ]
- )
- self.animations[i].SetActive(True)
- else:
- for i in range(len(self.animations)):
- if i not in activeIndices:
- self.animations[i].SetActive(False)
- continue
- anim = [anim for anim in self.animationData if anim.windowIndex == i][0]
- regions = anim.GetRegions()
- identifiers = sampleCmdMatrixAndCreateNames(
- anim.cmdMatrix, mapNamesDict[anim.firstStdsNameType[0]], regions
- )
- self.animations[i].SetFrames(identifiers)
- self.animations[i].SetActive(True)
- def _updateWindows(self, activeIndices):
- # add or remove window
- for windowIndex in range(len(self.animations)):
- if (
- not self.frame.IsWindowShown(windowIndex)
- and windowIndex in activeIndices
- ):
- self.frame.AddWindow(windowIndex)
- elif (
- self.frame.IsWindowShown(windowIndex)
- and windowIndex not in activeIndices
- ):
- self.frame.RemoveWindow(windowIndex)
- def _updateBitmapData(self):
- # unload previous data
- self.bitmapProvider.Unload()
- # load new data
- for animData in self.animationData:
- if animData.viewMode == "2d":
- self._set2DData(animData)
- else:
- self._load3DData(animData)
- self._loadLegend(animData)
- color = UserSettings.Get(group="animation", key="bgcolor", subkey="color")
- cpus = UserSettings.Get(group="animation", key="nprocs", subkey="value")
- self.bitmapProvider.Load(nprocs=cpus, bgcolor=color)
- # clear pools
- self.bitmapPool.Clear()
- self.mapFilesPool.Clear()
- def _set2DData(self, animationData):
- opacities = [layer.opacity for layer in animationData.layerList if layer.active]
- # w, h = self.mapwindows[animationData.GetWindowIndex()].GetClientSize()
- regions = animationData.GetRegions()
- self.bitmapProvider.SetCmds(animationData.cmdMatrix, opacities, regions)
- def _load3DData(self, animationData):
- nviz = animationData.GetNvizCommands()
- self.bitmapProvider.SetCmds3D(nviz["commands"], nviz["region"])
- def _loadLegend(self, animationData):
- if animationData.legendCmd:
- try:
- bitmap = self.bitmapProvider.LoadOverlay(animationData.legendCmd)
- try:
- from PIL import Image # noqa: F401
- for param in animationData.legendCmd:
- if param.startswith("at"):
- b, t, l, r = param.split("=")[1].split(",")
- x, y = float(l) / 100.0, 1 - float(t) / 100.0
- break
- except ImportError:
- x, y = 0, 0
- self.mapwindows[animationData.windowIndex].SetOverlay(bitmap, x, y)
- except GException:
- GError(message=_("Failed to display legend."))
- else:
- self.mapwindows[animationData.windowIndex].ClearOverlay()
- def EvaluateInput(self, animationData):
- stds = 0
- maps = 0
- mapCount = set()
- tempManager = None
- windowIndex = []
- for anim in animationData:
- for layer in anim.layerList:
- if layer.active and hasattr(layer, "maps"):
- if layer.mapType in ("strds", "stvds", "str3ds"):
- stds += 1
- else:
- maps += 1
- mapCount.add(len(layer.maps))
- windowIndex.append(anim.windowIndex)
- if maps and stds:
- temporalMode = TemporalMode.NONTEMPORAL
- elif maps:
- temporalMode = TemporalMode.NONTEMPORAL
- elif stds:
- temporalMode = TemporalMode.TEMPORAL
- else:
- temporalMode = None
- if temporalMode == TemporalMode.NONTEMPORAL:
- if len(mapCount) > 1:
- raise GException(
- _("Inconsistent number of maps, please check input data.")
- )
- elif temporalMode == TemporalMode.TEMPORAL:
- tempManager = TemporalManager()
- # these raise GException:
- for anim in animationData:
- tempManager.AddTimeSeries(*anim.firstStdsNameType)
- message = tempManager.EvaluateInputData()
- if message:
- GMessage(parent=self.frame, message=message)
- return temporalMode, tempManager
- def Reload(self):
- self.EndAnimation()
- color = UserSettings.Get(group="animation", key="bgcolor", subkey="color")
- cpus = UserSettings.Get(group="animation", key="nprocs", subkey="value")
- self.bitmapProvider.Load(nprocs=cpus, bgcolor=color, force=True)
- self.EndAnimation()
- def Export(self):
- if not self.animationData:
- GMessage(parent=self.frame, message=_("No animation to export."))
- return
- if "export" in self._dialogs:
- self._dialogs["export"].Show()
- self._dialogs["export"].Raise()
- else:
- dlg = ExportDialog(
- self.frame, temporal=self.temporalMode, timeTick=self.timeTick
- )
- dlg.CenterOnParent()
- dlg.doExport.connect(self._export)
- self._dialogs["export"] = dlg
- dlg.Show()
- def _export(self, exportInfo, decorations):
- size = self.frame.animationPanel.GetSize()
- if self.temporalMode == TemporalMode.TEMPORAL:
- timeLabels, mapNamesDict = self.temporalManager.GetLabelsAndMaps()
- frameCount = len(timeLabels)
- else:
- frameCount = self.animationData[0].mapCount # should be the same for all
- animWinSize = []
- animWinPos = []
- animWinIndex = []
- legends = [anim.legendCmd for anim in self.animationData]
- # determine position and sizes of bitmaps
- for i, (win, anim) in enumerate(zip(self.mapwindows, self.animations)):
- if anim.IsActive():
- pos = win.GetPosition()
- animWinPos.append(pos)
- animWinSize.append(win.GetSize())
- animWinIndex.append(i)
- images = []
- busy = wx.BusyInfo(_("Preparing export, please wait..."), parent=self.frame)
- wx.GetApp().Yield()
- lastBitmaps = {}
- fgcolor = UserSettings.Get(group="animation", key="font", subkey="fgcolor")
- bgcolor = UserSettings.Get(group="animation", key="font", subkey="bgcolor")
- for frameIndex in range(frameCount):
- image = EmptyImage(*size)
- image.Replace(0, 0, 0, 255, 255, 255)
- # collect bitmaps of all windows and paste them into the one
- for i in animWinIndex:
- frameId = self.animations[i].GetFrame(frameIndex)
- if not UserSettings.Get(
- group="animation", key="temporal", subkey=["nodata", "enable"]
- ):
- if frameId is not None:
- bitmap = self.bitmapProvider.GetBitmap(frameId)
- lastBitmaps[i] = bitmap
- else:
- if i not in lastBitmaps:
- lastBitmaps[i] = wx.NullBitmap()
- else:
- bitmap = self.bitmapProvider.GetBitmap(frameId)
- lastBitmaps[i] = bitmap
- im = ImageFromBitmap(lastBitmaps[i])
- # add legend if used
- legend = legends[i]
- if legend:
- legendBitmap = self.bitmapProvider.LoadOverlay(legend)
- x, y = self.mapwindows[i].GetOverlayPos()
- legImage = ImageFromBitmap(legendBitmap)
- # not so nice result, can we handle the transparency
- # otherwise?
- legImage.ConvertAlphaToMask()
- im.Paste(legImage, x, y)
- if im.GetSize() != animWinSize[i]:
- im.Rescale(*animWinSize[i])
- image.Paste(im, *animWinPos[i])
- # paste decorations
- for decoration in decorations:
- # add image
- x = decoration["pos"][0] / 100.0 * size[0]
- y = decoration["pos"][1] / 100.0 * size[1]
- if decoration["name"] == "image":
- decImage = wx.Image(decoration["file"])
- elif decoration["name"] == "time":
- timeLabel = timeLabels[frameIndex]
- if timeLabel[1]: # interval
- text = _("%(from)s %(dash)s %(to)s") % {
- "from": timeLabel[0],
- "dash": "\u2013",
- "to": timeLabel[1],
- }
- else:
- if (
- self.temporalManager.GetTemporalType()
- == TemporalType.ABSOLUTE
- ):
- text = timeLabel[0]
- else:
- text = _("%(start)s %(unit)s") % {
- "start": timeLabel[0],
- "unit": timeLabel[2],
- }
- decImage = RenderText(
- text, decoration["font"], bgcolor, fgcolor
- ).ConvertToImage()
- elif decoration["name"] == "text":
- text = decoration["text"]
- decImage = RenderText(
- text, decoration["font"], bgcolor, fgcolor
- ).ConvertToImage()
- image.Paste(decImage, x, y)
- images.append(image)
- del busy
- # export
- pilImages = [WxImageToPil(image) for image in images]
- self.busy = wx.BusyInfo(
- _("Exporting animation, please wait..."), parent=self.frame
- )
- wx.GetApp().Yield()
- try:
- def export_avi_callback(event):
- error = event.ret
- del self.busy
- if error:
- GError(parent=self.frame, message=error)
- return
- if exportInfo["method"] == "sequence":
- filename = os.path.join(
- exportInfo["directory"],
- exportInfo["prefix"] + "." + exportInfo["format"].lower(),
- )
- writeIms(filename=filename, images=pilImages)
- elif exportInfo["method"] == "gif":
- writeGif(
- filename=exportInfo["file"],
- images=pilImages,
- duration=self.timeTick / float(1000),
- repeat=True,
- )
- elif exportInfo["method"] == "swf":
- writeSwf(
- filename=exportInfo["file"],
- images=pilImages,
- duration=self.timeTick / float(1000),
- repeat=True,
- )
- elif exportInfo["method"] == "avi":
- thread = gThread()
- thread.Run(
- callable=writeAvi,
- filename=exportInfo["file"],
- images=pilImages,
- duration=self.timeTick / float(1000),
- encoding=exportInfo["encoding"],
- inputOptions=exportInfo["options"],
- bg_task=True,
- ondone=export_avi_callback,
- )
- except Exception as e:
- del self.busy
- GError(parent=self.frame, message=str(e))
- return
- if exportInfo["method"] in ("sequence", "gif", "swf"):
- del self.busy
|