Kaynağa Gözat

Merge branch 'master' of https://github.com/OSGeo/grass

Markus Metz 5 yıl önce
ebeveyn
işleme
2cb165dc8b

+ 4 - 0
REQUIREMENTS.html

@@ -185,6 +185,10 @@ newer versions are named "python-pillow"
 <a href="https://pypi.python.org/pypi/termcolor">https://pypi.python.org/pypi/termcolor</a>
 </li>
 
+<li><b>six</b> (needed for cross-version Python compatibility)<br>
+<a href="https://pypi.python.org/pypi/six">https://pypi.python.org/pypi/six</a>
+</li>
+
 <li><b>FFMPEG or alternative</b> (for wxGUI Animation tool - g.gui.module),
     specifically ffmpeg tool<br>
 <a href="http://ffmpeg.org">http://ffmpeg.org</a>

+ 1 - 0
Vagrantfile

@@ -71,6 +71,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
       "python-numpy",
       "python-ply",
       "python-pil",
+      "python-six",
       "libnetcdf-dev",
       "netcdf-bin",
       "libblas-dev",

+ 31 - 2
display/d.barscale/main.c

@@ -45,7 +45,7 @@ int main(int argc, char **argv)
     struct GModule *module;
     struct Option *bg_color_opt, *fg_color_opt, *coords, *fsize, *barstyle,
             *text_placement, *length_opt, *segm_opt, *units_opt, *label_opt,
-            *width_scale_opt;
+            *width_scale_opt, *font, *path, *charset;
     struct Flag *feet, *no_text, *n_symbol;
     struct Cell_head W;
     double east, north;
@@ -173,6 +173,13 @@ int main(int argc, char **argv)
     width_scale_opt->options = "0.5-100";
     width_scale_opt->description = _("Scale factor to change bar width");
 
+    font = G_define_option();
+    font->key = "font";
+    font->type = TYPE_STRING;
+    font->required = NO;
+    font->description = _("Font name");
+    font->guisection = _("Text");
+
     fsize = G_define_option();
     fsize->key = "fontsize";
     fsize->type = TYPE_DOUBLE;
@@ -182,7 +189,22 @@ int main(int argc, char **argv)
     fsize->description = _("Font size");
     fsize->guisection = _("Text");
 
-     G_option_exclusive(feet, units_opt, NULL);
+    path = G_define_standard_option(G_OPT_F_INPUT);
+    path->key = "path";
+    path->required = NO;
+    path->description = _("Path to font file");
+    path->gisprompt = "old,font,file";
+    path->guisection = _("Font settings");
+
+    charset = G_define_option();
+    charset->key = "charset";
+    charset->type = TYPE_STRING;
+    charset->required = NO;
+    charset->description =
+	_("Text encoding (only applicable to TrueType fonts)");
+    charset->guisection = _("Text");
+
+    G_option_exclusive(feet, units_opt, NULL);
 
     if (G_parser(argc, argv))
         exit(EXIT_FAILURE);
@@ -317,6 +339,13 @@ int main(int argc, char **argv)
 
     D_open_driver();
 
+    if (font->answer)
+	D_font(font->answer);
+    else if (path->answer)
+	D_font(path->answer);
+    if (charset->answer)
+	D_encoding(charset->answer);
+
     D_setup(0);
 
     draw_scale(east, north, length, segm, units, label, bar_style, text_position, width_scale, fontsize);

+ 10 - 7
docker/Dockerfile_alpine

@@ -76,7 +76,6 @@ ENV PACKAGES="\
       py3-pillow \
       py3-six \
       postgresql \
-      proj4 \
       sqlite \
       sqlite-libs \
       tiff \
@@ -102,7 +101,6 @@ ENV PACKAGES="\
       openjpeg-dev \
       openblas-dev \
       postgresql-dev \
-      proj4-dev \
       sqlite-dev \
       tar \
       tiff-dev \
@@ -146,7 +144,7 @@ RUN echo "Install main packages";\
     #
     # install the latest projection library for GRASS GIS
     #
-    echo "Install PROJ4";\
+    echo "Install PROJ-$PROJ_VERSION";\
     echo "  => Dowload proj-$PROJ_VERSION";\
     wget http://download.osgeo.org/proj/proj-$PROJ_VERSION.tar.gz && \
     tar xzvf proj-$PROJ_VERSION.tar.gz && \
@@ -168,11 +166,16 @@ RUN echo "Install main packages";\
     #
     echo "Install GRASS GIS";\
     echo "  => Dowload grass-$GRASS_VERSION";\
-    wget https://grass.osgeo.org/grass`echo $GRASS_VERSION | tr -d .`/source/snapshot/grass-$GRASS_VERSION.svn_src_snapshot_latest.tar.gz && \
+    wget https://grass.osgeo.org/grass`echo $GRASS_VERSION | tr -d .`/source/snapshot/grass-$GRASS_VERSION.git_src_snapshot_latest.tar.gz && \
     # unpack source code package and remove tarball archive:
     mkdir /src/grass_build && \
-    tar xfz grass-$GRASS_VERSION.svn_src_snapshot_latest.tar.gz --strip=1 -C /src/grass_build && \
-    rm -f grass-$GRASS_VERSION.svn_src_snapshot_latest.tar.gz; \
+    tar xfz grass-$GRASS_VERSION.git_src_snapshot_latest.tar.gz --strip=1 -C /src/grass_build && \
+    rm -f grass-$GRASS_VERSION.git_src_snapshot_latest.tar.gz; \
+    #
+    # Fixup python shebangs - TODO: will be resolved in future by grass-core
+    cd /src/grass_build && \
+    find -name '*.py' | xargs sed -i 's,#!/usr/bin/env python,#!/usr/bin/env python3,' && \
+    sed -i 's,python,python3,' include/Make/Platform.make.in && \
     #
     # Configure compile and install GRASS GIS
     #
@@ -239,6 +242,6 @@ ENV GRASS_SKIP_MAPSET_OWNER_CHECK=1 \
     LC_ALL="en_US.UTF-8"
 
 # debug
-RUN $GRASSBIN --config revision version
+RUN $GRASSBIN --config svn_revision version
 
 CMD [$GRASSBIN, "--version"]

+ 259 - 0
docker/Dockerfile_alpine_wxgui

