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

"""
/***************************************************************************
 OpenHLZ
                                 A QGIS plugin
 Open-source HLZ Identification Plugin
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2023-08-13
        copyright            : (C) 2023 by John Erskine
        email                : erskine.john.c@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__ = 'John Erskine'
__date__ = '2023-08-13'
__copyright__ = '(C) 2023 by John Erskine'

# This will get replaced with a git SHA1 when you do a git archive

__revision__ = '$Format:%H$'

from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtGui import QIcon

from qgis._core import *
from qgis import processing

import os
import shutil
import numpy
import pandas

from .openhlz_functions import getJson, downloadDem, downloadLULC, mosaicAndClipRasters, generateHlsRaster, identifyHlzs


class IdentifyHLZsFromLatLng(QgsProcessingAlgorithm):
    """
    Class to identify possible HLZs from lat/lng
    """

    # Constants used to refer to parameters and outputs. They will be
    # used when calling the algorithm from another algorithm, or when
    # calling from the QGIS console.

    LAT = "LAT"
    LNG = "LNG"
    SEARCHRADIUS = "SEARCHRADIUS"
    TDPDIAMETER = "TDPDIAMETER"
    SLOPECAUTION = "SLOPECAUTION"
    SLOPELIMIT = "SLOPELIMIT"
    USE10MDATA = "USE10MDATA"

    OUTPUTHLZPOINTS = "OUTPUTHLZPOINTS"
    OUTPUTHLSRASTER = "OUTPUTHLSRASTER"

    def initAlgorithm(self, config):
        """
        Here we define the inputs and output of the algorithm, along
        with some other properties.
        """

        # Save reference to project instance
        self.instance = QgsProject.instance()

        # Add input and output parameters
        # Input LAT
        self.addParameter(
            QgsProcessingParameterNumber(
                self.LAT,
                self.tr('Latitude (DDG)'),
                QgsProcessingParameterNumber.Double,
                minValue=-90.0,
                maxValue=90.0
            )
        )

        # Input LNG
        self.addParameter(
            QgsProcessingParameterNumber(
                self.LNG,
                self.tr('Longitude (DDG)'),
                QgsProcessingParameterNumber.Double,
                minValue=-180.0,
                maxValue=180.0
            )
        )

        # Input search radius
        self.addParameter(
            QgsProcessingParameterNumber(
                self.SEARCHRADIUS,
                self.tr('Search Radius (m)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                maxValue=10000
            )
        )

        # Input TDP diameter
        self.addParameter(
            QgsProcessingParameterNumber(
                self.TDPDIAMETER,
                self.tr('Touchdown Point Diameter (m)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                maxValue=500
            )
        )

        # Input slope caution value
        self.addParameter(
            QgsProcessingParameterNumber(
                self.SLOPECAUTION,
                self.tr('Slope Caution Value (°)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                maxValue=45
            )
        )

        # Input slope limit value
        self.addParameter(
            QgsProcessingParameterNumber(
                self.SLOPELIMIT,
                self.tr('Slope Limit Value (°)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                maxValue=45
            )
        )

        # Add input to choose whether to default to 10m data
        self.addParameter(
            QgsProcessingParameterBoolean(
                self.USE10MDATA,
                self.tr('Default to 10m Elevation Data?'),
                defaultValue=True
            )
        )

        # Add output for HLZ points
        self.addParameter(
            QgsProcessingParameterVectorDestination(
                self.OUTPUTHLZPOINTS,
                self.tr('Output HLZ Points'),
                type=QgsProcessing.TypeVectorAnyGeometry
            )
        )

        # Add output for HLS raster
        self.addParameter(
            QgsProcessingParameterRasterDestination(
                self.OUTPUTHLSRASTER,
                self.tr('Output HLS Raster')
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        """
        Here is where the processing itself takes place.
        """

        # Assign inputs to variables
        self.lat = self.parameterAsDouble(parameters, self.LAT, context)
        self.lng = self.parameterAsDouble(parameters, self.LNG, context)
        self.search_radius = self.parameterAsInt(parameters, self.SEARCHRADIUS, context)
        self.tdp_diameter = self.parameterAsInt(parameters, self.TDPDIAMETER, context)
        self.slope_caution = self.parameterAsInt(parameters, self.SLOPECAUTION, context)
        self.slope_limit = self.parameterAsInt(parameters, self.SLOPELIMIT, context)
        self.use_10m_data = self.parameterAsBool(parameters, self.USE10MDATA, context)

        self.output_hlz_points = self.parameterAsOutputLayer(parameters, self.OUTPUTHLZPOINTS, context)
        self.output_hls_raster = self.parameterAsOutputLayer(parameters, self.OUTPUTHLSRASTER, context)

        """
        ------------------------ Perform initial setup ---------------------------------
        """

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        feedback.setProgressText('Performing initial setup')

        # Perform initial setup
        # Variables/constants
        self.code_filename = 'openhlz_algorithm.py'
        current_path = __file__
        self.working_directory = current_path.replace(self.code_filename, '')
        os.chdir(self.working_directory)

        self.scratch_folder = 'modelScratch'

        self.utm_filename = 'data/World_UTM_Grid.shp'
        self.utm_path = self.utm_filename

        self.project_coordinate_system = self.instance.crs()
        self.project_coordinate_system_name = self.instance.crs().authid()
        self.model_coordinate_system_name = 'epsg:3857'
        self.model_coordinate_system = QgsCoordinateReferenceSystem(self.model_coordinate_system_name)

        self.hls_style_path = 'styles/hls_raster_style.qml'
        self.hlz_stle_path = 'styles/hlz_points_style.qml'

        # Delete/make directories
        if os.path.isdir(self.scratch_folder):
            shutil.rmtree(self.scratch_folder)
            os.mkdir(self.scratch_folder)
        else:
            os.mkdir(self.scratch_folder)

        # Create point feature from lat/lng values
        # Export lat/lng values to table
        coordinate_array = numpy.zeros((1, 3))
        coordinate_array[0][0] = 1
        coordinate_array[0][1] = self.lat
        coordinate_array[0][2] = self.lng
        df = pandas.DataFrame(coordinate_array, columns=['id', 'lat', 'lng'])
        df.to_csv(self.scratch_folder + '/coordinate_table.csv', index=False)

        # Convert xy table to point and project into project crs
        poi_layer = processing.run("native:createpointslayerfromtable", {
            'INPUT': self.scratch_folder + '/coordinate_table.csv',
            'XFIELD': 'lng', 'YFIELD': 'lat', 'ZFIELD': '', 'MFIELD': '',
            'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),
            'OUTPUT': 'TEMPORARY_OUTPUT'})['OUTPUT']

        poi_projected_layer = processing.run("native:reprojectlayer", {'INPUT': poi_layer,
                                                                       'TARGET_CRS': self.model_coordinate_system,
                                                                       'OUTPUT': 'TEMPORARY_OUTPUT'})['OUTPUT']

        # Generate AOI
        self.aoi_layer = processing.run("native:buffer", {'INPUT': poi_projected_layer, 'DISTANCE': self.search_radius,
                                                          'SEGMENTS': 5, 'END_CAP_STYLE': 2, 'JOIN_STYLE': 1,
                                                          'MITER_LIMIT': 2, 'DISSOLVE': False,
                                                          'OUTPUT': 'TEMPORARY_OUTPUT'})['OUTPUT']

        """
        ------------------------ Download and Prep Data ---------------------------------
        """

        feedback.setProgressText('Downloading DEM Data')

        self.utm_layer = QgsVectorLayer(self.utm_path, 'World_UTM_Grid')

        # Get JSON files
        json_1m, json_10m = getJson(self.aoi_layer, self.scratch_folder,
                                    self.model_coordinate_system_name)

        # Variable for output DEM filenames
        self.dem_return = []

        # Download DEM data
        downloaded_tiles = []
        if json_1m['total'] > 0 and not self.use_10m_data:
            for i in range(json_1m['total']):
                feedback.setProgressText('Downloading DEM Data ' + str(i+1) + '/' + str(json_1m['total']))
                # Stop the algorithm if cancel button has been clicked
                if feedback.isCanceled():
                    break
                temp_return = downloadDem(i, json_1m, self.scratch_folder, False)
                self.dem_return.append(temp_return)
        else:
            for i in range(json_10m['total']):
                temp_dict = json_10m['items'][i]
                min_x = int(temp_dict['boundingBox']['minX'])
                max_y = int(temp_dict['boundingBox']['maxY'])
                if [min_x, max_y] not in downloaded_tiles:
                    # Stop the algorithm if cancel button has been clicked
                    if feedback.isCanceled():
                        break
                    feedback.setProgressText('Downloading DEM Data ' + str(i+1) + '/' + str(json_10m['total']))
                    downloaded_tiles.append([min_x, max_y])
                    temp_return = downloadDem(i, json_10m, self.scratch_folder, True)
                    self.dem_return.append(temp_return)

        # Download LULC data
        # Select UTM zones that intersect input AOI
        processing.run("native:selectbylocation",
                       {'INPUT': self.utm_layer, 'PREDICATE': [0], 'INTERSECT': self.aoi_layer,
                        'METHOD': 0})
        QgsVectorFileWriter.writeAsVectorFormat(self.utm_layer,
                                                self.scratch_folder + '/selected_utm.shp', 'utf-8',
                                                driverName='ESRI Shapefile', onlySelected=True)
        temp_selection = QgsVectorLayer(self.scratch_folder + '/selected_utm.shp', 'temp_selection')
        temp_features = temp_selection.getFeatures()

        # Download LULC Data
        feedback.setProgressText('Downloading LULC Data')

        # Get zone IDs for necessary LULC files
        utm_zones = []
        for feature in temp_features:
            utm_zones.append([feature[1], feature[2]])

        # Variable for output LULC filenames
        self.lulc_return = []

        # Download necessary LULC files
        for i in range(len(utm_zones)):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break
            feedback.setProgressText('Downloading LULC Data ' + str(i+1) + '/' + str(len(utm_zones)))
            temp_return = downloadLULC(i, utm_zones, self.scratch_folder)
            self.lulc_return.append(temp_return)

        feedback.setProgressText('Mosaicing and Clipping Rasters')

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        mosaicAndClipRasters(self.dem_return, self.lulc_return, self.aoi_layer, self.scratch_folder, feedback)

        """
        ------------------------ Calculate HLS Raster ---------------------------------
        """

        feedback.setProgressText('Calculating HLS Raster')

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        projected_dem = self.scratch_folder + '/projected_dem.tif'
        hls_raster = generateHlsRaster(self.output_hls_raster, projected_dem, self.slope_caution, self.slope_limit,
                                       self.model_coordinate_system, self.hls_style_path, self.instance,
                                       self.scratch_folder)

        """
        ------------------------ ID HLZs ---------------------------------
        """

        feedback.setProgressText('Identifying Possible HLZs')

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        # Identify HLZ points
        hlz_points = identifyHlzs(self.output_hlz_points, hls_raster, self.tdp_diameter, self.model_coordinate_system,
                                  self.hlz_stle_path, self.instance, self.scratch_folder)

        return {self.OUTPUTHLZPOINTS: hlz_points, self.OUTPUTHLSRASTER: hls_raster}

    def name(self):
        """
        Returns the algorithm name, used for identifying the algorithm. This
        string should be fixed for the algorithm, and must not be localised.
        The name should be unique within each provider. Names should contain
        lowercase alphanumeric characters only and no spaces or other
        formatting characters.
        """
        return 'Identify HLZs (from Lat/Lng)'

    def displayName(self):
        """
        Returns the translated algorithm name, which should be used for any
        user-visible display of the algorithm name.
        """
        return self.tr(self.name())

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return IdentifyHLZsFromLatLng()

    def helpUrl(self):
        return "https://github.com/jojohn2468/openhlz/blob/master/README.md"

    def shortHelpString(self):
        str = """
        Identifies possible helicopter landing zones (HLZs) within an area of interest (AOI) based on a user-defined point (latitude/longitude) and search area.
        
        The algorithm uses elevation and land cover data automatically downloaded from online sources and takes input from user to determine slope constraints.
        
        Latitude and longitude values should be in decimal degrees (DDG) format using +/- to denote N/S or E/W.
        
        'Search Radius' defines the radius of the generated AOI in meters.
        
        'Touchdown Point Diameter' is the diameter, in meters, of a suitable area a helicopter needs to safely land.
        
        'Slope Caution Value' defines the slope magnitude, in degrees, beyond which a helicopter touching down MAY exceed a slope limitation (based on orientation of helicopter relative to slope - i.e. upslope landing, downslope landing, sideslope landing).
        
        'Slope Limit Value' defines the slope magnitude, in degrees, beyond which a helicopter touching down WILL exceed a slope limitation.
        
        The 'Default to 10m Elevation Data?' box allows the user to choose whether to use 10m elevation as the default or to first try to get 1m elevation data.  1m data provides greater slope accuracy than 10m data but at the cost of larger files, greater download times, and increased processing time.  See documentation on GitHub for more discussion on this topic.
        
        NOTE: Only works on locations within the United States.
        
        WARNING: The possible HLZ locations identified by this plugin are for research purposes only.  The results of this plugin have not been evaluated for accuracy and should not be relied upon as the sole means for determining where to land an aircraft.  It is ultimately the responsibiltiy of the pilot-in-command to determine the suitability of any location prior to landing their aircraft.  The developer of this plugin is not liable for ANY damage to equipment, bodily injury, or loss of life associated with its use.
        """
        return str

    def shortDescription(self):
        return "Identifies possible HLZs within an AOI centered on a user-defined point (lat/lng)"

    def icon(self):
        return QIcon(os.path.dirname(__file__) + "/images/latlng_icon.svg")


class IdentifyHLZsFromPoint(QgsProcessingAlgorithm):
    """
    Class to identify possible HLZs from a point
    """

    # Constants used to refer to parameters and outputs. They will be
    # used when calling the algorithm from another algorithm, or when
    # calling from the QGIS console.

    INPUTPOINT = "INPUTPOINT"
    SEARCHRADIUS = "SEARCHRADIUS"
    TDPDIAMETER = "TDPDIAMETER"
    SLOPECAUTION = "SLOPECAUTION"
    SLOPELIMIT = "SLOPELIMIT"
    USE10MDATA = "USE10MDATA"

    OUTPUTHLZPOINTS = "OUTPUTHLZPOINTS"
    OUTPUTHLSRASTER = "OUTPUTHLSRASTER"

    def initAlgorithm(self, config):
        """
        Here we define the inputs and output of the algorithm, along
        with some other properties.
        """

        # Save reference to project instance
        self.instance = QgsProject.instance()

        # Add input and output parameters
        # Input point
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUTPOINT,
                self.tr('Input Point'),
                [QgsProcessing.TypeVectorPoint]
            )
        )

        # Input search radius
        self.addParameter(
            QgsProcessingParameterNumber(
                self.SEARCHRADIUS,
                self.tr('Search Radius (m)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                maxValue=10000
            )
        )

        # Input TDP diameter
        self.addParameter(
            QgsProcessingParameterNumber(
                self.TDPDIAMETER,
                self.tr('Touchdown Point Diameter (m)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                maxValue=500
            )
        )

        # Input slope caution value
        self.addParameter(
            QgsProcessingParameterNumber(
                self.SLOPECAUTION,
                self.tr('Slope Caution Value (°)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                maxValue=45
            )
        )

        # Input slope limit value
        self.addParameter(
            QgsProcessingParameterNumber(
                self.SLOPELIMIT,
                self.tr('Slope Limit Value (°)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                maxValue=45
            )
        )

        # Add input to choose whether to default to 10m data
        self.addParameter(
            QgsProcessingParameterBoolean(
                self.USE10MDATA,
                self.tr('Default to 10m Elevation Data?'),
                defaultValue=True
            )
        )

        # Add output for HLZ points
        self.addParameter(
            QgsProcessingParameterVectorDestination(
                self.OUTPUTHLZPOINTS,
                self.tr('Output HLZ Points'),
                type=QgsProcessing.TypeVectorAnyGeometry
            )
        )

        # Add output for HLS raster
        self.addParameter(
            QgsProcessingParameterRasterDestination(
                self.OUTPUTHLSRASTER,
                self.tr('Output HLS Raster')
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        """
        Here is where the processing itself takes place.
        """

        # Assign inputs to variables
        self.input_point = self.parameterAsVectorLayer(parameters, self.INPUTPOINT, context)
        self.search_radius = self.parameterAsInt(parameters, self.SEARCHRADIUS, context)
        self.tdp_diameter = self.parameterAsInt(parameters, self.TDPDIAMETER, context)
        self.slope_caution = self.parameterAsInt(parameters, self.SLOPECAUTION, context)
        self.slope_limit = self.parameterAsInt(parameters, self.SLOPELIMIT, context)
        self.use_10m_data = self.parameterAsBool(parameters, self.USE10MDATA, context)

        self.output_hlz_points = self.parameterAsOutputLayer(parameters, self.OUTPUTHLZPOINTS, context)
        self.output_hls_raster = self.parameterAsOutputLayer(parameters, self.OUTPUTHLSRASTER, context)


        """
        ------------------------ Perform initial setup ---------------------------------
        """

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        feedback.setProgressText('Performing initial setup')

        # Perform initial setup
        self.code_filename = 'openhlz_algorithm.py'
        current_path = __file__
        self.working_directory = current_path.replace(self.code_filename, '')
        os.chdir(self.working_directory)

        self.scratch_folder = 'modelScratch'

        self.utm_filename = 'data/World_UTM_Grid.shp'
        self.utm_path = self.utm_filename

        self.project_coordinate_system = self.instance.crs()
        self.project_coordinate_system_name = self.instance.crs().authid()
        self.model_coordinate_system_name = 'epsg:3857'
        self.model_coordinate_system = QgsCoordinateReferenceSystem(self.model_coordinate_system_name)

        self.hls_style_path = 'styles/hls_raster_style.qml'
        self.hlz_stle_path = 'styles/hlz_points_style.qml'

        # Delete/make directories
        if os.path.isdir(self.scratch_folder):
            shutil.rmtree(self.scratch_folder)
            os.mkdir(self.scratch_folder)
        else:
            os.mkdir(self.scratch_folder)

        # Create AOI
        poi_projected_layer = processing.run("native:reprojectlayer", {'INPUT': self.input_point,
                                                                       'TARGET_CRS': self.model_coordinate_system,
                                                                       'OUTPUT': 'TEMPORARY_OUTPUT'})['OUTPUT']

        # Generate AOI
        self.aoi_layer = processing.run("native:buffer", {'INPUT': poi_projected_layer, 'DISTANCE': self.search_radius,
                                                          'SEGMENTS': 5, 'END_CAP_STYLE': 2, 'JOIN_STYLE': 1,
                                                          'MITER_LIMIT': 2, 'DISSOLVE': False,
                                                          'OUTPUT': 'TEMPORARY_OUTPUT'})['OUTPUT']

        """
        ------------------------ Download and Prep Data ---------------------------------
        """

        feedback.setProgressText('Downloading DEM Data')

        self.utm_layer = QgsVectorLayer(self.utm_path, 'World_UTM_Grid')

        # Get JSON files
        json_1m, json_10m = getJson(self.aoi_layer, self.scratch_folder,
                                    self.model_coordinate_system_name)

        # Variable for output DEM filenames
        self.dem_return = []

        # Download DEM data
        downloaded_tiles = []
        if json_1m['total'] > 0 and not self.use_10m_data:
            for i in range(json_1m['total']):
                feedback.setProgressText('Downloading DEM Data ' + str(i+1) + '/' + str(json_1m['total']))
                # Stop the algorithm if cancel button has been clicked
                if feedback.isCanceled():
                    break
                temp_return = downloadDem(i, json_1m, self.scratch_folder, False)
                self.dem_return.append(temp_return)
        else:
            for i in range(json_10m['total']):
                temp_dict = json_10m['items'][i]
                min_x = int(temp_dict['boundingBox']['minX'])
                max_y = int(temp_dict['boundingBox']['maxY'])
                if [min_x, max_y] not in downloaded_tiles:
                    # Stop the algorithm if cancel button has been clicked
                    if feedback.isCanceled():
                        break
                    feedback.setProgressText('Downloading DEM Data ' + str(i+1) + '/' + str(json_10m['total']))
                    downloaded_tiles.append([min_x, max_y])
                    temp_return = downloadDem(i, json_10m, self.scratch_folder, True)
                    self.dem_return.append(temp_return)

        # Download LULC data
        # Select UTM zones that intersect input AOI
        processing.run("native:selectbylocation",
                       {'INPUT': self.utm_layer, 'PREDICATE': [0], 'INTERSECT': self.aoi_layer,
                        'METHOD': 0})
        QgsVectorFileWriter.writeAsVectorFormat(self.utm_layer,
                                                self.scratch_folder + '/selected_utm.shp', 'utf-8',
                                                driverName='ESRI Shapefile', onlySelected=True)
        temp_selection = QgsVectorLayer(self.scratch_folder + '/selected_utm.shp', 'temp_selection')
        temp_features = temp_selection.getFeatures()

        # Download LULC Data
        feedback.setProgressText('Downloading LULC Data')

        # Get zone IDs for necessary LULC files
        utm_zones = []
        for feature in temp_features:
            utm_zones.append([feature[1], feature[2]])

        # Variable for output LULC filenames
        self.lulc_return = []

        # Download necessary LULC files
        for i in range(len(utm_zones)):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break
            feedback.setProgressText('Downloading LULC Data ' + str(i+1) + '/' + str(len(utm_zones)))
            temp_return = downloadLULC(i, utm_zones, self.scratch_folder)
            self.lulc_return.append(temp_return)

        feedback.setProgressText('Mosaicing and Clipping Rasters')

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        mosaicAndClipRasters(self.dem_return, self.lulc_return, self.aoi_layer, self.scratch_folder, feedback)

        """
        ------------------------ Calculate HLS Raster ---------------------------------
        """

        feedback.setProgressText('Calculating HLS Raster')

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        projected_dem = self.scratch_folder + '/projected_dem.tif'
        hls_raster = generateHlsRaster(self.output_hls_raster, projected_dem, self.slope_caution, self.slope_limit,
                                       self.model_coordinate_system, self.hls_style_path, self.instance,
                                       self.scratch_folder)

        """
        ------------------------ ID HLZs ---------------------------------
        """

        feedback.setProgressText('Identifying Possible HLZs')

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        # Identify HLZ points
        hlz_points = identifyHlzs(self.output_hlz_points, hls_raster, self.tdp_diameter, self.model_coordinate_system,
                                  self.hlz_stle_path, self.instance, self.scratch_folder)

        return {self.OUTPUTHLZPOINTS: hlz_points, self.OUTPUTHLSRASTER: hls_raster}

    def name(self):
        """
        Returns the algorithm name, used for identifying the algorithm. This
        string should be fixed for the algorithm, and must not be localised.
        The name should be unique within each provider. Names should contain
        lowercase alphanumeric characters only and no spaces or other
        formatting characters.
        """
        return 'Identify HLZs (from Point)'

    def displayName(self):
        """
        Returns the translated algorithm name, which should be used for any
        user-visible display of the algorithm name.
        """
        return self.tr(self.name())

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return IdentifyHLZsFromPoint()

    def helpUrl(self):
        return "https://github.com/jojohn2468/openhlz/blob/master/README.md"

    def shortHelpString(self):
        str = """
        Identifies possible helicopter landing zones (HLZs) within an area of interest (AOI) based on a user-defined point (vector file) and search area.

        Algorithm uses elevation and land cover data automatically downloaded from online sources and takes input from user to determine slope constraints.

        The input point file should contain only a singular point feature (support for multi-point files is under development).

        'Search Radius' defines the radius of the generated AOI in meters.
        
        'Touchdown Point Diameter' is the diameter, in meters, of a suitable area a helicopter needs to safely land.
        
        'Slope Caution Value' defines the slope magnitude, in degrees, beyond which a helicopter touching down MAY exceed a slope limitation (based on orientation of helicopter relative to slope - i.e. upslope landing, downslope landing, sideslope landing).
        
        'Slope Limit Value' defines the slope magnitude, in degrees, beyond which a helicopter touching down WILL exceed a slope limitation.
        
        The 'Default to 10m Elevation Data?' box allows the user to choose whether to use 10m elevation as the default or to first try to get 1m elevation data.  1m data provides greater slope accuracy than 10m data but at the cost of larger files, greater download times, and increased processing time.  See documentation on GitHub for more discussion on this topic.
        
        NOTE: Only works on locations within the United States.
        
        WARNING: The possible HLZ locations identified by this plugin are for research purposes only.  The results of this plugin have not been evaluated for accuracy and should not be relied upon as the sole means for determining where to land an aircraft.  It is ultimately the responsibiltiy of the pilot-in-command to determine the suitability of any location prior to landing their aircraft.  The developer of this plugin is not liable for ANY damage to equipment, bodily injury, or loss of life associated with its use.
        """
        return str

    def shortDescription(self):
        return "Identifies possible HLZs within an AOI centered on a user-defined point (vector file)"

    def icon(self):
        return QIcon(os.path.dirname(__file__) + "/images/point_icon.svg")


class IdentifyHLZsFromAoi(QgsProcessingAlgorithm):
    """
    Class to identify possible HLZs from aoi
    """

    # Constants used to refer to parameters and outputs. They will be
    # used when calling the algorithm from another algorithm, or when
    # calling from the QGIS console.

    INPUTAOI = "INPUTAOI"
    SEARCHRADIUS = "SEARCHRADIUS"
    TDPDIAMETER = "TDPDIAMETER"
    SLOPECAUTION = "SLOPECAUTION"
    SLOPELIMIT = "SLOPELIMIT"
    USE10MDATA = "USE10MDATA"

    OUTPUTHLZPOINTS = "OUTPUTHLZPOINTS"
    OUTPUTHLSRASTER = "OUTPUTHLSRASTER"

    def initAlgorithm(self, config):
        """
        Here we define the inputs and output of the algorithm, along
        with some other properties.
        """

        # Save reference to project instance
        self.instance = QgsProject.instance()

        # Add input and output parameters
        # Input point
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUTAOI,
                self.tr('Input AOI'),
                [QgsProcessing.TypeVectorPolygon]
            )
        )

        # Input TDP diameter
        self.addParameter(
            QgsProcessingParameterNumber(
                self.TDPDIAMETER,
                self.tr('Touchdown Point Diameter (m)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                maxValue=500
            )
        )

        # Input slope caution value
        self.addParameter(
            QgsProcessingParameterNumber(
                self.SLOPECAUTION,
                self.tr('Slope Caution Value (°)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                maxValue=45
            )
        )

        # Input slope limit value
        self.addParameter(
            QgsProcessingParameterNumber(
                self.SLOPELIMIT,
                self.tr('Slope Limit Value (°)'),
                QgsProcessingParameterNumber.Integer,
                minValue=0,
                maxValue=45
            )
        )

        # Add input to choose whether to default to 10m data
        self.addParameter(
            QgsProcessingParameterBoolean(
                self.USE10MDATA,
                self.tr('Default to 10m Elevation Data?'),
                defaultValue=True
            )
        )

        # Add output for HLZ points
        self.addParameter(
            QgsProcessingParameterVectorDestination(
                self.OUTPUTHLZPOINTS,
                self.tr('Output HLZ Points'),
                type=QgsProcessing.TypeVectorAnyGeometry
            )
        )

        # Add output for HLS raster
        self.addParameter(
            QgsProcessingParameterRasterDestination(
                self.OUTPUTHLSRASTER,
                self.tr('Output HLS Raster')
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        """
        Here is where the processing itself takes place.
        """

        # Assign inputs to variables
        self.input_aoi = self.parameterAsVectorLayer(parameters, self.INPUTAOI, context)
        self.tdp_diameter = self.parameterAsInt(parameters, self.TDPDIAMETER, context)
        self.slope_caution = self.parameterAsInt(parameters, self.SLOPECAUTION, context)
        self.slope_limit = self.parameterAsInt(parameters, self.SLOPELIMIT, context)
        self.use_10m_data = self.parameterAsBool(parameters, self.USE10MDATA, context)

        self.output_hlz_points = self.parameterAsOutputLayer(parameters, self.OUTPUTHLZPOINTS, context)
        self.output_hls_raster = self.parameterAsOutputLayer(parameters, self.OUTPUTHLSRASTER, context)

        """
        ------------------------ Perform initial setup ---------------------------------
        """

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        feedback.setProgressText('Performing initial setup')

        # Perform initial setup
        self.code_filename = 'openhlz_algorithm.py'
        current_path = __file__
        self.working_directory = current_path.replace(self.code_filename, '')
        os.chdir(self.working_directory)

        self.scratch_folder = 'modelScratch'

        self.utm_filename = 'data/World_UTM_Grid.shp'
        self.utm_path = self.utm_filename

        self.project_coordinate_system = self.instance.crs()
        self.project_coordinate_system_name = self.instance.crs().authid()
        self.model_coordinate_system_name = 'epsg:3857'
        self.model_coordinate_system = QgsCoordinateReferenceSystem(self.model_coordinate_system_name)

        self.hls_style_path = 'styles/hls_raster_style.qml'
        self.hlz_stle_path = 'styles/hlz_points_style.qml'

        # Delete/make directories
        if os.path.isdir(self.scratch_folder):
            shutil.rmtree(self.scratch_folder)
            os.mkdir(self.scratch_folder)
        else:
            os.mkdir(self.scratch_folder)

        # Project AOI
        self.aoi_layer = processing.run("native:reprojectlayer", {'INPUT': self.input_aoi,
                                                                  'TARGET_CRS': self.model_coordinate_system,
                                                                  'OUTPUT': 'TEMPORARY_OUTPUT'})['OUTPUT']

        """
        ------------------------ Download and Prep Data ---------------------------------
        """

        feedback.setProgressText('Downloading DEM Data')

        self.utm_layer = QgsVectorLayer(self.utm_path, 'World_UTM_Grid')

        # Get JSON files
        json_1m, json_10m = getJson(self.aoi_layer, self.scratch_folder,
                                    self.model_coordinate_system_name)

        # Variable for output DEM filenames
        self.dem_return = []

        # Download DEM data
        downloaded_tiles = []
        if json_1m['total'] > 0 and not self.use_10m_data:
            for i in range(json_1m['total']):
                feedback.setProgressText('Downloading DEM Data ' + str(i+1) + '/' + str(json_1m['total']))
                # Stop the algorithm if cancel button has been clicked
                if feedback.isCanceled():
                    break
                temp_return = downloadDem(i, json_1m, self.scratch_folder, False)
                self.dem_return.append(temp_return)
        else:
            for i in range(json_10m['total']):
                temp_dict = json_10m['items'][i]
                min_x = int(temp_dict['boundingBox']['minX'])
                max_y = int(temp_dict['boundingBox']['maxY'])
                if [min_x, max_y] not in downloaded_tiles:
                    # Stop the algorithm if cancel button has been clicked
                    if feedback.isCanceled():
                        break
                    feedback.setProgressText('Downloading DEM Data ' + str(i+1) + '/' + str(json_10m['total']))
                    downloaded_tiles.append([min_x, max_y])
                    temp_return = downloadDem(i, json_10m, self.scratch_folder, True)
                    self.dem_return.append(temp_return)

        # Download LULC data
        # Select UTM zones that intersect input AOI
        processing.run("native:selectbylocation",
                       {'INPUT': self.utm_layer, 'PREDICATE': [0], 'INTERSECT': self.aoi_layer,
                        'METHOD': 0})
        QgsVectorFileWriter.writeAsVectorFormat(self.utm_layer,
                                                self.scratch_folder + '/selected_utm.shp', 'utf-8',
                                                driverName='ESRI Shapefile', onlySelected=True)
        temp_selection = QgsVectorLayer(self.scratch_folder + '/selected_utm.shp', 'temp_selection')
        temp_features = temp_selection.getFeatures()

        # Download LULC Data
        feedback.setProgressText('Downloading LULC Data')

        # Get zone IDs for necessary LULC files
        utm_zones = []
        for feature in temp_features:
            utm_zones.append([feature[1], feature[2]])

        # Variable for output LULC filenames
        self.lulc_return = []

        # Download necessary LULC files
        for i in range(len(utm_zones)):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break
            feedback.setProgressText('Downloading LULC Data ' + str(i+1) + '/' + str(len(utm_zones)))
            temp_return = downloadLULC(i, utm_zones, self.scratch_folder)
            self.lulc_return.append(temp_return)

        feedback.setProgressText('Mosaicing and Clipping Rasters')

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        mosaicAndClipRasters(self.dem_return, self.lulc_return, self.aoi_layer, self.scratch_folder, feedback)

        """
        ------------------------ Calculate HLS Raster ---------------------------------
        """

        feedback.setProgressText('Calculating HLS Raster')

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        projected_dem = self.scratch_folder + '/projected_dem.tif'
        hls_raster = generateHlsRaster(self.output_hls_raster, projected_dem, self.slope_caution, self.slope_limit,
                                       self.model_coordinate_system, self.hls_style_path, self.instance,
                                       self.scratch_folder)

        """
        ------------------------ ID HLZs ---------------------------------
        """

        feedback.setProgressText('Identifying Possible HLZs')

        # Stop the algorithm if cancel button has been clicked
        if feedback.isCanceled():
            return

        # Identify HLZ points
        hlz_points = identifyHlzs(self.output_hlz_points, hls_raster, self.tdp_diameter, self.model_coordinate_system,
                                  self.hlz_stle_path, self.instance, self.scratch_folder)

        return {self.OUTPUTHLZPOINTS: hlz_points, self.OUTPUTHLSRASTER: hls_raster}

    def name(self):
        """
        Returns the algorithm name, used for identifying the algorithm. This
        string should be fixed for the algorithm, and must not be localised.
        The name should be unique within each provider. Names should contain
        lowercase alphanumeric characters only and no spaces or other
        formatting characters.
        """
        return 'Identify HLZs (from AOI)'

    def displayName(self):
        """
        Returns the translated algorithm name, which should be used for any
        user-visible display of the algorithm name.
        """
        return self.tr(self.name())

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return IdentifyHLZsFromAoi()

    def helpUrl(self):
        return "https://github.com/jojohn2468/openhlz/blob/master/README.md"

    def shortHelpString(self):
        str = """
        Identifies possible helicopter landing zones (HLZs) within a user-defined area of interest (AOI) input as a vector file.

        Algorithm uses elevation and land cover data automatically downloaded from online sources and takes input from user to determine slope constraints.

        The input AOI file should contain only a singular polygon feature (support for multi-polygon files is under development).
        
        'Touchdown Point Diameter' is the diameter, in meters, of a suitable area a helicopter needs to safely land.
        
        'Slope Caution Value' defines the slope magnitude, in degrees, beyond which a helicopter touching down MAY exceed a slope limitation (based on orientation of helicopter relative to slope - i.e. upslope landing, downslope landing, sideslope landing).
        
        'Slope Limit Value' defines the slope magnitude, in degrees, beyond which a helicopter touching down WILL exceed a slope limitation.
        
        The 'Default to 10m Elevation Data?' box allows the user to choose whether to use 10m elevation as the default or to first try to get 1m elevation data.  1m data provides greater slope accuracy than 10m data but at the cost of larger files, greater download times, and increased processing time.  See documentation on GitHub for more discussion on this topic.
        
        NOTE: Only works on locations within the United States.
        
        WARNING: The possible HLZ locations identified by this plugin are for research purposes only.  The results of this plugin have not been evaluated for accuracy and should not be relied upon as the sole means for determining where to land an aircraft.  It is ultimately the responsibiltiy of the pilot-in-command to determine the suitability of any location prior to landing their aircraft.  The developer of this plugin is not liable for ANY damage to equipment, bodily injury, or loss of life associated with its use.
        """
        return str

    def shortDescription(self):
        return "Identifies possible HLZs within a user-defined AOI (vector file)"

    def icon(self):
        return QIcon(os.path.dirname(__file__) + "/images/polygon_icon.svg")
