main.py 22 KB

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