provider.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. # -*- coding: utf-8 -*-
  2. """
  3. @package animation.provider
  4. @brief Animation files and bitmaps management
  5. Classes:
  6. - mapwindow::BitmapProvider
  7. - mapwindow::BitmapRenderer
  8. - mapwindow::BitmapComposer
  9. - mapwindow::DictRefCounter
  10. - mapwindow::MapFilesPool
  11. - mapwindow::BitmapPool
  12. - mapwindow::CleanUp
  13. (C) 2013 by the GRASS Development Team
  14. This program is free software under the GNU General Public License
  15. (>=v2). Read the file COPYING that comes with GRASS for details.
  16. @author Anna Petrasova <kratochanna gmail.com>
  17. """
  18. import os
  19. import sys
  20. import wx
  21. import tempfile
  22. from multiprocessing import Process, Queue
  23. from core.gcmd import RunCommand, GException
  24. from core.settings import UserSettings
  25. from core.debug import Debug
  26. from core.utils import _, autoCropImageFromFile
  27. from animation.utils import HashCmd, HashCmds, GetFileFromCmd, GetFileFromCmds
  28. import grass.script.core as gcore
  29. from grass.script.task import cmdlist_to_tuple
  30. from grass.pydispatch.signal import Signal
  31. class BitmapProvider:
  32. """Class for management of image files and bitmaps.
  33. There is one instance of this class in the application.
  34. It handles both 2D and 3D animations.
  35. """
  36. def __init__(self, bitmapPool, mapFilesPool, tempDir,
  37. imageWidth=640, imageHeight=480):
  38. self._bitmapPool = bitmapPool
  39. self._mapFilesPool = mapFilesPool
  40. self.imageWidth = imageWidth # width of the image to render with d.rast or d.vect
  41. self.imageHeight = imageHeight # height of the image to render with d.rast or d.vect
  42. self._tempDir = tempDir
  43. self._uniqueCmds = []
  44. self._cmdsForComposition = []
  45. self._opacities = []
  46. self._cmds3D = []
  47. self._regionFor3D = None
  48. self._regions = []
  49. self._regionsForUniqueCmds = []
  50. self._renderer = BitmapRenderer(self._mapFilesPool, self._tempDir,
  51. self.imageWidth, self.imageHeight)
  52. self._composer = BitmapComposer(self._tempDir, self._mapFilesPool,
  53. self._bitmapPool, self.imageWidth,
  54. self.imageHeight)
  55. self.renderingStarted = Signal('BitmapProvider.renderingStarted')
  56. self.compositionStarted = Signal('BitmapProvider.compositionStarted')
  57. self.renderingContinues = Signal('BitmapProvider.renderingContinues')
  58. self.compositionContinues = Signal('BitmapProvider.compositionContinues')
  59. self.renderingFinished = Signal('BitmapProvider.renderingFinished')
  60. self.compositionFinished = Signal('BitmapProvider.compositionFinished')
  61. self.mapsLoaded = Signal('BitmapProvider.mapsLoaded')
  62. self._renderer.renderingContinues.connect(self.renderingContinues)
  63. self._composer.compositionContinues.connect(self.compositionContinues)
  64. def SetCmds(self, cmdsForComposition, opacities, regions=None):
  65. """Sets commands to be rendered with opacity levels.
  66. Applies to 2D mode.
  67. :param cmdsForComposition: list of lists of command lists
  68. [[['d.rast', 'map=elev_2001'], ['d.vect', 'map=points']], # g.pnmcomp
  69. [['d.rast', 'map=elev_2002'], ['d.vect', 'map=points']],
  70. ...]
  71. :param opacities: list of opacity values
  72. :param regions: list of regions
  73. """
  74. Debug.msg(2, "BitmapProvider.SetCmds: {n} lists".format(n=len(cmdsForComposition)))
  75. self._cmdsForComposition.extend(cmdsForComposition)
  76. self._opacities.extend(opacities)
  77. self._regions.extend(regions)
  78. self._getUniqueCmds()
  79. def SetCmds3D(self, cmds, region):
  80. """Sets commands for 3D rendering.
  81. :param cmds: list of commands m.nviz.image (cmd as a list)
  82. :param region: for 3D rendering
  83. """
  84. Debug.msg(2, "BitmapProvider.SetCmds3D: {c} commands".format(c=len(cmds)))
  85. self._cmds3D = cmds
  86. self._regionFor3D = region
  87. def _getUniqueCmds(self):
  88. """Returns list of unique commands.
  89. Takes into account the region assigned."""
  90. unique = list()
  91. for cmdList, region in zip(self._cmdsForComposition, self._regions):
  92. for cmd in cmdList:
  93. if region:
  94. unique.append((tuple(cmd), tuple(sorted(region.items()))))
  95. else:
  96. unique.append((tuple(cmd), None))
  97. unique = list(set(unique))
  98. self._uniqueCmds = [cmdAndRegion[0] for cmdAndRegion in unique]
  99. self._regionsForUniqueCmds.extend([dict(cmdAndRegion[1]) if cmdAndRegion[1] else None
  100. for cmdAndRegion in unique])
  101. def Unload(self):
  102. """Unloads currently loaded data.
  103. Needs to be called before setting new data.
  104. """
  105. Debug.msg(2, "BitmapProvider.Unload")
  106. if self._cmdsForComposition:
  107. for cmd, region in zip(self._uniqueCmds, self._regionsForUniqueCmds):
  108. del self._mapFilesPool[HashCmd(cmd, region)]
  109. for cmdList, region in zip(self._cmdsForComposition, self._regions):
  110. del self._bitmapPool[HashCmds(cmdList, region)]
  111. self._uniqueCmds = []
  112. self._cmdsForComposition = []
  113. self._opacities = []
  114. self._regions = []
  115. self._regionsForUniqueCmds = []
  116. if self._cmds3D:
  117. self._cmds3D = []
  118. self._regionFor3D = None
  119. def _dryRender(self, uniqueCmds, regions, force):
  120. """Determines how many files will be rendered.
  121. :param uniqueCmds: list of commands which are to be rendered
  122. :param force: if forced rerendering
  123. :param regions: list of regions assigned to the commands
  124. """
  125. count = 0
  126. for cmd, region in zip(uniqueCmds, regions):
  127. filename = GetFileFromCmd(self._tempDir, cmd, region)
  128. if not force and os.path.exists(filename) and \
  129. self._mapFilesPool.GetSize(HashCmd(cmd, region)) == (self.imageWidth, self.imageHeight):
  130. continue
  131. count += 1
  132. Debug.msg(3, "BitmapProvider._dryRender: {c} files to be rendered".format(c=count))
  133. return count
  134. def _dryCompose(self, cmdLists, regions, force):
  135. """Determines how many lists of (commands) files
  136. will be composed (with g.pnmcomp).
  137. :param cmdLists: list of commands lists which are to be composed
  138. :param regions: list of regions assigned to the commands
  139. :param force: if forced rerendering
  140. """
  141. count = 0
  142. for cmdList, region in zip(cmdLists, regions):
  143. if not force and HashCmds(cmdList, region) in self._bitmapPool and \
  144. self._bitmapPool[HashCmds(cmdList, region)].GetSize() == (self.imageWidth,
  145. self.imageHeight):
  146. continue
  147. count += 1
  148. Debug.msg(2, "BitmapProvider._dryCompose: {c} files to be composed".format(c=count))
  149. return count
  150. def Load(self, force=False, bgcolor=(255, 255, 255), nprocs=4):
  151. """Loads data, both 2D and 3D. In case of 2D, it creates composites,
  152. even when there is only 1 layer to compose (to be changed for speedup)
  153. :param force: if True reload all data, otherwise only missing data
  154. :param bgcolor: background color as a tuple of 3 values 0 to 255
  155. :param nprocs: number of procs to be used for rendering
  156. """
  157. Debug.msg(2, "BitmapProvider.Load: "
  158. "force={f}, bgcolor={b}, nprocs={n}".format(f=force,
  159. b=bgcolor,
  160. n=nprocs))
  161. cmds = []
  162. regions = []
  163. if self._uniqueCmds:
  164. cmds.extend(self._uniqueCmds)
  165. regions.extend(self._regionsForUniqueCmds)
  166. if self._cmds3D:
  167. cmds.extend(self._cmds3D)
  168. regions.extend([None] * len(self._cmds3D))
  169. count = self._dryRender(cmds, regions, force=force)
  170. self.renderingStarted.emit(count=count)
  171. # create no data bitmap
  172. if None not in self._bitmapPool or force:
  173. self._bitmapPool[None] = createNoDataBitmap(self.imageWidth, self.imageHeight)
  174. ok = self._renderer.Render(cmds, regions, regionFor3D=self._regionFor3D,
  175. bgcolor=bgcolor, force=force, nprocs=nprocs)
  176. self.renderingFinished.emit()
  177. if not ok:
  178. self.mapsLoaded.emit() # what to do here?
  179. return
  180. if self._cmdsForComposition:
  181. count = self._dryCompose(self._cmdsForComposition, self._regions, force=force)
  182. self.compositionStarted.emit(count=count)
  183. self._composer.Compose(self._cmdsForComposition, self._regions, self._opacities,
  184. bgcolor=bgcolor, force=force, nprocs=nprocs)
  185. self.compositionFinished.emit()
  186. if self._cmds3D:
  187. for cmd in self._cmds3D:
  188. self._bitmapPool[HashCmds([cmd], None)] = \
  189. wx.Bitmap(GetFileFromCmd(self._tempDir, cmd, None))
  190. self.mapsLoaded.emit()
  191. def RequestStopRendering(self):
  192. """Requests to stop rendering/composition"""
  193. Debug.msg(2, "BitmapProvider.RequestStopRendering")
  194. self._renderer.RequestStopRendering()
  195. self._composer.RequestStopComposing()
  196. def GetBitmap(self, dataId):
  197. """Returns bitmap with given key
  198. or 'no data' bitmap if no such key exists.
  199. :param dataId: name of bitmap
  200. """
  201. try:
  202. bitmap = self._bitmapPool[dataId]
  203. except KeyError:
  204. bitmap = self._bitmapPool[None]
  205. return bitmap
  206. def WindowSizeChanged(self, width, height):
  207. """Sets size when size of related window changes."""
  208. Debug.msg(5, "BitmapProvider.WindowSizeChanged: w={w}, h={h}".format(w=width, h=height))
  209. self.imageWidth, self.imageHeight = width, height
  210. self._composer.imageWidth = self._renderer.imageWidth = width
  211. self._composer.imageHeight = self._renderer.imageHeight = height
  212. def LoadOverlay(self, cmd):
  213. """Creates raster legend with d.legend
  214. :param cmd: d.legend command as a list
  215. :return: bitmap with legend
  216. """
  217. Debug.msg(5, "BitmapProvider.LoadOverlay: cmd={c}".format(c=cmd))
  218. fileHandler, filename = tempfile.mkstemp(suffix=".png")
  219. os.close(fileHandler)
  220. # Set the environment variables for this process
  221. _setEnvironment(self.imageWidth, self.imageHeight, filename,
  222. transparent=True, bgcolor=(0, 0, 0))
  223. Debug.msg(1, "Render raster legend " + str(filename))
  224. cmdTuple = cmdlist_to_tuple(cmd)
  225. returncode, stdout, messages = read2_command(cmdTuple[0], **cmdTuple[1])
  226. if returncode == 0:
  227. return wx.BitmapFromImage(autoCropImageFromFile(filename))
  228. else:
  229. os.remove(filename)
  230. raise GException(messages)
  231. class BitmapRenderer:
  232. """Class which renderes 2D and 3D images to files."""
  233. def __init__(self, mapFilesPool, tempDir,
  234. imageWidth, imageHeight):
  235. self._mapFilesPool = mapFilesPool
  236. self._tempDir = tempDir
  237. self.imageWidth = imageWidth
  238. self.imageHeight = imageHeight
  239. self.renderingContinues = Signal('BitmapRenderer.renderingContinues')
  240. self._stopRendering = False
  241. self._isRendering = False
  242. def Render(self, cmdList, regions, regionFor3D, bgcolor, force, nprocs):
  243. """Renders all maps and stores files.
  244. :param cmdList: list of rendering commands to run
  245. :param regions: regions for 2D rendering assigned to commands
  246. :param regionFor3D: region for setting 3D view
  247. :param bgcolor: background color as a tuple of 3 values 0 to 255
  248. :param force: if True reload all data, otherwise only missing data
  249. :param nprocs: number of procs to be used for rendering
  250. """
  251. Debug.msg(3, "BitmapRenderer.Render")
  252. count = 0
  253. # Variables for parallel rendering
  254. proc_count = 0
  255. proc_list = []
  256. queue_list = []
  257. cmd_list = []
  258. filteredCmdList = []
  259. for cmd, region in zip(cmdList, regions):
  260. filename = GetFileFromCmd(self._tempDir, cmd, region)
  261. if not force and os.path.exists(filename) and \
  262. self._mapFilesPool.GetSize(HashCmd(cmd, region)) == (self.imageWidth, self.imageHeight):
  263. # for reference counting
  264. self._mapFilesPool[HashCmd(cmd, region)] = filename
  265. continue
  266. filteredCmdList.append((cmd, region))
  267. mapNum = len(filteredCmdList)
  268. stopped = False
  269. self._isRendering = True
  270. for cmd, region in filteredCmdList:
  271. count += 1
  272. # Queue object for interprocess communication
  273. q = Queue()
  274. # The separate render process
  275. if cmd[0] == 'm.nviz.image':
  276. p = Process(target=RenderProcess3D,
  277. args=(self.imageWidth, self.imageHeight, self._tempDir,
  278. cmd, regionFor3D, bgcolor, q))
  279. else:
  280. p = Process(target=RenderProcess2D,
  281. args=(self.imageWidth, self.imageHeight, self._tempDir,
  282. cmd, region, bgcolor, q))
  283. p.start()
  284. queue_list.append(q)
  285. proc_list.append(p)
  286. cmd_list.append((cmd, region))
  287. proc_count += 1
  288. # Wait for all running processes and read/store the created images
  289. if proc_count == nprocs or count == mapNum:
  290. for i in range(len(cmd_list)):
  291. proc_list[i].join()
  292. filename = queue_list[i].get()
  293. self._mapFilesPool[HashCmd(cmd_list[i][0], cmd_list[i][1])] = filename
  294. self._mapFilesPool.SetSize(HashCmd(cmd_list[i][0], cmd_list[i][1]),
  295. (self.imageWidth, self.imageHeight))
  296. proc_count = 0
  297. proc_list = []
  298. queue_list = []
  299. cmd_list = []
  300. self.renderingContinues.emit(current=count, text=_("Rendering map layers"))
  301. if self._stopRendering:
  302. self._stopRendering = False
  303. stopped = True
  304. break
  305. self._isRendering = False
  306. return not stopped
  307. def RequestStopRendering(self):
  308. """Requests to stop rendering."""
  309. if self._isRendering:
  310. self._stopRendering = True
  311. class BitmapComposer:
  312. """Class which handles the composition of image files with g.pnmcomp."""
  313. def __init__(self, tempDir, mapFilesPool, bitmapPool,
  314. imageWidth, imageHeight):
  315. self._mapFilesPool = mapFilesPool
  316. self._bitmapPool = bitmapPool
  317. self._tempDir = tempDir
  318. self.imageWidth = imageWidth
  319. self.imageHeight = imageHeight
  320. self.compositionContinues = Signal('BitmapComposer.composingContinues')
  321. self._stopComposing = False
  322. self._isComposing = False
  323. def Compose(self, cmdLists, regions, opacityList, bgcolor, force, nprocs):
  324. """Performs the composition of ppm/pgm files.
  325. :param cmdLists: lists of rendering commands lists to compose
  326. :param regions: regions for 2D rendering assigned to commands
  327. :param opacityList: list of lists of opacity values
  328. :param bgcolor: background color as a tuple of 3 values 0 to 255
  329. :param force: if True reload all data, otherwise only missing data
  330. :param nprocs: number of procs to be used for rendering
  331. """
  332. Debug.msg(3, "BitmapComposer.Compose")
  333. count = 0
  334. # Variables for parallel rendering
  335. proc_count = 0
  336. proc_list = []
  337. queue_list = []
  338. cmd_lists = []
  339. filteredCmdLists = []
  340. for cmdList, region in zip(cmdLists, regions):
  341. if not force and HashCmds(cmdList, region) in self._bitmapPool and \
  342. self._bitmapPool[HashCmds(cmdList, region)].GetSize() == (self.imageWidth,
  343. self.imageHeight):
  344. # TODO: find a better way than to assign the same to increase the reference
  345. self._bitmapPool[HashCmds(cmdList, region)] = self._bitmapPool[HashCmds(cmdList, region)]
  346. continue
  347. filteredCmdLists.append((cmdList, region))
  348. num = len(filteredCmdLists)
  349. self._isComposing = True
  350. for cmdList, region in filteredCmdLists:
  351. count += 1
  352. # Queue object for interprocess communication
  353. q = Queue()
  354. # The separate render process
  355. p = Process(target=CompositeProcess,
  356. args=(self.imageWidth, self.imageHeight, self._tempDir,
  357. cmdList, region, opacityList, bgcolor, q))
  358. p.start()
  359. queue_list.append(q)
  360. proc_list.append(p)
  361. cmd_lists.append((cmdList, region))
  362. proc_count += 1
  363. # Wait for all running processes and read/store the created images
  364. if proc_count == nprocs or count == num:
  365. for i in range(len(cmd_lists)):
  366. proc_list[i].join()
  367. filename = queue_list[i].get()
  368. if filename is None:
  369. self._bitmapPool[HashCmds(cmd_lists[i][0], cmd_lists[i][1])] = \
  370. createNoDataBitmap(self.imageWidth, self.imageHeight,
  371. text="Failed to render")
  372. else:
  373. self._bitmapPool[HashCmds(cmd_lists[i][0], cmd_lists[i][1])] = \
  374. wx.BitmapFromImage(wx.Image(filename))
  375. os.remove(filename)
  376. proc_count = 0
  377. proc_list = []
  378. queue_list = []
  379. cmd_lists = []
  380. self.compositionContinues.emit(current=count, text=_("Overlaying map layers"))
  381. if self._stopComposing:
  382. self._stopComposing = False
  383. break
  384. self._isComposing = False
  385. def RequestStopComposing(self):
  386. """Requests to stop the composition."""
  387. if self._isComposing:
  388. self._stopComposing = True
  389. def RenderProcess2D(imageWidth, imageHeight, tempDir, cmd, region, bgcolor, fileQueue):
  390. """Render raster or vector files as ppm image and write the
  391. resulting ppm filename in the provided file queue
  392. :param imageWidth: image width
  393. :param imageHeight: image height
  394. :param tempDir: directory for rendering
  395. :param cmd: d.rast/d.vect command as a list
  396. :param region: region as a dict or None
  397. :param bgcolor: background color as a tuple of 3 values 0 to 255
  398. :param fileQueue: the inter process communication queue
  399. storing the file name of the image
  400. """
  401. filename = GetFileFromCmd(tempDir, cmd, region)
  402. transparency = True
  403. # Set the environment variables for this process
  404. _setEnvironment(imageWidth, imageHeight, filename,
  405. transparent=transparency, bgcolor=bgcolor)
  406. if region:
  407. os.environ['GRASS_REGION'] = gcore.region_env(**region)
  408. cmdTuple = cmdlist_to_tuple(cmd)
  409. returncode, stdout, messages = read2_command(cmdTuple[0], **cmdTuple[1])
  410. if returncode != 0:
  411. gcore.warning("Rendering failed:\n" + messages)
  412. fileQueue.put(None)
  413. if region:
  414. os.environ.pop('GRASS_REGION')
  415. os.remove(filename)
  416. return
  417. if region:
  418. os.environ.pop('GRASS_REGION')
  419. fileQueue.put(filename)
  420. def RenderProcess3D(imageWidth, imageHeight, tempDir, cmd, region, bgcolor, fileQueue):
  421. """Renders image with m.nviz.image and writes the
  422. resulting ppm filename in the provided file queue
  423. :param imageWidth: image width
  424. :param imageHeight: image height
  425. :param tempDir: directory for rendering
  426. :param cmd: m.nviz.image command as a list
  427. :param region: region as a dict
  428. :param bgcolor: background color as a tuple of 3 values 0 to 255
  429. :param fileQueue: the inter process communication queue
  430. storing the file name of the image
  431. """
  432. filename = GetFileFromCmd(tempDir, cmd, None)
  433. os.environ['GRASS_REGION'] = gcore.region_env(region3d=True, **region)
  434. Debug.msg(1, "Render image to file " + str(filename))
  435. cmdTuple = cmdlist_to_tuple(cmd)
  436. cmdTuple[1]['output'] = os.path.splitext(filename)[0]
  437. # set size
  438. cmdTuple[1]['size'] = '%d,%d' % (imageWidth, imageHeight)
  439. # set format
  440. cmdTuple[1]['format'] = 'ppm'
  441. cmdTuple[1]['bgcolor'] = bgcolor = ':'.join([str(part) for part in bgcolor])
  442. returncode, stdout, messages = read2_command(cmdTuple[0], **cmdTuple[1])
  443. if returncode != 0:
  444. gcore.warning("Rendering failed:\n" + messages)
  445. fileQueue.put(None)
  446. os.environ.pop('GRASS_REGION')
  447. return
  448. os.environ.pop('GRASS_REGION')
  449. fileQueue.put(filename)
  450. def CompositeProcess(imageWidth, imageHeight, tempDir, cmdList, region, opacities, bgcolor, fileQueue):
  451. """Performs the composition of image ppm files and writes the
  452. resulting ppm filename in the provided file queue
  453. :param imageWidth: image width
  454. :param imageHeight: image height
  455. :param tempDir: directory for rendering
  456. :param cmdList: list of d.rast/d.vect commands
  457. :param region: region as a dict or None
  458. :param opacites: list of opacities
  459. :param bgcolor: background color as a tuple of 3 values 0 to 255
  460. :param fileQueue: the inter process communication queue
  461. storing the file name of the image
  462. """
  463. maps = []
  464. masks = []
  465. for cmd in cmdList:
  466. maps.append(GetFileFromCmd(tempDir, cmd, region))
  467. masks.append(GetFileFromCmd(tempDir, cmd, region, 'pgm'))
  468. filename = GetFileFromCmds(tempDir, cmdList, region)
  469. # Set the environment variables for this process
  470. _setEnvironment(imageWidth, imageHeight, filename,
  471. transparent=False, bgcolor=bgcolor)
  472. opacities = [str(op) for op in opacities]
  473. bgcolor = ':'.join([str(part) for part in bgcolor])
  474. returncode, stdout, messages = read2_command('g.pnmcomp',
  475. overwrite=True,
  476. input='%s' % ",".join(reversed(maps)),
  477. mask='%s' % ",".join(reversed(masks)),
  478. opacity='%s' % ",".join(reversed(opacities)),
  479. bgcolor=bgcolor,
  480. width=imageWidth,
  481. height=imageHeight,
  482. output=filename)
  483. if returncode != 0:
  484. gcore.warning("Rendering composite failed:\n" + messages)
  485. fileQueue.put(None)
  486. os.remove(filename)
  487. return
  488. fileQueue.put(filename)
  489. class DictRefCounter:
  490. """Base class storing map files/bitmaps (emulates dictionary).
  491. Counts the references to know which files/bitmaps to delete.
  492. """
  493. def __init__(self):
  494. self.dictionary = {}
  495. self.referenceCount = {}
  496. def __getitem__(self, key):
  497. return self.dictionary[key]
  498. def __setitem__(self, key, value):
  499. self.dictionary[key] = value
  500. if key not in self.referenceCount:
  501. self.referenceCount[key] = 1
  502. else:
  503. self.referenceCount[key] += 1
  504. Debug.msg(5, 'DictRefCounter.__setitem__: +1 for key {k}'.format(k=key))
  505. def __contains__(self, key):
  506. return key in self.dictionary
  507. def __delitem__(self, key):
  508. self.referenceCount[key] -= 1
  509. Debug.msg(5, 'DictRefCounter.__delitem__: -1 for key {k}'.format(k=key))
  510. def keys(self):
  511. return self.dictionary.keys()
  512. def Clear(self):
  513. """Clears items which are not needed any more."""
  514. Debug.msg(4, 'DictRefCounter.Clear')
  515. for key in self.dictionary.keys():
  516. if key is not None:
  517. if self.referenceCount[key] <= 0:
  518. del self.dictionary[key]
  519. del self.referenceCount[key]
  520. class MapFilesPool(DictRefCounter):
  521. """Stores rendered images as files."""
  522. def __init__(self):
  523. DictRefCounter.__init__(self)
  524. self.size = {}
  525. def SetSize(self, key, size):
  526. self.size[key] = size
  527. def GetSize(self, key):
  528. return self.size[key]
  529. def Clear(self):
  530. """Removes files which are not needed anymore.
  531. Removes both ppm and pgm.
  532. """
  533. Debug.msg(4, 'MapFilesPool.Clear')
  534. for key in self.dictionary.keys():
  535. if self.referenceCount[key] <= 0:
  536. name, ext = os.path.splitext(self.dictionary[key])
  537. os.remove(self.dictionary[key])
  538. if ext == '.ppm':
  539. os.remove(name + '.pgm')
  540. del self.dictionary[key]
  541. del self.referenceCount[key]
  542. del self.size[key]
  543. class BitmapPool(DictRefCounter):
  544. """Class storing bitmaps (emulates dictionary)"""
  545. def __init__(self):
  546. DictRefCounter.__init__(self)
  547. class CleanUp:
  548. """Responsible for cleaning up the files."""
  549. def __init__(self, tempDir):
  550. self._tempDir = tempDir
  551. def __call__(self):
  552. import shutil
  553. if os.path.exists(self._tempDir):
  554. try:
  555. shutil.rmtree(self._tempDir)
  556. Debug.msg(5, 'CleanUp: removed directory {t}'.format(t=self._tempDir))
  557. except OSError:
  558. gcore.warning(_("Directory {t} not removed.").format(t=self._tempDir))
  559. def _setEnvironment(width, height, filename, transparent, bgcolor):
  560. """Sets environmental variables for 2D rendering.
  561. :param width: rendering width
  562. :param height: rendering height
  563. :param filename: file name
  564. :param transparent: use transparency
  565. :param bgcolor: background color as a tuple of 3 values 0 to 255
  566. """
  567. Debug.msg(5, "_setEnvironment: width={w}, height={h}, "
  568. "filename={f}, transparent={t}, bgcolor={b}".format(w=width,
  569. h=height,
  570. f=filename,
  571. t=transparent,
  572. b=bgcolor))
  573. os.environ['GRASS_RENDER_WIDTH'] = str(width)
  574. os.environ['GRASS_RENDER_HEIGHT'] = str(height)
  575. driver = UserSettings.Get(group='display', key='driver', subkey='type')
  576. os.environ['GRASS_RENDER_IMMEDIATE'] = driver
  577. os.environ['GRASS_RENDER_BACKGROUNDCOLOR'] = '{r:02x}{g:02x}{b:02x}'.format(r=bgcolor[0],
  578. g=bgcolor[1],
  579. b=bgcolor[2])
  580. os.environ['GRASS_RENDER_TRUECOLOR'] = "TRUE"
  581. if transparent:
  582. os.environ['GRASS_RENDER_TRANSPARENT'] = "TRUE"
  583. else:
  584. os.environ['GRASS_RENDER_TRANSPARENT'] = "FALSE"
  585. os.environ['GRASS_RENDER_FILE'] = str(filename)
  586. def createNoDataBitmap(imageWidth, imageHeight, text="No data"):
  587. """Creates 'no data' bitmap.
  588. Used when requested bitmap is not available (loading data was not successful) or
  589. we want to show 'no data' bitmap.
  590. :param imageWidth: image width
  591. :param imageHeight: image height
  592. """
  593. Debug.msg(4, "createNoDataBitmap: w={w}, h={h}, text={t}".format(w=imageWidth,
  594. h=imageHeight,
  595. t=text))
  596. bitmap = wx.EmptyBitmap(imageWidth, imageHeight)
  597. dc = wx.MemoryDC()
  598. dc.SelectObject(bitmap)
  599. dc.Clear()
  600. text = _(text)
  601. dc.SetFont(wx.Font(pointSize=40, family=wx.FONTFAMILY_SCRIPT,
  602. style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_BOLD))
  603. tw, th = dc.GetTextExtent(text)
  604. dc.DrawText(text, (imageWidth - tw) / 2, (imageHeight - th) / 2)
  605. dc.SelectObject(wx.NullBitmap)
  606. return bitmap
  607. def read2_command(*args, **kwargs):
  608. kwargs['stdout'] = gcore.PIPE
  609. kwargs['stderr'] = gcore.PIPE
  610. ps = gcore.start_command(*args, **kwargs)
  611. stdout, stderr = ps.communicate()
  612. return ps.returncode, stdout, stderr
  613. def test():
  614. import shutil
  615. from core.layerlist import LayerList, Layer
  616. from animation.data import AnimLayer
  617. from animation.utils import layerListToCmdsMatrix
  618. import grass.temporal as tgis
  619. tgis.init()
  620. layerList = LayerList()
  621. layer = AnimLayer()
  622. layer.mapType = 'strds'
  623. layer.name = 'JR'
  624. layer.cmd = ['d.rast', 'map=elev_2007_1m']
  625. layerList.AddLayer(layer)
  626. layer = Layer()
  627. layer.mapType = 'vector'
  628. layer.name = 'buildings_2009_approx'
  629. layer.cmd = ['d.vect', 'map=buildings_2009_approx',
  630. 'color=grey']
  631. layer.opacity = 50
  632. layerList.AddLayer(layer)
  633. bPool = BitmapPool()
  634. mapFilesPool = MapFilesPool()
  635. tempDir = '/tmp/test'
  636. if os.path.exists(tempDir):
  637. shutil.rmtree(tempDir)
  638. os.mkdir(tempDir)
  639. # comment this line to keep the directory after prgm ends
  640. # cleanUp = CleanUp(tempDir)
  641. # import atexit
  642. # atexit.register(cleanUp)
  643. prov = BitmapProvider(bPool, mapFilesPool, tempDir,
  644. imageWidth=640, imageHeight=480)
  645. prov.renderingStarted.connect(
  646. lambda count: sys.stdout.write("Total number of maps: {c}\n".format(c=count)))
  647. prov.renderingContinues.connect(
  648. lambda current, text: sys.stdout.write("Current number: {c}\n".format(c=current)))
  649. prov.compositionStarted.connect(
  650. lambda count: sys.stdout.write("Composition: total number of maps: {c}\n".format(c=count)))
  651. prov.compositionContinues.connect(
  652. lambda current, text: sys.stdout.write("Composition: Current number: {c}\n".format(c=current)))
  653. prov.mapsLoaded.connect(
  654. lambda: sys.stdout.write("Maps loading finished\n"))
  655. cmdMatrix = layerListToCmdsMatrix(layerList)
  656. prov.SetCmds(cmdMatrix, [l.opacity for l in layerList])
  657. app = wx.App()
  658. prov.Load(bgcolor=(13, 156, 230), nprocs=4)
  659. for key in bPool.keys():
  660. if key is not None:
  661. bPool[key].SaveFile(os.path.join(tempDir, key + '.png'), wx.BITMAP_TYPE_PNG)
  662. # prov.Unload()
  663. # prov.SetCmds(cmdMatrix, [l.opacity for l in layerList])
  664. # prov.Load(bgcolor=(13, 156, 230))
  665. # prov.Unload()
  666. # newList = LayerList()
  667. # prov.SetCmds(layerListToCmdsMatrix(newList), [l.opacity for l in newList])
  668. # prov.Load()
  669. # prov.Unload()
  670. # mapFilesPool.Clear()
  671. # bPool.Clear()
  672. # print bPool.keys(), mapFilesPool.keys()
  673. if __name__ == '__main__':
  674. test()