class Rectanglify: """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", "Rectanglify_{}.qm".format(locale) ) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) QCoreApplication.installTranslator(self.translator) # Init settings self.settings = QSettings() self.settings.beginGroup("plugins/rectanglify") self.settings.setValue( "constantArea", self.settings.value("constantArea", True, bool) ) self.settings.setValue( "keepRings", self.settings.value("keepRings", True, bool) ) self.settings.setValue( "ringsShareAxes", self.settings.value("ringsShareAxes", True, bool) ) # 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("Rectanglify", message) def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" self.rectanglify_action = QAction( QIcon(":/plugins/rectanglify/actionRectanglify.svg"), self.tr("Rectanglify Selected Features"), parent=self.iface.mainWindow(), ) self.rectanglify_action.triggered.connect(self.rectanglify) self.settings_action = QAction( QIcon(""), self.tr("Rectanglify Settings"), parent=self.iface.mainWindow(), ) self.settings_action.triggered.connect(self.open_settings) self.iface.advancedDigitizeToolBar().addAction(self.rectanglify_action) self.iface.editMenu().addAction(self.rectanglify_action) self.plugin_menu = self.iface.pluginMenu().addMenu( QIcon(":/plugins/rectanglify/actionRectanglify.svg"), "Rectanglify" ) self.plugin_menu.addAction(self.rectanglify_action) self.plugin_menu.addAction(self.settings_action) self.iface.editMenu().addAction(self.rectanglify_action) self.iface.currentLayerChanged.connect(self.update_action_state) self.attach_to_project() self.update_action_state() self.task_rectanglify: QgsTask = None self.dialog = QDialog(self.iface.mainWindow()) self.dialog.ui = Ui_SettingsDialog() self.dialog.ui.setupUi(self.dialog) def attach_to_project(self): """Connect newly added layers to monitor their selection and editing state""" QgsProject.instance().layerWasAdded[QgsMapLayer].connect(self.connect_layer) for layer in QgsProject.instance().mapLayers().values(): self.connect_layer(layer) def detach_from_project(self): """Disconnect all layers""" QgsProject.instance().layerWasAdded[QgsMapLayer].disconnect(self.connect_layer) for layer in QgsProject.instance().mapLayers().values(): self.disconnect_layer(layer) def connect_layer(self, layer): """Connect layer to monitor their selection and editing state""" if ( isinstance(layer, QgsVectorLayer) and layer.geometryType() == QgsWkbTypes.PolygonGeometry ): layer.editingStarted.connect(self.update_action_state) layer.editingStopped.connect(self.update_action_state) layer.selectionChanged.connect(self.update_action_state) def disconnect_layer(self, layer): """Diconnect from layer signals""" if ( isinstance(layer, QgsVectorLayer) and layer.geometryType() == QgsWkbTypes.PolygonGeometry ): layer.editingStarted.disconnect(self.update_action_state) layer.editingStopped.disconnect(self.update_action_state) layer.selectionChanged.disconnect(self.update_action_state) def update_action_state(self): """Enable/Disable action""" layer: QgsVectorLayer = self.iface.activeLayer() enabled = ( isinstance(layer, QgsVectorLayer) and layer.geometryType() == QgsWkbTypes.PolygonGeometry and layer.isEditable() ) self.rectanglify_action.setEnabled(enabled) if enabled: if layer.selectedFeatureCount() in (0, layer.featureCount()): self.rectanglify_action.setText(self.tr("Rectanglify All Features")) else: self.rectanglify_action.setText( self.tr("Rectanglify Selected Features") ) def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" self.iface.advancedDigitizeToolBar().removeAction(self.rectanglify_action) self.iface.editMenu().removeAction(self.rectanglify_action) self.iface.pluginMenu().removeAction(self.plugin_menu.menuAction()) self.rectanglify_action.deleteLater() self.detach_from_project() def rectanglify(self): """Create a QgsTask that will perform the actual rectanglification""" # On finished will be called when the tasks ends, whether it failed # or succeeded self.task_rectanglify = QgsTask.fromFunction( "Rectanglify", self._rectanglify, on_finished=self.on_finished, ) QgsApplication.taskManager().addTask(self.task_rectanglify) def _rectanglify(self, task: QgsTask): """Rectanglify active layer's features""" layer: QgsVectorLayer = self.iface.activeLayer() only_selected = layer.selectedFeatureCount() > 0 constant_area = self.settings.value("constantArea", True, bool) keep_rings = self.settings.value("keepRings", True, bool) rings_share_axes = self.settings.value("ringsShareAxes", True, bool) with BeginCommand( layer, self.tr("Rectanglify selected features") if only_selected else self.tr("Rectanglify all features"), ): if only_selected: features = layer.getSelectedFeatures( QgsFeatureRequest().setSubsetOfAttributes([]) ) total = layer.selectedFeatureCount() else: features = layer.getFeatures( QgsFeatureRequest().setSubsetOfAttributes([]) ) total = layer.featureCount() for i, feat in enumerate(features, 1): # Check if task is canceled. Raising an exception will revert any # change made up to this point, thanks to the BeginCommand __exit__ # method if task.isCanceled(): raise Exception("Canceled") if feat.geometry().isMultipart(): multi = [ rectanglify_geometry( QgsGeometry.fromPolygonXY(polygon), constant_area, keep_rings, rings_share_axes, ) for polygon in feat.geometry().asMultiPolygon() ] new_geom = QgsGeometry.collectGeometry(multi) else: new_geom = rectanglify_geometry( feat.geometry(), constant_area, keep_rings, rings_share_axes ) layer.changeGeometry(feat.id(), new_geom) # Report task progress task.setProgress(i * 100 / total) def on_finished(self, exception, result=None): """Task completion handler""" # Task is either canceled or another exception occured if exception: # If task is canceled, simply display a temporary info message if self.task_rectanglify.isCanceled(): self.iface.messageBar().pushMessage(self.tr("Rectanglify canceled")) # Else, display a warning message, and log exception in the Message log else: trace = "\n".join( traceback.format_exception( type(exception), exception, exception.__traceback__ ) ) QgsMessageLog.logMessage( f"Exception during Rectanglify: {trace}", "Rectanglify", Qgis.Warning, ) self.iface.messageBar().pushMessage( self.tr("Rectanglify failed. See message log for details."), level=Qgis.Warning, ) self.task_rectanglify = None def open_settings(self): """Open the settings dialog""" # Update Checkboxes from plugin settings self.dialog.ui.constantAreaCheckBox.setChecked( self.settings.value("constantArea", True, bool) ) self.dialog.ui.keepRingsCheckBox.setChecked( self.settings.value("keepRings", True, bool) ) self.dialog.ui.sharedAxesCheckBox.setChecked( self.settings.value("ringsShareAxes", True, bool) ) # If dialog is accepted (click on Ok button), update plugin settings if self.dialog.exec() == QDialog.Accepted: self.settings.setValue( "constantArea", self.dialog.ui.constantAreaCheckBox.isChecked() ) self.settings.setValue( "keepRings", self.dialog.ui.keepRingsCheckBox.isChecked() ) self.settings.setValue( "ringsShareAxes", self.dialog.ui.sharedAxesCheckBox.isChecked() )