class ResourceSharingDialog(QDialog, FORM_CLASS):
    TAB_ALL = 0
    TAB_INSTALLED = 1
    TAB_SETTINGS = 2

    def __init__(self, parent=None, iface=None):
        """Constructor.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        super(ResourceSharingDialog, self).__init__(parent)
        self.setupUi(self)
        self.iface = iface

        # Reconfigure UI
        self.setModal(True)
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)
        self.button_install.setEnabled(False)
        self.button_open.setEnabled(False)
        self.button_uninstall.setEnabled(False)

        # Set up the "main menu" - QListWidgetItem
        # All collections
        icon_all = QIcon()
        icon_all.addFile(str(resources_path('img', 'plugin.svg')), QSize(),
                         QIcon.Normal, QIcon.Off)
        item_all = QListWidgetItem()
        item_all.setIcon(icon_all)
        item_all.setText(self.tr('All collections'))
        # Installed collections
        icon_installed = QIcon()
        icon_installed.addFile(
            str(resources_path('img', 'plugin-installed.svg')), QSize(),
            QIcon.Normal, QIcon.Off)
        item_installed = QListWidgetItem()
        item_installed.setIcon(icon_installed)
        item_installed.setText(self.tr('Installed collections'))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        # Settings / repositories
        icon_settings = QIcon()
        icon_settings.addFile(str(resources_path('img', 'settings.svg')),
                              QSize(), QIcon.Normal, QIcon.Off)
        item_settings = QListWidgetItem()
        item_settings.setIcon(icon_settings)
        item_settings.setText(self.tr('Settings'))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)

        # Add the items to the list widget
        self.menu_list_widget.addItem(item_all)
        self.menu_list_widget.addItem(item_installed)
        self.menu_list_widget.addItem(item_settings)

        # Init the message bar
        self.message_bar = QgsMessageBar(self)
        self.message_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        self.vlayoutRightColumn.insertWidget(0, self.message_bar)

        # Progress dialog for long running processes
        self.progress_dialog = None

        # Init the repository manager dialog
        self.repository_manager = RepositoryManager()
        self.collection_manager = CollectionManager()
        # Collections list view
        self.collections_model = QStandardItemModel(0, 1)
        self.collections_model.sort(0, Qt.AscendingOrder)
        self.collection_proxy = CustomSortFilterProxyModel(self)
        self.collection_proxy.setSourceModel(self.collections_model)
        self.list_view_collections.setModel(self.collection_proxy)
        # Active selected collection
        self._selected_collection_id = None

        # Slots
        self.button_add.clicked.connect(self.add_repository)
        self.button_edit.clicked.connect(self.edit_repository)
        self.button_delete.clicked.connect(self.delete_repository)
        self.button_reload.clicked.connect(self.reload_repositories)
        self.menu_list_widget.currentRowChanged.connect(self.set_current_tab)
        self.list_view_collections.selectionModel().currentChanged.connect(
            self.on_list_view_collections_clicked)
        self.line_edit_filter.textChanged.connect(self.filter_collections)
        self.button_install.clicked.connect(self.install_collection)
        self.button_open.clicked.connect(self.open_collection)
        self.button_uninstall.clicked.connect(self.uninstall_collection)
        self.button_box.button(QDialogButtonBox.Help).clicked.connect(
            self.open_help)

        # Populate the repositories widget and collections list view
        self.populate_repositories_widget()
        self.reload_collections_model()

    def set_current_tab(self, index):
        """Set stacked widget based on the active tab.

        :param index: The index of the active widget (in the list widget).
        :type index: int
        """
        # Clear message bar
        self.message_bar.clearWidgets()
        if index == (self.menu_list_widget.count() - 1):
            # Last menu entry - Settings
            self.stacked_menu_widget.setCurrentIndex(1)
        else:
            # Not settings, must be Collections (all or installed)
            if index == 1:
                # Installed collections
                self.collection_proxy.accepted_status = \
                    COLLECTION_INSTALLED_STATUS
                # Set the web view
                title = self.tr('Installed Collections')
                description = self.tr(
                    'On the left you see the list of all the '
                    'installed collections.')
            else:
                # All collections (0)
                self.collection_proxy.accepted_status = COLLECTION_ALL_STATUS
                # Set the web view
                title = self.tr('All Collections')
                description = self.tr(
                    'On the left you see a list of all the collections '
                    'that are available from the registered repositories.<br> '
                    'Installed collections are emphasized (in <b>bold</b>).')

            context = {
                'resources_path': str(resources_path()),
                'title': title,
                'description': description
            }
            self.web_view_details.setHtml(
                render_template('tab_description.html', context))
            self.stacked_menu_widget.setCurrentIndex(0)

    def add_repository(self):
        """Open add repository dialog."""
        dlg = ManageRepositoryDialog(self)
        if not dlg.exec_():
            return

        for repoName, repo in self.repository_manager.directories.items():
            if dlg.line_edit_url.text().strip() == repo['url']:
                self.message_bar.pushMessage(
                    self.tr(
                        'Unable to add another repository with the same URL!'),
                    Qgis.Warning, 5)
                return
            if dlg.line_edit_name.text().strip() == repoName:
                self.message_bar.pushMessage(
                    self.tr('Repositories must have unique names!'),
                    Qgis.Warning, 5)
                return

        repo_name = dlg.line_edit_name.text()
        repo_url = dlg.line_edit_url.text().strip()
        repo_auth_cfg = dlg.line_edit_auth_id.text().strip()
        if repo_name in self.repository_manager.directories:
            repo_name += '(2)'

        # Show progress dialog
        self.show_progress_dialog("Fetching repository's metadata")

        # Add repository
        try:
            status, adderror = self.repository_manager.add_directory(
                repo_name, repo_url, repo_auth_cfg)
            if status:
                self.message_bar.pushMessage(
                    self.tr('Repository was successfully added'), Qgis.Success,
                    5)
            else:
                self.message_bar.pushMessage(
                    self.tr('Unable to add repository: %s') % adderror,
                    Qgis.Warning, 5)
        except Exception as e:
            self.message_bar.pushMessage(self.tr('%s') % e, Qgis.Warning, 5)
        finally:
            self.progress_dialog.hide()

        # Reload data and widget
        self.reload_data_and_widget()

        # Deactivate edit and delete button
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)

    def edit_repository(self):
        """Open edit repository dialog."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item:
            repo_name = selected_item.text(0)

        if not repo_name:
            return

        # Check if it is among the officially approved QGIS repositories
        settings = QgsSettings()
        settings.beginGroup(repo_settings_group())
        if settings.value(repo_name + '/url') in \
                self.repository_manager._online_directories.values():
            self.message_bar.pushMessage(
                self.tr('You can not edit the official repositories!'),
                Qgis.Warning, 5)
            return

        dlg = ManageRepositoryDialog(self)
        dlg.line_edit_name.setText(repo_name)
        dlg.line_edit_url.setText(
            self.repository_manager.directories[repo_name]['url'])
        dlg.line_edit_auth_id.setText(
            self.repository_manager.directories[repo_name]['auth_cfg'])

        if not dlg.exec_():
            return

        # Check if the changed URL is already present and that
        # the new repository name is unique
        new_url = dlg.line_edit_url.text().strip()
        old_url = self.repository_manager.directories[repo_name]['url']
        new_name = dlg.line_edit_name.text().strip()
        for repoName, repo in self.repository_manager.directories.items():
            if new_url == repo['url'] and (old_url != new_url):
                self.message_bar.pushMessage(
                    self.tr('Unable to add another repository with the same '
                            'URL!'), Qgis.Warning, 5)
                return
            if new_name == repoName and (repo_name != new_name):
                self.message_bar.pushMessage(
                    self.tr('Repositories must have unique names!'),
                    Qgis.Warning, 5)
                return

        # Redundant
        if (new_name in self.repository_manager.directories) and (new_name !=
                                                                  repo_name):
            new_name += '(2)'

        new_auth_cfg = dlg.line_edit_auth_id.text()

        # Show progress dialog
        self.show_progress_dialog("Fetching repository's metadata")

        # Edit repository
        try:
            status, editerror = self.repository_manager.edit_directory(
                repo_name, new_name, old_url, new_url, new_auth_cfg)
            if status:
                self.message_bar.pushMessage(
                    self.tr('Repository is successfully updated'),
                    Qgis.Success, 5)
            else:
                self.message_bar.pushMessage(
                    self.tr('Unable to edit repository: %s') % editerror,
                    Qgis.Warning, 5)
        except Exception as e:
            self.message_bar.pushMessage(self.tr('%s') % e, Qgis.Warning, 5)
        finally:
            self.progress_dialog.hide()

        # Reload data and widget
        self.reload_data_and_widget()

        # Deactivate the edit and delete buttons
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)

    def delete_repository(self):
        """Delete a repository in the tree widget."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item:
            repo_name = selected_item.text(0)

        if not repo_name:
            return
        # Check if it is among the offical repositories
        repo_url = self.repository_manager.directories[repo_name]['url']
        if repo_url in self.repository_manager._online_directories.values():
            self.message_bar.pushMessage(
                self.tr('You can not remove official repositories!'),
                Qgis.Warning, 5)
            return

        warning = self.tr('Are you sure you want to remove the following '
                          'repository?') + '\n' + repo_name
        if QMessageBox.warning(self, self.tr('QGIS Resource Sharing'), warning,
                               QMessageBox.Yes,
                               QMessageBox.No) == QMessageBox.No:
            return

        # Remove repository
        installed_collections = \
            self.collection_manager.get_installed_collections(repo_url)
        if installed_collections:
            message = ('You have installed collections from this '
                       'repository. Please uninstall them first!')
            self.message_bar.pushMessage(message, Qgis.Warning, 5)
        else:
            self.repository_manager.remove_directory(repo_name)
            # Reload data and widget
            self.reload_data_and_widget()
            # Deactivate the edit and delete buttons
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)

    def reload_repositories(self):
        """Slot for when user clicks reload repositories button."""
        # Show progress dialog
        self.show_progress_dialog('Reloading all repositories')

        for repo_name in self.repository_manager.directories:
            directory = self.repository_manager.directories[repo_name]
            url = directory['url']
            auth_cfg = directory['auth_cfg']
            try:
                status, reloaderror = self.repository_manager.reload_directory(
                    repo_name, url, auth_cfg)
                if status:
                    self.message_bar.pushMessage(
                        self.tr('Repository %s is successfully reloaded') %
                        repo_name, Qgis.Info, 5)
                else:
                    self.message_bar.pushMessage(
                        self.tr('Unable to reload %s: %s') %
                        (repo_name, reloaderror), Qgis.Warning, 5)
            except Exception as e:
                self.message_bar.pushMessage(
                    self.tr('%s') % e, Qgis.Warning, 5)

        self.progress_dialog.hide()
        # Reload data and widget
        self.reload_data_and_widget()

    def install_collection(self):
        """Slot for when the user clicks the install/reinstall button."""
        # Save the current index to enable selection after installation
        self.current_index = self.list_view_collections.currentIndex()
        self.show_progress_dialog('Starting installation...')
        self.progress_dialog.canceled.connect(self.install_canceled)

        self.installer_thread = QThread()
        self.installer_worker = CollectionInstaller(
            self.collection_manager, self._selected_collection_id)
        self.installer_worker.moveToThread(self.installer_thread)
        self.installer_worker.finished.connect(self.install_finished)
        self.installer_worker.aborted.connect(self.install_aborted)
        self.installer_worker.progress.connect(self.install_progress)
        self.installer_thread.started.connect(self.installer_worker.run)
        self.installer_thread.start()

    def install_finished(self):
        # Process the result
        self.progress_dialog.hide()
        installStatus = self.installer_worker.install_status
        if not installStatus:
            message = self.installer_worker.error_message
        # Clean up the worker and thread
        self.installer_worker.deleteLater()
        self.installer_thread.quit()
        self.installer_thread.wait()
        self.installer_thread.deleteLater()

        if installStatus:
            self.reload_collections_model()
            # Report what has been installed
            message = '<b>%s</b> was successfully installed, containing:\n<ul>' % (
                config.COLLECTIONS[self._selected_collection_id]['name'])
            number = 0
            if 'style' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['style']
                message = message + '\n<li> ' + str(
                    number) + ' Layer style (QML) file'
                if number > 1:
                    message = message + 's'
            if 'symbol' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['symbol']
                message = message + '\n<li> ' + str(
                    number) + ' XML symbol file'
                if number > 1:
                    message = message + 's'
            if 'svg' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['svg']
                message = message + '\n<li> ' + str(number) + ' SVG file'
                if number > 1:
                    message = message + 's'
            if 'models' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['models']
                message = message + '\n<li> ' + str(number) + ' model'
                if number > 1:
                    message = message + 's'
            if 'expressions' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['expressions']
                message = message + '\n<li> ' + str(
                    number) + ' expression file'
                if number > 1:
                    message = message + 's'
            if 'processing' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['processing']
                message = message + '\n<li> ' + str(
                    number) + ' processing script'
                if number > 1:
                    message = message + 's'
            if 'rscripts' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['rscripts']
                message = message + '\n<li> ' + str(number) + ' R script'
                if number > 1:
                    message = message + 's'
            message = message + '\n</ul>'
        QMessageBox.information(self, 'Resource Sharing', message)
        self.populate_repositories_widget()
        # Set the selection
        oldRow = self.current_index.row()
        newIndex = self.collections_model.createIndex(oldRow, 0)
        selection_model = self.list_view_collections.selectionModel()
        selection_model.setCurrentIndex(newIndex,
                                        selection_model.ClearAndSelect)
        selection_model.select(newIndex, selection_model.ClearAndSelect)
        # Update the buttons
        self.button_install.setEnabled(True)
        self.button_install.setText('Reinstall')
        self.button_open.setEnabled(True)
        self.button_uninstall.setEnabled(True)

        self.show_collection_metadata(self._selected_collection_id)

    def install_canceled(self):
        self.progress_dialog.hide()
        self.show_progress_dialog('Cancelling installation...')
        self.installer_worker.abort()

    def install_aborted(self):
        if self.installer_thread.isRunning():
            self.installer_thread.quit()
        self.installer_thread.finished.connect(self.progress_dialog.hide)

    def install_progress(self, text):
        self.progress_dialog.setLabelText(text)

    def uninstall_collection(self):
        """Slot called when user clicks the uninstall button."""
        # get the QModelIndex for the item to be uninstalled
        uninstall_index = self.list_view_collections.currentIndex()
        coll_id = self._selected_collection_id
        try:
            self.collection_manager.uninstall(coll_id)
        except Exception as e:
            LOGGER.error('Could not uninstall collection ' +
                         config.COLLECTIONS[coll_id]['name'] + ':\n' + str(e))
        else:
            QMessageBox.information(
                self, 'Resource Sharing',
                'The collection was successfully uninstalled!')
            self.reload_collections_model()
            # Fix the GUI
            currentMenuRow = self.menu_list_widget.currentRow()
            self.set_current_tab(currentMenuRow)
            self.populate_repositories_widget()

            rowCount = self.collection_proxy.rowCount()
            if rowCount > 0:
                # Set the current (and selected) row in the listview
                newRow = uninstall_index.row()
                # Check if this was the last element
                rowCount = self.collection_proxy.rowCount()
                if newRow == rowCount:
                    newRow = newRow - 1
                # Select the new current element
                newIndex = self.collections_model.createIndex(newRow, 0)
                selection_model = self.list_view_collections.selectionModel()
                selection_model.setCurrentIndex(newIndex,
                                                selection_model.ClearAndSelect)
                # Get the id of the current collection
                proxyModel = self.list_view_collections.model()
                proxyIndex = proxyModel.index(newRow, 0)
                current_coll_id = proxyIndex.data(COLLECTION_ID_ROLE)
                self._selected_collection_id = current_coll_id
                # Update buttons
                status = config.COLLECTIONS[current_coll_id]['status']
                if status == COLLECTION_INSTALLED_STATUS:
                    self.button_install.setEnabled(True)
                    self.button_install.setText('Reinstall')
                    self.button_open.setEnabled(True)
                    self.button_uninstall.setEnabled(True)
                else:
                    self.button_install.setEnabled(True)
                    self.button_install.setText('Install')
                    self.button_open.setEnabled(False)
                    self.button_uninstall.setEnabled(False)
                # Update the web_view_details frame
                self.show_collection_metadata(current_coll_id)
            else:
                self.button_install.setEnabled(False)
                self.button_install.setText('Install')
                self.button_open.setEnabled(False)
                self.button_uninstall.setEnabled(False)

    def open_collection(self):
        """Slot for when user clicks 'Open' button."""
        collection_path = local_collection_path(self._selected_collection_id)
        directory_url = QUrl.fromLocalFile(str(collection_path))
        QDesktopServices.openUrl(directory_url)

    def reload_data_and_widget(self):
        """Reload repositories and collections and update widgets related."""
        self.reload_repositories_widget()
        self.reload_collections_model()

    def reload_repositories_widget(self):
        """Refresh tree repositories using new repositories data."""
        self.repository_manager.load_directories()
        self.populate_repositories_widget()

    def populate_repositories_widget(self):
        """Populate the current dictionary repositories to the tree widget."""
        # Clear the current tree widget
        self.tree_repositories.clear()
        installed_collections = \
            self.collection_manager.get_installed_collections()
        # Export the updated ones from the repository manager
        for repo_name in self.repository_manager.directories:
            url = self.repository_manager.directories[repo_name]['url']
            item = QTreeWidgetItem(self.tree_repositories, REPOSITORY_ITEM)
            item.setText(0, repo_name)
            item.setText(1, url)
            for coll_id in config.COLLECTIONS:
                if ('repository_name' in config.COLLECTIONS[coll_id].keys()
                        and config.COLLECTIONS[coll_id]['repository_name']
                        == repo_name):
                    coll_name = config.COLLECTIONS[coll_id]['name']
                    coll_tags = config.COLLECTIONS[coll_id]['tags']
                    collectionItem = QTreeWidgetItem(item, COLLECTION_ITEM)
                    collitemtext = coll_name
                    if installed_collections and coll_id in installed_collections.keys(
                    ):
                        collitemtext = coll_name + ' (installed)'
                        collectionFont = QFont()
                        collectionFont.setWeight(60)
                        collectionItem.setFont(0, collectionFont)
                    collectionItem.setText(0, collitemtext)
                    collectionItem.setText(1, coll_tags)
        self.tree_repositories.resizeColumnToContents(0)
        self.tree_repositories.resizeColumnToContents(1)
        self.tree_repositories.sortItems(1, Qt.AscendingOrder)

    def reload_collections_model(self):
        """Reload the collections model with the current collections."""
        self.collections_model.clear()
        installed_collections = \
            self.collection_manager.get_installed_collections()
        for id in config.COLLECTIONS:
            collection_name = config.COLLECTIONS[id]['name']
            collection_author = config.COLLECTIONS[id]['author']
            collection_tags = config.COLLECTIONS[id]['tags']
            collection_description = config.COLLECTIONS[id]['description']
            collection_status = config.COLLECTIONS[id]['status']
            repository_name = ''
            if 'repository_name' in config.COLLECTIONS[id].keys():
                repository_name = config.COLLECTIONS[id]['repository_name']
            item = QStandardItem(collection_name + ' (' + repository_name +
                                 ')')
            item.setEditable(False)
            item.setData(id, COLLECTION_ID_ROLE)
            item.setData(collection_name, COLLECTION_NAME_ROLE)
            item.setData(collection_description, COLLECTION_DESCRIPTION_ROLE)
            item.setData(collection_author, COLLECTION_AUTHOR_ROLE)
            item.setData(collection_tags, COLLECTION_TAGS_ROLE)
            item.setData(collection_status, COLLECTION_STATUS_ROLE)
            # Make installed collections stand out
            if installed_collections and id in installed_collections.keys():
                collectionFont = QFont()
                collectionFont.setWeight(60)
                item.setFont(collectionFont)
            self.collections_model.appendRow(item)
        self.collections_model.sort(0, Qt.AscendingOrder)

    def on_tree_repositories_itemSelectionChanged(self):
        """Slot for the itemSelectionChanged signal of tree_repositories."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item and selected_item.type() == REPOSITORY_ITEM:
            if selected_item:
                repo_name = selected_item.text(0)
            if not repo_name:
                return
            if not repo_name in self.repository_manager.directories.keys():
                return
            repo_url = self.repository_manager.directories[repo_name]['url']
            # Disable the edit and delete buttons for "official" repositories
            if repo_url in self.repository_manager._online_directories.values(
            ):
                self.button_edit.setEnabled(False)
                self.button_delete.setEnabled(False)
            else:
                # Activate the edit and delete buttons
                self.button_edit.setEnabled(True)
                self.button_delete.setEnabled(True)
        elif selected_item and selected_item.type() == COLLECTION_ITEM:
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)
        else:
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)

    def on_list_view_collections_clicked(self, index):
        """Slot for when the list_view_collections is clicked."""
        real_index = self.collection_proxy.mapToSource(index)
        if real_index.row() != -1:
            collection_item = self.collections_model.itemFromIndex(real_index)
            collection_id = collection_item.data(COLLECTION_ID_ROLE)
            self._selected_collection_id = collection_id

            # Enable / disable buttons
            status = config.COLLECTIONS[self._selected_collection_id]['status']
            is_installed = status == COLLECTION_INSTALLED_STATUS
            if is_installed:
                self.button_install.setEnabled(True)
                self.button_install.setText('Reinstall')
                self.button_open.setEnabled(True)
                self.button_uninstall.setEnabled(True)
            else:
                self.button_install.setEnabled(True)
                self.button_install.setText('Install')
                self.button_open.setEnabled(False)
                self.button_uninstall.setEnabled(False)

            # Show  metadata
            self.show_collection_metadata(collection_id)

    @pyqtSlot(str)
    def filter_collections(self, text):
        search = QRegExp(text, Qt.CaseInsensitive, QRegExp.RegExp)
        self.collection_proxy.setFilterRegExp(search)

    def show_collection_metadata(self, id):
        """Show the collection metadata given the ID."""
        html = self.collection_manager.get_html(id)
        self.web_view_details.setHtml(html)

    def reject(self):
        """Slot when the dialog is closed."""
        # Serialize collections to settings
        self.repository_manager.serialize_repositories()
        self.done(0)

    def open_help(self):
        """Open help."""
        doc_url = QUrl('http://qgis-contribution.github.io/' +
                       'QGIS-ResourceSharing/')
        QDesktopServices.openUrl(doc_url)

    def show_progress_dialog(self, text):
        """Show infinite progress dialog with given text.

        :param text: Text as the label of the progress dialog
        :type text: str
        """
        if self.progress_dialog is None:
            self.progress_dialog = QProgressDialog(self)
            self.progress_dialog.setWindowModality(Qt.WindowModal)
            self.progress_dialog.setAutoClose(False)
            title = self.tr('Resource Sharing')
            self.progress_dialog.setWindowTitle(title)
            # Just use an infinite progress bar here
            self.progress_dialog.setMaximum(0)
            self.progress_dialog.setMinimum(0)
            self.progress_dialog.setValue(0)
            self.progress_dialog.setLabelText(text)

        self.progress_dialog.show()
