6
\$\begingroup\$

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()
    # Bad and Excellent
    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()
    # Bad and Standard
    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()
    # Bad and Terrible
    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))
\$\endgroup\$

1 Answer 1

6
+50
\$\begingroup\$

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.

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.