Browse Source

wxGUI/animation: add option to change region during animation (suggested by lucadelu and neteler)

git-svn-id: https://svn.osgeo.org/grass/grass/trunk@58576 15284696-431f-4ddb-bdfa-cd5b030d7da7
Anna Petrášová 11 years ago
parent
commit
929bcf68c5

+ 8 - 3
gui/wxpython/animation/controller.py

@@ -327,7 +327,9 @@ class AnimationController(wx.EvtHandler):
                     self.animations[i].SetActive(False)
                     self.animations[i].SetActive(False)
                     continue
                     continue
                 anim = [anim for anim in self.animationData if anim.windowIndex == i][0]
                 anim = [anim for anim in self.animationData if anim.windowIndex == i][0]
-                self.animations[i].SetFrames([HashCmds(cmdList) for cmdList in anim.cmdMatrix])
+                regions = anim.GetRegions()
+                self.animations[i].SetFrames([HashCmds(cmdList, region)
+                                              for cmdList, region in zip(anim.cmdMatrix, regions)])
                 self.animations[i].SetActive(True)
                 self.animations[i].SetActive(True)
         else:
         else:
             for i in range(len(self.animations)):
             for i in range(len(self.animations)):
@@ -335,8 +337,10 @@ class AnimationController(wx.EvtHandler):
                     self.animations[i].SetActive(False)
                     self.animations[i].SetActive(False)
                     continue
                     continue
                 anim = [anim for anim in self.animationData if anim.windowIndex == i][0]
                 anim = [anim for anim in self.animationData if anim.windowIndex == i][0]
+                regions = anim.GetRegions()
                 identifiers = sampleCmdMatrixAndCreateNames(anim.cmdMatrix,
                 identifiers = sampleCmdMatrixAndCreateNames(anim.cmdMatrix,
-                                                            mapNamesDict[anim.firstStdsNameType[0]])
+                                                            mapNamesDict[anim.firstStdsNameType[0]],
+                                                            regions)
                 self.animations[i].SetFrames(identifiers)
                 self.animations[i].SetFrames(identifiers)
                 self.animations[i].SetActive(True)
                 self.animations[i].SetActive(True)
 
 
@@ -367,7 +371,8 @@ class AnimationController(wx.EvtHandler):
 
 
     def _set2DData(self, animationData):
     def _set2DData(self, animationData):
         opacities = [layer.opacity for layer in animationData.layerList if layer.active]
         opacities = [layer.opacity for layer in animationData.layerList if layer.active]
-        self.bitmapProvider.SetCmds(animationData.cmdMatrix, opacities)
+        regions = animationData.GetRegions()
+        self.bitmapProvider.SetCmds(animationData.cmdMatrix, opacities, regions)
 
 
     def _load3DData(self, animationData):
     def _load3DData(self, animationData):
         nviz = animationData.GetNvizCommands()
         nviz = animationData.GetNvizCommands()

+ 78 - 1
gui/wxpython/animation/data.py

@@ -17,6 +17,7 @@ This program is free software under the GNU General Public License
 @author Anna Petrasova <kratochanna gmail.com>
 @author Anna Petrasova <kratochanna gmail.com>
 """
 """
 import os
 import os
+import copy
 
 
 from grass.script import core as gcore
 from grass.script import core as gcore
 
 
@@ -24,7 +25,7 @@ from core.utils import _
 from core.gcmd import GException
 from core.gcmd import GException
 from animation.nviztask import NvizTask
 from animation.nviztask import NvizTask
 from animation.utils import validateMapNames, getRegisteredMaps, \
 from animation.utils import validateMapNames, getRegisteredMaps, \
-    checkSeriesCompatibility, validateTimeseriesName
+    checkSeriesCompatibility, validateTimeseriesName, interpolate
 from core.layerlist import LayerList, Layer
 from core.layerlist import LayerList, Layer
 import grass.temporal as tgis
 import grass.temporal as tgis
 
 
@@ -50,6 +51,11 @@ class AnimationData(object):
         self.workspaceFile = None
         self.workspaceFile = None
         self.legendCmd = None
         self.legendCmd = None
 
 
+        self._startRegion = None
+        self._endRegion = None
+        self._zoomRegionValue = None
+        self._regions = None
+
     def GetName(self):
     def GetName(self):
         return self._name
         return self._name
 
 
@@ -180,6 +186,77 @@ class AnimationData(object):
 
 
         return {'commands': cmds, 'region': region}
         return {'commands': cmds, 'region': region}
 
 
+    def SetStartRegion(self, region):
+        self._startRegion = region
+
+    def GetStartRegion(self):
+        return self._startRegion
+
+    startRegion = property(fset=SetStartRegion, fget=GetStartRegion)
+
+    def SetEndRegion(self, region):
+        self._endRegion = region
+
+    def GetEndRegion(self):
+        return self._endRegion
+
+    endRegion = property(fset=SetEndRegion, fget=GetEndRegion)
+
+    def SetZoomRegionValue(self, value):
+        self._zoomRegionValue = value
+
+    def GetZoomRegionValue(self):
+        return self._zoomRegionValue
+
+    zoomRegionValue = property(fset=SetZoomRegionValue, fget=GetZoomRegionValue)
+
+    def GetRegions(self):
+        self._computeRegions(self._mapCount, self._startRegion,
+                             self._endRegion, self._zoomRegionValue)
+        return self._regions
+
+    def _computeRegions(self, count, startRegion, endRegion=None, zoomValue=None):
+        """Computes regions based on start region and end region or zoom value
+        for each of the animation frames."""
+        currRegion = dict(gcore.region())  # cast to dict, otherwise deepcopy error
+        del currRegion['cells']
+        del currRegion['cols']
+        del currRegion['rows']
+        regions = []
+        for i in range(self._mapCount):
+            if endRegion or zoomValue:
+                regions.append(copy.copy(currRegion))
+            else:
+                regions.append(None)
+        if not startRegion:
+            self._regions = regions
+            return
+
+        startRegionDict = gcore.parse_key_val(gcore.read_command('g.region', flags='gu',
+                                                                 region=startRegion),
+                                              val_type=float)
+        if endRegion:
+            endRegionDict = gcore.parse_key_val(gcore.read_command('g.region', flags='gu',
+                                                                   region=endRegion),
+                                                val_type=float)
+            for key in ('n', 's', 'e', 'w'):
+                values = interpolate(startRegionDict[key], endRegionDict[key], self._mapCount)
+                for value, region in zip(values, regions):
+                    region[key] = value
+
+        elif zoomValue:
+            for i in range(self._mapCount):
+                regions[i]['n'] -= zoomValue[0] * i
+                regions[i]['e'] -= zoomValue[1] * i
+                regions[i]['s'] += zoomValue[0] * i
+                regions[i]['w'] += zoomValue[1] * i
+
+                # handle cases when north < south and similarly EW
+                if regions[i]['n'] < regions[i]['s'] or \
+                   regions[i]['e'] < regions[i]['w']:
+                        regions[i] = regions[i - 1]
+        self._regions = regions
+
     def __repr__(self):
     def __repr__(self):
         return "%s(%r)" % (self.__class__, self.__dict__)
         return "%s(%r)" % (self.__class__, self.__dict__)
 
 

