Browse Source

wxGUI/datacatalog: Add new location action to database node in Data tab (#790)

* After clicking on database node, two options for creating new location are available (new empty and download).
* Adds standalone functions download_location_interactively(), create_location_interactively(), and import_file().
* Modernizes and clarifies new location name validation in location wizard.
Linda Kladivova 4 years ago
parent
commit
bccf537ecc

+ 41 - 4
gui/wxpython/datacatalog/tree.py

@@ -35,10 +35,12 @@ from gui_core.wrap import Menu
 from datacatalog.dialogs import CatalogReprojectionDialog
 from icons.icon import MetaIcon
 from startup.guiutils import (create_mapset_interactively,
+                              create_location_interactively,
                               rename_mapset_interactively,
                               rename_location_interactively,
                               delete_mapset_interactively,
-                              delete_location_interactively)
+                              delete_location_interactively,
+                              download_location_interactively)
 
 from grass.pydispatch.signal import Signal
 
@@ -675,8 +677,8 @@ class DataCatalogTree(TreeView):
         location = self.selected_location[0]
         try:
             mapset = create_mapset_interactively(self,
-                                        gisdbase.data['name'],
-                                        location.data['name'],)
+                                                 gisdbase.data['name'],
+                                                 location.data['name'])
             if mapset:
                 self.InsertMapset(name=mapset,
                                   location_node=location)
@@ -686,6 +688,20 @@ class DataCatalogTree(TreeView):
                    message=_("Unable to create new mapset: %s") % e,
                    showTraceback=False)
 
