GEOG 489
Advanced Python Programming for GIS

4.10.2.3 Running the Analysis

PrintPrint

The method nextStep() defined in lines 47 to 71 of bus_track_analyzer.py is where the main work of running a single step in the analysis, meaning processing a single observation, happens. In addition, the class provides a method isFinished() for checking if the analysis has been completed, meaning there are no more observations to be processed in the observation priority queue. Let us first look at how we are calling nextStep() from our main program:

    def run(self):
        """performs only a single analysis step of the BusTrackAnalyzer but starts a timer after each step to call the function again after a
           brief delay until the analzzer has finished. Then saves events and bus tracks to GeoPackage output files."""
        mainWidget.button.setEnabled(False) # disable button so that it can't be run again
        if not analyzer.isFinished():       # if the analyzer hasn't finished yet, perform next step, update widget, and start timer 
                                            # to call this function again
            analyzer.nextStep()       
            mainWidget.busTrackerWidget.updateContent()  
            timer = QTimer()
            timer.singleShot(delay, self.run)
        else:                               # when the analyzer has finished write events and bus tracks to new GeoPackage files   
            analyzer.saveBusTrackPolylineFile("dublin_bus_tracks.gpkg", "GPKG")
            analyzer.saveEventPointFile("dublin_bus_events.gpkg",  "GPKG")

            # reset analyzer and enable button again
            analyzer.reset()
            mainWidget.button.setEnabled(True)  

The method run() in lines 47 to 63 of main.py is called when the “Run” button of our main window for the program is clicked. This connection is done in line 45 of main.py:

self.button.clicked.connect(self.run)

The idea of this method is that unless the analysis has already been completed, it calls nextStep() of the analyzer object to perform the next step (line 53) and then in lines 55 and 56 it starts a QT timer that will invoke run() again once the timer expires. That means run() will be called and executed again and again until all observations have been processed but with small delays between the steps whose length is controlled by variable delay defined in line 21. This gives us some control over how quickly the analysis is run allowing us to observe the changes in the BusTrackerWidget in more detail by increasing the delay value. To make this approach safe, the method first disables the Run button so that the timer is the only way the function can be invoked again.

Now let’s look at the code of nextStep() in more detail (lines 47 to 71 of bus_track_analyzer.py):

	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 

         # 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 

The code is actually simpler than one might think because the actual event detection is done in the bus event classes. The method does the following things in this order:

  1. Take next observation from queue: it takes the Observation object at the front of the _observationQueue priority queue out of the queue (line 49). This is the observation with the earliest timestamp that still needs to be processed. Remember that the Observation object consists of a BusTracker object and the observation Timepoint and its index.
  2. Call detect() function for all event classes: the method then goes through the list of event classes that contains the names of the four event classes making up the bottom level in our bus event hierarchy (see Figure 4.23 from Section 4.10.1) and for each calls the corresponding detect(…) class function (line 54) passing the Observation object as well as the list of depots and the list of all BusTracker objects as parameters, so that the event detection has all the information needed to decide whether an event of this kind is occurring for this observation. The event objects produced and returned as a list from calling detect(…) are then added to the list in allEvent (line 55).
  3. Update BusTracker: next, the BusTracker object for the current observation is updated to reflect that this Timepoint has been processed and we call its updateSpeed(…) method to compute a new speed estimate based on the current, previous, and next Timepoints for that bus. The code for this method can be found in lines 67 to 81 of core_classes.py and it uses the geopy function great_circle(…) to compute the distance between two WGS84 points. Since our bus event hierarchy does not include an event for when a stopped bus starts to move again, we need some place to change its BusTracker status to STATUS_DRIVING when this happens. This is done in lines 61 to 64 of this method.
  4. Put next Observation for this bus into queue: unless this observation was for the last Timepoint from the list of Timepoints for that bus, we now generate a new Observation object with the same BusTracker but for the next Timepoint from the Timepoints list and put this Observation into the priority queue (line 68). Since the queue is always kept sorted by observation time, it is guaranteed that all bus observation will be processed in the correct chronological order.
  5. Update analyzer status: finally, the lastProcessedTimepoint variable of the analyzer itself is updated to always provide the Timepoint of the last Observation processed (line 71).

