# Calculating areas of map features

I am developing a plugin for QGIS (a Geographic Information Systems) software which uses Python. I have calculated four areas of interest, it does this by identifying each feature on the map (called map_layer) which falls into certain categories determined by an expression.

For example, Area 1 consists of three categories with the following expressions:

Category                     Expression

NULL or Good and Excellent   "Category_A" IS NULL OR (( "Category_A" = 'Good') AND ( "Category_B" = 'Excellent'))
Good and Standard            ("Category_A" = 'Good') AND ("Category_B" = 'Standard')
Meh and Excellent            ("Category_A" = 'Meh') AND ("Category_B" = 'Excellent')


For each category, the geometric area is then calculated, totalled and shown in a QLineEdit box. This is then repeated for Area 2, Area 3 and Area 4.

My question is how could I make the following code more concise?

def land_area():
# Clear all area text boxes
self.dockwidget.area_Score1_lineEdit.clear()
self.dockwidget.area_Score2_lineEdit.clear()
self.dockwidget.area_Score3_lineEdit.clear()
self.dockwidget.area_Score4_lineEdit.clear()

##########################
######### AREA 1 #########
# NULL or Good and Excellent
# Set area to 0
area1_score = 0
area1_cat1 = QgsExpression( """ "Category_A" IS NULL OR (( "Category_A" = 'Good') AND ( "Category_B" = 'Excellent')) """ )
area1_cat1_feat = map_layer.getFeatures( QgsFeatureRequest( area1_cat1 ) )
area1_cat1_ids = [i for i in area1_cat1_feat]
for f in area1_cat1_ids:
area1_score += f.geometry().area()
# Good and Standard
area1_cat2 = QgsExpression( """ ("Category_A" = 'Good') AND ("Category_B" = 'Standard') """ )
area1_cat2_feat = map_layer.getFeatures( QgsFeatureRequest( area1_cat2 ) )
area1_cat2_ids = [i for i in area1_cat2_feat]
for f in area1_cat2_ids:
area1_score += f.geometry().area()
# Meh and Excellent
area1_cat3 = QgsExpression( """ ("Category_A" = 'Meh') AND ("Category_B" = 'Excellent') """ )
area1_cat3_feat = map_layer.getFeatures( QgsFeatureRequest( area1_cat3 ) )
area1_cat3_ids = [i for i in area1_cat3_feat]
for f in area1_cat3_ids:
area1_score += f.geometry().area()

##########################
######### AREA 2 #########
# Good and Terrible
# Set area to 0
area2_score = 0
area2_cat1 = QgsExpression( """ ( "Category_A" = 'Good') AND ( "Category_B" = 'Terrible') """ )
area2_cat1_feat = map_layer.getFeatures( QgsFeatureRequest( area2_cat1 ) )
area2_cat1_ids = [i for i in area2_cat1_feat]
for f in area2_cat1_ids:
area2_score += f.geometry().area()
# Meh and Standard
area2_cat2 = QgsExpression( """ ( "Category_A" = 'Meh') AND ( "Category_B" = 'Standard') """ )
area2_cat2_feat = map_layer.getFeatures( QgsFeatureRequest( area2_cat2 ) )
area2_cat2_ids = [i for i in area2_cat2_feat]
for f in area2_cat2_ids:
area2_score += f.geometry().area()
area2_cat3 = QgsExpression( """ ( "Category_A" = 'Bad') AND ( "Category_B" = 'Excellent') """ )
area2_cat3_feat = map_layer.getFeatures( QgsFeatureRequest( area2_cat3 ) )
area2_cat3_ids = [i for i in area2_cat3_feat]
for f in area2_cat3_ids:
area2_score += f.geometry().area()

##########################
######### AREA 3 #########
# Meh and Terrible
# Set area to 0
area3_score = 0
area3_cat1 = QgsExpression( """ ( "Category_A" = 'Meh') AND ( "Category_B" = 'Terrible') """ )
area3_cat1_feat = map_layer.getFeatures( QgsFeatureRequest( area3_cat1 ) )
area3_cat1_ids = [i for i in area3_cat1_feat]
for f in area3_cat1_ids:
area3_score += f.geometry().area()
area3_cat2 = QgsExpression( """ ( "Category_A" = 'Bad') AND ( "Category_B" = 'Standard') """ )
area3_cat2_feat = map_layer.getFeatures( QgsFeatureRequest( area3_cat2 ) )
area3_cat2_ids = [i for i in area3_cat2_feat]
for f in area3_cat2_ids:
area3_score += f.geometry().area()
area3_cat3 = QgsExpression( """ ( "Category_A" = 'Bad') AND ( "Category_B" = 'Terrible') """ )
area3_cat3_feat = map_layer.getFeatures( QgsFeatureRequest( area3_cat3 ) )
area3_cat3_ids = [i for i in area3_cat3_feat]
for f in area3_cat3_ids:
area3_score += f.geometry().area()