@@ -0,0 +1,259 @@
+FROM alpine:edge
+
+# Based on:
+# https://github.com/mundialis/docker-grass-gis/blob/master/Dockerfile
+LABEL authors="Markus Neteler"
+LABEL maintainer="neteler@osgeo.org"
+
+# PACKAGES VERSIONS
+ARG GRASS_VERSION=7.7
+ARG PYTHON_VERSION=3
+ARG PROJ_VERSION=5.2.0
+ARG PROJ_DATUMGRID_VERSION=1.8
+
+# ================
+# CONFIG VARIABLES
+# ================
+
+# set configuration options, with wxGUI
+ENV GRASS_CONFIG="\
+      --enable-largefile \
+      --with-cxx \
+      --with-proj --with-proj-share=/usr/share/proj \
+      --with-gdal \
+      --with-python \
+      --with-geos \
+      --with-sqlite \
+      --with-bzlib \
+      --with-zstd \
+      --with-cairo --with-cairo-ldflags=-lfontconfig \
+      --with-fftw \
+      --with-wxwidgets \
+      --with-postgres --with-postgres-includes='/usr/include/postgresql' \
+      --without-freetype \
+      --without-openmp \
+      --without-opengl \
+      --without-nls \
+      --without-mysql \
+      --without-odbc \
+      --without-openmp \
+      --without-ffmpeg \
+      "
+
+# Set environmental variables for GRASS GIS compilation, without debug symbols
+ENV MYCFLAGS="-O2 -std=gnu99 -m64" \
+    MYLDFLAGS="-s -Wl,--no-undefined -lblas" \
+    # CXX stuff:
+    LD_LIBRARY_PATH="/usr/local/lib" \
+    LDFLAGS="$MYLDFLAGS" \
+    CFLAGS="$MYCFLAGS" \
+    CXXFLAGS="$MYCXXFLAGS" \
+    NUMTHREADS=2
+
+
+# List of packages to be installed
+ENV PACKAGES="\
+      attr \
+      bash \
+      bison \
+      bzip2 \
+      cairo \
+      fftw \
+      flex \
+      freetype \
+      gdal \
+      gettext \
+      geos \
+      gnutls \
+      gtk+3.0 \
+      libbz2 \
+      libjpeg-turbo \
+      libpng \
+      musl \
+      musl-utils \
+      ncurses \
+      openjpeg \
+      openblas \
+      py3-numpy \
+      py3-pillow \
+      py3-six \
+      postgresql \
+      sqlite \
+      sqlite-libs \
+      tiff \
+      wxgtk-base \
+      wxgtk \
+      zstd \
+      zstd-libs \
+    " \
+    # These packages are required to compile GRASS GIS.
+    GRASS_BUILD_PACKAGES="\
+      build-base \
+      bzip2-dev \
+      cairo-dev \
+      fftw-dev \
+      freetype-dev \
+      g++ \
+      gcc \
+      gdal-dev \
+      geos-dev \
+      gnutls-dev \
+      gtk+3.0-dev \
+      libc6-compat \
+      libjpeg-turbo-dev \
+      libpng-dev \
+      make \
+      openjpeg-dev \
+      openblas-dev \
+      postgresql-dev \
+      python3-dev \
+      py-numpy-dev \
+      sqlite-dev \
+      tar \
+      tiff-dev \
+      unzip \
+      vim \
+      wget \
+      wxgtk-base-dev \
+      wxgtk-dev \
+      zip \
+      zstd-dev \
+    "
+
+# ====================
+# INSTALL DEPENDENCIES
+# ====================
+
+WORKDIR /src
+
+ENV PYTHONBIN=python$PYTHON_VERSION
+
+RUN echo "Install Python";\
+    apk add --no-cache $PYTHONBIN && \
+    $PYTHONBIN -m ensurepip && \
+    rm -r /usr/lib/python*/ensurepip && \
+    pip$PYTHON_VERSION install --upgrade pip setuptools && \
+    if [ ! -e /usr/bin/pip ]; then ln -s pip$PYTHON_VERSION /usr/bin/pip ; fi && \
+    if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/$PYTHONBIN /usr/bin/python; fi && \
+    rm -r /root/.cache
+
+# Add the packages
+RUN echo "Install main packages";\
+    apk update; \
+    apk add --no-cache \
+            --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing \
+            --repository http://dl-cdn.alpinelinux.org/alpine/edge/main \
+            $PACKAGES; \
+    # Add packages just for the GRASS build process
+    apk add --no-cache \
+            --repository http://dl-cdn.alpinelinux.org/alpine/edge/testing \
+            --repository http://dl-cdn.alpinelinux.org/alpine/edge/main \
+            --virtual .build-deps $GRASS_BUILD_PACKAGES; \
+    # echo LANG="en_US.UTF-8" > /etc/default/locale;
+    #
+    # compile wxPython, unfortunately there is no Alpine package (yet) - compilation is slow, though!
+    $PYTHONBIN -m pip install -U pathlib2 wxPython && \
+    #
+    # install the latest projection library for GRASS GIS
+    #
+    echo "Install PROJ-$PROJ_VERSION";\
+    echo "  => Dowload proj-$PROJ_VERSION";\
+    wget http://download.osgeo.org/proj/proj-$PROJ_VERSION.tar.gz && \
+    tar xzvf proj-$PROJ_VERSION.tar.gz && \
+    cd /src/proj-$PROJ_VERSION/ && \
+    echo "  => Dowload datumgrid-$PROJ_DATUMGRID_VERSION" &&\
+    wget http://download.osgeo.org/proj/proj-datumgrid-$PROJ_DATUMGRID_VERSION.zip && \
+    cd nad && \
+    unzip ../proj-datumgrid-$PROJ_DATUMGRID_VERSION.zip && \
+    cd .. && \
+    echo "  => configure" &&\
+    ./configure --prefix=/usr/ && \
+    echo "  => compile" &&\
+    make && \
+    echo "  => install" &&\
+    make install && \
+    ldconfig /etc/ld.so.conf.d; \
+    #
+    # Checkout and install GRASS GIS
+    #
+    echo "Install GRASS GIS";\
+    echo "  => Dowload grass-$GRASS_VERSION";\
+    wget https://grass.osgeo.org/grass`echo $GRASS_VERSION | tr -d .`/source/snapshot/grass-$GRASS_VERSION.git_src_snapshot_latest.tar.gz && \
+    # unpack source code package and remove tarball archive:
+    mkdir /src/grass_build && \
+    tar xfz grass-$GRASS_VERSION.git_src_snapshot_latest.tar.gz --strip=1 -C /src/grass_build && \
+    rm -f grass-$GRASS_VERSION.git_src_snapshot_latest.tar.gz; \
+    #
+    # Fixup python shebangs - TODO: will be resolved in future by grass-core
+    cd /src/grass_build && \
+    find -name '*.py' | xargs sed -i 's,#!/usr/bin/env python,#!/usr/bin/env python3,' && \
+    sed -i 's,python,python3,' include/Make/Platform.make.in && \
+    #
+    # Configure compile and install GRASS GIS
+    #
+    echo "  => Configure and compile grass";\
+    cd /src/grass_build && \
+    /src/grass_build/configure $GRASS_CONFIG && \
+    make -j $NUMTHREADS && \
+    make install && \
+    ldconfig /etc/ld.so.conf.d; \
+    #
+    # enable simple grass command regardless of version number
+    #
+    ln -s `find /usr/local/bin -name "grass*"` /usr/local/bin/grass; \
+    #
+    # Reduce the image size
+    #
+    rm -rf /src/*; \
+    # remove build dependencies and any leftover apk cache
+    apk del --no-cache --purge .build-deps; \
+    rm -rf /var/cache/apk/*; \
+    rm -rf /root/.cache; \
+    # Remove unnecessary grass files
+    rm -rf /usr/local/grass77/demolocation; \
+    rm -rf /usr/local/grass77/docs; \
+    rm -rf /usr/local/grass77/fonts; \
+    rm -rf /usr/local/grass77/gui; \
+    rm -rf /usr/local/grass77/share;
+
+
+# Unset environmental variables to avoid later compilation issues
+ENV INTEL="" \
+    MYCFLAGS="" \
+    MYLDFLAGS="" \
+    MYCXXFLAGS="" \
+    LD_LIBRARY_PATH="" \
+    LDFLAGS="" \
+    CFLAGS="" \
+    CXXFLAGS="" \
+    # set SHELL var to avoid /bin/sh fallback in interactive GRASS GIS sessions in docker
+    SHELL="/bin/bash"
+
+
+# =====================
+# INSTALL GRASS-SESSION
+# =====================
+
+# install external Python API
+RUN pip install grass-session
+
+# set GRASSBIN
+ENV GRASSBIN="/usr/local/bin/grass"
+
+# ========
+# FINALIZE
+# ========
+
+# Data workdir
+WORKDIR /grassdb
+VOLUME /grassdb
+
+# GRASS GIS specific
+# allow work with MAPSETs that are not owned by current user
+ENV GRASS_SKIP_MAPSET_OWNER_CHECK=1 \
+    LC_ALL="en_US.UTF-8"
+
+# debug
+RUN $GRASSBIN --config svn_revision version
+
+CMD [$GRASSBIN, "--version"]

+ 278 - 237
gui/wxpython/datacatalog/tree.py

@@ -255,8 +255,7 @@ class LocationMapTree(TreeView):
 
     def __init__(
             self, parent, model=None, style=wx.TR_HIDE_ROOT | wx.TR_EDIT_LABELS
-            | wx.TR_LINES_AT_ROOT | wx.TR_HAS_BUTTONS | wx.TR_FULL_ROW_HIGHLIGHT |
-            wx.TR_SINGLE):
+            | wx.TR_LINES_AT_ROOT | wx.TR_HAS_BUTTONS | wx.TR_FULL_ROW_HIGHLIGHT | wx.TR_MULTIPLE):
         """Location Map Tree constructor."""
         self._model = TreeModel(DataCatalogNode)
         self._orig_model = self._model
@@ -422,53 +421,68 @@ class LocationMapTree(TreeView):
 
     def _initVariables(self):
         """Init variables."""
-        self.selected_layer = None
-        self.selected_type = None
-        self.selected_mapset = None
-        self.selected_location = None
+        self.selected_layer = []
+        self.selected_type = []
+        self.selected_mapset = []
+        self.selected_location = []
+        self.mixed = False
 
     def GetControl(self):
         """Returns control itself."""
         return self
 
-    def DefineItems(self, item):
+    def DefineItems(self, selected):
         """Set selected items."""
-        self.selected_layer = None
-        self.selected_type = None
-        self.selected_mapset = None
-        self.selected_location = None
-
-        type = item.data['type']
-        if type in ('raster', 'raster_3d', 'vector'):
-            self.selected_layer = item
-            type = 'element'
-            item = item.parent
-
-        if type == 'element':
-            self.selected_type = item
-            type = 'mapset'
-            item = item.parent
-
-        if type == 'mapset':
-            self.selected_mapset = item
-            type = 'location'
-            item = item.parent
-
-        if type == 'location':
-            self.selected_location = item
+        self._initVariables()
+        mixed = []
+        for item in selected:
+            type = item.data['type']
+            if type in ('raster', 'raster_3d', 'vector'):
+                self.selected_layer.append(item)
+                self.selected_type.append(item.parent)
+                self.selected_mapset.append(item.parent.parent)
+                self.selected_location.append(item.parent.parent.parent)
+                mixed.append('layer')
+            elif type == 'element':
+                self.selected_layer.append(None)
+                self.selected_type.append(item)
+                self.selected_mapset.append(item.parent)
+                self.selected_location.append(item.parent.parent)
+                mixed.append('element')
+            elif type == 'mapset':
+                self.selected_layer.append(None)
+                self.selected_type.append(None)
+                self.selected_mapset.append(item)
+                self.selected_location.append(item.parent)
+                mixed.append('mapset')
+            elif type == 'location':
+                self.selected_layer.append(None)
+                self.selected_type.append(None)
+                self.selected_mapset.append(None)
+                self.selected_location.append(item)
+                mixed.append('location')
+        self.mixed = False
+        if len(set(mixed)) > 1:
+            self.mixed = True
 
     def OnSelChanged(self, event):
         self.selected_layer = None
 
     def OnRightClick(self, node):
         """Display popup menu."""
-        self.DefineItems(node)
-        if self.selected_layer:
+        self.DefineItems(self.GetSelected())
+        if self.mixed:
+            self._popupMenuEmpty()
+            return
+
+        if self.selected_layer[0]:
             self._popupMenuLayer()
-        elif self.selected_mapset and not self.selected_type:
+        elif self.selected_mapset[0] and not self.selected_type[0] and len(self.selected_mapset) == 1:
             self._popupMenuMapset()
-        elif self.selected_type:
+        elif self.selected_type[0] and len(self.selected_type) == 1:
             self._popupMenuElement()
+        else:
+            self._popupMenuEmpty()
 
     def OnDoubleClick(self, node):
         """Double click on item/node.
@@ -477,8 +491,8 @@ class LocationMapTree(TreeView):
         expand/collapse node.
         """
         if not isinstance(self._giface, StandaloneGrassInterface):
-            self.DefineItems(node)
-            if self.selected_layer:
+            self.DefineItems([node])
+            if self.selected_layer[0] is not None:
                 # display selected layer and return
                 self.DisplayLayer()
                 return
@@ -576,42 +590,48 @@ class DataCatalogTree(LocationMapTree):
     def OnMoveMap(self, event):
         """Move layer or mapset (just save it temporarily, copying is done by paste)"""
         self.copy_mode = False
-        self.copy_layer = self.selected_layer
-        self.copy_type = self.selected_type
-        self.copy_mapset = self.selected_mapset
-        self.copy_location = self.selected_location
-        label = _("Map <{layer}> marked for moving.").format(layer=self.copy_layer.label)
+        self.copy_layer = self.selected_layer[:]
+        self.copy_type = self.selected_type[:]
+        self.copy_mapset = self.selected_mapset[:]
+        self.copy_location = self.selected_location[:]
+        if len(self.copy_layer) > 1:
+            label = _("{c} maps marked for moving.").format(c=len(self.selected_layer))
+        else:
+            label = _("Map <{layer}> marked for moving.").format(layer=self.copy_layer[0].label)
         self.showNotification.emit(message=label)
 
     def OnCopyMap(self, event):
         """Copy layer or mapset (just save it temporarily, copying is done by paste)"""
         self.copy_mode = True
-        self.copy_layer = self.selected_layer
-        self.copy_type = self.selected_type
-        self.copy_mapset = self.selected_mapset
-        self.copy_location = self.selected_location
-        label = _("Map <{layer}> marked for copying.").format(layer=self.copy_layer.label)
+        self.copy_layer = self.selected_layer[:]
+        self.copy_type = self.selected_type[:]
+        self.copy_mapset = self.selected_mapset[:]
+        self.copy_location = self.selected_location[:]
+        if len(self.copy_layer) > 1:
+            label = _("{c} maps marked for copying.").format(c=len(self.selected_layer))
+        else:
+            label = _("Map <{layer}> marked for copying.").format(layer=self.copy_layer[0].label)
         self.showNotification.emit(message=label)
 
     def OnRenameMap(self, event):
         """Rename layer with dialog"""
-        old_name = self.selected_layer.label
+        old_name = self.selected_layer[0].label
         gisrc, env = gscript.create_environment(
             gisenv()['GISDBASE'],
-            self.selected_location.label, mapset=self.selected_mapset.label)
+            self.selected_location[0].label, mapset=self.selected_mapset[0].label)
         new_name = self._getNewMapName(
             _('New name'),
             _('Rename map'),
             old_name,
             env=env,
-            mapset=self.selected_mapset.label,
-            element=self.selected_type.label)
+            mapset=self.selected_mapset[0].label,
+            element=self.selected_type[0].label)
         if new_name:
             self.Rename(old_name, new_name)
 
     def OnStartEditLabel(self, node, event):
         """Start label editing"""
-        self.DefineItems(node)
+        self.DefineItems([node])
         Debug.msg(1, "Start label edit {name}".format(name=node.label))
         label = _("Editing {name}").format(name=node.label)
         self.showNotification.emit(message=label)
@@ -631,20 +651,20 @@ class DataCatalogTree(LocationMapTree):
         string = old + ',' + new
         gisrc, env = gscript.create_environment(
             gisenv()['GISDBASE'],
-            self.selected_location.label, self.selected_mapset.label)
+            self.selected_location[0].label, self.selected_mapset[0].label)
         label = _("Renaming map <{name}>...").format(name=string)
         self.showNotification.emit(message=label)
-        if self.selected_type.label == 'vector':
+        if self.selected_type[0].label == 'vector':
             renamed, cmd = self._runCommand('g.rename', vector=string, env=env)
-        elif self.selected_type.label == 'raster':
+        elif self.selected_type[0].label == 'raster':
             renamed, cmd = self._runCommand('g.rename', raster=string, env=env)
         else:
             renamed, cmd = self._runCommand(
                 'g.rename', raster3d=string, env=env)
         if renamed == 0:
-            self.selected_layer.label = new
-            self.selected_layer.data['name'] = new
-            self.RefreshNode(self.selected_layer)
+            self.selected_layer[0].label = new
+            self.selected_layer[0].data['name'] = new
+            self.RefreshNode(self.selected_layer[0])
             self.showNotification.emit(
                 message=_("{cmd} -- completed").format(cmd=cmd))
             Debug.msg(1, "LAYER RENAMED TO: " + new)
@@ -659,106 +679,110 @@ class DataCatalogTree(LocationMapTree):
                 GMessage(_("No map selected for moving."), parent=self)
             return
 
-        gisrc, env = gscript.create_environment(
-                        gisenv()['GISDBASE'], self.selected_location.label, mapset=self.selected_mapset.label)
-        gisrc2, env2 = gscript.create_environment(
-                        gisenv()['GISDBASE'], self.copy_location.label, mapset=self.copy_mapset.label)
-        new_name = self.copy_layer.label
-        if self.selected_location == self.copy_location:
-            # within one mapset
-            if self.selected_mapset == self.copy_mapset:
-                # ignore when just moves map
-                if self.copy_mode is False:
-                    return
-                new_name = self._getNewMapName(_('New name'), _('Select new name'),
-                                               self.copy_layer.label, env=env,
-                                               mapset=self.selected_mapset.label,
-                                               element=self.copy_type.label)
-                if not new_name:
-                    return
-            # within one location, different mapsets
-            else:
-                if map_exists(new_name, element=self.copy_type.label, env=env,
-                              mapset=self.selected_mapset.label):
-                    new_name = self._getNewMapName(_('New name'), _('Select new name'),
-                                                   self.copy_layer.label, env=env,
-                                                   mapset=self.selected_mapset.label,
-                                                   element=self.copy_type.label)
+        for i in range(len(self.copy_layer)):
+            gisrc, env = gscript.create_environment(
+                            gisenv()['GISDBASE'], self.selected_location[0].label, mapset=self.selected_mapset[0].label)
+            gisrc2, env2 = gscript.create_environment(
+                            gisenv()['GISDBASE'], self.copy_location[i].label, mapset=self.copy_mapset[i].label)
+            new_name = self.copy_layer[i].label
+            if self.selected_location[0] == self.copy_location[i]:
+                # within one mapset
+                if self.selected_mapset[0] == self.copy_mapset[i]:
+                    # ignore when just moves map
+                    if self.copy_mode is False:
+                        return
+                    new_name = self._getNewMapName(_('New name for <{n}>').format(n=self.copy_layer[i].label),
+                                                   _('Select new name'),
+                                                   self.copy_layer[i].label, env=env,
+                                                   mapset=self.selected_mapset[0].label,
+                                                   element=self.copy_type[i].label)
                     if not new_name:
                         return
-
-            string = self.copy_layer.label + '@' + self.copy_mapset.label + ',' + new_name
-            pasted = 0
-            if self.copy_mode:
-                label = _("Copying <{name}>...").format(name=string)
-            else:
-                label = _("Moving <{name}>...").format(name=string)
-            self.showNotification.emit(message=label)
-            if self.copy_type.label == 'vector':
-                pasted, cmd = self._runCommand('g.copy', vector=string, env=env)
-                node = 'vector'
-            elif self.copy_type.label == 'raster':
-                pasted, cmd = self._runCommand('g.copy', raster=string, env=env)
-                node = 'raster'
-            else:
-                pasted, cmd = self._runCommand('g.copy', raster_3d=string, env=env)
-                node = 'raster_3d'
-            if pasted == 0:
-                self.InsertLayer(name=new_name, mapset_node=self.selected_mapset,
-                                 element_name=node)
-                Debug.msg(1, "COPIED TO: " + new_name)
+                # within one location, different mapsets
+                else:
+                    if map_exists(new_name, element=self.copy_type[i].label, env=env,
+                                  mapset=self.selected_mapset[0].label):
+                        new_name = self._getNewMapName(_('New name for <{n}>').format(n=self.copy_layer[i].label),
+                                                       _('Select new name'),
+                                                       self.copy_layer[i].label, env=env,
+                                                       mapset=self.selected_mapset[0].label,
+                                                       element=self.copy_type[i].label)
+                        if not new_name:
+                            return
+    
+                string = self.copy_layer[i].label + '@' + self.copy_mapset[i].label + ',' + new_name
+                pasted = 0
                 if self.copy_mode:
-                    self.showNotification.emit(message=_("g.copy completed").format(cmd=cmd))
+                    label = _("Copying <{name}>...").format(name=string)
                 else:
-                    self.showNotification.emit(message=_("g.copy completed").format(cmd=cmd))
-
-                # remove old
-                if not self.copy_mode:
-                    self._removeMapAfterCopy(env2)
-
-            gscript.try_remove(gisrc)
-            gscript.try_remove(gisrc2)
-            # expand selected mapset
-            self.ExpandNode(self.selected_mapset, recursive=True)
-            self._initVariablesCatalog()
-        else:
-            if self.copy_type.label == 'raster_3d':
-                GError(_("Reprojection is not implemented for 3D rasters"), parent=self)
-                return
-            if map_exists(new_name, element=self.copy_type.label, env=env,
-                          mapset=self.selected_mapset.label):
-                new_name = self._getNewMapName(_('New name'), _('Select new name'),
-                                               self.copy_layer.label, env=env,
-                                               mapset=self.selected_mapset.label,
-                                               element=self.copy_type.label)
-                if not new_name:
+                    label = _("Moving <{name}>...").format(name=string)
+                self.showNotification.emit(message=label)
+                if self.copy_type[i].label == 'vector':
+                    pasted, cmd = self._runCommand('g.copy', vector=string, env=env)
+                    node = 'vector'
+                elif self.copy_type[i].label == 'raster':
+                    pasted, cmd = self._runCommand('g.copy', raster=string, env=env)
+                    node = 'raster'
+                else:
+                    pasted, cmd = self._runCommand('g.copy', raster_3d=string, env=env)
+                    node = 'raster_3d'
+                if pasted == 0:
+                    self.InsertLayer(name=new_name, mapset_node=self.selected_mapset[0],
+                                     element_name=node)
+                    Debug.msg(1, "COPIED TO: " + new_name)
+                    if self.copy_mode:
+                        self.showNotification.emit(message=_("g.copy completed"))
+                    else:
+                        self.showNotification.emit(message=_("g.copy completed"))
+    
+                    # remove old
+                    if not self.copy_mode:
+                        self._removeMapAfterCopy(self.copy_layer[i], self.copy_type[i], env2)
+    
+                gscript.try_remove(gisrc)
+                gscript.try_remove(gisrc2)
+                # expand selected mapset
+            else:
+                if self.copy_type[i].label == 'raster_3d':
+                    GError(_("Reprojection is not implemented for 3D rasters"), parent=self)
                     return
-            gisdbase = gisenv()['GISDBASE']
-            callback = lambda: self._onDoneReprojection(iEnv=env2, iGisrc=gisrc2, oGisrc=gisrc)
-            dlg = CatalogReprojectionDialog(self, self._giface, gisdbase, self.copy_location.label,
-                                            self.copy_mapset.label, self.copy_layer.label, env2,
-                                            gisdbase, self.selected_location.label, self.selected_mapset.label,
-                                            new_name, self.copy_type.label, env, callback)
-            dlg.ShowModal()
-
-    def _onDoneReprojection(self, iEnv, iGisrc, oGisrc):
-        self.InsertLayer(name=self.copy_layer.label, mapset_node=self.selected_mapset,
-                         element_name=self.copy_type.label)
-        if not self.copy_mode:
-            self._removeMapAfterCopy(iEnv)
+                if map_exists(new_name, element=self.copy_type[i].label, env=env,
+                              mapset=self.selected_mapset[0].label):
+                    new_name = self._getNewMapName(_('New name'), _('Select new name'),
+                                                   self.copy_layer[i].label, env=env,
+                                                   mapset=self.selected_mapset[0].label,
+                                                   element=self.copy_type[i].label)
+                    if not new_name:
+                        continue
+                gisdbase = gisenv()['GISDBASE']
+                callback = lambda gisrc2=gisrc2, gisrc=gisrc, cLayer=self.copy_layer[i], \
+                                  cType=self.copy_type[i], cMode=self.copy_mode, name=new_name: \
+                                  self._onDoneReprojection(env2, gisrc2, gisrc, cLayer, cType, cMode, name)
+                dlg = CatalogReprojectionDialog(self, self._giface, gisdbase, self.copy_location[i].label,
+                                                self.copy_mapset[i].label, self.copy_layer[i].label, env2,
+                                                gisdbase, self.selected_location[0].label, self.selected_mapset[0].label,
+                                                new_name, self.copy_type[i].label, env, callback)
+                dlg.ShowModal()
+        self.ExpandNode(self.selected_mapset[0], recursive=True)
+        self._initVariablesCatalog()
+
+    def _onDoneReprojection(self, iEnv, iGisrc, oGisrc, cLayer, cType, cMode, name):
+        self.InsertLayer(name=name, mapset_node=self.selected_mapset[0],
+                         element_name=cType.label)
+        if not cMode:
+            self._removeMapAfterCopy(cLayer, cType, iEnv)
         gscript.try_remove(iGisrc)
         gscript.try_remove(oGisrc)
-        self.ExpandNode(self.selected_mapset, recursive=True)
-        self._initVariablesCatalog()
+        self.ExpandNode(self.selected_mapset[0], recursive=True)
 
-    def _removeMapAfterCopy(self, env):
-        removed, cmd = self._runCommand('g.remove', type=self.copy_type.label,
-                                        name=self.copy_layer.label, flags='f', env=env)
+    def _removeMapAfterCopy(self, cLayer, cType, env):
+        removed, cmd = self._runCommand('g.remove', type=cType.label,
+                                        name=cLayer.label, flags='f', env=env)
         if removed == 0:
-            self._model.RemoveNode(self.copy_layer)
-            self.RefreshNode(self.copy_type, recursive=True)
-            Debug.msg(1, "LAYER " + self.copy_layer.label + " DELETED")
-            self.showNotification.emit(message=_("g.remove completed").format(cmd=cmd))
+            self._model.RemoveNode(cLayer)
+            self.RefreshNode(cType, recursive=True)
+            Debug.msg(1, "LAYER " + cLayer.label + " DELETED")
+            self.showNotification.emit(message=_("g.remove completed"))
 
     def InsertLayer(self, name, mapset_node, element_name):
         """Insert layer into model and refresh tree"""
@@ -779,73 +803,67 @@ class DataCatalogTree(LocationMapTree):
 
     def OnDeleteMap(self, event):
         """Delete layer or mapset"""
-        name = self.selected_layer.label
-        gisrc, env = gscript.create_environment(
-            gisenv()['GISDBASE'],
-            self.selected_location.label, self.selected_mapset.label)
-        if self._confirmDialog(
-                question=_(
-                    "Do you really want to delete map <{m}> of type <{etype}> from mapset "
-                    "<{mapset}> in location <{loc}>?").format(
-                    m=name, mapset=self.selected_mapset.label,
-                    etype=self.selected_type.label,
-                    loc=self.selected_location.label),
-                title=_('Delete map')) == wx.ID_YES:
-            label = _("Deleting {name}...").format(name=name)
+        names = [self.selected_layer[i].label + '@' + self.selected_mapset[i].label
+                 for i in range(len(self.selected_layer))]
+        if len(names) < 10:
+            question = _("Do you really want to delete map(s) <{m}>?").format(m=', '.join(names))
+        else:
+            question = _("Do you really want to delete {n} maps?").format(n=len(names))
+        if self._confirmDialog(question, title=_('Delete map')) == wx.ID_YES:
+            label = _("Deleting {name}...").format(name=names)
             self.showNotification.emit(message=label)
-
-            removed, cmd = self._runCommand(
-                'g.remove', flags='f', type=self.selected_type.label, name=name, env=env)
-            if removed == 0:
-                self._model.RemoveNode(self.selected_layer)
-                self.RefreshNode(self.selected_type, recursive=True)
-                Debug.msg(1, "LAYER " + name + " DELETED")
-                self.showNotification.emit(
-                    message=_("g.remove completed").format(cmd=cmd))
-
-                # remove map layer from layer tree if exists
-                if not isinstance(self._giface, StandaloneGrassInterface):
-                    name = self.selected_layer.label + '@' + self.selected_mapset.label
-                    layers = self._giface.GetLayerList().GetLayersByName(name)
-                    for layer in layers:
-                        self._giface.GetLayerList().DeleteLayer(layer)
-
-        gscript.try_remove(gisrc)
+            for i in range(len(self.selected_layer)):
+                gisrc, env = gscript.create_environment(
+                    gisenv()['GISDBASE'],
+                    self.selected_location[i].label, self.selected_mapset[i].label)
+                removed, cmd = self._runCommand(
+                        'g.remove', flags='f', type=self.selected_type[i].label,
+                        name=self.selected_layer[i].label, env=env)
+                if removed == 0:
+                    self._model.RemoveNode(self.selected_layer[i])
+                    self.RefreshNode(self.selected_type[i], recursive=True)
+                    Debug.msg(1, "LAYER " + self.selected_layer[i].label + " DELETED")
+
+                    # remove map layer from layer tree if exists
+                    if not isinstance(self._giface, StandaloneGrassInterface):
+                        name = self.selected_layer[i].label + '@' + self.selected_mapset[i].label
+                        layers = self._giface.GetLayerList().GetLayersByName(name)
+                        for layer in layers:
+                            self._giface.GetLayerList().DeleteLayer(layer)
+
+                gscript.try_remove(gisrc)
+            self.UnselectAll()
+            self.showNotification.emit(message=_("g.remove completed"))
 
     def OnDisplayLayer(self, event):
         """Display layer in current graphics view"""
         self.DisplayLayer()
-        
+
     def DisplayLayer(self):
         """Display selected layer in current graphics view"""
-        layerName = []
-        if self.selected_location.label == gisenv(
-        )['LOCATION_NAME'] and self.selected_mapset:
-            string = self.selected_layer.label + '@' + self.selected_mapset.label
-            layerName.append(string)
-            label = _("Displaying {name}...").format(name=string)
-            self.showNotification.emit(message=label)
-            self._giface.lmgr.AddMaps(
-                layerName, self.selected_type.label, True)
+        all_names = []
+        names = {'raster': [], 'vector': [], 'raster3d': []}
+        for i in range(len(self.selected_layer)):
+            name = self.selected_layer[i].label + '@' + self.selected_mapset[i].label
+            names[self.selected_type[i].label].append(name)
+            all_names.append(name)
+        #if self.selected_location[0].label == gisenv()['LOCATION_NAME'] and self.selected_mapset[0]:
+        for ltype in names:
+            if names[ltype]:
+                self._giface.lmgr.AddMaps(list(reversed(names[ltype])), ltype, True)
 
-            if len(self._giface.GetLayerList()) == 1:
-                # zoom to map if there is only one map layer
-                self._giface.GetMapWindow().ZoomToMap()
+        if len(self._giface.GetLayerList()) == 1:
+            # zoom to map if there is only one map layer
+            self._giface.GetMapWindow().ZoomToMap()
+
+        Debug.msg(1, "Displayed layer(s): " + str(all_names))
 
-            label = "d." + self.selected_type.label[:4] + " --q map=" + string + \
-                    _(" -- completed. Go to Layers tab for further operations.")
-            self.showNotification.emit(message=label)
-            Debug.msg(1, "LAYER " + self.selected_layer.label + " DISPLAYED")
-        else:
-            GError(
-                _("Failed to display layer: not in current mapset or invalid layer"),
-                parent=self)
 
     def OnBeginDrag(self, node, event):
         """Just copy necessary data"""
-        self.DefineItems(node)
+        self.DefineItems(self.GetSelected())
         if self.selected_layer and not (self._restricted and gisenv()[
-                                        'LOCATION_NAME'] != self.selected_location.label):
+                                        'LOCATION_NAME'] != self.selected_location[0].label):
             event.Allow()
             self.OnCopyMap(event)
             Debug.msg(1, "DRAG")
@@ -856,8 +874,8 @@ class DataCatalogTree(LocationMapTree):
         """Copy layer into target"""
         self.copy_mode = wx.GetMouseState().ControlDown()
         if node:
-            self.DefineItems(node)
-            if self._restricted and gisenv()['MAPSET'] != self.selected_mapset.label:
+            self.DefineItems([node])
+            if self._restricted and gisenv()['MAPSET'] != self.selected_mapset[0].label:
                 GMessage(_("To move or copy maps to other mapsets, unlock editing of other mapsets"),
                          parent=self)
                 event.Veto()
@@ -869,37 +887,41 @@ class DataCatalogTree(LocationMapTree):
 
     def OnSwitchLocationMapset(self, event):
         genv = gisenv()
-        if self.selected_location.label == genv['LOCATION_NAME']:
-            self.changeMapset.emit(mapset=self.selected_mapset.label)
+        if self.selected_location[0].label == genv['LOCATION_NAME']:
+            self.changeMapset.emit(mapset=self.selected_mapset[0].label)
         else:
-            self.changeLocation.emit(mapset=self.selected_mapset.label, location=self.selected_location.label)
+            self.changeLocation.emit(mapset=self.selected_mapset[0].label, location=self.selected_location[0].label)
         self.ExpandCurrentMapset()
 
     def OnMetadata(self, event):
         """Show metadata of any raster/vector/3draster"""
         def done(event):
-            gscript.try_remove(gisrc)
-
-        if self.selected_type.label == 'raster':
-            cmd = ['r.info']
-        elif self.selected_type.label == 'vector':
-            cmd = ['v.info']
-        elif self.selected_type.label == 'raster_3d':
-            cmd = ['r3.info']
-        cmd.append('map=%s@%s' % (self.selected_layer.label, self.selected_mapset.label))
-
-        gisrc, env = gscript.create_environment(
-            gisenv()['GISDBASE'],
-            self.selected_location.label, self.selected_mapset.label)
-        # print output to command log area
-        # temp gisrc file must be deleted onDone
-        self._giface.RunCmd(cmd, env=env, onDone=done)
+            gscript.try_remove(event.userData)
+
+        for i in range(len(self.selected_layer)):
+            if self.selected_type[i].label == 'raster':
+                cmd = ['r.info']
+            elif self.selected_type[i].label == 'vector':
+                cmd = ['v.info']
+            elif self.selected_type[i].label == 'raster_3d':
+                cmd = ['r3.info']
+            cmd.append('map=%s@%s' % (self.selected_layer[i].label, self.selected_mapset[i].label))
+
+            gisrc, env = gscript.create_environment(
+                gisenv()['GISDBASE'],
+                self.selected_location[i].label, self.selected_mapset[i].label)
+            # print output to command log area
+            # temp gisrc file must be deleted onDone
+            self._giface.RunCmd(cmd, env=env, onDone=done, userData=gisrc)
 
     def OnCopyName(self, event):
         """Copy layer name to clipboard"""
         if wx.TheClipboard.Open():
             do = wx.TextDataObject()
-            do.SetText('%s@%s' % (self.selected_layer.label, self.selected_mapset.label))
+            text = []
+            for i in range(len(self.selected_layer)):
+                text.append('%s@%s' % (self.selected_layer[i].label, self.selected_mapset[i].label))
+            do.SetText(','.join(text))
             wx.TheClipboard.SetData(do)
             wx.TheClipboard.Close()
 
@@ -947,11 +969,18 @@ class DataCatalogTree(LocationMapTree):
 
     def _isCurrent(self, genv):
         if self._restricted:
-            currentMapset = currentLocation = False
-            if self.selected_location.label == genv['LOCATION_NAME']:
-                currentLocation = True
-                if self.selected_mapset.label == genv['MAPSET']:
-                    currentMapset = True
+            currentMapset = currentLocation = True
+            for i in range(len(self.selected_location)):
+                if self.selected_location[i].label != genv['LOCATION_NAME']:
+                    currentLocation = False
+                    currentMapset = False
+                    break
+            if currentMapset:
+                for i in range(len(self.selected_mapset)):
+                    if self.selected_mapset[i].label != genv['MAPSET']:
+                        currentMapset = False
+                        break
+
             return currentLocation, currentMapset
         else:
             return True, True
@@ -990,15 +1019,18 @@ class DataCatalogTree(LocationMapTree):
         item = wx.MenuItem(menu, wx.NewId(), _("&Rename"))
         menu.AppendItem(item)
         self.Bind(wx.EVT_MENU, self.OnRenameMap, item)
-        item.Enable(currentMapset)
+        item.Enable(currentMapset and len(self.selected_layer) == 1)
 
         menu.AppendSeparator()
 
-        if not isinstance(self._giface, StandaloneGrassInterface) and \
-           self.selected_location.label == genv['LOCATION_NAME']:
-            item = wx.MenuItem(menu, wx.NewId(), _("&Display layer"))
-            menu.AppendItem(item)
-            self.Bind(wx.EVT_MENU, self.OnDisplayLayer, item)
+        if not isinstance(self._giface, StandaloneGrassInterface):
+            if all([each.label == genv['LOCATION_NAME'] for each in self.selected_location]):
+                if len(self.selected_layer) > 1:
+                    item = wx.MenuItem(menu, wx.NewId(), _("&Display layers"))
+                else:
+                    item = wx.MenuItem(menu, wx.NewId(), _("&Display layer"))
+                menu.AppendItem(item)
+                self.Bind(wx.EVT_MENU, self.OnDisplayLayer, item)
 
         item = wx.MenuItem(menu, wx.NewId(), _("Show &metadata"))
         menu.AppendItem(item)
@@ -1022,8 +1054,8 @@ class DataCatalogTree(LocationMapTree):
         item = wx.MenuItem(menu, wx.NewId(), _("&Switch mapset"))
         menu.AppendItem(item)
         self.Bind(wx.EVT_MENU, self.OnSwitchLocationMapset, item)
-        if (self.selected_location.label == genv['LOCATION_NAME']
-                and self.selected_mapset.label == genv['MAPSET']):
+        if (self.selected_location[0].label == genv['LOCATION_NAME']
+                and self.selected_mapset[0].label == genv['MAPSET']):
             item.Enable(False)
         self.PopupMenu(menu)
         menu.Destroy()
@@ -1041,3 +1073,12 @@ class DataCatalogTree(LocationMapTree):
 
         self.PopupMenu(menu)
         menu.Destroy()
+
+    def _popupMenuEmpty(self):
+        """Create empty popup when multiple different types of items are selected"""
+        menu = Menu()
+        item = wx.MenuItem(menu, wx.NewId(), _("No available options"))
+        menu.AppendItem(item)
+        item.Enable(False)
+        self.PopupMenu(menu)
+        menu.Destroy()

+ 3 - 1
imagery/i.vi/i.vi.html

@@ -353,10 +353,12 @@ r.univar -e gemi
 
 <h3>Calculation of GVI</h3>
 
-The calculation of GVI from the reflectance values is done as follows:
+The calculation of GVI (Green Vegetation Index - Tasseled Cap) from the
+reflectance values is done as follows:
 
 <div class="code"><pre>
 g.region raster=band.3 -p
+# assuming Landsat-7
 i.vi blue=band.1 green=band.2 red=band.3 nir=band.4 band5=band.5 band7=band.7 viname=gvi output=gvi
 r.univar -e gvi
 </pre></div>

+ 2 - 0
imagery/i.vi/main.c

@@ -161,6 +161,7 @@ int main(int argc, char *argv[])
     opt.blue->description = _("Range: [0.0;1.0]");
     opt.blue->guisection = _("Optional inputs");
 
+    /* TODO: the naming is suboptimal as specific to Landsat-7 */
     opt.chan5 = G_define_standard_option(G_OPT_R_INPUT);
     opt.chan5->key = "band5";
     opt.chan5->required = NO;