+ 145 - 31
gui/wxpython/animation/dialogs.py

@@ -39,6 +39,7 @@ from gui_core.forms import GUI
 from core.settings import UserSettings
 from core.settings import UserSettings
 from core.utils import _
 from core.utils import _
 from gui_core.gselect import Select
 from gui_core.gselect import Select
+from gui_core.widgets import FloatValidator
 
 
 from animation.utils import TemporalMode, getRegisteredMaps
 from animation.utils import TemporalMode, getRegisteredMaps
 from animation.data import AnimationData, AnimLayer
 from animation.data import AnimationData, AnimLayer
@@ -283,16 +284,40 @@ class InputDialog(wx.Dialog):
         self.OnViewMode(event=None)
         self.OnViewMode(event=None)
 
 
     def _layout(self):
     def _layout(self):
+        self.notebook = wx.Notebook(parent=self, style=wx.BK_DEFAULT)
+        sizer = wx.BoxSizer(wx.VERTICAL)
+        self.notebook.AddPage(self._createGeneralPage(self.notebook), _("General"))
+        self.notebook.AddPage(self._createAdvancedPage(self.notebook), _("Advanced"))
+        sizer.Add(self.notebook, proportion=1, flag=wx.ALL | wx.EXPAND, border=3)
+
+        # buttons
+        self.btnOk = wx.Button(self, wx.ID_OK)
+        self.btnCancel = wx.Button(self, wx.ID_CANCEL)
+        self.btnOk.SetDefault()
+        self.btnOk.Bind(wx.EVT_BUTTON, self.OnOk)
+        # button sizer
+        btnStdSizer = wx.StdDialogButtonSizer()
+        btnStdSizer.AddButton(self.btnOk)
+        btnStdSizer.AddButton(self.btnCancel)
+        btnStdSizer.Realize()
+
+        sizer.Add(item=btnStdSizer, proportion=0,
+                  flag=wx.EXPAND | wx.ALL | wx.ALIGN_RIGHT, border=5)
+        self.SetSizer(sizer)
+        sizer.Fit(self)
+
+    def _createGeneralPage(self, parent):
+        panel = wx.Panel(parent=parent)
         mainSizer = wx.BoxSizer(wx.VERTICAL)
         mainSizer = wx.BoxSizer(wx.VERTICAL)
 
 
-        self.windowChoice = wx.Choice(self, id=wx.ID_ANY,
+        self.windowChoice = wx.Choice(panel, id=wx.ID_ANY,
                                       choices=[_("top left"), _("top right"),
                                       choices=[_("top left"), _("top right"),
                                                _("bottom left"), _("bottom right")])
                                                _("bottom left"), _("bottom right")])
         self.windowChoice.SetSelection(self.animationData.windowIndex)
         self.windowChoice.SetSelection(self.animationData.windowIndex)
 
 
-        self.nameCtrl = wx.TextCtrl(self, id=wx.ID_ANY, value=self.animationData.name)
+        self.nameCtrl = wx.TextCtrl(panel, id=wx.ID_ANY, value=self.animationData.name)
 
 
-        self.nDChoice = wx.Choice(self, id=wx.ID_ANY)
+        self.nDChoice = wx.Choice(panel, id=wx.ID_ANY)
         mode = self.animationData.viewMode
         mode = self.animationData.viewMode
         index = 0
         index = 0
         for i, (viewMode, viewModeName) in enumerate(self.animationData.viewModes):
         for i, (viewMode, viewModeName) in enumerate(self.animationData.viewModes):
@@ -305,13 +330,13 @@ class InputDialog(wx.Dialog):
         self.nDChoice.Bind(wx.EVT_CHOICE, self.OnViewMode)
         self.nDChoice.Bind(wx.EVT_CHOICE, self.OnViewMode)
 
 
         gridSizer = wx.FlexGridSizer(cols=2, hgap=5, vgap=5)
         gridSizer = wx.FlexGridSizer(cols=2, hgap=5, vgap=5)
-        gridSizer.Add(item=wx.StaticText(self, id=wx.ID_ANY, label=_("Name:")),
+        gridSizer.Add(item=wx.StaticText(panel, id=wx.ID_ANY, label=_("Name:")),
                       flag=wx.ALIGN_CENTER_VERTICAL)
                       flag=wx.ALIGN_CENTER_VERTICAL)
         gridSizer.Add(item=self.nameCtrl, proportion=1, flag=wx.EXPAND)
         gridSizer.Add(item=self.nameCtrl, proportion=1, flag=wx.EXPAND)
-        gridSizer.Add(item=wx.StaticText(self, id=wx.ID_ANY, label=_("Window position:")),
+        gridSizer.Add(item=wx.StaticText(panel, id=wx.ID_ANY, label=_("Window position:")),
                       flag=wx.ALIGN_CENTER_VERTICAL)
                       flag=wx.ALIGN_CENTER_VERTICAL)
         gridSizer.Add(item=self.windowChoice, proportion=1, flag=wx.ALIGN_RIGHT)
         gridSizer.Add(item=self.windowChoice, proportion=1, flag=wx.ALIGN_RIGHT)
-        gridSizer.Add(item=wx.StaticText(self, id=wx.ID_ANY, label=_("View mode:")),
+        gridSizer.Add(item=wx.StaticText(panel, id=wx.ID_ANY, label=_("View mode:")),
                       flag=wx.ALIGN_CENTER_VERTICAL)
                       flag=wx.ALIGN_CENTER_VERTICAL)
         gridSizer.Add(item=self.nDChoice, proportion=1, flag=wx.ALIGN_RIGHT)
         gridSizer.Add(item=self.nDChoice, proportion=1, flag=wx.ALIGN_RIGHT)
         gridSizer.AddGrowableCol(0, 1)
         gridSizer.AddGrowableCol(0, 1)
@@ -319,40 +344,28 @@ class InputDialog(wx.Dialog):
         mainSizer.Add(item=gridSizer, proportion=0, flag=wx.ALL | wx.EXPAND, border=5)
         mainSizer.Add(item=gridSizer, proportion=0, flag=wx.ALL | wx.EXPAND, border=5)
         label = _("For 3D animation, please select only one space-time dataset\n"
         label = _("For 3D animation, please select only one space-time dataset\n"
                   "or one series of map layers.")
                   "or one series of map layers.")
