GEOG 489
Advanced Python Programming for GIS

4.12.5 Implement Main Functionality in BusTrackAnalyzerForQGISDockWidget class

PrintPrint

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.

  1. First, we need to important quite a few classes from our own .py files and also a few additional PyQt5 classes; so please change the import statements at the beginning of the code to the following:
    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.

  2. 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:

    • First, we introduce some new instance variables for this class that are needed for reading data and setting up a BusTrackAnalyzer object (you probably recognize the variables eventClasses and busFileColumnIndices from the original main.py file) and for controlling the analysis steps. Variable running will be set to True when the analyzer’s nextStep() method is supposed to be called continuously with small delays between the steps whose length is given by variable delay. It is certainly not optimal that the column indices for the GPS data file are hard-coded here in variable busFileColumnIndices but we decided against including an option for the user to specify these in this version to keep the GUI and code as simple as possible.
    • In the next block of code, we create an instance variable to store the BusTrackAnalyzer object we will be using, and we create the BusTrackerWidget widget and place it within the scroll area in the center of the GUI. Since we have not read any input data yet, we are using an empty bus dictionary and empty depot list to set these things up.
    • An instance variable for a QTimer object is created that will be used to queue up the execution of the method step() defined later when the widget is in continuous execution mode.
    • The next code block simply sets the icons for the different control buttons at the bottom of the widget using standard QT icons.
    • Next, we connect the “clicked” signals of the different control buttons to different methods of the class that are supposed to react to these signals. We do the same for the “valueChanged” signal of the QSlider widget in our GUI to be able to adapt the value of the variable delay when the slider is moved by the user.
    • Finally, we link the three buttons at the top of the GUI for reading in the data to the respective event handler methods.
  3. 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:

  • stop() is called when the “Pause” button in the middle is clicked and simply stops the timer if its currently running and sets the variable running to False so that the analyzer’s nextStep() method won’t be called anymore.
  • reset() is called when the control button on the left is called and it also stops the execution but, in addition, resets the analyzer so that the analysis will start from the beginning when the “Start” button is clicked again.
  • start() is called when the right control button (“Play”) for continuously performing analysis steps is clicked. It sets variable running to True and then invokes method step() to run the first analysis step.
  • step() is either called from start() or by the timer it sets up itself after each call of the analyzer’s nextStep() method to perform the next analysis step after a certain delay until all observations have been processed. Please note that we are using minimum delay of 5 milliseconds to make sure that the QGIS GUI remains responsive while we are running the analysis.
  • setDelay() is called when the slider is moved and it translates the slider position into a delay value between 0 and 1 second. It also immediately restarts the timer to use this new delay value.

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:

  1. Use the “…” buttons to select the input files for the bus GPS and depot data.
  2. Click “Read and init”; after that you should see the initial bus tracker configuration in the central area of the dock widget.
  3. Press the “Play” button to start the analysis; the content of the BusTrackerWidget should now continuously update to reflect the current state of the analysis
  4. Test out the “Pause” and “Rewind” buttons as well as changing the delay between steps with the slider to control the speed the analysis is run at.
 Screenshot of map next to window with individual bus information
Figure 4.46 The plugin running in QGIS showing the current status of the analysis and individual buses

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.