Exemplo n.º 2
0
    def stop(self):
        self.mutex.lock()
        self.stopMe = 1
        self.mutex.unlock()

        QThread.wait(self)
class ThreediDockWidget(QtWidgets.QDockWidget, FORM_CLASS):

    closingPlugin = pyqtSignal()

    def __init__(self, iface, plugin_settings, parent=None):
        """Constructor."""
        super().__init__(parent)
        self.setupUi(self)
        self.iface = iface
        self.plugin_settings = plugin_settings
        self.communication = UICommunication(self.iface,
                                             "3Di Models and Simulations",
                                             self.lv_log)
        self.simulations_progresses_thread = None
        self.simulations_progresses_sentinel = None
        self.threedi_api = None
        self.current_user = None
        self.current_user_full_name = None
        self.organisations = {}
        self.current_local_schematisation = None
        self.build_options = BuildOptions(self)
        self.simulation_overview_dlg = None
        self.simulation_results_dlg = None
        self.upload_dlg = None
        self.btn_log_in_out.clicked.connect(self.on_log_in_log_out)
        self.btn_load_schematisation.clicked.connect(
            self.build_options.load_local_schematisation)
        self.btn_load_revision.clicked.connect(
            self.build_options.load_local_schematisation)
        self.btn_new.clicked.connect(self.build_options.new_schematisation)
        self.btn_download.clicked.connect(
            self.build_options.download_schematisation)
        self.btn_upload.clicked.connect(self.show_upload_dialog)
        self.btn_simulate.clicked.connect(self.show_simulation_overview)
        self.btn_results.clicked.connect(self.show_simulation_results)
        self.btn_manage.clicked.connect(self.on_manage)
        self.plugin_settings.settings_changed.connect(self.on_log_out)
        set_icon(self.btn_new, "new.svg")
        set_icon(self.btn_download, "download.svg")
        set_icon(self.btn_upload, "upload.svg")
        set_icon(self.btn_simulate, "api.svg")
        set_icon(self.btn_results, "results.svg")
        set_icon(self.btn_manage, "manage.svg")
        set_icon(self.btn_log_in_out, "arrow.svg")
        set_icon(self.btn_load_schematisation, "arrow.svg")
        set_icon(self.btn_load_revision, "arrow.svg")

    def closeEvent(self, event):
        if self.threedi_api is not None:
            self.on_log_out()
        self.current_local_schematisation = None
        self.update_schematisation_view()
        self.closingPlugin.emit()
        event.accept()

    def on_log_in_log_out(self):
        """Trigger log-in or log-out action."""
        if self.threedi_api is None:
            self.on_log_in()
        else:
            self.on_log_out()

    def on_log_in(self):
        """Handle logging-in."""
        log_in_dialog = LogInDialog(self)
        log_in_dialog.show()
        QTimer.singleShot(10, log_in_dialog.log_in_threedi)
        log_in_dialog.exec_()
        if log_in_dialog.LOGGED_IN:
            self.threedi_api = log_in_dialog.threedi_api
            self.current_user = log_in_dialog.user
            self.current_user_full_name = log_in_dialog.user_full_name
            self.organisations = log_in_dialog.organisations
            self.initialize_authorized_view()

    def on_log_out(self):
        """Handle logging-out."""
        if self.simulations_progresses_thread is not None:
            self.stop_fetching_simulations_progresses()
            self.simulation_overview_dlg.model_selection_dlg.unload_cached_layers(
            )
            self.simulation_overview_dlg = None
        if self.simulation_results_dlg is not None:
            self.simulation_results_dlg.terminate_download_thread()
            self.simulation_results_dlg = None
        if self.upload_dlg:
            self.upload_dlg.hide()
            self.upload_dlg = None
        self.threedi_api = None
        self.current_user = None
        self.current_user_full_name = None
        self.organisations.clear()
        self.label_user.setText("-")
        set_icon(self.btn_log_in_out, "arrow.svg")
        self.btn_log_in_out.setToolTip("Log in")

    def update_schematisation_view(self):
        """Method for updating loaded schematisation labels."""
        if self.current_local_schematisation:
            schema_name = self.current_local_schematisation.name
            schema_dir = self.current_local_schematisation.main_dir
            schema_label_text = f'<a href="file:///{schema_dir}">{schema_name}</a>'
            schema_tooltip = f"{schema_name}\n{schema_dir}"
            self.label_schematisation.setText(schema_label_text)
            self.label_schematisation.setOpenExternalLinks(True)
            self.label_schematisation.setToolTip(schema_tooltip)
            if self.current_local_schematisation.wip_revision:
                self.label_revision.setText(
                    str(self.current_local_schematisation.wip_revision.number)
                    or "")
            else:
                self.label_revision.setText("")
        else:
            self.label_schematisation.setText("")
            self.label_schematisation.setToolTip("")
            self.label_revision.setText("")

    def initialize_authorized_view(self):
        """Method for initializing processes after logging in 3Di API."""
        set_icon(self.btn_log_in_out, "x.svg")
        self.btn_log_in_out.setToolTip("Log out")
        self.label_user.setText(self.current_user_full_name)
        self.initialize_simulations_progresses_thread()
        self.initialize_simulation_overview()
        self.initialize_simulation_results()

    def initialize_simulations_progresses_thread(self):
        """Initializing of the background thread."""
        if self.simulations_progresses_thread is not None:
            self.terminate_fetching_simulations_progresses_thread()
        self.simulations_progresses_thread = QThread()
        username, personal_api_key = self.plugin_settings.get_3di_auth()
        self.simulations_progresses_sentinel = WSProgressesSentinel(
            self.threedi_api, self.plugin_settings.wss_url, personal_api_key)
        self.simulations_progresses_sentinel.moveToThread(
            self.simulations_progresses_thread)
        self.simulations_progresses_sentinel.thread_finished.connect(
            self.on_fetching_simulations_progresses_finished)
        self.simulations_progresses_sentinel.thread_failed.connect(
            self.on_fetching_simulations_progresses_failed)
        self.simulations_progresses_thread.started.connect(
            self.simulations_progresses_sentinel.run)
        self.simulations_progresses_thread.start()

    def stop_fetching_simulations_progresses(self):
        """Changing 'thread_active' flag inside background task that is fetching simulations progresses."""
        if self.simulations_progresses_sentinel is not None:
            self.simulations_progresses_sentinel.stop_listening()

    def on_fetching_simulations_progresses_finished(self, msg):
        """Method for cleaning up background thread after it sends 'thread_finished'."""
        self.communication.bar_info(msg)
        self.simulations_progresses_thread.quit()
        self.simulations_progresses_thread.wait()
        self.simulations_progresses_thread = None
        self.simulations_progresses_sentinel = None

    def on_fetching_simulations_progresses_failed(self, msg):
        """Reporting fetching progresses failure."""
        self.communication.bar_error(msg, log_text_color=Qt.red)

    def terminate_fetching_simulations_progresses_thread(self):
        """Forcing termination of background thread if it's still running."""
        if self.simulations_progresses_thread is not None and self.simulations_progresses_thread.isRunning(
        ):
            self.communication.bar_info(
                "Terminating fetching simulations progresses thread.")
            self.simulations_progresses_thread.terminate()
            self.communication.bar_info(
                "Waiting for fetching simulations progresses thread termination."
            )
            self.simulations_progresses_thread.wait()
            self.communication.bar_info(
                "Fetching simulations progresses worker terminated.")
            self.simulations_progresses_thread = None
            self.simulations_progresses_sentinel = None

    def initialize_simulation_overview(self):
        """Initialization of the Simulation Overview window."""
        self.simulation_overview_dlg = SimulationOverview(self)
        self.simulation_overview_dlg.label_user.setText(
            self.current_user_full_name)

    @api_client_required
    def show_simulation_overview(self):
        """Showing Simulation Overview with running simulations progresses."""
        if self.simulation_overview_dlg is None:
            self.initialize_simulation_overview()
        self.simulation_overview_dlg.show()
        self.simulation_overview_dlg.raise_()
        self.simulation_overview_dlg.activateWindow()

    def initialize_simulation_results(self):
        """Initialization of the Simulations Results window."""
        self.simulation_results_dlg = SimulationResults(self)

    @api_client_required
    def show_simulation_results(self):
        """Showing finished simulations."""
        if self.simulation_results_dlg is None:
            self.initialize_simulation_results()
        self.simulation_results_dlg.show()
        self.simulation_results_dlg.raise_()
        self.simulation_results_dlg.activateWindow()

    def initialize_upload_status(self):
        """Initialization of the Upload Status dialog."""
        self.upload_dlg = UploadOverview(self)

    @api_client_required
    def show_upload_dialog(self):
        """Show upload status dialog."""
        if self.upload_dlg is None:
            self.initialize_upload_status()
        self.upload_dlg.show()
        self.upload_dlg.raise_()
        self.upload_dlg.activateWindow()

    @staticmethod
    def on_manage():
        """Open 3Di management webpage."""
        webbrowser.open("https://management.3di.live/")
