class SimilarityPlugin: """ Similarity Plugin parent class .. Attributes ----------- dlg : SimilarityPluginDialog MainPluginDialog simpleDialog : SimpleWarningDialog Show simple warning similarLayer : list=[] The result of calculation process previewLayer: int=0 Current index similarLayer that previewed in canvas widget calcThread : QThread(self.iface) Thread for data processing calcTask : CalculationModule Calculation module for checking similarity iface : QgsInterface 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. """ layer: QgsVectorLayer #: First layer layer2: QgsVectorLayer #: Second layer simpleDialog: SimpleWarnDialog #: Simple warning dialog 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 """ self.similarLayer = [] """ """ self.previewLayer = 0 self.calcTask = CalculationModule() # Save reference to the QGIS interface self.iface = iface # Registering worker calculation self.calcThread = QThread(self.iface) self.calcTask.moveToThread(self.calcThread) self.calcThread.started.connect(self.calcTask.run) self.calcThread.setTerminationEnabled(True) # multithreading signal calculation self.calcTask.progress.connect(self.updateCalcProgress) self.calcTask.progressSim.connect(self.updateSimList) self.calcTask.finished.connect(self.finishedCalcThread) self.calcTask.error.connect(self.errorCalcThread) self.calcTask.eventTask.connect(self.eventCalcThread) # pan event self.actionPan = QAction("Pan", self.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', 'SimilarityPlugin_{}.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'&Calculate Similarity Map') # 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 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('SimilarityPlugin', 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.addPluginToVectorMenu(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/similarity_plugin/icon-24.png' self.add_action(icon_path, text=self.tr(u'Check Similarity ...'), 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.removePluginVectorMenu( self.tr(u'&Calculate Similarity Map'), action) self.iface.removeToolBarIcon(action) def methodChange(self): """Signal when method changed""" if self.dlg.methodComboBox.currentIndex() == 2: # self.dlg.mergeCenterCheck.setChecked(False) # self.dlg.setPKBtn.setVisible(True) self.dlg.mergeCenterCheck.setEnabled(True) self.dlg.lineEditTreshold.setEnabled(False) self.dlg.nnRadiusEdit.setEnabled(False) # self.pkSelector.layerListWidget.clear() # self.pkSelector.layerListWidget.addItems( # self.dlg.layerSel1.currentLayer().fields().names() # ) # self.pkSelector.layer2ListWidget.clear() # self.pkSelector.layer2ListWidget.addItems( # self.dlg.layerSel2.currentLayer().fields().names() # ) # self.pkSelector.open() elif self.dlg.methodComboBox.currentIndex() == 0: self.dlg.mergeCenterCheck.setChecked(False) self.dlg.mergeCenterCheck.setEnabled(False) # self.dlg.setPKBtn.setVisible(False) self.dlg.lineEditTreshold.setEnabled(True) self.dlg.nnRadiusEdit.setEnabled(False) elif self.dlg.methodComboBox.currentIndex() == 1: self.dlg.mergeCenterCheck.setChecked(True) self.dlg.mergeCenterCheck.setEnabled(False) # self.dlg.setPKBtn.setVisible(False) self.dlg.lineEditTreshold.setEnabled(True) self.dlg.nnRadiusEdit.setEnabled(True) def resultPreview(self): """Activate preview section This method will called if calculation process is finished See Also ---------- refreshPreview() SimilarityPluginDialog.widgetCanvas SimilarityPluginDialog.nextBtn SimilarityPluginDialog.previousBtn SimilarityPluginDialog.removeBtn """ self.previewLayer = 0 self.refreshPreview() self.dlg.widgetCanvas.enableAntiAliasing(True) self.dlg.nextBtn.setEnabled(True) self.dlg.previousBtn.setEnabled(True) self.dlg.removeBtn.setEnabled(True) def attrPrinter(self, fieldsList: object, feature: QgsFeature, place: QTextEdit): """print the attribute table on preview panel :param fieldsList object: List the attribute value of feature :param feature QgsFeature: The feature will be printed :param place QTextEdit: The place for editing text """ temp = '' for f in fieldsList: temp += f.name() temp += ' : ' temp += str(feature.attribute(f.name())) temp += '\n' # print(place) place.setText(temp) def refreshPreview(self): """refreshing canvas on preview""" if len(self.similarLayer) > 0: # set the layer self.layerCanvas = QgsVectorLayer("Polygon?crs=ESPG:4326", 'SimilarityLayer', 'memory') self.layer2Canvas = QgsVectorLayer("Polygon?crs=ESPG:4326", 'SimilarityLayer', 'memory') # set the feature previewLayerFeature = self.calcTask.getLayersDup()[0].getFeature( self.similarLayer[self.previewLayer][0]) previewLayerFeature2 = self.calcTask.getLayersDup()[1].getFeature( self.similarLayer[self.previewLayer][1]) # set the label score on preview scoreLabel = "Score : " + str( round(self.similarLayer[self.previewLayer][2], 3)) # set cumulative score on preview if (not self.calcTask.getTranslate()): scoreLabel += " - Cumulative score : " + str( self.calcTask.getCumulative( self.similarLayer[self.previewLayer])) # show distance if the layer merge centered (NN and WK only) if (self.dlg.methodComboBox.currentIndex() == 1 or self.dlg.methodComboBox.currentIndex() == 2): distance = QgsGeometry.distance( previewLayerFeature.geometry().centroid(), previewLayerFeature2.geometry().centroid()) if distance < 0.00001: distance = 0 self.dlg.labelScore.setText(scoreLabel + " - Distance : " + str(round(distance, 3))) else: self.dlg.labelScore.setText(scoreLabel) self.attrPrinter( self.calcTask.getLayersDup() [0].dataProvider().fields().toList(), previewLayerFeature, self.dlg.previewAttr) self.attrPrinter( self.calcTask.getLayersDup() [1].dataProvider().fields().toList(), previewLayerFeature2, self.dlg.previewAttr_2) self.layerCanvas.dataProvider().addFeature(previewLayerFeature) # translating preview if self.calcTask.getTranslate(): tGeom = self.calcTask.translateCenterGeom( previewLayerFeature2.geometry(), previewLayerFeature.geometry()) nFeat = QgsFeature(previewLayerFeature2) nFeat.setGeometry(tGeom) self.layer2Canvas.dataProvider().addFeature(nFeat) else: self.layer2Canvas.dataProvider().addFeature( previewLayerFeature2) # set canvas to preview feature layer self.dlg.widgetCanvas.setExtent( previewLayerFeature.geometry().boundingBox(), True) self.dlg.widgetCanvas.setDestinationCrs( self.layerCanvas.sourceCrs()) symbol = self.layerCanvas.renderer().symbol() symbol.setColor(QColor(0, 147, 221, 127)) symbol2 = self.layer2Canvas.renderer().symbol() symbol2.setColor(QColor(231, 120, 23, 127)) self.dlg.widgetCanvas.setLayers( [self.layer2Canvas, self.layerCanvas]) # redraw the canvas self.dlg.widgetCanvas.refresh() def nextPreview(self): """Next preview signal for next button in preview section""" # f2 = open("engine/f2.txt", "w") if (self.previewLayer < len(self.similarLayer) - 1): self.previewLayer = int(self.previewLayer) + 1 # self.dlg.consoleTextEdit.setText(self.dlg.consoleTextEdit.toPlainText()+"\n\n Current Similar Layer Index : \n "+str([self.similarLayer[self.previewLayer], self.previewLayer])) self.refreshPreview() def previousPreview(self): """Previous preview signal""" if (self.previewLayer > 0): self.previewLayer = int(self.previewLayer) - 1 # self.dlg.consoleTextEdit.setText(self.dlg.consoleTextEdit.toPlainText()+"\n\n Current Similar Layer Index : \n "+str([self.similarLayer[self.previewLayer], self.previewLayer])) self.refreshPreview() def rmFeatResult(self): """Removing similarity info current result""" self.similarLayer.pop(self.previewLayer) if (self.previewLayer > len(self.similarLayer)): self.previewLayer = len(self.similarLayer - 1) self.refreshPreview() self.warnDlg.close() def rmWarn(self): """prevention remove item preview""" self.warnDlg = self.warnDialogInit( 'Are you sure to delete this feature ?') self.warnDlg.yesBtn.clicked.connect(self.rmFeatResult) self.warnDlg.noBtn.clicked.connect(self.warnDlg.close) self.warnDlg.show() def updateCalcProgress(self, value): """Progress signal for calcTask""" self.dlg.progressBar.setValue(int(round(value, 1))) def updateSimList(self, simList: list): """Updating similiarity result signal""" self.similarLayer.append(simList) cText = "Number of Result: " + str(len(self.similarLayer)) self.dlg.counterLabel.setText(cText) # thread signal def errorCalcThread(self, value: str): """Signal when an error occured""" print("error : ", value) self.dlg.consoleTextEdit.append("error : " + value + "\n\n") self.simpleWarnDialogInit(value) self.dlg.calcBtn.setEnabled(True) self.dlg.stopBtn.setEnabled(False) def finishedCalcThread(self, itemVal: list): """signal when calcTask calculation is finished :param itemVal list: the returned value emit """ # print("finished returned : ", itemVal) # self.similarLayer = itemVal # self.setLayers(self.calcTask.getLayersDup()) self.calcThread.exit() self.calcTask.kill() cText = "Number of Result: " + str(len(self.similarLayer)) self.dlg.consoleTextEdit.append(cText + "\n\n") if len(self.similarLayer) > 0: # self.addScoreItem() self.dlg.consoleTextEdit.append( "The 2 vector layer has been checked\n\n") self.previewLayer = 0 self.dlg.saveBtn.setEnabled(True) self.dlg.counterLabel.setText(cText) self.resultPreview() else: self.previewLayer = 0 self.dlg.counterLabel.setText(cText) self.dlg.calcBtn.setEnabled(True) self.dlg.stopBtn.setEnabled(False) def stopCalcThread(self): """Signal when calcTask is stopped """ self.calcThread.exit() self.dlg.eventLabel.setText("Event: Stopped") self.calcTask.kill() if (self.calcTask.getLayersDup()[0].featureCount() > 0 and self.calcTask.getLayersDup()[1].featureCount() > 0): cText = "Number of Result: " + str(len(self.similarLayer)) self.dlg.consoleTextEdit.append(cText + "\n\n") if len(self.similarLayer) > 0: # self.addScoreItem() self.previewLayer = 0 self.dlg.saveBtn.setEnabled(True) self.dlg.counterLabel.setText(cText) self.resultPreview() else: self.previewLayer = 0 self.dlg.counterLabel.setText(cText) self.dlg.calcBtn.setEnabled(True) self.dlg.stopBtn.setEnabled(False) def eventCalcThread(self, value: str): """Receiving signal event :param value str: the returned value emit """ self.dlg.eventLabel.setText("Event: " + value) # executing calculation def calculateScore(self): """Signal for executing calculation for cheking maps""" if (isinstance(self.dlg.layerSel1.currentLayer(), QgsVectorLayer) and isinstance(self.dlg.layerSel1.currentLayer(), QgsVectorLayer)): # set plugin to initial condition self.dlg.progressBar.setValue(0) self.dlg.saveBtn.setEnabled(False) self.dlg.nextBtn.setEnabled(False) self.dlg.previousBtn.setEnabled(False) self.dlg.removeBtn.setEnabled(False) self.dlg.widgetCanvas.setLayers([ QgsVectorLayer("Polygon?crs=ESPG:4326", 'SimilarityLayer', 'memory') ]) self.dlg.previewAttr.setText("") self.dlg.previewAttr_2.setText("") self.dlg.widgetCanvas.refresh() scoreLabel = "Score : 0" self.dlg.counterLabel.setText("Number of Result: 0") self.dlg.labelScore.setText(scoreLabel) self.similarLayer = [] # set input-output option self.calcTask.setLayers(self.dlg.layerSel1.currentLayer(), self.dlg.layerSel2.currentLayer()) self.calcTask.setTreshold(self.dlg.lineEditTreshold.value()) self.calcTask.setMethod(int( self.dlg.methodComboBox.currentIndex())) self.calcTask.setTranslate(self.dlg.mergeCenterCheck.isChecked()) self.calcTask.setRadius(self.dlg.nnRadiusEdit.value()) self.calcTask.setSuffix(str(self.dlg.sufLineEdit.text())) self.calcTask.setScoreName(str(self.dlg.attrOutLineEdit.text())) # print("input option set") # activating task self.calcTask.alive() # print("task alive") self.calcThread.start() # print("thread started") # set button self.dlg.calcBtn.setEnabled(False) self.dlg.stopBtn.setEnabled(True) else: # prevention on QgsVectorLayer only self.simpleWarnDialogInit("This plugin support Vector Layer only") # signal when saveBtn clicked def registerToProject(self): """Signal to registering project""" QgsProject.instance().addMapLayers(self.calcTask.getLayersResult()) # warning dialog for error or prevention def warnDialogInit(self, msg: str): """This dialog have Yes and No button. :param msg: str Display the warning message """ dialog = WarnDialog() #set the message dialog.msgLabel.setText(msg) return dialog # initializing simple warning dialog def simpleWarnDialogInit(self, msg: str): """ This dialog have ok button only :param: msg str: Display the warning message """ # Set the message self.simpleDialog.msgLabel.setText(msg) self.simpleDialog.show() # def pkSelectorAccepted(self): # if( len(self.pkSelector.layerListWidget.selectedItems()) > 0 and len(self.pkSelector.layer2ListWidget.selectedItems()) > 0 and # (len(self.pkSelector.layerListWidget.selectedItems()) == len(self.pkSelector.layer2ListWidget.selectedItems())) # ): # names = [j.text() for j in self.pkSelector.layerListWidget.selectedItems()] # names.sort() # names2 = [j.text() for j in self.pkSelector.layer2ListWidget.selectedItems()] # names2.sort() # print(names) # print(names2) # self.pkSelector.accept() # else: # self.simpleWarnDialogInit("Primary Key must be same in length as key or not null") def run(self): """Run method that performs all the real work""" # print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount()) # init run variable # self.canvas = QgsMapCanvas() # 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.previewLayer = 0 self.currentCheckLayer = [0, 0] self.first_start = False self.dlg = SimilarityPluginDialog() # self.dlg.setPKBtn.setVisible(False) self.simpleDialog = SimpleWarnDialog() # self.pkSelector = PkSelector() # set help documentation self.dlg.helpTextBrowser.load( QUrl( 'https://github.com/panickspa/SimilarityPlugin/wiki/User-Guide' )) self.dlg.nextHelpBtn.clicked.connect( self.dlg.helpTextBrowser.forward) self.dlg.previousHelpBtn.clicked.connect( self.dlg.helpTextBrowser.back) # filtering selection layer (empty layer not allowed) self.dlg.layerSel1.setAllowEmptyLayer(False) self.dlg.layerSel1.setAllowEmptyLayer(False) # self.pkSelector.okPushButton.clicked.connect(self.pkSelectorAccepted) # self.dlg.setPKBtn.clicked.connect(self.pkSelector.open) # method combobox initialiazation self.dlg.methodComboBox.clear() self.dlg.methodComboBox.addItems( ['Squential', 'Nearest Neightbour', 'Wilkerstat BPS']) # registering signal self.dlg.methodComboBox.currentIndexChanged.connect( self.methodChange) self.dlg.nextBtn.clicked.connect(self.nextPreview) self.dlg.previousBtn.clicked.connect(self.previousPreview) self.dlg.calcBtn.clicked.connect(self.calculateScore) self.dlg.saveBtn.clicked.connect(self.registerToProject) self.dlg.removeBtn.clicked.connect(self.rmWarn) self.dlg.stopBtn.clicked.connect(self.stopCalcThread) # intialize pan tool panTool = QgsMapToolPan(self.dlg.widgetCanvas) # set signal panTool.setAction(self.actionPan) # set map tool self.dlg.widgetCanvas.setMapTool(panTool) # set pan tool to be activate panTool.activate() # show the dialog self.dlg.show() # Run the dialog event loop result = self.dlg.exec_() # See if OK was pressed if result: self.similarLayer = [] self.dlg.widgetCanvas.setLayers([ QgsVectorLayer("Polygon?crs=ESPG:4326", 'SimilarityLayer', 'memory') ]) self.dlg.previewAttr.setText("") self.dlg.previewAttr_2.setText("") self.dlg.widgetCanvas.refresh() scoreLabel = "Score : 0" self.dlg.labelScore.setText(scoreLabel)
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)