@@ -169,6 +170,7 @@ int main(int argc, char *argv[])
     opt.chan5->description = _("Range: [0.0;1.0]");
     opt.chan5->guisection = _("Optional inputs");
 
+    /* TODO: the naming is suboptimal as specific to Landsat-7 */
     opt.chan7 = G_define_standard_option(G_OPT_R_INPUT);
     opt.chan7->key = "band7";
     opt.chan7->required = NO;

+ 1 - 1
include/Make/Html.make

@@ -8,7 +8,7 @@ $(HTMLDIR)/%.html: %.html %.tmp.html $(HTMLSRC) $(IMGDST) | $(HTMLDIR)
         $(PYTHON) $(GISBASE)/tools/mkhtml.py $* > $@
 
 $(MANDIR)/%.$(MANSECT): $(HTMLDIR)/%.html
-	$(HTML2MAN) $< $@
+	$(HTML2MAN) "$<" "$@"
 
 %.tmp.html: $(HTMLSRC)
 	if [ "$(HTMLSRC)" != "" ] ; then $(call htmldesc,$<,$@) ; fi

+ 208 - 262
lib/init/grass.py

@@ -40,48 +40,85 @@ is not safe, i.e. it has side effects (this should be changed in the future).
 from __future__ import print_function
 import sys
 import os
