main.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. """
  2. @package mapdisp.main
  3. @brief Start Map Display as standalone application
  4. Classes:
  5. - mapdisp::DMonMap
  6. - mapdisp::Layer
  7. - mapdisp::LayerList
  8. - mapdisp::DMonGrassInterface
  9. - mapdisp::DMonFrame
  10. - mapdisp::MapApp
  11. Usage:
  12. python mapdisp/main.py monitor-identifier /path/to/map/file /path/to/command/file /path/to/env/file
  13. (C) 2006-2015 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 Michael Barton
  17. @author Jachym Cepicky
  18. @author Martin Landa <landa.martin gmail.com>
  19. @author Vaclav Petras <wenzeslaus gmail.com> (MapFrameBase)
  20. @author Anna Kratochvilova <kratochanna gmail.com> (MapFrameBase)
  21. """
  22. from __future__ import print_function
  23. import os
  24. import sys
  25. import six
  26. import time
  27. import shutil
  28. import fileinput
  29. from grass.script.utils import try_remove
  30. from grass.script import core as grass
  31. from grass.script.task import cmdtuple_to_list, cmdlist_to_tuple
  32. from grass.pydispatch.signal import Signal
  33. from grass.script.setup import set_gui_path
  34. set_gui_path()
  35. # GUI imports require path to GUI code to be set.
  36. from core import globalvar # noqa: E402
  37. import wx # noqa: E402
  38. from core import utils # noqa: E402
  39. from core.giface import StandaloneGrassInterface # noqa: E402
  40. from core.gcmd import RunCommand # noqa: E402
  41. from core.render import Map, MapLayer, RenderMapMgr # noqa: E402
  42. from mapdisp.frame import MapFrame # noqa: E402
  43. from core.debug import Debug # noqa: E402
  44. from core.settings import UserSettings # noqa: E402
  45. # for standalone app
  46. monFile = {'cmd': None,
  47. 'map': None,
  48. 'env': None,
  49. }
  50. monName = None
  51. monSize = list(globalvar.MAP_WINDOW_SIZE)
  52. monDecor = False
  53. class DMonMap(Map):
  54. def __init__(self, giface, cmdfile=None, mapfile=None):
  55. """Map composition (stack of map layers and overlays)
  56. :param cmdline: full path to the cmd file (defined by d.mon)
  57. :param mapfile: full path to the map file (defined by d.mon)
  58. """
  59. Map.__init__(self)
  60. self._giface = giface
  61. # environment settings
  62. self.env = dict()
  63. self.cmdfile = cmdfile
  64. # list of layers for rendering added from cmd file
  65. # TODO temporary solution, layer management by different tools in GRASS
  66. # should be resolved
  67. self.ownedLayers = []
  68. self.oldOverlays = []
  69. if mapfile:
  70. self.mapfileCmd = mapfile
  71. self.maskfileCmd = os.path.splitext(mapfile)[0] + '.pgm'
  72. # generated file for g.pnmcomp output for rendering the map
  73. self.mapfile = monFile['map']
  74. if os.path.splitext(self.mapfile)[1] != '.ppm':
  75. self.mapfile += '.ppm'
  76. # signal sent when d.out.file/d.to.rast appears in cmd file, attribute
  77. # is cmd
  78. self.saveToFile = Signal('DMonMap.saveToFile')
  79. self.dToRast = Signal('DMonMap.dToRast')
  80. # signal sent when d.what.rast/vect appears in cmd file, attribute is
  81. # cmd
  82. self.query = Signal('DMonMap.query')
  83. self.renderMgr = RenderMapMgr(self)
  84. # update legend file variable with the one d.mon uses
  85. with open(monFile['env'], 'r') as f:
  86. lines = f.readlines()
  87. for line in lines:
  88. if 'GRASS_LEGEND_FILE' in line:
  89. legfile = line.split('=', 1)[1].strip()
  90. self.renderMgr.UpdateRenderEnv({'GRASS_LEGEND_FILE': legfile})
  91. break
  92. def GetLayersFromCmdFile(self):
  93. """Get list of map layers from cmdfile
  94. """
  95. if not self.cmdfile:
  96. return
  97. nlayers = 0
  98. try:
  99. fd = open(self.cmdfile, 'r')
  100. lines = fd.readlines()
  101. fd.close()
  102. # detect d.out.file, delete the line from the cmd file and export
  103. # graphics
  104. if len(lines) > 0:
  105. if lines[-1].startswith('d.out.file') or \
  106. lines[-1].startswith('d.to.rast'):
  107. dCmd = lines[-1].strip()
  108. fd = open(self.cmdfile, 'w')
  109. fd.writelines(lines[:-1])
  110. fd.close()
  111. if lines[-1].startswith('d.out.file'):
  112. self.saveToFile.emit(cmd=utils.split(dCmd))
  113. else:
  114. self.dToRast.emit(cmd=utils.split(dCmd))
  115. return
  116. if lines[-1].startswith('d.what'):
  117. dWhatCmd = lines[-1].strip()
  118. fd = open(self.cmdfile, 'w')
  119. fd.writelines(lines[:-1])
  120. fd.close()
  121. if '=' in utils.split(dWhatCmd)[1]:
  122. maps = utils.split(dWhatCmd)[1].split('=')[
  123. 1].split(',')
  124. else:
  125. maps = utils.split(dWhatCmd)[1].split(',')
  126. self.query.emit(ltype=utils.split(dWhatCmd)[
  127. 0].split('.')[-1], maps=maps)
  128. return
  129. else:
  130. # clean overlays after erase
  131. self.oldOverlays = []
  132. overlays = list(self._giface.GetMapDisplay().decorations.keys())
  133. for each in overlays:
  134. self._giface.GetMapDisplay().RemoveOverlay(each)
  135. existingLayers = self.GetListOfLayers()
  136. # holds new rendreing order for every layer in existingLayers
  137. layersOrder = [-1] * len(existingLayers)
  138. # next number in rendering order
  139. next_layer = 0
  140. mapFile = None
  141. render_env = dict()
  142. for line in lines:
  143. if line.startswith('#'):
  144. if 'GRASS_RENDER_FILE' in line:
  145. mapFile = line.split('=', 1)[1].strip()
  146. try:
  147. k, v = line[2:].strip().split('=', 1)
  148. except:
  149. pass
  150. render_env[k] = v
  151. continue
  152. cmd = utils.split(line.strip())
  153. ltype = None
  154. try:
  155. ltype = utils.command2ltype[cmd[0]]
  156. except KeyError:
  157. grass.warning(_("Unsupported command %s.") % cmd[0])
  158. continue
  159. name = utils.GetLayerNameFromCmd(cmd, fullyQualified=True,
  160. layerType=ltype)[0]
  161. args = {}
  162. if ltype in ('barscale', 'rastleg', 'northarrow', 'text', 'vectleg'):
  163. # TODO: this is still not optimal
  164. # it is there to prevent adding the same overlay multiple times
  165. if cmd in self.oldOverlays:
  166. continue
  167. if ltype == 'rastleg':
  168. self._giface.GetMapDisplay().AddLegendRast(cmd=cmd)
  169. elif ltype == 'barscale':
  170. self._giface.GetMapDisplay().AddBarscale(cmd=cmd)
  171. elif ltype == 'northarrow':
  172. self._giface.GetMapDisplay().AddArrow(cmd=cmd)
  173. elif ltype == 'text':
  174. self._giface.GetMapDisplay().AddDtext(cmd=cmd)
  175. elif ltype == 'vectleg':
  176. self._giface.GetMapDisplay().AddLegendVect(cmd=cmd)
  177. self.oldOverlays.append(cmd)
  178. continue
  179. classLayer = MapLayer
  180. args['ltype'] = ltype
  181. exists = False
  182. for i, layer in enumerate(existingLayers):
  183. if layer.GetCmd(
  184. string=True) == utils.GetCmdString(cmdlist_to_tuple(cmd)):
  185. exists = True
  186. if layersOrder[i] == -1:
  187. layersOrder[i] = next_layer
  188. next_layer += 1
  189. # layer must be put higher in render order (same cmd was insered more times)
  190. # TODO delete rendurant cmds from cmd file?
  191. else:
  192. for j, l_order in enumerate(layersOrder):
  193. if l_order > layersOrder[i]:
  194. layersOrder[j] -= 1
  195. layersOrder[i] = next_layer - 1
  196. break
  197. if exists:
  198. continue
  199. mapLayer = classLayer(name=name,
  200. cmd=cmd,
  201. Map=None,
  202. hidden=True,
  203. render=False,
  204. mapfile=mapFile,
  205. **args)
  206. mapLayer.GetRenderMgr().updateProgress.connect(self.GetRenderMgr().ReportProgress)
  207. if render_env:
  208. mapLayer.GetRenderMgr().UpdateRenderEnv(render_env)
  209. render_env = dict()
  210. newLayer = self._addLayer(mapLayer)
  211. existingLayers.append(newLayer)
  212. self.ownedLayers.append(newLayer)
  213. layersOrder.append(next_layer)
  214. next_layer += 1
  215. nlayers += 1
  216. reorderedLayers = [-1] * next_layer
  217. for i, layer in enumerate(existingLayers):
  218. # owned layer was not found in cmd file -> is deleted
  219. if layersOrder[i] == -1 and layer in self.ownedLayers:
  220. self.ownedLayers.remove(layer)
  221. self.DeleteLayer(layer)
  222. # other layer e. g. added by wx.vnet are added to the top
  223. elif layersOrder[i] == -1 and layer not in self.ownedLayers:
  224. reorderedLayers.append(layer)
  225. # owned layer found in cmd file is added into proper rendering
  226. # position
  227. else:
  228. reorderedLayers[layersOrder[i]] = layer
  229. self.SetLayers(reorderedLayers)
  230. except IOError as e:
  231. grass.warning(
  232. _("Unable to read cmdfile '%(cmd)s'. Details: %(det)s") %
  233. {'cmd': self.cmdfile, 'det': e})
  234. return
  235. Debug.msg(1, "Map.GetLayersFromCmdFile(): cmdfile=%s, nlayers=%d" %
  236. (self.cmdfile, nlayers))
  237. self._giface.updateMap.emit(render=False)
  238. def Render(self, *args, **kwargs):
  239. """Render layer to image.
  240. For input params and returned data see overridden method in Map class.
  241. """
  242. return Map.Render(self, *args, **kwargs)
  243. def AddLayer(self, *args, **kwargs):
  244. """Adds generic map layer to list of layers.
  245. For input params and returned data see overridden method in Map class.
  246. """
  247. driver = UserSettings.Get(group='display', key='driver', subkey='type')
  248. if driver == 'png':
  249. os.environ["GRASS_RENDER_IMMEDIATE"] = "png"
  250. else:
  251. os.environ["GRASS_RENDER_IMMEDIATE"] = "cairo"
  252. layer = Map.AddLayer(self, *args, **kwargs)
  253. del os.environ["GRASS_RENDER_IMMEDIATE"]
  254. return layer
  255. class Layer(object):
  256. """@implements core::giface::Layer"""
  257. def __init__(self, maplayer):
  258. self._maplayer = maplayer
  259. def __getattr__(self, name):
  260. if name == 'cmd':
  261. return cmdtuple_to_list(self._maplayer.GetCmd())
  262. elif hasattr(self._maplayer, name):
  263. return getattr(self._maplayer, name)
  264. elif name == 'maplayer':
  265. return self._maplayer
  266. elif name == 'type':
  267. return self._maplayer.GetType()
  268. # elif name == 'ctrl':
  269. elif name == 'label':
  270. return self._maplayer.GetName()
  271. # elif name == 'propwin':
  272. class LayerList(object):
  273. """@implements core::giface::LayerList"""
  274. def __init__(self, map, giface):
  275. self._map = map
  276. self._giface = giface
  277. self._index = 0
  278. def __len__(self):
  279. return len(self._map.GetListOfLayers())
  280. def __iter__(self):
  281. return self
  282. def __next__(self):
  283. items = self._map.GetListOfLayers()
  284. try:
  285. result = items[self._index]
  286. except IndexError:
  287. raise StopIteration
  288. self._index += 1
  289. return result
  290. def next(self):
  291. return self.__next__()
  292. def GetSelectedLayers(self, checkedOnly=True):
  293. # hidden and selected vs checked and selected
  294. items = self._map.GetListOfLayers()
  295. layers = []
  296. for item in items:
  297. layer = Layer(item)
  298. layers.append(layer)
  299. return layers
  300. def GetSelectedLayer(self, checkedOnly=False):
  301. """Returns selected layer or None when there is no selected layer."""
  302. layers = self.GetSelectedLayers()
  303. if len(layers) > 0:
  304. return layers[0]
  305. else:
  306. return None
  307. def AddLayer(self, ltype, name=None, checked=None,
  308. opacity=1.0, cmd=None):
  309. """Adds a new layer to the layer list.
  310. Launches property dialog if needed (raster, vector, etc.)
  311. :param ltype: layer type (raster, vector, raster_3d, ...)
  312. :param name: layer name
  313. :param checked: if True layer is checked
  314. :param opacity: layer opacity level
  315. :param cmd: command (given as a list)
  316. """
  317. self._map.AddLayer(ltype=ltype, command=cmd,
  318. name=name, active=True,
  319. opacity=opacity, render=True,
  320. pos=-1)
  321. # TODO: this should be solved by signal
  322. # (which should be introduced everywhere,
  323. # alternative is some observer list)
  324. self._giface.updateMap.emit(render=True, renderVector=True)
  325. def GetLayersByName(self, name):
  326. items = self._map.GetListOfLayers()
  327. layers = []
  328. for item in items:
  329. if item.GetName() == name:
  330. layer = Layer(item)
  331. layers.append(layer)
  332. return layers
  333. def GetLayerByData(self, key, value):
  334. # TODO: implementation was not tested
  335. items = self._map.GetListOfLayers()
  336. for item in items:
  337. layer = Layer(item)
  338. try:
  339. if getattr(layer, key) == value:
  340. return layer
  341. except AttributeError:
  342. pass
  343. return None
  344. class DMonGrassInterface(StandaloneGrassInterface):
  345. """@implements GrassInterface"""
  346. def __init__(self, mapframe):
  347. StandaloneGrassInterface.__init__(self)
  348. self._mapframe = mapframe
  349. def GetLayerList(self):
  350. return LayerList(self._mapframe.GetMap(), giface=self)
  351. def GetMapWindow(self):
  352. return self._mapframe.GetMapWindow()
  353. def GetMapDisplay(self):
  354. return self._mapframe
  355. def GetProgress(self):
  356. return self._mapframe.GetProgressBar()
  357. def ShowStatusbar(self, show=True):
  358. if not self._mapframe.statusbarManager:
  359. self._mapframe.CreateStatusbar()
  360. self._mapframe.statusbarManager.Show(show)
  361. def IsStatusbarShown(self):
  362. if not self._mapframe.statusbarManager:
  363. return False
  364. return self._mapframe.statusbarManager.IsShown()
  365. def ShowAllToolbars(self, show=True):
  366. if not show: # hide
  367. action = self._mapframe.RemoveToolbar
  368. else:
  369. action = self._mapframe.AddToolbar
  370. toolbars = list(self._mapframe.GetToolbarNames())
  371. if not toolbars:
  372. toolbars.append('map')
  373. for toolbar in toolbars:
  374. action(toolbar)
  375. def AreAllToolbarsShown(self):
  376. toolbar = self._mapframe.GetMapToolbar()
  377. if toolbar is None:
  378. return False
  379. return toolbar.IsShown()
  380. class DMonFrame(MapFrame):
  381. def OnZoomToMap(self, event):
  382. layers = self.MapWindow.GetMap().GetListOfLayers()
  383. self.MapWindow.ZoomToMap(layers=layers)
  384. def OnSize(self, event):
  385. super(DMonFrame, self).OnSize(event)
  386. # update env file
  387. width, height = self.MapWindow.GetClientSize()
  388. for line in fileinput.input(monFile['env'], inplace=True):
  389. if 'GRASS_RENDER_WIDTH' in line:
  390. print('GRASS_RENDER_WIDTH={0}'.format(width))
  391. elif 'GRASS_RENDER_HEIGHT' in line:
  392. print('GRASS_RENDER_HEIGHT={0}'.format(height))
  393. else:
  394. print(line.rstrip('\n'))
  395. class MapApp(wx.App):
  396. def OnInit(self):
  397. grass.set_raise_on_error(True)
  398. # actual use of StandaloneGrassInterface not yet tested
  399. # needed for adding functionality in future
  400. self._giface = DMonGrassInterface(None)
  401. return True
  402. def CreateMapFrame(self, name, decorations=True):
  403. toolbars = []
  404. if decorations:
  405. toolbars.append('map')
  406. if __name__ == "__main__":
  407. self.cmdTimeStamp = 0 # fake initial timestamp
  408. self.Map = DMonMap(giface=self._giface, cmdfile=monFile['cmd'],
  409. mapfile=monFile['map'])
  410. self.timer = wx.PyTimer(self.watcher)
  411. # check each 0.5s
  412. global mtime
  413. mtime = 500
  414. self.timer.Start(mtime)
  415. else:
  416. self.Map = None
  417. self.mapFrm = DMonFrame(
  418. parent=None,
  419. id=wx.ID_ANY,
  420. title=name,
  421. Map=self.Map,
  422. giface=self._giface,
  423. size=monSize,
  424. toolbars=toolbars,
  425. statusbar=decorations)
  426. # FIXME: hack to solve dependency
  427. self._giface._mapframe = self.mapFrm
  428. self.mapFrm.GetMapWindow().SetAlwaysRenderEnabled(False)
  429. # set default properties
  430. self.mapFrm.SetProperties(render=UserSettings.Get(
  431. group='display', key='autoRendering', subkey='enabled'),
  432. mode=UserSettings.Get(
  433. group='display', key='statusbarMode', subkey='selection'),
  434. alignExtent=UserSettings.Get(
  435. group='display', key='alignExtent', subkey='enabled'),
  436. constrainRes=UserSettings.Get(
  437. group='display', key='compResolution', subkey='enabled'),
  438. showCompExtent=UserSettings.Get(
  439. group='display', key='showCompExtent', subkey='enabled'))
  440. self.Map.saveToFile.connect(lambda cmd: self.mapFrm.DOutFile(cmd))
  441. self.Map.dToRast.connect(lambda cmd: self.mapFrm.DToRast(cmd))
  442. self.Map.query.connect(
  443. lambda ltype,
  444. maps: self.mapFrm.SetQueryLayersAndActivate(
  445. ltype=ltype,
  446. maps=maps))
  447. return self.mapFrm
  448. def OnExit(self):
  449. if __name__ == "__main__":
  450. # stop the timer
  451. if self.timer.IsRunning:
  452. self.timer.Stop()
  453. # terminate thread
  454. for f in six.itervalues(monFile):
  455. try_remove(f)
  456. return True
  457. def watcher(self):
  458. """Redraw, if new layer appears (check's timestamp of
  459. cmdfile)
  460. """
  461. ###
  462. # TODO: find a better solution
  463. ###
  464. # the check below disabled, it's too much invasive to call
  465. # g.gisenv in the watcher...
  466. # try:
  467. # GISBASE and other system environmental variables can not be used
  468. # since the process inherited them from GRASS
  469. # raises exception when vaiable does not exists
  470. # grass.gisenv()['GISDBASE']
  471. # except KeyError:
  472. # self.timer.Stop()
  473. # return
  474. # todo: events
  475. try:
  476. currentCmdFileTime = os.path.getmtime(monFile['cmd'])
  477. if currentCmdFileTime > self.cmdTimeStamp:
  478. self.timer.Stop()
  479. self.cmdTimeStamp = currentCmdFileTime
  480. self.mapFrm.GetMap().GetLayersFromCmdFile()
  481. self.timer.Start(mtime)
  482. except OSError as e:
  483. grass.warning("%s" % e)
  484. self.timer.Stop()
  485. def GetMapFrame(self):
  486. """Get Map Frame instance"""
  487. return self.mapFrm
  488. if __name__ == "__main__":
  489. if len(sys.argv) != 6:
  490. print(__doc__)
  491. sys.exit(0)
  492. # set command variable
  493. monName = sys.argv[1]
  494. monPath = sys.argv[2]
  495. monFile = {'map': os.path.join(monPath, 'map.ppm'),
  496. 'cmd': os.path.join(monPath, 'cmd'),
  497. 'env': os.path.join(monPath, 'env')}
  498. # monitor size
  499. monSize = (int(sys.argv[3]), int(sys.argv[4]))
  500. monDecor = not bool(int(sys.argv[5]))
  501. grass.verbose(_("Starting map display <%s>...") % (monName))
  502. # create pid file
  503. pidFile = os.path.join(monPath, "pid")
  504. fd = open(pidFile, 'w')
  505. if not fd:
  506. grass.fatal(_("Unable to create file <%s>") % pidFile)
  507. fd.write("%s\n" % os.getpid())
  508. fd.close()
  509. RunCommand('g.gisenv',
  510. set='MONITOR_%s_PID=%d' % (monName.upper(), os.getpid()))
  511. start = time.time()
  512. gmMap = MapApp(0)
  513. mapFrame = gmMap.CreateMapFrame(monName, monDecor)
  514. mapFrame.Show()
  515. Debug.msg(1, "WxMonitor started in %.6f sec" %
  516. (time.time() - start))
  517. gmMap.MainLoop()
  518. grass.verbose(_("Stopping map display <%s>...") % (monName))
  519. # clean up GRASS env variables
  520. try:
  521. shutil.rmtree(monPath)
  522. except OSError:
  523. pass
  524. RunCommand('g.gisenv',
  525. unset='MONITOR')
  526. sys.exit(0)