HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Calculating areas of map features

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
areascalculatingmapfeatures

Problem

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 = QgsE

Solution

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.

Code Snippets

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)
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)
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)
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)))
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)

Context

StackExchange Code Review Q#154245, answer score: 6

Revisions (0)

No revisions yet.