123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- """!
- @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
- import grass.script as grass
- from core.gcmd import RunCommand
- from core.debug import Debug
- from utils import ComputeScaledRect
- 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.ClientSize
- # 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(0, 0)
- self.x = self.y = 0
- self.text = ''
- self.size = wx.Size()
- self.rescaleNeeded = False
- self.region = None
- self.parent = parent
- 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)
- def Draw(self, dc):
- """!Draws bitmap."""
- Debug.msg(5, "AnimationWindow.Draw()")
- dc.Clear() # make sure you clear the bitmap!
- dc.DrawBitmap(self.bitmap, x = self.x, y = self.y)
- dc.DrawText(self.text, 0, 0)
- def OnSize(self, event):
- Debug.msg(5, "AnimationWindow.OnSize()")
- self._computeBitmapCoordinates()
- self.DrawBitmap(self.bitmap, self.text)
-
- BufferedWindow.OnSize(self, event)
- if event:
- event.Skip()
-
- def IsRescaled(self):
- return self.rescaleNeeded
- def _rescaleIfNeeded(self, bitmap):
- """!If the bitmap has different size than the window, rescale it."""
- bW, bH = bitmap.GetSize()
- wW, wH = self.size
- if abs(bW - wW) > 5 and abs(bH - wH) > 5:
- self.rescaleNeeded = True
- im = wx.ImageFromBitmap(bitmap)
- im.Rescale(*self.size)
- bitmap = wx.BitmapFromImage(im)
- else:
- self.rescaleNeeded = False
- return bitmap
-
- def DrawBitmap(self, bitmap, text):
- """!Draws bitmap.
- Does not draw the bitmap if it is the same one as last time.
- """
- bmp = self._rescaleIfNeeded(bitmap)
- if self.bitmap == bmp:
- return
- self.bitmap = bmp
- self.text = text
- self.UpdateDrawing()
- def _computeBitmapCoordinates(self):
- """!Computes where to place the bitmap
- to be in the center of the window."""
- if not self.region:
- return
- cols = self.region['cols']
- rows = self.region['rows']
- params = ComputeScaledRect((cols, rows), self.GetClientSize())
- self.x = params['x']
- self.y = params['y']
- self.size = (params['width'], params['height'])
- def SetRegion(self, region):
- """!Sets region for size computations.
- Region is set from outside to avoid calling g.region multiple times.
- """
- self.region = region
- self._computeBitmapCoordinates()
- def GetAdjustedSize(self):
- return self.size
- def GetAdjustedPosition(self):
- return self.x, self.y
- class BitmapProvider(object):
- """!Class responsible for loading data and providing bitmaps"""
- def __init__(self, frame, bitmapPool):
- self.datasource = None
- self.dataNames = None
- self.dataType = None
- self.region = None
- self.bitmapPool = bitmapPool
- self.frame = frame
- self.size = wx.Size()
- self.loadSize = wx.Size()
- self.suffix = ''
- self.nvizRegion = None
- 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, m.nviz.image commands)
- @param dataNames data labels (keys)
- @param dataType 'rast', '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 GetLoadSize(self):
- return self.loadSize
- def WindowSizeChanged(self, event, sizeMethod):
- """!Sets size when size of related window changes."""
- # sizeMethod is GetClientSize, must be used instead of GetSize
- self.size = sizeMethod()
- event.Skip()
- def _createNoDataBitmap(self, ncols, nrows):
- """!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(ncols, nrows)
- 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, (ncols-tw)/2, (nrows-th)/2)
- dc.SelectObject(wx.NullBitmap)
- return bitmap
- def Load(self, force = False):
- """!Loads data.
- Shows progress dialog.
- @param force if True reload all data, otherwise only missing data
- """
- count, maxLength = self._dryLoad(rasters = self.datasource,
- names = self.dataNames, force = force)
- progress = None
- if self.dataType == 'rast' 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 == 'rast':
- size, scale = self._computeScale()
- # loading ...
- self._loadRasters(rasters = self.datasource, names = self.dataNames,
- size = size, scale = scale, force = force, updateFunction = updateFunction)
- elif self.dataType == 'nviz':
- self._load3D(commands = self.datasource, region = self.nvizRegion, names = self.dataNames,
- force = force, updateFunction = updateFunction)
- if progress:
- progress.Destroy()
- def Unload(self):
- self.datasource = None
- self.dataNames = None
- self.dataType = None
- def _computeScale(self):
- """!Computes parameters for creating bitmaps."""
- region = grass.region()
- ncols, nrows = region['cols'], region['rows']
- params = ComputeScaledRect((ncols, nrows), self.size)
- return ((params['width'], params['height']), params['scale'])
- 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 _loadRasters(self, rasters, names, size, scale, force, updateFunction):
- """!Loads rasters (also rasters from temporal dataset).
- Uses r.out.ppm.
- @param rasters raster maps to be loaded
- @param names names used as keys for bitmaps
- @param size size of new bitmaps
- @param scale used for adjustment of region resolution for r.out.ppm
- @param force load everything even though it is already there
- @param updateFunction function called for updating progress dialog
- """
- region = grass.region()
- for key in ('rows', 'cols', 'cells'):
- region.pop(key)
- # sometimes it renderes nonsense - depends on resolution
- # should we set the resolution of the raster?
- region['nsres'] /= scale
- region['ewres'] /= scale
- os.environ['GRASS_REGION'] = grass.region_env(**region)
- ncols, nrows = size
- self.loadSize = size
- count = 0
- # create no data bitmap
- if None not in self.bitmapPool or force:
- self.bitmapPool[None] = self._createNoDataBitmap(ncols, nrows)
- for raster, name in zip(rasters, names):
- if name in self.bitmapPool and force is False:
- continue
- count += 1
- # RunCommand has problem with DecodeString
- returncode, stdout, messages = read2_command('r.out.ppm', input = raster,
- flags = 'h', output = '-', quiet = True)
- if returncode != 0:
- self.bitmapPool[name] = wx.EmptyBitmap(ncols, nrows)
- continue
-
- bitmap = wx.BitmapFromBuffer(ncols, nrows, stdout)
- self.bitmapPool[name] = bitmap
- if updateFunction:
- keepGoing, skip = updateFunction(count, raster)
- if not keepGoing:
- break
- os.environ.pop('GRASS_REGION')
- 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.size
- self.loadSize = ncols, nrows
- 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')
- 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 currentlu 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
|