Beispiel #1
0
    def test_FeatureRequestSortByVirtualField(self):
        layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer",
                               "addfeat", "memory")
        pr = layer.dataProvider()
        f1 = QgsFeature()
        f1.setAttributes(["test", 123])
        f2 = QgsFeature()
        f2.setAttributes(["test", 124])
        self.assertTrue(pr.addFeatures([f1, f2]))

        idx = layer.addExpressionField('if("fldint"=123,3,2)', QgsField('exp1', QVariant.LongLong))  # NOQA

        QgsProject.instance().addMapLayers([layer])

        request = QgsFeatureRequest()
        request.setOrderBy(QgsFeatureRequest.OrderBy([QgsFeatureRequest.OrderByClause('exp1', True)]))
        ids = []
        for feat in layer.getFeatures(request):
            ids.append(feat.id())
        self.assertEqual(ids, [2, 1])

        request.setOrderBy(QgsFeatureRequest.OrderBy([QgsFeatureRequest.OrderByClause('exp1', False)]))
        ids = []
        for feat in layer.getFeatures(request):
            ids.append(feat.id())
        self.assertEqual(ids, [1, 2])

        QgsProject.instance().removeMapLayers([layer.id()])
Beispiel #2
0
    def test_ZFeatureRequestSortByAuxiliaryField(self):
        s = QgsAuxiliaryStorage()
        self.assertTrue(s.isValid())

        layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer",
                               "addfeat", "memory")
        pr = layer.dataProvider()
        f1 = QgsFeature()
        f1.setAttributes(["test", 123])
        f2 = QgsFeature()
        f2.setAttributes(["test", 124])
        self.assertTrue(pr.addFeatures([f1, f2]))

        # Create a new auxiliary layer with 'pk' as key
        pkf = layer.fields().field(layer.fields().indexOf('fldint'))
        al = s.createAuxiliaryLayer(pkf, layer)
        self.assertTrue(al.isValid())
        layer.setAuxiliaryLayer(al)

        prop = QgsPropertyDefinition()
        prop.setComment('test_field')
        prop.setDataType(QgsPropertyDefinition.DataTypeNumeric)
        prop.setOrigin('user')
        prop.setName('custom')
        self.assertTrue(al.addAuxiliaryField(prop))

        layer.startEditing()
        i = 2
        for feat in layer.getFeatures():
            feat.setAttribute(2, i)
            layer.updateFeature(feat)
            i -= 1
        layer.commitChanges()

        request = QgsFeatureRequest()
        request.setOrderBy(QgsFeatureRequest.OrderBy([QgsFeatureRequest.OrderByClause(layer.fields()[2].name(), True)]))
        ids = []
        for feat in layer.getFeatures(request):
            ids.append(feat.id())
        self.assertEqual(ids, [2, 1])

        request.setOrderBy(QgsFeatureRequest.OrderBy([QgsFeatureRequest.OrderByClause(layer.fields()[2].name(), False)]))
        ids = []
        for feat in layer.getFeatures(request):
            ids.append(feat.id())
        self.assertEqual(ids, [1, 2])

        QgsProject.instance().removeMapLayers([layer.id()])
Beispiel #3
0
    def test_FeatureRequestSortByJoinField(self):
        """ test sorting requested features using a joined columns """
        joinLayer = QgsVectorLayer(
            "Point?field=x:string&field=y:integer&field=z:integer",
            "joinlayer", "memory")
        pr = joinLayer.dataProvider()
        f1 = QgsFeature()
        f1.setAttributes(["foo", 123, 321])
        f2 = QgsFeature()
        f2.setAttributes(["bar", 124, 654])
        self.assertTrue(pr.addFeatures([f1, f2]))

        layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer",
                               "addfeat", "memory")
        pr = layer.dataProvider()
        f1 = QgsFeature()
        f1.setAttributes(["test", 123])
        f2 = QgsFeature()
        f2.setAttributes(["test", 124])
        self.assertTrue(pr.addFeatures([f1, f2]))

        QgsProject.instance().addMapLayers([layer, joinLayer])

        join = QgsVectorLayerJoinInfo()
        join.setTargetFieldName("fldint")
        join.setJoinLayer(joinLayer)
        join.setJoinFieldName("y")
        join.setUsingMemoryCache(True)
        layer.addJoin(join)

        request = QgsFeatureRequest()
        request.setOrderBy(QgsFeatureRequest.OrderBy([QgsFeatureRequest.OrderByClause('joinlayer_z', True)]))
        ids = []
        for feat in layer.getFeatures(request):
            ids.append(feat.id())
        self.assertEqual(ids, [1, 2])

        request.setOrderBy(QgsFeatureRequest.OrderBy([QgsFeatureRequest.OrderByClause('joinlayer_z', False)]))
        ids = []
        for feat in layer.getFeatures(request):
            ids.append(feat.id())
        self.assertEqual(ids, [2, 1])

        QgsProject.instance().removeMapLayers([layer.id(), joinLayer.id()])