+import errno
 import atexit
+import gettext
+import shutil
+import signal
 import string
 import subprocess
+import types
 import re
+import six
 import platform
 import tempfile
 import locale
 
 
-# ----+- Python 3 compatibility start -+----
-PY2 = sys.version[0] == '2'
+# mechanism meant for debugging this script (only)
+# private global to store if we are debugging
+_DEBUG = None
+
+# for wxpath
+_WXPYTHON_BASE = None
+
 ENCODING = locale.getdefaultlocale()[1]
 if ENCODING is None:
     ENCODING = 'UTF-8'
     print("Default locale not found, using UTF-8")  # intentionally not translatable
 
+# The "@...@" variables are being substituted during build process
+#
+# TODO: should GISBASE be renamed to something like GRASS_PATH?
+# GISBASE marks complete runtime, so no need to get it here when
+# setting it up, possible scenario: existing runtime and starting
+# GRASS in that, we want to overwrite the settings, not to take it
+# possibly same for GRASS_PROJSHARE and others but maybe not
+#
+# We need to simultaneously make sure that:
+# - we get GISBASE from os.environ if it is defined (doesn't this mean that we are already
+#   inside a GRASS session? If we are, why do we need to run this script again???).
+# - GISBASE exists as an ENV variable
+#
+# pmav99: Ugly as hell, but that's what the code before the refactoring was doing.
+if 'GISBASE' in os.environ and len(os.getenv('GISBASE')) > 0:
+    GISBASE = os.path.normpath(os.environ["GISBASE"])
+else:
+    GISBASE = os.path.normpath("@GISBASE@")
+    os.environ['GISBASE'] = GISBASE
+CMD_NAME = "@START_UP@"
+GRASS_VERSION = "@GRASS_VERSION_NUMBER@"
+LD_LIBRARY_PATH_VAR = '@LD_LIBRARY_PATH_VAR@'
+CONFIG_PROJSHARE = os.environ.get('GRASS_PROJSHARE', "@CONFIG_PROJSHARE@")
+
+# Get the system name
+WINDOWS = sys.platform == 'win32'
+CYGWIN = "cygwin" in sys.platform
+MACOSX = "darwin" in sys.platform
+
 