+    def OnCreateLocation(self, event):
+        """
+        Location wizard started
+        """
+        grassdatabase, location, mapset = (
+            create_location_interactively(self,
+                                          self.selected_grassdb[0].data['name'])
+        )
+        if location is not None:
+            item = self._model.SearchNodes(name=grassdatabase, type='grassdb')
+            if not item:
+                self.InsertGrassDb(name=grassdatabase)
+            self.ReloadTreeItems()
+
     def OnRenameMapset(self, event):
         """
         Rename selected mapset
@@ -972,8 +988,20 @@ class DataCatalogTree(TreeView):
                    message=_("Unable to delete location: %s") % e,
                    showTraceback=False)
 
+    def OnDownloadLocation(self, event):
+        """
+        Download location online
+        """
+        grassdatabase, location, mapset = download_location_interactively(
+                self, self.selected_grassdb[0].data['name']
+        )
+        if location:
+            self.ReloadTreeItems()
+
     def OnDisplayLayer(self, event):
-        """Display layer in current graphics view"""
+        """
+        Display layer in current graphics view
+        """
         self.DisplayLayer()
 
     def DisplayLayer(self):
@@ -1299,6 +1327,15 @@ class DataCatalogTree(TreeView):
     def _popupMenuGrassDb(self):
         """Create popup menu for grass db"""
         menu = Menu()
+
+        item = wx.MenuItem(menu, wx.ID_ANY, _("&Create new location"))
+        menu.AppendItem(item)
+        self.Bind(wx.EVT_MENU, self.OnCreateLocation, item)
+
+        item = wx.MenuItem(menu, wx.ID_ANY, _("&Download sample location"))
+        menu.AppendItem(item)
+        self.Bind(wx.EVT_MENU, self.OnDownloadLocation, item)
+
         self.PopupMenu(menu)
         menu.Destroy()
 

+ 28 - 81
gui/wxpython/gis_set.py

@@ -32,19 +32,20 @@ from core import globalvar
 import wx
 import wx.lib.mixins.listctrl as listmix
 
-from core.gcmd import GMessage, GError, RunCommand
+from core.gcmd import GError, RunCommand
 from core.utils import GetListOfLocations, GetListOfMapsets
 from startup.utils import (
     get_lockfile_if_present, get_possible_database_path,
     create_database_directory)
 from startup.guiutils import (SetSessionMapset,
                               create_mapset_interactively,
+                              create_location_interactively,
                               rename_mapset_interactively,
                               rename_location_interactively,
                               delete_mapset_interactively,
-                              delete_location_interactively)
+                              delete_location_interactively,
+                              download_location_interactively)
 import startup.guiutils as sgui
-from location_wizard.dialogs import RegionDef
 from gui_core.widgets import StaticWrapText
 from gui_core.wrap import Button, ListCtrl, StaticText, StaticBox, \
     TextCtrl, BitmapFromImage
@@ -231,7 +232,7 @@ class GRASSStartup(wx.Frame):
         self.bexit.Bind(wx.EVT_BUTTON, self.OnExit)
         self.bhelp.Bind(wx.EVT_BUTTON, self.OnHelp)
         self.bmapset.Bind(wx.EVT_BUTTON, self.OnCreateMapset)
-        self.bwizard.Bind(wx.EVT_BUTTON, self.OnWizard)
+        self.bwizard.Bind(wx.EVT_BUTTON, self.OnCreateLocation)
 
         self.rename_location_button.Bind(wx.EVT_BUTTON, self.OnRenameLocation)
         self.delete_location_button.Bind(wx.EVT_BUTTON, self.OnDeleteLocation)
@@ -535,76 +536,23 @@ class GRASSStartup(wx.Frame):
                 'your home directory. '
                 'Press Browse button to select the directory.'))
 
-    def OnWizard(self, event):
+    def OnCreateLocation(self, event):
         """Location wizard started"""
-        from location_wizard.wizard import LocationWizard
-        gWizard = LocationWizard(parent=self,
-                                 grassdatabase=self.tgisdbase.GetValue())
-        if gWizard.location is not None:
-            self.tgisdbase.SetValue(gWizard.grassdatabase)
+        grassdatabase, location, mapset = (
+            create_location_interactively(self, self.gisdbase)
+        )
+        if location is not None:
+            self.OnSelectLocation(None)
+            self.lbmapsets.SetSelection(self.listOfMapsets.index(mapset))
+            self.bstart.SetFocus()
+            self.tgisdbase.SetValue(grassdatabase)
             self.OnSetDatabase(None)
-            self.UpdateMapsets(os.path.join(self.gisdbase, gWizard.location))
+            self.UpdateMapsets(os.path.join(grassdatabase, location))
             self.lblocations.SetSelection(
-                self.listOfLocations.index(
-                    gWizard.location))
+                self.listOfLocations.index(location))
             self.lbmapsets.SetSelection(0)
-            self.SetLocation(self.gisdbase, gWizard.location, 'PERMANENT')
-            if gWizard.georeffile:
-                message = _("Do you want to import <%(name)s> to the newly created location?") % {
-                    'name': gWizard.georeffile}
-                dlg = wx.MessageDialog(parent=self, message=message, caption=_(
-                    "Import data?"), style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
-                dlg.CenterOnParent()
-                if dlg.ShowModal() == wx.ID_YES:
-                    self.ImportFile(gWizard.georeffile)
-                dlg.Destroy()
-            if gWizard.default_region:
-                defineRegion = RegionDef(self, location=gWizard.location)
-                defineRegion.CenterOnParent()
-                defineRegion.ShowModal()
-                defineRegion.Destroy()
-
-            if gWizard.user_mapset:
-                self.OnCreateMapset(event)
-
-    def ImportFile(self, filePath):
-        """Tries to import file as vector or raster.
-
-        If successfull sets default region from imported map.
-        """
-        RunCommand('db.connect', flags='c')
-        mapName = os.path.splitext(os.path.basename(filePath))[0]
-        vectors = RunCommand('v.in.ogr', input=filePath, flags='l',
-                             read=True)
-
-        wx.BeginBusyCursor()
-        wx.GetApp().Yield()
-        if vectors:
-            # vector detected
-            returncode, error = RunCommand(
-                'v.in.ogr', input=filePath, output=mapName, flags='e',
-                getErrorMsg=True)
-        else:
-            returncode, error = RunCommand(
-                'r.in.gdal', input=filePath, output=mapName, flags='e',
-                getErrorMsg=True)
-        wx.EndBusyCursor()
+            self.SetLocation(grassdatabase, location, mapset)
 
-        if returncode != 0:
-            GError(
-                parent=self,
-                message=_(
-                    "Import of <%(name)s> failed.\n"
-                    "Reason: %(msg)s") % ({
-                        'name': filePath,
-                        'msg': error}))
-        else:
-            GMessage(
-                message=_(
-                    "Data file <%(name)s> imported successfully. "
-                    "The location's default region was set from this imported map.") % {
-                    'name': filePath},
-                parent=self)
 
     # the event can be refactored out by using lambda in bind
     def OnRenameMapset(self, event):
@@ -618,7 +566,7 @@ class GRASSStartup(wx.Frame):
             if newmapset:
                 self.OnSelectLocation(None)
                 self.lbmapsets.SetSelection(
-                        self.listOfMapsets.index(newmapset))
+                    self.listOfMapsets.index(newmapset))
         except Exception as e:
             GError(parent=self,
                    message=_("Unable to rename mapset: %s") % e,
@@ -634,7 +582,7 @@ class GRASSStartup(wx.Frame):
             if newlocation:
                 self.UpdateLocations(self.gisdbase)
                 self.lblocations.SetSelection(
-                        self.listOfLocations.index(newlocation))
+                    self.listOfLocations.index(newlocation))
                 self.UpdateMapsets(newlocation)
         except Exception as e:
             GError(parent=self,
@@ -674,24 +622,23 @@ class GRASSStartup(wx.Frame):
                    showTraceback=False)
 
     def OnDownloadLocation(self, event):
-        """Download location online"""
-        from startup.locdownload import LocationDownloadDialog
-
-        loc_download = LocationDownloadDialog(parent=self, database=self.gisdbase)
-        loc_download.ShowModal()
-        location = loc_download.GetLocation()
+        """
+        Download location online
+        """
+        grassdatabase, location, mapset = download_location_interactively(
+            self, self.gisdbase
+        )
         if location:
             # get the new location to the list
-            self.UpdateLocations(self.gisdbase)
+            self.UpdateLocations(grassdatabase)
             # seems to be used in similar context
-            self.UpdateMapsets(os.path.join(self.gisdbase, location))
+            self.UpdateMapsets(os.path.join(grassdatabase, location))
             self.lblocations.SetSelection(
                 self.listOfLocations.index(location))
             # wizard does this as well, not sure if needed
-            self.SetLocation(self.gisdbase, location, 'PERMANENT')
+            self.SetLocation(grassdatabase, location, mapset)
             # seems to be used in similar context
             self.OnSelectLocation(None)
-        loc_download.Destroy()
 
     def UpdateLocations(self, dbase):
         """Update list of locations"""

+ 27 - 36
gui/wxpython/location_wizard/wizard.py

@@ -53,10 +53,11 @@ import wx.lib.scrolledpanel as scrolled
 from core import utils
 from core.utils import cmp
 from core.gcmd import RunCommand, GError, GMessage, GWarning
-from gui_core.widgets import GenericValidator
+from gui_core.widgets import GenericMultiValidator
 from gui_core.wrap import SpinCtrl, SearchCtrl, StaticText, \
     TextCtrl, Button, CheckBox, StaticBox, NewId, ListCtrl, HyperlinkCtrl
 from location_wizard.dialogs import SelectTransformDialog
+from startup.utils import location_exists
 
 from grass.script import decode
 from grass.script import core as grass
@@ -171,10 +172,11 @@ class DatabasePage(TitledPage):
         self.tgisdbase = self.MakeLabel(grassdatabase)
         self.tlocation = self.MakeTextCtrl("newLocation", size=(400, -1))
         self.tlocation.SetFocus()
+
+        checks = [(grass.legal_name, self._nameValidationFailed),
+                  (self._checkLocationNotExists, self._locationAlreadyExists)]
         self.tlocation.SetValidator(
-            GenericValidator(
-                grass.legal_name,
-                self._nameValidationFailed))
+            GenericMultiValidator(checks))
         self.tlocTitle = self.MakeTextCtrl(size=(400, -1))
 
         # text for required options
@@ -255,15 +257,23 @@ class DatabasePage(TitledPage):
 
     def _nameValidationFailed(self, ctrl):
         message = _(
-            "Name <%(name)s> is not a valid name for location. "
-            "Please use only ASCII characters excluding %(chars)s "
-            "and space.") % {
-            'name': ctrl.GetValue(),
-            'chars': '/"\'@,=*~'}
-        GError(
-            parent=self,
-            message=message,
-            caption=_("Invalid location name"))
+            "Name '{}' is not a valid name for location. "
+            "Please use only ASCII characters excluding characters {} "
+            "and space.").format(ctrl.GetValue(), '/"\'@,=*~')
+        GError(parent=self, message=message, caption=_("Invalid name"))
+
+    def _checkLocationNotExists(self, text):
+        """Check whether user's input location exists or not."""
+        if location_exists(self.tgisdbase.GetLabel(), text):
+            return False
+        return True
+
+    def _locationAlreadyExists(self, ctrl):
+        message = _(
+            "Location '{}' already exists. Please consider using "
+            "another name for your location.").format(ctrl.GetValue())
+        GError(parent=self, message=message,
+               caption=_("Existing location path"))
 
     def OnChangeName(self, event):
         """Name for new location was changed"""
