123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690 |
- """
- @package mapdisp.main
- @brief Start Map Display as standalone application
- Classes:
- - mapdisp::DMonMap
- - mapdisp::Layer
- - mapdisp::LayerList
- - mapdisp::StandaloneMapDisplayGrassInterface
- - mapdisp::DMonGrassInterface
- - mapdisp::DMonDisplay
- - mapdisp::MapApp
- Usage:
- python mapdisp/main.py monitor-identifier /path/to/map/file /path/to/command/file /path/to/env/file
- (C) 2006-2015 by the GRASS Development Team
- This program is free software under the GNU General Public License
- (>=v2). Read the file COPYING that comes with GRASS for details.
- @author Michael Barton
- @author Jachym Cepicky
- @author Martin Landa <landa.martin gmail.com>
- @author Vaclav Petras <wenzeslaus gmail.com> (MapPanelBase)
- @author Anna Kratochvilova <kratochanna gmail.com> (MapPanelBase)
- """
- from __future__ import print_function
- import os
- import sys
- import six
- import time
- import shutil
- import fileinput
- from grass.script.utils import try_remove
- from grass.script import core as grass
- from grass.script.task import cmdtuple_to_list, cmdlist_to_tuple
- from grass.pydispatch.signal import Signal
- from grass.script.setup import set_gui_path
- set_gui_path()
- # GUI imports require path to GUI code to be set.
- from core import globalvar # noqa: E402
- import wx # noqa: E402
- from core import utils # noqa: E402
- from core.giface import StandaloneGrassInterface # noqa: E402
- from core.gcmd import RunCommand # noqa: E402
- from core.render import Map, MapLayer, RenderMapMgr # noqa: E402
- from mapdisp.frame import MapPanel # noqa: E402
- from gui_core.mapdisp import FrameMixin # noqa: E402
- from core.debug import Debug # noqa: E402
- from core.settings import UserSettings # noqa: E402
- # for standalone app
- monFile = {
- "cmd": None,
- "map": None,
- "env": None,
- }
- monName = None
- monSize = list(globalvar.MAP_WINDOW_SIZE)
- monDecor = False
- class DMonMap(Map):
- def __init__(self, giface, cmdfile=None, mapfile=None):
- """Map composition (stack of map layers and overlays)
- :param cmdline: full path to the cmd file (defined by d.mon)
- :param mapfile: full path to the map file (defined by d.mon)
- """
- Map.__init__(self)
- self._giface = giface
- # environment settings
- self.env = dict()
- self.cmdfile = cmdfile
- # list of layers for rendering added from cmd file
- # TODO temporary solution, layer management by different tools in GRASS
- # should be resolved
- self.ownedLayers = []
- self.oldOverlays = []
- if mapfile:
- self.mapfileCmd = mapfile
- self.maskfileCmd = os.path.splitext(mapfile)[0] + ".pgm"
- # generated file for g.pnmcomp output for rendering the map
- self.mapfile = monFile["map"]
- if os.path.splitext(self.mapfile)[1] != ".ppm":
- self.mapfile += ".ppm"
- # signal sent when d.out.file/d.to.rast appears in cmd file, attribute
- # is cmd
- self.saveToFile = Signal("DMonMap.saveToFile")
- self.dToRast = Signal("DMonMap.dToRast")
- # signal sent when d.what.rast/vect appears in cmd file, attribute is
- # cmd
- self.query = Signal("DMonMap.query")
- self.renderMgr = RenderMapMgr(self)
- # update legend file variable with the one d.mon uses
- with open(monFile["env"], "r") as f:
- lines = f.readlines()
- for line in lines:
- if "GRASS_LEGEND_FILE" in line:
- legfile = line.split("=", 1)[1].strip()
- self.renderMgr.UpdateRenderEnv({"GRASS_LEGEND_FILE": legfile})
- break
- def GetLayersFromCmdFile(self):
- """Get list of map layers from cmdfile"""
- if not self.cmdfile:
- return
- nlayers = 0
- try:
- fd = open(self.cmdfile, "r")
- lines = fd.readlines()
- fd.close()
- # detect d.out.file, delete the line from the cmd file and export
- # graphics
- if len(lines) > 0:
- if lines[-1].startswith("d.out.file") or lines[-1].startswith(
- "d.to.rast"
- ):
- dCmd = lines[-1].strip()
- fd = open(self.cmdfile, "w")
- fd.writelines(lines[:-1])
- fd.close()
- if lines[-1].startswith("d.out.file"):
- self.saveToFile.emit(cmd=utils.split(dCmd))
- else:
- self.dToRast.emit(cmd=utils.split(dCmd))
- return
- if lines[-1].startswith("d.what"):
- dWhatCmd = lines[-1].strip()
- fd = open(self.cmdfile, "w")
- fd.writelines(lines[:-1])
- fd.close()
- if "=" in utils.split(dWhatCmd)[1]:
- maps = utils.split(dWhatCmd)[1].split("=")[1].split(",")
- else:
- maps = utils.split(dWhatCmd)[1].split(",")
- self.query.emit(
- ltype=utils.split(dWhatCmd)[0].split(".")[-1], maps=maps
- )
- return
- else:
- # clean overlays after erase
- self.oldOverlays = []
- overlays = list(self._giface.GetMapDisplay().decorations.keys())
- for each in overlays:
- self._giface.GetMapDisplay().RemoveOverlay(each)
- existingLayers = self.GetListOfLayers()
- # holds new rendreing order for every layer in existingLayers
- layersOrder = [-1] * len(existingLayers)
- # next number in rendering order
- next_layer = 0
- mapFile = None
- render_env = dict()
- for line in lines:
- if line.startswith("#"):
- if "GRASS_RENDER_FILE" in line:
- mapFile = line.split("=", 1)[1].strip()
- try:
- k, v = line[2:].strip().split("=", 1)
- except:
- pass
- render_env[k] = v
- continue
- cmd = utils.split(line.strip())
- ltype = None
- try:
- ltype = utils.command2ltype[cmd[0]]
- except KeyError:
- grass.warning(_("Unsupported command %s.") % cmd[0])
- continue
- name = utils.GetLayerNameFromCmd(
- cmd, fullyQualified=True, layerType=ltype
- )[0]
- args = {}
- if ltype in ("barscale", "rastleg", "northarrow", "text", "vectleg"):
- # TODO: this is still not optimal
- # it is there to prevent adding the same overlay multiple times
- if cmd in self.oldOverlays:
- continue
- if ltype == "rastleg":
- self._giface.GetMapDisplay().AddLegendRast(cmd=cmd)
- elif ltype == "barscale":
- self._giface.GetMapDisplay().AddBarscale(cmd=cmd)
- elif ltype == "northarrow":
- self._giface.GetMapDisplay().AddArrow(cmd=cmd)
- elif ltype == "text":
- self._giface.GetMapDisplay().AddDtext(cmd=cmd)
- elif ltype == "vectleg":
- self._giface.GetMapDisplay().AddLegendVect(cmd=cmd)
- self.oldOverlays.append(cmd)
- continue
- classLayer = MapLayer
- args["ltype"] = ltype
- exists = False
- for i, layer in enumerate(existingLayers):
- if layer.GetCmd(string=True) == utils.GetCmdString(
- cmdlist_to_tuple(cmd)
- ):
- exists = True
- if layersOrder[i] == -1:
- layersOrder[i] = next_layer
- next_layer += 1
- # layer must be put higher in render order (same cmd was insered more times)
- # TODO delete rendurant cmds from cmd file?
- else:
- for j, l_order in enumerate(layersOrder):
- if l_order > layersOrder[i]:
- layersOrder[j] -= 1
- layersOrder[i] = next_layer - 1
- break
- if exists:
- continue
- mapLayer = classLayer(
- name=name,
- cmd=cmd,
- Map=None,
- hidden=True,
- render=False,
- mapfile=mapFile,
- **args,
- )
- mapLayer.GetRenderMgr().updateProgress.connect(
- self.GetRenderMgr().ReportProgress
- )
- if render_env:
- mapLayer.GetRenderMgr().UpdateRenderEnv(render_env)
- render_env = dict()
- newLayer = self._addLayer(mapLayer)
- existingLayers.append(newLayer)
- self.ownedLayers.append(newLayer)
- layersOrder.append(next_layer)
- next_layer += 1
- nlayers += 1
- reorderedLayers = [-1] * next_layer
- for i, layer in enumerate(existingLayers):
- # owned layer was not found in cmd file -> is deleted
- if layersOrder[i] == -1 and layer in self.ownedLayers:
- self.ownedLayers.remove(layer)
- self.DeleteLayer(layer)
- # other layer e. g. added by wx.vnet are added to the top
- elif layersOrder[i] == -1 and layer not in self.ownedLayers:
- reorderedLayers.append(layer)
- # owned layer found in cmd file is added into proper rendering
- # position
- else:
- reorderedLayers[layersOrder[i]] = layer
- self.SetLayers(reorderedLayers)
- except IOError as e:
- grass.warning(
- _("Unable to read cmdfile '%(cmd)s'. Details: %(det)s")
- % {"cmd": self.cmdfile, "det": e}
- )
- return
- Debug.msg(
- 1,
- "Map.GetLayersFromCmdFile(): cmdfile=%s, nlayers=%d"
- % (self.cmdfile, nlayers),
- )
- self._giface.updateMap.emit(render=False)
- def Render(self, *args, **kwargs):
- """Render layer to image.
- For input params and returned data see overridden method in Map class.
- """
- return Map.Render(self, *args, **kwargs)
- def AddLayer(self, *args, **kwargs):
- """Adds generic map layer to list of layers.
- For input params and returned data see overridden method in Map class.
- """
- driver = UserSettings.Get(group="display", key="driver", subkey="type")
- if driver == "png":
- os.environ["GRASS_RENDER_IMMEDIATE"] = "png"
- else:
- os.environ["GRASS_RENDER_IMMEDIATE"] = "cairo"
- layer = Map.AddLayer(self, *args, **kwargs)
- del os.environ["GRASS_RENDER_IMMEDIATE"]
- return layer
- class Layer(object):
- """@implements core::giface::Layer"""
- def __init__(self, maplayer):
- self._maplayer = maplayer
- def __getattr__(self, name):
- if name == "cmd":
- return cmdtuple_to_list(self._maplayer.GetCmd())
- elif hasattr(self._maplayer, name):
- return getattr(self._maplayer, name)
- elif name == "maplayer":
- return self._maplayer
- elif name == "type":
- return self._maplayer.GetType()
- # elif name == 'ctrl':
- elif name == "label":
- return self._maplayer.GetName()
- # elif name == 'propwin':
- class LayerList(object):
- """@implements core::giface::LayerList"""
- def __init__(self, map, giface):
- self._map = map
- self._giface = giface
- self._index = 0
- def __len__(self):
- return len(self._map.GetListOfLayers())
- def __iter__(self):
- return self
- def __next__(self):
- items = self._map.GetListOfLayers()
- try:
- result = items[self._index]
- except IndexError:
- raise StopIteration
- self._index += 1
- return result
- def next(self):
- return self.__next__()
- def GetSelectedLayers(self, checkedOnly=True):
- # hidden and selected vs checked and selected
- items = self._map.GetListOfLayers()
- layers = []
- for item in items:
- layer = Layer(item)
- layers.append(layer)
- return layers
- def GetSelectedLayer(self, checkedOnly=False):
- """Returns selected layer or None when there is no selected layer."""
- layers = self.GetSelectedLayers()
- if len(layers) > 0:
- return layers[0]
- else:
- return None
- def AddLayer(self, ltype, name=None, checked=None, opacity=1.0, cmd=None):
- """Adds a new layer to the layer list.
- Launches property dialog if needed (raster, vector, etc.)
- :param ltype: layer type (raster, vector, raster_3d, ...)
- :param name: layer name
- :param checked: if True layer is checked
- :param opacity: layer opacity level
- :param cmd: command (given as a list)
- """
- self._map.AddLayer(
- ltype=ltype,
- command=cmd,
- name=name,
- active=True,
- opacity=opacity,
- render=True,
- pos=-1,
- )
- # TODO: this should be solved by signal
- # (which should be introduced everywhere,
- # alternative is some observer list)
- self._giface.updateMap.emit(render=True, renderVector=True)
- def GetLayersByName(self, name):
- items = self._map.GetListOfLayers()
- layers = []
- for item in items:
- if item.GetName() == name:
- layer = Layer(item)
- layers.append(layer)
- return layers
- def GetLayerByData(self, key, value):
- # TODO: implementation was not tested
- items = self._map.GetListOfLayers()
- for item in items:
- layer = Layer(item)
- try:
- if getattr(layer, key) == value:
- return layer
- except AttributeError:
- pass
- return None
- class StandaloneMapDisplayGrassInterface(StandaloneGrassInterface):
- """@implements GrassInterface"""
- def __init__(self, mapframe):
- StandaloneGrassInterface.__init__(self)
- self._mapframe = mapframe
- def GetLayerList(self):
- return LayerList(self._mapframe.GetMap(), giface=self)
- def GetMapWindow(self):
- return self._mapframe.GetMapWindow()
- def GetMapDisplay(self):
- return self._mapframe
- def GetProgress(self):
- return self._mapframe.GetProgressBar()
- class DMonGrassInterface(StandaloneMapDisplayGrassInterface):
- """@implements GrassInterface"""
- def __init__(self, mapframe):
- StandaloneMapDisplayGrassInterface.__init__(self, mapframe)
- class DMonDisplay(FrameMixin, MapPanel):
- """Map display for wrapping map panel with d.mon mathods and frame methods"""
- def __init__(self, parent, giface, id, Map, title, toolbars, statusbar):
- # init map panel
- MapPanel.__init__(
- self,
- parent=parent,
- id=id,
- title=title,
- Map=Map,
- giface=giface,
- toolbars=toolbars,
- statusbar=statusbar,
- )
- # set system icon
- parent.iconsize = (16, 16)
- parent.SetIcon(
- wx.Icon(
- os.path.join(globalvar.ICONDIR, "grass_map.ico"), wx.BITMAP_TYPE_ICO
- )
- )
- # bindings
- parent.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
- # extend shortcuts and create frame accelerator table
- self.shortcuts_table.append((self.OnFullScreen, wx.ACCEL_NORMAL, wx.WXK_F11))
- self._initShortcuts()
- # update env file
- width, height = self.MapWindow.GetClientSize()
- for line in fileinput.input(monFile["env"], inplace=True):
- if "GRASS_RENDER_WIDTH" in line:
- print("GRASS_RENDER_WIDTH={0}".format(width))
- elif "GRASS_RENDER_HEIGHT" in line:
- print("GRASS_RENDER_HEIGHT={0}".format(height))
- else:
- print(line.rstrip("\n"))
- # add Map Display panel to Map Display frame
- sizer = wx.BoxSizer(wx.VERTICAL)
- sizer.Add(self, proportion=1, flag=wx.EXPAND)
- parent.SetSizer(sizer)
- parent.Layout()
- def OnZoomToMap(self, event):
- layers = self.MapWindow.GetMap().GetListOfLayers()
- self.MapWindow.ZoomToMap(layers=layers)
- class MapApp(wx.App):
- def OnInit(self):
- grass.set_raise_on_error(True)
- # actual use of StandaloneGrassInterface not yet tested
- # needed for adding functionality in future
- self._giface = DMonGrassInterface(None)
- return True
- def CreateMapDisplay(self, name, decorations=True):
- toolbars = []
- if decorations:
- toolbars.append("map")
- if __name__ == "__main__":
- self.cmdTimeStamp = 0 # fake initial timestamp
- self.Map = DMonMap(
- giface=self._giface, cmdfile=monFile["cmd"], mapfile=monFile["map"]
- )
- self.timer = wx.PyTimer(self.watcher)
- # check each 0.5s
- global mtime
- mtime = 500
- self.timer.Start(mtime)
- else:
- self.Map = None
- mapframe = wx.Frame(
- None, id=wx.ID_ANY, size=monSize, style=wx.DEFAULT_FRAME_STYLE, title=name
- )
- self.mapDisplay = DMonDisplay(
- parent=mapframe,
- id=wx.ID_ANY,
- title=name,
- Map=self.Map,
- giface=self._giface,
- toolbars=toolbars,
- statusbar=decorations,
- )
- # FIXME: hack to solve dependency
- self._giface._mapframe = self.mapDisplay
- self.mapDisplay.GetMapWindow().SetAlwaysRenderEnabled(False)
- # set default properties
- self.mapDisplay.SetProperties(
- render=UserSettings.Get(
- group="display", key="autoRendering", subkey="enabled"
- ),
- mode=UserSettings.Get(
- group="display", key="statusbarMode", subkey="selection"
- ),
- alignExtent=UserSettings.Get(
- group="display", key="alignExtent", subkey="enabled"
- ),
- constrainRes=UserSettings.Get(
- group="display", key="compResolution", subkey="enabled"
- ),
- showCompExtent=UserSettings.Get(
- group="display", key="showCompExtent", subkey="enabled"
- ),
- )
- self.Map.saveToFile.connect(lambda cmd: self.mapDisplay.DOutFile(cmd))
- self.Map.dToRast.connect(lambda cmd: self.mapDisplay.DToRast(cmd))
- self.Map.query.connect(
- lambda ltype, maps: self.mapDisplay.SetQueryLayersAndActivate(
- ltype=ltype, maps=maps
- )
- )
- return self.mapDisplay
- def OnExit(self):
- if __name__ == "__main__":
- # stop the timer
- if self.timer.IsRunning:
- self.timer.Stop()
- # terminate thread
- for f in six.itervalues(monFile):
- try_remove(f)
- return True
- def watcher(self):
- """Redraw, if new layer appears (check's timestamp of
- cmdfile)
- """
- ###
- # TODO: find a better solution
- ###
- # the check below disabled, it's too much invasive to call
- # g.gisenv in the watcher...
- # try:
- # GISBASE and other system environmental variables can not be used
- # since the process inherited them from GRASS
- # raises exception when vaiable does not exists
- # grass.gisenv()['GISDBASE']
- # except KeyError:
- # self.timer.Stop()
- # return
- # todo: events
- try:
- currentCmdFileTime = os.path.getmtime(monFile["cmd"])
- if currentCmdFileTime > self.cmdTimeStamp:
- self.timer.Stop()
- self.cmdTimeStamp = currentCmdFileTime
- self.mapDisplay.GetMap().GetLayersFromCmdFile()
- self.timer.Start(mtime)
- except OSError as e:
- grass.warning("%s" % e)
- self.timer.Stop()
- def GetMapDisplay(self):
- """Get Map Display instance"""
- return self.mapDisplay
- if __name__ == "__main__":
- if len(sys.argv) != 6:
- print(__doc__)
- sys.exit(0)
- # set command variable
- monName = sys.argv[1]
- monPath = sys.argv[2]
- monFile = {
- "map": os.path.join(monPath, "map.ppm"),
- "cmd": os.path.join(monPath, "cmd"),
- "env": os.path.join(monPath, "env"),
- }
- # monitor size
- monSize = (int(sys.argv[3]), int(sys.argv[4]))
- monDecor = not bool(int(sys.argv[5]))
- grass.verbose(_("Starting map display <%s>...") % (monName))
- # create pid file
- pidFile = os.path.join(monPath, "pid")
- fd = open(pidFile, "w")
- if not fd:
- grass.fatal(_("Unable to create file <%s>") % pidFile)
- fd.write("%s\n" % os.getpid())
- fd.close()
- RunCommand("g.gisenv", set="MONITOR_%s_PID=%d" % (monName.upper(), os.getpid()))
- start = time.time()
- gmMap = MapApp(0)
- mapDisplay = gmMap.CreateMapDisplay(monName, monDecor)
- mapDisplay.Show()
- Debug.msg(1, "WxMonitor started in %.6f sec" % (time.time() - start))
- gmMap.MainLoop()
- grass.verbose(_("Stopping map display <%s>...") % (monName))
- # clean up GRASS env variables
- try:
- shutil.rmtree(monPath)
- except OSError:
- pass
- RunCommand("g.gisenv", unset="MONITOR")
- sys.exit(0)
|