diff --git a/src/qgis_stac/gui/assets_dialog.py b/src/qgis_stac/gui/assets_dialog.py index 3dfbe9b..61e456d 100644 --- a/src/qgis_stac/gui/assets_dialog.py +++ b/src/qgis_stac/gui/assets_dialog.py @@ -37,7 +37,8 @@ from ..api.models import ( AssetLayerType, - ApiCapability + ApiCapability, + ResourceAsset ) from ..definitions.constants import ( @@ -640,7 +641,211 @@ def handle_layer_error(self, message): message ) +class ItemsAssetsDialog(AssetsDialog): + def __init__( + self, + item, + parent, + main_widget, + items, + ): + super().__init__(item, parent, main_widget) + self.items = items + + def prepare_assets(self): + """ Loads the dialog with the list of assets. + """ + super().prepare_assets() + if len(self.assets) > 0: + self.title.setText( + tr("Collection {}"). + format(self.item.id) + ) + self.asset_count.setText( + tr("{} available asset(s)"). + format(len(self.assets)) + + ) + else: + self.title.setText( + tr("Collection {} has no assets"). + format(self.item.id) + ) + + + def load_btn_clicked(self): + """ Runs logic after the asset load button has been clicked. + """ + for key, asset in self.load_assets.items(): + for item in self.items: + if key in item.stac_object.assets: + asset = item.stac_object.assets[key] + rasset = ResourceAsset( + href=asset.href, + title=asset.title or key, + description=asset.description, + type=asset.media_type or asset.type, + roles=asset.roles or [] + ) + + try: + load_task = QgsTask.fromFunction( + 'Load asset function', + self.load_asset(rasset) + ) + QgsApplication.taskManager().addTask(load_task) + except Exception as err: + log(tr("An error occurred when running task for " + "loading an asset, error message \"{}\" ".format(err)) + ) + + def download_btn_clicked(self): + """ Runs logic after the asset download button has been clicked. + """ + auto_asset_loading = settings_manager.get_value( + Settings.AUTO_ASSET_LOADING, + False, + setting_type=bool + ) + + for key, asset in self.download_assets.items(): + for item in self.items: + if key in item.stac_object.assets: + asset = item.stac_object.assets[key] + rasset = ResourceAsset( + href=asset.href, + title=asset.title or key, + description=asset.description, + type=asset.media_type or asset.type, + roles=asset.roles or [] + ) + + try: + download_task = QgsTask.fromFunction( + 'Download asset function', + self.download_asset(rasset, item.id, auto_asset_loading) + ) + QgsApplication.taskManager().addTask(download_task) + + except Exception as err: + self.update_inputs(True) + log(tr("An error occured when running task for" + " downloading asset {}, error message \"{}\" ").format( + asset.title, + str(err)) + ) + + def download_asset(self, asset, item_id, load_asset=False): + """ Downloads the passed asset into directory defined in the plugin settings. + + :param asset: Item asset + :type asset: models.ResourceAsset + + :param load_asset: Whether to load an asset after download has finished. + :type load_asset: bool + """ + self.update_inputs(False) + download_folder = settings_manager.get_value( + Settings.DOWNLOAD_FOLDER + ) + item_folder = os.path.join(download_folder, item_id) \ + if download_folder else None + feedback = QgsProcessingFeedback() + try: + if item_folder: + os.mkdir(item_folder) + except FileExistsError as fe: + pass + except FileNotFoundError as fn: + self.update_inputs(True) + self.main_widget.show_message( + tr("Folder {} is not found").format(download_folder), + Qgis.Critical + ) + return + except PermissionError as pe: + self.update_inputs(True) + self.main_widget.show_message( + tr("Permission error writing in download folder"), + Qgis.Critical + ) + return + + url = self.sign_asset_href(asset.href) + extension = Path(asset.href).suffix + extension_suffix = extension.split('?')[0] if extension else "" + title = f"{asset.title}{extension_suffix}" + + title = self.clean_filename(title) + + output = os.path.join( + item_folder, title + ) if item_folder else QgsProcessing.TEMPORARY_OUTPUT + params = {'URL': url, 'OUTPUT': output} + + self.download_result["file"] = output + + layer_types = [ + AssetLayerType.COG.value, + AssetLayerType.COPC.value, + AssetLayerType.GEOTIFF.value, + AssetLayerType.NETCDF.value, + ] + try: + self.main_widget.show_message( + tr("Download for file {} to {} has started." + ).format( + title, + item_folder + ), + level=Qgis.Info + ) + self.main_widget.show_progress( + f"Downloading {url}", + minimum=0, + maximum=100, + ) + + feedback.progressChanged.connect( + self.main_widget.update_progress_bar + ) + feedback.progressChanged.connect(self.download_progress) + + results = processing.run( + "qgis:filedownloader", + params, + feedback=feedback + ) + + # After asset download has finished, load the asset + # if it can be loaded as a QGIS map layer. + if results and load_asset and asset.type in ''.join(layer_types): + asset.href = self.download_result["file"] + asset.name = title + asset.type = AssetLayerType.GEOTIFF.value \ + if AssetLayerType.COG.value in asset.type else asset.type + self.load_asset(asset) + except Exception as e: + self.update_inputs(True) + self.main_widget.show_message( + tr("Error in downloading file, {}").format(str(e)) + ) + + def update_inputs(self, enabled): + """ Updates the inputs widgets state in the main search item widget. + + :param enabled: Whether to enable the inputs or disable them. + :type enabled: bool + """ + self.scroll_area.setEnabled(enabled) + # self.parent.update_inputs(enabled) + self.load_btn.setEnabled( + enabled and len(self.load_assets.items()) > 0 + ) + self.download_btn.setEnabled( + enabled and len(self.download_assets.items()) > 0 + ) class LayerLoader(QgsTask): """ Prepares and loads items assets inside QGIS as layers.""" diff --git a/src/qgis_stac/gui/qgis_stac_widget.py b/src/qgis_stac/gui/qgis_stac_widget.py index 74018c9..0c75d08 100644 --- a/src/qgis_stac/gui/qgis_stac_widget.py +++ b/src/qgis_stac/gui/qgis_stac_widget.py @@ -5,6 +5,7 @@ """ import os +from copy import deepcopy from functools import partial @@ -41,7 +42,8 @@ SearchFilters, SortField, SortOrder, - QueryableFetchType + QueryableFetchType, + ResourceAsset ) from ..api.client import Client @@ -54,7 +56,8 @@ tr, ) -from .result_item_widget import add_footprint_helper, ResultItemWidget +from .result_item_widget import add_footprint_helper, add_footprints_helper, ResultItemWidget +from .assets_dialog import ItemsAssetsDialog WidgetUi, _ = loadUiType( os.path.join(os.path.dirname(__file__), "../ui/qgis_stac_main.ui") @@ -93,6 +96,13 @@ def __init__( self.footprint_btn.clicked.connect( self.footprint_btn_clicked ) + self.items_btn.clicked.connect( + self.open_selected_items_dialog + ) + self.items_btn.setEnabled( + len(self.footprint_items.items()) > 0 + ) + self.all_footprints_btn.clicked.connect( self.all_footprints_btn_clicked ) @@ -218,6 +228,10 @@ def __init__( self.queryable_property_widgets = [] self.queryable_properties = [] + self.items_assets_btn.setEnabled(self.result_items is not None) + self.items_assets_btn.clicked.connect(self.open_all_items_dialog) + + def prepare_plugin_settings(self): """ Initializes all the plugin related settings""" @@ -766,6 +780,9 @@ def display_results(self, results, pagination=None): self.footprint_btn.setEnabled( False ) + self.items_btn.setEnabled( + False + ) self.all_footprints_btn.setEnabled( len(self.result_items) > 0 ) @@ -854,51 +871,138 @@ def footprint_selected(self, item): self.footprint_btn.setEnabled( len(self.footprint_items.items()) > 0 ) - + self.items_btn.setText( + f"Add the selected item(s) ({len(self.footprint_items.items())})" + ) + self.items_btn.setEnabled( + len(self.footprint_items.items()) > 0 + ) def footprint_deselected(self, item): """ Removes the passed item from the list of the footprints to be added. """ self.footprint_items.pop(item.id) - self.footprint_btn.setText( - f"Add the selected footprint(s) ({len(self.footprint_items.items())})" - ) if self.footprint_items else \ + if self.footprint_items: + self.footprint_btn.setText( + f"Add the selected footprint(s) ({len(self.footprint_items.items())})" + ) + self.items_btn.setText( + f"Add the selected item(s) ({len(self.footprint_items.items())})" + ) + else: self.footprint_btn.setText( "Add the selected footprint(s)" ) + self.items_btn.setText( + f"Add the selected item(s)" + ) + self.footprint_btn.setEnabled( len(self.footprint_items.items()) > 0 ) - + self.items_btn.setEnabled( + len(self.footprint_items.items()) > 0 + ) def footprint_btn_clicked(self): """ Adds selected footprints as map layers.""" - for key, item in self.footprint_items.items(): - try: - footprint_task = QgsTask.fromFunction( - 'Add footprints', - add_footprint_helper(item, self) - ) - QgsApplication.taskManager().addTask(footprint_task) - except Exception as err: - log( - tr("Error loading item footprint {}, {}". - format(item.id, err)) - ) + items = self.footprint_items.values() + try: + footprint_task = QgsTask.fromFunction( + 'Add footprints', + add_footprints_helper(items, self) + ) + QgsApplication.taskManager().addTask(footprint_task) + except Exception as err: + log( + tr("Error loading item footprints {}". + format(err)) + ) def all_footprints_btn_clicked(self): """ Adds all footprints for the current page items as map layers.""" - for item in self.result_items: - try: - footprint_task = QgsTask.fromFunction( - 'Add footprint', - add_footprint_helper(item, self) - ) - QgsApplication.taskManager().addTask(footprint_task) - except Exception as err: - log( - tr("Error loading item footprint {}, {}". - format(item.id, err)) - ) + try: + footprint_task = QgsTask.fromFunction( + 'Add footprint', + add_footprints_helper(self.result_items, self) + ) + QgsApplication.taskManager().addTask(footprint_task) + except Exception as err: + log( + tr("Error loading item footprints {}".format(err)) + ) + + def open_all_items_dialog(self): + """ Opens the assets dialog for the STAC item. + Queries the plugin Item from the plugin settings to get the + most recent updated assets. + """ + # connection = settings_manager.get_current_connection() + # saved_item = settings_manager.get_items( + # connection.id, + # [str(self.item.item_uuid)] + # ) + # if saved_item: + items = self.result_items + if len(set([item.collection for item in items])) > 1: + raise NotImplementedError( + "Adding assets from multiple collections is not supported.\n" + "Please select a single collection.") + + item = deepcopy(items[0]) + if item.collection is not None: + item.id = item.collection + stored_assets = [ + ResourceAsset( + href=asset.href, + title=key, + description=asset.description, + type=asset.media_type, + roles=asset.roles or [] + ) + for key, asset in item.stac_object.assets.items() + ] + item.assets = stored_assets + + assets_dialog = ItemsAssetsDialog( + item, + parent=self, + main_widget=self, + items=items, + ) + assets_dialog.exec_() + + def open_selected_items_dialog(self): + """ Opens the assets dialog for the selected STAC items, + based on the first item assets. + """ + items = list(self.footprint_items.values()) + if len(set([item.collection for item in items])) > 1: + raise NotImplementedError( + "Adding assets from multiple collections is not supported.\n" + "Please select a single collection.") + + item = deepcopy(items[0]) + if item.collection is not None: + item.id = item.collection + stored_assets = [ + ResourceAsset( + href=asset.href, + title=key, + description=asset.description, + type=asset.media_type, + roles=asset.roles or [] + ) + for key, asset in item.stac_object.assets.items() + ] + item.assets = stored_assets + + assets_dialog = ItemsAssetsDialog( + item, + parent=self, + main_widget=self, + items=items, + ) + assets_dialog.exec_() def clear_search_results(self): """ Clear current search results from the UI""" diff --git a/src/qgis_stac/gui/result_item_widget.py b/src/qgis_stac/gui/result_item_widget.py index 807d419..86967d6 100644 --- a/src/qgis_stac/gui/result_item_widget.py +++ b/src/qgis_stac/gui/result_item_widget.py @@ -421,3 +421,46 @@ def add_footprint_helper(item, main_widget): ).format(item.id), level=Qgis.Critical ) + +from ..lib.pystac import ItemCollection +def add_footprints_helper(items, main_widget): + """ Adds the item footprint inside QGIS as a map layer + + :param item: STAC item whose footprint is going to be added + :type item: Item + + :param main_widget: Parent widget that the function is called from + :type main_widget: QWidget + """ + layer_file = tempfile.NamedTemporaryFile( + mode="w+", + suffix='.geojson', + delete=False + ) + layer_name = "stac_footprints" + ItemCollection([item.stac_object for item in items]).save_object(layer_file.name) + + layer = QgsVectorLayer( + layer_file.name, + layer_name, + AssetLayerType.VECTOR.value + ) + if layer.isValid(): + QgsProject.instance().addMapLayer(layer) + main_widget.show_message( + tr( + "Successfully loaded footprint layer for item {}." + ).format( + ", ".join([item.id for item in items]) + ), + level=Qgis.Info + ) + + else: + main_widget.show_message( + tr( + "Couldn't load footprint into QGIS for items {}," + " its layer is not valid." + ).format(", ".join([item.id for item in items])), + level=Qgis.Critical + ) diff --git a/src/qgis_stac/ui/qgis_stac_main.ui b/src/qgis_stac/ui/qgis_stac_main.ui index b541bd4..b8063db 100644 --- a/src/qgis_stac/ui/qgis_stac_main.ui +++ b/src/qgis_stac/ui/qgis_stac_main.ui @@ -896,18 +896,50 @@ Add the selected footprints + + Add the footprint of the selected items in a vector layer + + + + + + + false + + + + 0 + 0 + + + + Add the selected items + + + Add assets of the selected items as new layers + - Add all current items footprints at once as separate layers + Add the footprint of all current items in a vector layer Add all footprints + + + + Add assets of all current items + + + Add all items assets + + + diff --git a/src/qgis_stac/ui/result_item_widget.ui b/src/qgis_stac/ui/result_item_widget.ui index c3414bb..4dbec0d 100644 --- a/src/qgis_stac/ui/result_item_widget.ui +++ b/src/qgis_stac/ui/result_item_widget.ui @@ -181,10 +181,10 @@ - Select footprint and add it to the list of footprints to be added. + Select item and add it to the list of footprints or items assets to be added. - Select footprint + Select item