GEOG 489
Advanced Python Programming for GIS

4.13 Lesson 4 Practice Exercise

PrintPrint

The focus in this lesson has been on object-oriented programming in Python and applying it in the context of QGIS to create GUI-based programs and plugins. In the only practice exercise of this lesson, we are going to apply the concepts of self-defined classes, inheritance and overriding methods to build a standalone GIS tool based on the qgis package that is significantly simpler than the project from the lesson walkthroughs. As before, this is intended as a preparation for this lesson's homework assignment in which you are supposed to create a somewhat larger object-oriented tool.

Here is the task: You have been given a .csv file that contains observations of animals in Kruger National Park. Each row in the .csv file contains a unique ID for the observed animal and the latitude and longitude coordinates of the observation, in that order. The observations are ordered chronologically. The test file we will be working with has just the following nine rows. Please download the L4exercise_data.zip file containing this data.

123AD127,-23.965517,31.629621 
183AE121,-23.921094,31.688953 
223FF097,-23.876783,31.661707 
183AE121,-23.876783,31.661707 
123AD121,-23.961818,31.694983 
223FF097,-24.083749,31.824532 
123AD127,-24.083749,31.824532 
873TF129,-24.040581,31.426711 
123AD127,-24.006232,31.428593 

The goal is to write a standalone qgis script that produces a point GeoPackage file with a point feature for just the first observation of each animal occurring in the .csv file. The file contains observations for five different animals and the result when opened in QGIS should look like this:

screenshot of a map with the first animal sightings of five animals marked      
GeoPackage file to be produced in this exercise with first observation points for the five animals

You have already produced some code that reads the data from the file into a pandas data frame stored in variable data. You also want to reuse a class PointObject that you already have for representing point objects with lat and lon coordinates and that has a method called toQgsFeature(…) that is able to produce and return a QgsFeature (see again Section 4.5.3) for a point object of this class.

import qgis 
import sys, os 
import pandas as pd 

# create pandas data frame from input data
data = pd.read_csv(r"C:\489\L4\exercise\L4exercise_data.csv") 

class PointObject(): 

    # constructor for creating PointObject instances with lon/lat instance variables
    def __init__(self, lat, lon): 
        self.lon = lon 
        self.lat = lat 

    # methods for creating QgsFeature object from a PointObject instance
    def toQgsFeature(self): 
        feat = qgis.core.QgsFeature() 
        feat.setGeometry(qgis.core.QgsGeometry.fromPointXY(qgis.core.QgsPointXY(self.lon, self.lat))) 
        return feat 

firstObservations = []          # for storing objects of class pointWithID 
firstObservationsFeatures = []  # for storing objects of class QgsFeature 

When you look at method toQgsFeature(), you will see that it creates a new QgsFeature (see Section 4.5.3), sets the geometry of the feature to a point with the given longitude and latitude coordinates, and then returns the feature. Since PointObject does not have any further attributes, no attributes are defined for the created QgsFeature object.

Your plan now is to write a new class called PointWithID that is derived from the class PointObject and that also stores the unique animal ID in an instance variable. You also want to override the definition of toQgsFeature() in this derived class (see again Section 4.7), so that it also uses setAttributes(…) to make the ID of the animal an attribute of the produced QgsFeature object. To do this, you can first call the toQgsFeature() method of the base class PointObject with the command

super(PointWithID, self).toQgsFeature() 

… and then take the QgsFeature object returned from this call and set the ID attribute for it with setAttributes(…).

Furthermore, you want to override the == operator for PointWithID so that two objects of that class are considered equal if their ID instance variable are the same. This will allow you to store the PointWithID objects created in a list firstObservations and check whether or not the list already contains an observation for the animal in a given PointWithID object in variable pointWithID with the expression

pointWithID in firstObservations

To override the == operator, class PointWithID needs to be given its own definition of the __eq__() method as shown in Section 4.6.2.

What you need to do in this exercise is:

  • Define the class PointWithID according to the specification above.
  • Add the code needed for making this a standalone qgis program (Sections 4.5.4 and 4.10.2.4).
  • Implement a loop that goes through the rows of the pandas data frame (with data.itertuples(), Section 3.8.2) and creates an object of the class PointWithID for the given row, then adds this object to list firstObervations, unless the list already contains an object with the same animal ID.
  • Add the code for creating the new point layer with EPSG:4326 as the CRS.
  • Make firstObservationFeatures a list with a QgsFeature object for each PointWithID object in list firstObservations using the overridden toQgsFeature() method. Then add all features from that new list to the new layer (Section 4.5.3).
  • Finally, write the new layer to a new GeoPackage file and check whether or not you get the same result as shown in the image above.