123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582 |
- """!
- @package animation.mapwindow
- @brief Animation window and bitmaps management
- Classes:
- - mapwindow::BufferedWindow
- - mapwindow::AnimationWindow
- - mapwindow::BitmapProvider
- - mapwindow::BitmapPool
- (C) 2012 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 Kratochvilova <kratochanna gmail.com>
- """
- import os
- import wx
- from multiprocessing import Process, Queue
- import tempfile
- import grass.script as grass
- from core.gcmd import RunCommand, GException
- from core.debug import Debug
- from core.settings import UserSettings
- from core.utils import _, CmdToTuple, autoCropImageFromFile
- from grass.pydispatch.signal import Signal
- class BufferedWindow(wx.Window):
- """
- A Buffered window class (http://wiki.wxpython.org/DoubleBufferedDrawing).
- To use it, subclass it and define a Draw(DC) method that takes a DC
- to draw to. In that method, put the code needed to draw the picture
- you want. The window will automatically be double buffered, and the
- screen will be automatically updated when a Paint event is received.
- When the drawing needs to change, you app needs to call the
- UpdateDrawing() method. Since the drawing is stored in a bitmap, you
- can also save the drawing to file by calling the
- SaveToFile(self, file_name, file_type) method.
- """
- def __init__(self, *args, **kwargs):
- # make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set.
- kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE
- wx.Window.__init__(self, *args, **kwargs)
- Debug.msg(2, "BufferedWindow.__init__()")
- wx.EVT_PAINT(self, self.OnPaint)
- wx.EVT_SIZE(self, self.OnSize)
- # OnSize called to make sure the buffer is initialized.
- # This might result in OnSize getting called twice on some
- # platforms at initialization, but little harm done.
- self.OnSize(None)
- def Draw(self, dc):
- ## just here as a place holder.
- ## This method should be over-ridden when subclassed
- pass
- def OnPaint(self, event):
- Debug.msg(5, "BufferedWindow.OnPaint()")
- # All that is needed here is to draw the buffer to screen
- dc = wx.BufferedPaintDC(self, self._Buffer)
- def OnSize(self, event):
- Debug.msg(5, "BufferedWindow.OnSize()")
- # The Buffer init is done here, to make sure the buffer is always
- # the same size as the Window
- #Size = self.GetClientSizeTuple()
- size = self.GetClientSize()
- # Make new offscreen bitmap: this bitmap will always have the
- # current drawing in it, so it can be used to save the image to
- # a file, or whatever.
- self._Buffer = wx.EmptyBitmap(*size)
- self.UpdateDrawing()
- # event.Skip()
- def SaveToFile(self, FileName, FileType=wx.BITMAP_TYPE_PNG):
- ## This will save the contents of the buffer
- ## to the specified file. See the wxWindows docs for
- ## wx.Bitmap::SaveFile for the details
- self._Buffer.SaveFile(FileName, FileType)
- def UpdateDrawing(self):
- """
- This would get called if the drawing needed to change, for whatever reason.
- The idea here is that the drawing is based on some data generated
- elsewhere in the system. If that data changes, the drawing needs to
- be updated.
- This code re-draws the buffer, then calls Update, which forces a paint event.
- """
- dc = wx.MemoryDC()
- dc.SelectObject(self._Buffer)
- self.Draw(dc)
- del dc # need to get rid of the MemoryDC before Update() is called.
- self.Refresh()
- self.Update()
- class AnimationWindow(BufferedWindow):
- def __init__(self, parent, id=wx.ID_ANY,
- style = wx.DEFAULT_FRAME_STYLE | wx.FULL_REPAINT_ON_RESIZE | wx.BORDER_RAISED):
- Debug.msg(2, "AnimationWindow.__init__()")
- self.bitmap = wx.EmptyBitmap(1, 1)
- self.text = ''
- self.parent = parent
- self._pdc = wx.PseudoDC()
- self._overlay = None
- self._tmpMousePos = None
- BufferedWindow.__init__(self, parent=parent, id=id, style=style)
- self.SetBackgroundColour(wx.BLACK)
- self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
- self.Bind(wx.EVT_SIZE, self.OnSize)
- self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouseEvents)
- def Draw(self, dc):
- """!Draws bitmap."""
- Debug.msg(5, "AnimationWindow.Draw()")
- dc.Clear() # make sure you clear the bitmap!
- dc.DrawBitmap(self.bitmap, x=0, y=0)
- dc.DrawText(self.text, 0, 0)
- def OnSize(self, event):
- Debug.msg(5, "AnimationWindow.OnSize()")
- self.DrawBitmap(self.bitmap, self.text)
-
- BufferedWindow.OnSize(self, event)
- if event:
- event.Skip()
- def DrawBitmap(self, bitmap, text):
- """!Draws bitmap.
- Does not draw the bitmap if it is the same one as last time.
- """
- if self.bitmap == bitmap:
- return
- self.bitmap = bitmap
- self.text = text
- self.UpdateDrawing()
- def DrawOverlay(self, x, y):
- self._pdc.BeginDrawing()
- self._pdc.SetId(1)
- self._pdc.DrawBitmap(bmp=self._overlay, x=x, y=y)
- self._pdc.SetIdBounds(1, wx.Rect(x, y, self._overlay.GetWidth(),
- self._overlay.GetHeight()))
- self._pdc.EndDrawing()
- def SetOverlay(self, bitmap, xperc, yperc):
- """!Sets overlay bitmap (legend)
- @param bitmap instance of wx.Bitmap
- @param xperc x coordinate of bitmap top left corner in % of screen
- @param yperc y coordinate of bitmap top left corner in % of screen
- """
- Debug.msg(3, "AnimationWindow.SetOverlay()")
- if bitmap:
- if self._overlay:
- self._pdc.RemoveAll()
- self._overlay = bitmap
- size = self.GetClientSize()
- x = xperc * size[0]
- y = yperc * size[1]
- self.DrawOverlay(x, y)
- else:
- self._overlay = None
- self._pdc.RemoveAll()
- self.UpdateDrawing()
- def OnPaint(self, event):
- Debug.msg(5, "AnimationWindow.OnPaint()")
- # All that is needed here is to draw the buffer to screen
- dc = wx.BufferedPaintDC(self, self._Buffer)
- if self._overlay:
- self._pdc.DrawToDC(dc)
- def OnMouseEvents(self, event):
- """!Handle mouse events."""
- # If it grows larger, split it.
- current = event.GetPosition()
- if event.LeftDown():
- self._dragid = None
- idlist = self._pdc.FindObjects(current[0], current[1],
- radius=10)
- if 1 in idlist:
- self._dragid = 1
- self._tmpMousePos = current
- elif event.LeftUp():
- self._dragid = None
- self._tmpMousePos = None
- elif event.Dragging():
- if self._dragid is None:
- return
- dx = current[0] - self._tmpMousePos[0]
- dy = current[1] - self._tmpMousePos[1]
- self._pdc.TranslateId(self._dragid, dx, dy)
- self.UpdateDrawing()
- self._tmpMousePos = current
- def GetOverlayPos(self):
- """!Returns x, y position in pixels"""
- rect = self._pdc.GetIdBounds(1)
- return rect.GetX(), rect.GetY()
- class BitmapProvider(object):
- """!Class responsible for loading data and providing bitmaps"""
- def __init__(self, frame, bitmapPool, imageWidth=640, imageHeight=480, nprocs=4):
- self.datasource = None
- self.dataNames = None
- self.dataType = None
- self.bitmapPool = bitmapPool
- self.frame = frame
- 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.nprocs = nprocs # Number of procs to be used for rendering
- self.suffix = ''
- self.nvizRegion = None
-
- self.mapsLoaded = Signal('mapsLoaded')
- def GetDataNames(self):
- return self.dataNames
- def SetData(self, datasource, dataNames = None, dataType = 'rast',
- suffix = '', nvizRegion = None):
- """!Sets data.
- @param datasource data to load (raster maps, vector maps, m.nviz.image commands)
- @param dataNames data labels (keys)
- @param dataType 'rast', 'vect', 'nviz'
- @param nvizRegion region which must be set for m.nviz.image
- """
- self.datasource = datasource
- self.dataType = dataType
- self.suffix = suffix
- self.nvizRegion = nvizRegion
-
- if dataNames:
- self.dataNames = dataNames
- else:
- self.dataNames = datasource
- self.dataNames = [name + self.suffix for name in self.dataNames]
- def GetBitmap(self, dataId):
- """!Returns bitmap with given key
- or 'no data' bitmap if no such key exists.
- @param dataId name of bitmap
- """
- if dataId:
- dataId += self.suffix
- 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."""
- self.imageWidth, self.imageHeight = width, height
- def _createNoDataBitmap(self, width, height):
- """!Creates 'no data' bitmap.
- Used when requested bitmap is not available (loading data was not successful) or
- we want to show 'no data' bitmap.
- """
- bitmap = wx.EmptyBitmap(width, height)
- dc = wx.MemoryDC()
- dc.SelectObject(bitmap)
- dc.Clear()
- text = _("No data")
- 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, (width-tw)/2, (height-th)/2)
- dc.SelectObject(wx.NullBitmap)
- return bitmap
- def Load(self, force = False, nprocs=4):
- """!Loads data.
- Shows progress dialog.
- @param force if True reload all data, otherwise only missing data
- @param imageWidth width of the image to render with d.rast or d.vect
- @param imageHeight height of the image to render with d.rast or d.vect
- @param nprocs number of procs to be used for rendering
- """
- if nprocs <= 0:
- nprocs = 1
- count, maxLength = self._dryLoad(rasters = self.datasource,
- names = self.dataNames, force = force)
- progress = None
- if self.dataType in ('rast', 'vect', 'strds', 'stvds') and count > 5 or \
- self.dataType == 'nviz':
- progress = wx.ProgressDialog(title = "Loading data",
- message = " " * (maxLength + 20), # ?
- maximum = count,
- parent = self.frame,
- style = wx.PD_CAN_ABORT | wx.PD_APP_MODAL |
- wx.PD_AUTO_HIDE | wx.PD_SMOOTH)
- updateFunction = progress.Update
- else:
- updateFunction = None
- if self.dataType in ('rast', 'vect', 'strds', 'stvds'):
- self._loadMaps(mapType=self.dataType, maps = self.datasource, names = self.dataNames,
- force = force, updateFunction = updateFunction,
- imageWidth=self.imageWidth, imageHeight=self.imageHeight, nprocs=nprocs)
- elif self.dataType == 'nviz':
- self._load3D(commands = self.datasource, region = self.nvizRegion, names = self.dataNames,
- force = force, updateFunction = updateFunction)
- if progress:
- progress.Destroy()
- self.mapsLoaded.emit()
- def Unload(self):
- self.datasource = None
- self.dataNames = None
- self.dataType = None
- def _dryLoad(self, rasters, names, force):
- """!Tries how many bitmaps will be loaded.
- Used for progress dialog.
- @param rasters raster maps to be loaded
- @param names names used as keys for bitmaps
- @param force load everything even though it is already there
- """
- count = 0
- maxLength = 0
- for raster, name in zip(rasters, names):
- if not(name in self.bitmapPool and force is False):
- count += 1
- if len(raster) > maxLength:
- maxLength = len(raster)
- return count, maxLength
-
- def _loadMaps(self, mapType, maps, names, force, updateFunction,
- imageWidth, imageHeight, nprocs):
- """!Loads rasters/vectors (also from temporal dataset).
- Uses d.rast/d.vect and multiprocessing for parallel rendering
- @param mapType Must be "rast" or "vect"
- @param maps raster or vector maps to be loaded
- @param names names used as keys for bitmaps
- @param force load everything even though it is already there
- @param updateFunction function called for updating progress dialog
- @param imageWidth width of the image to render with d.rast or d.vect
- @param imageHeight height of the image to render with d.rast or d.vect
- @param nprocs number of procs to be used for rendering
- """
- count = 0
- # Variables for parallel rendering
- proc_count = 0
- proc_list = []
- queue_list = []
- name_list = []
- mapNum = len(maps)
- # create no data bitmap
- if None not in self.bitmapPool or force:
- self.bitmapPool[None] = self._createNoDataBitmap(imageWidth, imageHeight)
- for mapname, name in zip(maps, names):
- count += 1
- if name in self.bitmapPool and force is False:
- continue
- # Queue object for interprocess communication
- q = Queue()
- # The separate render process
- p = Process(target=mapRenderProcess, args=(mapType, mapname, imageWidth, imageHeight, q))
- p.start()
- queue_list.append(q)
- proc_list.append(p)
- name_list.append(name)
- 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(name_list)):
- proc_list[i].join()
- filename = queue_list[i].get()
- # Unfortunately the png files must be read here,
- # since the swig wx objects can not be serialized by the Queue object :(
- if filename == None:
- self.bitmapPool[name_list[i]] = wx.EmptyBitmap(imageWidth, imageHeight)
- else:
- self.bitmapPool[name_list[i]] = wx.BitmapFromImage(wx.Image(filename))
- os.remove(filename)
- proc_count = 0
- proc_list = []
- queue_list = []
- name_list = []
- if updateFunction:
- keepGoing, skip = updateFunction(count, mapname)
- if not keepGoing:
- break
- def _load3D(self, commands, region, names, force, updateFunction):
- """!Load 3D view images using m.nviz.image.
- @param commands
- @param region
- @param names names used as keys for bitmaps
- @param force load everything even though it is already there
- @param updateFunction function called for updating progress dialog
- """
- ncols, nrows = self.imageWidth, self.imageHeight
- count = 0
- format = 'ppm'
- tempFile = grass.tempfile(False)
- tempFileFormat = tempFile + '.' + format
- os.environ['GRASS_REGION'] = grass.region_env(**region)
- # create no data bitmap
- if None not in self.bitmapPool or force:
- self.bitmapPool[None] = self._createNoDataBitmap(ncols, nrows)
- for command, name in zip(commands, names):
- if name in self.bitmapPool and force is False:
- continue
- count += 1
- # set temporary file
- command[1]['output'] = tempFile
- # set size
- command[1]['size'] = '%d,%d' % (ncols, nrows)
- # set format
- command[1]['format'] = format
- returncode, messages = RunCommand(getErrorMsg = True, prog = command[0], **command[1])
- if returncode != 0:
- self.bitmapPool[name] = wx.EmptyBitmap(ncols, nrows)
- continue
- self.bitmapPool[name] = wx.Bitmap(tempFileFormat)
- if updateFunction:
- keepGoing, skip = updateFunction(count, name)
- if not keepGoing:
- break
- grass.try_remove(tempFileFormat)
- os.environ.pop('GRASS_REGION')
- def LoadOverlay(self, cmd):
- """!Creates raster legend with d.legend
- @param cmd d.legend command as a list
- @return bitmap with legend
- """
- fileHandler, filename = tempfile.mkstemp(suffix=".png")
- os.close(fileHandler)
- # Set the environment variables for this process
- _setEnvironment(self.imageWidth, self.imageHeight, filename, transparent=True)
- 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)
- def mapRenderProcess(mapType, mapname, width, height, fileQueue):
- """!Render raster or vector files as png image and write the
- resulting png filename in the provided file queue
-
- @param mapType Must be "rast" or "vect"
- @param mapname raster or vector map name to be rendered
- @param width Width of the resulting image
- @param height Height of the resulting image
- @param fileQueue The inter process communication queue storing the file name of the image
- """
-
- # temporary file, we use python here to avoid calling g.tempfile for each render process
- fileHandler, filename = tempfile.mkstemp(suffix=".png")
- os.close(fileHandler)
-
- # Set the environment variables for this process
- _setEnvironment(width, height, filename, transparent=False)
- if mapType in ('rast', 'strds'):
- Debug.msg(1, "Render raster image " + str(filename))
- returncode, stdout, messages = read2_command('d.rast', map = mapname)
- elif mapType in ('vect', 'stvds'):
- Debug.msg(1, "Render vector image " + str(filename))
- returncode, stdout, messages = read2_command('d.vect', map = mapname)
- else:
- returncode = 1
- return
- if returncode != 0:
- fileQueue.put(None)
- os.remove(filename)
- return
- fileQueue.put(filename)
-
- def _setEnvironment(width, height, filename, transparent):
- 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'] = 'ffffff'
- os.environ['GRASS_TRUECOLOR'] = "TRUE"
- if transparent:
- os.environ['GRASS_TRANSPARENT'] = "TRUE"
- else:
- os.environ['GRASS_TRANSPARENT'] = "FALSE"
- os.environ['GRASS_PNGFILE'] = str(filename)
- class BitmapPool():
- """!Class storing bitmaps (emulates dictionary)"""
- def __init__(self):
- self.bitmaps = {}
- def __getitem__(self, key):
- return self.bitmaps[key]
- def __setitem__(self, key, bitmap):
- self.bitmaps[key] = bitmap
- def __contains__(self, key):
- return key in self.bitmaps
- def Clear(self, usedKeys):
- """!Removes all bitmaps which are currently not used.
- @param usedKeys keys which are currently used
- """
- for key in self.bitmaps.keys():
- if key not in usedKeys and key is not None:
- del self.bitmaps[key]
- def read2_command(*args, **kwargs):
- kwargs['stdout'] = grass.PIPE
- kwargs['stderr'] = grass.PIPE
- ps = grass.start_command(*args, **kwargs)
- stdout, stderr = ps.communicate()
- return ps.returncode, stdout, stderr
|