Procházet zdrojové kódy

wxGUI/datacatalog: change ReloadTreeItems to use node-specific reloads (#847)

The change is done in order to reload only nodes that are needed to be reloaded, e.g. when mapset is deleted, we don't need to reload entire tree including all dbs.

Includes fixes in treeview and treemodel.
Anna Petrasova před 4 roky
rodič
revize
939739c71c

+ 1 - 1
gui/wxpython/core/treemodel.py

@@ -117,7 +117,7 @@ class TreeModel(object):
     def GetIndexOfNode(self, node):
         """Method used for communication between view (VirtualTree) and model."""
         index = []
-        return self._getIndex(node, index)
+        return tuple(self._getIndex(node, index))
 
     def _getIndex(self, node, index):
         if node.parent:

+ 1 - 1
gui/wxpython/datacatalog/catalog.py

@@ -75,7 +75,7 @@ class DataCatalog(wx.Panel):
         if self._loaded:
             return
 
-        self.thread.Run(callable=self.tree.InitTreeItems,
+        self.thread.Run(callable=self.tree.ReloadTreeItems,
                         ondone=lambda event: self.LoadItemsDone())
 
     def LoadItemsDone(self):

+ 1 - 1
gui/wxpython/datacatalog/frame.py

@@ -52,7 +52,7 @@ class DataCatalogFrame(wx.Frame):
 
         # tree
         self.tree = DataCatalogTree(parent=self.panel, giface=self._giface)
-        self.tree.InitTreeItems()
+        self.tree.ReloadTreeItems()
         self.tree.UpdateCurrentDbLocationMapsetNode()
         self.tree.ExpandCurrentMapset()
         self.tree.changeMapset.connect(lambda mapset:

+ 203 - 163
gui/wxpython/datacatalog/tree.py

@@ -124,18 +124,16 @@ def getLocationTree(gisdbase, location, queue, mapsets=None):
         gscript.try_remove(tmp_gisrc_file)
         return
     else:
-        listOfMapsets = mapsets.split(',')
+        mapsets = mapsets.split(',')
         Debug.msg(
             4, "Location <{0}>: {1} mapsets found".format(
-                location, len(listOfMapsets)))
-        for each in listOfMapsets:
-            maps_dict[each] = {}
-            for elem in elements:
-                maps_dict[each][elem] = []
+                location, len(mapsets)))
+        for each in mapsets:
+            maps_dict[each] = []
     try:
         maplist = gscript.read_command(
             'g.list', flags='mt', type=elements,
-            mapset=','.join(listOfMapsets),
+            mapset=','.join(mapsets),
             quiet=True, env=env).strip()
     except CalledModuleError:
         queue.put(
@@ -153,7 +151,7 @@ def getLocationTree(gisdbase, location, queue, mapsets=None):
         for each in listOfMaps:
             ltype, wholename = each.split('/')
             name, mapset = wholename.split('@')
-            maps_dict[mapset][ltype].append(name)
+            maps_dict[mapset].append({'name': name, 'type': ltype})
 
     queue.put((maps_dict, None))
     gscript.try_remove(tmp_gisrc_file)
@@ -312,73 +310,139 @@ class DataCatalogTree(TreeView):
         self.startEdit.connect(self.OnStartEditLabel)
         self.endEdit.connect(self.OnEditLabel)
 
-    def _initTreeItems(self, locations=None, mapsets=None):
-        """Add grass databases, locations, mapsets and layers to the tree.
-        Runs in multiple processes. Saves resulting data and error."""
-        # mapsets param currently unused
-        for grassdatabase in self.grassdatabases:
 
-            locations = GetListOfLocations(grassdatabase)
+    def _reloadMapsetNode(self, mapset_node):
+        """Recursively reload the model of a specific mapset node"""
+        if mapset_node.children:
+            del mapset_node.children[:]
+
+        q = Queue()
+        p = Process(
+            target=getLocationTree,
+            args=(
+                mapset_node.parent.parent.data['name'],
+                mapset_node.parent.data['name'],
+                q,
+                mapset_node.data['name']))
+        p.start()
+        maps, error = q.get()
+        self._populateMapsetItem(mapset_node,
+                                 maps[mapset_node.data['name']])
+        self._orig_model = copy.deepcopy(self._model)
+        return error
+
+    def _reloadLocationNode(self, location_node):
+        """Recursively reload the model of a specific location node"""
+        if location_node.children:
+            del location_node.children[:]
 
-            loc_count = proc_count = 0
-            queue_list = []
-            proc_list = []
-            loc_list = []
+        q = Queue()
+        p = Process(
+            target=getLocationTree,
+            args=(
+                location_node.parent.data['name'],
+                location_node.data['name'],
+                q,
+                None))
+        p.start()
+        maps, error = q.get()
+        for mapset in maps:
+            mapset_node = self._model.AppendNode(
+                                parent=location_node,
+                                data=dict(type='mapset', name=mapset))
+            self._populateMapsetItem(mapset_node,
+                                     maps[mapset])
+        self._model.SortChildren(location_node)
+        self._orig_model = copy.deepcopy(self._model)
+        return error
+
+    def _reloadGrassDBNode(self, grassdb_node):
+        """Recursively reload the model of a specific grassdb node.
+        Runs reloading locations in parallel."""
+        if grassdb_node.children:
+            del grassdb_node.children[:]
+        locations = GetListOfLocations(grassdb_node.data["name"])
+
+        loc_count = proc_count = 0
+        queue_list = []
+        proc_list = []
+        loc_list = []
+        try:
+            nprocs = cpu_count()
+        except NotImplementedError:
             nprocs = 4
-            try:
-                nprocs = cpu_count()
-            except NotImplementedError:
-                nprocs = 4
-
-            results = dict()
-            errors = []
-            location_nodes = []
-            nlocations = len(locations)
-            grassdata_node = self._model.AppendNode(
-                parent=self._model.root,
-                data=dict(type='grassdb', name=grassdatabase))
-            for location in locations:
-                results[location] = dict()
-                varloc = self._model.AppendNode(
-                    parent=grassdata_node, data=dict(
-                        type='location', name=location))
-                location_nodes.append(varloc)
-                loc_count += 1
-
-                Debug.msg(
-                    3, "Scanning location <{0}> ({1}/{2})".format(location, loc_count, nlocations))
-
-                q = Queue()
-                p = Process(target=getLocationTree,
-                            args=(grassdatabase, location, q))
-                p.start()
-
-                queue_list.append(q)
-                proc_list.append(p)
-                loc_list.append(location)
-
-                proc_count += 1
-                # Wait for all running processes
-                if proc_count == nprocs or loc_count == nlocations:
-                    Debug.msg(4, "Process subresults")
-                    for i in range(len(loc_list)):
-                        maps, error = queue_list[i].get()
-                        proc_list[i].join()
-                        if error:
-                            errors.append(error)
-
-                        for key in sorted(maps.keys()):
-                            mapset_node = self._model.AppendNode(
+
+        results = dict()
+        errors = []
+        location_nodes = []
+        all_location_nodes = []
+        nlocations = len(locations)
+        for location in locations:
+            results[location] = dict()
+            varloc = self._model.AppendNode(parent=grassdb_node,
+                                            data=dict(type='location',
+                                                      name=location))
+            location_nodes.append(varloc)
+            all_location_nodes.append(varloc)
+            loc_count += 1
+
+            Debug.msg(
+                3, "Scanning location <{0}> ({1}/{2})".format(location, loc_count, nlocations))
+
+            q = Queue()
+            p = Process(target=getLocationTree,
+                        args=(grassdb_node.data["name"], location, q))
+            p.start()
+
+            queue_list.append(q)
+            proc_list.append(p)
+            loc_list.append(location)
+
+            proc_count += 1
+            # Wait for all running processes
+            if proc_count == nprocs or loc_count == nlocations:
+                Debug.msg(4, "Process subresults")
+                for i in range(len(loc_list)):
+                    maps, error = queue_list[i].get()
+                    proc_list[i].join()
+                    if error:
+                        errors.append(error)
+
+                    for key in sorted(maps.keys()):
+                        mapset_node = self._model.AppendNode(
                                 parent=location_nodes[i],
                                 data=dict(type='mapset', name=key))
-                            self._populateMapsetItem(mapset_node, maps[key])
+                        self._populateMapsetItem(mapset_node, maps[key])
+
+                proc_count = 0
+                proc_list = []
+                queue_list = []
+                loc_list = []
+                location_nodes = []    
 
-                    proc_count = 0
-                    proc_list = []
-                    queue_list = []
-                    loc_list = []
-                    location_nodes = []
+        for node in all_location_nodes:
+            self._model.SortChildren(node)
+        self._model.SortChildren(grassdb_node)
+        self._orig_model = copy.deepcopy(self._model)
+        return errors
 
+    def _reloadTreeItems(self):
+        """Updates grass databases, locations, mapsets and layers in the tree.
+        Saves resulting data and error."""
+        errors = []
+        for grassdatabase in self.grassdatabases:
+            grassdb_nodes = self._model.SearchNodes(name=grassdatabase,
+                                                    type='grassdb')
+            if not grassdb_nodes:
+                grassdb_node = self._model.AppendNode(parent=self._model.root,
+                                                      data=dict(type='grassdb',
+                                                                name=grassdatabase))
+            else:
+                grassdb_node = grassdb_nodes[0]
+            error = self._reloadGrassDBNode(grassdb_node)
+            if error:
+                errors.append(error)
+            
         if errors:
             wx.CallAfter(GWarning, '\n'.join(errors))
         Debug.msg(1, "Tree filled")
@@ -386,60 +450,34 @@ class DataCatalogTree(TreeView):
         self.UpdateCurrentDbLocationMapsetNode()
         self.RefreshItems()
 
+    def _renameNode(self, node, name):
+        """Rename node (map, mapset, location), sort and refresh.
+        Should be called after actual renaming of a map, mapset, location."""
+        node.data['name'] = name
+        self._model.SortChildren(node.parent)
+        self.RefreshNode(node.parent, recursive=True)
+
     def UpdateCurrentDbLocationMapsetNode(self):
         self.current_grassdb_node, self.current_location_node, self.current_mapset_node = \
             self.GetCurrentDbLocationMapsetNode()
 
     def ReloadTreeItems(self):
         """Reload dbs, locations, mapsets and layers in the tree."""
-        self._orig_model = self._model
-        self._model.RemoveNode(self._model.root)
-        self.InitTreeItems()
+        self._reloadTreeItems()
 
     def ReloadCurrentMapset(self):
         """Reload current mapset tree only."""
-        def get_first_child(node):
-            try:
-                child = self.current_mapset_node.children[0]
-            except IndexError:
-                child = None
-            return child
-
         self.UpdateCurrentDbLocationMapsetNode()
         if not self.current_grassdb_node or not self.current_location_node or not self.current_mapset_node:
             return
 
-        if self.current_mapset_node.children:
-            node = get_first_child(self.current_mapset_node)
-            while node:
-                self._model.RemoveNode(node)
-                node = get_first_child(self.current_mapset_node)
-
-        q = Queue()
-        p = Process(
-            target=getLocationTree,
-            args=(
-                self.current_grassdb_node.data['name'],
-                self.current_location_node.data['name'],
-                q,
-                self.current_mapset_node.data['name']))
-        p.start()
-        maps, error = q.get()
-        if error:
-            raise CalledModuleError(error)
-
-        self._populateMapsetItem(self.current_mapset_node,
-                                 maps[self.current_mapset_node.data['name']])
-        self._orig_model = copy.deepcopy(self._model)
-        self.RefreshNode(self.current_mapset_node)
-        self.RefreshItems()
+        self._reloadMapsetNode(self.current_mapset_node)
+        self.RefreshNode(self.current_mapset_node, recursive=True)
 
     def _populateMapsetItem(self, mapset_node, data):
-        for elem in data:
-            if data[elem]:
-                for layer in data[elem]:
-                    self._model.AppendNode(parent=mapset_node,
-                                           data=dict(type=elem, name=layer))
+        for item in data:
+            self._model.AppendNode(parent=mapset_node,
+                                   data=dict(**item))
         self._model.SortChildren(mapset_node)
 
     def _initVariables(self):
@@ -626,10 +664,6 @@ class DataCatalogTree(TreeView):
 
         return ret, cmdString
 
-    def InitTreeItems(self):
-        """Add locations, mapsets and layers to the tree."""
-        self._initTreeItems()
-
     def OnMoveMap(self, event):
         """Move layer or mapset (just save it temporarily, copying is done by paste)"""
         self.copy_mode = False
@@ -673,7 +707,6 @@ class DataCatalogTree(TreeView):
             element=self.selected_layer[0].data['type'])
         if new_name:
             self.Rename(old_name, new_name)
-            self.ReloadTreeItems()
 
     def CreateMapset(self, grassdb_node, location_node):
         """Creates new mapset interactively and adds it to the tree."""
@@ -696,10 +729,10 @@ class DataCatalogTree(TreeView):
             create_location_interactively(self, grassdb_node.data['name'])
         )
         if location:
-            item = self._model.SearchNodes(name=grassdatabase, type='grassdb')
-            if not item:
-                self.InsertGrassDb(name=grassdatabase)
-            self.ReloadTreeItems()
+            grassdb_nodes = self._model.SearchNodes(name=grassdatabase, type='grassdb')
+            if not grassdb_nodes:
+                grassdb_node = self.InsertGrassDb(name=grassdatabase)
+            self.InsertLocation(location, grassdb_node)
 
     def OnCreateLocation(self, event):
         """Create new location"""
@@ -709,43 +742,35 @@ class DataCatalogTree(TreeView):
         """
         Rename selected mapset
         """
-        try:
-            newmapset = rename_mapset_interactively(
-                    self,
-                    self.selected_grassdb[0].data['name'],
-                    self.selected_location[0].data['name'],
-                    self.selected_mapset[0].data['name'])
-            if newmapset:
-                self.ReloadTreeItems()
-        except Exception as e:
-            GError(parent=self,
-                   message=_("Unable to rename mapset: %s") % e,
-                   showTraceback=False)
+        newmapset = rename_mapset_interactively(
+                self,
+                self.selected_grassdb[0].data['name'],
+                self.selected_location[0].data['name'],
+                self.selected_mapset[0].data['name'])
+        if newmapset:
+            self._renameNode(self.selected_mapset[0], newmapset)
 
     def OnRenameLocation(self, event):
         """
         Rename selected location
         """
-        try:
-            newlocation = rename_location_interactively(
-                    self,
-                    self.selected_grassdb[0].data['name'],
-                    self.selected_location[0].data['name'])
-            if newlocation:
-                self.ReloadTreeItems()
-        except Exception as e:
-            GError(parent=self,
-                   message=_("Unable to rename location: %s") % e,
-                   showTraceback=False)
+        newlocation = rename_location_interactively(
+                self,
+                self.selected_grassdb[0].data['name'],
+                self.selected_location[0].data['name'])
+        if newlocation:
+            self._renameNode(self.selected_location[0], newlocation)
 
     def OnStartEditLabel(self, node, event):
         """Start label editing"""
         self.DefineItems([node])
+        # TODO: add renaming mapset/location
+        if not self.selected_layer[0]:
+            event.Veto()
+            return
         Debug.msg(1, "Start label edit {name}".format(name=node.data['name']))
         label = _("Editing {name}").format(name=node.data['name'])
         self.showNotification.emit(message=label)
-        if not self.selected_layer:
-            event.Veto()
 
     def OnEditLabel(self, node, event):
         """End label editing"""
@@ -772,8 +797,7 @@ class DataCatalogTree(TreeView):
             renamed, cmd = self._runCommand(
                 'g.rename', raster3d=string, env=env)
         if renamed == 0:
-            self.selected_layer[0].data['name'] = new
-            self.RefreshNode(self.selected_layer[0])
+            self._renameNode(self.selected_layer[0], new)
             self.showNotification.emit(
                 message=_("{cmd} -- completed").format(cmd=cmd))
             Debug.msg(1, "LAYER RENAMED TO: " + new)
@@ -911,19 +935,32 @@ class DataCatalogTree(TreeView):
         self.RefreshNode(mapset_node, recursive=True)
 
     def InsertMapset(self, name, location_node):
-        """Insert mapset into model and refresh tree"""
-        self._model.AppendNode(parent=location_node,
-                               data=dict(type="mapset", name=name))
+        """Insert new mapset into model and refresh tree.
+        Assumes mapset is empty."""
+        mapset_node = self._model.AppendNode(parent=location_node,
+                                             data=dict(type="mapset", name=name))
         self._model.SortChildren(location_node)
         self.RefreshNode(location_node, recursive=True)
+        return mapset_node
+
+    def InsertLocation(self, name, grassdb_node):
+        """Insert new location into model and refresh tree"""
+        location_node = self._model.AppendNode(parent=grassdb_node,
+                                               data=dict(type="location", name=name))
+        # reload new location since it has a mapset
+        self._reloadLocationNode(location_node)
+        self._model.SortChildren(grassdb_node)
+        self.RefreshNode(grassdb_node, recursive=True)
+        return location_node
 
     def InsertGrassDb(self, name):
-        """Insert grass db into model and refresh tree"""
+        """Insert new grass db into model and refresh tree"""
         self.grassdatabases.append(name)
-        self._model.AppendNode(parent=self._model.root,
-                               data=dict(type="grassdb", name=name))
-        self._model.SortChildren(self._model.root)
-        self.ReloadTreeItems()
+        grassdb_node = self._model.AppendNode(parent=self._model.root,
+                                              data=dict(type="grassdb", name=name))
+        self._reloadGrassDBNode(grassdb_node)
+        self.RefreshItems()
+        return grassdb_node
 
     def OnDeleteMap(self, event):
         """Delete layer or mapset"""
@@ -973,22 +1010,23 @@ class DataCatalogTree(TreeView):
                 self.selected_mapset[i].data['name']
             ))
         if delete_mapsets_interactively(self, mapsets):
