provider.py 31 KB

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