-        self.warning3DLayers = wx.StaticText(self, label=label)
+        self.warning3DLayers = wx.StaticText(panel, label=label)
         self.warning3DLayers.SetForegroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_GRAYTEXT))
         self.warning3DLayers.SetForegroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_GRAYTEXT))
         mainSizer.Add(item=self.warning3DLayers, proportion=0, flag=wx.EXPAND | wx.LEFT, border=5)
         mainSizer.Add(item=self.warning3DLayers, proportion=0, flag=wx.EXPAND | wx.LEFT, border=5)
 
 
-        self.dataPanel = self._createDataPanel()
-        self.threeDPanel = self._create3DPanel()
+        self.dataPanel = self._createDataPanel(panel)
+        self.threeDPanel = self._create3DPanel(panel)
         mainSizer.Add(item=self.dataPanel, proportion=1, flag=wx.EXPAND | wx.ALL, border=3)
         mainSizer.Add(item=self.dataPanel, proportion=1, flag=wx.EXPAND | wx.ALL, border=3)
         mainSizer.Add(item=self.threeDPanel, proportion=0, flag=wx.EXPAND | wx.ALL, border=3)
         mainSizer.Add(item=self.threeDPanel, proportion=0, flag=wx.EXPAND | wx.ALL, border=3)
 
 
-        # buttons
-        self.btnOk = wx.Button(self, wx.ID_OK)
-        self.btnCancel = wx.Button(self, wx.ID_CANCEL)
-        self.btnOk.SetDefault()
-        self.btnOk.Bind(wx.EVT_BUTTON, self.OnOk)
-        # button sizer
-        btnStdSizer = wx.StdDialogButtonSizer()
-        btnStdSizer.AddButton(self.btnOk)
-        btnStdSizer.AddButton(self.btnCancel)
-        btnStdSizer.Realize()
+        panel.SetSizer(mainSizer)
+        mainSizer.Fit(panel)
 
 
-        mainSizer.Add(item=btnStdSizer, proportion=0,
-                      flag=wx.EXPAND | wx.ALL | wx.ALIGN_RIGHT, border=5)
-
-        self.SetSizer(mainSizer)
-        mainSizer.Fit(self)
+        return panel
 
 
-    def _createDataPanel(self):
-        panel = wx.Panel(self)
+    def _createDataPanel(self, parent):
+        panel = wx.Panel(parent)
         slmgrSizer = wx.BoxSizer(wx.VERTICAL)
         slmgrSizer = wx.BoxSizer(wx.VERTICAL)
         self._layerList = copy.deepcopy(self.animationData.layerList)
         self._layerList = copy.deepcopy(self.animationData.layerList)
         self.simpleLmgr = AnimSimpleLayerManager(parent=panel,
         self.simpleLmgr = AnimSimpleLayerManager(parent=panel,
                                                  layerList=self._layerList,
                                                  layerList=self._layerList,
                                                  modal=True)
                                                  modal=True)
-        self.simpleLmgr.SetMinSize((globalvar.DIALOG_GSELECT_SIZE[0], 120))
+        self.simpleLmgr.SetMinSize((globalvar.DIALOG_GSELECT_SIZE[0], 80))
         slmgrSizer.Add(self.simpleLmgr, proportion=1, flag=wx.EXPAND | wx.ALL, border=5)
         slmgrSizer.Add(self.simpleLmgr, proportion=1, flag=wx.EXPAND | wx.ALL, border=5)
 
 
         self.legend = wx.CheckBox(panel, label=_("Show raster legend"))
         self.legend = wx.CheckBox(panel, label=_("Show raster legend"))
@@ -371,8 +384,8 @@ class InputDialog(wx.Dialog):
 
 
         return panel
         return panel
 
 
-    def _create3DPanel(self):
-        panel = wx.Panel(self, id=wx.ID_ANY)
+    def _create3DPanel(self, parent):
+        panel = wx.Panel(parent, id=wx.ID_ANY)
         dataStBox = wx.StaticBox(parent=panel, id=wx.ID_ANY,
         dataStBox = wx.StaticBox(parent=panel, id=wx.ID_ANY,
                                  label=' %s ' % _("3D view parameters"))
                                  label=' %s ' % _("3D view parameters"))
         dataBoxSizer = wx.StaticBoxSizer(dataStBox, wx.VERTICAL)
         dataBoxSizer = wx.StaticBoxSizer(dataStBox, wx.VERTICAL)
@@ -407,16 +420,92 @@ class InputDialog(wx.Dialog):
 
 
         return panel
         return panel
 
 