@@ -287,21 +297,6 @@ class DatabasePage(TitledPage):
         dlg.Destroy()
 
     def OnPageChanging(self, event=None):
-        error = None
-        if os.path.isdir(
-            os.path.join(
-                self.tgisdbase.GetLabel(),
-                self.tlocation.GetValue())):
-            error = _("Location already exists in GRASS Database.")
-
-        if error:
-            GError(parent=self,
-                   message="%s <%s>.%s%s" % (_("Unable to create location"),
-                                             str(self.tlocation.GetValue()),
-                                             os.linesep,
-                                             error))
-            event.Veto()
-            return
 
         self.location = self.tlocation.GetValue()
         self.grassdatabase = self.tgisdbase.GetLabel()
@@ -600,7 +595,7 @@ class ItemList(ListCtrl,
         for column in columns:
             self.InsertColumn(i, column)
             i += 1
-        
+
         self.EnableAlternateRowColours()
 
         if self.sourceData:
@@ -1482,6 +1477,7 @@ class WKTPage(TitledPage):
             if not nextButton.IsEnabled():
                 nextButton.Enable()
 
+
 class EPSGPage(TitledPage):
     """Wizard page for selecting EPSG code for
     setting coordinate system parameters"""
@@ -1489,7 +1485,7 @@ class EPSGPage(TitledPage):
     def __init__(self, wizard, parent):
         TitledPage.__init__(self, wizard, _("Select CRS from a list"))
 
-        self.sizer = wx.BoxSizer(wx.VERTICAL)  
+        self.sizer = wx.BoxSizer(wx.VERTICAL)
         searchBoxSizer = wx.BoxSizer(wx.HORIZONTAL)
         epsglistBoxSizer = wx.BoxSizer(wx.HORIZONTAL)
         informationBoxSizer = wx.BoxSizer(wx.HORIZONTAL)
@@ -1982,7 +1978,7 @@ class CustomPage(TitledPage):
                 return
 
             # check for datum tranforms
-            # FIXME: -t flag is a hack-around for trac bug #1849          
+            # FIXME: -t flag is a hack-around for trac bug #1849
             ret, out, err = RunCommand('g.proj',
                                        read=True, getErrorMsg=True,
                                        proj4=self.customstring,
@@ -2550,11 +2546,6 @@ class LocationWizard(wx.Object):
                             database))
                     return None
 
