mapwindow.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. """!
  2. @package animation.mapwindow
  3. @brief Animation window and bitmaps management
  4. Classes:
  5. - mapwindow::BufferedWindow
  6. - mapwindow::AnimationWindow
  7. - mapwindow::BitmapProvider
  8. - mapwindow::BitmapPool
  9. (C) 2012 by the GRASS Development Team
  10. This program is free software under the GNU General Public License
  11. (>=v2). Read the file COPYING that comes with GRASS for details.
  12. @author Anna Kratochvilova <kratochanna gmail.com>
  13. """
  14. import os
  15. import wx
  16. import grass.script as grass
  17. from core.gcmd import RunCommand
  18. from core.debug import Debug
  19. from utils import ComputeScaledRect
  20. class BufferedWindow(wx.Window):
  21. """
  22. A Buffered window class (http://wiki.wxpython.org/DoubleBufferedDrawing).
  23. To use it, subclass it and define a Draw(DC) method that takes a DC
  24. to draw to. In that method, put the code needed to draw the picture
  25. you want. The window will automatically be double buffered, and the
  26. screen will be automatically updated when a Paint event is received.
  27. When the drawing needs to change, you app needs to call the
  28. UpdateDrawing() method. Since the drawing is stored in a bitmap, you
  29. can also save the drawing to file by calling the
  30. SaveToFile(self, file_name, file_type) method.
  31. """
  32. def __init__(self, *args, **kwargs):
  33. # make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set.
  34. kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE
  35. wx.Window.__init__(self, *args, **kwargs)
  36. Debug.msg(2, "BufferedWindow.__init__()")
  37. wx.EVT_PAINT(self, self.OnPaint)
  38. wx.EVT_SIZE(self, self.OnSize)
  39. # OnSize called to make sure the buffer is initialized.
  40. # This might result in OnSize getting called twice on some
  41. # platforms at initialization, but little harm done.
  42. self.OnSize(None)
  43. def Draw(self, dc):
  44. ## just here as a place holder.
  45. ## This method should be over-ridden when subclassed
  46. pass
  47. def OnPaint(self, event):
  48. Debug.msg(5, "BufferedWindow.OnPaint()")
  49. # All that is needed here is to draw the buffer to screen
  50. dc = wx.BufferedPaintDC(self, self._Buffer)
  51. def OnSize(self, event):
  52. Debug.msg(5, "BufferedWindow.OnSize()")
  53. # The Buffer init is done here, to make sure the buffer is always
  54. # the same size as the Window
  55. #Size = self.GetClientSizeTuple()
  56. size = self.ClientSize
  57. # Make new offscreen bitmap: this bitmap will always have the
  58. # current drawing in it, so it can be used to save the image to
  59. # a file, or whatever.
  60. self._Buffer = wx.EmptyBitmap(*size)
  61. self.UpdateDrawing()
  62. # event.Skip()
  63. def SaveToFile(self, FileName, FileType=wx.BITMAP_TYPE_PNG):
  64. ## This will save the contents of the buffer
  65. ## to the specified file. See the wxWindows docs for
  66. ## wx.Bitmap::SaveFile for the details
  67. self._Buffer.SaveFile(FileName, FileType)
  68. def UpdateDrawing(self):
  69. """
  70. This would get called if the drawing needed to change, for whatever reason.
  71. The idea here is that the drawing is based on some data generated
  72. elsewhere in the system. If that data changes, the drawing needs to
  73. be updated.
  74. This code re-draws the buffer, then calls Update, which forces a paint event.
  75. """
  76. dc = wx.MemoryDC()
  77. dc.SelectObject(self._Buffer)
  78. self.Draw(dc)
  79. del dc # need to get rid of the MemoryDC before Update() is called.
  80. self.Refresh()
  81. self.Update()
  82. class AnimationWindow(BufferedWindow):
  83. def __init__(self, parent, id = wx.ID_ANY,
  84. style = wx.DEFAULT_FRAME_STYLE | wx.FULL_REPAINT_ON_RESIZE | wx.BORDER_RAISED):
  85. Debug.msg(2, "AnimationWindow.__init__()")
  86. self.bitmap = wx.EmptyBitmap(0, 0)
  87. self.x = self.y = 0
  88. self.text = ''
  89. self.size = wx.Size()
  90. self.rescaleNeeded = False
  91. self.region = None
  92. self.parent = parent
  93. BufferedWindow.__init__(self, parent = parent, id = id, style = style)
  94. self.SetBackgroundColour(wx.BLACK)
  95. self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
  96. self.Bind(wx.EVT_SIZE, self.OnSize)
  97. def Draw(self, dc):
  98. """!Draws bitmap."""
  99. Debug.msg(5, "AnimationWindow.Draw()")
  100. dc.Clear() # make sure you clear the bitmap!
  101. dc.DrawBitmap(self.bitmap, x = self.x, y = self.y)
  102. dc.DrawText(self.text, 0, 0)
  103. def OnSize(self, event):
  104. Debug.msg(5, "AnimationWindow.OnSize()")
  105. self._computeBitmapCoordinates()
  106. self.DrawBitmap(self.bitmap, self.text)
  107. BufferedWindow.OnSize(self, event)
  108. if event:
  109. event.Skip()
  110. def IsRescaled(self):
  111. return self.rescaleNeeded
  112. def _rescaleIfNeeded(self, bitmap):
  113. """!If the bitmap has different size than the window, rescale it."""
  114. bW, bH = bitmap.GetSize()
  115. wW, wH = self.size
  116. if abs(bW - wW) > 5 and abs(bH - wH) > 5:
  117. self.rescaleNeeded = True
  118. im = wx.ImageFromBitmap(bitmap)
  119. im.Rescale(*self.size)
  120. bitmap = wx.BitmapFromImage(im)
  121. else:
  122. self.rescaleNeeded = False
  123. return bitmap
  124. def DrawBitmap(self, bitmap, text):
  125. """!Draws bitmap.
  126. Does not draw the bitmap if it is the same one as last time.
  127. """
  128. bmp = self._rescaleIfNeeded(bitmap)
  129. if self.bitmap == bmp:
  130. return
  131. self.bitmap = bmp
  132. self.text = text
  133. self.UpdateDrawing()
  134. def _computeBitmapCoordinates(self):
  135. """!Computes where to place the bitmap
  136. to be in the center of the window."""
  137. if not self.region:
  138. return
  139. cols = self.region['cols']
  140. rows = self.region['rows']
  141. params = ComputeScaledRect((cols, rows), self.GetClientSize())
  142. self.x = params['x']
  143. self.y = params['y']
  144. self.size = (params['width'], params['height'])
  145. def SetRegion(self, region):
  146. """!Sets region for size computations.
  147. Region is set from outside to avoid calling g.region multiple times.
  148. """
  149. self.region = region
  150. self._computeBitmapCoordinates()
  151. def GetAdjustedSize(self):
  152. return self.size
  153. def GetAdjustedPosition(self):
  154. return self.x, self.y
  155. class BitmapProvider(object):
  156. """!Class responsible for loading data and providing bitmaps"""
  157. def __init__(self, frame, bitmapPool):
  158. self.datasource = None
  159. self.dataNames = None
  160. self.dataType = None
  161. self.region = None
  162. self.bitmapPool = bitmapPool
  163. self.frame = frame
  164. self.size = wx.Size()
  165. self.loadSize = wx.Size()
  166. self.suffix = ''
  167. self.nvizRegion = None
  168. def GetDataNames(self):
  169. return self.dataNames
  170. def SetData(self, datasource, dataNames = None, dataType = 'rast',
  171. suffix = '', nvizRegion = None):
  172. """!Sets data.
  173. @param datasource data to load (raster maps, m.nviz.image commands)
  174. @param dataNames data labels (keys)
  175. @param dataType 'rast', 'nviz'
  176. @param nvizRegion region which must be set for m.nviz.image
  177. """
  178. self.datasource = datasource
  179. self.dataType = dataType
  180. self.suffix = suffix
  181. self.nvizRegion = nvizRegion
  182. if dataNames:
  183. self.dataNames = dataNames
  184. else:
  185. self.dataNames = datasource
  186. self.dataNames = [name + self.suffix for name in self.dataNames]
  187. def GetBitmap(self, dataId):
  188. """!Returns bitmap with given key
  189. or 'no data' bitmap if no such key exists.
  190. @param dataId name of bitmap
  191. """
  192. if dataId:
  193. dataId += self.suffix
  194. try:
  195. bitmap = self.bitmapPool[dataId]
  196. except KeyError:
  197. bitmap = self.bitmapPool[None]
  198. return bitmap
  199. def GetLoadSize(self):
  200. return self.loadSize
  201. def WindowSizeChanged(self, event, sizeMethod):
  202. """!Sets size when size of related window changes."""
  203. # sizeMethod is GetClientSize, must be used instead of GetSize
  204. self.size = sizeMethod()
  205. event.Skip()
  206. def _createNoDataBitmap(self, ncols, nrows):
  207. """!Creates 'no data' bitmap.
  208. Used when requested bitmap is not available (loading data was not successful) or
  209. we want to show 'no data' bitmap.
  210. """
  211. bitmap = wx.EmptyBitmap(ncols, nrows)
  212. dc = wx.MemoryDC()
  213. dc.SelectObject(bitmap)
  214. dc.Clear()
  215. text = _("No data")
  216. dc.SetFont(wx.Font(pointSize = 40, family = wx.FONTFAMILY_SCRIPT,
  217. style = wx.FONTSTYLE_NORMAL, weight = wx.FONTWEIGHT_BOLD))
  218. tw, th = dc.GetTextExtent(text)
  219. dc.DrawText(text, (ncols-tw)/2, (nrows-th)/2)
  220. dc.SelectObject(wx.NullBitmap)
  221. return bitmap
  222. def Load(self, force = False):
  223. """!Loads data.
  224. Shows progress dialog.
  225. @param force if True reload all data, otherwise only missing data
  226. """
  227. count, maxLength = self._dryLoad(rasters = self.datasource,
  228. names = self.dataNames, force = force)
  229. progress = None
  230. if self.dataType == 'rast' and count > 5 or \
  231. self.dataType == 'nviz':
  232. progress = wx.ProgressDialog(title = "Loading data",
  233. message = " " * (maxLength + 20), # ?
  234. maximum = count,
  235. parent = self.frame,
  236. style = wx.PD_CAN_ABORT | wx.PD_APP_MODAL |
  237. wx.PD_AUTO_HIDE | wx.PD_SMOOTH)
  238. updateFunction = progress.Update
  239. else:
  240. updateFunction = None
  241. if self.dataType == 'rast':
  242. size, scale = self._computeScale()
  243. # loading ...
  244. self._loadRasters(rasters = self.datasource, names = self.dataNames,
  245. size = size, scale = scale, force = force, updateFunction = updateFunction)
  246. elif self.dataType == 'nviz':
  247. self._load3D(commands = self.datasource, region = self.nvizRegion, names = self.dataNames,
  248. force = force, updateFunction = updateFunction)
  249. if progress:
  250. progress.Destroy()
  251. def Unload(self):
  252. self.datasource = None
  253. self.dataNames = None
  254. self.dataType = None
  255. def _computeScale(self):
  256. """!Computes parameters for creating bitmaps."""
  257. region = grass.region()
  258. ncols, nrows = region['cols'], region['rows']
  259. params = ComputeScaledRect((ncols, nrows), self.size)
  260. return ((params['width'], params['height']), params['scale'])
  261. def _dryLoad(self, rasters, names, force):
  262. """!Tries how many bitmaps will be loaded.
  263. Used for progress dialog.
  264. @param rasters raster maps to be loaded
  265. @param names names used as keys for bitmaps
  266. @param force load everything even though it is already there
  267. """
  268. count = 0
  269. maxLength = 0
  270. for raster, name in zip(rasters, names):
  271. if not(name in self.bitmapPool and force is False):
  272. count += 1
  273. if len(raster) > maxLength:
  274. maxLength = len(raster)
  275. return count, maxLength
  276. def _loadRasters(self, rasters, names, size, scale, force, updateFunction):
  277. """!Loads rasters (also rasters from temporal dataset).
  278. Uses r.out.ppm.
  279. @param rasters raster maps to be loaded
  280. @param names names used as keys for bitmaps
  281. @param size size of new bitmaps
  282. @param scale used for adjustment of region resolution for r.out.ppm
  283. @param force load everything even though it is already there
  284. @param updateFunction function called for updating progress dialog
  285. """
  286. region = grass.region()
  287. for key in ('rows', 'cols', 'cells'):
  288. region.pop(key)
  289. # sometimes it renderes nonsense - depends on resolution
  290. # should we set the resolution of the raster?
  291. region['nsres'] /= scale
  292. region['ewres'] /= scale
  293. os.environ['GRASS_REGION'] = grass.region_env(**region)
  294. ncols, nrows = size
  295. self.loadSize = size
  296. count = 0
  297. # create no data bitmap
  298. if None not in self.bitmapPool or force:
  299. self.bitmapPool[None] = self._createNoDataBitmap(ncols, nrows)
  300. for raster, name in zip(rasters, names):
  301. if name in self.bitmapPool and force is False:
  302. continue
  303. count += 1
  304. # RunCommand has problem with DecodeString
  305. returncode, stdout, messages = read2_command('r.out.ppm', input = raster,
  306. flags = 'h', output = '-', quiet = True)
  307. if returncode != 0:
  308. self.bitmapPool[name] = wx.EmptyBitmap(ncols, nrows)
  309. continue
  310. bitmap = wx.BitmapFromBuffer(ncols, nrows, stdout)
  311. self.bitmapPool[name] = bitmap
  312. if updateFunction:
  313. keepGoing, skip = updateFunction(count, raster)
  314. if not keepGoing:
  315. break
  316. os.environ.pop('GRASS_REGION')
  317. def _load3D(self, commands, region, names, force, updateFunction):
  318. """!Load 3D view images using m.nviz.image.
  319. @param commands
  320. @param region
  321. @param names names used as keys for bitmaps
  322. @param force load everything even though it is already there
  323. @param updateFunction function called for updating progress dialog
  324. """
  325. ncols, nrows = self.size
  326. self.loadSize = ncols, nrows
  327. count = 0
  328. format = 'ppm'
  329. tempFile = grass.tempfile(False)
  330. tempFileFormat = tempFile + '.' + format
  331. os.environ['GRASS_REGION'] = grass.region_env(**region)
  332. # create no data bitmap
  333. if None not in self.bitmapPool or force:
  334. self.bitmapPool[None] = self._createNoDataBitmap(ncols, nrows)
  335. for command, name in zip(commands, names):
  336. if name in self.bitmapPool and force is False:
  337. continue
  338. count += 1
  339. # set temporary file
  340. command[1]['output'] = tempFile
  341. # set size
  342. command[1]['size'] = '%d,%d' % (ncols, nrows)
  343. # set format
  344. command[1]['format'] = format
  345. returncode, messages = RunCommand(getErrorMsg = True, prog = command[0], **command[1])
  346. if returncode != 0:
  347. self.bitmapPool[name] = wx.EmptyBitmap(ncols, nrows)
  348. continue
  349. self.bitmapPool[name] = wx.Bitmap(tempFileFormat)
  350. if updateFunction:
  351. keepGoing, skip = updateFunction(count, name)
  352. if not keepGoing:
  353. break
  354. grass.try_remove(tempFileFormat)
  355. os.environ.pop('GRASS_REGION')
  356. class BitmapPool():
  357. """!Class storing bitmaps (emulates dictionary)"""
  358. def __init__(self):
  359. self.bitmaps = {}
  360. def __getitem__(self, key):
  361. return self.bitmaps[key]
  362. def __setitem__(self, key, bitmap):
  363. self.bitmaps[key] = bitmap
  364. def __contains__(self, key):
  365. return key in self.bitmaps
  366. def Clear(self, usedKeys):
  367. """!Removes all bitmaps which are currentlu not used.
  368. @param usedKeys keys which are currently used
  369. """
  370. for key in self.bitmaps.keys():
  371. if key not in usedKeys and key is not None:
  372. del self.bitmaps[key]
  373. def read2_command(*args, **kwargs):
  374. kwargs['stdout'] = grass.PIPE
  375. kwargs['stderr'] = grass.PIPE
  376. ps = grass.start_command(*args, **kwargs)
  377. stdout, stderr = ps.communicate()
  378. return ps.returncode, stdout, stderr