TD2 2.2.1

from statistics import mean
from qgis.core import (
    QgsProcessingAlgorithm,
    QgsProcessingParameterFeatureSource,
    QgsProcessingParameterNumber,
    QgsProcessingParameterFeatureSink,
    QgsVectorLayer,
    QgsFeatureSink,
    QgsCoordinateReferenceSystem,
    QgsField,
    QgsRectangle,
    QgsFeatureRequest,
    QgsFeature,
)
import processing
from PyQt5.QtCore import QVariant

from typing import List, Tuple


def spatial_query(layer: QgsVectorLayer, cell: QgsRectangle) -> List[QgsFeature]:
    """Query features from layer that intersects cell.

    Args:
        layer (QgsVectorLayer): Layer that contains wanted features.
        cell (QgsRectangle): Rectangle to query features.

    Returns:
        List[QgsFeature]: List of features from layer that intersects cell.
    """
    query = QgsFeatureRequest().setFilterRect(cell)
    return list(layer.getFeatures(query))


def diversity(
    features: List[QgsFeature], field: str, cell: QgsRectangle
) -> Tuple[float, float]:
    """Compute richness and inverse simpson index from crop features.

    Args:
        features (List[QgsFeature]): Crop features.
        field (str): Field to extract crop type.
        cell (QgsRectangle): Area extent to compute diversity metrics.

    Returns:
        Tuple[float, float]: richness & inverse simpson.
    """
    crops = set([f[field] for f in features])
    richness = len(set([f[field] for f in features]))
    total_crop_area = sum([f.geometry().intersection(cell).area() for f in features])
    crops_area = dict(zip(crops, [0] * len(crops)))
    for feat in features:
        crops_area[feat[field]] += feat.geometry().intersection(cell).area()

    inv_simpson = 1 / sum(
        [(area / total_crop_area) ** 2 for area in list(crops_area.values())]
    )
    return richness, inv_simpson


def mean_field_size(features: List[QgsFeature], cell: QgsRectangle) -> float:
    """Compute mean crop plots area.

    Args:
        features (List[QgsFeature]): List of crop features.
        cell (QgsRectangle):  Area extent to compute field size.

    Returns:
        float: Mean field size
    """
    return mean([f.geometry().intersection(cell).area() for f in features])


def semi_natural_cover(
    cell: QgsRectangle,
    crop_features: List[QgsFeature],
    artificial_features: List[QgsFeature],
) -> float:
    """Compute semi natural cover from crops & artificial features.

    Args:
        cell (QgsRectangle): Area extent semi natural cover.
        crop_features (List[QgsFeature]): Crop features
        artificial_features (List[QgsFeature]): Artificial features.

    Returns:
        float: Semi natural cover.
    """
    cell_area = cell.area()
    crops_area = sum([f.geometry().intersection(cell).area() for f in crop_features])
    artificial_area = sum(
        [f.geometry().intersection(cell).area() for f in artificial_features]
    )

    return (cell_area - crops_area - artificial_area) / cell_area

def spatial_query(layer: QgsVectorLayer, cell: QgsRectangle) -> List[QgsFeature]:
    """Query features from layer that intersects cell.

    Args:
        layer (QgsVectorLayer): Layer that contains wanted features.
        cell (QgsRectangle): Rectangle to query features.

    Returns:
        List[QgsFeature]: List of features from layer that intersects cell.
    """
    query = QgsFeatureRequest().setFilterRect(cell)
    return list(layer.getFeatures(query))

class LandscapeBiodiversityMetrics(QgsProcessingAlgorithm):

    RPG = "RPG"
    ARTIFICIAL = "ARTIFICIAL"
    METRICS = "METRICS"

    def initAlgorithm(self, config=None):
        self.addParameter(QgsProcessingParameterFeatureSource(self.RPG, self.RPG))
        self.addParameter(
            QgsProcessingParameterFeatureSource(self.ARTIFICIAL, self.ARTIFICIAL)
        )
        self.addParameter(QgsProcessingParameterFeatureSink(self.METRICS, self.METRICS))

    def processAlgorithm(self, parameters, context, feedback):
        rpg: QgsVectorLayer = self.parameterAsVectorLayer(parameters, self.RPG, context)
        artificial = self.parameterAsVectorLayer(parameters, self.ARTIFICIAL, context)

        grid: QgsVectorLayer = processing.run(
            "native:creategrid",
            {
                "TYPE": 2,
                "EXTENT": rpg,
                "HSPACING": 1000,
                "VSPACING": 1000,
                "HOVERLAY": 0,
                "VOVERLAY": 0,
                "CRS": QgsCoordinateReferenceSystem("IGNF:LAMB93"),
                "OUTPUT": "TEMPORARY_OUTPUT",
            },feedback=feedback, context=context
        )["OUTPUT"]

        new_fields = [
            QgsField("RICHNESS", QVariant.Double),
            QgsField("DIVERSITY", QVariant.Double),
            QgsField("FIELD_SIZE", QVariant.Double),
            QgsField("SEMI_NATURAL", QVariant.Double),
        ]

        metrics = QgsVectorLayer(
            f"Polygon?crs={grid.crs().authid()}", "Results", "memory"
        )

        metrics.dataProvider().addAttributes(new_fields)
        metrics.updateFields()
        new_features = []

        for cell_feat in grid.getFeatures():
            cell = cell_feat.geometry()
            crops_features = spatial_query(rpg, cell.boundingBox())
            artificial_features = spatial_query(artificial, cell.boundingBox())

            if len(crops_features) == 0:
                continue

            richness, inv_simpson = diversity(crops_features, "CODE_CULTU", cell)

            feature = QgsFeature(metrics.fields())
            feature.setGeometry(cell)

            feature["RICHNESS"] = richness
            feature["DIVERSITY"] = inv_simpson
            feature["FIELD_SIZE"] = mean_field_size(crops_features, cell)
            feature["SEMI_NATURAL"] = semi_natural_cover(
                cell, crops_features, artificial_features
            )
            new_features.append(feature)

        metrics.dataProvider().addFeatures(new_features)

        (sink, dest_id) = self.parameterAsSink(
            parameters,
            self.METRICS,
            context,
            metrics.fields(),
            metrics.wkbType(),
            metrics.sourceCrs(),
        )

        for f in metrics.getFeatures():
            sink.addFeature(f, QgsFeatureSink.FastInsert)

        return {self.METRICS: dest_id}

    def name(self):
        return "landscapebiodiv"

    def displayName(self):
        return "Landscape Biodiversity"

    def group(self):
        return "Custom processing"

    def groupId(self):
        return "custom_processing"

    def createInstance(self):
        return LandscapeBiodiversityMetrics()