# -*- coding: utf-8 -*- """! @package animation.provider @brief Animation files and bitmaps management Classes: - mapwindow::BitmapProvider - mapwindow::BitmapRenderer - mapwindow::BitmapComposer - mapwindow::DictRefCounter - mapwindow::MapFilesPool - mapwindow::BitmapPool - mapwindow::CleanUp (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 """ import os import sys import wx import tempfile from multiprocessing import Process, Queue if __name__ == '__main__': sys.path.append(os.path.join(os.environ['GISBASE'], "etc", "gui", "wxpython")) from core.gcmd import RunCommand, GException from core.settings import UserSettings from core.debug import Debug from core.utils import _, CmdToTuple, autoCropImageFromFile from animation.utils import HashCmd, HashCmds, GetFileFromCmd, GetFileFromCmds import grass.script.core as gcore from grass.pydispatch.signal import Signal class BitmapProvider: """!Class for management of image files and bitmaps. There is one instance of this class in the application. It handles both 2D and 3D animations. """ def __init__(self, bitmapPool, mapFilesPool, tempDir, imageWidth=640, imageHeight=480): self._bitmapPool = bitmapPool self._mapFilesPool = mapFilesPool self.imageWidth = imageWidth # width of the image to render with d.rast or d.vect self.imageHeight = imageHeight # height of the image to render with d.rast or d.vect self._tempDir = tempDir self._uniqueCmds = [] self._cmdsForComposition = [] self._opacities = [] self._cmds3D = [] self._regionFor3D = None self._regions = [] self._regionsForUniqueCmds = [] self._renderer = BitmapRenderer(self._mapFilesPool, self._tempDir, self.imageWidth, self.imageHeight) self._composer = BitmapComposer(self._tempDir, self._mapFilesPool, self._bitmapPool, self.imageWidth, self.imageHeight) self.renderingStarted = Signal('BitmapProvider.renderingStarted') self.compositionStarted = Signal('BitmapProvider.compositionStarted') self.renderingContinues = Signal('BitmapProvider.renderingContinues') self.compositionContinues = Signal('BitmapProvider.compositionContinues') self.renderingFinished = Signal('BitmapProvider.renderingFinished') self.compositionFinished = Signal('BitmapProvider.compositionFinished') self.mapsLoaded = Signal('BitmapProvider.mapsLoaded') self._renderer.renderingContinues.connect(self.renderingContinues) self._composer.compositionContinues.connect(self.compositionContinues) def SetCmds(self, cmdsForComposition, opacities, regions=None): """!Sets commands to be rendered with opacity levels. Applies to 2D mode. @param cmdsForComposition list of lists of command lists [[['d.rast', 'map=elev_2001'], ['d.vect', 'map=points']], # g.pnmcomp [['d.rast', 'map=elev_2002'], ['d.vect', 'map=points']], ...] @param opacities list of opacity values @param regions list of regions """ Debug.msg(2, "BitmapProvider.SetCmds: {n} lists".format(n=len(cmdsForComposition))) self._cmdsForComposition.extend(cmdsForComposition) self._opacities.extend(opacities) self._regions.extend(regions) self._getUniqueCmds() def SetCmds3D(self, cmds, region): """!Sets commands for 3D rendering. @param cmds list of commands m.nviz.image (cmd as a list) @param region for 3D rendering """ Debug.msg(2, "BitmapProvider.SetCmds3D: {c} commands".format(c=len(cmds))) self._cmds3D = cmds self._regionFor3D = region def _getUniqueCmds(self): """!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: 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): """!Unloads currently loaded data. Needs to be called before setting new data. """ Debug.msg(2, "BitmapProvider.Unload") if self._cmdsForComposition: for cmd, region in zip(self._uniqueCmds, self._regionsForUniqueCmds): del self._mapFilesPool[HashCmd(cmd, region)] for cmdList, region in zip(self._cmdsForComposition, self._regions): del self._bitmapPool[HashCmds(cmdList, region)] self._uniqueCmds = [] self._cmdsForComposition = [] self._opacities = [] self._regions = [] self._regionsForUniqueCmds = [] if self._cmds3D: self._cmds3D = [] self._regionFor3D = None def _dryRender(self, uniqueCmds, regions, force): """!Determines how many files will be rendered. @param uniqueCmds list of commands which are to be rendered @param force if forced rerendering @param regions list of regions assigned to the commands """ count = 0 for cmd, region in zip(uniqueCmds, regions): filename = GetFileFromCmd(self._tempDir, cmd, region) if not force and os.path.exists(filename) and \ self._mapFilesPool.GetSize(HashCmd(cmd, region)) == (self.imageWidth, self.imageHeight): continue count += 1 Debug.msg(3, "BitmapProvider._dryRender: {c} files to be rendered".format(c=count)) return count def _dryCompose(self, cmdLists, regions, force): """!Determines how many lists of (commands) files will be composed (with g.pnmcomp). @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 """ count = 0 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 count += 1 Debug.msg(2, "BitmapProvider._dryCompose: {c} files to be composed".format(c=count)) return count def Load(self, force=False, bgcolor=(255, 255, 255), nprocs=4): """!Loads data, both 2D and 3D. In case of 2D, it creates composites, even when there is only 1 layer to compose (to be changed for speedup) @param force if True reload all data, otherwise only missing data @param bgcolor background color as a tuple of 3 values 0 to 255 @param nprocs number of procs to be used for rendering """ Debug.msg(2, "BitmapProvider.Load: " "force={f}, bgcolor={b}, nprocs={n}".format(f=force, b=bgcolor, n=nprocs)) cmds = [] regions = [] if self._uniqueCmds: cmds.extend(self._uniqueCmds) regions.extend(self._regionsForUniqueCmds) if self._cmds3D: cmds.extend(self._cmds3D) regions.extend([None] * len(self._cmds3D)) count = self._dryRender(cmds, regions, force=force) self.renderingStarted.emit(count=count) # create no data bitmap if None not in self._bitmapPool or force: self._bitmapPool[None] = createNoDataBitmap(self.imageWidth, self.imageHeight) ok = self._renderer.Render(cmds, regions, regionFor3D=self._regionFor3D, bgcolor=bgcolor, force=force, nprocs=nprocs) self.renderingFinished.emit() if not ok: self.mapsLoaded.emit() # what to do here? return if self._cmdsForComposition: count = self._dryCompose(self._cmdsForComposition, self._regions, force=force) self.compositionStarted.emit(count=count) self._composer.Compose(self._cmdsForComposition, self._regions, self._opacities, bgcolor=bgcolor, force=force, nprocs=nprocs) self.compositionFinished.emit() if self._cmds3D: for cmd in self._cmds3D: self._bitmapPool[HashCmds([cmd], None)] = \ wx.Bitmap(GetFileFromCmd(self._tempDir, cmd, None)) self.mapsLoaded.emit() def RequestStopRendering(self): """!Requests to stop rendering/composition""" Debug.msg(2, "BitmapProvider.RequestStopRendering") self._renderer.RequestStopRendering() self._composer.RequestStopComposing() def GetBitmap(self, dataId): """!Returns bitmap with given key or 'no data' bitmap if no such key exists. @param dataId name of bitmap """ try: bitmap = self._bitmapPool[dataId] except KeyError: bitmap = self._bitmapPool[None] return bitmap def WindowSizeChanged(self, width, height): """!Sets size when size of related window changes.""" Debug.msg(5, "BitmapProvider.WindowSizeChanged: w={w}, h={h}".format(w=width, h=height)) self.imageWidth, self.imageHeight = width, height self._composer.imageWidth = self._renderer.imageWidth = width self._composer.imageHeight = self._renderer.imageHeight = height def LoadOverlay(self, cmd): """!Creates raster legend with d.legend @param cmd d.legend command as a list @return bitmap with legend """ Debug.msg(5, "BitmapProvider.LoadOverlay: cmd={c}".format(c=cmd)) fileHandler, filename = tempfile.mkstemp(suffix=".png") os.close(fileHandler) # Set the environment variables for this process _setEnvironment(self.imageWidth, self.imageHeight, filename, transparent=True, bgcolor=(0, 0, 0)) Debug.msg(1, "Render raster legend " + str(filename)) cmdTuple = CmdToTuple(cmd) returncode, stdout, messages = read2_command(cmdTuple[0], **cmdTuple[1]) if returncode == 0: return wx.BitmapFromImage(autoCropImageFromFile(filename)) else: os.remove(filename) raise GException(messages) class BitmapRenderer: """!Class which renderes 2D and 3D images to files.""" def __init__(self, mapFilesPool, tempDir, imageWidth, imageHeight): self._mapFilesPool = mapFilesPool self._tempDir = tempDir self.imageWidth = imageWidth self.imageHeight = imageHeight self.renderingContinues = Signal('BitmapRenderer.renderingContinues') self._stopRendering = False self._isRendering = False def Render(self, cmdList, regions, regionFor3D, bgcolor, force, nprocs): """!Renders all maps and stores files. @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 bgcolor background color as a tuple of 3 values 0 to 255 @param force if True reload all data, otherwise only missing data @param nprocs number of procs to be used for rendering """ Debug.msg(3, "BitmapRenderer.Render") count = 0 # Variables for parallel rendering proc_count = 0 proc_list = [] queue_list = [] cmd_list = [] filteredCmdList = [] for cmd, region in zip(cmdList, regions): filename = GetFileFromCmd(self._tempDir, cmd, region) if not force and os.path.exists(filename) and \ self._mapFilesPool.GetSize(HashCmd(cmd, region)) == (self.imageWidth, self.imageHeight): # for reference counting self._mapFilesPool[HashCmd(cmd, region)] = filename continue filteredCmdList.append((cmd, region)) mapNum = len(filteredCmdList) stopped = False self._isRendering = True for cmd, region in filteredCmdList: count += 1 # Queue object for interprocess communication q = Queue() # The separate render process if cmd[0] == 'm.nviz.image': p = Process(target=RenderProcess3D, args=(self.imageWidth, self.imageHeight, self._tempDir, cmd, regionFor3D, bgcolor, q)) else: p = Process(target=RenderProcess2D, args=(self.imageWidth, self.imageHeight, self._tempDir, cmd, region, bgcolor, q)) p.start() queue_list.append(q) proc_list.append(p) cmd_list.append((cmd, region)) proc_count += 1 # Wait for all running processes and read/store the created images if proc_count == nprocs or count == mapNum: for i in range(len(cmd_list)): proc_list[i].join() filename = queue_list[i].get() 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)) proc_count = 0 proc_list = [] queue_list = [] cmd_list = [] self.renderingContinues.emit(current=count, text=_("Rendering map layers")) if self._stopRendering: self._stopRendering = False stopped = True break self._isRendering = False return not stopped def RequestStopRendering(self): """!Requests to stop rendering.""" if self._isRendering: self._stopRendering = True class BitmapComposer: """!Class which handles the composition of image files with g.pnmcomp.""" def __init__(self, tempDir, mapFilesPool, bitmapPool, imageWidth, imageHeight): self._mapFilesPool = mapFilesPool self._bitmapPool = bitmapPool self._tempDir = tempDir self.imageWidth = imageWidth self.imageHeight = imageHeight self.compositionContinues = Signal('BitmapComposer.composingContinues') self._stopComposing = False self._isComposing = False def Compose(self, cmdLists, regions, opacityList, bgcolor, force, nprocs): """!Performs the composition of ppm/pgm files. @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 bgcolor background color as a tuple of 3 values 0 to 255 @param force if True reload all data, otherwise only missing data @param nprocs number of procs to be used for rendering """ Debug.msg(3, "BitmapComposer.Compose") count = 0 # Variables for parallel rendering proc_count = 0 proc_list = [] queue_list = [] cmd_lists = [] filteredCmdLists = [] 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 self._bitmapPool[HashCmds(cmdList, region)] = self._bitmapPool[HashCmds(cmdList, region)] continue filteredCmdLists.append((cmdList, region)) num = len(filteredCmdLists) self._isComposing = True for cmdList, region in filteredCmdLists: count += 1 # Queue object for interprocess communication q = Queue() # The separate render process p = Process(target=CompositeProcess, args=(self.imageWidth, self.imageHeight, self._tempDir, cmdList, region, opacityList, bgcolor, q)) p.start() queue_list.append(q) proc_list.append(p) cmd_lists.append((cmdList, region)) proc_count += 1 # Wait for all running processes and read/store the created images if proc_count == nprocs or count == num: for i in range(len(cmd_lists)): proc_list[i].join() filename = queue_list[i].get() if filename is None: self._bitmapPool[HashCmds(cmd_lists[i][0], cmd_lists[i][1])] = \ createNoDataBitmap(self.imageWidth, self.imageHeight, text="Failed to render") else: self._bitmapPool[HashCmds(cmd_lists[i][0], cmd_lists[i][1])] = \ wx.BitmapFromImage(wx.Image(filename)) os.remove(filename) proc_count = 0 proc_list = [] queue_list = [] cmd_lists = [] self.compositionContinues.emit(current=count, text=_("Overlaying map layers")) if self._stopComposing: self._stopComposing = False break self._isComposing = False def RequestStopComposing(self): """!Requests to stop the composition.""" if self._isComposing: self._stopComposing = True def RenderProcess2D(imageWidth, imageHeight, tempDir, cmd, region, bgcolor, fileQueue): """!Render raster or vector files as ppm image and write the resulting ppm filename in the provided file queue @param imageWidth image width @param imageHeight image height @param tempDir directory for rendering @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 fileQueue the inter process communication queue storing the file name of the image """ filename = GetFileFromCmd(tempDir, cmd, region) transparency = True # Set the environment variables for this process _setEnvironment(imageWidth, imageHeight, filename, transparent=transparency, bgcolor=bgcolor) if region: os.environ['GRASS_REGION'] = gcore.region_env(**region) cmdTuple = CmdToTuple(cmd) returncode, stdout, messages = read2_command(cmdTuple[0], **cmdTuple[1]) if returncode != 0: gcore.warning("Rendering failed:\n" + messages) fileQueue.put(None) if region: os.environ.pop('GRASS_REGION') os.remove(filename) return if region: os.environ.pop('GRASS_REGION') fileQueue.put(filename) def RenderProcess3D(imageWidth, imageHeight, tempDir, cmd, region, bgcolor, fileQueue): """!Renders image with m.nviz.image and writes the resulting ppm filename in the provided file queue @param imageWidth image width @param imageHeight image height @param tempDir directory for rendering @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 fileQueue the inter process communication queue storing the file name of the image """ filename = GetFileFromCmd(tempDir, cmd, None) os.environ['GRASS_REGION'] = gcore.region_env(region3d=True, **region) Debug.msg(1, "Render image to file " + str(filename)) cmdTuple = CmdToTuple(cmd) cmdTuple[1]['output'] = os.path.splitext(filename)[0] # set size cmdTuple[1]['size'] = '%d,%d' % (imageWidth, imageHeight) # set format cmdTuple[1]['format'] = 'ppm' cmdTuple[1]['bgcolor'] = bgcolor = ':'.join([str(part) for part in bgcolor]) returncode, stdout, messages = read2_command(cmdTuple[0], **cmdTuple[1]) if returncode != 0: gcore.warning("Rendering failed:\n" + messages) fileQueue.put(None) os.environ.pop('GRASS_REGION') return os.environ.pop('GRASS_REGION') fileQueue.put(filename) def CompositeProcess(imageWidth, imageHeight, tempDir, cmdList, region, opacities, bgcolor, fileQueue): """!Performs the composition of image ppm files and writes the resulting ppm filename in the provided file queue @param imageWidth image width @param imageHeight image height @param tempDir directory for rendering @param cmdList list of d.rast/d.vect commands @param region region as a dict or None @param opacities list of opacities @param bgcolor background color as a tuple of 3 values 0 to 255 @param fileQueue the inter process communication queue storing the file name of the image """ maps = [] masks = [] for cmd in 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 _setEnvironment(imageWidth, imageHeight, filename, transparent=False, bgcolor=bgcolor) opacities = [str(op) for op in opacities] bgcolor = ':'.join([str(part) for part in bgcolor]) returncode, stdout, messages = read2_command('g.pnmcomp', overwrite=True, input='%s' % ",".join(reversed(maps)), mask='%s' % ",".join(reversed(masks)), opacity='%s' % ",".join(reversed(opacities)), bgcolor=bgcolor, width=imageWidth, height=imageHeight, output=filename) if returncode != 0: gcore.warning("Rendering composite failed:\n" + messages) fileQueue.put(None) os.remove(filename) return fileQueue.put(filename) class DictRefCounter: """!Base class storing map files/bitmaps (emulates dictionary). Counts the references to know which files/bitmaps to delete.""" def __init__(self): self.dictionary = {} self.referenceCount = {} def __getitem__(self, key): return self.dictionary[key] def __setitem__(self, key, value): self.dictionary[key] = value if key not in self.referenceCount: self.referenceCount[key] = 1 else: self.referenceCount[key] += 1 Debug.msg(5, 'DictRefCounter.__setitem__: +1 for key {k}'.format(k=key)) def __contains__(self, key): return key in self.dictionary def __delitem__(self, key): self.referenceCount[key] -= 1 Debug.msg(5, 'DictRefCounter.__delitem__: -1 for key {k}'.format(k=key)) def keys(self): return self.dictionary.keys() def Clear(self): """!Clears items which are not needed any more.""" Debug.msg(4, 'DictRefCounter.Clear') for key in self.dictionary.keys(): if key is not None: if self.referenceCount[key] <= 0: del self.dictionary[key] del self.referenceCount[key] class MapFilesPool(DictRefCounter): """!Stores rendered images as files.""" def __init__(self): DictRefCounter.__init__(self) self.size = {} def SetSize(self, key, size): self.size[key] = size def GetSize(self, key): return self.size[key] def Clear(self): """!Removes files which are not needed anymore. Removes both ppm and pgm. """ Debug.msg(4, 'MapFilesPool.Clear') for key in self.dictionary.keys(): if self.referenceCount[key] <= 0: name, ext = os.path.splitext(self.dictionary[key]) os.remove(self.dictionary[key]) if ext == '.ppm': os.remove(name + '.pgm') del self.dictionary[key] del self.referenceCount[key] del self.size[key] class BitmapPool(DictRefCounter): """!Class storing bitmaps (emulates dictionary)""" def __init__(self): DictRefCounter.__init__(self) class CleanUp: """!Responsible for cleaning up the files.""" def __init__(self, tempDir): self._tempDir = tempDir def __call__(self): import shutil if os.path.exists(self._tempDir): try: shutil.rmtree(self._tempDir) Debug.msg(5, 'CleanUp: removed directory {t}'.format(t=self._tempDir)) except OSError: gcore.warning(_("Directory {t} not removed.").format(t=self._tempDir)) def _setEnvironment(width, height, filename, transparent, bgcolor): """!Sets environmental variables for 2D rendering. @param width rendering width @param height rendering height @param filename file name @param transparent use transparency @param bgcolor background color as a tuple of 3 values 0 to 255 """ Debug.msg(5, "_setEnvironment: width={w}, height={h}, " "filename={f}, transparent={t}, bgcolor={b}".format(w=width, h=height, f=filename, t=transparent, b=bgcolor)) os.environ['GRASS_WIDTH'] = str(width) os.environ['GRASS_HEIGHT'] = str(height) driver = UserSettings.Get(group='display', key='driver', subkey='type') os.environ['GRASS_RENDER_IMMEDIATE'] = driver os.environ['GRASS_BACKGROUNDCOLOR'] = '{r:02x}{g:02x}{b:02x}'.format(r=bgcolor[0], g=bgcolor[1], b=bgcolor[2]) os.environ['GRASS_TRUECOLOR'] = "TRUE" if transparent: os.environ['GRASS_TRANSPARENT'] = "TRUE" else: os.environ['GRASS_TRANSPARENT'] = "FALSE" os.environ['GRASS_PNGFILE'] = str(filename) def createNoDataBitmap(imageWidth, imageHeight, text="No data"): """!Creates 'no data' bitmap. Used when requested bitmap is not available (loading data was not successful) or we want to show 'no data' bitmap. @param imageWidth image width @param imageHeight image height """ Debug.msg(4, "createNoDataBitmap: w={w}, h={h}, text={t}".format(w=imageWidth, h=imageHeight, t=text)) bitmap = wx.EmptyBitmap(imageWidth, imageHeight) dc = wx.MemoryDC() dc.SelectObject(bitmap) dc.Clear() text = _(text) dc.SetFont(wx.Font(pointSize=40, family=wx.FONTFAMILY_SCRIPT, style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD)) tw, th = dc.GetTextExtent(text) dc.DrawText(text, (imageWidth - tw) / 2, (imageHeight - th) / 2) dc.SelectObject(wx.NullBitmap) return bitmap def read2_command(*args, **kwargs): kwargs['stdout'] = gcore.PIPE kwargs['stderr'] = gcore.PIPE ps = gcore.start_command(*args, **kwargs) stdout, stderr = ps.communicate() return ps.returncode, stdout, stderr def test(): import shutil from core.layerlist import LayerList, Layer from animation.data import AnimLayer from animation.utils import layerListToCmdsMatrix import grass.temporal as tgis tgis.init() layerList = LayerList() layer = AnimLayer() layer.mapType = 'strds' layer.name = 'JR' layer.cmd = ['d.rast', 'map=elev_2007_1m'] layerList.AddLayer(layer) layer = Layer() layer.mapType = 'vect' layer.name = 'buildings_2009_approx' layer.cmd = ['d.vect', 'map=buildings_2009_approx', 'color=grey'] layer.opacity = 50 layerList.AddLayer(layer) bPool = BitmapPool() mapFilesPool = MapFilesPool() tempDir = '/tmp/test' if os.path.exists(tempDir): shutil.rmtree(tempDir) os.mkdir(tempDir) # comment this line to keep the directory after prgm ends # cleanUp = CleanUp(tempDir) # import atexit # atexit.register(cleanUp) prov = BitmapProvider(bPool, mapFilesPool, tempDir, imageWidth=640, imageHeight=480) prov.renderingStarted.connect( lambda count: sys.stdout.write("Total number of maps: {c}\n".format(c=count))) prov.renderingContinues.connect( lambda current, text: sys.stdout.write("Current number: {c}\n".format(c=current))) prov.compositionStarted.connect( lambda count: sys.stdout.write("Composition: total number of maps: {c}\n".format(c=count))) prov.compositionContinues.connect( lambda current, text: sys.stdout.write("Composition: Current number: {c}\n".format(c=current))) prov.mapsLoaded.connect( lambda: sys.stdout.write("Maps loading finished\n")) cmdMatrix = layerListToCmdsMatrix(layerList) prov.SetCmds(cmdMatrix, [l.opacity for l in layerList]) app = wx.App() prov.Load(bgcolor=(13, 156, 230), nprocs=4) for key in bPool.keys(): if key is not None: bPool[key].SaveFile(os.path.join(tempDir, key + '.png'), wx.BITMAP_TYPE_PNG) # prov.Unload() # prov.SetCmds(cmdMatrix, [l.opacity for l in layerList]) # prov.Load(bgcolor=(13, 156, 230)) # prov.Unload() # newList = LayerList() # prov.SetCmds(layerListToCmdsMatrix(newList), [l.opacity for l in newList]) # prov.Load() # prov.Unload() # mapFilesPool.Clear() # bPool.Clear() # print bPool.keys(), mapFilesPool.keys() if __name__ == '__main__': test()