-def decode(bytes_, encoding=None):
+def decode(bytes_, encoding=ENCODING):
     """Decode bytes with default locale and return (unicode) string
     Adapted from lib/python/core/utils.py
 
     No-op if parameter is not bytes (assumed unicode string).
 
     :param bytes bytes_: the bytes to decode
-    :param encoding: encoding to be used, default value is None
+    :param encoding: encoding to be used, default value is the system's default
+        encoding or, if that cannot be determined, 'UTF-8'.
     """
     if sys.version_info.major >= 3:
         unicode = str
     if isinstance(bytes_, unicode):
         return bytes_
     elif isinstance(bytes_, bytes):
-        if encoding is None:
-            enc = ENCODING
-        else:
-            enc = encoding
-        return bytes_.decode(enc)
+        return bytes_.decode(encoding)
     else:
         # if something else than text
         raise TypeError("can only accept types str and bytes")
 
 
-def encode(string, encoding=None):
+def encode(string, encoding=ENCODING):
     """Encode string with default locale and return bytes with that encoding
     Adapted from lib/python/core/utils.py
 
@@ -89,7 +126,8 @@ def encode(string, encoding=None):
     This ensures garbage in, garbage out.
 
     :param str string: the string to encode
-    :param encoding: encoding to be used, default value is None
+    :param encoding: encoding to be used, default value is the system's default
+        encoding or, if that cannot be determined, 'UTF-8'.
     """
     if sys.version_info.major >= 3:
         unicode = str
@@ -97,20 +135,16 @@ def encode(string, encoding=None):
         return string
     # this also tests str in Py3:
     elif isinstance(string, unicode):
-        if encoding is None:
-            enc = ENCODING
-        else:
-            enc = encoding
-        return string.encode(enc)
+        return string.encode(encoding)
     else:
         # if something else than text
         raise TypeError("can only accept types str and bytes")
 
 
-# currently not used, see https://trac.osgeo.org/grass/ticket/3508
+# see https://trac.osgeo.org/grass/ticket/3508
 def to_text_string(obj, encoding=ENCODING):
     """Convert `obj` to (unicode) text string"""
-    if PY2:
+    if six.PY2:
         # Python 2
         return encode(obj, encoding=encoding)
     else:
@@ -118,53 +152,6 @@ def to_text_string(obj, encoding=ENCODING):
         return decode(obj, encoding=encoding)
 
 
-if PY2:
-    import types
-    string_types = basestring,
-    integer_types = (int, long)
-    class_types = (type, types.ClassType)
-    text_type = unicode
-    binary_type = str
-else:
-    string_types = str,
-    integer_types = int,
-    class_types = type,
-    text_type = str
-    binary_type = bytes
-    MAXSIZE = sys.maxsize
-
-# ----+- Python 3 compatibility end -+----
-
-# Variables substituted during build process
-if 'GISBASE' in os.environ and len(os.getenv('GISBASE')) > 0:
-    # TODO: should this be something like GRASS_PATH?
-    # GISBASE marks complete runtime, so no need to get it here when
-    # setting it up, possible scenario: existing runtime and starting
-    # GRASS in that, we want to overwrite the settings, not to take it
-    # possibly same for GRASS_PROJSHARE and others but maybe not
-    gisbase = os.environ['GISBASE']
-else:
-    gisbase = "@GISBASE@"
-cmd_name = "@START_UP@"
-grass_version = "@GRASS_VERSION_NUMBER@"
-ld_library_path_var = '@LD_LIBRARY_PATH_VAR@'
-if 'GRASS_PROJSHARE' in os.environ:
-    config_projshare = os.environ['GRASS_PROJSHARE']
-else:
-    config_projshare = "@CONFIG_PROJSHARE@"
-
-gisbase = os.path.normpath(gisbase)
-
-# i18N
-import gettext
-# TODO: is this needed or even desirable when we have set_language()?
-gettext.install('grasslibs', os.path.join(gisbase, 'locale'))
-
-
-def warning(text):
-    sys.stderr.write(_("WARNING") + ': ' + text + os.linesep)
-
-
 def try_remove(path):
     try:
         os.remove(path)
