provider.py 32 KB

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