-            self.ReloadTreeItems()
+            locations = set([each for each in self.selected_location])
+            for loc_node in locations:
+                self._reloadLocationNode(loc_node)
+                self.UpdateCurrentDbLocationMapsetNode()
+                self.RefreshNode(loc_node, recursive=True)
 
     def OnDeleteLocation(self, event):
         """
         Delete selected location
         """
-        try:
-            if (delete_location_interactively(
-                    self,
-                    self.selected_grassdb[0].data['name'],
-                    self.selected_location[0].data['name'])):
-                self.ReloadTreeItems()
-        except Exception as e:
-            GError(parent=self,
-                   message=_("Unable to delete location: %s") % e,
-                   showTraceback=False)
+        if (delete_location_interactively(
+                self,
+                self.selected_grassdb[0].data['name'],
+                self.selected_location[0].data['name'])):
+            self._reloadGrassDBNode(self.selected_grassdb[0])
+            self.UpdateCurrentDbLocationMapsetNode()
+            self.RefreshNode(self.selected_grassdb[0], recursive=True)
 
     def DownloadLocation(self, grassdb_node):
         """
@@ -998,7 +1036,9 @@ class DataCatalogTree(TreeView):
             download_location_interactively(self, grassdb_node.data['name'])
         )
         if location:
-            self.ReloadTreeItems()
+            self._reloadGrassDBNode(grassdb_node)
+            self.UpdateCurrentDbLocationMapsetNode()
+            self.RefreshItems()
 
     def OnDownloadLocation(self, event):
         """

+ 7 - 2
gui/wxpython/gui_core/treeview.py

@@ -158,9 +158,14 @@ class AbstractTreeViewMixin(VirtualTree):
     def RefreshNode(self, node, recursive=False):
         """Refreshes node."""
         index = self._model.GetIndexOfNode(node)
-        self.RefreshItem(index)
         if recursive:
-            self.RefreshChildrenRecursively(self.GetItemByIndex(index))
+            try:
+                item = self.GetItemByIndex(index)
+            except IndexError:
+                return
+            self.RefreshItemRecursively(item, index)
+        else:
+            self.RefreshItem(index)
 
     def _emitSignal(self, item, signal, **kwargs):
         """Helper method for emitting signals.