[GRASS-SVN] r65158 - in grass/trunk/gui/wxpython: core mapdisp

svn_grass at osgeo.org svn_grass at osgeo.org
Mon Apr 27 12:06:29 PDT 2015


Author: martinl
Date: 2015-04-27 12:06:29 -0700 (Mon, 27 Apr 2015)
New Revision: 65158

Modified:
   grass/trunk/gui/wxpython/core/render.py
   grass/trunk/gui/wxpython/core/utils.py
   grass/trunk/gui/wxpython/mapdisp/main.py
Log:
wxGUI: modify standalone wx0 monitors to render map layers into
mapfile to void g.pnmcomp call (work in progress) see also #2286


Modified: grass/trunk/gui/wxpython/core/render.py
===================================================================
--- grass/trunk/gui/wxpython/core/render.py	2015-04-27 19:04:19 UTC (rev 65157)
+++ grass/trunk/gui/wxpython/core/render.py	2015-04-27 19:06:29 UTC (rev 65158)
@@ -17,7 +17,7 @@
  - render::Overlay
  - render::Map
 
-(C) 2006-2014 by the GRASS Development Team
+(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.
@@ -50,10 +50,6 @@
 from core.debug    import Debug
 from core.settings import UserSettings
 
-
-USE_GPNMCOMP = True
-
-
 class Layer(object):
     """Virtual class which stores information about layers (map layers and
     overlays) of the map composition.
@@ -62,7 +58,7 @@
     - For overlays use Overlay class.
     """
     def __init__(self, ltype, cmd, Map, name=None,
-                 active=True, hidden=False, opacity=1.0):
+                 active=True, hidden=False, opacity=1.0, mapfile=None):
         """Create new instance
 
         .. todo::
@@ -77,10 +73,11 @@
         :param active: layer is active, will be rendered only if True
         :param hidden: layer is hidden, won't be listed in Layer Manager if True
         :param float opacity: layer opacity <0;1>
+        :param mapfile full path to output file or None
         """
-
-        # generated file for each layer
-        if USE_GPNMCOMP or ltype == 'overlay':
+        if mapfile:
+            self.mapfile = mapfile
+        else:
             if ltype == 'overlay':
                 tempfile_sfx = ".png"
             else:
@@ -90,10 +87,8 @@
             # we don't want it open, we just need the name
             self.mapfile = mapfile.name
             mapfile.close()
-            self.maskfile = self.mapfile.rsplit(".",1)[0] + ".pgm"
-        else:
-            self.mapfile = self.maskfile = None
-
+        self.maskfile = self.mapfile.rsplit(".",1)[0] + ".pgm"
+        
         # stores class which manages rendering instead of simple command - e.g. WMS
         self.renderMgr = None
 
@@ -101,7 +96,6 @@
         self.type = None
         self.SetType(ltype)
         self.name = name
-        self.environ = os.environ.copy()
 
         if self.type == 'command':
             self.cmd = list()
@@ -116,6 +110,8 @@
 
         self.forceRender = True
 