+    def _createAdvancedPage(self, parent):
+        panel = wx.Panel(parent=parent)
+
+        mainSizer = wx.BoxSizer(wx.VERTICAL)
+        box = wx.StaticBox(parent=panel, label=" %s " % _("Animate region change (2D view only)"))
+        sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
+
+        gridSizer = wx.GridBagSizer(hgap=3, vgap=3)
+        gridSizer.Add(wx.StaticText(panel, label=_("Start region:")),
+                      pos=(0, 0), flag=wx.ALIGN_CENTER_VERTICAL)
+        self.stRegion = Select(parent=panel, type='region', size=(200, -1))
+        if self.animationData.startRegion:
+            self.stRegion.SetValue(self.animationData.startRegion)
+        gridSizer.Add(self.stRegion, pos=(0, 1), flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND)
+
+        self.endRegRadio = wx.RadioButton(panel, label=_("End region:"), style=wx.RB_GROUP)
+        gridSizer.Add(self.endRegRadio, pos=(1, 0), flag=wx.EXPAND)
+        self.endRegion = Select(parent=panel, type='region', size=(200, -1))
+        gridSizer.Add(self.endRegion, pos=(1, 1), flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND)
+        self.zoomRadio = wx.RadioButton(panel, label=_("Zoom value:"))
+        self.zoomRadio.SetToolTipString(_("N-S/E-W distances in map units used to "
+                                          "gradually reduce region."))
+        gridSizer.Add(self.zoomRadio, pos=(2, 0), flag=wx.EXPAND)
+
+        zoomSizer = wx.BoxSizer(wx.HORIZONTAL)
+        self.zoomNS = wx.TextCtrl(panel, validator=FloatValidator())
+        self.zoomEW = wx.TextCtrl(panel, validator=FloatValidator())
+        zoomSizer.Add(wx.StaticText(panel, label=_("N-S:")), proportion=0,
+                      flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=3)
+        zoomSizer.Add(self.zoomNS, proportion=1, flag=wx.LEFT, border=3)
+        zoomSizer.Add(wx.StaticText(panel, label=_("E-W:")), proportion=0,
+                      flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=3)
+        zoomSizer.Add(self.zoomEW, proportion=1, flag=wx.LEFT, border=3)
+        gridSizer.Add(zoomSizer, pos=(2, 1), flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND)
+        if self.animationData.endRegion:
+            self.endRegRadio.SetValue(True)
+            self.zoomRadio.SetValue(False)
+            self.endRegion.SetValue(self.animationData.endRegion)
+        if self.animationData.zoomRegionValue:
+            self.endRegRadio.SetValue(False)
+            self.zoomRadio.SetValue(True)
+            zoom = self.animationData.zoomRegionValue
+            self.zoomNS.SetValue(str(zoom[0]))
+            self.zoomEW.SetValue(str(zoom[1]))
+
+        self.endRegRadio.Bind(wx.EVT_RADIOBUTTON, lambda evt: self._enableRegionWidgets())
+        self.zoomRadio.Bind(wx.EVT_RADIOBUTTON, lambda evt: self._enableRegionWidgets())
+        self._enableRegionWidgets()
+
+        gridSizer.AddGrowableCol(1)
+        sizer.Add(gridSizer, proportion=0, flag=wx.EXPAND | wx.ALL, border=3)
+        mainSizer.Add(sizer, proportion=0, flag=wx.EXPAND | wx.ALL, border=3)
+
+        panel.SetSizer(mainSizer)
+        mainSizer.Fit(panel)
+
+        return panel
+
+    def _enableRegionWidgets(self):
+        """!Enables/disables region widgets
+        according to which radiobutton is active."""
+        endReg = self.endRegRadio.GetValue()
+        self.endRegion.Enable(endReg)
+        self.zoomNS.Enable(not endReg)
+        self.zoomEW.Enable(not endReg)
+
     def OnViewMode(self, event):
     def OnViewMode(self, event):
         mode = self.nDChoice.GetSelection()
         mode = self.nDChoice.GetSelection()
         self.Freeze()
         self.Freeze()
         self.simpleLmgr.Activate3D(mode == 1)
         self.simpleLmgr.Activate3D(mode == 1)
         self.warning3DLayers.Show(mode == 1)
         self.warning3DLayers.Show(mode == 1)
+
+        # disable region widgets for 3d
+        regSizer = self.stRegion.GetContainingSizer()
+        for child in regSizer.GetChildren():
+            if child.IsSizer():
+                for child_ in child.GetSizer().GetChildren():
+                    child_.GetWindow().Enable(mode != 1)
+            elif child.IsWindow():
+                child.GetWindow().Enable(mode != 1)
+        self._enableRegionWidgets()
+
+        # update layout
         sizer = self.threeDPanel.GetContainingSizer()
         sizer = self.threeDPanel.GetContainingSizer()
         sizer.Show(self.threeDPanel, mode == 1, True)
         sizer.Show(self.threeDPanel, mode == 1, True)
         sizer.Layout()
         sizer.Layout()
-        self.Layout()
-        self.Fit()
         self.Thaw()
         self.Thaw()
 
 
     def OnLegend(self, event):
     def OnLegend(self, event):
@@ -478,6 +567,31 @@ class InputDialog(wx.Dialog):
             self.animationData.workspaceFile = self.fileSelector.GetValue()
             self.animationData.workspaceFile = self.fileSelector.GetValue()
         if self.threeDPanel.IsShown():
         if self.threeDPanel.IsShown():
             self.animationData.nvizParameter = self.paramChoice.GetStringSelection()
             self.animationData.nvizParameter = self.paramChoice.GetStringSelection()
+        # region (2d only)
+        if self.animationData.viewMode == '3d':
+            self.animationData.startRegion = None
+            self.animationData.endRegion = None
+            self.animationData.zoomRegionValue = None
+            return
+        isEnd = self.endRegRadio.GetValue() and self.endRegion.GetValue()
+        isZoom = self.zoomRadio.GetValue() and self.zoomNS.GetValue() and self.zoomEW.GetValue()
+        isStart = self.stRegion.GetValue()
+        condition = bool(isStart) + bool(isZoom) + bool(isEnd)
+        if condition == 1:
+            raise GException(_("Region information is not complete"))
+        elif condition == 2:
+            self.animationData.startRegion = isStart
+            if isEnd:
+                self.animationData.endRegion = self.endRegion.GetValue()
+                self.animationData.zoomRegionValue = None
+            else:
+                self.animationData.zoomRegionValue = (float(self.zoomNS.GetValue()),
+                                                      float(self.zoomEW.GetValue()))
+                self.animationData.endRegion = None
+        else:
+            self.animationData.startRegion = None
+            self.animationData.endRegion = None
+            self.animationData.zoomRegionValue = None
 
 
     def OnOk(self, event):
     def OnOk(self, event):
         try:
         try:

+ 87 - 57
gui/wxpython/animation/provider.py

@@ -60,6 +60,8 @@ class BitmapProvider:
 
 
         self._cmds3D = []
         self._cmds3D = []
         self._regionFor3D = None
         self._regionFor3D = None
+        self._regions = []
+        self._regionsForUniqueCmds = []
 
 
         self._renderer = BitmapRenderer(self._mapFilesPool, self._tempDir,
         self._renderer = BitmapRenderer(self._mapFilesPool, self._tempDir,
                                         self.imageWidth, self.imageHeight)
                                         self.imageWidth, self.imageHeight)
@@ -77,7 +79,7 @@ class BitmapProvider:
         self._renderer.renderingContinues.connect(self.renderingContinues)
         self._renderer.renderingContinues.connect(self.renderingContinues)
         self._composer.compositionContinues.connect(self.compositionContinues)
         self._composer.compositionContinues.connect(self.compositionContinues)
 
 
-    def SetCmds(self, cmdsForComposition, opacities):
+    def SetCmds(self, cmdsForComposition, opacities, regions=None):
         """!Sets commands to be rendered with opacity levels.
         """!Sets commands to be rendered with opacity levels.
         Applies to 2D mode.
         Applies to 2D mode.
 
 
@@ -86,11 +88,14 @@ class BitmapProvider:
                  [['d.rast', 'map=elev_2002'], ['d.vect', 'map=points']],
                  [['d.rast', 'map=elev_2002'], ['d.vect', 'map=points']],
                  ...]
                  ...]
         @param opacities list of opacity values
         @param opacities list of opacity values
+        @param regions list of regions
         """
         """
         Debug.msg(2, "BitmapProvider.SetCmds: {} lists".format(len(cmdsForComposition)))
         Debug.msg(2, "BitmapProvider.SetCmds: {} lists".format(len(cmdsForComposition)))
         self._cmdsForComposition.extend(cmdsForComposition)
         self._cmdsForComposition.extend(cmdsForComposition)
