mapwindow.py 21 KB

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