# -*- coding: utf-8 -*-

"""
/***************************************************************************
 QProto
                                 A QGIS plugin
 Source affecting index for QPROTO
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2026-03-15
        copyright            : (C) 2026 by Stefano Campus & Codex
        email                : skampus@gmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

__author__ = 'Stefano Campus'
__date__ = '2026-03-15'
__copyright__ = '(C) 2026 by Stefano Campus'

from collections import defaultdict
from typing import Any, Dict, Optional
import os

from qgis.PyQt.QtCore import QVariant
from qgis.PyQt.QtGui import QIcon
from qgis.core import (
    QgsCoordinateTransform,
    QgsCoordinateTransformContext,
    QgsProcessing,
    QgsProcessingAlgorithm,
    QgsProcessingContext,
    QgsProcessingException,
    QgsProcessingFeedback,
    QgsProcessingParameterBoolean,
    QgsProcessingParameterFeatureSink,
    QgsProcessingParameterField,
    QgsProcessingParameterString,
    QgsProcessingParameterVectorLayer,
    QgsFeature,
    QgsFeatureSink,
    QgsField,
    QgsFields,
    QgsProcessingUtils,
    QgsProject,
    QgsSpatialIndex,
)

pluginPath = os.path.dirname(__file__)


class SourceAffectingIndexAlgorithm(QgsProcessingAlgorithm):
    CRITICAL_AREAS = 'critical_areas'
    CRITICALITY_FIELD = 'criticality_field'
    FINALPOINTS = 'finalpoints'
    SUMMARY = 'summary'
    SOURCES = 'sources'
    SOURCE_ID_FIELD = 'source_id_field'
    DETACHMENT_FIELD = 'detachment_field'
    SELECTED_SOURCES_ONLY = 'selected_sources_only'
    OUTPUT_NAME = 'output_name'
    OUTPUT = 'output'
    FINALPOINTS_SOURCE_ID_FIELD = 'ID_IN'

    def __init__(self):
        super().__init__()
        self._target_group_name = None
        self._output_dest_id = None
        self._output_layer_name = None

    def initAlgorithm(self, config: Optional[Dict[str, Any]] = None):
        self.addParameter(QgsProcessingParameterVectorLayer(self.CRITICAL_AREAS, 'Critical areas', [QgsProcessing.TypeVectorPolygon]))
        self.addParameter(
            QgsProcessingParameterField(
                self.CRITICALITY_FIELD,
                'Criticality field in critical areas',
                parentLayerParameterName=self.CRITICAL_AREAS,
                optional=True
            )
        )
        self.addParameter(QgsProcessingParameterVectorLayer(self.FINALPOINTS, 'Final points', [QgsProcessing.TypeVectorPoint]))
        self.addParameter(QgsProcessingParameterVectorLayer(self.SUMMARY, 'Results summary layer', [QgsProcessing.TypeVectorPoint]))
        self.addParameter(QgsProcessingParameterVectorLayer(self.SOURCES, 'Source points', [QgsProcessing.TypeVectorPoint]))
        self.addParameter(
            QgsProcessingParameterBoolean(
                self.SELECTED_SOURCES_ONLY,
                'Use only selected source points',
                defaultValue=False
            )
        )
        self.addParameter(
            QgsProcessingParameterField(
                self.SOURCE_ID_FIELD,
                'Source point ID field',
                defaultValue='ID',
                parentLayerParameterName=self.SOURCES
            )
        )
        self.addParameter(
            QgsProcessingParameterField(
                self.DETACHMENT_FIELD,
                'Detachment propensity field',
                defaultValue='WEIGHT',
                parentLayerParameterName=self.SOURCES
            )
        )
        self.addParameter(
            QgsProcessingParameterString(
                self.OUTPUT_NAME,
                'Output layer name',
                defaultValue='',
                optional=True
            )
        )
        self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, 'Source affecting critical areas', QgsProcessing.TypeVectorAnyGeometry))

    def _coerce_detachment_value(self, value: Any) -> float:
        if value is None:
            return 1.0
        if hasattr(value, 'isNull') and value.isNull():
            return 1.0
        try:
            return float(value)
        except (TypeError, ValueError):
            return 1.0

    def _is_missing_or_invalid_numeric(self, value: Any) -> bool:
        if value is None:
            return True
        if hasattr(value, 'isNull') and value.isNull():
            return True
        try:
            float(value)
            return False
        except (TypeError, ValueError):
            return True

    def _geometry_key(self, geometry) -> bytes:
        return bytes(geometry.asWkb())

    def _sanitize_output_name(self, output_name: str) -> str:
        if not output_name:
            return ''
        invalid_chars = '<>:"/\\|?*'
        return ''.join('_' if char in invalid_chars else char for char in output_name).strip().strip('.')

    def processAlgorithm(
        self,
        parameters: Dict[str, Any],
        context: QgsProcessingContext,
        model_feedback: QgsProcessingFeedback,
    ) -> Dict[str, Any]:
        critical_areas = self.parameterAsVectorLayer(parameters, self.CRITICAL_AREAS, context)
        criticality_field = self.parameterAsString(parameters, self.CRITICALITY_FIELD, context)
        finalpoints = self.parameterAsVectorLayer(parameters, self.FINALPOINTS, context)
        summary = self.parameterAsVectorLayer(parameters, self.SUMMARY, context)
        sources = self.parameterAsVectorLayer(parameters, self.SOURCES, context)
        source_id_field = self.parameterAsString(parameters, self.SOURCE_ID_FIELD, context)
        detachment_field = self.parameterAsString(parameters, self.DETACHMENT_FIELD, context)
        selected_sources_only = self.parameterAsBool(parameters, self.SELECTED_SOURCES_ONLY, context)
        output_name = self._sanitize_output_name(self.parameterAsString(parameters, self.OUTPUT_NAME, context))
        if not critical_areas or not critical_areas.isValid():
            raise QgsProcessingException('Critical areas layer is not valid')
        if not finalpoints or not finalpoints.isValid():
            raise QgsProcessingException('Final points layer is not valid')
        if not summary or not summary.isValid():
            raise QgsProcessingException('Results summary layer is not valid')
        if not sources or not sources.isValid():
            raise QgsProcessingException('Source points layer is not valid')
        if not source_id_field:
            raise QgsProcessingException('Source ID field is required')
        if not detachment_field:
            raise QgsProcessingException('Detachment propensity field is required')
        source_field_names = {f.name() for f in sources.fields()}
        critical_area_field_names = {f.name() for f in critical_areas.fields()}
        finalpoints_field_names = {f.name() for f in finalpoints.fields()}
        if criticality_field and criticality_field not in critical_area_field_names:
            raise QgsProcessingException('Criticality field not found in critical areas: {}'.format(criticality_field))
        if source_id_field not in source_field_names:
            raise QgsProcessingException('Source ID field not found in source points: {}'.format(source_id_field))
        if detachment_field not in source_field_names:
            raise QgsProcessingException('Detachment propensity field not found in source points: {}'.format(detachment_field))
        if self.FINALPOINTS_SOURCE_ID_FIELD in finalpoints_field_names:
            finalpoints_source_id_field = self.FINALPOINTS_SOURCE_ID_FIELD
        elif source_id_field in finalpoints_field_names:
            finalpoints_source_id_field = source_id_field
        else:
            raise QgsProcessingException(
                'Final points layer must contain "{}" or "{}" to link source points.'.format(
                    self.FINALPOINTS_SOURCE_ID_FIELD,
                    source_id_field
                )
            )

        feedback = model_feedback
        generated_field_names = {'Tot_DFP', 'Tot_FP_Crop', 'Tot_DP_Crop', 'GAI', 'SAI', 'TAI'}
        conflicting_fields = generated_field_names.intersection(source_field_names)
        if conflicting_fields:
            raise QgsProcessingException(
                'Output field name conflict in source points layer: {}'.format(', '.join(sorted(conflicting_fields)))
            )

        self._target_group_name = None
        finalpoints_node = QgsProject.instance().layerTreeRoot().findLayer(finalpoints.id())
        if finalpoints_node and finalpoints_node.parent() and finalpoints_node.parent() != QgsProject.instance().layerTreeRoot():
            self._target_group_name = finalpoints_node.parent().name()

        selected_source_ids = None
        source_features = []
        if selected_sources_only:
            if sources.selectedFeatureCount() <= 0:
                raise QgsProcessingException('Select at least one source point or disable "Use only selected source points".')
            source_features = list(sources.getSelectedFeatures())
            selected_source_ids = {feature[source_id_field] for feature in source_features if feature[source_id_field] is not None}
        else:
            source_features = list(sources.getFeatures())

        critical_index = QgsSpatialIndex()
        critical_geometries = {}
        invalid_criticality_values = 0
        transform_to_finalpoints = None
        if critical_areas.sourceCrs() != finalpoints.sourceCrs():
            transform_to_finalpoints = QgsCoordinateTransform(
                critical_areas.sourceCrs(),
                finalpoints.sourceCrs(),
                context.transformContext() if context else QgsCoordinateTransformContext(),
            )
        transform_summary_to_finalpoints = None
        if summary.sourceCrs() != finalpoints.sourceCrs():
            transform_summary_to_finalpoints = QgsCoordinateTransform(
                summary.sourceCrs(),
                finalpoints.sourceCrs(),
                context.transformContext() if context else QgsCoordinateTransformContext(),
            )
        for area_feature in critical_areas.getFeatures():
            if feedback.isCanceled():
                return {}
            area_geometry = area_feature.geometry()
            if not area_geometry or area_geometry.isEmpty():
                continue
            if transform_to_finalpoints is not None:
                area_geometry = area_geometry.clone()
                area_geometry.transform(transform_to_finalpoints)
            criticality_value = 1.0
            if criticality_field:
                criticality_raw = area_feature[criticality_field]
                if self._is_missing_or_invalid_numeric(criticality_raw):
                    invalid_criticality_values += 1
                criticality_value = self._coerce_detachment_value(criticality_raw)
            critical_geometries[area_feature.id()] = {
                'geometry': area_geometry,
                'criticality': criticality_value,
            }
            area_feature_for_index = QgsFeature(area_feature)
            area_feature_for_index.setGeometry(area_geometry)
            critical_index.addFeature(area_feature_for_index)

        total_distinct_points_by_source = defaultdict(set)
        total_points_in_critical_areas_by_source = defaultdict(int)
        max_criticality_by_source = defaultdict(lambda: 1.0)
        total_distinct_summary_points_in_critical_areas = set()
        null_finalpoint_source_ids = 0
        null_source_ids = 0
        invalid_detachment_values = 0

        finalpoints_count = max(finalpoints.featureCount(), 1)
        for current, final_feature in enumerate(finalpoints.getFeatures(), start=1):
            if feedback.isCanceled():
                return {}

            source_id = final_feature[finalpoints_source_id_field]
            if source_id is None:
                null_finalpoint_source_ids += 1
                feedback.setProgress((current / float(finalpoints_count)) * 70)
                continue
            if selected_source_ids is not None and source_id not in selected_source_ids:
                feedback.setProgress((current / float(finalpoints_count)) * 70)
                continue

            geometry = final_feature.geometry()
            if not geometry or geometry.isEmpty():
                continue

            point_key = self._geometry_key(geometry)
            total_distinct_points_by_source[source_id].add(point_key)

            if feedback.isCanceled():
                return {}

            candidate_area_ids = critical_index.intersects(geometry.boundingBox())
            if not candidate_area_ids:
                feedback.setProgress((current / float(finalpoints_count)) * 70)
                continue

            intersects_critical_area = False
            source_max_criticality = max_criticality_by_source[source_id]
            for area_id in candidate_area_ids:
                area_entry = critical_geometries.get(area_id)
                area_geometry = area_entry['geometry'] if area_entry else None
                if area_geometry and area_geometry.intersects(geometry):
                    intersects_critical_area = True
                    source_max_criticality = max(source_max_criticality, area_entry['criticality'])

            if intersects_critical_area:
                total_points_in_critical_areas_by_source[source_id] += 1
                max_criticality_by_source[source_id] = source_max_criticality

            feedback.setProgress((current / float(finalpoints_count)) * 70)

        summary_count = max(summary.featureCount(), 1)
        for current, summary_feature in enumerate(summary.getFeatures(), start=1):
            if feedback.isCanceled():
                return {}

            geometry = summary_feature.geometry()
            if not geometry or geometry.isEmpty():
                continue
            if transform_summary_to_finalpoints is not None:
                geometry = geometry.clone()
                geometry.transform(transform_summary_to_finalpoints)

            candidate_area_ids = critical_index.intersects(geometry.boundingBox())
            if not candidate_area_ids:
                feedback.setProgress(70 + (current / float(summary_count)) * 10)
                continue

            for area_id in candidate_area_ids:
                area_entry = critical_geometries.get(area_id)
                area_geometry = area_entry['geometry'] if area_entry else None
                if area_geometry and area_geometry.intersects(geometry):
                    total_distinct_summary_points_in_critical_areas.add(self._geometry_key(geometry))
                    break

            feedback.setProgress(70 + (current / float(summary_count)) * 10)

        tot_dp_crop = len(total_distinct_summary_points_in_critical_areas)

        output_fields = QgsFields()
        for source_field in sources.fields():
            output_fields.append(source_field)
        generated_fields = [
            QgsField('Tot_DFP', QVariant.Int),
            QgsField('Tot_FP_Crop', QVariant.Int),
            QgsField('Tot_DP_Crop', QVariant.Int),
            QgsField('GAI', QVariant.Double, '', 20, 6),
            QgsField('SAI', QVariant.Double, '', 20, 6),
            QgsField('TAI', QVariant.Double, '', 20, 6),
        ]
        for field in generated_fields:
            output_fields.append(field)

        sink_parameters = dict(parameters)
        output_destination = sink_parameters.get(self.OUTPUT)
        if output_name:
            if output_destination == QgsProcessing.TEMPORARY_OUTPUT:
                temp_path = os.path.join(QgsProcessingUtils.tempFolder(), '{}.gpkg'.format(output_name))
                sink_parameters[self.OUTPUT] = '{}|layername={}'.format(temp_path, output_name)
            elif isinstance(output_destination, str) and output_destination.lower().endswith('.gpkg') and '|layername=' not in output_destination.lower():
                sink_parameters[self.OUTPUT] = '{}|layername={}'.format(output_destination, output_name)

        sink, dest_id = self.parameterAsSink(
            sink_parameters,
            self.OUTPUT,
            context,
            output_fields,
            sources.wkbType(),
            sources.sourceCrs(),
        )
        if sink is None:
            raise QgsProcessingException('Failed to create output layer')

        sources_count = max(len(source_features), 1)
        for current, source_feature in enumerate(source_features, start=1):
            if feedback.isCanceled():
                return {}

            source_id = source_feature[source_id_field]
            if source_id is None:
                null_source_ids += 1
                tot_dfp = 0
                tot_fp_crop = 0
                tot_dp_crop_value = tot_dp_crop
                gai = 0.0
                sai = 0.0
                tai = 0.0
            else:
                detachment_value = source_feature[detachment_field]
                detachment_work = self._coerce_detachment_value(detachment_value)
                if detachment_value is None or (hasattr(detachment_value, 'isNull') and detachment_value.isNull()):
                    invalid_detachment_values += 1

                tot_dfp = len(total_distinct_points_by_source.get(source_id, set()))
                tot_fp_crop = total_points_in_critical_areas_by_source.get(source_id, 0)
                tot_dp_crop_value = tot_dp_crop
                gai = 0.0 if tot_dp_crop_value == 0 else (tot_fp_crop / float(tot_dp_crop_value)) * 100.0
                sai = gai * detachment_work
                tai = sai * max_criticality_by_source.get(source_id, 1.0)

            output_feature = QgsFeature(output_fields)
            output_feature.setGeometry(source_feature.geometry())
            output_feature.setAttributes(
                list(source_feature.attributes()) + [
                    tot_dfp,
                    tot_fp_crop,
                    tot_dp_crop_value,
                    gai,
                    sai,
                    tai,
                ]
            )
            sink.addFeature(output_feature, QgsFeatureSink.FastInsert)
            feedback.setProgress(80 + (current / float(sources_count)) * 20)

        if null_finalpoint_source_ids:
            feedback.pushWarning('Skipped {} final points with null source ID.'.format(null_finalpoint_source_ids))
        if null_source_ids:
            feedback.pushWarning('Some source features have a null source ID and were written with zero metrics.')
        if invalid_detachment_values:
            feedback.pushWarning(
                'Some detachment propensity values are null or invalid and were treated as 1: {}'.format(invalid_detachment_values)
            )
        if invalid_criticality_values:
            feedback.pushWarning(
                'Some criticality values are null or invalid and were treated as 1: {}'.format(invalid_criticality_values)
            )

        self._output_dest_id = dest_id
        self._output_layer_name = output_name if output_name else 'Source affecting critical areas'
        return {self.OUTPUT: dest_id}

    def postProcessAlgorithm(self, context, feedback):
        if not self._output_dest_id:
            return {}

        output_layer = QgsProcessingUtils.mapLayerFromString(str(self._output_dest_id), context)
        if not output_layer or not output_layer.isValid():
            feedback.pushWarning('Source affecting index output could not be resolved for layer-tree placement.')
            return {self.OUTPUT: self._output_dest_id}

        if self._output_layer_name:
            output_layer.setName(self._output_layer_name)

        root = QgsProject.instance().layerTreeRoot()
        target_group = root.findGroup(self._target_group_name) if self._target_group_name else None
        if not target_group:
            return {self.OUTPUT: self._output_dest_id}

        layer_node = root.findLayer(output_layer.id())
        if layer_node:
            parent_node = layer_node.parent()
            if parent_node and parent_node != target_group:
                parent_node.removeChild(layer_node)
                target_group.insertChildNode(0, layer_node)
        else:
            QgsProject.instance().addMapLayer(output_layer, False)
            target_group.addLayer(output_layer)

        return {self.OUTPUT: self._output_dest_id}

    def name(self):
        return 'source_affecting_index'

    def displayName(self):
        return 'Source affecting index'

    def icon(self):
        return QIcon(os.path.join(pluginPath, 'icons', 'QPROTO_source_affecting_index_icon.svg'))

    def group(self):
        return 'QPROTO post-processing'

    def groupId(self):
        return 'qproto_postprocessing'

    def shortHelpString(self):
        return '''
            <p><strong>Source Affecting Index</strong> This QPROTO post-processing modules identifies which source points are most relevant for selected critical areas, based on the <em>Final points</em> produced by the QPROTO main analysis.</p>
            <p>The algorithm selects all the <u>final points</u> that drop within <u>critical areas</u> defined by the user, links them to the corresponding <u>source points</u>, and computes summary indicators for each source point.</p>
            <p style="margin-left: 40px;">&#8226; <u>GAI</u> (<em>Geometric Affecting Index</em>): number of final points from the source that fall within the critical areas compared to the total number of distinct final-point locations from the summary layer that fall within the critical areas (in percentage).</p>
            <p style="margin-left: 40px;">&#8226; <u>SAI</u> (<em>Source Affecting Index</em>): weighted version of GAI, obtained by multiplying the GAI by the detachment propensity associated to the source point.</p>
            <p style="margin-left: 40px;">&#8226; <u>TAI</u> (<em>Target Affecting Index</em>): weighted version of SAI, obtained by multiplying the SAI by the criticality value associated to the affected critical areas.</p>

            <p><strong>Input</strong></p>
            <p><u>Critical areas</u>: Polygon vector layer representing the areas of interest or the exposed targets to be evaluated.</p>
            <p><u>Criticality field in critical areas</u>: Optional numeric field chosen from the <u>Critical areas</u> layer. It is a value between <u>0</u> and <u>1</u> representing the level of attention associated with each feature in the critical-areas dataset. When selected, it is used to compute <u>TAI</u>. For each source point, the algorithm uses the maximum criticality value among the critical-area features intersected by that source's final points. If no field is selected, or if some values are null/invalid, the corresponding criticality is treated as <u>1</u>.</p>
            <p><u>Final points</u>: Point vector layer produced by the QPROTO main analysis module, representing all the simulated rockfall impact points. Each final point must keep the identifier of the source point from which it was generated. In the standard QPROTO workflow, this link is stored in the field <u>ID_IN</u> of <em>finalpoints</em> layer.</p>
            <p><u>Results summary layer</u>: Point vector layer of distinct final-point locations, normally the <em>results_summary</em> output of the QPROTO main analysis. This layer is intersected with the critical areas to compute <u>Tot_DP_Crop</u>, i.e. the total number of distinct final points that fall within the critical areas.</p>
            <p><u>Source points</u>: Point vector layer representing the rockfall source points used in the QPROTO main analysis module. The output of this module is built from this layer: one output feature is produced for each source point considered in the analysis.</p>
            <p>The following attributes are needed for the analysis:</p>
            
            <p style="margin-left: 40px;">&#8226; <u>Source point ID field</u>: Unique identifier field of the source points layer. This field is essential because it is used to link each source point to the corresponding records in the <u>finalpoints</u> layer. The algorithm first looks for the field <u>ID_IN</u> in <u>finalpoints</u>, which is the standard source-link field created by QPROTO. If <u>ID_IN</u> is not available, the algorithm tries to use a field with the same name as the selected source ID field. For correct results, the identifier values must be consistent between <u>source points</u> and <u>final points</u>. If the IDs are missing, duplicated, or inconsistent, the computed indicators may be incomplete or assigned to the wrong source point.</p>
            <p style="margin-left: 40px;">&#8226; <u>Detachment propensity field</u>: Numeric field from the source points layer representing the detachment propensity used to weight the Source Affecting Index.</p>
            <p><u>Use only selected source points</u>: If enabled, the analysis is restricted to the currently selected features in the source points layer. In this case, only final points linked to the selected source-point IDs are considered.</p>
            <p><u>Output layer name</u>: Optional name used for the generated output layer. When the output is written to a GeoPackage without a layer name, the same value is also used as GeoPackage layer name.</p>

            <p><strong>Output</strong></p>
            <p><u>Source affecting critical areas</u>: Point vector layer based on the original source points, enriched with the following computed affecting-index for the selected critical areas:</p>

            
            <p><u>Original source attributes</u>: All original attributes from the source points layer are preserved in the output.</p>
            <p><u>Tot_DFP</u>: Total number of final points associated with the source, considering the whole simulated propagation area. In the standard QPROTO workflow, the same source point cannot generate two overlapping final points in the same destination cell, so this value corresponds to the total number of simulated final points for that source.</p>
            <p><u>Tot_FP_Crop</u>: Total number of final points from the source that fall within the critical areas.</p>
            <p><u>Tot_DP_Crop</u>: Total number of distinct final-point locations from the <em>summary layer</em> that fall within the critical areas. This value is the denominator of GAI.</p>
            <p><u>GAI</u>: Geometric Affecting Index, expressed as a percentage, computed as (<u>Tot_FP_Crop</u> / <u>Tot_DP_Crop</u>) * 100.</p>
            <p><u>SAI</u>: Source Affecting Index, computed as <u>GAI</u> multiplied by the selected <u>detachment propensity</u> value.</p>
            <p><u>TAI</u>: Target Affecting Index, computed as <u>SAI</u> multiplied by the criticality value. If a source affects more than one critical-area feature, the maximum criticality among the intersected features is used.</p>

            <p style="margin:0 0 4px 0;"><strong>Note</strong></p>
            <p style="margin:0 0 4px 0;">In the output file, the various critical area features are not distinguished from one another.</p>
            <p style="margin:0 0 4px 0;">The current behaviour of the Source Affecting Index module is as follows:</p>
            <p style="margin:0 0 2px 40px;">&#8226; it produces a single feature for each source point;</p>
            <p style="margin:0 0 2px 40px;">&#8226; it treats all critical areas as a single logical set;</p>
            <p style="margin:0 0 2px 40px;">&#8226; if a source's final point falls within at least one critical area, it is counted;</p>
            <p style="margin:0 0 2px 40px;">&#8226; if that final point falls within two different polygons, it is not duplicated;</p>
            <p style="margin:0 0 4px 40px;">&#8226; in the final output, there is no attribute indicating which feature(s) it refers to.</p>
            <p style="margin:0 0 4px 0;">Therefore, if a source contributes to two critical area features:</p>
            <p style="margin:0 0 2px 40px;">&#8226; in the final result, you still have only a single row;</p>
            <p style="margin:0 0 0 40px;">&#8226; the values Tot_FP_Crop, Tot_DP_Crop, GAI, and SAI are calculated on the union of all critical areas;</p>
            <p style="margin:0 0 0 40px;">&#8226; the value TAI uses the maximum criticality among the critical-area features intersected by the source.</p>
        '''

    def createInstance(self):
        return SourceAffectingIndexAlgorithm()