-        self._uniqueCmds = self._getUniqueCmds()
         self._opacities.extend(opacities)
         self._opacities.extend(opacities)
+        self._regions.extend(regions)
+
+        self._getUniqueCmds()
 
 
     def SetCmds3D(self, cmds, region):
     def SetCmds3D(self, cmds, region):
         """!Sets commands for 3D rendering.
         """!Sets commands for 3D rendering.
@@ -103,12 +108,19 @@ class BitmapProvider:
         self._regionFor3D = region
         self._regionFor3D = region
 
 
     def _getUniqueCmds(self):
     def _getUniqueCmds(self):
-        """!Returns list of unique commands."""
-        unique = set()
-        for cmdList in self._cmdsForComposition:
+        """!Returns list of unique commands.
+        Takes into account the region assigned."""
+        unique = list()
+        for cmdList, region in zip(self._cmdsForComposition, self._regions):
             for cmd in cmdList:
             for cmd in cmdList:
-                unique.add(tuple(cmd))
-        return list(unique)
+                if region:
+                    unique.append((tuple(cmd), tuple(sorted(region.items()))))
+                else:
+                    unique.append((tuple(cmd), None))
+        unique = list(set(unique))
+        self._uniqueCmds = [cmdAndRegion[0] for cmdAndRegion in unique]
+        self._regionsForUniqueCmds.extend([dict(cmdAndRegion[1]) if cmdAndRegion[1] else None
+                                           for cmdAndRegion in unique])
 
 
     def Unload(self):
     def Unload(self):
         """!Unloads currently loaded data.
         """!Unloads currently loaded data.
@@ -116,29 +128,32 @@ class BitmapProvider:
         """
         """
         Debug.msg(2, "BitmapProvider.Unload")
         Debug.msg(2, "BitmapProvider.Unload")
         if self._cmdsForComposition:
         if self._cmdsForComposition:
-            for cmd in self._uniqueCmds:
-                del self._mapFilesPool[HashCmd(cmd)]
+            for cmd, region in zip(self._uniqueCmds, self._regionsForUniqueCmds):
+                del self._mapFilesPool[HashCmd(cmd, region)]
 
 
-            for cmdList in self._cmdsForComposition:
-                del self._bitmapPool[HashCmds(cmdList)]
+            for cmdList, region in zip(self._cmdsForComposition, self._regions):
+                del self._bitmapPool[HashCmds(cmdList, region)]
             self._uniqueCmds = []
             self._uniqueCmds = []
             self._cmdsForComposition = []
             self._cmdsForComposition = []
             self._opacities = []
             self._opacities = []
+            self._regions = []
+            self._regionsForUniqueCmds = []
         if self._cmds3D:
         if self._cmds3D:
             self._cmds3D = []
             self._cmds3D = []
             self._regionFor3D = None
             self._regionFor3D = None
 
 
-    def _dryRender(self, uniqueCmds, force):
+    def _dryRender(self, uniqueCmds, regions, force):
         """!Determines how many files will be rendered.
         """!Determines how many files will be rendered.
 
 
         @param uniqueCmds list of commands which are to be rendered
         @param uniqueCmds list of commands which are to be rendered
         @param force if forced rerendering
         @param force if forced rerendering
+        @param regions list of regions assigned to the commands
         """
         """
         count = 0
         count = 0
-        for cmd in uniqueCmds:
-            filename = GetFileFromCmd(self._tempDir, cmd)
+        for cmd, region in zip(uniqueCmds, regions):
+            filename = GetFileFromCmd(self._tempDir, cmd, region)
             if not force and os.path.exists(filename) and \
             if not force and os.path.exists(filename) and \
-               self._mapFilesPool.GetSize(HashCmd(cmd)) == (self.imageWidth, self.imageHeight):
+               self._mapFilesPool.GetSize(HashCmd(cmd, region)) == (self.imageWidth, self.imageHeight):
                 continue
                 continue
             count += 1
             count += 1
 
 
@@ -146,18 +161,19 @@ class BitmapProvider:
 
 
         return count
         return count
 
 
-    def _dryCompose(self, cmdLists, force):
+    def _dryCompose(self, cmdLists, regions, force):
         """!Determines how many lists of (commands) files
         """!Determines how many lists of (commands) files
         will be composed (with g.pnmcomp).
         will be composed (with g.pnmcomp).
 
 
         @param cmdLists list of commands lists which are to be composed
         @param cmdLists list of commands lists which are to be composed
+        @param regions list of regions assigned to the commands
         @param force if forced rerendering
         @param force if forced rerendering
         """
         """
         count = 0
         count = 0
-        for cmdList in cmdLists:
-            if not force and HashCmds(cmdList) in self._bitmapPool and \
-                self._bitmapPool[HashCmds(cmdList)].GetSize() == (self.imageWidth,
-                                                                  self.imageHeight):
+        for cmdList, region in zip(cmdLists, regions):
+            if not force and HashCmds(cmdList, region) in self._bitmapPool and \
+                self._bitmapPool[HashCmds(cmdList, region)].GetSize() == (self.imageWidth,
+                                                                          self.imageHeight):
                 continue
                 continue
             count += 1
             count += 1
 
 
@@ -176,34 +192,37 @@ class BitmapProvider:
         Debug.msg(2, "BitmapProvider.Load: "
         Debug.msg(2, "BitmapProvider.Load: "
                      "force={}, bgcolor={}, nprocs={}".format(force, bgcolor, nprocs))
                      "force={}, bgcolor={}, nprocs={}".format(force, bgcolor, nprocs))
         cmds = []
         cmds = []
+        regions = []
         if self._uniqueCmds:
         if self._uniqueCmds:
             cmds.extend(self._uniqueCmds)
             cmds.extend(self._uniqueCmds)
+            regions.extend(self._regionsForUniqueCmds)
         if self._cmds3D:
         if self._cmds3D:
             cmds.extend(self._cmds3D)
             cmds.extend(self._cmds3D)
+            regions.extend([None] * len(self._cmds3D))
 
 
-        count = self._dryRender(cmds, force=force)
+        count = self._dryRender(cmds, regions, force=force)
         self.renderingStarted.emit(count=count)
         self.renderingStarted.emit(count=count)
 
 
         # create no data bitmap
         # create no data bitmap
         if None not in self._bitmapPool or force:
         if None not in self._bitmapPool or force:
             self._bitmapPool[None] = createNoDataBitmap(self.imageWidth, self.imageHeight)
             self._bitmapPool[None] = createNoDataBitmap(self.imageWidth, self.imageHeight)
 
 