@@ -172,65 +159,17 @@ def try_remove(path):
         pass
 
 
-def try_rmdir(path):
-    try:
-        os.rmdir(path)
-    except:
-        pass
-
-
-def clean_env(gisrc):
+def clean_env():
+    gisrc = os.environ['GISRC']
     env_curr = read_gisrc(gisrc)
     env_new = {}
     for k, v in env_curr.items():
         if k.endswith('PID') or k.startswith('MONITOR'):
             continue
         env_new[k] = v
-
     write_gisrc(env_new, gisrc)
 
 
-def cleanup_dir(path):
-    if not path:
-        return
-
-    for root, dirs, files in os.walk(path, topdown=False):
-        for name in files:
-            try_remove(os.path.join(root, name))
-        for name in dirs:
-            try_rmdir(os.path.join(root, name))
-
-
-class Cleaner(object):  # pylint: disable=R0903
-    """Holds directories and files which needs to be cleaned or deleted"""
-    def __init__(self):
-        self.tmpdir = None
-
-    def cleanup(self):
-        """This can be registered with atexit
-
-        Object can then still change and add or remove directories to clean"""
-        # all exits after setting up tmp dirs (system/location) should
-        # also tidy it up
-        cleanup_dir(self.tmpdir)
-        try_rmdir(self.tmpdir)
-
-
-def fatal(msg):
-    sys.stderr.write("%s: " % _('ERROR') + msg + os.linesep)
-    sys.exit(_("Exiting..."))
-
-
-def message(msg):
-    sys.stderr.write(msg + "\n")
-    sys.stderr.flush()
-
-
-# mechanism meant for debugging this script (only)
-# private global to store if we are debugging
-_DEBUG = None
-
-
 def is_debug():
     """Returns True if we are in debug mode
 
@@ -258,6 +197,20 @@ def debug(msg):
         sys.stderr.flush()
 
 
+def message(msg):
+    sys.stderr.write(msg + "\n")
+    sys.stderr.flush()
+
+
+def warning(text):
+    sys.stderr.write(_("WARNING") + ': ' + text + os.linesep)
+
+
+def fatal(msg):
+    sys.stderr.write("%s: " % _('ERROR') + msg + os.linesep)
+    sys.exit(_("Exiting..."))
+
+
 def readfile(path):
     debug("Reading %s" % path)
     f = open(path, 'r')
@@ -275,14 +228,14 @@ def writefile(path, s):
 
 def call(cmd, **kwargs):
     """Wrapper for subprocess.call to deal with platform-specific issues"""
-    if windows:
+    if WINDOWS:
         kwargs['shell'] = True
     return subprocess.call(cmd, **kwargs)
 
 
 def Popen(cmd, **kwargs):  # pylint: disable=C0103
     """Wrapper for subprocess.Popen to deal with platform-specific issues"""
-    if windows:
+    if WINDOWS:
         kwargs['shell'] = True
     return subprocess.Popen(cmd, **kwargs)
 
@@ -290,33 +243,29 @@ def Popen(cmd, **kwargs):  # pylint: disable=C0103
 def gpath(*args):
     """Costruct path to file or directory in GRASS GIS installation
 
-    Can be called only after gisbase was set.
+    Can be called only after GISBASE was set.
     """
-    return os.path.join(gisbase, *args)
-
-
-# for wxpath
-_WXPYTHON_BASE = None
+    return os.path.join(GISBASE, *args)
 
 
 def wxpath(*args):
     """Costruct path to file or directory in GRASS wxGUI
 
-    Can be called only after gisbase was set.
+    Can be called only after GISBASE was set.
 
     This function does not check if the directories exist or if GUI works
     this must be done by the caller if needed.
     """
     global _WXPYTHON_BASE
     if not _WXPYTHON_BASE:
-        # this can be called only after gisbase was set
+        # this can be called only after GISBASE was set
         _WXPYTHON_BASE = gpath("gui", "wxpython")
     return os.path.join(_WXPYTHON_BASE, *args)
 
 
 # using format for most but leaving usage of template for the dynamic ones
 # two different methods are easy way to implement two phase construction
-help_text = r"""GRASS GIS $VERSION_NUMBER
+HELP_TEXT = r"""GRASS GIS $VERSION_NUMBER
 Geographic Resources Analysis Support System (GRASS GIS).
 
 {usage}:
@@ -365,49 +314,51 @@ Geographic Resources Analysis Support System (GRASS GIS).
   GRASS_ADDON_BASE               {addon_base_var}
   GRASS_BATCH_JOB                {batch_var}
   GRASS_PYTHON                   {python_var}
-""".format(
-    usage=_("Usage"),
-    flags=_("Flags"),
-    help_flag=_("print this help message"),
-    version_flag=_("show version information and exit"),
-    create=_("create given database, location or mapset if it doesn't exist"),
-    exit_after=_("exit after creation of location or mapset. Only with -c flag"),
-    force_removal=_("force removal of .gislock if exists (use with care!). Only with --text flag"),
-    text=_("use text based interface (skip graphical welcome screen)"),
-    text_detail=_("and set as default"),
-    gtext=_("use text based interface (show graphical welcome screen)"),
-    gtext_detail=_("and set as default"),
-    gui=_("use $DEFAULT_GUI graphical user interface"),
-    gui_detail=_("and set as default"),
-    config=_("print GRASS configuration parameters"),
-    config_detail=_("options: arch,build,compiler,path,revision,svn_revision,version"),
-    params=_("Parameters"),
-    gisdbase=_("initial GRASS database directory"),
-    gisdbase_detail=_("directory containing Locations"),
-    location=_("initial GRASS Location"),
-    location_detail=_("directory containing Mapsets with one common coordinate system (projection)"),
-    mapset=_("initial GRASS Mapset"),
-    full_mapset=_("fully qualified initial Mapset directory"),
-    env_vars=_("Environment variables relevant for startup"),
-    gui_var=_("select GUI (text, gui, gtext)"),
-    html_var=_("set html web browser for help pages"),
-    addon_path_var=_("set additional path(s) to local GRASS modules or user scripts"),
-    addon_base_var=_("set additional GISBASE for locally installed GRASS Addons"),
-    batch_var=_("shell script to be processed as batch job"),
-    python_var=_("set Python interpreter name to override 'python'"),
-    exec_=_("execute GRASS module or script"),
-    exec_detail=_("provided executable will be executed in GRASS session"),
-    executable=_("GRASS module, script or any other executable"),
-    executable_params=_("parameters of the executable"),
-    standard_flags=_("standard flags"),
-    tmp_location=_("create temporary location (use with the --exec flag)"),
-    )
+"""
 
 
 def help_message(default_gui):
-    t = string.Template(help_text)
-    s = t.substitute(CMD_NAME=cmd_name, DEFAULT_GUI=default_gui,
-                     VERSION_NUMBER=grass_version)
+    t = string.Template(
+        HELP_TEXT.format(
+            usage=_("Usage"),
+            flags=_("Flags"),
+            help_flag=_("print this help message"),
+            version_flag=_("show version information and exit"),
+            create=_("create given database, location or mapset if it doesn't exist"),
+            exit_after=_("exit after creation of location or mapset. Only with -c flag"),
+            force_removal=_("force removal of .gislock if exists (use with care!). Only with --text flag"),
+            text=_("use text based interface (skip graphical welcome screen)"),
+            text_detail=_("and set as default"),
+            gtext=_("use text based interface (show graphical welcome screen)"),
+            gtext_detail=_("and set as default"),
+            gui=_("use $DEFAULT_GUI graphical user interface"),
+            gui_detail=_("and set as default"),
+            config=_("print GRASS configuration parameters"),
+            config_detail=_("options: arch,build,compiler,path,revision,svn_revision,version"),
+            params=_("Parameters"),
+            gisdbase=_("initial GRASS database directory"),
+            gisdbase_detail=_("directory containing Locations"),
+            location=_("initial GRASS Location"),
+            location_detail=_("directory containing Mapsets with one common coordinate system (projection)"),
+            mapset=_("initial GRASS Mapset"),
+            full_mapset=_("fully qualified initial Mapset directory"),
+            env_vars=_("Environment variables relevant for startup"),
+            gui_var=_("select GUI (text, gui, gtext)"),
+            html_var=_("set html web browser for help pages"),
+            addon_path_var=_("set additional path(s) to local GRASS modules or user scripts"),
+            addon_base_var=_("set additional GISBASE for locally installed GRASS Addons"),
+            batch_var=_("shell script to be processed as batch job"),
+            python_var=_("set Python interpreter name to override 'python'"),
+            exec_=_("execute GRASS module or script"),
+            exec_detail=_("provided executable will be executed in GRASS session"),
+            executable=_("GRASS module, script or any other executable"),
+            executable_params=_("parameters of the executable"),
+            standard_flags=_("standard flags"),
+            tmp_location=_("create temporary location (use with the --exec flag)"),
+        )
+    )
+    s = t.substitute(CMD_NAME=CMD_NAME, DEFAULT_GUI=default_gui,
+                     VERSION_NUMBER=GRASS_VERSION)
     sys.stderr.write(s)
 
 
@@ -434,12 +385,12 @@ def get_grass_config_dir():
     else:
         grass_config_dirname = ".grass7"
         directory = os.path.join(os.getenv('HOME'), grass_config_dirname)
-    if not os.path.exists(directory):
+    if not os.path.isdir(directory) :
         try:
             os.mkdir(directory)
         except OSError as e:
             # Can happen as a race condition
-            if not e.errno == 17:
+            if not e.errno == errno.EEXIST or not os.path.isdir(directory):
                 fatal(
                     _("Failed to create configuration directory '%s' with error: %s")
                     % (directory, e.strerror))
@@ -629,12 +580,12 @@ def set_paths(grass_config_dir):
     if not addon_base:
         addon_base = os.path.join(grass_config_dir, 'addons')
         os.environ['GRASS_ADDON_BASE'] = addon_base
-    if not windows:
+    if not WINDOWS:
         path_prepend(os.path.join(addon_base, 'scripts'), 'PATH')
     path_prepend(os.path.join(addon_base, 'bin'), 'PATH')
 
     # standard installation
-    if not windows:
+    if not WINDOWS:
         path_prepend(gpath('scripts'), 'PATH')
     path_prepend(gpath('bin'), 'PATH')
 
@@ -676,7 +627,7 @@ def set_paths(grass_config_dir):
 
     # Set LD_LIBRARY_PATH (etc) to find GRASS shared libraries
     # this works for subprocesses but won't affect the current process
-    path_prepend(gpath("lib"), ld_library_path_var)
+    path_prepend(gpath("lib"), LD_LIBRARY_PATH_VAR)
 
 
 def find_exe(pgm):
@@ -694,7 +645,7 @@ def set_defaults():
             pager = "more"
         elif find_exe("less"):
             pager = "less"
-        elif windows:
+        elif WINDOWS:
             pager = "more"
         else:
             pager = "cat"
@@ -702,7 +653,7 @@ def set_defaults():
 
     # GRASS_PYTHON
     if not os.getenv('GRASS_PYTHON'):
-        if windows:
+        if WINDOWS:
             os.environ['GRASS_PYTHON'] = "python.exe"
         else:
             os.environ['GRASS_PYTHON'] = "python"
@@ -713,7 +664,7 @@ def set_defaults():
 
     # GRASS_PROJSHARE
     if not os.getenv('GRASS_PROJSHARE'):
-        os.environ['GRASS_PROJSHARE'] = config_projshare
+        os.environ['GRASS_PROJSHARE'] = CONFIG_PROJSHARE
 
 
 def set_display_defaults():
@@ -728,15 +679,15 @@ def set_browser():
     # GRASS_HTML_BROWSER
     browser = os.getenv('GRASS_HTML_BROWSER')
     if not browser:
-        if macosx:
+        if MACOSX:
             # OSX doesn't execute browsers from the shell PATH - route through a
             # script
             browser = gpath('etc', "html_browser_mac.sh")
             os.environ['GRASS_HTML_BROWSER_MACOSX'] = "-b com.apple.helpviewer"
 
-        if windows:
+        if WINDOWS:
             browser = "start"
-        elif cygwin:
+        elif CYGWIN:
             browser = "explorer"
         else:
             # the usual suspects
@@ -748,7 +699,7 @@ def set_browser():
                     browser = b
                     break
 
-    elif macosx:
+    elif MACOSX:
         # OSX doesn't execute browsers from the shell PATH - route through a
         # script
         os.environ['GRASS_HTML_BROWSER_MACOSX'] = "-b %s" % browser
@@ -763,7 +714,7 @@ def set_browser():
 
 def ensure_home():
     """Set HOME if not set on MS Windows"""
-    if windows and not os.getenv('HOME'):
+    if WINDOWS and not os.getenv('HOME'):
         os.environ['HOME'] = os.path.join(os.getenv('HOMEDRIVE'),
                                           os.getenv('HOMEPATH'))
 
@@ -780,7 +731,7 @@ MAPSET: <UNKNOWN>
 def check_gui(expected_gui):
     grass_gui = expected_gui
     # Check if we are running X windows by checking the DISPLAY variable
-    if os.getenv('DISPLAY') or windows or macosx:
+    if os.getenv('DISPLAY') or WINDOWS or MACOSX:
         # Check if python is working properly
         if expected_gui in ('wxpython', 'gtext'):
             nul = open(os.devnull, 'w')
@@ -1139,7 +1090,7 @@ def gui_startup(grass_gui):
                 "Use '--help' for further options\n"
                 "     {cmd_name} --help\n"
                 "See also: https://grass.osgeo.org/{cmd_name}/manuals/helptext.html").format(
-                    cmd_name=cmd_name))
+                    cmd_name=CMD_NAME))
     elif ret == 5:  # defined in gui/wxpython/gis_set.py
         # User wants to exit from GRASS
         message(_("Exit was requested in GUI.\nGRASS GIS will not start. Bye."))
@@ -1193,11 +1144,11 @@ def load_gisrc(gisrc, gisrcrc):
     mapset_settings.mapset = kv.get('MAPSET')
     if not mapset_settings.is_valid():
         fatal(_("Error reading data path information from g.gisenv.\n"
-                "GISDBASE={gisbase}\n"
+                "GISDBASE={gisdbase}\n"
                 "LOCATION_NAME={location}\n"
                 "MAPSET={mapset}\n\n"
                 "Check the <{file}> file.").format(
-                    gisbase=mapset_settings.gisdbase,
+                    gisdbase=mapset_settings.gisdbase,
                     location=mapset_settings.location,
                     mapset=mapset_settings.mapset,
                     file=gisrcrc))
@@ -1481,7 +1432,7 @@ def ensure_db_connected(mapset):
 def get_shell():
     # set SHELL on ms windowns
     # this was at the very beginning of the script but it can be anywhere
-    if windows:
+    if WINDOWS:
         if os.getenv('GRASS_SH'):
             os.environ['SHELL'] = os.getenv('GRASS_SH')
         if not os.getenv('SHELL'):
@@ -1490,7 +1441,7 @@ def get_shell():
     # cygwin has many problems with the shell setup
     # below, so i hardcoded everything here.
     if sys.platform == 'cygwin':
-        sh = "cygwin"
+        sh = "CYGWIN"
         shellname = "GNU Bash (Cygwin)"
         os.environ['SHELL'] = "/usr/bin/bash.exe"
         os.environ['OSTYPE'] = "cygwin"
@@ -1503,7 +1454,7 @@ def get_shell():
             sh = 'sh'
             os.environ['SHELL'] = sh
 
-        if windows and sh:
+        if WINDOWS and sh:
             sh = os.path.splitext(sh)[0]
 
         if sh == "ksh":
@@ -1578,11 +1529,11 @@ def run_batch_job(batch_job):
     :param batch_job: executable and parameters in a list or a string
     """
     batch_job_string = batch_job