Beispiel #4
0
def __get_qgis_features(qgis_layer,
                        qgis_feature_request=None,
                        bbox_filter=None,
                        attribute_filters=None,
                        search_filter=None,
                        with_geometry=True,
                        page=None,
                        page_size=None,
                        ordering=None,
                        exclude_fields=None,
                        extra_expression=None,
                        extra_subset_string=None):
    """Private implementation for count and get"""

    if qgis_feature_request is None:
        qgis_feature_request = QgsFeatureRequest()

    if exclude_fields is not None:
        if exclude_fields == '__all__':
            qgis_feature_request.setNoAttributes()
        else:
            qgis_feature_request.setSubsetOfAttributes([
                name for name in qgis_layer.fields().names()
                if name not in exclude_fields
            ], qgis_layer.fields())

    expression_parts = []

    if extra_expression is not None:
        expression_parts.append(extra_expression)

    if not with_geometry:
        qgis_feature_request.setFlags(QgsFeatureRequest.NoGeometry)

    if bbox_filter is not None:
        assert isinstance(bbox_filter, QgsRectangle)
        qgis_feature_request.setFilterRect(bbox_filter)

    # Ordering
    if ordering is not None:
        ascending = True
        if ordering.startswith('-'):
            ordering = ordering[1:]
            ascending = False
        order_by = QgsFeatureRequest.OrderBy(
            [QgsFeatureRequest.OrderByClause('"%s"' % ordering, ascending)])
        qgis_feature_request.setOrderBy(order_by)

    # Search
    if search_filter is not None:
        exp_template = '"{field_name}" ILIKE \'%' + search_filter.replace(
            '\'', '\\\'') + '%\''
        exp_parts = []
        for f in qgis_layer.fields():
            exp_parts.append(
                exp_template.format(field_name=f.name().replace('"', '\\"')))
        expression_parts.append(' OR '.join(exp_parts))

    # Attribute filters
    if attribute_filters is not None:
        exp_parts = []
        for field_name, field_value in attribute_filters.items():
            exp_parts.append('"{field_name}" ILIKE \'%{field_value}%\''.format(
                field_name=field_name.replace('"', '\\"'),
                field_value=str(field_value).replace('\'', '\\\'')))
        expression_parts.append(' AND '.join(exp_parts))

    offset = 0
    feature_count = qgis_layer.featureCount()

    if page is not None and page_size is not None:
        page_size = int(page_size)
        page = int(page)
        offset = page_size * (page - 1)
        feature_count = page_size * page
        # Set to max, without taking filters into account
        qgis_feature_request.setLimit(feature_count)
    else:
        page_size = None  # make sure it's none

    # Fetch features
    if expression_parts:
        qgis_feature_request.combineFilterExpression(
            '(' + ') AND ('.join(expression_parts) + ')')

    logger.debug(
        'Fetching features from layer {layer_name} - filter expression: {filter} - BBOX: {bbox}'
        .format(layer_name=qgis_layer.name(),
                filter=qgis_feature_request.filterExpression(),
                bbox=qgis_feature_request.filterRect()))

    features = []

    original_subset_string = qgis_layer.subsetString()
    if extra_subset_string is not None:
        subset_string = original_subset_string
        if subset_string:
            qgis_layer.setSubsetString(
                "({original_subset_string}) AND ({extra_subset_string})".
                format(original_subset_string=original_subset_string,
                       extra_subset_string=extra_subset_string))
        else:
            qgis_layer.setSubsetString(extra_subset_string)

    iterator = qgis_layer.getFeatures(qgis_feature_request)

    try:
        for _ in range(offset):
            next(iterator)
        if page_size is not None:
            for __ in range(page_size):
                features.append(next(iterator))
        else:
            while True:
                features.append(next(iterator))
    except StopIteration:
        pass

    if extra_subset_string is not None:
        qgis_layer.setSubsetString(original_subset_string)

    return features
    def processAlgorithm(self, parameters, context, model_feedback):
        # Use a multi-step feedback, so that individual child algorithm progress reports are adjusted for the
        # overall progress through the model
        feedback = QgsProcessingMultiStepFeedback(5, model_feedback)
        results = {}
        outputs = {}

        MNT = self.parameterAsRasterLayer(parameters, self.INPUT, context)
        emprise = self.parameterAsVectorLayer(parameters, self.EMPRISE,
                                              context)
        dZ = self.parameterAsDouble(parameters, self.DZ, context)
        fichier_html = self.parameterAsFileOutput(parameters, self.OUTPUT,
                                                  context)
        fichier_txt = "{}.txt".format(os.path.splitext(fichier_html)[0])

        if parameters[self.MAXZ] == None:
            maxZ = 99999
        else:
            maxZ = self.parameterAsDouble(parameters, self.MAXZ, context)

        # Découper un raster selon une couche de masquage
        alg_params = {
            "ALPHA_BAND": False,
            "CROP_TO_CUTLINE": True,
            "DATA_TYPE": 0,
            "EXTRA": "",
            "INPUT": MNT.source(),
            "KEEP_RESOLUTION": True,
            "MASK": emprise,
            "MULTITHREADING": False,
            "NODATA": None,
            "OPTIONS": "",
            "SET_RESOLUTION": False,
            "SOURCE_CRS": None,
            "TARGET_CRS": "ProjectCrs",
            "X_RESOLUTION": None,
            "Y_RESOLUTION": None,
            "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT,
        }
        outputs["Clip"] = processing.run(
            "gdal:cliprasterbymasklayer",
            alg_params,
            context=context,
            feedback=feedback,
            is_child_algorithm=True,
        )

        feedback.setCurrentStep(1)
        if feedback.isCanceled():
            return {}

        # Polygones Courbes de niveau
        alg_params = {
            "BAND": 1,
            "CREATE_3D": False,
            "EXTRA": "",
            "FIELD_NAME_MAX": "ELEV_MAX",
            "FIELD_NAME_MIN": "ELEV_MIN",
            "IGNORE_NODATA": False,
            "INPUT": outputs["Clip"]["OUTPUT"],
            "INTERVAL": dZ,
            "NODATA": None,
            "OFFSET": 0,
            "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT,
        }
        outputs["PolygonesCourbesDeNiveau"] = processing.run(
            "gdal:contour_polygon",
            alg_params,
            context=context,
            feedback=feedback,
            is_child_algorithm=True,
        )

        feedback.setCurrentStep(2)
        if feedback.isCanceled():
            return {}

        # Collecter les géométries
        alg_params = {
            "FIELD": ["ELEV_MIN"],
            "INPUT": outputs["PolygonesCourbesDeNiveau"]["OUTPUT"],
            "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT,
        }
        outputs["CollectGeom"] = processing.run("native:collect",
                                                alg_params,
                                                context=context,
                                                feedback=feedback,
                                                is_child_algorithm=True)

        feedback.setCurrentStep(3)
        if feedback.isCanceled():
            return {}

        # Calcul "Surface"
        layer = QgsProcessingUtils.generateTempFilename("layer.shp")
        alg_params = {
            "FIELD_LENGTH": 12,
            "FIELD_NAME": "Surface",
            "FIELD_PRECISION": 2,
            "FIELD_TYPE": 0,
            "FORMULA": "$area",
            "INPUT": outputs["CollectGeom"]["OUTPUT"],
            "OUTPUT": layer,
        }
        outputs["CalculSurface"] = processing.run(
            "native:fieldcalculator",
            alg_params,
            context=context,
            feedback=feedback,
            is_child_algorithm=True,
        )

        feedback.setCurrentStep(4)
        if feedback.isCanceled():
            return {}

        fields = QgsFields()
        fields.append(QgsField("Z", QVariant.Double))
        fields.append(QgsField("Surface", QVariant.Double))
        fields.append(QgsField("Volume", QVariant.Double))
        (couche, dest_id) = self.parameterAsSink(
            parameters,
            self.OUTPUT2,
            context,
            fields,
            QgsWkbTypes.NoGeometry,
            QgsProject.instance().crs(),
        )

        with open(fichier_html, "w") as f_html, open(fichier_txt,
                                                     "w") as f_txt:
            f_html.write("""
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Courbe HSV</title>
    <style>
    html {
      font-family: sans-serif;
    }

    table {
      border-collapse: collapse;
      border: 2px solid rgb(200,200,200);
      letter-spacing: 1px;
      font-size: 0.8rem;
    }
    
    td, th {
      border: 1px solid rgb(190,190,190);
      padding: 10px 20px;
    }

    td {
      text-align: center;
    }

    caption {
      padding: 10px;
    }
    </style>
  </head>
  <body>
    <h1>Courbe HSV</h1>

    <table>
        <tr>
            <th>Z</th>
            <th>Surface (m&sup2;)</th>
            <th>Volume (m&sup3;)</th>
        </tr>
""")
            f_txt.write("Z\tSurface\tVolume\n")

            z = []
            surface = []
            volume = []

            vLayer = QgsVectorLayer(layer, "temp")

            request = QgsFeatureRequest()

            # Ordonner par ELEV_MIN ascendant
            clause = QgsFeatureRequest.OrderByClause("ELEV_MIN",
                                                     ascending=True)
            orderby = QgsFeatureRequest.OrderBy([clause])
            request.setOrderBy(orderby)

            for current, feat in enumerate(vLayer.getFeatures(request)):
                if feedback.isCanceled():
                    return {}
                if feat["ELEV_MAX"] > maxZ:
                    break
                if current == 0:
                    z.append(round(feat["ELEV_MAX"], 2))
                    surface.append(round(feat["Surface"], 2))
                    volume.append(round(feat["Surface"] * dZ / 2, 2))
                else:
                    z.append(round(feat["ELEV_MAX"], 2))
                    surface.append(round(surface[-1] + feat["Surface"], 2))
                    volume.append(
                        round(
                            feat["Surface"] * dZ / 2 + surface[-2] * dZ +
                            volume[-1], 2))

                self.writeHTMLTableLine(f_html, z[-1], surface[-1], volume[-1])
                f_txt.write("{}\t{}\t{}\n".format(z[-1], surface[-1],
                                                  volume[-1]))

                if couche is not None:
                    fet = QgsFeature()
                    tabAttr = [z[-1], surface[-1], volume[-1]]
                    fet.setAttributes(tabAttr)
                    couche.addFeature(fet)

            f_html.write("""
    </table>

  </body>
</html>
""")

        return {self.OUTPUT: fichier_html, self.OUTPUT2: dest_id}
    def processAlgorithm(self, parameters, context, feedback):
        #retrieve the layer inputs
        source1 = self.parameterAsSource(
            parameters,
            self.INPUT1,
            context
        )
        source2 = self.parameterAsSource(
            parameters,
            self.INPUT2,
            context
        )
        source3 = self.parameterAsSource(
            parameters,
            self.INPUT3,
            context
        )
        source4 = self.parameterAsSource(
            parameters,
            self.INPUT4,
            context
        )
        source5 = self.parameterAsSource(
            parameters,
            self.INPUT5,
            context
        )
        source6 = self.parameterAsSource(
            parameters,
            self.INPUT6,
            context
        )
        source7 = self.parameterAsSource(
            parameters,
            self.INPUT7,
            context
        )
        source8 = self.parameterAsSource(
            parameters,
            self.INPUT8,
            context
        )

        #if a layer was not found, throw an exception to indicate that the algorithm encountered a fatal error
        if source1 is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT1))
        if source2 is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT2))
        if source3 is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT3))
        if source4 is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT4))
        if source5 is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT5))
        if source6 is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT6))
        if source7 is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT7))
        if source8 is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT8))
        
        feedback.pushInfo('--------------------------------------------------------------------------')
        feedback.pushInfo("1/5 - Checking CRS...")
        feedback.pushInfo('--------------------------------------------------------------------------')
        
        #get crs of each layer
        crs1 = source1.sourceCrs().authid()
        crs2 = source2.sourceCrs().authid()
        crs3 = source3.sourceCrs().authid()
        crs4 = source4.sourceCrs().authid()
        crs5 = source5.sourceCrs().authid()
        crs6 = source6.sourceCrs().authid()
        crs7 = source7.sourceCrs().authid()
        crs8 = source8.sourceCrs().authid()
        
        #check crs of each layer and stop script if not all matching
        if crs1 == crs2 and crs1 == crs3 and crs1 == crs4 and crs1 == crs5 and crs1 == crs6 and crs1 == crs7 and crs1 == crs8:
            feedback.pushInfo('CRS is ' + (crs1))            
            feedback.pushInfo('CRS is matching for all layers')
        else:   
            feedback.pushInfo('Please ensure matching CRS for all layers')
            feedback.pushInfo('CRS for INPUT1 is ' + (crs1))
            feedback.pushInfo('CRS for INPUT2 is ' + (crs2))
            feedback.pushInfo('CRS for INPUT3 is ' + (crs3))
            feedback.pushInfo('CRS for INPUT4 is ' + (crs4))
            feedback.pushInfo('CRS for INPUT5 is ' + (crs5))
            feedback.pushInfo('CRS for INPUT6 is ' + (crs6))
            feedback.pushInfo('CRS for INPUT7 is ' + (crs7))
            feedback.pushInfo('CRS for INPUT8 is ' + (crs8))
            return{}
         
        #check if script has been cancelled before next stage
        if feedback.isCanceled():
            return{}   
        
        #output current stage of script
        feedback.pushInfo('--------------------------------------------------------------------------') 
        feedback.pushInfo("2/5 - Preparing Layer Data...")
        feedback.pushInfo('--------------------------------------------------------------------------')

        #get the layer data from the inputs
        lgaLayer = parameters['INPUT1']
        parcelsLayer = parameters['INPUT2']
        addressLayer = parameters['INPUT3']
        zonesLayer = parameters['INPUT4']
        overlaysLayer = parameters['INPUT5']
        floodLayer = parameters['INPUT6']
        coastLayer = parameters['INPUT7']
        watercourseLayer = parameters['INPUT8']
        
        #clip flood area to LGA
        result = processing.run('native:clip', { 'INPUT': floodLayer, 'OUTPUT': 'memory:', 'OVERLAY': lgaLayer }, context=context, feedback=feedback)
        floodLayer = result["OUTPUT"]
        
        #create a flood check layer (coast and waterways)
        floodCheckList = [coastLayer,watercourseLayer]
        result = processing.run('native:mergevectorlayers', {"LAYERS": floodCheckList, "OUTPUT": 'memory:' }, context=context, feedback=feedback)
        floodCheckLayer = result["OUTPUT"]
        
        #clean up flood layer (keep only areas that intersect with flood check layer)
        result = processing.run('native:extractbylocation', { 'INPUT': floodLayer, 'INTERSECT': floodCheckLayer, 'METHOD': 0, 'OUTPUT': 'memory:', 'PREDICATE': [0] }, context=context, feedback=feedback)
        floodCleanedLayer = result["OUTPUT"]
        
        #get total area of LGA
        result = processing.run('qgis:exportaddgeometrycolumns', { 'CALC_METHOD': 0, 'INPUT': lgaLayer, 'OUTPUT': 'memory:' }, context=context, feedback=feedback)
        lgaAreaLayer = result["OUTPUT"]
        
        #get zone areas that intersect with flood layer
        result = processing.run('native:extractbylocation', { 'INPUT': zonesLayer, 'INTERSECT': floodCleanedLayer, 'METHOD': 0, 'OUTPUT': 'memory:', 'PREDICATE': [0] }, context=context, feedback=feedback)
        zonesLayer = result["OUTPUT"]
        
        #add "ZONE_CLASS" field to zones layer
        zonesLayer.startEditing()
        zonesLayer.dataProvider().addAttributes([QgsField("ZONE_CLASS", QVariant.String)])
        zonesLayer.updateFields()
        zonesLayer.commitChanges()
        
        #add "ZONE_CLASS" attribute for each feature ("ZONE_CODE" without numeric digit)
        zFeatures = zonesLayer.getFeatures()
        zonesLayer.startEditing()
        for feature in zFeatures:
            ini_string = feature["ZONE_CODE"]
            res = ''.join([i for i in ini_string if not i.isdigit()]) 
            feature["ZONE_CLASS"] = res
            zonesLayer.updateFeature(feature)
        zonesLayer.commitChanges()
        
        #check if script has been cancelled before next stage
        if feedback.isCanceled():
            return{}      
        
        #output current stage of script
        feedback.pushInfo('--------------------------------------------------------------------------')
        feedback.pushInfo("3/5 - Calculating Flooded Area...")
        feedback.pushInfo('--------------------------------------------------------------------------')
        
        #clip zones layer to the flooded area
        #NOTE: must turn off invalid features filtering in QGIS - otherwise this process won't work as some geometry is invalid
        result = processing.run('native:clip', { 'INPUT': zonesLayer, 'OUTPUT': 'memory:', 'OVERLAY': floodCleanedLayer }, context=context, feedback=feedback)
        floodedZonesLayer = result["OUTPUT"]
        
        #group by "ZONE_CLASS"
        result = processing.run('native:dissolve', { 'FIELD': ['ZONE_CLASS'], 'INPUT': floodedZonesLayer, 'OUTPUT': 'memory:' }, context=context, feedback=feedback)
        floodedZonesLayerDissolved = result["OUTPUT"]
        
        #add area for each "ZONE_CLASS"
        result = processing.run('qgis:exportaddgeometrycolumns', { 'CALC_METHOD': 0, 'INPUT': floodedZonesLayerDissolved, 'OUTPUT': 'memory:Flooded Area' }, context=context, feedback=feedback)
        floodedZonesAreaLayer = result["OUTPUT"]
        
        #add the "Flooded Area" layer to the layers panel
        #first add the layer without showing it
        QgsProject.instance().addMapLayer(floodedZonesAreaLayer, False)
        #obtain the layer tree of the top-level group in the project
        layerTree = iface.layerTreeCanvasBridge().rootGroup()
        #insert the layer - the position is a number starting from 0, with -1 an alias for the end
        layerTree.insertChildNode(-1, QgsLayerTreeLayer(floodedZonesAreaLayer))
        
        #customise the symbology for the "Flooded Area" layer
        layer = QgsProject.instance().mapLayersByName("Flooded Area")[0]
        single_symbol_renderer = layer.renderer()
        symbol = single_symbol_renderer.symbol()
        symbol.setColor(QColor.fromRgb(150, 206, 250))
        symbol.symbolLayer(0).setStrokeColor(QColor.fromRgb(70, 130, 180))
        symbol.setOpacity(0.3)
        layer.triggerRepaint()
        qgis.utils.iface.layerTreeView().refreshLayerSymbology(layer.id())
        
        #check if script has been cancelled before next stage
        if feedback.isCanceled():
            return{}
        
        #output current stage of script
        feedback.pushInfo('--------------------------------------------------------------------------')
        feedback.pushInfo("4/5 - Calculating Flood-Affected Private Parcels...")
        feedback.pushInfo('--------------------------------------------------------------------------')
        
        #get all flood-affected parcels
        result = processing.run('native:extractbylocation', { 'INPUT': parcelsLayer, 'INTERSECT': floodCleanedLayer, 'METHOD': 0, 'OUTPUT': 'memory:', 'PREDICATE': [0] }, context=context, feedback=feedback)
        floodAffectedParcelsOriginal = result["OUTPUT"]
        
        #create inside buffer on zones layer (to avoid zones touching other parcels when intersecting)
        result = processing.run('native:buffer', { 'DISSOLVE': False, 'DISTANCE': -1, 'END_CAP_STYLE': 0, 'INPUT': zonesLayer, 'JOIN_STYLE': 0, 'MITER_LIMIT': 2, 'OUTPUT' : 'memory:', 'SEGMENTS': 200 }, context=context, feedback=feedback)
        zonesLayerClean = result["OUTPUT"]
        
        #exclude irrelevant zones (public zones, PZ and UFZ)
        request = QgsFeatureRequest().setFilterExpression("\"ZONE_CLASS\" = \'PCRZ\' OR \"ZONE_CLASS\" = \'PPRZ\' OR \"ZONE_CLASS\" = \'PUZ\' OR \"ZONE_CLASS\" = \'RDZ\' OR \"ZONE_CLASS\" = \'CA\' OR \"ZONE_CLASS\" = \'PZ\' OR \"ZONE_CLASS\" = \'UFZ\'")
        ids = [f.id() for f in zonesLayerClean.getFeatures(request)]
        zonesLayerClean.startEditing()
        for fid in ids:
            zonesLayerClean.deleteFeature(fid)
        zonesLayerClean.commitChanges()
        
        #add zone data to parcels
        result = processing.run('qgis:joinattributesbylocation', { 'DISCARD_NONMATCHING': True, 'INPUT': floodAffectedParcelsOriginal, 'JOIN': zonesLayerClean, 'METHOD': 1, 'OUTPUT': 'memory:', 'PREDICATE': [0] }, context=context, feedback=feedback)
        floodAffectedParcelsAll = result["OUTPUT"]
          
        #delete parcels with duplicate geometries - a work-around because 'qgis:deleteduplicategeometries' was causing crashes on return{}
        result = processing.run('qgis:exportaddgeometrycolumns', { 'CALC_METHOD': 0, 'INPUT': floodAffectedParcelsAll, 'OUTPUT': 'memory:' }, context=context, feedback=feedback)
        floodAffectedParcelsArea = result["OUTPUT"]
        result = processing.run('native:removeduplicatesbyattribute', { 'FIELDS': ['area','perimeter'], 'INPUT': floodAffectedParcelsArea, 'OUTPUT': 'memory:' }, context=context, feedback=feedback)
        floodAffectedParcels = result["OUTPUT"]

        #delete irrelevant parcels by attribute
        #"PC_LOTNO" LIKE 'CM%' = driveways, carparking and building surrounds
        #"PC_LOTNO" LIKE 'R%' = park reserves
        #"PC_STAT" = 'P' = proposed parcels
        #"PC_CRSTAT" = 'C' = crown parcels
        #"PC_CRSTAT" = 'G' = road reserves
        #"PC_SPIC" = '200' = shared driveways
        request = QgsFeatureRequest().setFilterExpression("\"PC_LOTNO\" LIKE \'CM%\' OR \"PC_LOTNO\" LIKE \'R%\' OR \"PC_STAT\" = \'P\' OR \"PC_CRSTAT\" = \'C\' OR \"PC_CRSTAT\" = \'G\' OR \"PC_SPIC\" = \'200\'")
        ids = [f.id() for f in floodAffectedParcels.getFeatures(request)]
        floodAffectedParcels.startEditing()
        for fid in ids:
            floodAffectedParcels.deleteFeature(fid)
        floodAffectedParcels.commitChanges()
        
        #remove address points without a specified address number
        result = processing.run('native:extractbyexpression', {'EXPRESSION': '(\"BUNIT_ID1\" != \'0\' OR \"BUNIT_ID2\" != \'0\' OR \"FLOOR_NO_1\" != \'0\' OR \"FLOOR_NO_2\" != \'0\' OR \"HSE_NUM1\" != \'0\' OR \"HSE_NUM2\" != \'0\' OR \"DISP_NUM1\" != \'0\' OR \"DISP_NUM2\" != \'0\')', 'INPUT': addressLayer, 'OUTPUT': 'memory:' }, context=context, feedback=feedback)
        addressLayerClean = result["OUTPUT"]

        #get parcels with a valid address point
        result = processing.run('native:extractbylocation', { 'INPUT': floodAffectedParcels, 'INTERSECT': addressLayerClean, 'METHOD': 0, 'OUTPUT': 'memory:', 'PREDICATE': [0] }, context=context, feedback=feedback)
        floodAffectedPrivateParcels = result["OUTPUT"]
        
        #remove parcels under 40sqm (indicates it is not a regular private land parcel)
        result = processing.run('native:extractbyexpression', { 'EXPRESSION': '(\"area\" > \'40\')', 'INPUT': floodAffectedPrivateParcels, 'OUTPUT': 'memory:' }, context=context, feedback=feedback)
        finalParcelsAreaClean = result["OUTPUT"]
        
        #delete parcels with inner rings (indicates it is not a regular private land parcel)
        #add "POLY_RING" field to parcels layer
        finalParcelsAreaClean.startEditing()
        finalParcelsAreaClean.dataProvider().addAttributes([QgsField("POLY_RING", QVariant.Double)])
        finalParcelsAreaClean.updateFields()
        finalParcelsAreaClean.commitChanges()
        
        #add "POLY_RING" attribute for each feature (count rings for each polygon within feature geometry)
        pFeatures = finalParcelsAreaClean.getFeatures()
        finalParcelsAreaClean.startEditing()
        for feature in pFeatures:
            geometry = feature.geometry()
            polyCount = 0
            ringCount = 0
            if geometry.isMultipart():
                polygons = geometry.asMultiPolygon()
            else:
                polygons = geometry.asPolygon()
            for polygon in polygons:
                polyCount = polyCount + 1
                for ring in polygon:
                    ringCount = ringCount + 1
                count = (ringCount/polyCount)
                feature["POLY_RING"] = count
                finalParcelsAreaClean.updateFeature(feature) 
        finalParcelsAreaClean.commitChanges()
        
        #delete features with more rings than polygons (indicates it has an inner ring)
        request = QgsFeatureRequest().setFilterExpression("\"POLY_RING\" > \'1\'")
        ids = [f.id() for f in finalParcelsAreaClean.getFeatures(request)]
        finalParcelsAreaClean.startEditing()
        for fid in ids:
            finalParcelsAreaClean.deleteFeature(fid)
        finalParcelsAreaClean.commitChanges()
        
        #get relevant flood control overlays
        result = processing.run('native:extractbyexpression', { 'EXPRESSION': '(\"ZONE_CODE\" = \'SBO\' OR \"ZONE_CODE\" = \'LSIO\' OR \"ZONE_CODE\" = \'FO\')', 'INPUT': overlaysLayer, 'OUTPUT': 'memory:' }, context=context, feedback=feedback)
        relevantOverlays = result["OUTPUT"]
        
        #assign parcels with relevant flood control overlays (if intersection)
        result = processing.run('qgis:joinattributesbylocation', { 'DISCARD_NONMATCHING': False, 'INPUT': finalParcelsAreaClean, 'JOIN': relevantOverlays, 'METHOD': 1, 'OUTPUT': 'memory:Flood-Affected Private Parcels', 'PREDICATE': [0] }, context=context, feedback=feedback)
        finalParcels = result["OUTPUT"]
        
        #add the "Flood-Affected Private Parcels" layer to the layers panel
        #first add the layer without showing it
        QgsProject.instance().addMapLayer(finalParcels, False)
        #obtain the layer tree of the top-level group in the project
        layerTree = iface.layerTreeCanvasBridge().rootGroup()
        #insert the layer - the position is a number starting from 0, with -1 an alias for the end
        layerTree.insertChildNode(-1, QgsLayerTreeLayer(finalParcels))
        
        #customise the symbology for the "Flood-Affected Private Parcels" layer
        layer = QgsProject.instance().mapLayersByName("Flood-Affected Private Parcels")[0]
        single_symbol_renderer = layer.renderer()
        symbol = single_symbol_renderer.symbol()
        symbol.setColor(QColor.fromRgb(225, 225, 225))
        symbol.symbolLayer(0).setStrokeColor(QColor.fromRgb(115, 115, 115))
        layer.triggerRepaint()
        qgis.utils.iface.layerTreeView().refreshLayerSymbology(layer.id())
        
        #check if script has been cancelled before next stage
        if feedback.isCanceled():
            return{}
        
        #output current stage of script
        feedback.pushInfo('--------------------------------------------------------------------------')
        feedback.pushInfo("5/5 - Calculating statistics...")
        feedback.pushInfo('--------------------------------------------------------------------------')
        feedback.pushInfo("FLOODED AREA (sqm):")
        feedback.pushInfo('--------------------------------------------------------------------------')

        #get lga area
        lgaFeatures = lgaAreaLayer.getFeatures()
        for feature in lgaFeatures:
            lgaArea = feature["area"]
        
        #get flooded lga area
        lgaFloodArea = 0
        zoneFeatures = floodedZonesAreaLayer.getFeatures()
        for feature in zoneFeatures:
            zoneArea = feature["area"]
            lgaFloodArea = lgaFloodArea + zoneArea
        
        #get flooded area percentage
        floodPercent = ((lgaFloodArea/lgaArea)*100)
        
        #ouput flooded lga area and percentage
        feedback.pushInfo('Flooded Area: ' + (str(round(lgaFloodArea, 2))))
        feedback.pushInfo((str(round(floodPercent, 2))) + '% of LGA Area')
        feedback.pushInfo('BY ZONE:')
        feedback.pushInfo('---------------------------------')
        
        #get and output flooded area for each "ZONE_CLASS"
        request = QgsFeatureRequest()
        clause = QgsFeatureRequest.OrderByClause('area', ascending=False)
        orderby = QgsFeatureRequest.OrderBy([clause])
        request.setOrderBy(orderby)
        zoneFeatures = floodedZonesAreaLayer.getFeatures(request)
        for feature in zoneFeatures:
            zoneName = feature["ZONE_CLASS"]
            zoneArea = feature["area"]
            feedback.pushInfo((zoneName) + ': ' + (str(round(zoneArea, 2))))
            lgaFloodArea = lgaFloodArea + zoneArea
        
        #output current stage of script
        feedback.pushInfo('--------------------------------------------------------------------------')
        feedback.pushInfo("FLOOD-AFFECTED PRIVATE PARCELS:")
        feedback.pushInfo('--------------------------------------------------------------------------')
        
        #get counts for ALL flood-affected private parcels and their flood overlays
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"area\" > \'0\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        allCount = finalParcels.selectedFeatureCount()
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CODE_2\" = \'SBO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        allCountSBO = finalParcels.selectedFeatureCount()
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CODE_2\" = \'LSIO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        allCountLSIO = finalParcels.selectedFeatureCount()
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CODE_2\" = \'FO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        allCountFO = finalParcels.selectedFeatureCount()
        
        #output total parcel counts and by flood overlay
        feedback.pushInfo('Flood-Affected Private Parcels: ' + (str(allCount)))
        feedback.pushInfo('With SBO Overlay: ' + (str(allCountSBO)))
        feedback.pushInfo('With LSIO Overlay: ' + (str(allCountLSIO)))
        feedback.pushInfo('With FO Overlay: ' + (str(allCountFO)))
        feedback.pushInfo('BY ZONE:')
        feedback.pushInfo('---------------------------------')
        
        #get counts for GRZ flood-affected private parcels and their flood overlays (if applicable)
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'GRZ\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        grzCount = finalParcels.selectedFeatureCount()
        if grzCount > 0:
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'GRZ\' AND \"ZONE_CODE_2\" = \'SBO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            grzCountSBO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'GRZ\' AND \"ZONE_CODE_2\" = \'LSIO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            grzCountLSIO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'GRZ\' AND \"ZONE_CODE_2\" = \'FO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            grzCountFO = finalParcels.selectedFeatureCount()
        
            #output total parcel counts and by flood overlay
            feedback.pushInfo('Total General Residential (GRZ): ' + (str(grzCount)))
            feedback.pushInfo('With SBO Overlay: ' + (str(grzCountSBO)))
            feedback.pushInfo('With LSIO Overlay: ' + (str(grzCountLSIO)))
            feedback.pushInfo('With FO Overlay: ' + (str(grzCountFO)))
            feedback.pushInfo('---------------------------------')
        
        #get counts for RZ flood-affected private parcels and thier flood overlays (if applicable)
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'RZ\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        rzCount = finalParcels.selectedFeatureCount()
        if rzCount > 0:
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'RZ\' AND \"ZONE_CODE_2\" = \'SBO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            rzCountSBO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'RZ\' AND \"ZONE_CODE_2\" = \'LSIO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            rzCountLSIO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'RZ\' AND \"ZONE_CODE_2\" = \'FO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            rzCountFO = finalParcels.selectedFeatureCount()
            
            #output total parcel counts and by flood overlay
            feedback.pushInfo('Total Residential (RZ): ' + (str(rzCount)))
            feedback.pushInfo('With SBO Overlay: ' + (str(rzCountSBO)))
            feedback.pushInfo('With LSIO Overlay: ' + (str(rzCountLSIO)))
            feedback.pushInfo('With FO Overlay: ' + (str(rzCountFO)))
            feedback.pushInfo('---------------------------------')
        
        #get counts for RGZ flood-affected private parcels and their flood overlays (if applicable)
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'RGZ\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        rgzCount = finalParcels.selectedFeatureCount()
        if rgzCount > 0:
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'RGZ\' AND \"ZONE_CODE_2\" = \'SBO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            rgzCountSBO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'RGZ\' AND \"ZONE_CODE_2\" = \'LSIO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            rgzCountLSIO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'RGZ\' AND \"ZONE_CODE_2\" = \'FO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            rgzCountFO = finalParcels.selectedFeatureCount()
            
            #output total parcel counts and by flood overlay
            feedback.pushInfo('Total Residential Growth (RGZ): ' + (str(rgzCount)))
            feedback.pushInfo('With SBO Overlay: ' + (str(rgzCountSBO)))
            feedback.pushInfo('With LSIO Overlay: ' + (str(rgzCountLSIO)))
            feedback.pushInfo('With FO Overlay: ' + (str(rgzCountFO)))
            feedback.pushInfo('---------------------------------')
        
        #get counts for MUZ flood-affected private parcels and their flood overlays (if applicable)
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'MUZ\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        muzCount = finalParcels.selectedFeatureCount()
        if muzCount > 0:
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'MUZ\' AND \"ZONE_CODE_2\" = \'SBO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            muzCountSBO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'MUZ\' AND \"ZONE_CODE_2\" = \'LSIO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            muzCountLSIO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'MUZ\' AND \"ZONE_CODE_2\" = \'FO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            muzCountFO = finalParcels.selectedFeatureCount()
            
            #output total parcel counts and by flood overlay
            feedback.pushInfo('Total Mixed Use (MUZ): ' + (str(muzCount)))
            feedback.pushInfo('With SBO Overlay: ' + (str(muzCountSBO)))
            feedback.pushInfo('With LSIO Overlay: ' + (str(muzCountLSIO)))
            feedback.pushInfo('With FO Overlay: ' + (str(muzCountFO)))
            feedback.pushInfo('---------------------------------')
        
        #get counts for CZ flood-affected private parcels and their flood overlays (if applicable)
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'CZ\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        czCount = finalParcels.selectedFeatureCount()
        if czCount > 0:
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'CZ\' AND \"ZONE_CODE_2\" = \'SBO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            czCountSBO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'CZ\' AND \"ZONE_CODE_2\" = \'LSIO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            czCountLSIO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'CZ\' AND \"ZONE_CODE_2\" = \'FO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            czCountFO = finalParcels.selectedFeatureCount()
            
            #output total parcel counts and by flood overlay
            feedback.pushInfo('Total Commercial (CZ): ' + (str(czCount)))
            feedback.pushInfo('With SBO Overlay: ' + (str(czCountSBO)))
            feedback.pushInfo('With LSIO Overlay: ' + (str(czCountLSIO)))
            feedback.pushInfo('With FO Overlay: ' + (str(czCountFO)))
            feedback.pushInfo('---------------------------------')
        
        #get counts for BZ flood-affected private parcels and their flood overlays (if applicable)
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'BZ\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        bzCount = finalParcels.selectedFeatureCount()
        if bzCount > 0:
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'BZ\' AND \"ZONE_CODE_2\" = \'SBO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            bzCountSBO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'BZ\' AND \"ZONE_CODE_2\" = \'LSIO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            bzCountLSIO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'BZ\' AND \"ZONE_CODE_2\" = \'FO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            bzCountFO = finalParcels.selectedFeatureCount()
            
            #output total parcel counts and by flood overlay
            feedback.pushInfo('Total Commercial (BZ): ' + (str(bzCount)))
            feedback.pushInfo('With SBO Overlay: ' + (str(bzCountSBO)))
            feedback.pushInfo('With LSIO Overlay: ' + (str(bzCountLSIO)))
            feedback.pushInfo('With FO Overlay: ' + (str(bzCountFO)))
            feedback.pushInfo('---------------------------------')
        
        #get counts for INZ flood-affected private parcels and their flood overlays (if applicable)
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'INZ\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        inzCount = finalParcels.selectedFeatureCount()
        if inzCount > 0:
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'INZ\' AND \"ZONE_CODE_2\" = \'SBO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            inzCountSBO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'INZ\' AND \"ZONE_CODE_2\" = \'LSIO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            inzCountLSIO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'INZ\' AND \"ZONE_CODE_2\" = \'FO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            inzCountFO = finalParcels.selectedFeatureCount()
            
            #output total parcel counts and by flood overlay
            feedback.pushInfo('Total Industrial (INZ): ' + (str(inzCount)))
            feedback.pushInfo('With SBO Overlay: ' + (str(inzCountSBO)))
            feedback.pushInfo('With LSIO Overlay: ' + (str(inzCountLSIO)))
            feedback.pushInfo('With FO Overlay: ' + (str(inzCountFO)))
            feedback.pushInfo('---------------------------------')
        
        #get counts for SUZ flood-affected private parcels and their flood overlays (if applicable)
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'SUZ\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        suzCount = finalParcels.selectedFeatureCount()
        if suzCount > 0:
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'SUZ\' AND \"ZONE_CODE_2\" = \'SBO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            suzCountSBO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'SUZ\' AND \"ZONE_CODE_2\" = \'LSIO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            suzCountLSIO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'SUZ\' AND \"ZONE_CODE_2\" = \'FO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            suzCountFO = finalParcels.selectedFeatureCount()
            
            #output total parcel counts and by flood overlay
            feedback.pushInfo('Total Special Use (SUZ): ' + (str(suzCount)))
            feedback.pushInfo('With SBO Overlay: ' + (str(suzCountSBO)))
            feedback.pushInfo('With LSIO Overlay: ' + (str(suzCountLSIO)))
            feedback.pushInfo('With FO Overlay: ' + (str(suzCountFO)))
            feedback.pushInfo('---------------------------------')
        
        #get counts for CDZ flood-affected private parcels and their flood overlays (if applicable)
        processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'CDZ\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
        cdzCount = finalParcels.selectedFeatureCount()
        if cdzCount > 0:
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'CDZ\' AND \"ZONE_CODE_2\" = \'SBO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            cdzCountSBO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'CDZ\' AND \"ZONE_CODE_2\" = \'LSIO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            cdzCountLSIO = finalParcels.selectedFeatureCount()
            processing.run('qgis:selectbyexpression', { 'EXPRESSION': '(\"ZONE_CLASS\" = \'CDZ\' AND \"ZONE_CODE_2\" = \'FO\')', 'INPUT': finalParcels, 'METHOD': 0 }, context=context)
            cdzCountFO = finalParcels.selectedFeatureCount()
            
            #output total parcel counts and by flood overlay
            feedback.pushInfo('Total Comprehensive Development (CDZ): ' + (str(cdzCount)))
            feedback.pushInfo('With SBO Overlay: ' + (str(cdzCountSBO)))
            feedback.pushInfo('With LSIO Overlay: ' + (str(cdzCountLSIO)))
            feedback.pushInfo('With FO Overlay: ' + (str(cdzCountFO)))
            feedback.pushInfo('---------------------------------')
        
        #end the script
        return{}