-        ok = self._renderer.Render(cmds, regionFor3D=self._regionFor3D,
+        ok = self._renderer.Render(cmds, regions, regionFor3D=self._regionFor3D,
                                    bgcolor=bgcolor, force=force, nprocs=nprocs)
                                    bgcolor=bgcolor, force=force, nprocs=nprocs)
         self.renderingFinished.emit()
         self.renderingFinished.emit()
         if not ok:
         if not ok:
             self.mapsLoaded.emit()  # what to do here?
             self.mapsLoaded.emit()  # what to do here?
             return
             return
         if self._cmdsForComposition:
         if self._cmdsForComposition:
-            count = self._dryCompose(self._cmdsForComposition, force=force)
+            count = self._dryCompose(self._cmdsForComposition, self._regions, force=force)
             self.compositionStarted.emit(count=count)
             self.compositionStarted.emit(count=count)
-            self._composer.Compose(self._cmdsForComposition, self._opacities,
+            self._composer.Compose(self._cmdsForComposition, self._regions, self._opacities,
                                    bgcolor=bgcolor, force=force, nprocs=nprocs)
                                    bgcolor=bgcolor, force=force, nprocs=nprocs)
             self.compositionFinished.emit()
             self.compositionFinished.emit()
         if self._cmds3D:
         if self._cmds3D:
             for cmd in self._cmds3D:
             for cmd in self._cmds3D:
-                self._bitmapPool[HashCmds([cmd])] = \
-                    wx.Bitmap(GetFileFromCmd(self._tempDir, cmd))
+                self._bitmapPool[HashCmds([cmd], None)] = \
+                    wx.Bitmap(GetFileFromCmd(self._tempDir, cmd, None))
 
 
         self.mapsLoaded.emit()
         self.mapsLoaded.emit()
 
 
@@ -273,10 +292,11 @@ class BitmapRenderer:
         self._stopRendering = False
         self._stopRendering = False
         self._isRendering = False
         self._isRendering = False
 
 
-    def Render(self, cmdList, regionFor3D, bgcolor, force, nprocs):
+    def Render(self, cmdList, regions, regionFor3D, bgcolor, force, nprocs):
         """!Renders all maps and stores files.
         """!Renders all maps and stores files.
 
 
         @param cmdList list of rendering commands to run
         @param cmdList list of rendering commands to run
+        @param regions regions for 2D rendering assigned to commands
         @param regionFor3D region for setting 3D view
         @param regionFor3D region for setting 3D view
         @param bgcolor background color as a tuple of 3 values 0 to 255
         @param bgcolor background color as a tuple of 3 values 0 to 255
         @param force if True reload all data, otherwise only missing data
         @param force if True reload all data, otherwise only missing data
@@ -292,19 +312,19 @@ class BitmapRenderer:
         cmd_list = []
         cmd_list = []
 
 
         filteredCmdList = []
         filteredCmdList = []
-        for cmd in cmdList:
-            filename = GetFileFromCmd(self._tempDir, cmd)
+        for cmd, region in zip(cmdList, regions):
+            filename = GetFileFromCmd(self._tempDir, cmd, region)
             if not force and os.path.exists(filename) and \
             if not force and os.path.exists(filename) and \
-               self._mapFilesPool.GetSize(HashCmd(cmd)) == (self.imageWidth, self.imageHeight):
+               self._mapFilesPool.GetSize(HashCmd(cmd, region)) == (self.imageWidth, self.imageHeight):
                 # for reference counting
                 # for reference counting
-                self._mapFilesPool[HashCmd(cmd)] = filename
+                self._mapFilesPool[HashCmd(cmd, region)] = filename
                 continue
                 continue
-            filteredCmdList.append(cmd)
+            filteredCmdList.append((cmd, region))
 
 
         mapNum = len(filteredCmdList)
         mapNum = len(filteredCmdList)
         stopped = False
         stopped = False
         self._isRendering = True
         self._isRendering = True
-        for cmd in filteredCmdList:
+        for cmd, region in filteredCmdList:
             count += 1
             count += 1
 
 
             # Queue object for interprocess communication
             # Queue object for interprocess communication
@@ -316,12 +336,13 @@ class BitmapRenderer:
                                   cmd, regionFor3D, bgcolor, q))
                                   cmd, regionFor3D, bgcolor, q))
             else:
             else:
                 p = Process(target=RenderProcess2D,
                 p = Process(target=RenderProcess2D,
-                            args=(self.imageWidth, self.imageHeight, self._tempDir, cmd, bgcolor, q))
+                            args=(self.imageWidth, self.imageHeight, self._tempDir,
+                                  cmd, region, bgcolor, q))
             p.start()
             p.start()
 
 
             queue_list.append(q)
             queue_list.append(q)
             proc_list.append(p)
             proc_list.append(p)
-            cmd_list.append(cmd)
+            cmd_list.append((cmd, region))
 
 
             proc_count += 1
             proc_count += 1
             # Wait for all running processes and read/store the created images
             # Wait for all running processes and read/store the created images
@@ -329,8 +350,8 @@ class BitmapRenderer:
                 for i in range(len(cmd_list)):
                 for i in range(len(cmd_list)):
                     proc_list[i].join()
                     proc_list[i].join()
                     filename = queue_list[i].get()
                     filename = queue_list[i].get()
-                    self._mapFilesPool[HashCmd(cmd_list[i])] = filename
-                    self._mapFilesPool.SetSize(HashCmd(cmd_list[i]),
+                    self._mapFilesPool[HashCmd(cmd_list[i][0], cmd_list[i][1])] = filename
+                    self._mapFilesPool.SetSize(HashCmd(cmd_list[i][0], cmd_list[i][1]),
                                                (self.imageWidth, self.imageHeight))
                                                (self.imageWidth, self.imageHeight))
 
 
                 proc_count = 0
                 proc_count = 0
@@ -367,10 +388,11 @@ class BitmapComposer:
         self._stopComposing = False
         self._stopComposing = False
         self._isComposing = False
         self._isComposing = False
 
 
-    def Compose(self, cmdLists, opacityList, bgcolor, force, nprocs):
+    def Compose(self, cmdLists, regions, opacityList, bgcolor, force, nprocs):
         """!Performs the composition of ppm/pgm files.
         """!Performs the composition of ppm/pgm files.
 
 
         @param cmdLisst lists of rendering commands lists to compose
         @param cmdLisst lists of rendering commands lists to compose
+        @param regions regions for 2D rendering assigned to commands
         @param opacityList list of lists of opacity values
         @param opacityList list of lists of opacity values
         @param bgcolor background color as a tuple of 3 values 0 to 255
         @param bgcolor background color as a tuple of 3 values 0 to 255
         @param force if True reload all data, otherwise only missing data
         @param force if True reload all data, otherwise only missing data