-    if not isinstance(batch_job, string_types):
+    if not isinstance(batch_job, six.string_types):
         # for messages only
         batch_job_string = ' '.join(batch_job)
     message(_("Executing <%s> ...") % batch_job_string)
-    if isinstance(batch_job, string_types):
+    if isinstance(batch_job, six.string_types):
         # shell=True is keeping the original GRASS_BATCH_JOB behavior
         def quote(string):
             if '"' in string:
@@ -1622,7 +1573,6 @@ def close_gui():
     env = gcore.gisenv()
     if 'GUI_PID' not in env:
         return
-    import signal
     for pid in env['GUI_PID'].split(','):
         debug("Exiting GUI with pid={0}".format(pid))
         try:
@@ -1645,8 +1595,8 @@ def show_banner():
 
 def say_hello():
     """Write welcome to stderr including Subversion revision if in svn copy"""
-    sys.stderr.write(_("Welcome to GRASS GIS %s") % grass_version)
-    if grass_version.endswith('svn'):
+    sys.stderr.write(_("Welcome to GRASS GIS %s") % GRASS_VERSION)
+    if GRASS_VERSION.endswith('svn'):
         try:
             filerev = open(gpath('etc', 'VERSIONNUMBER'))
             linerev = filerev.readline().rstrip('\n')
@@ -1658,22 +1608,25 @@ def say_hello():
             pass
 
 
-def show_info(shellname, grass_gui, default_gui):
-    """Write basic info about GRASS GIS and GRASS session to stderr"""
-    sys.stderr.write(
-r"""
+INFO_TEXT = r"""\
 %-41shttps://grass.osgeo.org
 %-41s%s (%s)
 %-41sg.manual -i
 %-41sg.version -c
 %-41sg.version -x
-""" % (_("GRASS GIS homepage:"),
+"""
+
+
+def show_info(shellname, grass_gui, default_gui):
+    """Write basic info about GRASS GIS and GRASS session to stderr"""
+    sys.stderr.write(INFO_TEXT % (
+        _("GRASS GIS homepage:"),
         # GTC Running through: SHELL NAME
-       _("This version running through:"),
-       shellname, os.getenv('SHELL'),
-       _("Help is available with the command:"),
-       _("See the licence terms with:"),
-       _("See citation options with:")))
+        _("This version running through:"),
+        shellname, os.getenv('SHELL'),
+        _("Help is available with the command:"),
+        _("See the licence terms with:"),
+        _("See citation options with:")))
 
     if grass_gui == 'wxpython':
         message("%-41sg.gui wxpython" % _("If required, restart the GUI with:"))
@@ -1702,7 +1655,7 @@ def csh_startup(location, location_name, mapset, grass_env_file):
 
     f.write("set prompt = '\\\n")
     f.write("Mapset <%s> in Location <%s> \\\n" % (mapset, location_name))
-    f.write("GRASS GIS %s > '\n" % grass_version)
+    f.write("GRASS GIS %s > '\n" % GRASS_VERSION)
     f.write("set BOGUS=``;unset BOGUS\n")
 
     # csh shell rc file left for backward compatibility
@@ -1758,8 +1711,8 @@ def bash_startup(location, location_name, grass_env_file):
         grass_name = "ISIS-GRASS"
     else:
         grass_name = "GRASS"
-    f.write("PS1='{name} {version} ({location}):\\W > '\n".format(
-        name=grass_name, version=grass_version, location=location_name))
+    f.write("PS1='{name} {version} ({location}):\\w > '\n".format(
+        name=grass_name, version=GRASS_VERSION, location=location_name))
 
     # TODO: have a function and/or module to test this
     mask2d_test = 'test -f "$MAPSET_PATH/cell/MASK"'