Exemplo n.º 4
0
    def stop(self):
        self.mutex.lock()
        self.stopMe = 1
        self.mutex.unlock()

        QThread.wait(self)
class NGWResourcesModelJob(QObject):
    started = pyqtSignal()
    statusChanged = pyqtSignal(str)
    warningOccurred = pyqtSignal(object)
    errorOccurred = pyqtSignal(object)
    finished = pyqtSignal()

    def __init__(self, parent, worker, model_response=None):
        """Create job.

            Arguments:
                job_id -- Job identification
                worker -- The class object inherits from NGWResourceModelJob
        """
        QObject.__init__(self, parent)
        self.__result = None
        self.__worker = worker
        self.__job_id = self.__worker.id
        self.__error = None
        self.__warnings = []
        # self.__job_id = "%s_%s" % (self.__worker.id, str(uuid.uuid1()))

        self.__worker.started.connect(self.started.emit)
        self.__worker.dataReceived.connect(self.__rememberResult)
        self.__worker.statusChanged.connect(self.statusChanged.emit)
        self.__worker.errorOccurred.connect(self.processJobError)
        self.__worker.warningOccurred.connect(self.processJobWarnings)

        self.model_response = model_response

    def setResponseObject(self, resp):
        self.model_response = resp
        self.model_response.job_id = self.__job_id

    def __rememberResult(self, result):
        self.__result = result

    def getJobId(self):
        return self.__job_id

    def getResult(self):
        return self.__result

    def error(self):
        return self.__error

    def processJobError(self, job_error):
        self.__error = job_error
        self.errorOccurred.emit(job_error)

    def processJobWarnings(self, job_error):
        if self.model_response:
            self.model_response._warnings.append(job_error)
        # self.warningOccurred.emit(job_error)

    def start(self):
        self.__thread = QThread(self)
        self.__worker.moveToThread(self.__thread)
        self.__worker.finished.connect(self.finishProcess)
        self.__thread.started.connect(self.__worker.run)

        self.__thread.start()

    def finishProcess(self):
        self.__worker.started.disconnect()
        self.__worker.dataReceived.disconnect()
        self.__worker.statusChanged.disconnect()
        self.__worker.errorOccurred.disconnect()
        self.__worker.warningOccurred.disconnect()
        self.__worker.finished.disconnect()

        self.__thread.quit()
        self.__thread.wait()

        self.finished.emit()