@@ -387,31 +409,31 @@ class BitmapComposer:
         cmd_lists = []
         cmd_lists = []
 
 
         filteredCmdLists = []
         filteredCmdLists = []
-        for cmdList in cmdLists:
-            if not force and HashCmds(cmdList) in self._bitmapPool and \
-                self._bitmapPool[HashCmds(cmdList)].GetSize() == (self.imageWidth,
-                                                                  self.imageHeight):
+        for cmdList, region in zip(cmdLists, regions):
+            if not force and HashCmds(cmdList, region) in self._bitmapPool and \
+                self._bitmapPool[HashCmds(cmdList, region)].GetSize() == (self.imageWidth,
+                                                                          self.imageHeight):
                 # TODO: find a better way than to assign the same to increase the reference
                 # TODO: find a better way than to assign the same to increase the reference
-                self._bitmapPool[HashCmds(cmdList)] = self._bitmapPool[HashCmds(cmdList)]
+                self._bitmapPool[HashCmds(cmdList, region)] = self._bitmapPool[HashCmds(cmdList, region)]
                 continue
                 continue
-            filteredCmdLists.append(cmdList)
+            filteredCmdLists.append((cmdList, region))
 
 
         num = len(filteredCmdLists)
         num = len(filteredCmdLists)
 
 
         self._isComposing = True
         self._isComposing = True
-        for cmdList in filteredCmdLists:
+        for cmdList, region in filteredCmdLists:
             count += 1
             count += 1
             # Queue object for interprocess communication
             # Queue object for interprocess communication
             q = Queue()
             q = Queue()
             # The separate render process
             # The separate render process
             p = Process(target=CompositeProcess,
             p = Process(target=CompositeProcess,
                         args=(self.imageWidth, self.imageHeight, self._tempDir,
                         args=(self.imageWidth, self.imageHeight, self._tempDir,
-                              cmdList, opacityList, bgcolor, q))
+                              cmdList, region, opacityList, bgcolor, q))
             p.start()
             p.start()
 
 
             queue_list.append(q)
             queue_list.append(q)
             proc_list.append(p)
             proc_list.append(p)
-            cmd_lists.append(cmdList)
+            cmd_lists.append((cmdList, region))
 
 
             proc_count += 1
             proc_count += 1
 
 
@@ -421,11 +443,11 @@ class BitmapComposer:
                     proc_list[i].join()
                     proc_list[i].join()
                     filename = queue_list[i].get()
                     filename = queue_list[i].get()
                     if filename is None:
                     if filename is None:
-                        self._bitmapPool[HashCmds(cmd_lists[i])] = \
+                        self._bitmapPool[HashCmds(cmd_lists[i][0], cmd_lists[i][1])] = \
                             createNoDataBitmap(self.imageWidth, self.imageHeight,
                             createNoDataBitmap(self.imageWidth, self.imageHeight,
                                                text="Failed to render")
                                                text="Failed to render")
                     else:
                     else:
-                        self._bitmapPool[HashCmds(cmd_lists[i])] = \
+                        self._bitmapPool[HashCmds(cmd_lists[i][0], cmd_lists[i][1])] = \
                             wx.BitmapFromImage(wx.Image(filename))
                             wx.BitmapFromImage(wx.Image(filename))
                         os.remove(filename)
                         os.remove(filename)
                 proc_count = 0
                 proc_count = 0
@@ -446,7 +468,7 @@ class BitmapComposer:
             self._stopComposing = True
             self._stopComposing = True
 
 
 
 
-def RenderProcess2D(imageWidth, imageHeight, tempDir, cmd, bgcolor, fileQueue):
+def RenderProcess2D(imageWidth, imageHeight, tempDir, cmd, region, bgcolor, fileQueue):
     """!Render raster or vector files as ppm image and write the
     """!Render raster or vector files as ppm image and write the
        resulting ppm filename in the provided file queue
        resulting ppm filename in the provided file queue
 
 
@@ -454,26 +476,32 @@ def RenderProcess2D(imageWidth, imageHeight, tempDir, cmd, bgcolor, fileQueue):
     @param imageHeight image height
     @param imageHeight image height
     @param tempDir directory for rendering
     @param tempDir directory for rendering
     @param cmd d.rast/d.vect command as a list
     @param cmd d.rast/d.vect command as a list
+    @param region region as a dict or None
     @param bgcolor background color as a tuple of 3 values 0 to 255
     @param bgcolor background color as a tuple of 3 values 0 to 255
     @param fileQueue the inter process communication queue
     @param fileQueue the inter process communication queue
     storing the file name of the image
     storing the file name of the image
     """
     """
 
 
-    filename = GetFileFromCmd(tempDir, cmd)
+    filename = GetFileFromCmd(tempDir, cmd, region)
     transparency = True
     transparency = True
 
 
     # Set the environment variables for this process
     # Set the environment variables for this process
     _setEnvironment(imageWidth, imageHeight, filename,
     _setEnvironment(imageWidth, imageHeight, filename,
                     transparent=transparency, bgcolor=bgcolor)
                     transparent=transparency, bgcolor=bgcolor)
-
+    if region:
+        os.environ['GRASS_REGION'] = gcore.region_env(**region)
     cmdTuple = CmdToTuple(cmd)
     cmdTuple = CmdToTuple(cmd)
     returncode, stdout, messages = read2_command(cmdTuple[0], **cmdTuple[1])
     returncode, stdout, messages = read2_command(cmdTuple[0], **cmdTuple[1])
     if returncode != 0:
     if returncode != 0:
         gcore.warning("Rendering failed:\n" + messages)
         gcore.warning("Rendering failed:\n" + messages)
         fileQueue.put(None)
         fileQueue.put(None)
+        if region:
+            os.environ.pop('GRASS_REGION')
         os.remove(filename)
         os.remove(filename)
         return
         return
 
 
+    if region:
+        os.environ.pop('GRASS_REGION')
     fileQueue.put(filename)
     fileQueue.put(filename)
 
 
 
 
