main.py 20 KB

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