At this point, our plugin is still missing the code that ties everything together, that is, the code that reads in the data from the input files when the “Read and init” button is clicked and reacts to the control buttons at the bottom of the BusTrackAnalyzerForQGISDockWidget widget by starting to continuously call the analyzer’s nextStep() method, pausing that process, or completely resetting the analysis to start from the beginning. We are going to place the code for this directly in the definition of class BusTrackAnalyzerForQGISDockWidget in bus_track_analyzer_for_qgis_dockwidget.py, so you should open that file for editing now. Here is the code that needs to be added, together with some explanations.
import os from PyQt5 import QtGui, QtWidgets, uic from PyQt5.QtCore import pyqtSignal, QTimer, QCoreApplication # added imports from .bus_track_analyzer import BusTrackAnalyzer from .bus_tracker_widget import BusTrackerWidget #from .qgis_event_and_track_layer_creator import QGISEventAndTrackLayerCreator from .core_classes import Bus, Depot from .bus_events import LeavingDepotEvent, EnteringDepotEvent, BusStoppedEvent, BusEncounterEvent
Note that we again have to use the notation with the dot at the beginning of the module names here. Also, please note that there is one import statement that is still commented out because it is for a class that we have not yet written. That will happen a bit later in Section 4.12.6 and we will then uncomment this line.
Next, we are going to add some initialization code to the constructor, directly after the last line of the __init__(…) method saying “self.setupUi(self)”:
# own code added to template file self.running = False # True if currently running analysis self.delay = 0 # delay between steps in milliseconds self.eventClasses = [ LeavingDepotEvent, EnteringDepotEvent, BusStoppedEvent, BusEncounterEvent ] # list of event classes to detect self.busFileColumnIndices = { 'lon': 8, 'lat': 9, 'busId': 5, 'time': 0, 'line': 1 } # dictionary of column indices for required info # create initial BusTrackAnalyzer and BusTrackerWidget objects, and add the later to the scroll area of this widget self.analyzer = BusTrackAnalyzer({}, [], self.eventClasses) self.trackerWidget = BusTrackerWidget(self.analyzer) self.busTrackerContainerWidget.layout().addWidget(self.trackerWidget,0,0) self.layerCreator = None # QGISEventAndTrackLayerCreator object, will only be initialized when input files are read # create a QTimer user to wait some time between steps self.timer = QTimer() self.timer.setSingleShot(True) self.timer.timeout.connect(self.step) # set icons for play control buttons self.startTB.setIcon(QCoreApplication.instance().style().standardIcon(QtWidgets.QStyle.SP_MediaPlay)) self.pauseTB.setIcon(QCoreApplication.instance().style().standardIcon(QtWidgets.QStyle.SP_MediaPause)) self.stopAndResetTB.setIcon(QCoreApplication.instance().style().standardIcon(QtWidgets.QStyle.SP_MediaSkipBackward)) # connect play control buttons and slide to respetive methods defined below self.startTB.clicked.connect(self.start) self.pauseTB.clicked.connect(self.stop) self.stopAndResetTB.clicked.connect(self.reset) self.delaySlider.valueChanged.connect(self.setDelay) # connect edit fields and buttons for selecting input files to respetive methods defined below self.browseTrackFileTB.clicked.connect(self.selectTrackFile) self.browseDepotFileTB.clicked.connect(self.selectDepotFile) self.readAndInitPB.clicked.connect(self.readData)
What happens in this piece of code is the following:
Now the last thing that needs to happen is adding the different event handler methods we have already been referring to in the previously added code. This is another larger chunk of code since there are quite a few methods to define. Please add the definitions at the end of the file after the definition of the method closeEvent(…) that is already there by default.
# own methods added to template file def selectTrackFile(self): """displays open file dialog to select bus track input file""" fileName, _ = QtWidgets.QFileDialog.getOpenFileName(self,"Select CSV file with bus track data", "","(*.*)") if fileName: self.trackFileNameLE.setText(fileName) def selectDepotFile(self): """displays open file dialog to select depot input file""" fileName, _ = QtWidgets.QFileDialog.getOpenFileName(self,"Select CSV file with depot data", "","(*.*)") if fileName: self.depotFileNameLE.setText(fileName) def readData(self): """reads bus track and depot data from selected files and creates new analyzer and creates analyzer and layer creator for new input""" if self.running: self.stop() try: # read data depotData = Depot.readFromCSV(self.depotFileNameLE.text()) busData = Bus.readFromCSV(self.trackFileNameLE.text(), self.busFileColumnIndices) except Exception as e: QtWidgets.QMessageBox.information(self, 'Operation failed', 'Could not read data from files provided: '+ str(e.__class__) + ': ' + str(e), QtWidgets.QMessageBox.Ok) busData = {} depotData = [] # create new analyzer and layer creator objects and connect them self.analyzer = BusTrackAnalyzer(busData, depotData, self.eventClasses) self.trackerWidget.analyzer = self.analyzer # self.createLayerCreator() self.trackerWidget.updateContent() def stop(self): """halts analysis but analysis can be continued from this point""" self.timer.stop() self.running = False def reset(self): """halts analysis and resets analyzer to start from the beginning""" self.stop() self.analyzer.reset() # self.createLayerCreator() self.trackerWidget.updateContent() def start(self): """starts analysis if analysis isn't already running""" if not self.running: self.running = True self.step() def step(self): """performs a single analysis step of the BusTrackAnalyzer but starts singleshot timer after each step to call itself again""" if self.running: if self.analyzer.isFinished(): self.stop() else: self.analyzer.nextStep() # perform next analysis step self.trackerWidget.updateContent() # redraw tracker widget self.timer.start(max([5,self.delay])) # start timer to call this method again after delay def setDelay(self): """adapt delay when slider has been moved""" self.delay = 10 * self.delaySlider.value() if self.running: # if analysis is running, change to the new delay immediately self.timer.stop() self.timer.start(max([5,self.delay]))
The first two methods selectTrackFile() and selectDepotFile() are called when the “…” buttons at the top are clicked and will open file dialog boxes for picking the input files. The method readData() is invoked when the “Read and init” button is clicked. It stops all ongoing executions of the analyzer, attempts to read the data from the selected files, and then creates a new BusTrackAnalyzer object for this input data and connects it to the BusTrackerWidget in our GUI. The code of this function contains another two lines that are commented out and that we will uncomment later.
The other methods we define in this piece of code are the event handler functions for the control buttons at the bottom:
Our plugin is operational now and you can open QGIS and run it. In case you already have QGIS running or you encounter any errors that need to be fixed, don’t forget to reload the plugin code with the help of the Plugin Reloader plugin. Once the dock widget appears at the right side of the QGIS window (as shown in the figure below), do the following:
So far, so good. We have now turned our original standalone project into a QGIS plugin and even added in some extra functionality allowing the user to pause and restart the analysis and control the speed. However, typically a QGIS plugin in some way interacts with the content of the project that is currently open in QGIS, for instance by taking some of its layers as input or adding new layers to the project. We will add this kind of functionality in the next section and this will be the final addition we are making to our plugin.