provider.py 31 KB

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