-            # change to new GISDbase directory
-            RunCommand('g.gisenv',
-                       parent=self.wizard,
-                       set='GISDBASE=%s' % database)
-
             wx.MessageBox(
                 parent=self.wizard,
                 message=_(

+ 118 - 0
gui/wxpython/startup/guiutils.py

@@ -24,6 +24,7 @@ import grass.script as gs
 from core import globalvar
 from core.gcmd import GError, GMessage, DecodeString, RunCommand
 from gui_core.dialogs import TextEntryDialog
+from location_wizard.dialogs import RegionDef
 from gui_core.widgets import GenericMultiValidator
 from startup.utils import (create_mapset, delete_mapset, delete_location,
                            rename_mapset, rename_location, mapset_exists,
@@ -211,6 +212,59 @@ def create_mapset_interactively(guiparent, grassdb, location):
     return mapset
 
 
+def create_location_interactively(guiparent, grassdb):
+    """
+    Create new location using Location Wizard.
+
+    Returns tuple (database, location, mapset) where mapset is "PERMANENT"
+    by default or another mapset a user created and may want to switch to.
+    """
+    from location_wizard.wizard import LocationWizard
+
+    gWizard = LocationWizard(parent=guiparent,
+                             grassdatabase=grassdb)
+
+    if gWizard.location is None:
+        gWizard_output = (None, None, None)
+        # Returns Nones after Cancel
+        return gWizard_output
+
+    if gWizard.georeffile:
+        message = _(
+            "Do you want to import {}"
+            "to the newly created location?"
+        ).format(gWizard.georeffile)
+        dlg = wx.MessageDialog(parent=guiparent,
+                               message=message,
+                               caption=_("Import data?"),
+                               style=wx.YES_NO | wx.YES_DEFAULT |
+                               wx.ICON_QUESTION)
+        dlg.CenterOnParent()
+        if dlg.ShowModal() == wx.ID_YES:
+            import_file(guiparent, gWizard.georeffile)
+        dlg.Destroy()
+
+    if gWizard.default_region:
+        defineRegion = RegionDef(guiparent, location=gWizard.location)
+        defineRegion.CenterOnParent()
+        defineRegion.ShowModal()
+        defineRegion.Destroy()
+
+    if gWizard.user_mapset:
+        mapset = create_mapset_interactively(guiparent,
+                                                  gWizard.grassdatabase,
+                                                  gWizard.location)
+        # Returns database and location created by user
+        # and a mapset user may want to switch to
+        gWizard_output = (gWizard.grassdatabase, gWizard.location,
+                          mapset)
+    else:
+        # Returns PERMANENT mapset when user mapset not defined
+        gWizard_output = (gWizard.grassdatabase, gWizard.location,
+                          "PERMANENT")
+    return gWizard_output
+
+
 def rename_mapset_interactively(guiparent, grassdb, location, mapset):
     """
     Rename selected mapset
@@ -281,6 +335,29 @@ def rename_location_interactively(guiparent, grassdb, location):
     return newlocation
 
 
+def download_location_interactively(guiparent, grassdb):
+    """
+    Download new location using Location Wizard.
+
+    Returns tuple (database, location, mapset) where mapset is "PERMANENT"
+    by default or in future it could be the mapset the user may want to
+    switch to.
+    """
+    from startup.locdownload import LocationDownloadDialog
+
+    result = (None, None, None)
+    loc_download = LocationDownloadDialog(parent=guiparent,
+                                          database=grassdb)
+    loc_download.ShowModal()
+
+    if loc_download.GetLocation() is not None:
+        # Returns database and location created by user
+        # and a mapset user may want to switch to
+        result = (grassdb, loc_download.GetLocation(), "PERMANENT")
+    loc_download.Destroy()
+    return result
+
+
 def delete_mapset_interactively(guiparent, grassdb, location, mapset):
     """
     Delete selected mapset
@@ -355,3 +432,44 @@ def delete_location_interactively(guiparent, grassdb, location):
             )
     dlg.Destroy()
     return False
+
+
+def import_file(guiparent, filePath):
+    """Tries to import file as vector or raster.
+
+    If successfull sets default region from imported map.
+    """
+    RunCommand('db.connect', flags='c')
+    mapName = os.path.splitext(os.path.basename(filePath))[0]
+    vectors = RunCommand('v.in.ogr', input=filePath, flags='l',
+                         read=True)
+
+    wx.BeginBusyCursor()
+    wx.GetApp().Yield()
+    if vectors:
+        # vector detected
+        returncode, error = RunCommand(
+            'v.in.ogr', input=filePath, output=mapName, flags='e',
+            getErrorMsg=True)
+    else:
+        returncode, error = RunCommand(
+            'r.in.gdal', input=filePath, output=mapName, flags='e',
+            getErrorMsg=True)
+    wx.EndBusyCursor()
+
+    if returncode != 0:
+        GError(
+            parent=guiparent,
+            message=_(
+                "Import of <%(name)s> failed.\n"
+                "Reason: %(msg)s") % ({
+                    'name': filePath,
+                    'msg': error}))
+    else:
+        GMessage(
+            message=_(
+                "Data file <%(name)s> imported successfully. "
+                "The location's default region was set from "
+                "this imported map.") % {
+                'name': filePath},
+            parent=guiparent)