Exemplo n.º 6
0
class ResourceSharingDialog(QDialog, FORM_CLASS):
    TAB_ALL = 0
    TAB_INSTALLED = 1
    TAB_SETTINGS = 2

    def __init__(self, parent=None, iface=None):
        """Constructor.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        super(ResourceSharingDialog, self).__init__(parent)
        self.setupUi(self)
        self.iface = iface
        # Reconfigure UI
        self.setModal(True)
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)
        self.button_install.setEnabled(False)
        self.button_open.setEnabled(False)
        self.button_uninstall.setEnabled(False)
        # Set up the "main menu" - QListWidgetItem
        # All collections
        icon_all = QIcon()
        icon_all.addFile(str(resources_path("img", "plugin.svg")), QSize(),
                         QIcon.Normal, QIcon.Off)
        item_all = QListWidgetItem()
        item_all.setIcon(icon_all)
        item_all.setText(self.tr("All collections"))
        # Installed collections
        icon_installed = QIcon()
        icon_installed.addFile(
            str(resources_path("img", "plugin-installed.svg")),
            QSize(),
            QIcon.Normal,
            QIcon.Off,
        )
        item_installed = QListWidgetItem()
        item_installed.setIcon(icon_installed)
        item_installed.setText(self.tr("Installed collections"))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        # Settings / repositories
        icon_settings = QIcon()
        icon_settings.addFile(str(resources_path("img", "settings.svg")),
                              QSize(), QIcon.Normal, QIcon.Off)
        item_settings = QListWidgetItem()
        item_settings.setIcon(icon_settings)
        item_settings.setText(self.tr("Settings"))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        # Add the items to the list widget
        self.menu_list_widget.addItem(item_all)
        self.menu_list_widget.addItem(item_installed)
        self.menu_list_widget.addItem(item_settings)
        # Init the message bar
        self.message_bar = QgsMessageBar(self)
        self.message_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        self.vlayoutRightColumn.insertWidget(0, self.message_bar)
        # Progress dialog for long running processes
        self.progress_dialog = None
        # Init the repository manager dialog
        self.repository_manager = RepositoryManager()
        self.collection_manager = CollectionManager()
        # Collections list view
        self.collections_model = QStandardItemModel(0, 1)
        self.collections_model.sort(0, Qt.AscendingOrder)
        self.collection_proxy = CustomSortFilterProxyModel(self)
        self.collection_proxy.setSourceModel(self.collections_model)
        self.list_view_collections.setModel(self.collection_proxy)
        # Active selected collection
        self._sel_coll_id = None
        # Slots
        self.button_add.clicked.connect(self.add_repository)
        self.button_edit.clicked.connect(self.edit_repository)
        self.button_delete.clicked.connect(self.delete_repository)
        self.button_reload.clicked.connect(self.reload_repositories)
        self.button_reload_dir.clicked.connect(self.reload_off_res_directory)
        self.menu_list_widget.currentRowChanged.connect(self.set_current_tab)
        self.list_view_collections.selectionModel().currentChanged.connect(
            self.on_list_view_collections_clicked)
        self.line_edit_filter.textChanged.connect(self.filter_collections)
        self.button_install.clicked.connect(self.install_collection)
        self.button_open.clicked.connect(self.open_collection)
        self.button_uninstall.clicked.connect(self.uninstall_collection)
        self.button_box.button(QDialogButtonBox.Help).clicked.connect(
            self.open_help)
        # Populate the repositories widget and collections list view
        self.populate_repositories_widget()
        self.reload_collections_model()

    def set_current_tab(self, index):
        """Set stacked widget based on the active tab.

        :param index: The index of the active widget (in the list widget).
        :type index: int
        """
        # Clear message bar
        self.message_bar.clearWidgets()
        if index == (self.menu_list_widget.count() - 1):
            # Last menu entry - Settings
            self.stacked_menu_widget.setCurrentIndex(1)
        else:
            # Not settings, must be Collections (all or installed)
            if index == 1:
                # Installed collections
                self.collection_proxy.accepted_status = COLLECTION_INSTALLED_STATUS
                # Set the web view
                title = self.tr("Installed Collections")
                description = self.tr(
                    "On the left you see the list of all the "
                    "installed collections.")
            else:
                # All collections (0)
                self.collection_proxy.accepted_status = COLLECTION_ALL_STATUS
                # Set the web view
                title = self.tr("All Collections")
                description = self.tr(
                    "On the left you see a list of all the collections "
                    "that are available from the registered repositories.<br> "
                    "Installed collections are emphasized (in <b>bold</b>).")

            context = {
                "resources_path": str(resources_path()),
                "title": title,
                "description": description,
            }
            self.web_view_details.setHtml(
                render_template("tab_description.html", context))
            self.stacked_menu_widget.setCurrentIndex(0)

    def add_repository(self):
        """Open add repository dialog."""
        dlg = ManageRepositoryDialog(self)
        if not dlg.exec_():
            return
        for repoName, repo in self.repository_manager.directories.items():
            if dlg.line_edit_url.text().strip() == repo["url"]:
                self.message_bar.pushMessage(
                    self.tr(
                        "Unable to add another repository with the same URL!"),
                    Qgis.Warning,
                    5,
                )
                return
            if dlg.line_edit_name.text().strip() == repoName:
                self.message_bar.pushMessage(
                    self.tr("Repositories must have unique names!"),
                    Qgis.Warning, 5)
                return
        repo_name = dlg.line_edit_name.text()
        repo_url = dlg.line_edit_url.text().strip()
        repo_auth_cfg = dlg.line_edit_auth_id.text().strip()
        if repo_name in self.repository_manager.directories:
            repo_name += "(2)"
        # Show progress dialog
        self.show_progress_dialog("Fetching repository's metadata")
        # Add repository
        try:
            status, adderror = self.repository_manager.add_directory(
                repo_name, repo_url, repo_auth_cfg)
            if status:
                self.message_bar.pushMessage(
                    self.tr("Repository was successfully added"), Qgis.Success,
                    5)
            else:
                self.message_bar.pushMessage(
                    self.tr("Unable to add repository: %s") % adderror,
                    Qgis.Warning, 5)
        except Exception as e:
            self.message_bar.pushMessage(self.tr("%s") % e, Qgis.Warning, 5)
        finally:
            self.progress_dialog.hide()
        # Reload data and widget
        self.reload_data_and_widget()
        # Deactivate edit and delete button
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)

    def edit_repository(self):
        """Open edit repository dialog."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item:
            repo_name = selected_item.text(0)
        if not repo_name:
            return
        # Check if it is among the officially approved QGIS repositories
        settings = QgsSettings()
        settings.beginGroup(repo_settings_group())
        if (settings.value(repo_name + "/url")
                in self.repository_manager._online_directories.values()):
            self.message_bar.pushMessage(
                self.tr("You can not edit the official repositories!"),
                Qgis.Warning, 5)
            return
        dlg = ManageRepositoryDialog(self)
        dlg.line_edit_name.setText(repo_name)
        dlg.line_edit_url.setText(
            self.repository_manager.directories[repo_name]["url"])
        dlg.line_edit_auth_id.setText(
            self.repository_manager.directories[repo_name]["auth_cfg"])
        if not dlg.exec_():
            return
        # Check if the changed URL is already present and that
        # the new repository name is unique
        new_url = dlg.line_edit_url.text().strip()
        old_url = self.repository_manager.directories[repo_name]["url"]
        new_name = dlg.line_edit_name.text().strip()
        for repoName, repo in self.repository_manager.directories.items():
            if new_url == repo["url"] and (old_url != new_url):
                self.message_bar.pushMessage(
                    self.tr("Unable to add another repository with the same "
                            "URL!"),
                    Qgis.Warning,
                    5,
                )
                return
            if new_name == repoName and (repo_name != new_name):
                self.message_bar.pushMessage(
                    self.tr("Repositories must have unique names!"),
                    Qgis.Warning, 5)
                return
        # Redundant
        if (new_name in self.repository_manager.directories) and (new_name !=
                                                                  repo_name):
            new_name += "(2)"
        new_auth_cfg = dlg.line_edit_auth_id.text()
        # Show progress dialog
        self.show_progress_dialog("Fetching repository's metadata")
        # Edit repository
        try:
            status, editerror = self.repository_manager.edit_directory(
                repo_name, new_name, old_url, new_url, new_auth_cfg)
            if status:
                self.message_bar.pushMessage(
                    self.tr("Repository is successfully updated"),
                    Qgis.Success, 5)
            else:
                self.message_bar.pushMessage(
                    self.tr("Unable to edit repository: %s") % editerror,
                    Qgis.Warning,
                    5,
                )
        except Exception as e:
            self.message_bar.pushMessage(self.tr("%s") % e, Qgis.Warning, 5)
        finally:
            self.progress_dialog.hide()
        # Reload data and widget
        self.reload_data_and_widget()
        # Deactivate the edit and delete buttons
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)

    def delete_repository(self):
        """Delete a repository in the tree widget."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item:
            repo_name = selected_item.text(0)
        if not repo_name:
            return
        # Check if it is among the offical repositories
        repo_url = self.repository_manager.directories[repo_name]["url"]
        if repo_url in self.repository_manager._online_directories.values():
            self.message_bar.pushMessage(
                self.tr("You can not remove official repositories!"),
                Qgis.Warning, 5)
            return
        warning = (self.tr("Are you sure you want to remove the following "
                           "repository?") + "\n" + repo_name)
        if (QMessageBox.warning(
                self,
                self.tr("QGIS Resource Sharing"),
                warning,
                QMessageBox.Yes,
                QMessageBox.No,
        ) == QMessageBox.No):
            return

        # Remove repository
        installed_collections = self.collection_manager.get_installed_collections(
            repo_url)
        if installed_collections:
            message = ("You have installed collections from this "
                       "repository. Please uninstall them first!")
            self.message_bar.pushMessage(message, Qgis.Warning, 5)
        else:
            self.repository_manager.remove_directory(repo_name)
            # Reload data and widget
            self.reload_data_and_widget()
            # Deactivate the edit and delete buttons
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)

    def reload_off_res_directory(self):
        """Slot called when the user clicks the 'Reload directory'
        button."""
        # Show progress dialog
        self.show_progress_dialog("Reloading the official QGIS resource"
                                  " directory")
        self.repository_manager._online_directories = {}
        # Registered directories
        self.repository_manager._directories = {}
        self.repository_manager.fetch_online_directories()
        # Load directory of repositories from settings
        self.repository_manager.load_directories()
        self.message_bar.pushMessage("On-line directory reloaded", Qgis.Info,
                                     5)
        self.progress_dialog.hide()
        # Reload data and widget
        self.reload_data_and_widget()

    def reload_repositories(self):
        """Slot called when the user clicks the 'Reload repositories'
        button."""
        # Show progress dialog
        self.show_progress_dialog("Reloading all repositories")
        for repo_name in self.repository_manager.directories:
            directory = self.repository_manager.directories[repo_name]
            url = directory["url"]
            auth_cfg = directory["auth_cfg"]
            try:
                status, reloaderror = self.repository_manager.reload_directory(
                    repo_name, url, auth_cfg)
                if status:
                    self.message_bar.pushMessage(
                        self.tr("Repository %s is successfully reloaded") %
                        repo_name,
                        Qgis.Info,
                        5,
                    )
                else:
                    self.message_bar.pushMessage(
                        self.tr("Unable to reload %s: %s") %
                        (repo_name, reloaderror),
                        Qgis.Warning,
                        5,
                    )
            except Exception as e:
                self.message_bar.pushMessage(
                    self.tr("%s") % e, Qgis.Warning, 5)
        self.progress_dialog.hide()
        # Reload data and widget
        self.reload_data_and_widget()

    def install_collection(self):
        """Slot called when the user clicks the Install/Reinstall button."""
        # Save the current index to enable selection after installation
        self.current_index = self.list_view_collections.currentIndex()
        self.show_progress_dialog("Starting installation...")
        self.progress_dialog.canceled.connect(self.install_canceled)
        self.installer_thread = QThread()
        self.installer_worker = CollectionInstaller(self.collection_manager,
                                                    self._sel_coll_id)
        self.installer_worker.moveToThread(self.installer_thread)
        self.installer_worker.finished.connect(self.install_finished)
        self.installer_worker.aborted.connect(self.install_aborted)
        self.installer_worker.progress.connect(self.install_progress)
        self.installer_thread.started.connect(self.installer_worker.run)
        self.installer_thread.start()

    def install_finished(self):
        # Process the result
        self.progress_dialog.hide()
        installStatus = self.installer_worker.install_status
        if not installStatus:
            message = self.installer_worker.error_message
        # Clean up the worker and thread
        self.installer_worker.deleteLater()
        self.installer_thread.quit()
        self.installer_thread.wait()
        self.installer_thread.deleteLater()
        if installStatus:
            self.reload_collections_model()
            # Report what has been installed
            message = "<b>%s</b> was successfully installed, " "containing:\n<ul>" % (
                config.COLLECTIONS[self._sel_coll_id]["name"])
            number = 0
            for type_, description in SUPPORTED_RESOURCES_MAP.items():
                if type_ in config.COLLECTIONS[self._sel_coll_id].keys():
                    number = config.COLLECTIONS[self._sel_coll_id][type_]
                    message += (f"\n<li>{number} {description}"
                                f'{"s" if number > 1 else ""}'
                                f"</li>")
            message += "\n</ul>"
        QMessageBox.information(self, "Resource Sharing", message)
        self.populate_repositories_widget()
        # Set the selection
        oldRow = self.current_index.row()
        newIndex = self.collections_model.createIndex(oldRow, 0)
        selection_model = self.list_view_collections.selectionModel()
        selection_model.setCurrentIndex(newIndex,
                                        selection_model.ClearAndSelect)
        selection_model.select(newIndex, selection_model.ClearAndSelect)
        # Update the buttons
        self.button_install.setEnabled(True)
        self.button_install.setText("Reinstall")
        self.button_open.setEnabled(True)
        self.button_uninstall.setEnabled(True)
        self.show_collection_metadata(self._sel_coll_id)

    def install_canceled(self):
        self.progress_dialog.hide()
        self.show_progress_dialog("Cancelling installation...")
        self.installer_worker.abort()

    def install_aborted(self):
        if self.installer_thread.isRunning():
            self.installer_thread.quit()
        self.installer_thread.finished.connect(self.progress_dialog.hide)

    def install_progress(self, text):
        self.progress_dialog.setLabelText(text)

    def uninstall_collection(self):
        """Slot called when the user clicks the 'Uninstall' button."""
        # get the QModelIndex for the item to be uninstalled
        uninstall_index = self.list_view_collections.currentIndex()
        coll_id = self._sel_coll_id
        try:
            self.collection_manager.uninstall(coll_id)
        except Exception as e:
            LOGGER.error("Could not uninstall collection " +
                         config.COLLECTIONS[coll_id]["name"] + ":\n" + str(e))
        else:
            QMessageBox.information(
                self, "Resource Sharing",
                "The collection was successfully uninstalled!")
            self.reload_collections_model()
            # Fix the GUI
            currentMenuRow = self.menu_list_widget.currentRow()
            self.set_current_tab(currentMenuRow)
            self.populate_repositories_widget()
            rowCount = self.collection_proxy.rowCount()
            if rowCount > 0:
                # Set the current (and selected) row in the listview
                newRow = uninstall_index.row()
                # Check if this was the last element
                rowCount = self.collection_proxy.rowCount()
                if newRow == rowCount:
                    newRow = newRow - 1
                # Select the new current element
                newIndex = self.collections_model.createIndex(newRow, 0)
                selection_model = self.list_view_collections.selectionModel()
                selection_model.setCurrentIndex(newIndex,
                                                selection_model.ClearAndSelect)
                # Get the id of the current collection
                proxyModel = self.list_view_collections.model()
                proxyIndex = proxyModel.index(newRow, 0)
                current_coll_id = proxyIndex.data(COLLECTION_ID_ROLE)
                self._sel_coll_id = current_coll_id
                # Update buttons
                status = config.COLLECTIONS[current_coll_id]["status"]
                if status == COLLECTION_INSTALLED_STATUS:
                    self.button_install.setEnabled(True)
                    self.button_install.setText("Reinstall")
                    self.button_open.setEnabled(True)
                    self.button_uninstall.setEnabled(True)
                else:
                    self.button_install.setEnabled(True)
                    self.button_install.setText("Install")
                    self.button_open.setEnabled(False)
                    self.button_uninstall.setEnabled(False)
                # Update the web_view_details frame
                self.show_collection_metadata(current_coll_id)
            else:
                self.button_install.setEnabled(False)
                self.button_install.setText("Install")
                self.button_open.setEnabled(False)
                self.button_uninstall.setEnabled(False)

    def open_collection(self):
        """Slot called when the user clicks the 'Open' button."""
        collection_path = local_collection_path(self._sel_coll_id)
        directory_url = QUrl.fromLocalFile(str(collection_path))
        QDesktopServices.openUrl(directory_url)

    def reload_data_and_widget(self):
        """Reload repositories and collections and update widgets related."""
        self.reload_repositories_widget()
        self.reload_collections_model()

    def reload_repositories_widget(self):
        """Refresh tree repositories using new repositories data."""
        self.repository_manager.load_directories()
        self.populate_repositories_widget()

    def populate_repositories_widget(self):
        """Populate the current dictionary repositories to the tree widget."""
        # Clear the current tree widget
        self.tree_repositories.clear()
        installed_collections = self.collection_manager.get_installed_collections(
        )
        # Export the updated ones from the repository manager
        repo_Font = QFont()
        repo_with_installed_Font = QFont()
        repo_with_installed_Font.setWeight(60)
        collection_brush = QBrush(Qt.darkGray)
        installed_collection_brush = QBrush(QColor(60, 25, 10))
        for repo_name in self.repository_manager.directories:
            url = self.repository_manager.directories[repo_name]["url"]
            item = QTreeWidgetItem(self.tree_repositories, REPOSITORY_ITEM)
            # Is the repository in the QGIS resource directory?
            if url in self.repository_manager._online_directories.values():
                repo_with_installed_Font.setUnderline(True)
                repo_Font.setUnderline(True)
            else:
                repo_with_installed_Font.setUnderline(False)
                repo_Font.setUnderline(False)
            item.setText(0, repo_name)
            item.setText(1, url)
            item.setFont(0, repo_Font)
            for coll_id in config.COLLECTIONS:
                if ("repository_name" in config.COLLECTIONS[coll_id].keys()
                        and config.COLLECTIONS[coll_id]["repository_name"]
                        == repo_name):
                    coll_name = config.COLLECTIONS[coll_id]["name"]
                    coll_tags = config.COLLECTIONS[coll_id]["tags"]
                    collectionItem = QTreeWidgetItem(item, COLLECTION_ITEM)
                    brush = collection_brush
                    collectionFont = QFont()
                    collectionFont.setStyle(QFont.StyleItalic)
                    collitemtext = coll_name
                    if (installed_collections
                            and coll_id in installed_collections.keys()):
                        collitemtext = coll_name + " (installed)"
                        brush = installed_collection_brush
                        item.setFont(0, repo_with_installed_Font)
                        item.setForeground(0, brush)
                        item.setForeground(1, brush)
                    collectionItem.setFont(0, collectionFont)
                    collectionItem.setForeground(0, brush)
                    collectionItem.setText(0, collitemtext)
                    collectionItem.setFont(1, collectionFont)
                    collectionItem.setForeground(1, brush)
                    collectionItem.setText(1, coll_tags)
        self.tree_repositories.resizeColumnToContents(0)
        self.tree_repositories.resizeColumnToContents(1)
        self.tree_repositories.sortItems(1, Qt.AscendingOrder)

    def reload_collections_model(self):
        """Reload the collections model with the current collections."""
        self.collections_model.clear()
        installed_collections = self.collection_manager.get_installed_collections(
        )
        for id in config.COLLECTIONS:
            collection_name = config.COLLECTIONS[id]["name"]
            collection_author = config.COLLECTIONS[id]["author"]
            collection_tags = config.COLLECTIONS[id]["tags"]
            collection_description = config.COLLECTIONS[id]["description"]
            collection_status = config.COLLECTIONS[id]["status"]
            repository_name = ""
            if "repository_name" in config.COLLECTIONS[id].keys():
                repository_name = config.COLLECTIONS[id]["repository_name"]
            item = QStandardItem(collection_name + " (" + repository_name +
                                 ")")
            item.setEditable(False)
            item.setData(id, COLLECTION_ID_ROLE)
            item.setData(collection_name, COLLECTION_NAME_ROLE)
            item.setData(collection_description, COLLECTION_DESCRIPTION_ROLE)
            item.setData(collection_author, COLLECTION_AUTHOR_ROLE)
            item.setData(collection_tags, COLLECTION_TAGS_ROLE)
            item.setData(collection_status, COLLECTION_STATUS_ROLE)
            # Make installed collections stand out
            if installed_collections and id in installed_collections.keys():
                collectionFont = QFont()
                collectionFont.setWeight(60)
                item.setFont(collectionFont)
            self.collections_model.appendRow(item)
        self.collections_model.sort(0, Qt.AscendingOrder)

    def on_tree_repositories_itemSelectionChanged(self):
        """Slot for the itemSelectionChanged signal of tree_repositories."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item and selected_item.type() == REPOSITORY_ITEM:
            if selected_item:
                repo_name = selected_item.text(0)
            if not repo_name:
                return
            if repo_name not in self.repository_manager.directories.keys():
                return
            repo_url = self.repository_manager.directories[repo_name]["url"]
            # Disable the edit and delete buttons for "official" repositories
            if repo_url in self.repository_manager._online_directories.values(
            ):
                self.button_edit.setEnabled(False)
                self.button_delete.setEnabled(False)
            else:
                # Activate the edit and delete buttons
                self.button_edit.setEnabled(True)
                self.button_delete.setEnabled(True)
        elif selected_item and selected_item.type() == COLLECTION_ITEM:
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)
        else:
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)

    def on_list_view_collections_clicked(self, index):
        """Slot called when the user clicks an item in
        list_view_collections."""
        real_index = self.collection_proxy.mapToSource(index)
        if real_index.row() != -1:
            collection_item = self.collections_model.itemFromIndex(real_index)
            collection_id = collection_item.data(COLLECTION_ID_ROLE)
            self._sel_coll_id = collection_id
            # Enable / disable buttons
            status = config.COLLECTIONS[self._sel_coll_id]["status"]
            is_installed = status == COLLECTION_INSTALLED_STATUS
            if is_installed:
                self.button_install.setEnabled(True)
                self.button_install.setText("Reinstall")
                self.button_open.setEnabled(True)
                self.button_uninstall.setEnabled(True)
            else:
                self.button_install.setEnabled(True)
                self.button_install.setText("Install")
                self.button_open.setEnabled(False)
                self.button_uninstall.setEnabled(False)
            # Show  metadata
            self.show_collection_metadata(collection_id)

    @pyqtSlot(str)
    def filter_collections(self, text):
        search = QRegExp(text, Qt.CaseInsensitive, QRegExp.RegExp)
        self.collection_proxy.setFilterRegExp(search)

    def show_collection_metadata(self, id):
        """Show the collection metadata given the ID."""
        html = self.collection_manager.get_html(id)
        self.web_view_details.setHtml(html)

    def reject(self):
        """Slot called when the dialog is closed."""
        # Serialize collections to settings
        self.repository_manager.serialize_repositories()
        self.done(0)

    def open_help(self):
        """Open help."""
        doc_url = QUrl("http://qgis-contribution.github.io/" +
                       "QGIS-ResourceSharing/")
        QDesktopServices.openUrl(doc_url)

    def show_progress_dialog(self, text):
        """Show infinite progress dialog with given text.

        :param text: Text as the label of the progress dialog
        :type text: str
        """
        if self.progress_dialog is None:
            self.progress_dialog = QProgressDialog(self)
            self.progress_dialog.setWindowModality(Qt.WindowModal)
            self.progress_dialog.setAutoClose(False)
            title = self.tr("Resource Sharing")
            self.progress_dialog.setWindowTitle(title)
            # Just use an infinite progress bar here
            self.progress_dialog.setMaximum(0)
            self.progress_dialog.setMinimum(0)
            self.progress_dialog.setValue(0)
            self.progress_dialog.setLabelText(text)
        self.progress_dialog.show()
