""" @package mapdisp.main @brief Start Map Display as standalone application Classes: - mapdisp::DMonMap - mapdisp::Layer - mapdisp::LayerList - mapdisp::DMonGrassInterface - mapdisp::DMonFrame - 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 @author Vaclav Petras (MapFrameBase) @author Anna Kratochvilova (MapFrameBase) """ 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 MapFrame # 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 DMonGrassInterface(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() def ShowStatusbar(self, show=True): if not self._mapframe.statusbarManager: self._mapframe.CreateStatusbar() self._mapframe.statusbarManager.Show(show) def IsStatusbarShown(self): if not self._mapframe.statusbarManager: return False return self._mapframe.statusbarManager.IsShown() def ShowAllToolbars(self, show=True): if not show: # hide action = self._mapframe.RemoveToolbar else: action = self._mapframe.AddToolbar toolbars = list(self._mapframe.GetToolbarNames()) if not toolbars: toolbars.append('map') for toolbar in toolbars: action(toolbar) def AreAllToolbarsShown(self): toolbar = self._mapframe.GetMapToolbar() if toolbar is None: return False return toolbar.IsShown() class DMonFrame(MapFrame): def OnZoomToMap(self, event): layers = self.MapWindow.GetMap().GetListOfLayers() self.MapWindow.ZoomToMap(layers=layers) def OnSize(self, event): super(DMonFrame, self).OnSize(event) # 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')) 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 CreateMapFrame(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 self.mapFrm = DMonFrame( parent=None, id=wx.ID_ANY, title=name, Map=self.Map, giface=self._giface, size=monSize, toolbars=toolbars, statusbar=decorations) # FIXME: hack to solve dependency self._giface._mapframe = self.mapFrm self.mapFrm.GetMapWindow().SetAlwaysRenderEnabled(False) # set default properties self.mapFrm.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.mapFrm.DOutFile(cmd)) self.Map.dToRast.connect(lambda cmd: self.mapFrm.DToRast(cmd)) self.Map.query.connect( lambda ltype, maps: self.mapFrm.SetQueryLayersAndActivate( ltype=ltype, maps=maps)) return self.mapFrm 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.mapFrm.GetMap().GetLayersFromCmdFile() self.timer.Start(mtime) except OSError as e: grass.warning("%s" % e) self.timer.Stop() def GetMapFrame(self): """Get Map Frame instance""" return self.mapFrm 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) mapFrame = gmMap.CreateMapFrame(monName, monDecor) mapFrame.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)