##########################
######### AREA 4 #########
# Unavailable
# Set area to 0
area4_score = 0
area4_cat = QgsExpression( """ ( "Category_A" = 'Unavailable') """ )
area4_cat_feat = map_layer.getFeatures( QgsFeatureRequest( area4_cat ) )
area4_cat_ids = [i for i in area4_cat_feat]
for f in area4_cat_ids:
area4_score += f.geometry().area()

########################
self.dockwidget.area_Score1_lineEdit.setText("{:,.0f}".format(area1_score))
self.dockwidget.area_Score2_lineEdit.setText("{:,.0f}".format(area2_score))
self.dockwidget.area_Score3_lineEdit.setText("{:,.0f}".format(area3_score))
self.dockwidget.area_Score4_lineEdit.setText("{:,.0f}".format(area4_score))


You can greatly simplify this code by introducing a helper function that calculates the area_score for one of the categories.

Let's take the first category as an example. You do three times the same job, with a different expression each time. This can be put into a function that takes an expression and returns the total area for that expression:

def land_area(expression):
"""Returns the area of all features which match the given expression."""
qgs_expression = QgsExpression(expression)
features = map_layer.getFeatures(QgsFeatureRequest(qgs_expression))
return sum(feature.geometry().area() for feature in features)


Note that I used the fact that sum can take a generator expression. There is also no need for the intermediate index list. It also assumes map_layer is some global variable accessible. otherwise you will have to pass it as parameter, or make this function a method by adding self as a parameter.

With this done we can now loop over all expressions defining one category and output the total land area of multiple expressions:

def land_area_total(expressions):
"""
Returns the sum of all areas matching any expression in expressions.
Does not remove double-counting.
"""
return sum(land_area(expression) for expression in expressions)


Note the caveat that this does not remove double-counting. So if feature matches more than one expression it will be counted for more than once. To avoid this you would have to OR them all together:

def land_area_total(expressions):
"""
Returns the sum of all areas matching any expression in expressions.
Removes double-counting.
"""
expression = " OR ".join("({})".format(e) for e in expressions)
return land_area(expression)


The only thing left now, is to give this function the appropriate expressions. For this we loop over a list of list of expressions (one list for each category) and the text field belonging to that category:

def set_land_areas(area_lines, area_expressions):
"""Update the text in all area_lines with the areas calculated using the area_expressions."""
for area_line, expressions in zip(area_lines, area_expressions):
area_line.clear()
area_line.setText("{:,.0f}".format(land_area_total(expressions)))


These two lists can be set either in the above function or (which I opted for here) defined outside and passed to it, making it more re-usable.

def set_category_areas(self):
area_lines = (self.dockwidget.area_Score1_lineEdit,
self.dockwidget.area_Score2_lineEdit,
self.dockwidget.area_Score3_lineEdit,
self.dockwidget.area_Score4_lineEdit)
area_expressions = (['"Category_A" IS NULL OR(("Category_A"="Good") AND("Category_B"="Excellent"))',
'("Category_A" = "Good") AND ("Category_B" = "Standard")',
'("Category_A" = "Meh") AND ("Category_B" = "Excellent")'],
...)

set_land_areas(area_lines, area_expressions)


Note that you will have to complete area_expressions with the expressions for the other categories.

• Works absolutely perfectly! Many thanks =) – Joseph Feb 3 '17 at 14:22
• @Joseph You're welcome. Note that I just updated the answer to contain docstrings and to fix a possible issue of double counting. – Graipher Feb 3 '17 at 16:39
• Thank you for the update, that indeed fixed the issue you described (which I wasn't aware of initially). Awarding you a bounty :) – Joseph Feb 6 '17 at 10:23
• @Joseph Yay, my first bounty :) – Graipher Feb 13 '17 at 15:10
• First of many I'm sure ;) – Joseph Feb 13 '17 at 15:15