Exemplo n.º 7
0
class OpenTripPlannerPlugin():
    """QGIS Plugin Implementation."""
    def __init__(self, iface):
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """

        # Save reference to the QGIS interface
        self.iface = iface
        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)
        # initialize locale
        locale = QSettings().value('locale/userLocale')[0:2]
        locale_path = os.path.join(
            self.plugin_dir, 'i18n',
            'OpenTripPlannerPlugin_{}.qm'.format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)
            QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr(u'&OpenTripPlanner Plugin')

        # Check if plugin was started the first time in current QGIS session
        # Must be set in initGui() to survive plugin reloads
        self.first_start = None

    # noinspection PyMethodMayBeStatic
    def tr(self, message):
        """Get the translation for a string using Qt translation API.

        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate('OpenTripPlannerPlugin', message)

    def add_action(self,
                   icon_path,
                   text,
                   callback,
                   enabled_flag=True,
                   add_to_menu=True,
                   add_to_toolbar=True,
                   status_tip=None,
                   whats_this=None,
                   parent=None):
        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :type icon_path: str

        :param text: Text that should be shown in menu items for this action.
        :type text: str

        :param callback: Function to be called when the action is triggered.
        :type callback: function

        :param enabled_flag: A flag indicating if the action should be enabled
            by default. Defaults to True.
        :type enabled_flag: bool

        :param add_to_menu: Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        :type add_to_menu: bool

        :param add_to_toolbar: Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        :type add_to_toolbar: bool

        :param status_tip: Optional text to show in a popup when mouse pointer
            hovers over the action.
        :type status_tip: str

        :param parent: Parent widget for the new action. Defaults None.
        :type parent: QWidget

        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        :rtype: QAction
        """

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            # Adds plugin icon to Plugins toolbar
            self.iface.addToolBarIcon(action)

        if add_to_menu:
            self.iface.addPluginToMenu(self.menu, action)

        self.actions.append(action)

        return action

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        icon_path = ':/plugins/otp_plugin/icon.png'
        self.add_action(icon_path,
                        text=self.tr(u'OpenTripPlanner Plugin'),
                        callback=self.run,
                        parent=self.iface.mainWindow())

        # will be set False in run()
        self.first_start = True

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        for action in self.actions:
            self.iface.removePluginMenu(self.tr(u'&OpenTripPlanner Plugin'),
                                        action)
            self.iface.removeToolBarIcon(action)

    def isochronesStartWorker(self):  # method to start the worker thread
        if not self.gf.isochrones_selectedlayer:  # dont execute if no layer is selected
            QgsMessageLog.logMessage(
                "Warning! No inputlayer selected. Choose an inputlayer and try again.",
                MESSAGE_CATEGORY, Qgis.Critical)
            self.iface.messageBar().pushMessage(
                "Warning",
                " No inputlayer selected. Choose an inputlayer and try again.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)
            return
        if self.gf.isochrones_selectedlayer.featureCount() == 0:
            QgsMessageLog.logMessage(
                "Warning! Inputlayer is empty. Add some features and try again.",
                MESSAGE_CATEGORY, Qgis.Critical)
            self.iface.messageBar().pushMessage(
                "Warning",
                " Inputlayer is empty. Add some features and try again.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)
            return
        isochrones_memorylayer_vl = QgsVectorLayer(
            "MultiPolygon?crs=epsg:4326", "Isochrones",
            "memory")  # Create temporary polygon layer (output file)
        self.isochrones_thread = QThread()
        self.isochrones_worker = OpenTripPlannerPluginIsochronesWorker(
            self.dlg, self.iface, self.gf, isochrones_memorylayer_vl)
        # see https://realpython.com/python-pyqt-qthread/#using-qthread-to-prevent-freezing-guis
        # and https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html
        self.isochrones_worker.moveToThread(
            self.isochrones_thread)  # move Worker-Class to a thread
        # Connect signals and slots:
        self.isochrones_thread.started.connect(self.isochrones_worker.run)
        self.isochrones_worker.isochrones_finished.connect(
            self.isochrones_thread.quit)
        self.isochrones_worker.isochrones_finished.connect(
            self.isochrones_worker.deleteLater)
        self.isochrones_thread.finished.connect(
            self.isochrones_thread.deleteLater)
        self.isochrones_worker.isochrones_progress.connect(
            self.isochronesReportProgress)
        self.isochrones_worker.isochrones_finished.connect(
            self.isochronesFinished)
        self.isochrones_thread.start()  # finally start the thread
        # Disable/Enable GUI elements to prevent them from beeing used while worker threads are running and accidentially changing settings during progress
        self.gf.disableIsochronesGui()
        self.gf.disableGeneralSettingsGui()
        self.isochrones_thread.finished.connect(
            lambda: self.gf.enableIsochronesGui())
        self.isochrones_thread.finished.connect(
            lambda: self.gf.enableGeneralSettingsGui())

    def isochronesKillWorker(self):  # method to kill/cancel the worker thread
        # print('pushed cancel') # debugging
        # see https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html
        try:  # to prevent a Python error when the cancel button has been clicked but no thread is running use try/except
            self.isochrones_worker.stop(
            )  # call the stop method in worker class to break the work-loop so we can quit the thread
            if self.isochrones_thread.isRunning(
            ):  # check if a thread is running
                # print('pushed cancel, thread is running, trying to cancel') # debugging
                self.isochrones_thread.requestInterruption()
                self.isochrones_thread.exit(
                )  # Tells the thread’s event loop to exit with a return code.
                self.isochrones_thread.quit(
                )  # Tells the thread’s event loop to exit with return code 0 (success). Equivalent to calling exit (0).
                self.isochrones_thread.wait(
                )  # Blocks the thread until https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html#PySide6.QtCore.PySide6.QtCore.QThread.wait
        except:
            self.dlg.Isochrones_ProgressBar.setValue(0)
            self.dlg.Isochrones_StatusBox.setText('')

    def isochronesReportProgress(
            self, progress, status):  # method to report the progress to gui
        self.dlg.Isochrones_ProgressBar.setValue(
            progress)  # set the current progress in progress bar
        self.dlg.Isochrones_StatusBox.setText(status)

    def isochronesFinished(
        self,
        isochrones_resultlayer,
        isochrones_state,
        unique_errors="",
        runtime="00:00:00 (unknown)"
    ):  # method to interact with gui when thread is finished or canceled
        QgsProject.instance().addMapLayer(
            isochrones_resultlayer)  # Show resultlayer in project
        # isochrones_state is indicating different states of the thread/result as integer
        if unique_errors:
            self.iface.messageBar().pushMessage(
                "Warning",
                " Errors occurred. Check the resultlayer for details. The errors were: "
                + unique_errors +
                " - Created dummy geometries at coordinate 0,0 on error features",
                MESSAGE_CATEGORY,
                level=Qgis.Warning,
                duration=12)
            QgsMessageLog.logMessage(
                "Errors occurred. Check the resultlayer for details. The errors were: "
                + unique_errors +
                " - Created dummy geometries at coordinate 0,0 on error features",
                MESSAGE_CATEGORY, Qgis.Warning)
        if isochrones_state == 0:
            self.iface.messageBar().pushMessage(
                "Warning",
                " Run-Method was never executed.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)
        elif isochrones_state == 1:
            self.iface.messageBar().pushMessage(
                "Done!",
                " Isochrones job finished after " + runtime,
                MESSAGE_CATEGORY,
                level=Qgis.Success,
                duration=6)
        elif isochrones_state == 2:
            self.iface.messageBar().pushMessage(
                "Done!",
                " Isochrones job canceled after " + runtime,
                MESSAGE_CATEGORY,
                level=Qgis.Success,
                duration=6)
        elif isochrones_state == 3:
            self.iface.messageBar().pushMessage(
                "Warning",
                " No Isochrones to create - Check your settings and retry.",
                MESSAGE_CATEGORY,
                level=Qgis.Warning,
                duration=6)
        elif isochrones_state == 99:
            self.iface.messageBar().pushMessage(
                "Debugging",
                " Just having some debugging fun :)",
                MESSAGE_CATEGORY,
                level=Qgis.Info,
                duration=6)
        else:
            self.iface.messageBar().pushMessage(
                "Warning",
                " Unknown error occurred during execution.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)

    def aggregated_isochronesStartWorker(
            self):  # method to start the worker thread
        if not self.gf.aggregated_isochrones_selectedlayer:  # dont execute if no layer is selected
            QgsMessageLog.logMessage(
                "Warning! No inputlayer selected. Choose an inputlayer and try again.",
                MESSAGE_CATEGORY, Qgis.Critical)
            self.iface.messageBar().pushMessage(
                "Warning",
                " No inputlayer selected. Choose an inputlayer and try again.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)
            return
        if self.gf.aggregated_isochrones_selectedlayer.featureCount() == 0:
            QgsMessageLog.logMessage(
                "Warning! Inputlayer is empty. Add some features and try again.",
                MESSAGE_CATEGORY, Qgis.Critical)
            self.iface.messageBar().pushMessage(
                "Warning",
                " Inputlayer is empty. Add some features and try again.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)
            return
        aggregated_isochrones_memorylayer_vl = QgsVectorLayer(
            "MultiPolygon?crs=epsg:4326", "AggregatedIsochrones",
            "memory")  # Create temporary polygon layer (output file)
        self.aggregated_isochrones_thread = QThread()
        self.aggregated_isochrones_worker = OpenTripPlannerPluginAggregatedIsochronesWorker(
            self.dlg, self.iface, self.gf,
            aggregated_isochrones_memorylayer_vl)
        # see https://realpython.com/python-pyqt-qthread/#using-qthread-to-prevent-freezing-guis
        # and https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html
        self.aggregated_isochrones_worker.moveToThread(
            self.aggregated_isochrones_thread)  # move Worker-Class to a thread
        # Connect signals and slots:
        self.aggregated_isochrones_thread.started.connect(
            self.aggregated_isochrones_worker.run)
        self.aggregated_isochrones_worker.aggregated_isochrones_finished.connect(
            self.aggregated_isochrones_thread.quit)
        self.aggregated_isochrones_worker.aggregated_isochrones_finished.connect(
            self.aggregated_isochrones_worker.deleteLater)
        self.aggregated_isochrones_thread.finished.connect(
            self.aggregated_isochrones_thread.deleteLater)
        self.aggregated_isochrones_worker.aggregated_isochrones_progress.connect(
            self.aggregated_isochronesReportProgress)
        self.aggregated_isochrones_worker.aggregated_isochrones_finished.connect(
            self.aggregated_isochronesFinished)
        self.aggregated_isochrones_thread.start()  # finally start the thread
        # Disable/Enable GUI elements to prevent them from beeing used while worker threads are running and accidentially changing settings during progress
        self.gf.disableAggregatedIsochronesGui()
        self.gf.disableGeneralSettingsGui()
        self.aggregated_isochrones_thread.finished.connect(
            lambda: self.gf.enableAggregatedIsochronesGui())
        self.aggregated_isochrones_thread.finished.connect(
            lambda: self.gf.enableGeneralSettingsGui())

    def aggregated_isochronesKillWorker(
            self):  # method to kill/cancel the worker thread
        # print('pushed cancel') # debugging
        # see https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html
        try:  # to prevent a Python error when the cancel button has been clicked but no thread is running use try/except
            self.aggregated_isochrones_worker.stop(
            )  # call the stop method in worker class to break the work-loop so we can quit the thread
            if self.aggregated_isochrones_thread.isRunning(
            ):  # check if a thread is running
                # print('pushed cancel, thread is running, trying to cancel') # debugging
                self.aggregated_isochrones_thread.requestInterruption()
                self.aggregated_isochrones_thread.exit(
                )  # Tells the thread’s event loop to exit with a return code.
                self.aggregated_isochrones_thread.quit(
                )  # Tells the thread’s event loop to exit with return code 0 (success). Equivalent to calling exit (0).
                self.aggregated_isochrones_thread.wait(
                )  # Blocks the thread until https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html#PySide6.QtCore.PySide6.QtCore.QThread.wait
        except:
            self.dlg.AggregatedIsochrones_ProgressBar.setValue(0)
            self.dlg.AggregatedIsochrones_StatusBox.setText('')

    def aggregated_isochronesReportProgress(
            self, progress, status):  # method to report the progress to gui
        self.dlg.AggregatedIsochrones_ProgressBar.setValue(
            progress)  # set the current progress in progress bar
        self.dlg.AggregatedIsochrones_StatusBox.setText(status)

    def aggregated_isochronesFinished(
        self,
        aggregated_isochrones_resultlayer,
        aggregated_isochrones_state,
        unique_errors="",
        runtime="00:00:00 (unknown)"
    ):  # method to interact with gui when thread is finished or canceled
        QgsProject.instance().addMapLayer(
            aggregated_isochrones_resultlayer)  # Show resultlayer in project
        # aggregated_isochrones_state is indicating different states of the thread/result as integer
        if unique_errors:
            self.iface.messageBar().pushMessage(
                "Warning",
                " Errors occurred. Check the resultlayer for details. The errors were: "
                + unique_errors,
                MESSAGE_CATEGORY,
                level=Qgis.Warning,
                duration=12)
            QgsMessageLog.logMessage(
                "Errors occurred. Check the resultlayer for details. The errors were: "
                + unique_errors, MESSAGE_CATEGORY, Qgis.Warning)
        if aggregated_isochrones_state == 0:
            self.iface.messageBar().pushMessage(
                "Warning",
                " Run-Method was never executed.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)
        elif aggregated_isochrones_state == 1:
            self.iface.messageBar().pushMessage(
                "Done!",
                " Isochrones job finished after " + runtime,
                MESSAGE_CATEGORY,
                level=Qgis.Success,
                duration=6)
        elif aggregated_isochrones_state == 2:
            self.iface.messageBar().pushMessage(
                "Done!",
                " Isochrones job canceled after " + runtime,
                MESSAGE_CATEGORY,
                level=Qgis.Success,
                duration=6)
        elif aggregated_isochrones_state == 3:
            self.iface.messageBar().pushMessage(
                "Warning",
                " No Isochrones to create - Check your settings and retry.",
                MESSAGE_CATEGORY,
                level=Qgis.Warning,
                duration=6)
        elif aggregated_isochrones_state == 4:
            self.iface.messageBar().pushMessage(
                "Warning",
                " There is something wrong with your DateTime-Settings, check them and try again.",
                MESSAGE_CATEGORY,
                level=Qgis.Warning,
                duration=6)
        elif aggregated_isochrones_state == 99:
            self.iface.messageBar().pushMessage(
                "Debugging",
                " Just having some debugging fun :)",
                MESSAGE_CATEGORY,
                level=Qgis.Info,
                duration=6)
        else:
            self.iface.messageBar().pushMessage(
                "Warning",
                " Unknown error occurred during execution.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)

    def routesStartWorker(self):  # method to start the worker thread
        if not self.gf.routes_selectedlayer_source or not self.gf.routes_selectedlayer_target:
            QgsMessageLog.logMessage(
                "Warning! No inputlayer selected. Choose your inputlayers and try again.",
                MESSAGE_CATEGORY, Qgis.Critical)
            self.iface.messageBar().pushMessage(
                "Warning",
                " No sourcelayer or no targetlayer selected. Choose your inputlayers and try again.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)
            return
        if self.gf.routes_selectedlayer_source.fields().count(
        ) == 0 or self.gf.routes_selectedlayer_target.fields().count() == 0:
            QgsMessageLog.logMessage(
                "Warning! Inputlayer has no fields. Script wont work until you add at least one dummy ID-Field.",
                MESSAGE_CATEGORY, Qgis.Critical)
            self.iface.messageBar().pushMessage(
                "Warning",
                " Inputlayer has no fields - Add at least a dummy-id field.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)
            return
        if self.gf.routes_selectedlayer_source.featureCount(
        ) == 0 or self.gf.routes_selectedlayer_target.featureCount() == 0:
            QgsMessageLog.logMessage(
                "Warning! One or both inputlayers are empty. Add some features and try again.",
                MESSAGE_CATEGORY, Qgis.Critical)
            self.iface.messageBar().pushMessage(
                "Warning",
                " One or both inputlayers are empty. Add some features and try again.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)
            return
        routes_memorylayer_vl = QgsVectorLayer(
            "LineString?crs=epsg:4326", "Routes",
            "memory")  # Create temporary polygon layer (output file)
        self.routes_thread = QThread()
        self.routes_worker = OpenTripPlannerPluginRoutesWorker(
            self.dlg, self.iface, self.gf, routes_memorylayer_vl)
        # see https://realpython.com/python-pyqt-qthread/#using-qthread-to-prevent-freezing-guis
        # and https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html
        self.routes_worker.moveToThread(
            self.routes_thread)  # move Worker-Class to a thread
        # Connect signals and slots:
        self.routes_thread.started.connect(self.routes_worker.run)
        self.routes_worker.routes_finished.connect(self.routes_thread.quit)
        self.routes_worker.routes_finished.connect(
            self.routes_worker.deleteLater)
        self.routes_thread.finished.connect(self.routes_thread.deleteLater)
        self.routes_worker.routes_progress.connect(self.routesReportProgress)
        self.routes_worker.routes_finished.connect(self.routesFinished)
        self.routes_thread.start()  # finally start the thread
        # Disable/Enable GUI elements to prevent them from beeing used while worker threads are running and accidentially changing settings during progress
        self.gf.disableRoutesGui()
        self.gf.disableGeneralSettingsGui()
        self.routes_thread.finished.connect(lambda: self.gf.enableRoutesGui())
        self.routes_thread.finished.connect(
            lambda: self.gf.enableGeneralSettingsGui())

    def routesKillWorker(self):  # method to kill/cancel the worker thread
        # print('pushed cancel') # debugging
        # see https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html
        try:  # to prevent a Python error when the cancel button has been clicked but no thread is running use try/except
            self.routes_worker.stop(
            )  # call the stop method in worker class to break the work-loop so we can quit the thread
            if self.routes_thread.isRunning():  # check if a thread is running
                # print('pushed cancel, thread is running, trying to cancel') # debugging
                self.routes_thread.requestInterruption()
                self.routes_thread.exit(
                )  # Tells the thread’s event loop to exit with a return code.
                self.routes_thread.quit(
                )  # Tells the thread’s event loop to exit with return code 0 (success). Equivalent to calling exit (0).
                self.routes_thread.wait(
                )  # Blocks the thread until https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html#PySide6.QtCore.PySide6.QtCore.QThread.wait
        except:
            self.dlg.Routes_ProgressBar.setValue(0)
            self.dlg.Routes_StatusBox.setText('')

    def routesReportProgress(self, progress,
                             status):  # method to report the progress to gui
        self.dlg.Routes_ProgressBar.setValue(
            progress)  # set the current progress in progress bar
        self.dlg.Routes_StatusBox.setText(status)

    def routesFinished(
        self,
        routes_resultlayer,
        routes_state,
        unique_errors="",
        runtime="00:00:00 (unknown)"
    ):  # method to interact with gui when thread is finished or canceled
        QgsProject.instance().addMapLayer(
            routes_resultlayer)  # Show resultlayer in project
        # routes_state is indicating different states of the thread/result as integer
        if unique_errors:
            self.iface.messageBar().pushMessage(
                "Warning",
                " Errors occurred. Check the resultlayer for details. The errors were: "
                + unique_errors +
                " - Created dummy geometries at coordinate 0,0 on error features",
                MESSAGE_CATEGORY,
                level=Qgis.Warning,
                duration=12)
            QgsMessageLog.logMessage(
                "Errors occurred. Check the resultlayer for details. The errors were: "
                + unique_errors +
                " - Created dummy geometries at coordinate 0,0 on error features",
                MESSAGE_CATEGORY, Qgis.Warning)
        if routes_state == 0:
            self.iface.messageBar().pushMessage(
                "Warning",
                " Run-Method was never executed.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)
        elif routes_state == 1:
            self.iface.messageBar().pushMessage("Done!",
                                                " Routes job finished after " +
                                                runtime,
                                                MESSAGE_CATEGORY,
                                                level=Qgis.Success,
                                                duration=6)
        elif routes_state == 2:
            self.iface.messageBar().pushMessage("Done!",
                                                " Routes job canceled after " +
                                                runtime,
                                                MESSAGE_CATEGORY,
                                                level=Qgis.Success,
                                                duration=6)
        elif routes_state == 3:
            self.iface.messageBar().pushMessage(
                "Warning",
                " No Routes to create / no matching attributes - Check your settings and retry.",
                MESSAGE_CATEGORY,
                level=Qgis.Warning,
                duration=6)
        elif routes_state == 99:
            self.iface.messageBar().pushMessage(
                "Debugging",
                " Just having some debugging fun :)",
                MESSAGE_CATEGORY,
                level=Qgis.Info,
                duration=6)
        else:
            self.iface.messageBar().pushMessage(
                "Warning",
                " Unknown error occurred during execution.",
                MESSAGE_CATEGORY,
                level=Qgis.Critical,
                duration=6)

    def run(self):
        """Run method that performs all the real work"""
        # Create the dialog with elements (after translation) and keep reference
        # Only create GUI ONCE in callback, so that it will only load when the plugin is started
        if self.first_start == True:
            self.first_start = False
            self.dlg = OpenTripPlannerPluginDialog()
            self.gf = OpenTripPlannerPluginGeneralFunctions(
                self.dlg, self.iface)
            # Calling maplayer selection on first startup to load layers into QgsMapLayerComboBox and initialize QgsOverrideButton stuff so selections can be done without actually using the QgsMapLayerComboBox (related to currentIndexChanged.connect(self.isochrones_maplayerselection) below)
            self.gf.routes_maplayerselection()
            self.gf.isochrones_maplayerselection()
            self.gf.aggregated_isochrones_maplayerselection()
            # Execute Main-Functions on Click: Placing them here prevents them from beeing executed multiple times, see https://gis.stackexchange.com/a/137161/107424
            self.dlg.Isochrones_RequestIsochrones.clicked.connect(
                lambda: self.isochronesStartWorker()
            )  #Call the start worker method
            self.dlg.Isochrones_Cancel.clicked.connect(
                lambda: self.isochronesKillWorker())
            self.dlg.AggregatedIsochrones_RequestIsochrones.clicked.connect(
                lambda: self.aggregated_isochronesStartWorker()
            )  #Call the start worker method
            self.dlg.AggregatedIsochrones_Cancel.clicked.connect(
                lambda: self.aggregated_isochronesKillWorker())
            self.dlg.Routes_RequestRoutes.clicked.connect(
                lambda: self.routesStartWorker())
            self.dlg.Routes_Cancel.clicked.connect(
                lambda: self.routesKillWorker())

            # Calling Functions on button click
            self.dlg.GeneralSettings_CheckServerStatus.clicked.connect(
                self.gf.check_server_status)
            self.dlg.GeneralSettings_Save.clicked.connect(
                self.gf.store_general_variables
            )  #Call store_general_variables function when clicking on save button
            self.dlg.GeneralSettings_Restore.clicked.connect(
                self.gf.restore_general_variables)
            self.dlg.Isochrones_SaveSettings.clicked.connect(
                self.gf.store_isochrone_variables)
            self.dlg.Isochrones_RestoreDefaultSettings.clicked.connect(
                self.gf.restore_isochrone_variables)
            self.dlg.Isochrones_Now.clicked.connect(
                self.gf.set_datetime_now_isochrone)
            self.dlg.AggregatedIsochrones_SaveSettings.clicked.connect(
                self.gf.store_aggregated_isochrone_variables)
            self.dlg.AggregatedIsochrones_RestoreDefaultSettings.clicked.connect(
                self.gf.restore_aggregated_isochrone_variables)
            self.dlg.AggregatedIsochrones_Now.clicked.connect(
                self.gf.set_datetime_now_aggregated_isochrone)
            self.dlg.Routes_SaveSettings.clicked.connect(
                self.gf.store_route_variables)
            self.dlg.Routes_RestoreDefaultSettings.clicked.connect(
                self.gf.restore_route_variables)
            self.dlg.Routes_Now.clicked.connect(self.gf.set_datetime_now_route)

            # Calling Functions to update layer stuff when layerselection has changed
            self.dlg.Isochrones_SelectInputLayer.currentIndexChanged.connect(
                self.gf.isochrones_maplayerselection
            )  # Call function isochrones_maplayerselection to update all selection related stuff when selection has been changed
            self.dlg.AggregatedIsochrones_SelectInputLayer.currentIndexChanged.connect(
                self.gf.aggregated_isochrones_maplayerselection)
            self.dlg.Routes_SelectInputLayer_Source.currentIndexChanged.connect(
                self.gf.routes_maplayerselection)
            self.dlg.Routes_SelectInputLayer_Target.currentIndexChanged.connect(
                self.gf.routes_maplayerselection)
            self.dlg.Routes_SelectInputField_Source.currentIndexChanged.connect(
                self.gf.routes_maplayerselection)  # or "fieldChanged"?
            self.dlg.Routes_SelectInputField_Target.currentIndexChanged.connect(
                self.gf.routes_maplayerselection)
            self.dlg.Routes_DataDefinedLayer_Source.stateChanged.connect(
                self.gf.routes_maplayerselection)
            self.dlg.Routes_DataDefinedLayer_Target.stateChanged.connect(
                self.gf.routes_maplayerselection)

        # Setting GUI stuff for startup every time the plugin is opened
        self.dlg.Isochrones_Date.setDateTime(
            QtCore.QDateTime.currentDateTime()
        )  # Set Dateselection to today on restart or firststart, only functional if never used save settings, otherwise overwritten by read_route_variables()
        self.dlg.AggregatedIsochrones_FromDateTime.setDateTime(
            QtCore.QDateTime.currentDateTime())
        self.dlg.AggregatedIsochrones_ToDateTime.setDateTime(
            QtCore.QDateTime.currentDateTime())
        self.dlg.Routes_Date.setDateTime(QtCore.QDateTime.currentDateTime())
        self.dlg.Isochrones_ProgressBar.setValue(
            0)  # Set Progressbar to 0 on restart or first start
        self.dlg.AggregatedIsochrones_ProgressBar.setValue(0)
        self.dlg.Routes_ProgressBar.setValue(0)
        self.dlg.GeneralSettings_ServerStatusResult.setText(
            "Serverstatus Unknown")
        self.dlg.GeneralSettings_ServerStatusResult.setStyleSheet(
            "background-color: white; color: black ")

        # Functions to execute every time the plugin is opened
        self.gf.read_general_variables(
        )  #Run Read-Stored-Variables-Function on every start
        self.gf.read_isochrone_variables()
        self.gf.read_aggregated_isochrone_variables()
        self.gf.read_route_variables()

        # show the dialog
        self.dlg.show()
        # Run the dialog event loop
        result = self.dlg.exec_()
        # See if OK was pressed
        if result:
            # Do something useful here - delete the line containing pass and
            # substitute with your code.
            QgsMessageLog.logMessage(
                "OpenTripPlanner Plugin is already running! Close it before, if you wish to restart it.",
                MESSAGE_CATEGORY, Qgis.Warning)
            self.iface.messageBar().pushMessage(
                "Error",
                "OpenTripPlanner Plugin is already running! Close it before, if you wish to restart it.",
                MESSAGE_CATEGORY,
                level=Qgis.Warning,
                duration=6)
class ResourceSharingDialog(QDialog, FORM_CLASS):
    TAB_ALL = 0
    TAB_INSTALLED = 1
    TAB_SETTINGS = 2

    def __init__(self, parent=None, iface=None):
        """Constructor.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        super(ResourceSharingDialog, self).__init__(parent)
        self.setupUi(self)
        self.iface = iface

        # Reconfigure UI
        self.setModal(True)
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)
        self.button_install.setEnabled(False)
        self.button_open.setEnabled(False)
        self.button_uninstall.setEnabled(False)

        # Set QListWidgetItem
        # All
        icon_all = QIcon()
        icon_all.addFile(
            resources_path('img', 'plugin.svg'),
            QSize(),
            QIcon.Normal,
            QIcon.Off)
        item_all = QListWidgetItem()
        item_all.setIcon(icon_all)
        item_all.setText(self.tr('All'))
        # Installed
        icon_installed = QIcon()
        icon_installed.addFile(
            resources_path('img', 'plugin-installed.svg'),
            QSize(),
            QIcon.Normal,
            QIcon.Off)
        item_installed = QListWidgetItem()
        item_installed.setIcon(icon_installed)
        item_installed.setText(self.tr('Installed'))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        # Settings
        icon_settings = QIcon()
        icon_settings.addFile(
            resources_path('img', 'settings.svg'),
            QSize(),
            QIcon.Normal,
            QIcon.Off)
        item_settings = QListWidgetItem()
        item_settings.setIcon(icon_settings)
        item_settings.setText(self.tr('Settings'))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)

        # Add the list widget item to the widget
        self.menu_list_widget.addItem(item_all)
        self.menu_list_widget.addItem(item_installed)
        self.menu_list_widget.addItem(item_settings)

        # Init the message bar
        self.message_bar = QgsMessageBar(self)
        self.message_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        self.vlayoutRightColumn.insertWidget(0, self.message_bar)

        # Progress dialog for any long running process
        self.progress_dialog = None

        # Init repository manager
        self.repository_manager = RepositoryManager()
        self.collection_manager = CollectionManager()
        # Collections list view
        self.collections_model = QStandardItemModel(0, 1)
        self.collections_model.sort(0, Qt.AscendingOrder)
        self.collection_proxy = CustomSortFilterProxyModel(self)
        self.collection_proxy.setSourceModel(self.collections_model)
        self.list_view_collections.setModel(self.collection_proxy)
        # Active selected collection
        self._selected_collection_id = None

        # Slots
        self.button_add.clicked.connect(self.add_repository)
        self.button_edit.clicked.connect(self.edit_repository)
        self.button_delete.clicked.connect(self.delete_repository)
        self.button_reload.clicked.connect(self.reload_repositories)
        self.menu_list_widget.currentRowChanged.connect(self.set_current_tab)
        self.list_view_collections.selectionModel().currentChanged.connect(
            self.on_list_view_collections_clicked)
        self.line_edit_filter.textChanged.connect(self.filter_collections)
        self.button_install.clicked.connect(self.install_collection)
        self.button_open.clicked.connect(self.open_collection)
        self.button_uninstall.clicked.connect(self.uninstall_collection)
        self.button_box.button(QDialogButtonBox.Help).clicked.connect(
            self.open_help)

        # Populate repositories widget and collections list view
        self.populate_repositories_widget()
        self.reload_collections_model()

    def set_current_tab(self, index):
        """Set stacked widget based on active tab.

        :param index: The index of the active list widget item.
        :type index: int
        """
        # Clear message bar first
        self.message_bar.clearWidgets()
        if index == (self.menu_list_widget.count() - 1):
            # Switch to settings tab
            self.stacked_menu_widget.setCurrentIndex(1)
        else:
            # Switch to plugins tab
            if index == 1:
                # Installed
                self.collection_proxy.accepted_status = \
                    COLLECTION_INSTALLED_STATUS
                # Set the web view
                title = self.tr('Installed Collections')
                description = self.tr(
                    'On the left you see the list of all collections '
                    'installed on your QGIS')
            else:
                # All
                self.collection_proxy.accepted_status = COLLECTION_ALL_STATUS
                # Set the web view
                title = self.tr('All Collections')
                description = self.tr(
                    'On the left you see the list of all collections '
                    'available from the repositories registered in the '
                    'settings.')

            context = {
                'resources_path': resources_path(),
                'title': title,
                'description': description
            }
            self.web_view_details.setHtml(
                render_template('tab_description.html', context))
            self.stacked_menu_widget.setCurrentIndex(0)

    def add_repository(self):
        """Open add repository dialog."""
        dlg = ManageRepositoryDialog(self)
        if not dlg.exec_():
            return

        for repo in self.repository_manager.directories.values():
            if dlg.line_edit_url.text().strip() == repo['url']:
                self.message_bar.pushMessage(
                    self.tr(
                        'Unable to add another repository with the same URL!'),
                    Qgis.Critical, 5)
                return

        repo_name = dlg.line_edit_name.text()
        repo_url = dlg.line_edit_url.text().strip()
        repo_auth_cfg = dlg.line_edit_auth_id.text().strip()
        if repo_name in self.repository_manager.directories:
            repo_name += '(2)'

        # Show progress dialog
        self.show_progress_dialog("Fetching repository's metadata")

        # Add repository
        try:
            status, description = self.repository_manager.add_directory(
                repo_name, repo_url, repo_auth_cfg)
            if status:
                self.message_bar.pushMessage(
                    self.tr(
                        'Repository is successfully added'),
                    Qgis.Success, 5)
            else:
                self.message_bar.pushMessage(
                    self.tr(
                        'Unable to add repository: %s') % description,
                    Qgis.Critical, 5)
        except Exception as e:
            self.message_bar.pushMessage(
                self.tr('%s') % e,
                Qgis.Critical, 5)
        finally:
            self.progress_dialog.hide()

        # Reload data and widget
        self.reload_data_and_widget()

        # Deactivate edit and delete button
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)

    def edit_repository(self):
        """Open edit repository dialog."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item:
            repo_name = selected_item.text(0)

        if not repo_name:
            return

        # Check if it's the approved online dir repository
        settings = QSettings()
        settings.beginGroup(repo_settings_group())
        if settings.value(repo_name + '/url') in \
                self.repository_manager._online_directories.values():
            self.message_bar.pushMessage(
                self.tr(
                    'You can not edit the official repositories!'),
                Qgis.Warning, 5)
            return

        dlg = ManageRepositoryDialog(self)
        dlg.line_edit_name.setText(repo_name)
        dlg.line_edit_url.setText(
            self.repository_manager.directories[repo_name]['url'])
        dlg.line_edit_auth_id.setText(
            self.repository_manager.directories[repo_name]['auth_cfg'])

        if not dlg.exec_():
            return

        # Check if the changed URL is already there in the repo
        new_url = dlg.line_edit_url.text().strip()
        old_url = self.repository_manager.directories[repo_name]['url']
        for repo in self.repository_manager.directories.values():
            if new_url == repo['url'] and (old_url != new_url):
                self.message_bar.pushMessage(
                    self.tr('Unable to add another repository with the same '
                            'URL!'),
                    Qgis.Critical, 5)
                return

        new_name = dlg.line_edit_name.text()
        if (new_name in self.repository_manager.directories) and (
                    new_name != repo_name):
            new_name += '(2)'

        new_auth_cfg = dlg.line_edit_auth_id.text()

        # Show progress dialog
        self.show_progress_dialog("Fetching repository's metadata")

        # Edit repository
        try:
            status, description = self.repository_manager.edit_directory(
                repo_name,
                new_name,
                old_url,
                new_url,
                new_auth_cfg
            )
            if status:
                self.message_bar.pushMessage(
                    self.tr('Repository is successfully updated'),
                    Qgis.Success, 5)
            else:
                self.message_bar.pushMessage(
                    self.tr('Unable to add repository: %s') % description,
                    Qgis.Critical, 5)
        except Exception as e:
            self.message_bar.pushMessage(
                self.tr('%s') % e, Qgis.Critical, 5)
        finally:
            self.progress_dialog.hide()

        # Reload data and widget
        self.reload_data_and_widget()

        # Deactivate edit and delete button
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)

    def delete_repository(self):
        """Delete a repository in the tree widget."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item:
            repo_name = selected_item.text(0)

        if not repo_name:
            return
        # Check if it's the approved online dir repository
        repo_url = self.repository_manager.directories[repo_name]['url']
        if repo_url in self.repository_manager._online_directories.values():
            self.message_bar.pushMessage(
                self.tr(
                    'You can not remove the official repositories!'),
                Qgis.Warning, 5)
            return

        warning = self.tr('Are you sure you want to remove the following '
                          'repository?') + '\n' + repo_name
        if QMessageBox.warning(
                self,
                self.tr('QGIS Resource Sharing'),
                warning,
                QMessageBox.Yes,
                QMessageBox.No) == QMessageBox.No:
            return

        # Remove repository
        installed_collections = \
            self.collection_manager.get_installed_collections(repo_url)
        if installed_collections:
            message = ('You have some installed collections from this '
                       'repository. Please uninstall them first!')
            self.message_bar.pushMessage(message, Qgis.Warning, 5)
        else:
            self.repository_manager.remove_directory(repo_name)
            # Reload data and widget
            self.reload_data_and_widget()
            # Deactivate edit and delete button
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)

    def reload_repositories(self):
        """Slot for when user clicks reload repositories button."""
        # Show progress dialog
        self.show_progress_dialog('Reloading all repositories')

        for repo_name in self.repository_manager.directories:
            directory = self.repository_manager.directories[repo_name]
            url = directory['url']
            auth_cfg = directory['auth_cfg']
            try:
                status, description = self.repository_manager.reload_directory(
                    repo_name, url, auth_cfg)
                if status:
                    self.message_bar.pushMessage(
                        self.tr(
                            'Repository %s is successfully reloaded') %
                        repo_name, Qgis.Info, 5)
                else:
                    self.message_bar.pushMessage(
                        self.tr(
                            'Unable to reload %s: %s') % (
                            repo_name, description),
                        Qgis.Critical, 5)
            except Exception as e:
                self.message_bar.pushMessage(
                    self.tr('%s') % e,
                    Qgis.Critical, 5)

        self.progress_dialog.hide()
        # Reload data and widget
        self.reload_data_and_widget()

    def install_collection(self):
        """Slot for when user clicks download button."""
        self.show_progress_dialog('Starting installation process...')
        self.progress_dialog.canceled.connect(self.install_canceled)

        self.installer_thread = QThread()
        self.installer_worker = CollectionInstaller(
            self.collection_manager, self._selected_collection_id)
        self.installer_worker.moveToThread(self.installer_thread)
        self.installer_worker.finished.connect(self.install_finished)
        self.installer_worker.aborted.connect(self.install_aborted)
        self.installer_worker.progress.connect(self.install_progress)
        self.installer_thread.started.connect(self.installer_worker.run)
        self.installer_thread.start()

    def install_finished(self):
        # Process the result
        self.progress_dialog.hide()
        if self.installer_worker.install_status:
            self.reload_collections_model()
            message = '%s is installed successfully' % (
                config.COLLECTIONS[self._selected_collection_id]['name'])
        else:
            message = self.installer_worker.error_message
        QMessageBox.information(self, 'Resource Sharing', message)
        # Clean up the worker and thread
        self.installer_worker.deleteLater()
        self.installer_thread.quit()
        self.installer_thread.wait()
        self.installer_thread.deleteLater()

    def install_canceled(self):
        self.progress_dialog.hide()
        self.show_progress_dialog('Cancelling installation...')
        self.installer_worker.abort()

    def install_aborted(self):
        if self.installer_thread.isRunning():
            self.installer_thread.quit()
        self.installer_thread.finished.connect(self.progress_dialog.hide)

    def install_progress(self, text):
        self.progress_dialog.setLabelText(text)

    def uninstall_collection(self):
        """Slot called when user clicks uninstall button."""
        try:
            self.collection_manager.uninstall(self._selected_collection_id)
        except Exception as e:
            raise
        self.reload_collections_model()
        QMessageBox.information(
            self,
            'Resource Sharing',
            'The collection is uninstalled succesfully!')

    def open_collection(self):
        """Slot for when user clicks 'Open' button."""
        collection_path = local_collection_path(self._selected_collection_id)
        directory_url = QUrl.fromLocalFile(collection_path)
        QDesktopServices.openUrl(directory_url)

    def reload_data_and_widget(self):
        """Reload repositories and collections and update widgets related."""
        self.reload_repositories_widget()
        self.reload_collections_model()

    def reload_repositories_widget(self):
        """Refresh tree repositories using new repositories data."""
        self.repository_manager.load_directories()
        self.populate_repositories_widget()

    def populate_repositories_widget(self):
        """Populate the current dictionary repositories to the tree widget."""
        # Clear the current tree widget
        self.tree_repositories.clear()

        # Export the updated ones from the repository manager
        for repo_name in self.repository_manager.directories:
            url = self.repository_manager.directories[repo_name]['url']
            item = QTreeWidgetItem(self.tree_repositories)
            item.setText(0, repo_name)
            item.setText(1, url)
        self.tree_repositories.resizeColumnToContents(0)
        self.tree_repositories.resizeColumnToContents(1)
        self.tree_repositories.sortItems(1, Qt.AscendingOrder)

    def reload_collections_model(self):
        """Reload the collections model with the current collections."""
        self.collections_model.clear()
        for id in config.COLLECTIONS:
            collection_name = config.COLLECTIONS[id]['name']
            collection_author = config.COLLECTIONS[id]['author']
            collection_tags = config.COLLECTIONS[id]['tags']
            collection_description = config.COLLECTIONS[id]['description']
            collection_status = config.COLLECTIONS[id]['status']
            item = QStandardItem(collection_name)
            item.setEditable(False)
            item.setData(id, COLLECTION_ID_ROLE)
            item.setData(collection_name, COLLECTION_NAME_ROLE)
            item.setData(collection_description, COLLECTION_DESCRIPTION_ROLE)
            item.setData(collection_author, COLLECTION_AUTHOR_ROLE)
            item.setData(collection_tags, COLLECTION_TAGS_ROLE)
            item.setData(collection_status, COLLECTION_STATUS_ROLE)
            self.collections_model.appendRow(item)
        self.collections_model.sort(0, Qt.AscendingOrder)

    def on_tree_repositories_itemSelectionChanged(self):
        """Slot for when the itemSelectionChanged signal emitted."""
        # Activate edit and delete button
        self.button_edit.setEnabled(True)
        self.button_delete.setEnabled(True)

    def on_list_view_collections_clicked(self, index):
        """Slot for when the list_view_collections is clicked."""
        real_index = self.collection_proxy.mapToSource(index)
        if real_index.row() != -1:
            collection_item = self.collections_model.itemFromIndex(real_index)
            collection_id = collection_item.data(COLLECTION_ID_ROLE)
            self._selected_collection_id = collection_id

            # Enable/disable button
            status = config.COLLECTIONS[self._selected_collection_id]['status']
            is_installed = status == COLLECTION_INSTALLED_STATUS
            if is_installed:
                self.button_install.setEnabled(True)
                self.button_install.setText('Reinstall')
                self.button_open.setEnabled(True)
                self.button_uninstall.setEnabled(True)
            else:
                self.button_install.setEnabled(True)
                self.button_install.setText('Install')
                self.button_open.setEnabled(False)
                self.button_uninstall.setEnabled(False)

            # Show  metadata
            self.show_collection_metadata(collection_id)

    @pyqtSlot(str)
    def filter_collections(self, text):
        search = QRegExp(
            text,
            Qt.CaseInsensitive,
            QRegExp.RegExp)
        self.collection_proxy.setFilterRegExp(search)

    def show_collection_metadata(self, id):
        """Show the collection metadata given the id."""
        html = self.collection_manager.get_html(id)
        self.web_view_details.setHtml(html)

    def reject(self):
        """Slot when the dialog is closed."""
        # Serialize collections to settings
        self.repository_manager.serialize_repositories()
        self.done(0)

    def open_help(self):
        """Open help."""
        doc_url = QUrl('http://www.akbargumbira.com/qgis_resources_sharing')
        QDesktopServices.openUrl(doc_url)

    def show_progress_dialog(self, text):
        """Show infinite progress dialog with given text.

        :param text: Text as the label of the progress dialog
        :type text: str
        """
        if self.progress_dialog is None:
            self.progress_dialog = QProgressDialog(self)
            self.progress_dialog.setWindowModality(Qt.WindowModal)
            self.progress_dialog.setAutoClose(False)
            title = self.tr('Resource Sharing')
            self.progress_dialog.setWindowTitle(title)
            # Just use infinite progress bar here
            self.progress_dialog.setMaximum(0)
            self.progress_dialog.setMinimum(0)
            self.progress_dialog.setValue(0)
            self.progress_dialog.setLabelText(text)

        self.progress_dialog.show()