@@ -1795,7 +1748,7 @@ PROMPT_COMMAND=grass_prompt\n""".format(
         for line in readfile(env_file).splitlines():
             # Bug related to OS X "SIP", see
             # https://trac.osgeo.org/grass/ticket/3462#comment:13
-            if macosx or not line.startswith('export'):
+            if MACOSX or not line.startswith('export'):
                 f.write(line + '\n')
 
     f.write("export PATH=\"%s\"\n" % os.getenv('PATH'))
@@ -1808,12 +1761,12 @@ PROMPT_COMMAND=grass_prompt\n""".format(
 
 
 def default_startup(location, location_name):
-    if windows:
-        os.environ['PS1'] = "GRASS %s> " % (grass_version)
+    if WINDOWS:
+        os.environ['PS1'] = "GRASS %s> " % (GRASS_VERSION)
         # "$ETC/run" doesn't work at all???
         process = subprocess.Popen([os.getenv('SHELL')])
     else:
-        os.environ['PS1'] = "GRASS %s (%s):\\W > " % (grass_version, location_name)
+        os.environ['PS1'] = "GRASS %s (%s):\\w > " % (GRASS_VERSION, location_name)
         process = Popen([gpath("etc", "run"), os.getenv('SHELL')])
 
     return process
@@ -1842,7 +1795,7 @@ def clean_all():
     clean_temp()
     # save 'last used' GISRC after removing variables which shouldn't
     # be saved, e.g. d.mon related
-    clean_env(os.environ['GISRC'])
+    clean_env()
 
 
 def grep(pattern, lines):
@@ -1857,20 +1810,24 @@ def grep(pattern, lines):
 
 def print_params():
     """Write compile flags and other configuration to stderr"""
-    plat = gpath('include', 'Make', 'Platform.make')
-    if not os.path.exists(plat):
-        fatal(_("Please install the GRASS GIS development package"))
-    fileplat = open(plat)
-    linesplat = fileplat.readlines()
-    fileplat.close()
-
     params = sys.argv[2:]
     if not params:
         params = ['arch', 'build', 'compiler', 'path', 'revision', 'version']
 
+    # check if we are dealing with parameters which require dev files
+    dev_params = ["arch", "compiler", "build", "revision"]
+    if any([param in dev_params for param in params]):
+        plat = gpath('include', 'Make', 'Platform.make')
+        if not os.path.exists(plat):
+            fatal(_("Please install the GRASS GIS development package"))
+        fileplat = open(plat)
+        # this is in fact require only for some, but prepare it anyway
+        linesplat = fileplat.readlines()
+        fileplat.close()
+
     for arg in params:
         if arg == 'path':
-            sys.stdout.write("%s\n" % gisbase)
+            sys.stdout.write("%s\n" % GISBASE)
         elif arg == 'arch':
             val = grep('ARCH', linesplat)
             sys.stdout.write("%s\n" % val[0].split('=')[1].strip())
@@ -1901,14 +1858,14 @@ def print_params():
             except:
                sys.stdout.write("No SVN revision defined\n")
         elif arg == 'version':
-            sys.stdout.write("%s\n" % grass_version)
+            sys.stdout.write("%s\n" % GRASS_VERSION)
         else:
             message(_("Parameter <%s> not supported") % arg)
 
 
 def get_username():
     """Get name of the current user"""
-    if windows:
+    if WINDOWS:
         user = os.getenv('USERNAME')
         if not user:
             user = "user_name"
@@ -1952,7 +1909,7 @@ def parse_cmdline(argv, default_gui):
     for i in argv:
         # Check if the user asked for the version
         if i in ["-v", "--version"]:
-            message("GRASS GIS %s" % grass_version)
+            message("GRASS GIS %s" % GRASS_VERSION)
             message('\n' + readfile(gpath("etc", "license")))
             sys.exit()
         # Check if the user asked for help
@@ -1998,24 +1955,24 @@ def parse_cmdline(argv, default_gui):
     return params
 
 
-# The main script starts here
-
-# Get the system name
-windows = sys.platform == 'win32'
-cygwin = "cygwin" in sys.platform
-macosx = "darwin" in sys.platform
-
-# TODO: it is OK to remove this?
-# at the beginning of this file were are happily getting GISBASE
-# from the environment and we don't care about inconsistencies it might cause
-# The following was commented out because of breaking winGRASS
-# if 'GISBASE' in os.environ:
-#     sys.exit(_("ERROR: GRASS GIS is already running "
-#                "(environmental variable GISBASE found)"))
-# this is not really an issue, we should be able to overpower another session
-
-# Set GISBASE
-os.environ['GISBASE'] = gisbase
+def validate_cmdline(params):
+    """ Validate the cmdline params and exit if necessary. """
+    if params.exit_grass and not params.create_new:
+        fatal(_("Flag -e requires also flag -c"))
+    if params.tmp_location and not params.geofile:
+        fatal(
+            _(
+                "Coordinate reference system argument (e.g. EPSG)"
+                " is needed for --tmp-location"
+            )
+        )
+    if params.tmp_location and params.mapset:
+        fatal(
+            _(
+                "Only one argument (e.g. EPSG) is needed for"
+                " --tmp-location, mapset name <{}> provided"
+            ).format(params.mapset)
+        )
 
 
 def main():
@@ -2023,6 +1980,13 @@ def main():
 
     Only few things are set on the module level.
     """
+    # Set language
+    # This has to be called before any _() function call!
+    # Subsequent functions are using _() calls and
+    # thus must be called only after Language has been set.
+    grass_config_dir = get_grass_config_dir()
+    set_language(grass_config_dir)
+
     # Set default GUI
     default_gui = "wxpython"
 
@@ -2034,14 +1998,12 @@ def main():
 
     # Set GRASS version number for R interface etc
     # (must be an env var for MS Windows)
-    os.environ['GRASS_VERSION'] = grass_version
+    os.environ['GRASS_VERSION'] = GRASS_VERSION
 
     # Set the GIS_LOCK variable to current process id
     gis_lock = str(os.getpid())
     os.environ['GIS_LOCK'] = gis_lock
 
-    grass_config_dir = get_grass_config_dir()
-
     batch_job = get_batch_job_from_env_variable()
 
     # Parse the command-line options and set several global variables
@@ -2054,15 +2016,7 @@ def main():
         params = parse_cmdline(clean_argv, default_gui=default_gui)
     except ValueError:
         params = parse_cmdline(sys.argv[1:], default_gui=default_gui)
-    if params.exit_grass and not params.create_new:
-        fatal(_("Flag -e requires also flag -c"))
-    if params.tmp_location and not params.geofile:
-        fatal(_("Coordinate reference system argument (e.g. EPSG)"
-                " is needed for --tmp-location"))
-    if params.tmp_location and params.mapset:
-        fatal(_("Only one argument (e.g. EPSG) is needed for"
-                " --tmp-location, mapset name <{}> provided").format(
-                    params.mapset))
+    validate_cmdline(params)
     # For now, we allow, but not advertise/document, --tmp-location
     # without --exec (usefulness to be evaluated).
 
@@ -2076,13 +2030,6 @@ def main():
     # Set the username
     user = get_username()
 
-    # TODO: this might need to be moved before processing of parameters
-    # and getting batch job
-    # Set language
-    # This has to be called before any _() function call!
-    # Subsequent functions are using _() calls and
-    # thus must be called only after Language has been set.
-    set_language(grass_config_dir)
 
     # Set shell (needs to be called before load_env())
     sh, shellname = get_shell()
@@ -2095,10 +2042,9 @@ def main():
     # Create the temporary directory and session grassrc file
     tmpdir = create_tmp(user, gis_lock)
 
-    cleaner = Cleaner()
-    cleaner.tmpdir = tmpdir
-    # object is not destroyed when its method is registered
-    atexit.register(cleaner.cleanup)
+    # Remove the tmpdir
+    # The removal will be executed when the python process terminates.
+    atexit.register(lambda: shutil.rmtree(tmpdir, ignore_errors=True))
 
     # Create the session grassrc file
     gisrc = create_gisrc(tmpdir, gisrcrc)
@@ -2130,7 +2076,7 @@ def main():
                     " - Use '--help' for further options\n"
                     "     {cmd_name} --help\n"
                     "See also: https://grass.osgeo.org/{cmd_name}/manuals/helptext.html").format(
-                        cmd_name=cmd_name, gisrcrc=gisrcrc))
+                        cmd_name=CMD_NAME, gisrcrc=gisrcrc))
         create_initial_gisrc(gisrc)
 
     message(_("Starting GRASS GIS..."))

+ 6 - 5
lib/python/script/task.py

@@ -445,7 +445,7 @@ def convert_xml_to_utf8(xml_text):
 
     # modify: fetch encoding from the interface description text(xml)
     # e.g. <?xml version="1.0" encoding="GBK"?>
-    pattern = re.compile('<\?xml[^>]*\Wencoding="([^"]*)"[^>]*\?>')
+    pattern = re.compile(b'<\?xml[^>]*\Wencoding="([^"]*)"[^>]*\?>')
     m = re.match(pattern, xml_text)
     if m is None:
         return xml_text.encode("utf-8") if xml_text else None
@@ -453,10 +453,11 @@ def convert_xml_to_utf8(xml_text):
     enc = m.groups()[0]
 
     # modify: change the encoding to "utf-8", for correct parsing
-    p = re.compile('encoding="' + enc + '"', re.IGNORECASE)
-    xml_text_utf8 = p.sub('encoding="utf-8"', xml_text)
+    xml_text_utf8 = xml_text.decode(enc.decode('ascii')).encode("utf-8")
+    p = re.compile(b'encoding="' + enc + b'"', re.IGNORECASE)
+    xml_text_utf8 = p.sub(b'encoding="utf-8"', xml_text_utf8)
 
-    return xml_text_utf8.encode("utf-8")
+    return xml_text_utf8
 
 
 def get_interface_description(cmd):
@@ -500,7 +501,7 @@ def get_interface_description(cmd):
         raise ScriptError(_("Unable to fetch interface description for command '<{cmd}>'."
                             "\n\nDetails: <{det}>").format(cmd=cmd, det=e))
 
-    desc = convert_xml_to_utf8(decode(cmdout))
+    desc = convert_xml_to_utf8(cmdout)
     desc = desc.replace(b'grass-interface.dtd',
                         os.path.join(os.getenv('GISBASE'), 'gui', 'xml',
                                      'grass-interface.dtd').encode('utf-8'))

+ 0 - 1
lib/python/temporal/core.py

@@ -1319,7 +1319,6 @@ class DBConnection(object):
 
            :param statement: The executable SQL statement or SQL script
         """
-        print (statement)
         connected = False
         if not self.connected:
             self.connect()

+ 3 - 1
man/Makefile

@@ -47,8 +47,10 @@ default: $(DSTFILES)
 
 # This must be a separate target so that evaluation of $(MANPAGES)
 # is delayed until the indices have been generated
+left := (
+right := )
 manpages:
-	$(MAKE) $(MANPAGES)
+	$(MAKE) $(subst $(left),\$(left),$(subst $(right),\$(right),$(MANPAGES)))
 
 .PHONY: manpages
 

+ 1 - 1
scripts/g.extension/g.extension.py

@@ -1023,7 +1023,7 @@ def install_extension_win(name):
                "grass-%(major)s.%(minor)s.%(patch)s" % \
                {'platform': platform,
                 'major': version[0], 'minor': version[1],
-                'patch': version[2]}
+                'patch': 'dev'}
 
     # resolve ZIP URL
     source, url = resolve_source_code(url='{0}/{1}.zip'.format(base_url, name))

+ 1 - 1
tools/mkhtml.py

@@ -381,7 +381,7 @@ if sys.platform == 'win32':
 
 if index_name:
     sys.stdout.write(sourcecode.substitute(URL_SOURCE=url_source, PGM=pgm,
-                                           URL_LOG=url_source.replace('browser',  'log')))
+                                           URL_LOG=url_source.replace('grass/tree',  'grass/commits')))
     sys.stdout.write(footer_index.substitute(INDEXNAME=index_name,
                                              INDEXNAMECAP=index_name_cap,
                                              YEAR=year, GRASS_VERSION=grass_version))

+ 3 - 1
vector/v.in.ascii/v.in.ascii.html

@@ -103,7 +103,9 @@ cut -d&lt;the_field_separator_character&gt; -f&lt;comma-separated_list_of_column
 
 <h3>Example 1a) - standard format mode</h3>
 Sample ASCII polygon vector map for 'standard' format mode. 
-The two areas will be assigned categories 20 and 21. 
+The two areas will be assigned categories 20 and 21. For details on the structure of
+standard format data files see the second reference at the bottom of this page.
+
 <p><div class="code"><pre>
 echo "ORGANIZATION: GRASS Development Team
 DIGIT DATE:   1/9/2005