mapwindow.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  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. from multiprocessing import Process, Queue
  17. import tempfile
  18. import grass.script as grass
  19. from core.gcmd import RunCommand
  20. from core.debug import Debug
  21. from core.settings import UserSettings
  22. from grass.pydispatch.signal import Signal
  23. class BufferedWindow(wx.Window):
  24. """
  25. A Buffered window class (http://wiki.wxpython.org/DoubleBufferedDrawing).
  26. To use it, subclass it and define a Draw(DC) method that takes a DC
  27. to draw to. In that method, put the code needed to draw the picture
  28. you want. The window will automatically be double buffered, and the
  29. screen will be automatically updated when a Paint event is received.
  30. When the drawing needs to change, you app needs to call the
  31. UpdateDrawing() method. Since the drawing is stored in a bitmap, you
  32. can also save the drawing to file by calling the
  33. SaveToFile(self, file_name, file_type) method.
  34. """
  35. def __init__(self, *args, **kwargs):
  36. # make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set.
  37. kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE
  38. wx.Window.__init__(self, *args, **kwargs)
  39. Debug.msg(2, "BufferedWindow.__init__()")
  40. wx.EVT_PAINT(self, self.OnPaint)
  41. wx.EVT_SIZE(self, self.OnSize)
  42. # OnSize called to make sure the buffer is initialized.
  43. # This might result in OnSize getting called twice on some
  44. # platforms at initialization, but little harm done.
  45. self.OnSize(None)
  46. def Draw(self, dc):
  47. ## just here as a place holder.
  48. ## This method should be over-ridden when subclassed
  49. pass
  50. def OnPaint(self, event):
  51. Debug.msg(5, "BufferedWindow.OnPaint()")
  52. # All that is needed here is to draw the buffer to screen
  53. dc = wx.BufferedPaintDC(self, self._Buffer)
  54. def OnSize(self, event):
  55. Debug.msg(5, "BufferedWindow.OnSize()")
  56. # The Buffer init is done here, to make sure the buffer is always
  57. # the same size as the Window
  58. #Size = self.GetClientSizeTuple()
  59. size = self.ClientSize
  60. # Make new offscreen bitmap: this bitmap will always have the
  61. # current drawing in it, so it can be used to save the image to
  62. # a file, or whatever.
  63. self._Buffer = wx.EmptyBitmap(*size)
  64. self.UpdateDrawing()
  65. # event.Skip()
  66. def SaveToFile(self, FileName, FileType=wx.BITMAP_TYPE_PNG):
  67. ## This will save the contents of the buffer
  68. ## to the specified file. See the wxWindows docs for
  69. ## wx.Bitmap::SaveFile for the details
  70. self._Buffer.SaveFile(FileName, FileType)
  71. def UpdateDrawing(self):
  72. """
  73. This would get called if the drawing needed to change, for whatever reason.
  74. The idea here is that the drawing is based on some data generated
  75. elsewhere in the system. If that data changes, the drawing needs to
  76. be updated.
  77. This code re-draws the buffer, then calls Update, which forces a paint event.
  78. """
  79. dc = wx.MemoryDC()
  80. dc.SelectObject(self._Buffer)
  81. self.Draw(dc)
  82. del dc # need to get rid of the MemoryDC before Update() is called.
  83. self.Refresh()
  84. self.Update()
  85. class AnimationWindow(BufferedWindow):
  86. def __init__(self, parent, id = wx.ID_ANY,
  87. style = wx.DEFAULT_FRAME_STYLE | wx.FULL_REPAINT_ON_RESIZE | wx.BORDER_RAISED):
  88. Debug.msg(2, "AnimationWindow.__init__()")
  89. self.bitmap = wx.EmptyBitmap(1, 1)
  90. self.text = ''
  91. self.parent = parent
  92. BufferedWindow.__init__(self, parent = parent, id = id, style = style)
  93. self.SetBackgroundColour(wx.BLACK)
  94. self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
  95. self.Bind(wx.EVT_SIZE, self.OnSize)
  96. def Draw(self, dc):
  97. """!Draws bitmap."""
  98. Debug.msg(5, "AnimationWindow.Draw()")
  99. dc.Clear() # make sure you clear the bitmap!
  100. dc.DrawBitmap(self.bitmap, x=0, y=0)
  101. dc.DrawText(self.text, 0, 0)
  102. def OnSize(self, event):
  103. Debug.msg(5, "AnimationWindow.OnSize()")
  104. self.DrawBitmap(self.bitmap, self.text)
  105. BufferedWindow.OnSize(self, event)
  106. if event:
  107. event.Skip()
  108. def DrawBitmap(self, bitmap, text):
  109. """!Draws bitmap.
  110. Does not draw the bitmap if it is the same one as last time.
  111. """
  112. if self.bitmap == bitmap:
  113. return
  114. self.bitmap = bitmap
  115. self.text = text
  116. self.UpdateDrawing()
  117. class BitmapProvider(object):
  118. """!Class responsible for loading data and providing bitmaps"""
  119. def __init__(self, frame, bitmapPool, imageWidth=640, imageHeight=480, nprocs=4):
  120. self.datasource = None
  121. self.dataNames = None
  122. self.dataType = None
  123. self.bitmapPool = bitmapPool
  124. self.frame = frame
  125. self.imageWidth = imageWidth # width of the image to render with d.rast or d.vect
  126. self.imageHeight = imageHeight # height of the image to render with d.rast or d.vect
  127. self.nprocs = nprocs # Number of procs to be used for rendering
  128. self.suffix = ''
  129. self.nvizRegion = None
  130. self.mapsLoaded = Signal('mapsLoaded')
  131. def GetDataNames(self):
  132. return self.dataNames
  133. def SetData(self, datasource, dataNames = None, dataType = 'rast',
  134. suffix = '', nvizRegion = None):
  135. """!Sets data.
  136. @param datasource data to load (raster maps, vector maps, m.nviz.image commands)
  137. @param dataNames data labels (keys)
  138. @param dataType 'rast', 'vect', 'nviz'
  139. @param nvizRegion region which must be set for m.nviz.image
  140. """
  141. self.datasource = datasource
  142. self.dataType = dataType
  143. self.suffix = suffix
  144. self.nvizRegion = nvizRegion
  145. if dataNames:
  146. self.dataNames = dataNames
  147. else:
  148. self.dataNames = datasource
  149. self.dataNames = [name + self.suffix for name in self.dataNames]
  150. def GetBitmap(self, dataId):
  151. """!Returns bitmap with given key
  152. or 'no data' bitmap if no such key exists.
  153. @param dataId name of bitmap
  154. """
  155. if dataId:
  156. dataId += self.suffix
  157. try:
  158. bitmap = self.bitmapPool[dataId]
  159. except KeyError:
  160. bitmap = self.bitmapPool[None]
  161. return bitmap
  162. def WindowSizeChanged(self, width, height):
  163. """!Sets size when size of related window changes."""
  164. self.imageWidth, self.imageHeight = width, height
  165. def _createNoDataBitmap(self, width, height):
  166. """!Creates 'no data' bitmap.
  167. Used when requested bitmap is not available (loading data was not successful) or
  168. we want to show 'no data' bitmap.
  169. """
  170. bitmap = wx.EmptyBitmap(width, height)
  171. dc = wx.MemoryDC()
  172. dc.SelectObject(bitmap)
  173. dc.Clear()
  174. text = _("No data")
  175. dc.SetFont(wx.Font(pointSize = 40, family = wx.FONTFAMILY_SCRIPT,
  176. style = wx.FONTSTYLE_NORMAL, weight = wx.FONTWEIGHT_BOLD))
  177. tw, th = dc.GetTextExtent(text)
  178. dc.DrawText(text, (width-tw)/2, (height-th)/2)
  179. dc.SelectObject(wx.NullBitmap)
  180. return bitmap
  181. def Load(self, force = False, nprocs=4):
  182. """!Loads data.
  183. Shows progress dialog.
  184. @param force if True reload all data, otherwise only missing data
  185. @param imageWidth width of the image to render with d.rast or d.vect
  186. @param imageHeight height of the image to render with d.rast or d.vect
  187. @param nprocs number of procs to be used for rendering
  188. """
  189. if nprocs <= 0:
  190. nprocs = 1
  191. count, maxLength = self._dryLoad(rasters = self.datasource,
  192. names = self.dataNames, force = force)
  193. progress = None
  194. if self.dataType in ('rast', 'vect', 'strds', 'stvds') and count > 5 or \
  195. self.dataType == 'nviz':
  196. progress = wx.ProgressDialog(title = "Loading data",
  197. message = " " * (maxLength + 20), # ?
  198. maximum = count,
  199. parent = self.frame,
  200. style = wx.PD_CAN_ABORT | wx.PD_APP_MODAL |
  201. wx.PD_AUTO_HIDE | wx.PD_SMOOTH)
  202. updateFunction = progress.Update
  203. else:
  204. updateFunction = None
  205. if self.dataType in ('rast', 'vect', 'strds', 'stvds'):
  206. self._loadMaps(mapType=self.dataType, maps = self.datasource, names = self.dataNames,
  207. force = force, updateFunction = updateFunction,
  208. imageWidth=self.imageWidth, imageHeight=self.imageHeight, nprocs=nprocs)
  209. elif self.dataType == 'nviz':
  210. self._load3D(commands = self.datasource, region = self.nvizRegion, names = self.dataNames,
  211. force = force, updateFunction = updateFunction)
  212. if progress:
  213. progress.Destroy()
  214. self.mapsLoaded.emit()
  215. def Unload(self):
  216. self.datasource = None
  217. self.dataNames = None
  218. self.dataType = None
  219. def _dryLoad(self, rasters, names, force):
  220. """!Tries how many bitmaps will be loaded.
  221. Used for progress dialog.
  222. @param rasters raster maps to be loaded
  223. @param names names used as keys for bitmaps
  224. @param force load everything even though it is already there
  225. """
  226. count = 0
  227. maxLength = 0
  228. for raster, name in zip(rasters, names):
  229. if not(name in self.bitmapPool and force is False):
  230. count += 1
  231. if len(raster) > maxLength:
  232. maxLength = len(raster)
  233. return count, maxLength
  234. def _loadMaps(self, mapType, maps, names, force, updateFunction,
  235. imageWidth, imageHeight, nprocs):
  236. """!Loads rasters/vectors (also from temporal dataset).
  237. Uses d.rast/d.vect and multiprocessing for parallel rendering
  238. @param mapType Must be "rast" or "vect"
  239. @param maps raster or vector maps to be loaded
  240. @param names names used as keys for bitmaps
  241. @param force load everything even though it is already there
  242. @param updateFunction function called for updating progress dialog
  243. @param imageWidth width of the image to render with d.rast or d.vect
  244. @param imageHeight height of the image to render with d.rast or d.vect
  245. @param nprocs number of procs to be used for rendering
  246. """
  247. count = 0
  248. # Variables for parallel rendering
  249. proc_count = 0
  250. proc_list = []
  251. queue_list = []
  252. name_list = []
  253. mapNum = len(maps)
  254. # create no data bitmap
  255. if None not in self.bitmapPool or force:
  256. self.bitmapPool[None] = self._createNoDataBitmap(imageWidth, imageHeight)
  257. for mapname, name in zip(maps, names):
  258. count += 1
  259. if name in self.bitmapPool and force is False:
  260. continue
  261. # Queue object for interprocess communication
  262. q = Queue()
  263. # The separate render process
  264. p = Process(target=mapRenderProcess, args=(mapType, mapname, imageWidth, imageHeight, q))
  265. p.start()
  266. queue_list.append(q)
  267. proc_list.append(p)
  268. name_list.append(name)
  269. proc_count += 1
  270. # Wait for all running processes and read/store the created images
  271. if proc_count == nprocs or count == mapNum:
  272. for i in range(len(name_list)):
  273. proc_list[i].join()
  274. filename = queue_list[i].get()
  275. # Unfortunately the png files must be read here,
  276. # since the swig wx objects can not be serialized by the Queue object :(
  277. if filename == None:
  278. self.bitmapPool[name_list[i]] = wx.EmptyBitmap(imageWidth, imageHeight)
  279. else:
  280. self.bitmapPool[name_list[i]] = wx.BitmapFromImage(wx.Image(filename))
  281. os.remove(filename)
  282. proc_count = 0
  283. proc_list = []
  284. queue_list = []
  285. name_list = []
  286. if updateFunction:
  287. keepGoing, skip = updateFunction(count, mapname)
  288. if not keepGoing:
  289. break
  290. def _load3D(self, commands, region, names, force, updateFunction):
  291. """!Load 3D view images using m.nviz.image.
  292. @param commands
  293. @param region
  294. @param names names used as keys for bitmaps
  295. @param force load everything even though it is already there
  296. @param updateFunction function called for updating progress dialog
  297. """
  298. ncols, nrows = self.imageWidth, self.imageHeight
  299. count = 0
  300. format = 'ppm'
  301. tempFile = grass.tempfile(False)
  302. tempFileFormat = tempFile + '.' + format
  303. os.environ['GRASS_REGION'] = grass.region_env(**region)
  304. # create no data bitmap
  305. if None not in self.bitmapPool or force:
  306. self.bitmapPool[None] = self._createNoDataBitmap(ncols, nrows)
  307. for command, name in zip(commands, names):
  308. if name in self.bitmapPool and force is False:
  309. continue
  310. count += 1
  311. # set temporary file
  312. command[1]['output'] = tempFile
  313. # set size
  314. command[1]['size'] = '%d,%d' % (ncols, nrows)
  315. # set format
  316. command[1]['format'] = format
  317. returncode, messages = RunCommand(getErrorMsg = True, prog = command[0], **command[1])
  318. if returncode != 0:
  319. self.bitmapPool[name] = wx.EmptyBitmap(ncols, nrows)
  320. continue
  321. self.bitmapPool[name] = wx.Bitmap(tempFileFormat)
  322. if updateFunction:
  323. keepGoing, skip = updateFunction(count, name)
  324. if not keepGoing:
  325. break
  326. grass.try_remove(tempFileFormat)
  327. os.environ.pop('GRASS_REGION')
  328. def mapRenderProcess(mapType, mapname, width, height, fileQueue):
  329. """!Render raster or vector files as png image and write the
  330. resulting png filename in the provided file queue
  331. @param mapType Must be "rast" or "vect"
  332. @param mapname raster or vector map name to be rendered
  333. @param width Width of the resulting image
  334. @param height Height of the resulting image
  335. @param fileQueue The inter process communication queue storing the file name of the image
  336. """
  337. # temporary file, we use python here to avoid calling g.tempfile for each render process
  338. fileHandler, filename = tempfile.mkstemp(suffix=".png")
  339. os.close(fileHandler)
  340. # Set the environment variables for this process
  341. os.environ['GRASS_WIDTH'] = str(width)
  342. os.environ['GRASS_HEIGHT'] = str(height)
  343. driver = UserSettings.Get(group = 'display', key = 'driver', subkey = 'type')
  344. os.environ['GRASS_RENDER_IMMEDIATE'] = driver
  345. os.environ['GRASS_TRUECOLOR'] = "1"
  346. os.environ['GRASS_TRANSPARENT'] = "1"
  347. os.environ['GRASS_PNGFILE'] = str(filename)
  348. if mapType in ('rast', 'strds'):
  349. Debug.msg(1, "Render raster image " + str(filename))
  350. returncode, stdout, messages = read2_command('d.rast', map = mapname)
  351. elif mapType in ('vect', 'stvds'):
  352. Debug.msg(1, "Render vector image " + str(filename))
  353. returncode, stdout, messages = read2_command('d.vect', map = mapname)
  354. else:
  355. returncode = 1
  356. return
  357. if returncode != 0:
  358. fileQueue.put(None)
  359. os.remove(filename)
  360. return
  361. fileQueue.put(filename)
  362. class BitmapPool():
  363. """!Class storing bitmaps (emulates dictionary)"""
  364. def __init__(self):
  365. self.bitmaps = {}
  366. def __getitem__(self, key):
  367. return self.bitmaps[key]
  368. def __setitem__(self, key, bitmap):
  369. self.bitmaps[key] = bitmap
  370. def __contains__(self, key):
  371. return key in self.bitmaps
  372. def Clear(self, usedKeys):
  373. """!Removes all bitmaps which are currently not used.
  374. @param usedKeys keys which are currently used
  375. """
  376. for key in self.bitmaps.keys():
  377. if key not in usedKeys and key is not None:
  378. del self.bitmaps[key]
  379. def read2_command(*args, **kwargs):
  380. kwargs['stdout'] = grass.PIPE
  381. kwargs['stderr'] = grass.PIPE
  382. ps = grass.start_command(*args, **kwargs)
  383. stdout, stderr = ps.communicate()
  384. return ps.returncode, stdout, stderr