@@ -485,12 +513,13 @@ def RenderProcess3D(imageWidth, imageHeight, tempDir, cmd, region, bgcolor, file
     @param imageHeight image height
     @param imageHeight image height
     @param tempDir directory for rendering
     @param tempDir directory for rendering
     @param cmd m.nviz.image command as a list
     @param cmd m.nviz.image command as a list
+    @param region region as a dict
     @param bgcolor background color as a tuple of 3 values 0 to 255
     @param bgcolor background color as a tuple of 3 values 0 to 255
     @param fileQueue the inter process communication queue
     @param fileQueue the inter process communication queue
     storing the file name of the image
     storing the file name of the image
     """
     """
 
 
-    filename = GetFileFromCmd(tempDir, cmd)
+    filename = GetFileFromCmd(tempDir, cmd, None)
     os.environ['GRASS_REGION'] = gcore.region_env(region3d=True, **region)
     os.environ['GRASS_REGION'] = gcore.region_env(region3d=True, **region)
     Debug.msg(1, "Render image to file " + str(filename))
     Debug.msg(1, "Render image to file " + str(filename))
 
 
@@ -512,7 +541,7 @@ def RenderProcess3D(imageWidth, imageHeight, tempDir, cmd, region, bgcolor, file
     fileQueue.put(filename)
     fileQueue.put(filename)
 
 
 
 
-def CompositeProcess(imageWidth, imageHeight, tempDir, cmdList, opacities, bgcolor, fileQueue):
+def CompositeProcess(imageWidth, imageHeight, tempDir, cmdList, region, opacities, bgcolor, fileQueue):
     """!Performs the composition of image ppm files and writes the
     """!Performs the composition of image ppm files and writes the
        resulting ppm filename in the provided file queue
        resulting ppm filename in the provided file queue
 
 
@@ -520,6 +549,7 @@ def CompositeProcess(imageWidth, imageHeight, tempDir, cmdList, opacities, bgcol
     @param imageHeight image height
     @param imageHeight image height
     @param tempDir directory for rendering
     @param tempDir directory for rendering
     @param cmdList list of d.rast/d.vect commands
     @param cmdList list of d.rast/d.vect commands
+    @param region region as a dict or None
     @param opacities list of opacities
     @param opacities list of opacities
     @param bgcolor background color as a tuple of 3 values 0 to 255
     @param bgcolor background color as a tuple of 3 values 0 to 255
     @param fileQueue the inter process communication queue
     @param fileQueue the inter process communication queue
@@ -529,9 +559,9 @@ def CompositeProcess(imageWidth, imageHeight, tempDir, cmdList, opacities, bgcol
     maps = []
     maps = []
     masks = []
     masks = []
     for cmd in cmdList:
     for cmd in cmdList:
-        maps.append(GetFileFromCmd(tempDir, cmd))
-        masks.append(GetFileFromCmd(tempDir, cmd, 'pgm'))
-    filename = GetFileFromCmds(tempDir, cmdList)
+        maps.append(GetFileFromCmd(tempDir, cmd, region))
+        masks.append(GetFileFromCmd(tempDir, cmd, region, 'pgm'))
+    filename = GetFileFromCmds(tempDir, cmdList, region)
     # Set the environment variables for this process
     # Set the environment variables for this process
     _setEnvironment(imageWidth, imageHeight, filename,
     _setEnvironment(imageWidth, imageHeight, filename,
                     transparent=False, bgcolor=bgcolor)
                     transparent=False, bgcolor=bgcolor)

+ 42 - 12
gui/wxpython/animation/utils.py

@@ -242,26 +242,30 @@ def WxImageToPil(image):
     return pilImage
     return pilImage
 
 
 
 
-def HashCmd(cmd):
-    """!Returns a hash from command given as a list."""
+def HashCmd(cmd, region):
+    """!Returns a hash from command given as a list and a region as a dict."""
     name = '_'.join(cmd)
     name = '_'.join(cmd)
+    if region:
+        name += str(sorted(region.items()))
     return hashlib.sha1(name).hexdigest()
     return hashlib.sha1(name).hexdigest()
 
 
 
 
-def HashCmds(cmds):
-    """!Returns a hash from list of commands."""
+def HashCmds(cmds, region):
+    """!Returns a hash from list of commands and regions as dicts."""
     name = ';'.join([item for sublist in cmds for item in sublist])
     name = ';'.join([item for sublist in cmds for item in sublist])
+    if region:
+        name += str(sorted(region.items()))
     return hashlib.sha1(name).hexdigest()
     return hashlib.sha1(name).hexdigest()
 
 
 
 
-def GetFileFromCmd(dirname, cmd, extension='ppm'):
-    """!Returns file path created as a hash from command."""
-    return os.path.join(dirname, HashCmd(cmd) + '.' + extension)
+def GetFileFromCmd(dirname, cmd, region, extension='ppm'):
+    """!Returns file path created as a hash from command and region."""
+    return os.path.join(dirname, HashCmd(cmd, region) + '.' + extension)
 
 
 
 
-def GetFileFromCmds(dirname, cmds, extension='ppm'):
-    """!Returns file path created as a hash from list of commands."""
-    return os.path.join(dirname, HashCmds(cmds) + '.' + extension)
+def GetFileFromCmds(dirname, cmds, region, extension='ppm'):
+    """!Returns file path created as a hash from list of commands and regions."""
+    return os.path.join(dirname, HashCmds(cmds, region) + '.' + extension)
 
 
 
 
 def layerListToCmdsMatrix(layerList):
 def layerListToCmdsMatrix(layerList):
@@ -295,7 +299,7 @@ def layerListToCmdsMatrix(layerList):
     return zip(*cmdsForComposition)
     return zip(*cmdsForComposition)
 
 
 
 
-def sampleCmdMatrixAndCreateNames(cmdMatrix, sampledSeries):
+def sampleCmdMatrixAndCreateNames(cmdMatrix, sampledSeries, regions):
     """!Applies information from temporal sampling
     """!Applies information from temporal sampling
     to the command matrix."""
     to the command matrix."""
     namesList = []
     namesList = []
@@ -306,7 +310,7 @@ def sampleCmdMatrixAndCreateNames(cmdMatrix, sampledSeries):
             if lastName != name:
             if lastName != name:
                 lastName = name
                 lastName = name
                 j += 1
                 j += 1
-            namesList.append(HashCmds(cmdMatrix[j]))
+            namesList.append(HashCmds(cmdMatrix[j], regions[j]))
         else:
         else:
             namesList.append(None)
             namesList.append(None)
     assert(j == len(cmdMatrix) - 1)
     assert(j == len(cmdMatrix) - 1)
@@ -321,3 +325,29 @@ def getCpuCount():
         return cpu_count()
         return cpu_count()
     except NotImplementedError:
     except NotImplementedError:
         return 4
         return 4
+
+
+def interpolate(start, end, count):
+    """!Interpolates values between start and end.
+
+    @param start start value (float)
+    @param end end value (float)
+    @param count total number of values including start and end
+
+    >>> interpolate(0, 10, 5)
+    [0, 2.5, 5.0, 7.5, 10]
+    >>> interpolate(10, 0, 5)
+    [10, 7.5, 5.0, 2.5, 0]
+    """
+    step = (end - start) / float(count - 1)
+    values = []
+    if start < end:
+        while start < end:
+            values.append(start)
+            start += step
+    else:
+        while end < start:
+            values.append(start)
+            start += step
+    values.append(end)
+    return values