class LocatePointsDialog(QtWidgets.QDialog, FORM_CLASS):
    """Main dialog."""
    def __init__(self, iface,  parent=None):
        super(LocatePointsDialog, self).__init__(parent)
        self.setupUi(self)
        self.iface = iface
        self.in_combo.currentIndexChanged.connect(self.combo_changed)
        self.out_lyr.textChanged.connect(self.line_edit_text_changed)
        self.check_vertices.stateChanged.connect(self.checkbox_changed)
        self.run_button.clicked.connect(self.on_start)

        # Extra attributes
        self.in_name = False
        self.out_name = False

        # Extra thread attributes
        self.worker = None
        self.thread = None

    def on_start(self):
        self.pbar.setRange(0, 0)
        self.run_button.setEnabled(False)
        self.close_button.setEnabled(False)
        self.in_combo.setEnabled(False)
        self.out_lyr.setEnabled(False)
        self.offset.setEnabled(False)
        self.interval.setEnabled(False)
        self.check_attrs.setEnabled(False)
        self.check_vertices.setEnabled(False)
        self.check_endpoints.setEnabled(False)

        inlyr = self.in_combo.itemData(self.in_combo.currentIndex())
        outlyr = self.out_lyr.text()
        offset = self.offset.value()
        interval = self.interval.value()
        keep_attrs = self.check_attrs.isChecked()
        add_ver = self.check_vertices.isChecked()
        add_end = self.check_endpoints.isChecked()

        self.worker = Worker(inlyr, outlyr, offset, interval, keep_attrs, add_ver, add_end)
        self.thread = QThread()
        self.worker.moveToThread(self.thread)
        self.worker.finished.connect(self.on_finished)
        self.thread.started.connect(self.worker.run)
        self.thread.start()

    def on_finished(self, error):
        vl = self.worker.vl
        self.worker.deleteLater()
        self.thread.quit()
        self.thread.wait()
        self.thread.deleteLater()

        self.pbar.setRange(0, 10)
        self.pbar.setValue(10)
        self.run_button.setEnabled(True)
        self.close_button.setEnabled(True)
        self.in_combo.setEnabled(True)
        self.out_lyr.setEnabled(True)
        self.offset.setEnabled(True)
        self.interval.setEnabled(True)
        self.check_attrs.setEnabled(True)
        self.check_vertices.setEnabled(True)

        if self.check_vertices.isChecked() is False:
            self.check_endpoints.setEnabled(True)

        if error:
            self.iface.messageBar().pushMessage('Failed to create points', '{}'.format(error), level=1)
        else:
            try:
                QgsProject.instance().addMapLayer(vl)
            except AttributeError:
                QgsMapLayerRegistry.instance().addMapLayer(vl)
            self.iface.messageBar().pushMessage('Calculations finished', 'Points successfully created!', level=0)

    def combo_changed(self, idx):
        if idx > 0:
            crs = self.in_combo.itemData(self.in_combo.currentIndex()).crs()
            units = QgsUnitTypes.toString(crs.mapUnits())
            self.offset.setToolTip('Offset value ({})'.format(units))
            self.interval.setToolTip('Interval value ({})'.format(units))
            self.in_name = True
            if self.out_name is True:
                self.run_button.setEnabled(True)
            else:
                self.run_button.setEnabled(False)
        else:
            self.offset.setToolTip('')
            self.interval.setToolTip('')
            self.in_name = False
            self.run_button.setEnabled(False)

    def line_edit_text_changed(self, text):
        if text:
            self.out_name = True
            if self.in_name is True:
                self.run_button.setEnabled(True)
            else:
                self.run_button.setEnabled(False)
        else:
            self.out_name = False
            self.run_button.setEnabled(False)

    def checkbox_changed(self, state):
        if state == 2:
            self.check_endpoints.setChecked(0)
            self.check_endpoints.setEnabled(False)
        else:
            self.check_endpoints.setEnabled(True)