GEOG 489
Advanced Python Programming for GIS

4.12.4 Modifications to the Original Project Files

PrintPrint

The next thing we are going to do is make a few smaller changes to the files we copied over from the original project. First of all, it is unfortunately required that we adapt all import statements in which we are importing .py files located in the project folder. The reason is that when we write something like

from core_classes import BusTracker 

, this will work fine when the file core_classes.py is located in the current working directory when the program is executed. This is usually the same folder in which the main Python script is located. Therefore, we didn’t have any problems when executing main.py since main.py and the other .py files we wrote are all in the same directory. However, when being run as a QGIS plugin, the working directory will not be the folder containing the plugin code. As a result, you will get error messages when trying to import the other .py files like this. What we have to do is adapt the import statements to start with a dot which tells Python to look for the file in the same folder in which the file in which the import statement appears is located. So the previous example needs to become:

from .core_classes import BusTracker 

Here is quick overview of where we have to make these changes:

  1. In bus_events.py change the import statement at the beginning to:
    from .core_classes import Depot, BusTracker 
  2. In bus_track_analyzer.py change the two import statement at the beginning to:
    from .bus_events import BusEvent 
    from .core_classes import BusTracker, Observation, Depot
  3. Lastly, in bus_tracker_widget.py change the import statement at the beginning to:
    from .core_classes import BusTracker 

In addition to adapting the import statement, we are going to slightly adapt the bus_track_analyzer.py code to better work in concert with the GUI related classes of our plugin code: we are going to add the functionality to emit signals that we can connect to using the QT signal-slot approach. The two signals we are going to add are the following:

  • observationProcessed: This signal will be emitted at the end of the nextStep() method, so whenever the BusTrackAnalyzer object is done with processing one observation, the observation object that has just been processed will be included with the signal.
  • eventDetected: This signal will be emitted for each new event that is added to the allEvents list. The emitted signal includes the new bus event object.

Both signals will be used to connect an object of a new class we are going to write in Section 4.12.6 that has the purpose of showing the developing bus tracks and detected events live in QGIS. For this, it is required that the object be informed about newly processed observations and newly detected events, and this is what we are going to facilitate with these signals. Luckily, adding these signals to bus_track_analyzer.py just requires you to make a few small changes:

  1. Add the following PyQt5 import statement somewhere before the line that starts the class definition with “class BusTrackAnalyzer …”:
    from PyQt5.QtCore import QObject, pyqtSignal
  2. To be able to emit signals we need the class BusTrackerAnalyzer to be derived from the QT class QObject we are importing with the newly added import statement. Therefore, please change the first line of the class definition to:
    class BusTrackAnalyzer(QObject):
  3. Then directly before the start of the constructor with “def __init__...", add the following two lines indented relative to the “class BusTrackAnalyzer…” but not part of any method:
    observationProcessed = pyqtSignal(Observation) 
    eventDetected = pyqtSignal(BusEvent)

    With these two lines we are defining the two signals that can be emitted by this class and the types of the parameters they will include.

  4. Since we are now deriving BusTrackAnalyzer from QObject, we should call the constructor of the base class QObject from the first line of our __init__(…) method. So please add the following line there:
    super(BusTrackAnalyzer, self).__init__()
  5. The last two changes we need to make are both in the definition of the nextStep() method. We therefore show the entire new version of that method here:
      def nextStep(self): 
             """performs next step by processing Observation at the front of the Observations priority queue""" 
             observation = heapq.heappop(self._observationQueue)           # get Observation that is at front of queue 
    
             # go through list of BusEvent subclasses and invoke their detect() method; then collect the events produced 
             # and add them to the allEvents lists 
             for evClass in self._eventClasses: 
                 eventsProduced = evClass.detect(observation, self._depotData, self.allBusTrackers) # invoke event detection method 
                 self.allEvents.extend(eventsProduced)  # add resulting events to event list 
                 for event in eventsProduced:            
                     self.eventDetected.emit(event) 
    
             # update BusTracker of Observation that was just processed 
             observation.busTracker.lastProcessedIndex += 1 
             observation.busTracker.updateSpeed() 
    
             if observation.busTracker.status == BusTracker.STATUS_STOPPED: # if  duration of a stopped event has just expired, change status to "DRIVING" 
                 if observation.timepoint.time > observation.busTracker.statusEvent.timepoint.time + observation.busTracker.statusEvent.duration: 
    		        observation.busTracker.status = BusTracker.STATUS_DRIVING 
                    observation.busTracker.statusEvent = None 
    
             # if this was not the last GPS Timepoint of this bus, create new Observation for the next point and add it to the Observation queue 
             if observation.timepointIndex < len(observation.busTracker.bus.timepoints) - 1:  # not last point 
                 heapq.heappush(self._observationQueue, Observation(observation.busTracker, observation.timepointIndex + 1)  )  
    
             # update analyzer status 
             self.lastProcessedTimepoint = observation.timepoint 
    
             self.observationProcessed.emit(observation)

    In lines 10 and 11 of this new version we added a for-loop that goes through the events produced by the previous call of detect(…) and emit an eventDetected signal for each using the bus event object as a parameter. In the last line of the method, we do the same with the observationProcessed signal including the just processed Observation object.