+        self.render_env = { "GRASS_RENDER_FILE": self.mapfile }
+        
         Debug.msg (3, "Layer.__init__(): type=%s, cmd='%s', name=%s, " \
                        "active=%d, opacity=%d, hidden=%d" % \
                        (self.type, self.GetCmd(string=True), self.name,
@@ -125,7 +121,13 @@
         Debug.msg (3, "Layer.__del__(): layer=%s, cmd='%s'" %
                    (self.name, self.GetCmd(string = True)))
 
-    def Render(self):
+    def __str__(self):
+        return self.name
+
+    def __repr__(self):
+        return self.__str__()
+    
+    def Render(self, env):
         """Render layer to image
 
         :return: rendered image filename
@@ -138,8 +140,8 @@
         if self.type == '3d-raster':
             return None
 
-        Debug.msg (3, "Layer.Render(): type=%s, name=%s" % \
-                       (self.type, self.name))
+        Debug.msg (3, "Layer.Render(): type=%s, name=%s, file=%s" % \
+                       (self.type, self.name, self.mapfile))
 
         # prepare command for each layer
         layertypes = utils.command2ltype.values() + ['overlay', 'command']
@@ -148,23 +150,21 @@
             raise GException(_("<%(name)s>: layer type <%(type)s> is not supported") % \
                                  {'type' : self.type, 'name' : self.name})
 
-        if self.mapfile:
-            self.environ["GRASS_RENDER_FILE"] = self.mapfile
-
+        env.update(self.render_env)
         # execute command
         try:
             if self.type == 'command':
                 read = False
                 for c in self.cmd:
-                    ret, msg = self._runCommand(c)
+                    ret, msg = self._runCommand(c, env)
                     if ret != 0:
                         break
                     if not read:
-                        self.environ["GRASS_RENDER_FILE_READ"] = "TRUE"
+                        env["GRASS_RENDER_FILE_READ"] = "TRUE"
 
-                self.environ["GRASS_RENDER_FILE_READ"] = "FALSE"
+                env["GRASS_RENDER_FILE_READ"] = "FALSE"
             else:
-                ret, msg = self._runCommand(self.cmd)
+                ret, msg = self._runCommand(self.cmd, env)
             if ret != 0:
                 sys.stderr.write(_("Command '%s' failed\n") % self.GetCmd(string = True))
                 if msg:
@@ -183,18 +183,18 @@
 
         return self.mapfile
 
-    def _runCommand(self, cmd):
+    def _runCommand(self, cmd, env):
         """Run command to render data
         """
         if self.type == 'wms':
             ret = 0
             msg = ''
-            self.renderMgr.Render(cmd, env=self.environ)
+            self.renderMgr.Render(cmd, env=env)
         else:
             ret, msg = RunCommand(cmd[0],
                                   getErrorMsg = True,
                                   quiet = True,
-                                  env=self.environ,
+                                  env=env,
                                   **cmd[1])
 
         return ret, msg
@@ -318,10 +318,6 @@
         # for re-rendering
         self.forceRender = True
 
-    def SetEnvironment(self, environ):
-        """Sets environment for rendering."""
-        self.environ = environ
-
     def IsDownloading(self):
         """Is data downloading from web server e. g. wms"""
         if self.renderMgr is None:
@@ -341,23 +337,11 @@
         return self.renderMgr
 
 class MapLayer(Layer):
-    def __init__(self, ltype, cmd, Map, name = None,
-                 active = True, hidden = False, opacity = 1.0):
+    def __init__(self, *args, **kwargs):
         """Represents map layer in the map canvas
-
-        :param ltype: layer type ('raster', 'vector', 'command', etc.)
-        :param cmd: GRASS command to render layer,
-                    given as list, e.g. ['d.rast',
-                    'map=elevation at PERMANENT']
-        :param map: render.Map instance
-        :param name: layer name, e.g. 'elevation at PERMANENT' (for layer tree) or None
-        :param active: layer is active, will be rendered only if True
-        :param hidden: layer is hidden, won't be listed in Layer Manager if True
-        :param opacity: layer opacity <0;1>
         """
-        Layer.__init__(self, ltype, cmd, Map, name,
-                       active, hidden, opacity)
-
+        Layer.__init__(self, *args, **kwargs)
+        
     def GetMapset(self):
         """Get mapset of map layer
 
@@ -373,24 +357,16 @@
             return self.name
 
 class Overlay(Layer):
-    def __init__(self, id, ltype, cmd, Map,
-                 active = True, hidden = True, opacity = 1.0):
+    def __init__(self, id, *args, **kwargs):
         """Represents overlay displayed in map canvas
 
         :param id: overlay id (for PseudoDC)
-        :param type: overlay type ('barscale', 'legend', etc.)
-        :param cmd: GRASS command to render overlay,
-                    given as list, e.g. ['d.legend',
-                    'raster=elevation at PERMANENT']
-        :param map: render.Map instance
-        :param active: layer is active, will be rendered only if True
-        :param hidden: layer is hidden, won't be listed in Layer Manager if True
-        :param opacity: layer opacity <0;1>
         """
-        Layer.__init__(self, 'overlay', cmd, Map, ltype,
-                       active, hidden, opacity)
+        Layer.__init__(self, ltype='overlay', *args, **kwargs)
         self.id = id
-
+        self.render_env["GRASS_RENDER_FILE_READ"] = "FALSE"
+        self.render_env["GRASS_RENDER_TRANSPARENT"] = "TRUE"
+        
 class Map(object):
     def __init__(self, gisrc = None):
         """Map composition (stack of map layers and overlays)
@@ -415,7 +391,7 @@
 
         # generated file for g.pnmcomp output for rendering the map
         self.mapfile = grass.tempfile(create = False) + '.ppm'
-
+        
         # setting some initial env. variables
         if not self.GetWindow():
             sys.stderr.write(_("Trying to recover from default region..."))
@@ -425,10 +401,10 @@
         self.progressInfo = None
 
         # GRASS environment variable (for rendering)
-        self.default_env = {"GRASS_RENDER_BACKGROUNDCOLOR" : "000000",
+        self.render_env = {"GRASS_RENDER_BACKGROUNDCOLOR" : "000000",
                             "GRASS_RENDER_FILE_COMPRESSION" : "0",
                             "GRASS_RENDER_TRUECOLOR"       : "TRUE",
-                            "GRASS_RENDER_TRANSPARENT"     : "TRUE"
+                            "GRASS_RENDER_TRANSPARENT"     : "TRUE",
                             }
 
         # projection info
@@ -864,7 +840,7 @@
             else:
                 selected.append(layer)
 
-        Debug.msg (3, "Map.GetListOfLayers(): numberof=%d" % len(selected))
+        Debug.msg (3, "Map.GetListOfLayers(ltype=%s): -> %d" % (ltype, len(selected)))
 
         return selected
 
@@ -886,10 +862,9 @@
             layers = self.layers + self.overlays
 
         self.downloading = False
-
+        
         self.ReportProgress(layer=None)
 
-
         for layer in layers:
             # skip non-active map layers
             if not layer or not layer.active:
@@ -897,8 +872,7 @@
 
             # render
             if force or layer.forceRender:
-                layer.SetEnvironment(env)
-                if not layer.Render():
+                if not layer.Render(env):
                     continue
 
             if layer.IsDownloading():
@@ -915,9 +889,7 @@
                 maps.append(layer.mapfile)
                 masks.append(layer.maskfile)
                 opacities.append(str(layer.opacity))
-
-            Debug.msg(3, "Map.Render() type=%s, layer=%s " % (layer.type, layer.name))
-
+        
         return maps, masks, opacities
 
     def GetMapsMasksAndOpacities(self, force, windres, env):
@@ -942,7 +914,7 @@
         """
         wx.BeginBusyCursor()
         env = os.environ.copy()
-        env.update(self.default_env)
+        env.update(self.render_env)
         # use external gisrc if defined
         if self.gisrc:
             env['GISRC'] = self.gisrc
@@ -1004,6 +976,24 @@
 
         return self.mapfile
 
+    def _addLayer(self, layer, render=False, pos=-1):
+        if layer.type == 'overlay':
+            llist = self.overlays
+        else:
+            llist = self.layers
+        
+        # add maplayer to the list of layers
+        if pos > -1:
+            llist.insert(pos, layer)
+        else:
+            llist.append(layer)
+        
+        Debug.msg (3, "Map._addLayer(): layer=%s type=%s" % (layer.name, layer.type))
+        if render and not layer.Render():
+            raise GException(_("Unable to render map layer <%s>.") % name)
+        
+        return layer
+
     def AddLayer(self, ltype, command, name = None,
                  active = True, hidden = False, opacity = 1.0, render = False,
                  pos = -1):
@@ -1021,7 +1011,6 @@
         :return: new layer on success
         :return: None on failure
         """
-        wx.BeginBusyCursor()
         # opacity must be <0;1>
         if opacity < 0:
             opacity = 0
@@ -1029,27 +1018,16 @@
             opacity = 1
         layer = MapLayer(ltype = ltype, name = name, cmd = command, Map = self,
                          active = active, hidden = hidden, opacity = opacity)
-
-        # add maplayer to the list of layers
-        if pos > -1:
-            self.layers.insert(pos, layer)
-        else:
-            self.layers.append(layer)
-
-        Debug.msg (3, "Map.AddLayer(): layer=%s" % layer.name)
-        if render:
-            if not layer.Render():
-                raise GException(_("Unable to render map layer <%s>.") % name)
-
+        
+        self._addLayer(layer, render, pos)
+        
         renderMgr = layer.GetRenderMgr()
         if renderMgr:
             renderMgr.dataFetched.connect(self.layerChanged)
             renderMgr.updateProgress.connect(self.ReportProgress)
-
-        wx.EndBusyCursor()
-
+        
         self.layerAdded.emit(layer=layer)
-
+        
         return layer
 
     def DeleteAllLayers(self, overlay = False):
@@ -1095,15 +1073,8 @@
 
     def SetLayers(self, layers):
         self.layers = layers
+        Debug.msg(5, "Map.SetLayers(): layers=%s" % (layers))
 
-        # only for debug
-        # might be removed including message, it seems more than clear
-        layerNameList = ""
-        for layer in self.layers:
-            if layer.GetName():
-                layerNameList += layer.GetName() + ','
-        Debug.msg(5, "Map.SetLayers(): layers=%s" % (layerNameList))
-
     def ChangeLayer(self, layer, render = False, **kargs):
         """Change map layer properties
 
@@ -1242,18 +1213,11 @@
         :return: None on failure
         """
         Debug.msg (2, "Map.AddOverlay(): cmd=%s, render=%d" % (command, render))
-        overlay = Overlay(id = id, ltype = ltype, cmd = command, Map = self,
+        overlay = Overlay(id = id, name = ltype, cmd = command, Map = self,
                           active = active, hidden = hidden, opacity = opacity)
+        
+        return self._addLayer(overlay, render)
 
-        # add maplayer to the list of layers
-        self.overlays.append(overlay)
-
-        if render and command != '' and not overlay.Render():
-            raise GException(_("Unable to render overlay <%s>.") %
-                             ltype)
-
-        return self.overlays[-1]
-
     def ChangeOverlay(self, id, render = False, **kargs):
         """Change overlay properities
 

Modified: grass/trunk/gui/wxpython/core/utils.py
===================================================================
--- grass/trunk/gui/wxpython/core/utils.py	2015-04-27 19:04:19 UTC (rev 65157)
+++ grass/trunk/gui/wxpython/core/utils.py	2015-04-27 19:06:29 UTC (rev 65158)
@@ -971,7 +971,8 @@
                  'd.graph'        : 'graph',
                  'd.out.file'     : 'export',
                  'd.to.rast'      : 'torast',
-                 'd.text'         : 'text'
+                 'd.text'         : 'text',
+                 'd.northarrow'   : 'northarrow'
                  }
 ltype2command = {}
 for (cmd, ltype) in command2ltype.items():

Modified: grass/trunk/gui/wxpython/mapdisp/main.py
===================================================================
--- grass/trunk/gui/wxpython/mapdisp/main.py	2015-04-27 19:04:19 UTC (rev 65157)
+++ grass/trunk/gui/wxpython/mapdisp/main.py	2015-04-27 19:06:29 UTC (rev 65158)
@@ -5,6 +5,7 @@
 
 Classes:
  - mapdisp::DMonMap
+ - mapdisp::DMonMapDirect
  - mapdisp::Layer
  - mapdisp::LayerList
  - mapdisp::DMonGrassInterface
@@ -14,7 +15,7 @@
 Usage:
 python mapdisp/main.py monitor-identifier /path/to/map/file /path/to/command/file /path/to/env/file
 
-(C) 2006-2014 by the GRASS Development Team
+(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.
@@ -37,7 +38,7 @@
 from core          import utils
 from core.giface   import StandaloneGrassInterface
 from core.gcmd     import RunCommand
-from core.render   import Map, MapLayer
+from core.render   import Map, MapLayer, Overlay
 from core.utils import _
 from mapdisp.frame import MapFrame
 from core.debug    import Debug
@@ -64,7 +65,6 @@
         :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
@@ -93,21 +93,21 @@
         # signal sent when d.what.rast/vect appears in cmd file, attribute is cmd
         self.query = Signal('DMonMap.query')
 
-    def GetLayersFromCmdFile(self):
+    def GetLayersFromCmdFile(self, mapfile=None):
         """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'):
+                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])
@@ -128,38 +128,42 @@
                         maps = utils.split(dWhatCmd)[1].split(',')
                     self.query.emit(ltype=utils.split(dWhatCmd)[0].split('.')[-1], maps=maps)
                     return
-
+            
             existingLayers = self.GetListOfLayers()
 
             # holds new rendreing order for every layer in existingLayers
-            layersOrder = [-1] * len(self.GetListOfLayers())
+            layersOrder = [-1] * len(existingLayers)
 
             # next number in rendering order
             next_layer = 0
-
             for line in lines:
                 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]
 
-                # creating temporary layer object to compare commands
-                # neccessary to get the same format
-                # supposing that there are no side effects
-                tmpMapLayer = MapLayer(ltype = ltype, name = name,
-                                       cmd = cmd, Map = None,
-                                       active = False, hidden = True,
-                                       opacity = 0)
+                args = {}
+                if ltype in ('barscale', 'legend', 'northarrow'):
+                    classLayer = Overlay
+                    args['id'] = 1
+                else:
+                    classLayer = MapLayer
+                    args['mapfile'] = mapfile
+                    args['ltype'] = ltype
+                
+                mapLayer = classLayer(name = name, cmd = cmd, Map = None,
+                                      hidden = True, **args)
+                
                 exists = False
                 for i, layer in enumerate(existingLayers):
-                    if layer.GetCmd(string=True) == tmpMapLayer.GetCmd(string=True):
+                    if layer.GetCmd(string=True) == mapLayer.GetCmd(string=True):
                         exists = True
 
                         if layersOrder[i] == -1: 
@@ -176,12 +180,12 @@
                         break
                 if exists:
                     continue
-
-                newLayer = Map.AddLayer(self, ltype = ltype, command = cmd, active = True, name = name)
                 
+                newLayer = self._addLayer(mapLayer)
+                
                 existingLayers.append(newLayer)
                 self.ownedLayers.append(newLayer)
-
+                
                 layersOrder.append(next_layer)
                 next_layer += 1
 
@@ -202,27 +206,25 @@
                 # 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()
-
-        Debug.msg(1, "Map.GetLayersFromCmdFile(): cmdfile=%s" % self.cmdfile)
-        Debug.msg(1, "                            nlayers=%d" % nlayers)
-                
+        
     def Render(self, *args, **kwargs):
         """Render layer to image.
 
         For input params and returned data see overridden method in Map class.
         """
-        ret = Map.Render(self, *args, **kwargs)
-
-        return ret
+        return Map.Render(self, *args, **kwargs)
     
     def AddLayer(self, *args, **kwargs):
         """Adds generic map layer to list of layers.
@@ -236,13 +238,57 @@
         else:
             os.environ["GRASS_RENDER_IMMEDIATE"] = "cairo"
 
-        layer = Map.AddLayer(self, *args, **kwargs)
-
+        layer = Map.AddLayer(self, render = False, *args, **kwargs)
+        llayer.SetMapFile(self.mapfile)
+        
         del os.environ["GRASS_RENDER_IMMEDIATE"]
 
-        return layer
+        #return layer
 
+class DMonMapDirect(DMonMap):
+    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)
+        """
+        DMonMap.__init__(self, giface, cmdfile, mapfile)
+        
+        self.render_env['GRASS_RENDER_BACKGROUNDCOLOR'] = 'FFFFFF'
+        self.render_env['GRASS_RENDER_TRANSPARENT'] = 'FALSE'
+        self.render_env['GRASS_RENDER_FILE_READ'] = 'TRUE'
+        
+    def Render(self, *args, **kwargs):
+        """Render layer to image.
+
+        For input params and returned data see overridden method in Map class.
+        """
+        wx.BeginBusyCursor()
+        env = os.environ.copy()
+        env.update(self.render_env)
+        # use external gisrc if defined
+        if self.gisrc:
+            env['GISRC'] = self.gisrc
+        env['GRASS_REGION'] = self.SetRegion(kwargs.get('windres', False))
+        env['GRASS_RENDER_WIDTH'] = str(self.width)
+        env['GRASS_RENDER_HEIGHT'] = str(self.height)
+        driver = UserSettings.Get(group = 'display', key = 'driver', subkey = 'type')
+        if driver == 'png':
+            env['GRASS_RENDER_IMMEDIATE'] = 'png'
+        else:
+            env['GRASS_RENDER_IMMEDIATE'] = 'cairo'
+
+        if os.path.exists(self.mapfile):
+            os.remove(self.mapfile)
+        
+        self.GetMapsMasksAndOpacities(kwargs['force'], kwargs.get('windres', False), env)
+        wx.EndBusyCursor()
+
+        return self.mapfile
+
+    def GetLayersFromCmdFile(self):
+        super(self.__class__, self).GetLayersFromCmdFile(self.mapfile)
+        
 class Layer(object):
     """@implements core::giface::Layer"""
     def __init__(self, maplayer):
@@ -264,8 +310,8 @@
 
 
 class LayerList(object):
+    """@implements core::giface::LayerList"""
     def __init__(self, map, giface):
-        """@implements core::giface::LayerList"""
         self._map = map
         self._giface = giface
 
@@ -389,10 +435,20 @@
         # 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__":
+            dmonMap = DMonMapDirect
+            # if decorations:
+            #    dmonMap = DMonMap
             self.cmdTimeStamp = os.path.getmtime(monFile['cmd'])
-            self.Map = DMonMap(giface=self._giface, cmdfile=monFile['cmd'],
+            self.Map = dmonMap(giface=self._giface, cmdfile=monFile['cmd'],
                                mapfile = monFile['map'])
             
             self.timer = wx.PyTimer(self.watcher)
@@ -403,19 +459,9 @@
         else:
             self.Map = None
         
-        return True
-    
-    def CreateMapFrame(self, name, decorations=True):
-        if decorations:
-            toolbars = ['map']
-            statusbar = True
-        else:
-            toolbars = []
-            statusbar = False
-        
         self.mapFrm = DMonFrame(parent=None, id=wx.ID_ANY, title=name, Map=self.Map,
                                 giface=self._giface, size=monSize,
-                                toolbars=toolbars, statusbar=statusbar)
+                                toolbars=toolbars, statusbar=decorations)
         
         # FIXME: hack to solve dependency
         self._giface._mapframe = self.mapFrm



More information about the grass-commit mailing list