4.13 Lesson 4 Practice Exercise
4.13 Lesson 4 Practice Exercise jmk649The focus in this lesson has been on object-oriented programming in Python and applying it within the QGIS environment 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. This will be significantly simpler than the bus tracker walkthrough and is intended to prepare you for this lesson's homework assignment.
Here is the task: You have been given a .csv file that contains observations of five different 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 GeoPackage file with a point feature for just the first observation of each animal occurring in the .csv file. The result when opened in QGIS should look like this:
You have already produced some code that reads the data from the file into a pandas dataframe 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 next step 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().toQgsFeature()
… and then take the QgsFeature object returned from this call and set the ID attribute for it with setAttributes(…).
Next, you want to override the == ( __eq__ ) 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 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 dataframe (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 layer to a new GeoPackage file and check whether you get the same result as shown in the image above.
- There are several 'overload' options for the writeAsVectorFormat methods that accepts different parameters and they are in different orders. The example solution uses the V3 (current) version that requires: layer, fileName, transformContext: qgis.core.QgsCoordinateTransformContext(), options: QgsVectorFileWriter.SaveVectorOptions
4.13.1 Lesson 4 Practice Exercise Solution and Explanation
4.13.1 Lesson 4 Practice Exercise Solution and Explanation jmk6494.13 Lesson 4 Practice Exercise
Solution
import qgis
import qgis.core
import sys, os
import pandas as pd
# read data into pandas data frame
data = pd.read_csv(r"C:\489\L4\exercise\L4exercise_data.csv")
# class definition for PointObject
class PointObject():
def __init__(self, lat, lon):
self.lon = lon
self.lat = lat
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
# code for creating QgsApplication and initializing QGIS environment
qgis_prefix = os.getenv("QGIS_PREFIX_PATH")
qgis.core.QgsApplication.setPrefixPath(qgis_prefix, True)
qgs = qgis.core.QgsApplication([], False)
qgs.initQgis()
# definition of class PointWithID derived from PointObject
# to represent animal observation from the input data
class PointWithID(PointObject):
def __init__(self, pID, lat, lon):
super().__init__(lat, lon)
self.pID = pID # instance variable for storing animal ID
# overwriting the == operator to be based on the animal ID
def __eq__(self, other):
return self.pID == other.pID
# overwriting this method to include animal ID as attribute of QgsFeature created
def toQgsFeature(self):
feat = super().toQgsFeature()
feat.setAttributes([self.pID])
return feat
# create list of PointWithID object with first observations for each animal in the data frame
for row in data.itertuples(index=False):
pointWithID = PointWithID(row[0], row[1], row[2])
if not pointWithID in firstObservations: # here __eq__() is used to do the comparison
firstObservations.append(pointWithID)
# list comprehension for creating list of features from firstObservations list
firstObservationsFeatures = [ o.toQgsFeature() for o in firstObservations ]
# create new point layer with field for animal ID
layer = qgis.core.QgsVectorLayer("Point?crs=EPSG:4326&field=AnimalID:string(255)", 'animal first observations' ,'memory')
# add features to new layer
prov = layer.dataProvider()
prov.addFeatures(firstObservationsFeatures)
# save layer as GeoPackage file
options = QgsVectorFileWriter.SaveVectorOptions()
options.driverName = "GPKG"
# writeAsVectorFormatV3(layer, fileName, transformContext: qgis.core.QgsCoordinateTransformContext(), options: QgsVectorFileWriter.SaveVectorOptions)
qgis.core.QgsVectorFileWriter.writeAsVectorFormatV3(layer, r"C:\489\firstobservations.gpkg", qgis.core.QgsCoordinateTransformContext(), options)
# clean up
qgs.exitQgis()
Explanation
- In line 34 the new class PointWithID is declared to be derived from class PointObject.
- The constructor in line 36 calls the constructor of the base class and then adds the additional instance variable for the animal ID (called pID).
- __eq__() is defined in lines 43 and 44 to compare the pID instance variables of both involved objects and return True if they are equal.
- In lines 48 and 49, we call toQgsFeature() of the base class PointObject and then take the returned feature and add the animal ID as the only attribute.
- In lines 53 to 56, we use itertuples() of the data frame to loop through its rows. We create a new PointWithID object in variable pointWithID from the cell values of the row and then test whether an object with that ID is already contained in list firstObservations. If not, we add this object to the list.
- In line 59, we use toQgsFeature() to create a QgsFeature object from each PointWithID object in the list. These are then added to the new layer in line 66.
- When creating the new layer in line 62, we have to make sure to include the field for storing the animal ID in the string for the first parameter.