class OsmDownloaderDialog(QDialog, FORM_CLASS): """Downloader for OSM data.""" def __init__(self, parent=None, iface=None): """Constructor for import dialog. :param parent: Optional widget to use as parent. :type parent: QWidget :param iface: An instance of QgisInterface. :type iface: QgisInterface """ QDialog.__init__(self, parent) self.parent = parent self.setupUi(self) icon = resources_path('img', 'icons', 'show-osm-download.svg') self.setWindowIcon(QtGui.QIcon(icon)) title = self.tr('InaSAFE OpenStreetMap Downloader') self.setWindowTitle(title) self.iface = iface self.progress_dialog = None # Set up things for context help self.help_button = self.button_box.button( QtWidgets.QDialogButtonBox.Help) # Allow toggling the help button self.help_button.setCheckable(True) self.help_button.toggled.connect(self.help_toggled) self.main_stacked_widget.setCurrentIndex(1) # Output directory self.directory_button.clicked.connect(self.directory_button_clicked) self.output_directory.setPlaceholderText( self.tr('[Create a temporary layer]')) # Disable boundaries group box until boundary checkbox is ticked self.boundary_group.setEnabled(False) # set up the validator for the file name prefix expression = QRegExp('^[A-Za-z0-9-_]*$') validator = QtGui.QRegExpValidator(expression, self.filename_prefix) self.filename_prefix.setValidator(validator) # Advanced panel self.line_edit_custom.setPlaceholderText(STAGING_SERVER) developer_mode = setting('developer_mode', expected_type=bool) self.group_box_advanced.setVisible(developer_mode) self.restore_state() # Setup the rectangle map tool self.canvas = iface.mapCanvas() self.rectangle_map_tool = \ RectangleMapTool(self.canvas) self.rectangle_map_tool.rectangle_created.connect( self.update_extent_from_rectangle) self.capture_button.clicked.connect(self.drag_rectangle_on_map_canvas) # Setup pan tool self.pan_tool = QgsMapToolPan(self.canvas) self.canvas.setMapTool(self.pan_tool) # Setup helper for admin_level json_file_path = resources_path('osm', 'admin_level_per_country.json') if os.path.isfile(json_file_path): with open(json_file_path, encoding='utf-8') as f: self.countries = json.load(f) self.bbox_countries = None self.populate_countries() # connect self.country_comboBox.currentIndexChanged.connect( self.update_helper_political_level) self.admin_level_comboBox.currentIndexChanged.connect( self.update_helper_political_level) self.update_extent_from_map_canvas() def update_helper_political_level(self): """To update the helper about the country and the admin_level.""" current_country = self.country_comboBox.currentText() index = self.admin_level_comboBox.currentIndex() current_level = self.admin_level_comboBox.itemData(index) content = None try: content = \ self.countries[current_country]['levels'][str(current_level)] if content == 'N/A' or content == 'fixme' or content == '': raise KeyError except KeyError: content = self.tr('undefined') finally: text = self.tr('which represents %s in') % content self.boundary_helper.setText(text) def populate_countries(self): """Populate the combobox about countries and levels.""" for i in range(1, 12): self.admin_level_comboBox.addItem(self.tr('Level %s') % i, i) # Set current index to admin_level 8, the most common one self.admin_level_comboBox.setCurrentIndex(7) list_countries = sorted( [self.tr(country) for country in list(self.countries.keys())]) for country in list_countries: self.country_comboBox.addItem(country) self.bbox_countries = {} for country in list_countries: multipolygons = self.countries[country]['bbox'] self.bbox_countries[country] = [] for coords in multipolygons: bbox = QgsRectangle(coords[0], coords[3], coords[2], coords[1]) self.bbox_countries[country].append(bbox) self.update_helper_political_level() def help_toggled(self, flag): """Show or hide the help tab in the stacked widget. .. versionadded: 3.2 :param flag: Flag indicating whether help should be shown or hidden. :type flag: bool """ if flag: self.help_button.setText(self.tr('Hide Help')) self.show_help() else: self.help_button.setText(self.tr('Show Help')) self.hide_help() def hide_help(self): """Hide the usage info from the user. .. versionadded:: 3.2 """ self.main_stacked_widget.setCurrentIndex(1) def show_help(self): """Show usage info to the user.""" # Read the header and footer html snippets self.main_stacked_widget.setCurrentIndex(0) header = html_header() footer = html_footer() string = header message = osm_downloader_help() string += message.to_html() string += footer self.help_web_view.setHtml(string) def restore_state(self): """Read last state of GUI from configuration file.""" last_path = setting('directory', '', expected_type=str) self.output_directory.setText(last_path) def save_state(self): """Store current state of GUI to configuration file.""" set_setting('directory', self.output_directory.text()) def update_extent(self, extent): """Update extent value in GUI based from an extent. :param extent: A list in the form [xmin, ymin, xmax, ymax] where all coordinates provided are in Geographic / EPSG:4326. :type extent: list """ self.x_minimum.setValue(extent[0]) self.y_minimum.setValue(extent[1]) self.x_maximum.setValue(extent[2]) self.y_maximum.setValue(extent[3]) # Updating the country if possible. rectangle = QgsRectangle(extent[0], extent[1], extent[2], extent[3]) center = rectangle.center() for country in self.bbox_countries: for polygon in self.bbox_countries[country]: if polygon.contains(center): index = self.country_comboBox.findText(country) self.country_comboBox.setCurrentIndex(index) break else: # Continue if the inner loop wasn't broken. continue # Inner loop was broken, break the outer. break else: self.country_comboBox.setCurrentIndex(0) def update_extent_from_map_canvas(self): """Update extent value in GUI based from value in map. .. note:: Delegates to update_extent() """ self.bounding_box_group.setTitle( self.tr('Bounding box from the map canvas')) # Get the extent as [xmin, ymin, xmax, ymax] extent = viewport_geo_array(self.iface.mapCanvas()) self.update_extent(extent) def update_extent_from_rectangle(self): """Update extent value in GUI based from the QgsMapTool rectangle. .. note:: Delegates to update_extent() """ self.show() self.canvas.unsetMapTool(self.rectangle_map_tool) self.canvas.setMapTool(self.pan_tool) rectangle = self.rectangle_map_tool.rectangle() if rectangle: self.bounding_box_group.setTitle( self.tr('Bounding box from rectangle')) extent = rectangle_geo_array(rectangle, self.iface.mapCanvas()) self.update_extent(extent) def directory_button_clicked(self): """Show a dialog to choose directory.""" # noinspection PyCallByClass,PyTypeChecker self.output_directory.setText( QFileDialog.getExistingDirectory( self, self.tr('Select download directory'))) def drag_rectangle_on_map_canvas(self): """Hide the dialog and allow the user to draw a rectangle.""" self.hide() self.rectangle_map_tool.reset() self.canvas.unsetMapTool(self.pan_tool) self.canvas.setMapTool(self.rectangle_map_tool) def get_checked_features(self): """Create a tab with all checked features. :return A list with all features which are checked in the UI. :rtype list """ feature_types = [] if self.roads_flag.isChecked(): feature_types.append('roads') if self.buildings_flag.isChecked(): feature_types.append('buildings') if self.building_points_flag.isChecked(): feature_types.append('building-points') if self.flood_prone_flag.isChecked(): feature_types.append('flood-prone') if self.evacuation_centers_flag.isChecked(): feature_types.append('evacuation-centers') if self.boundary_flag.isChecked(): level = self.admin_level_comboBox.currentIndex() + 1 feature_types.append('boundary-%s' % level) return feature_types def accept(self): """Do osm download and display it in QGIS.""" error_dialog_title = self.tr('InaSAFE OpenStreetMap Downloader Error') # Lock the bounding_box_group self.bounding_box_group.setDisabled(True) # Get the extent y_minimum = self.y_minimum.value() y_maximum = self.y_maximum.value() x_minimum = self.x_minimum.value() x_maximum = self.x_maximum.value() extent = [x_minimum, y_minimum, x_maximum, y_maximum] # Validate extent valid_flag = validate_geo_array(extent) if not valid_flag: message = self.tr( 'The bounding box is not valid. Please make sure it is ' 'valid or check your projection!') # noinspection PyCallByClass,PyTypeChecker,PyArgumentList display_warning_message_box(self, error_dialog_title, message) # Unlock the bounding_box_group self.bounding_box_group.setEnabled(True) return # Validate features feature_types = self.get_checked_features() if len(feature_types) < 1: message = self.tr('No feature selected. ' 'Please make sure you have checked one feature.') # noinspection PyCallByClass,PyTypeChecker,PyArgumentList display_warning_message_box(self, error_dialog_title, message) # Unlock the bounding_box_group self.bounding_box_group.setEnabled(True) return if self.radio_custom.isChecked(): server_url = self.line_edit_custom.text() if not server_url: # It's the place holder. server_url = STAGING_SERVER else: server_url = PRODUCTION_SERVER try: self.save_state() self.require_directory() # creating progress dialog for download self.progress_dialog = QProgressDialog(self) self.progress_dialog.setAutoClose(False) self.progress_dialog.setWindowTitle(self.windowTitle()) for feature_type in feature_types: output_directory = self.output_directory.text() if output_directory == '': output_directory = temp_dir(sub_dir='work') output_prefix = self.filename_prefix.text() overwrite = self.overwrite_flag.isChecked() output_base_file_path = self.get_output_base_path( output_directory, output_prefix, feature_type, overwrite) # noinspection PyTypeChecker download(feature_type, output_base_file_path, extent, self.progress_dialog, server_url) try: self.load_shapefile(feature_type, output_base_file_path) except FileMissingError as exception: display_warning_message_box(self, error_dialog_title, str(exception)) self.done(QDialog.Accepted) self.rectangle_map_tool.reset() except CanceledImportDialogError: # don't show anything because this exception raised # when user canceling the import process directly pass except Exception as exception: # pylint: disable=broad-except # noinspection PyCallByClass,PyTypeChecker,PyArgumentList display_warning_message_box(self, error_dialog_title, str(exception)) self.progress_dialog.cancel() self.progress_dialog.deleteLater() finally: # Unlock the bounding_box_group self.bounding_box_group.setEnabled(True) def get_output_base_path(self, output_directory, output_prefix, feature_type, overwrite): """Get a full base name path to save the shapefile. :param output_directory: The directory where to put results. :type output_directory: str :param output_prefix: The prefix to add for the shapefile. :type output_prefix: str :param feature_type: What kind of features should be downloaded. Currently 'buildings', 'building-points' or 'roads' are supported. :type feature_type: str :param overwrite: Boolean to know if we can overwrite existing files. :type overwrite: bool :return: The base path. :rtype: str """ path = os.path.join(output_directory, '%s%s' % (output_prefix, feature_type)) if overwrite: # If a shapefile exists, we must remove it (only the .shp) shp = '%s.shp' % path if os.path.isfile(shp): os.remove(shp) else: separator = '-' suffix = self.get_unique_file_path_suffix('%s.shp' % path, separator) if suffix: path = os.path.join( output_directory, '%s%s%s%s' % (output_prefix, feature_type, separator, suffix)) return path @staticmethod def get_unique_file_path_suffix(file_path, separator='-', i=0): """Return the minimum number to suffix the file to not overwrite one. Example : /tmp/a.txt exists. - With file_path='/tmp/b.txt' will return 0. - With file_path='/tmp/a.txt' will return 1 (/tmp/a-1.txt) :param file_path: The file to check. :type file_path: str :param separator: The separator to add before the prefix. :type separator: str :param i: The minimum prefix to check. :type i: int :return: The minimum prefix you should add to not overwrite a file. :rtype: int """ basename = os.path.splitext(file_path) if i != 0: file_path_test = os.path.join( '%s%s%s%s' % (basename[0], separator, i, basename[1])) else: file_path_test = file_path if os.path.isfile(file_path_test): return OsmDownloaderDialog.get_unique_file_path_suffix( file_path, separator, i + 1) else: return i def require_directory(self): """Ensure directory path entered in dialog exist. When the path does not exist, this function will ask the user if he want to create it or not. :raises: CanceledImportDialogError - when user choose 'No' in the question dialog for creating directory. """ path = self.output_directory.text() if path == '': # If let empty, we create an temporary directory return if os.path.exists(path): return title = self.tr('Directory %s not exist') % path question = self.tr( 'Directory %s not exist. Do you want to create it?') % path # noinspection PyCallByClass,PyTypeChecker answer = QMessageBox.question(self, title, question, QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: if len(path) != 0: os.makedirs(path) else: # noinspection PyCallByClass,PyTypeChecker,PyArgumentList display_warning_message_box( self, self.tr('InaSAFE error'), self.tr('Output directory can not be empty.')) raise CanceledImportDialogError() else: raise CanceledImportDialogError() def load_shapefile(self, feature_type, base_path): """Load downloaded shape file to QGIS Main Window. :param feature_type: What kind of features should be downloaded. Currently 'buildings', 'building-points' or 'roads' are supported. :type feature_type: str :param base_path: The base path of the shape file (without extension). :type base_path: str :raises: FileMissingError - when buildings.shp not exist """ path = '%s.shp' % base_path if not os.path.exists(path): message = self.tr( '%s does not exist. The server does not have any data for ' 'this extent.' % path) raise FileMissingError(message) layer = self.iface.addVectorLayer(path, feature_type, 'ogr') # Check if it's a building layer about the 2.5D if feature_type == 'buildings': layer_scope = QgsExpressionContextUtils.layerScope(layer) if not layer_scope.variable('qgis_25d_height'): QgsExpressionContextUtils.setLayerVariable( layer, 'qgis_25d_height', 0.0002) if not layer_scope.variable('qgis_25d_angle'): QgsExpressionContextUtils.setLayerVariable( layer, 'qgis_25d_angle', 70) def reject(self): """Redefinition of the method to remove the rectangle selection tool. It will call the super method. """ self.canvas.unsetMapTool(self.rectangle_map_tool) self.rectangle_map_tool.reset() super(OsmDownloaderDialog, self).reject()
class ResourceSharingDialog(QDialog, FORM_CLASS): TAB_ALL = 0 TAB_INSTALLED = 1 TAB_SETTINGS = 2 def __init__(self, parent=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) # Reconfigure UI self.setWindowTitle(f"{__title__} - {__version__}") 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()
class Qgeric: def __init__(self, iface): locale = QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join( os.path.dirname(__file__), 'i18n', 'qgeric_{}.qm'.format(locale)) self.translator = None if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) if qVersion() > '4.3.3': QCoreApplication.installTranslator(self.translator) self.iface = iface self.sb = self.iface.mainWindow().statusBar() self.tool = None self.results = [] self.actions = [] self.menu = '&Qgeric' self.toolbar = self.iface.addToolBar('Qgeric') self.toolbar.setObjectName('Qgeric') self.loadingWindow = QProgressDialog(self.tr('Selecting...'),self.tr('Pass'),0,100) self.loadingWindow.setAutoClose(False) self.loadingWindow.close() self.themeColor = QColor(60,151,255, 128) def unload(self): for action in self.actions: self.iface.removePluginVectorMenu('&Qgeric', action) self.iface.removeToolBarIcon(action) del self.toolbar def tr(self, message): return QCoreApplication.translate('Qgeric', message) def add_action( self, icon_path, text, callback, enabled_flag=True, checkable=False, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, menu=None, parent=None): icon = QIcon(icon_path) action = QAction(icon, text, parent) action.triggered.connect(callback) action.setEnabled(enabled_flag) action.setCheckable(checkable) if status_tip is not None: action.setStatusTip(status_tip) if whats_this is not None: action.setWhatsThis(whats_this) if menu is not None: action.setMenu(menu) if add_to_toolbar: self.toolbar.addAction(action) if add_to_menu: self.iface.addPluginToVectorMenu( self.menu, action) self.actions.append(action) return action def initGui(self): icon_path = ':/plugins/qgeric/resources/icon_AT.png' self.add_action( icon_path, text=self.tr('Display selection\'s results'), callback=self.showAttributesTable, parent=self.iface.mainWindow() ) self.toolbar.addSeparator() icon_path = ':/plugins/qgeric/resources/icon_SelPt.png' self.add_action( icon_path, text=self.tr('Point request tool'), checkable=True, callback=self.pointSelection, parent=self.iface.mainWindow() ) icon_path = ':/plugins/qgeric/resources/icon_SelR.png' self.add_action( icon_path, text=self.tr('Rectangle request tool'), checkable=True, callback=self.rectangleSelection, parent=self.iface.mainWindow() ) icon_path = ':/plugins/qgeric/resources/icon_SelC.png' self.add_action( icon_path, text=self.tr('Circle request tool'), checkable=True, callback=self.circleSelection, parent=self.iface.mainWindow() ) icon_path = ':/plugins/qgeric/resources/icon_SelP.png' self.add_action( icon_path, text=self.tr('Polygon request tool'), checkable=True, callback=self.polygonSelection, parent=self.iface.mainWindow() ) bufferMenu = QMenu() polygonBufferAction = QAction(QIcon(':/plugins/qgeric/resources/icon_SelTP.png'), self.tr('Polygon buffer request tool on the selected layer'), bufferMenu) polygonBufferAction.triggered.connect(self.polygonBufferSelection) bufferMenu.addAction(polygonBufferAction) icon_path = ':/plugins/qgeric/resources/icon_SelT.png' self.add_action( icon_path, text=self.tr('Buffer request tool on the selected layer'), checkable=True, menu=bufferMenu, callback=self.bufferSelection, parent=self.iface.mainWindow() ) def showAttributesTable(self): tab = AttributesTable(self.iface) layers = QgsProject().instance().mapLayers().values() for layer in layers: if layer.type() == QgsMapLayer.VectorLayer and QgsProject.instance().layerTreeRoot().findLayer(layer.id()).isVisible(): fields_name = [field.name() for field in layer.fields()] fields_type = [field.type() for field in layer.fields()] cells = layer.selectedFeatures() if len(cells) != 0: tab.addLayer(layer, fields_name, fields_type, cells) tab.loadingWindow.close() tab.show() tab.activateWindow(); tab.showNormal(); self.results.append(tab) def closeAttributesTable(self, tab): self.results.remove(tab) def pointSelection(self): if self.tool: self.tool.reset() self.request = 'intersects' self.tool = selectPoint(self.iface, self.themeColor) self.tool.setAction(self.actions[1]) self.tool.selectionDone.connect(self.returnedBounds) self.iface.mapCanvas().setMapTool(self.tool) self.sb.showMessage(self.tr('Left click to place a point.')) def rectangleSelection(self): if self.tool: self.tool.reset() self.request = 'intersects' self.tool = selectRect(self.iface, self.themeColor, 1) self.tool.setAction(self.actions[2]) self.tool.selectionDone.connect(self.returnedBounds) self.iface.mapCanvas().setMapTool(self.tool) self.sb.showMessage(self.tr('Maintain the left click to draw a rectangle.')) def circleSelection(self): if self.tool: self.tool.reset() self.request = 'intersects' self.tool = selectCircle(self.iface, self.themeColor, 1, 40) # last parameter = number of vertices self.tool.setAction(self.actions[3]) self.tool.selectionDone.connect(self.returnedBounds) self.iface.mapCanvas().setMapTool(self.tool) self.sb.showMessage(self.tr('Maintain the left click to draw a circle. Simple Left click to give a perimeter.')) def polygonSelection(self): if self.tool: self.tool.reset() self.request = 'intersects' self.tool = selectPolygon(self.iface, self.themeColor, 1) self.tool.setAction(self.actions[4]) self.tool.selectionDone.connect(self.returnedBounds) self.iface.mapCanvas().setMapTool(self.tool) self.sb.showMessage(self.tr('Left click to place points. Right click to confirm.')) def bufferSelection(self): if self.tool: self.tool.reset() self.request = 'buffer' self.tool = selectPoint(self.iface, self.themeColor) self.actions[5].setIcon(QIcon(':/plugins/qgeric/resources/icon_SelT.png')) self.actions[5].setText(self.tr('Buffer request tool on the selected layer')) self.actions[5].triggered.disconnect() self.actions[5].triggered.connect(self.bufferSelection) self.actions[5].menu().actions()[0].setIcon(QIcon(':/plugins/qgeric/resources/icon_SelTP.png')) self.actions[5].menu().actions()[0].setText(self.tr('Polygon buffer request tool on the selected layer')) self.actions[5].menu().actions()[0].triggered.disconnect() self.actions[5].menu().actions()[0].triggered.connect(self.polygonBufferSelection) self.tool.setAction(self.actions[5]) self.tool.selectionDone.connect(self.returnedBounds) self.iface.mapCanvas().setMapTool(self.tool) self.sb.showMessage(self.tr('Select a vector layer in the Layer Tree, then left click on an attribute of this layer on the map.')) def polygonBufferSelection(self): if self.tool: self.tool.reset() self.request = 'buffer' self.tool = selectPolygon(self.iface, self.themeColor, 1) self.actions[5].setIcon(QIcon(':/plugins/qgeric/resources/icon_SelTP.png')) self.actions[5].setText(self.tr('Polygon buffer request tool on the selected layer')) self.actions[5].triggered.disconnect() self.actions[5].triggered.connect(self.polygonBufferSelection) self.actions[5].menu().actions()[0].setIcon(QIcon(':/plugins/qgeric/resources/icon_SelT.png')) self.actions[5].menu().actions()[0].setText(self.tr('Buffer request tool on the selected layer')) self.actions[5].menu().actions()[0].triggered.disconnect() self.actions[5].menu().actions()[0].triggered.connect(self.bufferSelection) self.tool.setAction(self.actions[5]) self.tool.selectionDone.connect(self.returnedBounds) self.iface.mapCanvas().setMapTool(self.tool) self.sb.showMessage(self.tr('Left click to place points. Right click to confirm.')) def geomTransform(self, geom, crs_orig, crs_dest): g = QgsGeometry(geom) crsTransform = QgsCoordinateTransform(crs_orig, crs_dest, QgsProject().instance()) g.transform(crsTransform) return g def returnedBounds(self): rb = self.tool.rb warning = True ok = True active = False errBuffer_noAtt = False errBuffer_Vertices = False buffer_geom = None buffer_geom_crs = None # we check if there's at least one visible layer for layer in QgsProject().instance().mapLayers().values(): if QgsProject.instance().layerTreeRoot().findLayer(layer.id()).isVisible(): warning = False active = True break # buffer creation on the current layer if self.request == 'buffer': layer = self.iface.layerTreeView().currentLayer() if layer is not None and layer.type() == QgsMapLayer.VectorLayer and QgsProject.instance().layerTreeRoot().findLayer(layer.id()).isVisible(): # rubberband reprojection g = self.geomTransform(rb.asGeometry(), self.iface.mapCanvas().mapSettings().destinationCrs(), layer.crs()) features = layer.getFeatures(QgsFeatureRequest(g.boundingBox())) rbGeom = [] for feature in features: geom = feature.geometry() if g.intersects(geom): rbGeom.append(QgsGeometry(feature.geometry())) if len(rbGeom) > 0: union_geoms = rbGeom[0] for geometry in rbGeom: if union_geoms.combine(geometry) is not None: union_geoms = union_geoms.combine(geometry) rb.setToGeometry(union_geoms, layer) perim, ok = QInputDialog.getDouble(self.iface.mainWindow(), self.tr('Perimeter'), self.tr('Give a perimeter in m:')+'\n'+self.tr('(works only with metric crs)'), min=0) buffer_geom_crs = layer.crs() buffer_geom = union_geoms.buffer(perim, 40) rb.setToGeometry(buffer_geom, QgsVectorLayer("Polygon?crs="+layer.crs().authid(),"","memory")) if buffer_geom.length == 0 : warning = True errBuffer_Vertices = True else: warning = True errBuffer_noAtt = True else: warning = True if len(QgsProject().instance().mapLayers().values()) > 0 and warning == False and ok: self.loadingWindow.show() self.loadingWindow.activateWindow(); self.loadingWindow.showNormal(); for layer in QgsProject().instance().mapLayers().values(): if layer.type() == QgsMapLayer.VectorLayer and QgsProject.instance().layerTreeRoot().findLayer(layer.id()).isVisible(): if self.request == 'buffer' and self.iface.layerTreeView().currentLayer() == layer: layer.selectByIds([]) continue self.loadingWindow.reset() self.loadingWindow.setWindowTitle(self.tr('Selecting...')) self.loadingWindow.setLabelText(layer.name()) # rubberband reprojection if self.request == 'buffer': if buffer_geom_crs.authid() != layer.crs().authid(): g = self.geomTransform(buffer_geom, buffer_geom_crs, layer.crs()) else: g = self.geomTransform(buffer_geom, buffer_geom_crs, layer.crs()) else: g = self.geomTransform(rb.asGeometry(), self.iface.mapCanvas().mapSettings().destinationCrs(), layer.crs()) feat_id = [] features = layer.getFeatures(QgsFeatureRequest(g.boundingBox())) count = layer.getFeatures(QgsFeatureRequest(g.boundingBox())) nbfeatures = 0 for feature in count: nbfeatures+=1 # Select attributes intersecting with the rubberband index = 0 for feature in features: geom = feature.geometry() try: if g.intersects(geom): feat_id.append(feature.id()) except: # There's an error but it intersects print('error with '+layer.name()+' on '+str(feature.id())) feat_id.append(feature.id()) index += 1 self.loadingWindow.setValue(int((float(index)/nbfeatures)*100)) if self.loadingWindow.wasCanceled(): self.loadingWindow.reset() break QApplication.processEvents() layer.selectByIds(feat_id) self.loadingWindow.close() self.showAttributesTable() else: # Display a warning in the message bar depending of the error if active == False: self.iface.messageBar().pushWarning(self.tr('Warning'), self.tr('There is no active layer !')) elif ok == False: pass elif errBuffer_noAtt: self.iface.messageBar().pushWarning(self.tr('Warning'), self.tr('You didn\'t click on a layer\'s attribute !')) elif errBuffer_Vertices: self.iface.messageBar().pushWarning(self.tr('Warning'), self.tr('You must give a non-null value for a point\'s or line\'s perimeter !')) else: self.iface.messageBar().pushWarning(self.tr('Warning'), self.tr('There is no selected layer, or it is not vector nor visible !'))
class AttributesTable(QWidget): def __init__(self, iface): QWidget.__init__(self) self.setWindowTitle(self.tr('Search results')) self.resize(480,320) self.setMinimumSize(320,240) self.center() # Results export button self.btn_saveTab = QAction(QIcon(':/plugins/qgeric/resources/icon_save.png'), self.tr('Save this tab\'s results'), self) self.btn_saveTab.triggered.connect(lambda : self.saveAttributes(True)) self.btn_saveAllTabs = QAction(QIcon(':/plugins/qgeric/resources/icon_saveAll.png'), self.tr('Save all results'), self) self.btn_saveAllTabs.triggered.connect(lambda : self.saveAttributes(False)) self.btn_export = QAction(QIcon(':/plugins/qgeric/resources/icon_export.png'), self.tr('Export the selection as a memory layer'), self) self.btn_export.triggered.connect(self.exportLayer) self.btn_zoom = QAction(QIcon(':/plugins/qgeric/resources/icon_Zoom.png'), self.tr('Zoom to selected attributes'), self) self.btn_zoom.triggered.connect(self.zoomToFeature) self.btn_selectGeom = QAction(QIcon(':/plugins/qgeric/resources/icon_HlG.png'), self.tr('Highlight feature\'s geometry'), self) self.btn_selectGeom.triggered.connect(self.selectGeomChanged) self.btn_rename = QAction(QIcon(':/plugins/qgeric/resources/icon_Settings.png'), self.tr('Settings'), self) self.btn_rename.triggered.connect(self.renameWindow) self.tabWidget = QTabWidget() # Tab container self.tabWidget.setTabsClosable(True) self.tabWidget.currentChanged.connect(self.highlight_features) self.tabWidget.tabCloseRequested.connect(self.closeTab) self.loadingWindow = QProgressDialog() self.loadingWindow.setWindowTitle(self.tr('Loading...')) self.loadingWindow.setRange(0,100) self.loadingWindow.setAutoClose(False) self.loadingWindow.setCancelButton(None) self.canvas = iface.mapCanvas() self.canvas.extentsChanged.connect(self.highlight_features) self.highlight = [] self.highlight_rows = [] toolbar = QToolBar() toolbar.addAction(self.btn_saveTab) toolbar.addAction(self.btn_saveAllTabs) toolbar.addAction(self.btn_export) toolbar.addSeparator() toolbar.addAction(self.btn_zoom) toolbar.addSeparator() toolbar.addAction(self.btn_selectGeom) toolbar.addAction(self.btn_rename) vbox = QVBoxLayout() vbox.setContentsMargins(0,0,0,0) vbox.addWidget(toolbar) vbox.addWidget(self.tabWidget) self.setLayout(vbox) self.mb = iface.messageBar() self.selectGeom = False # False for point, True for geometry def renameWindow(self): title, ok = QInputDialog.getText(self, self.tr('Rename window'), self.tr('Enter a new title:')) if ok: self.setWindowTitle(title) def closeTab(self, index): self.tabWidget.widget(index).deleteLater() self.tabWidget.removeTab(index) def selectGeomChanged(self): if self.selectGeom: self.selectGeom = False self.btn_selectGeom.setText(self.tr('Highlight feature\'s geometry')) self.btn_selectGeom.setIcon(QIcon(':/plugins/qgeric/resources/icon_HlG.png')) else: self.selectGeom = True self.btn_selectGeom.setText(self.tr('Highlight feature\'s centroid')) self.btn_selectGeom.setIcon(QIcon(':/plugins/qgeric/resources/icon_HlC.png')) self.highlight_features() def exportLayer(self): if self.tabWidget.count() != 0: index = self.tabWidget.currentIndex() table = self.tabWidget.widget(index).findChildren(QTableWidget)[0] items = table.selectedItems() if len(items) > 0: type = '' if items[0].feature.geometry().type() == QgsWkbTypes.PointGeometry: type = 'Point' elif items[0].feature.geometry().type() == QgsWkbTypes.LineGeometry: type = 'LineString' else: type = 'Polygon' features = [] for item in items: if item.feature not in features: features.append(item.feature) name = '' ok = True while not name.strip() and ok == True: name, ok = QInputDialog.getText(self, self.tr('Layer name'), self.tr('Give a name to the layer:')) if ok: layer = QgsVectorLayer(type+"?crs="+table.crs.authid(),name,"memory") layer.startEditing() layer.dataProvider().addAttributes(features[0].fields().toList()) layer.dataProvider().addFeatures(features) layer.commitChanges() QgsProject.instance().addMapLayer(layer) else: self.mb.pushWarning(self.tr('Warning'), self.tr('There is no selected feature !')) def highlight_features(self): for item in self.highlight: self.canvas.scene().removeItem(item) del self.highlight[:] del self.highlight_rows[:] index = self.tabWidget.currentIndex() tab = self.tabWidget.widget(index) if self.tabWidget.count() != 0: table = self.tabWidget.widget(index).findChildren(QTableWidget)[0] nb = 0 area = 0 length = 0 items = table.selectedItems() for item in items: if item.row() not in self.highlight_rows: if self.selectGeom: highlight = QgsHighlight(self.canvas, item.feature.geometry(), self.tabWidget.widget(index).layer) else: highlight = QgsHighlight(self.canvas, item.feature.geometry().centroid(), self.tabWidget.widget(index).layer) highlight.setColor(QColor(255,0,0)) self.highlight.append(highlight) self.highlight_rows.append(item.row()) g = QgsGeometry(item.feature.geometry()) g.transform(QgsCoordinateTransform(tab.layer.crs(), QgsCoordinateReferenceSystem(2154), QgsProject.instance())) # geometry reprojection to get meters nb += 1 area += g.area() length += g.length() if tab.layer.geometryType()==QgsWkbTypes.PolygonGeometry: tab.sb.showMessage(self.tr('Selected features')+': '+str(nb)+' '+self.tr('Area')+': '+"%.2f"%area+' m'+u'²') elif tab.layer.geometryType()==QgsWkbTypes.LineGeometry: tab.sb.showMessage(self.tr('Selected features')+': '+str(nb)+' '+self.tr('Length')+': '+"%.2f"%length+' m') else: tab.sb.showMessage(self.tr('Selected features')+': '+str(nb)) def tr(self, message): return QCoreApplication.translate('Qgeric', message) def zoomToFeature(self): index = self.tabWidget.currentIndex() table = self.tabWidget.widget(index).findChildren(QTableWidget)[0] items = table.selectedItems() feat_id = [] for item in items: feat_id.append(item.feature.id()) if len(feat_id) >= 1: if len(feat_id) == 1: self.canvas.setExtent(items[0].feature.geometry().buffer(5, 0).boundingBox()) # in case of a single point, it will still zoom to it else: self.canvas.zoomToFeatureIds(self.tabWidget.widget(self.tabWidget.currentIndex()).layer, feat_id) self.canvas.refresh() # Add a new tab def addLayer(self, layer, headers, types, features): tab = QWidget() tab.layer = layer p1_vertical = QVBoxLayout(tab) p1_vertical.setContentsMargins(0,0,0,0) table = QTableWidget() table.itemSelectionChanged.connect(self.highlight_features) table.title = layer.name() table.crs = layer.crs() table.setColumnCount(len(headers)) if len(features) > 0: table.setRowCount(len(features)) nbrow = len(features) self.loadingWindow.show() self.loadingWindow.setLabelText(table.title) self.loadingWindow.activateWindow() self.loadingWindow.showNormal() # Table population m = 0 for feature in features: n = 0 for cell in feature.attributes(): item = QTableWidgetItem() item.setData(Qt.DisplayRole, cell) item.setFlags(item.flags() ^ Qt.ItemIsEditable) item.feature = feature table.setItem(m, n, item) n += 1 m += 1 self.loadingWindow.setValue(int((float(m)/nbrow)*100)) QApplication.processEvents() else: table.setRowCount(0) table.setHorizontalHeaderLabels(headers) table.horizontalHeader().setSectionsMovable(True) table.types = types table.filter_op = [] table.filters = [] for i in range(0, len(headers)): table.filters.append('') table.filter_op.append(0) header = table.horizontalHeader() header.setContextMenuPolicy(Qt.CustomContextMenu) header.customContextMenuRequested.connect(partial(self.filterMenu, table)) table.setSortingEnabled(True) p1_vertical.addWidget(table) # Status bar to display informations (ie: area) tab.sb = QStatusBar() p1_vertical.addWidget(tab.sb) title = table.title # We reduce the title's length to 20 characters if len(title)>20: title = title[:20]+'...' # We add the number of elements to the tab's title. title += ' ('+str(len(features))+')' self.tabWidget.addTab(tab, title) # Add the tab to the conatiner self.tabWidget.setTabToolTip(self.tabWidget.indexOf(tab), table.title) # Display a tooltip with the layer's full name def filterMenu(self, table, pos): index = table.columnAt(pos.x()) menu = QMenu() filter_operation = QComboBox() if table.types[index] in [10]: filter_operation.addItems([self.tr('Contains'),self.tr('Equals')]) else: filter_operation.addItems(['=','>','<']) filter_operation.setCurrentIndex(table.filter_op[index]) action_filter_operation = QWidgetAction(self) action_filter_operation.setDefaultWidget(filter_operation) if table.types[index] in [14]: if not isinstance(table.filters[index], QDate): filter_value = QDateEdit() else: filter_value = QDateEdit(table.filters[index]) elif table.types[index] in [15]: if not isinstance(table.filters[index], QTime): filter_value = QTimeEdit() else: filter_value = QTimeEdit(table.filters[index]) elif table.types[index] in [16]: if not isinstance(table.filters[index], QDateTime): filter_value = QDateTimeEdit() else: filter_value = QDateTimeEdit(table.filters[index]) else: filter_value = QLineEdit(table.filters[index]) action_filter_value = QWidgetAction(self) action_filter_value.setDefaultWidget(filter_value) menu.addAction(action_filter_operation) menu.addAction(action_filter_value) action_filter_apply = QAction(self.tr('Apply'), self) action_filter_apply.triggered.connect(partial(self.applyFilter, table, index, filter_value, filter_operation)) action_filter_cancel = QAction(self.tr('Cancel'), self) action_filter_cancel.triggered.connect(partial(self.applyFilter, table, index, None, filter_operation)) menu.addAction(action_filter_apply) menu.addAction(action_filter_cancel) menu.exec_(QtGui.QCursor.pos()) def applyFilter(self, table, index, filter_value, filter_operation): if filter_value == None: table.filters[index] = None else: if isinstance(filter_value, QDateEdit): table.filters[index] = filter_value.date() elif isinstance(filter_value, QTimeEdit): table.filters[index] = filter_value.time() elif isinstance(filter_value, QDateTimeEdit): table.filters[index] = filter_value.dateTime() else: table.filters[index] = filter_value.text() table.filter_op[index] = filter_operation.currentIndex() nb_elts = 0 for i in range(0, table.rowCount()): table.setRowHidden(i, False) nb_elts += 1 hidden_rows = [] for nb_col in range(0, table.columnCount()): filtered = False header = table.horizontalHeaderItem(nb_col).text() valid = False if table.filters[nb_col] is not None: if type(table.filters[nb_col]) in [QDate, QTime, QDateTime]: valid = True else: if table.filters[nb_col].strip(): valid = True if valid: filtered = True items = None if table.types[nb_col] in [10]:# If it's a string filter_type = None if table.filter_op[nb_col] == 0: # Contain filter_type = Qt.MatchContains if table.filter_op[nb_col] == 1: # Equal filter_type = Qt.MatchFixedString items = table.findItems(table.filters[nb_col], filter_type) elif table.types[nb_col] in [14, 15, 16]: # If it's a date/time items = [] for nb_row in range(0, table.rowCount()): item = table.item(nb_row, nb_col) if table.filter_op[nb_col] == 0: # = if item.data(QTableWidgetItem.Type) == table.filters[nb_col]: items.append(item) if table.filter_op[nb_col] == 1: # > if item.data(QTableWidgetItem.Type) > table.filters[nb_col]: items.append(item) if table.filter_op[nb_col] == 2: # < if item.data(QTableWidgetItem.Type) < table.filters[nb_col]: items.append(item) else: # If it's a number items = [] for nb_row in range(0, table.rowCount()): item = table.item(nb_row, nb_col) if item.text().strip(): if table.filter_op[nb_col] == 0: # = if float(item.text()) == float(table.filters[nb_col]): items.append(item) if table.filter_op[nb_col] == 1: # > if float(item.text()) > float(table.filters[nb_col]): items.append(item) if table.filter_op[nb_col] == 2: # < if float(item.text()) < float(table.filters[nb_col]): items.append(item) rows = [] for item in items: if item.column() == nb_col: rows.append(item.row()) for i in range(0, table.rowCount()): if i not in rows: if i not in hidden_rows: nb_elts -= 1 table.setRowHidden(i, True) hidden_rows.append(i) if filtered: if header[len(header)-1] != '*': table.setHorizontalHeaderItem(nb_col, QTableWidgetItem(header+'*')) else: if header[len(header)-1] == '*': header = header[:-1] table.setHorizontalHeaderItem(nb_col, QTableWidgetItem(header)) title = self.tabWidget.tabText(self.tabWidget.currentIndex()) for i in reversed(range(len(title))): if title[i] == ' ': break title = title[:-1] title += '('+str(nb_elts)+')' self.tabWidget.setTabText(self.tabWidget.currentIndex(), title) # Save tables in OpenDocument format # Use odswriter library def saveAttributes(self, active): file = QFileDialog.getSaveFileName(self, self.tr('Save in...'),'', self.tr('OpenDocument Spreadsheet (*.ods)')) if file[0]: try: with ods.writer(open(file[0],"wb")) as odsfile: tabs = None if active: tabs = self.tabWidget.currentWidget().findChildren(QTableWidget) else: tabs = self.tabWidget.findChildren(QTableWidget) for table in reversed(tabs): sheet = odsfile.new_sheet(table.title[:20]+'...') # For each tab in the container, a new sheet is created sheet.writerow([table.title]) # As the tab's title's lenght is limited, the full name of the layer is written in the first row nb_row = table.rowCount() nb_col = table.columnCount() # Fetching and writing of the table's header header = [] for i in range(0,nb_col): header.append(table.horizontalHeaderItem(i).text()) sheet.writerow(header) # Fetching and writing of the table's items for i in range(0,nb_row): row = [] for j in range(0,nb_col): row.append(table.item(i,j).text()) if not table.isRowHidden(i): sheet.writerow(row) return True except IOError: QMessageBox.critical(self, self.tr('Error'), self.tr('The file can\'t be written.')+'\n'+self.tr('Maybe you don\'t have the rights or are trying to overwrite an opened file.')) return False def center(self): screen = QDesktopWidget().screenGeometry() size = self.geometry() self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2) def clear(self): self.tabWidget.clear() for table in self.tabWidget.findChildren(QTableWidget): table.setParent(None) def closeEvent(self, e): result = QMessageBox.question(self, self.tr("Saving ?"), self.tr("Would you like to save results before exit ?"), buttons = QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) if result == QMessageBox.Yes: if self.saveAttributes(False): self.clear() e.accept() else: e.ignore() elif result == QMessageBox.No: self.clear() e.accept() else: e.ignore()