diff --git a/gui/icons/grass/STR3DS.png b/gui/icons/grass/STR3DS.png new file mode 100644 index 00000000000..1d5748a698a Binary files /dev/null and b/gui/icons/grass/STR3DS.png differ diff --git a/gui/icons/grass/STRDS.png b/gui/icons/grass/STRDS.png new file mode 100644 index 00000000000..03f29f66638 Binary files /dev/null and b/gui/icons/grass/STRDS.png differ diff --git a/gui/icons/grass/STVDS.png b/gui/icons/grass/STVDS.png new file mode 100644 index 00000000000..3e143f0d6b5 Binary files /dev/null and b/gui/icons/grass/STVDS.png differ diff --git a/gui/wxpython/datacatalog/tree.py b/gui/wxpython/datacatalog/tree.py index dca1043ba16..28a95aed7b8 100644 --- a/gui/wxpython/datacatalog/tree.py +++ b/gui/wxpython/datacatalog/tree.py @@ -66,6 +66,7 @@ from grass.pydispatch.signal import Signal import grass.script as gs +from grass.tools import Tools, ToolError from grass.script import gisenv from grass.grassdb.data import map_exists from grass.grassdb.checks import ( @@ -76,9 +77,11 @@ ) from grass.exceptions import CalledModuleError +tools = Tools() + def getLocationTree(gisdbase, location, queue, mapsets=None, lazy=False): - """Creates dictionary with mapsets, elements, layers for given location. + """Creates dictionary with mapsets, stds, elements, layers for given location. Returns tuple with the dictionary and error (or None)""" tmp_gisrc_file, env = gs.create_environment(gisdbase, location, "PERMANENT") env["GRASS_SKIP_MAPSET_OWNER_CHECK"] = "1" @@ -134,6 +137,22 @@ def getLocationTree(gisdbase, location, queue, mapsets=None, lazy=False): name, mapset = wholename.split("@", maxsplit=1) maps_dict[mapset].append({"name": name, "type": ltype}) + try: + for t_type in ["strds", "stvds", "str3ds"]: + items = tools.t_list( + type=t_type, format="json", mapset="*", quiet=True, env=env + ) + + if items: + for item in items: + m_set = item["mapset"] + d_name = item["name"] + if m_set and d_name and m_set in maps_dict: + maps_dict[m_set].append({"name": d_name, "type": t_type}) + + except ToolError as e: + Debug.msg(1, "Temporal fetch failed: {0}".format(e)) + queue.put((maps_dict, None)) gs.try_remove(tmp_gisrc_file) @@ -271,9 +290,16 @@ def __init__( "grassdb", "location", "mapset", + "strds", + "stvds", + "str3ds", "raster", "vector", "raster_3d", + "raster_group", + "vector_group", + "raster_3d_group", + "stds_group", ] self._initImages() self.thread = gThread() @@ -345,6 +371,8 @@ def _resetSelectVariables(self): """Reset variables related to item selection.""" self.selected_grassdb = [] self.selected_layer = [] + self.selected_stds = [] + self.selected_stds_map = [] self.selected_mapset = [] self.selected_location = [] self.mixed = False @@ -438,6 +466,62 @@ def _saveGrassDBs(self): UserSettings.SaveToFile(grassdbSettings) + def _reloadDatasetNode(self, dataset_node, mapset_node): + """Recursively reload the model of a specific temporal dataset node""" + if dataset_node.children: + del dataset_node.children[:] + + dataset_name = dataset_node.data["name"] + mapset_name = mapset_node.data["name"] + + # Traverse the tree via node.parent instead of using + # self.selected_location[0] or self.selected_grassdb[0]. + # In standalone mode (when a user runs `g.gui.datacatalog`), + # the selection arrays are empty and will throw an IndexError. + location_node = mapset_node.parent + grassdb_node = location_node.parent + + ds_type = dataset_node.data["type"] + + t_map = { + "strds": ("t.rast.list", "raster"), + "stvds": ("t.vect.list", "vector"), + "str3ds": ("t.rast3d.list", "raster_3d"), + } + list_cmd, child_type = t_map[ds_type] + + gisrc, env = gs.create_environment( + grassdb_node.data["name"], + location_node.data["name"], + mapset_name, + ) + + self.busy = wx.BusyCursor() + try: + items = getattr(tools, list_cmd)( + input=f"{dataset_name}@{mapset_name}", + columns="id,name", + format="json", + quiet=True, + env=env, + ) + + if items: + for item in items["data"]: + self._model.AppendNode( + parent=dataset_node, + data={ + "type": child_type, + "name": item["name"], + "map_id": item["id"], + }, + ) + except ToolError as e: + Debug.msg(1, "Lazy load failed: {0}".format(e)) + finally: + gs.try_remove(gisrc) + del self.busy + def _reloadMapsetNode(self, mapset_node): """Recursively reload the model of a specific mapset node""" if mapset_node.children: @@ -724,8 +808,10 @@ def EnableWatchingMapset(self): EVT_CURRENT_MAPSET_CHANGED, lambda evt: self._updateAfterMapsetChanged() ) - def GetDbNode(self, grassdb, location=None, mapset=None, map=None, map_type=None): - """Returns node representing db/location/mapset/map or None if not found.""" + def GetDbNode( + self, grassdb, location=None, mapset=None, element_name=None, element_type=None + ): + """Returns node representing db/location/mapset/map(or stds) or None if not found.""" grassdb_nodes = self._model.SearchNodes(name=grassdb, type="grassdb") if grassdb_nodes: if not location: @@ -740,13 +826,27 @@ def GetDbNode(self, grassdb, location=None, mapset=None, map=None, map_type=None parent=location_nodes[0], name=mapset, type="mapset" ) if mapset_nodes: - if not map: + if not element_name: return mapset_nodes[0] map_nodes = self._model.SearchNodes( - parent=mapset_nodes[0], name=map, type=map_type + parent=mapset_nodes[0], name=element_name, type=element_type ) if map_nodes: return map_nodes[0] + + group_types = { + "raster_group", + "vector_group", + "raster_3d_group", + "stds_group", + } + for child in mapset_nodes[0].children: + if child.data.get("type") in group_types: + map_nodes = self._model.SearchNodes( + parent=child, name=element_name, type=element_type + ) + if map_nodes: + return map_nodes[0] return None def _renameNode(self, node, name): @@ -835,9 +935,22 @@ def ReloadCurrentMapset(self): self.RefreshNode(self.current_mapset_node, recursive=True) def _populateMapsetItem(self, mapset_node, data): - for item in data: - self._model.AppendNode(parent=mapset_node, data=dict(**item)) - self._model.SortChildren(mapset_node) + groups = [ + ("raster_group", _("Raster maps"), ["raster"]), + ("raster_3d_group", _("3D raster maps"), ["raster_3d"]), + ("vector_group", _("Vector maps"), ["vector"]), + ("stds_group", _("Spatio-temporal datasets"), ["strds", "stvds", "str3ds"]), + ] + + for g_type, g_name, types in groups: + items = [item for item in data if item["type"] in types] + if items: + group_node = self._model.AppendNode( + parent=mapset_node, data={"type": g_type, "name": g_name} + ) + for item in items: + self._model.AppendNode(parent=group_node, data=dict(**item)) + self._model.SortChildren(group_node) def _initImages(self): bmpsize = (16, 16) @@ -845,9 +958,16 @@ def _initImages(self): "grassdb": MetaIcon(img="grassdb").GetBitmap(bmpsize), "location": MetaIcon(img="location").GetBitmap(bmpsize), "mapset": MetaIcon(img="mapset").GetBitmap(bmpsize), + "strds": MetaIcon(img="STRDS").GetBitmap(bmpsize), + "stvds": MetaIcon(img="STVDS").GetBitmap(bmpsize), + "str3ds": MetaIcon(img="STR3DS").GetBitmap(bmpsize), "raster": MetaIcon(img="raster").GetBitmap(bmpsize), "vector": MetaIcon(img="vector").GetBitmap(bmpsize), "raster_3d": MetaIcon(img="raster3d").GetBitmap(bmpsize), + "raster_group": MetaIcon(img="raster").GetBitmap(bmpsize), + "vector_group": MetaIcon(img="vector").GetBitmap(bmpsize), + "raster_3d_group": MetaIcon(img="raster3d").GetBitmap(bmpsize), + "stds_group": MetaIcon(img="time-period").GetBitmap(bmpsize), } il = wx.ImageList(bmpsize[0], bmpsize[1], mask=False) for each in self._iconTypes: @@ -865,25 +985,73 @@ def DefineItems(self, selected): for item in selected: type = item.data["type"] if type in {"raster", "raster_3d", "vector"}: - self.selected_layer.append(item) - self.selected_mapset.append(item.parent) - self.selected_location.append(item.parent.parent) - self.selected_grassdb.append(item.parent.parent.parent) - mixed.append("layer") + if item.parent.data["type"] in {"strds", "stvds", "str3ds"}: + self.selected_layer.append(item) + self.selected_stds.append(None) + self.selected_stds_map.append(item) + mixed.append("stds_map") + else: + self.selected_layer.append(item) + self.selected_stds.append(None) + self.selected_stds_map.append(None) + mixed.append("layer") + + parent_node = item.parent + while parent_node and parent_node.data["type"] != "mapset": + parent_node = parent_node.parent + + self.selected_mapset.append(parent_node) + self.selected_location.append(parent_node.parent) + self.selected_grassdb.append(parent_node.parent.parent) + + elif type in {"strds", "stvds", "str3ds"}: + self.selected_layer.append(None) + self.selected_stds.append(item) + self.selected_stds_map.append(None) + + parent_node = item.parent + while parent_node and parent_node.data["type"] != "mapset": + parent_node = parent_node.parent + + self.selected_mapset.append(parent_node) + self.selected_location.append(parent_node.parent) + self.selected_grassdb.append(parent_node.parent.parent) + mixed.append("stds") + + elif type in { + "raster_group", + "vector_group", + "raster_3d_group", + "stds_group", + }: + self.selected_layer.append(None) + self.selected_stds.append(None) + self.selected_stds_map.append(None) + self.selected_mapset.append(None) + self.selected_location.append(None) + self.selected_grassdb.append(None) + mixed.append("group") + elif type == "mapset": self.selected_layer.append(None) + self.selected_stds.append(None) + self.selected_stds_map.append(None) self.selected_mapset.append(item) self.selected_location.append(item.parent) self.selected_grassdb.append(item.parent.parent) mixed.append("mapset") elif type == "location": self.selected_layer.append(None) + self.selected_stds.append(None) + self.selected_stds_map.append(None) self.selected_mapset.append(None) self.selected_location.append(item) self.selected_grassdb.append(item.parent) mixed.append("location") elif type == "grassdb": self.selected_layer.append(None) + self.selected_stds.append(None) + self.selected_stds_map.append(None) self.selected_mapset.append(None) self.selected_location.append(None) self.selected_grassdb.append(item) @@ -902,10 +1070,20 @@ def OnRightClick(self, node): self._popupMenuEmpty() return - if not self.selected_layer: + if not self.selected_layer and not self.selected_stds: self._popupMenuEmpty() + elif self.selected_stds_map[0]: + self._popupMenuStdsMap() elif self.selected_layer[0]: self._popupMenuLayer() + elif self.selected_stds[0] and len(self.selected_stds) == 1: + self._popupMenuStds() + elif ( + self.selected_stds[0] + and len(self.selected_stds) > 1 + and not self.selected_layer[0] + ): + self._popupMenuMultipleStds() elif self.selected_mapset[0] and len(self.selected_mapset) == 1: self._popupMenuMapset() elif ( @@ -979,7 +1157,28 @@ def OnDoubleClick(self, node): if node.data["type"] == "mapset" and not node.children: self._reloadMapsetNode(node) self.RefreshNode(node, recursive=True) - if node.data["type"] in {"mapset", "location", "grassdb"}: + + if node.data["type"] in {"strds", "stvds", "str3ds"} and not node.children: + mapset_node = ( + node.parent.parent + if node.parent.data["type"] == "stds_group" + else node.parent + ) + self._reloadDatasetNode(node, mapset_node) + self.RefreshNode(node, recursive=True) + + if node.data["type"] in { + "mapset", + "location", + "grassdb", + "strds", + "stvds", + "str3ds", + "raster_group", + "vector_group", + "raster_3d_group", + "stds_group", + }: # expand/collapse location/mapset... if self.IsNodeExpanded(node): self.CollapseNode(node, recursive=False) @@ -1060,7 +1259,7 @@ def ExpandCurrentMapset(self): """Expand current mapset""" if self.current_mapset_node: self.Select(self.current_mapset_node, select=True) - self.ExpandNode(self.current_mapset_node, recursive=True) + self.ExpandNode(self.current_mapset_node, recursive=False) def SetRestriction(self, restrict): self._restricted = restrict @@ -1145,6 +1344,20 @@ def OnCreateMapset(self, event): """Create new mapset""" self.CreateMapset(self.selected_grassdb[0], self.selected_location[0]) + def OnCreateStds(self, event): + """Launch t.create dialog to create a new temporal dataset""" + mapset_node = self.selected_mapset[0] + + self._giface.RunCmd( + ["t.create", "--ui"], + onDone=lambda e: wx.CallAfter( + lambda: ( + self._reloadMapsetNode(mapset_node), + self.RefreshNode(mapset_node, recursive=True), + ) + ), + ) + def CreateLocation(self, grassdb_node): """ Creates new location interactively and adds it to the tree and switch @@ -1193,6 +1406,50 @@ def OnRenameMapset(self, event): newname=newmapset, ) + def OnRenameStds(self, event): + """Rename temporal dataset natively using t.rename""" + old_name = self.selected_stds[0].data["name"] + stds_type = self.selected_stds[0].data["type"] + + gisrc, env = gs.create_environment( + self.selected_grassdb[0].data["name"], + self.selected_location[0].data["name"], + self.selected_mapset[0].data["name"], + ) + + new_name = self._getNewMapName( + _("New name"), + _("Rename dataset"), + old_name, + env=env, + mapset=self.selected_mapset[0].data["name"], + element=stds_type, + ) + + if new_name: + string = f"{old_name},{new_name}" + label = _("Renaming dataset <{name}>...").format(name=string) + self.showNotification.emit(message=label) + + renamed, cmd = self._runCommand( + "t.rename", input=old_name, output=new_name, type=stds_type, env=env + ) + + if renamed == 0: + self.showNotification.emit( + message=_("{cmd} -- completed").format(cmd=cmd) + ) + self._giface.grassdbChanged.emit( + grassdb=self.selected_grassdb[0].data["name"], + location=self.selected_location[0].data["name"], + mapset=self.selected_mapset[0].data["name"], + map=old_name, + element=stds_type, + newname=new_name, + action="rename", + ) + gs.try_remove(gisrc) + def OnRenameLocation(self, event): """ Rename selected location @@ -1545,12 +1802,34 @@ def _removeMapAfterCopy(self, cLayer, cMapset, env): action="delete", ) - def InsertLayer(self, name, mapset_node, element_name): - """Insert layer into model and refresh tree""" + def InsertElements(self, name, mapset_node, element_name): + """Insert elements into model and refresh tree""" + group_info = { + "raster": {"name": _("Raster maps"), "type": "raster_group"}, + "vector": {"name": _("Vector maps"), "type": "vector_group"}, + "raster_3d": {"name": _("3D raster maps"), "type": "raster_3d_group"}, + "strds": {"name": _("Spatio-temporal datasets"), "type": "stds_group"}, + "stvds": {"name": _("Spatio-temporal datasets"), "type": "stds_group"}, + "str3ds": {"name": _("Spatio-temporal datasets"), "type": "stds_group"}, + } + g_info = group_info[element_name] + g_name = g_info["name"] + g_type = g_info["type"] + + group_nodes = self._model.SearchNodes( + parent=mapset_node, name=g_name, type=g_type + ) + if group_nodes: + group_node = group_nodes[0] + else: + group_node = self._model.AppendNode( + parent=mapset_node, data={"type": g_type, "name": g_name} + ) + self._model.AppendNode( - parent=mapset_node, data={"type": element_name, "name": name} + parent=group_node, data={"type": element_name, "name": name} ) - self._model.SortChildren(mapset_node) + self._model.SortChildren(group_node) self.RefreshNode(mapset_node, recursive=True) def InsertMapset(self, name, location_node): @@ -1657,6 +1936,52 @@ def OnDeleteMap(self, event): self.UnselectAll() self.showNotification.emit(message=_("g.remove completed")) + def OnDeleteStds(self, event): + """Delete one or multiple temporal datasets natively using t.remove""" + names = [ + self.selected_stds[i].data["name"] for i in range(len(self.selected_stds)) + ] + + if len(names) < 10: + question = _("Do you really want to delete dataset(s) <{m}>?").format( + m=", ".join(names) + ) + else: + question = _("Do you really want to delete {n} datasets?").format( + n=len(names) + ) + + if self._confirmDialog(question, title=_("Delete dataset")) == wx.ID_YES: + self.showNotification.emit(message=_("Deleting datasets...")) + + for i in range(len(self.selected_stds)): + stds_type = self.selected_stds[i].data["type"] + stds_name = self.selected_stds[i].data["name"] + + gisrc, env = gs.create_environment( + self.selected_grassdb[i].data["name"], + self.selected_location[i].data["name"], + self.selected_mapset[i].data["name"], + ) + + removed, cmd = self._runCommand( + "t.remove", inputs=stds_name, type=stds_type, flags="f", env=env + ) + gs.try_remove(gisrc) + + if removed == 0: + self._giface.grassdbChanged.emit( + grassdb=self.selected_grassdb[i].data["name"], + location=self.selected_location[i].data["name"], + mapset=self.selected_mapset[i].data["name"], + element=stds_type, + map=stds_name, + action="delete", + ) + + self.UnselectAll() + self.showNotification.emit(message=_("t.remove completed")) + def OnDeleteMapset(self, event): """ Delete selected mapset or mapsets @@ -1772,12 +2097,14 @@ def DisplayLayer(self): all_names = [] names = {"raster": [], "vector": [], "raster_3d": []} for i in range(len(self.selected_layer)): - name = ( - self.selected_layer[i].data["name"] - + "@" - + self.selected_mapset[i].data["name"] + if self.selected_layer[i] is None: + continue + item_data = self.selected_layer[i].data + name = item_data.get( + "map_id", f"{item_data['name']}@{self.selected_mapset[i].data['name']}" ) - names[self.selected_layer[i].data["type"]].append(name) + + names[item_data["type"]].append(name) all_names.append(name) # if self.selected_location[0].data['name'] == gisenv()['LOCATION_NAME'] and # self.selected_mapset[0]: @@ -1933,10 +2260,14 @@ def _updateAfterGrassdbChanged( self._reloadMapsetNode(node) self.RefreshNode(node.parent, recursive=True) # check if map already exists - elif not self._model.SearchNodes( - parent=node, name=map, type=element + elif not self.GetDbNode( + grassdb=grassdb, + location=location, + mapset=mapset, + element_name=map, + element_type=element, ): - self.InsertLayer( + self.InsertElements( name=map, mapset_node=node, element_name=element ) else: @@ -1948,19 +2279,49 @@ def _updateAfterGrassdbChanged( grassdb=grassdb, location=location, mapset=mapset, - map=map, - map_type=element, + element_name=map, + element_type=element, + ) + if node: + self._model.RemoveNode(node) + self.RefreshNode(node.parent, recursive=True) + elif action == "rename": + node = self.GetDbNode( + grassdb=grassdb, + location=location, + mapset=mapset, + element_name=map, + element_type=element, + ) + if node: + self._renameNode(node, newname) + + elif element in {"strds", "stvds", "str3ds"}: + if action == "new": + node = self.GetDbNode(grassdb=grassdb, location=location, mapset=mapset) + if node: + self._reloadMapsetNode(node) + self.RefreshNode(node, recursive=True) + + elif action == "delete": + node = self.GetDbNode( + grassdb=grassdb, + location=location, + mapset=mapset, + element_name=map, + element_type=element, ) if node: self._model.RemoveNode(node) self.RefreshNode(node.parent, recursive=True) + elif action == "rename": node = self.GetDbNode( grassdb=grassdb, location=location, mapset=mapset, - map=map, - map_type=element, + element_name=map, + element_type=element, ) if node: self._renameNode(node, newname) @@ -1999,14 +2360,13 @@ def done(event): cmd = ["v.info"] elif self.selected_layer[i].data["type"] == "raster_3d": cmd = ["r3.info"] - cmd.append( - "map=%s@%s" - % ( - self.selected_layer[i].data["name"], - self.selected_mapset[i].data["name"], - ) + map_name = self.selected_layer[i].data.get( + "map_id", + f"{self.selected_layer[i].data['name']}@{self.selected_mapset[i].data['name']}", ) + cmd.append(f"map={map_name}") + gisrc, env = gs.create_environment( self.selected_grassdb[i].data["name"], self.selected_location[i].data["name"], @@ -2016,6 +2376,24 @@ def done(event): # temp gisrc file must be deleted onDone self._giface.RunCmd(cmd, env=env, onDone=done, userData=gisrc) + def OnMetadataStds(self, event): + """Show metadata natively using t.info""" + + def done(event): + gs.try_remove(event.userData) + + stds_type = self.selected_stds[0].data["type"] + stds_name = self.selected_stds[0].data["name"] + mapset_name = self.selected_mapset[0].data["name"] + + cmd = ["t.info", f"type={stds_type}", f"input={stds_name}@{mapset_name}"] + gisrc, env = gs.create_environment( + self.selected_grassdb[0].data["name"], + self.selected_location[0].data["name"], + mapset_name, + ) + self._giface.RunCmd(cmd, env=env, onDone=done, userData=gisrc) + def OnShowProjection(self, event): """Show projection info of selected project (location)""" @@ -2050,13 +2428,11 @@ def OnCopyName(self, event): do = wx.TextDataObject() text = [] for i in range(len(self.selected_layer)): - text.append( - "%s@%s" - % ( - self.selected_layer[i].data["name"], - self.selected_mapset[i].data["name"], - ) + map_name = self.selected_layer[i].data.get( + "map_id", + f"{self.selected_layer[i].data['name']}@{self.selected_mapset[i].data['name']}", ) + text.append(map_name) do.SetText(",".join(text)) wx.TheClipboard.SetData(do) wx.TheClipboard.Close() @@ -2141,6 +2517,171 @@ def OnCopyLocationPath(self, event): wx.TheClipboard.SetData(do) wx.TheClipboard.Close() + def OnRegisterStds(self, event): + """Launch t.register dialog pre-filled with selected dataset""" + type_map = {"strds": "raster", "stvds": "vector", "str3ds": "raster_3d"} + stds_node = self.selected_stds[0] + mapset_node = self.selected_mapset[0] + stds_type = type_map[stds_node.data["type"]] + + self._giface.RunCmd( + [ + "t.register", + "--ui", + f"input={stds_node.data['name']}@{mapset_node.data['name']}", + f"type={stds_type}", + ], + onDone=lambda e: wx.CallAfter( + lambda: ( + self._reloadDatasetNode(stds_node, mapset_node), + self.RefreshNode(stds_node, recursive=True), + ) + ), + ) + + def OnUnregisterStds(self, event): + """Launch t.unregister dialog pre-filled with selected dataset""" + type_map = {"strds": "raster", "stvds": "vector", "str3ds": "raster_3d"} + stds_node = self.selected_stds[0] + mapset_node = self.selected_mapset[0] + stds_type = type_map[stds_node.data["type"]] + + self._giface.RunCmd( + [ + "t.unregister", + "--ui", + f"input={stds_node.data['name']}@{mapset_node.data['name']}", + f"type={stds_type}", + ], + onDone=lambda e: wx.CallAfter( + lambda: ( + self._reloadDatasetNode(stds_node, mapset_node), + self.RefreshNode(stds_node, recursive=True), + ) + ), + ) + + def OnUnregisterStdsMap(self, event): + """Unregister selected maps from their temporal datasets natively""" + stds_groups = {} + for map_node in self.selected_stds_map: + stds_node = map_node.parent + if stds_node not in stds_groups: + stds_groups[stds_node] = [] + stds_groups[stds_node].append(map_node) + + num_maps = len(self.selected_stds_map) + if num_maps == 1: + question = _("Do you really want to unregister map <{m}>?").format( + m=self.selected_stds_map[0].data["name"] + ) + else: + question = _("Do you really want to unregister {n} maps?").format( + n=num_maps + ) + + if self._confirmDialog(question, title=_("Unregister map")) != wx.ID_YES: + return + + if num_maps == 1: + msg = _("Unregistering map...") + else: + msg = _("Unregistering {c} maps...").format(c=num_maps) + self.showNotification.emit(message=msg) + + success_count = 0 + + for stds_node, map_nodes in stds_groups.items(): + group_node = stds_node.parent + mapset_node = group_node.parent + location_node = mapset_node.parent + grassdb_node = location_node.parent + + mapset_name = mapset_node.data["name"] + stds_name = stds_node.data["name"] + map_type = map_nodes[0].data["type"] + + maps = ",".join([n.data["map_id"] for n in map_nodes]) + + gisrc, env = gs.create_environment( + grassdb_node.data["name"], + location_node.data["name"], + mapset_name, + ) + + unregistered, cmd = self._runCommand( + "t.unregister", + input=stds_name, + type=map_type, + maps=maps, + env=env, + ) + + gs.try_remove(gisrc) + + if unregistered == 0: + success_count += len(map_nodes) + for node in map_nodes: + self._model.RemoveNode(node) + self.RefreshNode(stds_node, recursive=True) + + if success_count > 0: + if success_count == 1: + msg = _("Successfully unregistered map") + else: + msg = _("Successfully unregistered {c} maps").format(c=success_count) + self.showNotification.emit(message=msg) + self.UnselectAll() + + def OnPlotTimeline(self, event): + """Launch g.gui.timeline for one or more selected datasets""" + inputs = [ + f"{n.data['name']}@{m.data['name']}" + for n, m in zip(self.selected_stds, self.selected_mapset, strict=True) + ] + self._giface.RunCmd(["g.gui.timeline", f"inputs={','.join(inputs)}"]) + + def OnModifyMetadata(self, event): + """Launch t.support dialog to modify dataset metadata""" + stds_node = self.selected_stds[0] + mapset_node = self.selected_mapset[0] + stds_type = stds_node.data["type"] + + self._giface.RunCmd( + [ + "t.support", + "--ui", + f"input={stds_node.data['name']}@{mapset_node.data['name']}", + f"type={stds_type}", + ] + ) + + def OnMergeStds(self, event): + """Launch t.merge dialog pre-filled with selected datasets""" + stds_types = {node.data["type"] for node in self.selected_stds} + if len(stds_types) > 1: + GMessage( + _("Cannot merge temporal datasets of different types."), parent=self + ) + return + + stds_type = list(stds_types)[0] + mapset_node = self.selected_mapset[0] + inputs = [ + f"{n.data['name']}@{m.data['name']}" + for n, m in zip(self.selected_stds, self.selected_mapset, strict=True) + ] + + self._giface.RunCmd( + ["t.merge", "--ui", f"inputs={','.join(inputs)}", f"type={stds_type}"], + onDone=lambda e: wx.CallAfter( + lambda: ( + self._reloadMapsetNode(mapset_node), + self.RefreshNode(mapset_node, recursive=True), + ) + ), + ) + def OnReloadGrassdb(self, event): """Reload all mapsets in selected grass db""" node = self.selected_grassdb[0] @@ -2204,6 +2745,88 @@ def _isCurrent(self, genv): return currentGrassDb, currentLocation, currentMapset return True, True, True + def _popupMenuStds(self): + """Create dedicated popup menu for space time datasets""" + menu = Menu() + + is_active = self.selected_mapset[0] == self.current_mapset_node + + item = wx.MenuItem(menu, wx.ID_ANY, _("&Register maps")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnRegisterStds, item) + item.Enable(is_active) + + item = wx.MenuItem(menu, wx.ID_ANY, _("&Unregister maps")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnUnregisterStds, item) + item.Enable(is_active) + + item = wx.MenuItem(menu, wx.ID_ANY, _("Modify metadata")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnModifyMetadata, item) + item.Enable(is_active) + + item = wx.MenuItem(menu, wx.ID_ANY, _("Plot &timeline")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnPlotTimeline, item) + + item = wx.MenuItem(menu, wx.ID_ANY, _("&Rename dataset")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnRenameStds, item) + + item = wx.MenuItem(menu, wx.ID_ANY, _("&Delete dataset")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnDeleteStds, item) + + menu.AppendSeparator() + + item = wx.MenuItem(menu, wx.ID_ANY, _("Show &metadata")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnMetadataStds, item) + + self.PopupMenu(menu) + menu.Destroy() + + def _popupMenuStdsMap(self): + """Create popup menu for maps inside a temporal dataset""" + menu = Menu() + genv = gisenv() + currentGrassDb, currentLocation, currentMapset = self._isCurrent(genv) + + item = wx.MenuItem(menu, wx.ID_ANY, _("Copy &name")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnCopyName, item) + + if not isinstance(self._giface, StandaloneGrassInterface): + if all( + each.data["name"] == genv["LOCATION_NAME"] + for each in self.selected_location + ): + if len(self.selected_layer) > 1: + item = wx.MenuItem(menu, wx.ID_ANY, _("&Display layers")) + else: + item = wx.MenuItem(menu, wx.ID_ANY, _("&Display layer")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnDisplayLayer, item) + + item = wx.MenuItem(menu, wx.ID_ANY, _("Show &metadata")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnMetadata, item) + + menu.AppendSeparator() + + if len(self.selected_layer) > 1: + item = wx.MenuItem(menu, wx.ID_ANY, _("&Unregister maps")) + else: + item = wx.MenuItem(menu, wx.ID_ANY, _("&Unregister map")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnUnregisterStdsMap, item) + if not currentMapset: + item.Enable(False) + + self.PopupMenu(menu) + menu.Destroy() + def _popupMenuLayer(self): """Create popup menu for layers""" menu = Menu() @@ -2267,6 +2890,12 @@ def _popupMenuMapset(self): genv = gisenv() currentGrassDb, currentLocation, currentMapset = self._isCurrent(genv) + item = wx.MenuItem(menu, wx.ID_ANY, _("Create &temporal dataset")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnCreateStds, item) + if self.selected_mapset[0] != self.current_mapset_node: + item.Enable(False) + item = wx.MenuItem(menu, wx.ID_ANY, _("&Paste")) menu.AppendItem(item) self.Bind(wx.EVT_MENU, self.OnPasteMap, item) @@ -2462,6 +3091,45 @@ def _popupMenuMultipleMapsets(self): self.PopupMenu(menu) menu.Destroy() + def _popupMenuMultipleStds(self): + """Create popup menu for multiple selected space time datasets""" + menu = Menu() + + item = wx.MenuItem(menu, wx.ID_ANY, _("Plot &timeline")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnPlotTimeline, item) + + try: + path_output = tools.g_mapsets(format="json", flags="p", quiet=True) + accessible = set(path_output["mapsets"]) + except ToolError as e: + Debug.msg(1, f"Failed to read search path: {e}") + accessible = set() + + curr_loc = ( + self.current_location_node.data["name"] + if self.current_location_node + else "" + ) + + # Must be same location (by name) AND in the search path + is_mergeable = all( + loc.data["name"] == curr_loc and m.data["name"] in accessible + for loc, m in zip(self.selected_location, self.selected_mapset, strict=True) + ) + + item = wx.MenuItem(menu, wx.ID_ANY, _("&Merge datasets")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnMergeStds, item) + item.Enable(is_mergeable) + + item = wx.MenuItem(menu, wx.ID_ANY, _("&Delete datasets")) + menu.AppendItem(item) + self.Bind(wx.EVT_MENU, self.OnDeleteStds, item) + + self.PopupMenu(menu) + menu.Destroy() + def OnRemoveLabel(self, path): """Remove label for database.""" grassdbs = UserSettings.Get(group="datacatalog", key="grassdbs") or {}