To illustrate this process, let’s imagine we run the first analysis step for the initial situation from Section 4.10.2.2 with a bus that is not in one of the depots but is nevertheless stopped at the moment. The first Obervation object taken from the queue in step (1) then contains the BusTracker for the bus with ID 2145 and the Timepoint 2013/1/30 23:45:00.

In step(2), we first invoke the detect(…) function of the LeavingDepotEvent class because that is the first class appearing in the list. The code for this function can be found in lines 122 to 140 of bus_events.py.

def detect(observation, depots, activeBusTrackers):
        """process observation and checks whether this event occurs at the given observation. If yes, one or more instances of 
           this Event class are created and returned as a list."""
        producedEvents = [] # initialize list of newly created events to be returned by this function
        
        if observation.busTracker.status == BusTracker.STATUS_INDEPOT:
            isInDepot, depot = Depot.inDepot(observation.timepoint.lat, observation.timepoint.lon, depots)  # test whether bus is still in a depot
            if not isInDepot: # bus not in depot anymore, so leaving depot event will be created and added to result list
                event =  LeavingDepotEvent(observation.timepoint, observation.busTracker.bus, observation.busTracker.depot)
                producedEvents.append(event)
                observation.busTracker.status = BusTracker.STATUS_DRIVING  # update BusTracker object to reflect detected new status
                observation.busTracker.statusEvent = None  
                observation.busTracker.depot = None
                print("Event produced:", str(event))
        
        else:
            pass # nothing to do if bus is not in depot
        
        return producedEvents

The first thing tested there is whether or not the current status of the BusTracker is STATUS_INDEPOT which is not the case. Hence, we immediately return from that function with an empty list as the return value. If instead the condition would have been true, the code of this function would have checked whether or not the bus is currenctly still in a depot by calling the Depot.inDepot(...) function (line 128). If that would not be the case, an event object of this class would be created by calling the LeavingDepotEvent(...) constructor (line 130) and the status information in the corresponding BusTracker object would be updated accordingly (lines 132-135). The created LeavingDepotEvent object would be added to the list in variable producedEvents (line 131) that is returned when the end of the function is reached.

Next, this step is repeated with the detect(…) function from EnteringDepotEvent defined in lines 152 to 170 of bus_events.py. The condition that the bus should currently be driving is satisfied, so the code next checks whether or not the current position of the bus given by observation.timepoint.lat and observation.timepoint.lon (line 158) is inside one of the depots by calling the function inDepot(…) defined as part of the class definition of class Depot in lines 104 to 110 of core_classes.py. This is not the case, so again an empty event list is returned.

Next, detect(…) of the class BusStoppedEvent is called. This is most likely the most difficult to understand version of the detect(…) functions and it can be found in lines 35 to 65 of bus_events.py:

def detect(observation, depots, activeBusTrackers): 
         """process observation and checks whether this event occurs at the given observation. If yes, one or more instances of  
            this Event class are created and returned as a list.""" 
         producedEvents = []  # initialize list of newly created events to be returned by this function 

         if observation.busTracker.status == BusTracker.STATUS_DRIVING: 
             # look ahead until bus has moved at least 3 meters or the end of the Timepoint list is reached 
             timeNotMoving = datetime.timedelta(seconds=0) # for keeping track of time the bus hasn't moved more than 3 meters 
             distance = 0 # for keeping track of distance to original location  
             c = 1 # counter variable for looking ahead 
             while distance < 3 and observation.timepointIndex  + c < len(observation.busTracker.bus.timepoints):   
                 nextTimepoint =  observation.busTracker.bus.timepoints[observation.timepointIndex  + c] # next Timepoint while looking ahead 
                 distance = great_circle( (nextTimepoint.lat, nextTimepoint.lon), (observation.timepoint.lat, observation.timepoint.lon) ).m # distance to next Timepoint 

                 if distance < 3:  # if still below 3 meters, update timeNotMoving 
                     timeNotMoving = nextTimepoint.time - observation.timepoint.time 

                 c += 1 

             # check whether bus didn't move for at least 60 seconds and if so generate event 
             if timeNotMoving.total_seconds() >= 60: 
                 event =  BusStoppedEvent(observation.timepoint, observation.busTracker.bus, timeNotMoving)    # create stopped event 
                 producedEvents.append(event)     # add new event to result list 
                 observation.busTracker.status = BusTracker.STATUS_STOPPED    # update BusTracker object to reflect detected stopped status 
                 observation.busTracker.statusEvent = event 
                 print("Event produced: ", str(event)) 

         else: 
             pass # no stop event will be created while bus status is "IN DEPOT" or "DRIVING" 

         return producedEvents 

The condition is again that the current BusTracker status is “driving” which is satisfied. The code will then run a while-loop that looks at the next Timepoints in the list of Timepoints for this bus until the distance to the current position gets larger than 3 meters. In this case, this only happens for the fifth Timepoint following the current Timepoint. The code then looks at the time difference between these two Timepoints (line 55) and if its more than 60 second, like in this case, creates a new object of class BusStoppedEvent (line 56) using the current Timepoint, Bus object from the BusTracker, and time difference to set the instance variables of the newly created event object. This event object is put into the event list that will be returned by the detect(…) function (line 57). Finally, the status of the BusTracker object involved will be changed to “stopped” (line 58) and we also store the event object inside the BusTracker to be able to change the status back to “driving” when the duration of the event is over (line 59). When we return from the detect(…) function, the produced BusStoppedEvent will be added to the allEvents list of the analyzer (line 55 of bus_track_analyzer.py).

Finally, detect(…) of BusEncounterEvent will be called, the last event class from the list. If you look at lines 83 to 102 of bus_events.py, you will see that a requirement for this event is that the bus is currently “driving”. Since we just changed the status of the BusTracker to “stopped” this is not the case and no events will be generated and returned from this function call. Just to emphasize this again, the details of how the different detect(...) functions work are less important here; the important thing to understand is that we are using the detect(...) functions defined in each of the bottom level bus event classes to test whether or not one (or even multiple) event(s) of that type occurred and if so generate an event object of that class with information describing that event by calling the constructor of the class (e.g., BusStoppedEvent(...)). Each created event object is added to the list that the detect(...) function returns to the calling nextStep(...) function. In the lesson's homework assignment you will have to use a similar approach but within a much less complicated project.

Now steps (3) –(6) are performed with the result that the lastProcessedIndex of the BusTracker is increased by one (to 1), a new estimated speed is computed for it, a new observation is created for bus 2145 and added to the queue, now for time point 2013/1/30 23:45:03. Since the first observation for bus 270 only has a timestamp of 23:45:05, the new Observation is inserted into the queue in second place after the first Observation for the bus with busId 5. Finally, the lastProcessedTimepoint of the analyzer is changed to 2013/1/30 23:45:00. The resulting constellation after this first run of nextStep() is shown in the image below.

Screenshot to show the constellation after the first run of nextStep()
Figure 4.26 Situation after first event detection step has been performed processing the first Observation from the priority queue

We have intentionally placed some print statements inside the bus event classes from bus_events.py whenever a new event of that class is detected and a corresponding object is created. Normally you wouldn’t do that but here we want to keep track of the events produced when running the main program. So test out the program by executing main.py (e.g., from the OSGeo4W shell after running the commands for setting the environment variables as described in Section 4.4.1) and just look at the output produced in the console, while still ignoring the graphical output in the window for a moment.

The produced output will start like this but list quite a few more bus events detected during the analysis:

Screen shot to show list of bus events
Figure 4.27 Output about detected events produced by running the program