"""
/***************************************************************************
 qgSurf - plugin for Quantum GIS

 Processing of geological planes and surfaces

                              -------------------
        begin                : 2011-12-21
        copyright            : (C) 2011-2023 by Mauro Alberti
        email                : alberti.m65@gmail.com

 ***************************************************************************/

# licensed under the terms of GNU GPL 3

/***************************************************************************
 *                                                                         *
 *   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 3 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""

from math import atan, isfinite

try:
    from osgeo import ogr
except ImportError:
    import ogr

from qgis.PyQt.QtWidgets import *

from gst.core.geometries.grids.rasters import *
from gst.io.rasters.gdal_io import *
from gst.qgis.canvas import *
from gst.qgis.orientations import *
from gst.qgis.points import *
from gst.qgis.project import *
from gst.qgis.projections import *
from gst.qgis.tables import *
from gst.qt.messages import *
from gst.io.vectors.ogr_io import *
from gst.qgis.messages import *

from ..configurations.colors import *
from ..configurations.fields import *
from ..configurations.general_params import *
from ..configurations.messages import *
from ..configurations.plane_points_distances_params import *

from ..ui.auxiliary_windows.distances import *
from ..utils.planes import *


class PlaneDistancesWidget(QWidget):
    """
    Constructor
    
    """

    line_colors = [
        "white",
        "red",
        "blue",
        "yellow",
        "orange",
        "brown"
    ]

    def __init__(self,
                 plugin_folder: str,
                 module_name: str,
                 canvas,
                 plugin_qaction):

        super(PlaneDistancesWidget, self).__init__()

        self.plugin_folder = plugin_folder
        self.module_name = module_name

        self.canvas = canvas
        self.plugin = plugin_qaction

        self.setup_gui()
        self.initialize_parameters()
        self.store_current_map_crs()

    def initialize_parameters(self):

        self.previous_tool = None
        self.use_elevation_from_dem = False
        self.map_tool = None

        self.reset_everything()

    def reset_post_dem_change(self):

        self.disable_canvas_tools()
        self.reset_input_dem_parameters()
        self.reset_intersections()

    def reset_everything(self):

        self.disable_canvas_tools()
        self.reset_all_io_parameters()

    def reset_source_point_parameters(self):

        self.reset_source_point_in_map_QPushButton.setEnabled(False)
        self.reset_source_point_and_intersections()

    def reset_all_io_parameters(self):

        self.reset_all_input_parameters()
        self.reset_intersections()

    def reset_all_input_parameters(self):

        self.reset_input_dem_parameters()
        self.reset_source_point()
        self.reset_input_geoplane_parameters()

    def reset_input_dem_parameters(self):

        self.qgis_grid_layer = None
        self.elevation_grid = None
        self.current_source_point_elevation_value = None

    def disable_canvas_tools(self):

        self.set_source_point_in_map_QPushButton.setEnabled(False)
        self.reset_source_point_in_map_QPushButton.setEnabled(False)

        try:
            self.map_tool.canvasClicked.disconnect(self.update_source_point_location)
        except:
            pass

        try:
            self.restore_previous_map_tool(self.map_tool)
        except:
            pass

    def reset_source_point_and_intersections(self):

        self.reset_intersections()
        self.reset_source_point()

    def reset_intersections(self):

        if hasattr(self, 'intersections_markers_list'):
            self.remove_intersection_markers_from_canvas()
        self.reset_intersections_markers_and_values()

    def reset_intersections_markers_list(self):

        self.intersections_markers_list = []

    def reset_intersections_values(self):

        self.intersections_x = []
        self.intersections_y = []
        self.intersections_xprt = {}
        self.inters = None

    def reset_intersections_markers_and_values(self):

        self.reset_intersections_markers_list()
        self.reset_intersections_values()

    def reset_source_point(self):

        self.clear_source_point_qlineedit_forms()
        if hasattr(self, 'source_point_marker_list'):
            self.remove_source_point_marker_from_canvas()
        self.reset_source_point_coordinates()
        self.reset_source_point_marker_list()

    def reset_source_point_coordinates(self):

        self.source_point_longitude = None
        self.source_point_latitude = None
        self.source_point_elevation = None

    def reset_source_point_marker_list(self):

        self.source_point_marker_list = []

    def remove_intersection_markers_from_canvas(self):

        for mrk in self.intersections_markers_list:
            self.canvas.scene().removeItem(mrk)

    def remove_source_point_marker_from_canvas(self):

        for mrk in self.source_point_marker_list:
            self.canvas.scene().removeItem(mrk)

    def reset_input_geoplane_parameters(self):

        self.source_dip_direction = None
        self.source_dip_angle = None

    def refresh_raster_layer_list_for_elevation(self):

        try:
            self.choose_source_dem_QComboBox.currentIndexChanged[int].disconnect(self.get_chosen_dem)
        except:
            pass

        try:
            self.reset_everything()
        except:
            pass

        try:
            self.choose_source_dem_QComboBox.clear()
        except:
            return

        self.choose_source_dem_QComboBox.addItem(OPTIONAL_TEXT)

        self.qgis_raster_layers = loaded_raster_layers()
        if self.qgis_raster_layers is None or len(self.qgis_raster_layers) == 0:
            return
        for layer in self.qgis_raster_layers:
            self.choose_source_dem_QComboBox.addItem(layer.name())

        self.choose_source_dem_QComboBox.currentIndexChanged[int].connect(self.get_chosen_dem)

    def refresh_raster_layer_list_for_distances(self):

        try:
            self.choose_distances_dem_QComboBox.clear()
        except:
            return

        self.choose_distances_dem_QComboBox.addItem(REQUIRED_TEXT)

        self.qgis_raster_layers_for_elevations = loaded_raster_layers()
        if self.qgis_raster_layers_for_elevations is None or len(self.qgis_raster_layers_for_elevations) == 0:
            return
        for layer in self.qgis_raster_layers_for_elevations:
            self.choose_distances_dem_QComboBox.addItem(layer.name())

    def setup_gui(self):

        layout = QVBoxLayout()

        widget = QTabWidget()

        widget.addTab(self.setup_point_intersections_tab(), "Processing")
        widget.addTab(self.setup_help_tab(), "Help")
        
        layout.addWidget(widget)

        self.setLayout(layout)
        self.adjustSize()

        self.setWindowTitle(f'{plugin_name} - {self.module_name}')

    def setup_point_intersections_tab(self):
        
        widget = QWidget()

        layout = QVBoxLayout()
        layout.addWidget(self.setup_analysis_parameters_loading())
        layout.addWidget(self.setup_processing_tabs())

        widget.setLayout(layout)
        return widget

    def setup_analysis_parameters_loading(self):

        widget = QWidget()

        layout = QGridLayout()

        # load geographical and geological parameters

        layout.addWidget(QLabel(self.tr("Load analysis parameters (plane source point and orientation) from file:")), 2, 0, 1, 2)

        input_file_browser = QPushButton(".....")
        input_file_browser.clicked.connect(self.load_analysis_parameters)
        layout.addWidget(input_file_browser, 2, 2, 1, 1)

        widget.setLayout(layout)
        return widget

    def setup_processing_tabs(self):

        widget = QWidget()

        layout = QVBoxLayout()

        toolbox = QToolBox()
        toolbox.addItem(self.setup_source_point_parameters_tab(), 'Plane -> source point definition')
        toolbox.addItem(self.setup_plane_orientation_parameters_tab(), 'Plane -> orientation definition')
        toolbox.addItem(self.setup_calculations_distances_from_dem_tab(), 'Calculate distances of DEM cell centers from plane')
        toolbox.addItem(self.setup_calculations_points_distances_tab(), 'Calculate distances of points from plane')
        toolbox.addItem(self.setup_export_geoparameters_tab(), 'Export analysis parameters')

        layout.addWidget(toolbox)
        widget.setLayout(layout)

        return widget

    def setup_help_tab(self):
        
        widget = QWidget()
        layout = QVBoxLayout()

        # About section

        text_browser = QTextBrowser(widget)

        url_path = f"file:///{self.plugin_folder}/help/help_pts_plane_dists.html"
        text_browser.setSource(QUrl(url_path))
        text_browser.setSearchPaths([f'{self.plugin_folder}/help/images/di'])

        layout.addWidget(text_browser)

        widget.setLayout(layout)

        return widget

    def setup_source_point_parameters_tab(self):
        
        widget = QWidget()

        layout = QGridLayout()

        self.set_source_point_in_map_QPushButton = QPushButton("Set source point in map")
        self.set_source_point_in_map_QPushButton.clicked[bool].connect(self.set_source_point_in_map)
        layout.addWidget(self.set_source_point_in_map_QPushButton, 0, 0, 1, 3)

        self.lock_z_value_to_dem_surface_QCheckBox = QCheckBox("Get elevation from DEM ->")
        self.lock_z_value_to_dem_surface_QCheckBox.setChecked(False)
        self.lock_z_value_to_dem_surface_QCheckBox.stateChanged[int].connect(self.update_source_point_elevation_value)
        layout.addWidget(self.lock_z_value_to_dem_surface_QCheckBox, 1, 0, 1, 1)

        self.choose_source_dem_QComboBox = QComboBox()
        self.choose_source_dem_QComboBox.addItem(OPTIONAL_TEXT)
        layout.addWidget(self.choose_source_dem_QComboBox, 1, 1, 1, 2)

        self.refresh_raster_layer_list_for_elevation_QPushButton = QPushButton("Update raster layer list")
        self.refresh_raster_layer_list_for_elevation_QPushButton.clicked[bool].connect(self.refresh_raster_layer_list_for_elevation)
        self.refresh_raster_layer_list_for_elevation_QPushButton.setEnabled(True)
        layout.addWidget(self.refresh_raster_layer_list_for_elevation_QPushButton, 2, 0, 1, 3)

        self.refresh_raster_layer_list_for_elevation()

        layout.addWidget(QLabel("Longitude (EPSG: 4326)"), 3, 0, 1, 1)
        self.source_point_longitude_QLineEdit = QLineEdit()
        self.source_point_longitude_QLineEdit.textEdited.connect(self.update_source_point_location)
        layout.addWidget(self.source_point_longitude_QLineEdit, 3, 1, 1, 2)
              
        layout.addWidget(QLabel("Latitude (EPSG: 4326)"), 4, 0, 1, 1)
        self.source_point_latitude_QLineEdit = QLineEdit()
        self.source_point_latitude_QLineEdit.textEdited.connect(self.update_source_point_location)
        layout.addWidget(self.source_point_latitude_QLineEdit, 4, 1, 1, 2)
                      
        layout.addWidget(QLabel("Elevation"), 5, 0, 1, 1)
        self.source_point_elevation_QLineEdit = QLineEdit()
        self.source_point_elevation_QLineEdit.textEdited.connect(self.check_elevation_congruence_with_dem)
        layout.addWidget(self.source_point_elevation_QLineEdit, 5, 1, 1, 2)

        self.reset_source_point_in_map_QPushButton = QPushButton("Reset source point")
        self.reset_source_point_in_map_QPushButton.clicked[bool].connect(self.reset_source_point_parameters)
        layout.addWidget(self.reset_source_point_in_map_QPushButton, 6, 0, 1, 3)

        widget.setLayout(layout)
        
        return widget

    def store_current_map_crs(self):

        self.map_crs = self.canvas.mapSettings().destinationCrs()

    def get_current_map_crs(self):

        return self.canvas.mapSettings().destinationCrs()

    def check_elevation_congruence_with_dem(self):
        
        if self.use_elevation_from_dem and float(self.source_point_elevation_QLineEdit.text()) != self.current_source_point_elevation_value:
            self.use_elevation_from_dem = False
            self.lock_z_value_to_dem_surface_QCheckBox.setChecked(False)
            
        self.current_source_point_elevation_value = float(self.source_point_elevation_QLineEdit.text())

    def setup_plane_orientation_parameters_tab(self):

        widget = QWidget()
        layout = QGridLayout()

        layout.addWidget(QLabel("Input dip azimuth is relative to:"), 0, 0, 1, 3)

        self.direction_azimuth_is_relative_to_geographic_north_QRadioButton = QRadioButton("Geographic North")
        self.direction_azimuth_is_relative_to_geographic_north_QRadioButton.setChecked(True)
        layout.addWidget(self.direction_azimuth_is_relative_to_geographic_north_QRadioButton, 1, 1, 1, 1)

        self.direction_azimuth_is_relative_to_map_top_QRadioButton = QRadioButton("Current map top (Y axis)")
        layout.addWidget(self.direction_azimuth_is_relative_to_map_top_QRadioButton, 1, 2, 1, 1)

        dip_dir_label = QLabel("Dip direction")
        dip_dir_label.setAlignment (Qt.AlignCenter)       
        layout.addWidget(dip_dir_label, 2, 0, 1, 2)

        dip_ang_label = QLabel("Dip angle")
        dip_ang_label.setAlignment(Qt.AlignCenter)       
        layout.addWidget(dip_ang_label, 2, 2, 1, 1)
        
        self.dip_direction_QDial = QDial()
        self.dip_direction_QDial.setRange(2, 360)
        self.dip_direction_QDial.setPageStep(1)
        self.dip_direction_QDial.setProperty("value", 180)
        self.dip_direction_QDial.setSliderPosition(180)
        self.dip_direction_QDial.setTracking(True)
        self.dip_direction_QDial.setOrientation(Qt.Vertical)
        self.dip_direction_QDial.setWrapping(True)
        self.dip_direction_QDial.setNotchTarget(30.0)
        self.dip_direction_QDial.setNotchesVisible(True)
        self.dip_direction_QDial.valueChanged[int].connect(self.update_dip_direction_spinbox)
        layout.addWidget(self.dip_direction_QDial, 3, 0, 1, 2)
                
        self.DAngle_verticalSlider = QSlider()
        self.DAngle_verticalSlider.setRange(0, 90)
        self.DAngle_verticalSlider.setProperty("value", 45)
        self.DAngle_verticalSlider.setOrientation(Qt.Vertical)
        self.DAngle_verticalSlider.setInvertedAppearance(True)
        self.DAngle_verticalSlider.setTickPosition(QSlider.TicksBelow)
        self.DAngle_verticalSlider.setTickInterval(15)
        self.DAngle_verticalSlider.valueChanged[int].connect(self.update_dip_angle_spinbox)
        layout.addWidget(self.DAngle_verticalSlider, 3, 2, 1, 1)

        self.dip_direction_QDoubleSpinBox = QDoubleSpinBox()
        self.dip_direction_QDoubleSpinBox.setRange(0.0, 359.999)
        self.dip_direction_QDoubleSpinBox.setDecimals(3)
        self.dip_direction_QDoubleSpinBox.setSingleStep(1.0)
        self.dip_direction_QDoubleSpinBox.valueChanged[float].connect(self.update_dip_direction_slider)
        layout.addWidget(self.dip_direction_QDoubleSpinBox, 4, 0, 1, 2)
         
        self.dip_angle_QDoubleSpinBox = QDoubleSpinBox()
        self.dip_angle_QDoubleSpinBox.setRange(0.0, 90.0)
        self.dip_angle_QDoubleSpinBox.setDecimals(3)
        self.dip_angle_QDoubleSpinBox.setSingleStep(0.1)
        self.dip_angle_QDoubleSpinBox.setProperty("value", 45)
        self.dip_angle_QDoubleSpinBox.valueChanged[float].connect(self.update_dip_angle_slider)
        layout.addWidget(self.dip_angle_QDoubleSpinBox, 4, 2, 1, 1)

        widget.setLayout(layout)

        return widget
        
    def setup_calculations_distances_from_dem_tab(self):

        widget = QWidget()
        layout = QGridLayout()

        layout.addWidget(QLabel("Use DEM"), 0, 0, 1, 1)

        self.choose_distances_dem_QComboBox = QComboBox()
        self.choose_distances_dem_QComboBox.addItem(REQUIRED_TEXT)
        layout.addWidget(self.choose_distances_dem_QComboBox, 0, 1, 1, 2)

        self.refresh_raster_layer_list_for_distances_QPushButton = QPushButton("Update raster layer list")
        self.refresh_raster_layer_list_for_distances_QPushButton.clicked[bool].connect(self.refresh_raster_layer_list_for_distances)
        self.refresh_raster_layer_list_for_distances_QPushButton.setEnabled(True)
        layout.addWidget(self.refresh_raster_layer_list_for_distances_QPushButton, 0, 3, 1, 1)

        self.refresh_raster_layer_list_for_distances()

        layout.addWidget(QLabel("Upper distance threshold (positive: above plane)"), 1, 0, 1, 3)

        self.above_thresold_QDoubleSpinBox = QDoubleSpinBox()
        self.above_thresold_QDoubleSpinBox.setMinimum(-1000.0)
        self.above_thresold_QDoubleSpinBox.setMaximum(1000.0)
        self.above_thresold_QDoubleSpinBox.setValue(100.0)
        layout.addWidget(self.above_thresold_QDoubleSpinBox, 1, 3, 1, 1)

        layout.addWidget(QLabel("Lower distance threshold (negative: below plane)"), 2, 0, 1, 3)

        self.lower_threshold_QDoubleSpinBox = QDoubleSpinBox()
        self.lower_threshold_QDoubleSpinBox.setMinimum(-1000.0)
        self.lower_threshold_QDoubleSpinBox.setMaximum(1000.0)
        self.lower_threshold_QDoubleSpinBox.setValue(-100.0)
        layout.addWidget(self.lower_threshold_QDoubleSpinBox, 2, 3, 1, 1)

        layout.addWidget(QLabel(self.tr("Save results in")), 3, 0, 1, 1)

        self.output_grid_path_input_QLineEdit = QLineEdit()
        layout.addWidget(self.output_grid_path_input_QLineEdit, 3, 1, 1, 1)

        output_browser = QPushButton(".....")
        output_browser.clicked.connect(self.select_output_grid_file)
        layout.addWidget(output_browser, 3, 2, 1, 1)

        self.load_output_grid_in_map_QCheckBox = QCheckBox("Load in map")
        layout.addWidget(self.load_output_grid_in_map_QCheckBox, 3, 3, 1, 1)

        self.calculate_distance_grid_QPushButton = QPushButton("Calculate distances grid")
        self.calculate_distance_grid_QPushButton.clicked[bool].connect(self.calculate_distances_of_dem_centers_from_plane)
        layout.addWidget(self.calculate_distance_grid_QPushButton, 4, 0, 1, 4)

        # final setup

        widget.setLayout(layout)

        return widget

    def setup_calculations_points_distances_tab(self):

        widget = QWidget()

        layout = QGridLayout()

        # input layer

        input_point_layer_QGroupBox = QGroupBox("Input point layer")
        input_point_layer_QGridLayout = QGridLayout()

        self.input_point_layer_QComboBox = QComboBox()
        input_point_layer_QGridLayout.addWidget(self.input_point_layer_QComboBox, 0, 0, 1, 2)

        input_point_layer_QGroupBox.setLayout(input_point_layer_QGridLayout)
        layout.addWidget(input_point_layer_QGroupBox)

        # plane values

        input_fields_QGroupBox = QGroupBox("Point layer source fields")
        input_fields_QGridLayout = QGridLayout()

        input_fields_QGridLayout.addWidget(QLabel("id"), 0, 0, 1, 1)
        self.id_field_QComboBox = QComboBox()
        input_fields_QGridLayout.addWidget(self.id_field_QComboBox, 0, 1, 1, 1)

        input_fields_QGridLayout.addWidget(QLabel("z"), 1, 0, 1, 1)
        self.z_field_QComboBox = QComboBox()
        input_fields_QGridLayout.addWidget(self.z_field_QComboBox, 1, 1, 1, 1)

        self.input_fields_QComboBoxes = [
            self.id_field_QComboBox,
            self.z_field_QComboBox]

        self.refresh_struct_point_lyr_combobox()

        self.input_point_layer_QComboBox.currentIndexChanged[int].connect(self.refresh_structural_fields_comboboxes)
        # from https://github.com/qgis/QGIS/issues/41135
        project = QgsProject.instance()
        project.layersAdded.connect(self.refresh_struct_point_lyr_combobox)
        project.layersRemoved.connect(self.refresh_struct_point_lyr_combobox)

        input_fields_QGroupBox.setLayout(input_fields_QGridLayout)
        layout.addWidget(input_fields_QGroupBox)

        # output layer

        output_layer_QGroupBox = QGroupBox("Output point layer")
        output_layer_QGridLayout = QGridLayout()

        self.output_layer_filepath_QLineEdit = QLineEdit()
        output_layer_QGridLayout.addWidget(self.output_layer_filepath_QLineEdit, 0, 0, 1, 1)

        self.define_output_filepath_QPushButton = QPushButton(".....")
        self.define_output_filepath_QPushButton.clicked.connect(self.define_output_vector_file)
        output_layer_QGridLayout.addWidget(self.define_output_filepath_QPushButton, 0, 1, 1, 1)

        self.load_output_shapefile_in_map_QCheckBox = QCheckBox("Load in map")
        output_layer_QGridLayout.addWidget(self.load_output_shapefile_in_map_QCheckBox, 0, 2, 1, 1)

        self.calculate_points_distances_QPushButton = QPushButton("Calculate distances")
        self.calculate_points_distances_QPushButton.clicked.connect(self.calculate_distances_of_points_from_plane)
        output_layer_QGridLayout.addWidget(self.calculate_points_distances_QPushButton, 1, 0, 1, 3)

        output_layer_QGroupBox.setLayout(output_layer_QGridLayout)
        layout.addWidget(output_layer_QGroupBox)

        widget.setLayout(layout)

        return widget


    def refresh_struct_point_lyr_combobox(self):

        self.loaded_point_layers = loaded_point_layers()
        self.input_point_layer_QComboBox.clear()

        self.input_point_layer_QComboBox.addItem(CHOOSE_TEXT)
        self.input_point_layer_QComboBox.addItems([layer.name() for layer in self.loaded_point_layers])

        self.reset_structural_field_comboboxes()

    def reset_structural_field_comboboxes(self):

        for structural_combox in self.input_fields_QComboBoxes:
            structural_combox.clear()
            structural_combox.addItem(UNDEFINED_TEXT)

    def refresh_structural_fields_comboboxes(self):

        self.reset_structural_field_comboboxes()

        point_shape_qgis_ndx = self.input_point_layer_QComboBox.currentIndex() - 1
        if point_shape_qgis_ndx <= -1:
            return

        self.point_layer = self.loaded_point_layers[point_shape_qgis_ndx]

        point_layer_field_list = self.point_layer.dataProvider().fields().toList()

        field_names = [field.name() for field in point_layer_field_list]

        for structural_combox in self.input_fields_QComboBoxes:
            structural_combox.addItems(field_names)

    def define_output_vector_file(self):

        output_filename, __ = QFileDialog.getSaveFileName(self,
                                                      self.tr("Save shapefile"),
                                                      "*.shp",
                                                      "shp (*.shp *.SHP)")
        if not output_filename:
            return
        self.output_layer_filepath_QLineEdit.setText(output_filename)

    def calculate_distances_of_points_from_plane(self):
        """
        Calculate distances of input layer points from geological plane.
        """

        try:

            # check validity of geographical and geological input parameters

            err_msg = self.check_input_source_point_parameters()

            if err_msg:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    err_msg
                )
                return

            # extracting plane parameters (source point plus plane orientation)

            current_plane_parameters = self.extract_current_plane_parameters()

            if isinstance(current_plane_parameters, str):
                warn_qgis(
                    self.module_name,
                    current_plane_parameters
                )
                return

            source_point_longitude, \
            source_point_latitude, \
            source_point_elevation, \
            dip_direction_as_north_azimuth, \
            source_dip_angle = current_plane_parameters

            # process input parameters

            map_crs = self.get_current_map_crs()

            result, err = reorient_plane_and_source_point(
                source_point_longitude=source_point_longitude,
                source_point_latitude=source_point_latitude,
                source_point_elevation=source_point_elevation,
                dip_direction_as_north_azimuth=dip_direction_as_north_azimuth,
                source_dip_angle=source_dip_angle,
                dest_crs=map_crs,
            )

            if err:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    f"{err!r}",
                )
                return

            map_crs_plane_orientation, map_crs_src_pt_3d = result

            map_crs_cartesian_plane = CPlane3D.from_geol_plane(
                geol_plane=map_crs_plane_orientation,
                pt=map_crs_src_pt_3d
            )

            # Extracting point layer parameters

            point_shape_qgis_ndx = self.input_point_layer_QComboBox.currentIndex() - 1
            if point_shape_qgis_ndx <= -1:
                warn_qgis(
                    self.module_name,
                    "Input point layer is not defined"
                )
                return

            point_layer = self.loaded_point_layers[point_shape_qgis_ndx]

            if not point_layer:
                warn_qgis(
                    self.module_name,
                    "Input point layer is not defined"
                )
                return

            id_field_name = self.id_field_QComboBox.currentText()
            if id_field_name == UNDEFINED_TEXT:
                warn_qgis(
                    self.module_name,
                    "Input id field is not defined"
                )
                return

            z_field_name = self.z_field_QComboBox.currentText()
            if z_field_name == UNDEFINED_TEXT:
                warn_qgis(
                    self.module_name,
                    "Input z field is not defined"
                )
                return

            # get input point layer data

            input_points_layer_data, err = extract_point_layer_geometries_and_attributes(
                layer=point_layer,
                fields=[z_field_name, id_field_name]
            )

            if err:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    f"{err!r}",
                )
                return

            len_data = len(input_points_layer_data)

            if len_data == 0:
                warn_qgis(
                    self.module_name,
                    "No data to calculate"
                )
                return

            # Calculates results

            point_attributes = []
            geographic_point_coordinates = []

            point_layer_crs = point_layer.crs()

            for input_point_record in input_points_layer_data:

                point_layer_crs_input_pt_x, point_layer_crs_input_pt_y, input_pt_z, id_of_input_point = input_point_record

                result, err = qgis_project_coords(
                    x=point_layer_crs_input_pt_x,
                    y=point_layer_crs_input_pt_y,
                    src_crs=point_layer_crs,
                    dest_crs=map_crs
                )

                if err:
                    QMessageBox.critical(
                        self,
                        self.module_name,
                        f"{err!r}",
                    )
                    return

                map_crs_input_pt_x, map_crs_input_pt_y = result

                map_crs_input_pt = Point(
                    map_crs_input_pt_x,
                    map_crs_input_pt_y,
                    input_pt_z
                )

                """20230917 no longer needed
                map_crs_plane_projected_pt, err = map_crs_cartesian_plane.project_point(map_crs_input_pt)
                if err:
                    warn_qgis(
                        self.module_name,
                        f"{err!r}"
                    )
                    return
                """
                #map_crs_plane_projected_pt_x, map_crs_plane_projected_pt_y, map_crs_plane_projected_pt_z = map_crs_plane_projected_pt.xyz()

                signed_distance_of_input_point_from_plane = map_crs_cartesian_plane.signed_distance_to_point(map_crs_input_pt)

                """20230917 no longer calculated
                vector_from_source_point_to_input_point = Segment(
                    start_pt=map_crs_src_pt_3d,
                    end_pt=map_crs_input_pt
                ).as_vector2d()

                vector_from_source_point_to_plane_projected_point = Segment(
                    start_pt=map_crs_src_pt_3d,
                    end_pt=map_crs_plane_projected_pt
                ).as_vector2d()

                distance_from_source_point_to_input_point = vector_from_source_point_to_input_point.length
                distance_from_source_point_to_plane_projected_point = vector_from_source_point_to_plane_projected_point.length

                azimuth_of_vector_from_source_point_to_input_point = vector_from_source_point_to_input_point.azimuth_degr()
                azimuth_of_vector_from_source_point_to_plane_projected_point = vector_from_source_point_to_plane_projected_point.azimuth_degr()

                angle_between_the_source_point_vectors = vector_from_source_point_to_input_point.angle_with(vector_from_source_point_to_plane_projected_point)
                """

                result, err = qgis_project_coords(
                    x=point_layer_crs_input_pt_x,
                    y=point_layer_crs_input_pt_y,
                    src_crs=point_layer_crs,
                )

                if err:
                    QMessageBox.critical(
                        self,
                        self.module_name,
                        f"{err!r}",
                    )
                    return

                long, lat = result

                vals = (
                    id_of_input_point,
                    long,
                    lat,
                    input_pt_z,
                    #map_crs_plane_projected_pt_x,
                    #map_crs_plane_projected_pt_y,
                    #map_crs_plane_projected_pt_z,
                    signed_distance_of_input_point_from_plane,
                    #distance_from_source_point_to_input_point,
                    #azimuth_of_vector_from_source_point_to_input_point,
                    #distance_from_source_point_to_plane_projected_point,
                    #azimuth_of_vector_from_source_point_to_plane_projected_point,
                    #angle_between_the_source_point_vectors
                )

                point_attributes.append(vals)
                geographic_point_coordinates.append((long, lat, input_pt_z))

            field_names = [f["name"] for f in field_parameters_of_output_table]

            output_shape_pth = self.output_layer_filepath_QLineEdit.text()
            if not output_shape_pth:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    f"Output shapefile path is not defined",
                )
                return

            result, err = create_and_open_shapefile(
                path=output_shape_pth,
                geom_type=ogr.wkbPoint25D,
                fields_dict_list=field_parameters_of_output_table,
                crs=4326,
            )

            if err:
                if not output_shape_pth:
                    QMessageBox.critical(
                        self,
                        self.module_name,
                        f"{err!r}",
                    )
                    return

            shapefile_datasource, point_shapelayer = result

            print(f"Shapefile {output_shape_pth} created")

            success, msg = try_write_pt_shapefile(
                point_layer=point_shapelayer,
                geoms=geographic_point_coordinates,
                field_names=field_names,
                attrs=point_attributes)

            if not success:
                warn_qgis(
                    self.module_name,
                    msg
                )
                return

            if self.load_output_shapefile_in_map_QCheckBox.isChecked():
                print(f"Loading distances vector layer")

                try:

                    distances_point_layer = QgsVectorLayer(
                        output_shape_pth,
                        QFileInfo(output_shape_pth).baseName()
                    )
                    QgsProject.instance().addMapLayer(distances_point_layer)

                    if distances_point_layer.isValid():
                        print("Point layer was loaded successfully")
                    else:
                        QMessageBox.critical(
                            self,
                            self.module_name,
                            "Unable to read basename and file path.\nOutput string is possibly invalid"
                        )

                except Exception as e:

                    QMessageBox.critical(
                        self,
                        self.module_name,
                        f"Unable to load point layer in project:\n{e!r}"
                    )
                    return
            else:
                print(f"self.load_output_shapefile_in_map_QCheckBox.isChecked() = > FALSE")

            QMessageBox.information(
                self,
                self.module_name,
                "Distance point layer created"
            )

        except Exception as e:

            QMessageBox.critical(
                self,
                self.module_name,
                f"Exception while calculating points distance:\n{e!r}")
            return

    def setup_export_geoparameters_tab(self):

        widget = QWidget()

        layout = QGridLayout()

        self.export_analysis_parameters_QPushButton = QPushButton("Export analysis parameters")
        self.export_analysis_parameters_QPushButton.clicked.connect(self.export_analysis_parameters)
        layout.addWidget(self.export_analysis_parameters_QPushButton, 0, 0, 1, 1)

        self.export_params_file_path_QLineEdit = QLineEdit()
        layout.addWidget(self.export_params_file_path_QLineEdit, 0, 1, 1, 2)

        self.define_export_file_path_QPushButton = QPushButton(".....")
        self.define_export_file_path_QPushButton.clicked.connect(self.define_output_params_file_path)
        layout.addWidget(self.define_export_file_path_QPushButton, 0, 3, 1, 1)

        widget.setLayout(layout)

        return widget

    def export_analysis_parameters(self):

        export_file_path = self.export_params_file_path_QLineEdit.text()

        if export_file_path == "":
            QMessageBox.critical(
                self,
                self.module_name,
                f"No export file path has been defined"
            )
            return

        result = self.extract_current_plane_parameters()

        if isinstance(result, str):
            return None, Error(
                True,
                caller_name(),
                Exception(result),
                traceback.format_exc(),
            )

        source_point_longitude = result[0]
        source_point_latitude = result[1]
        source_point_elevation = result[2]
        dip_direction_as_north_azimuth = result[3]
        source_dip_angle = result[4]

        with open(export_file_path, "w") as ofile:
            ofile.write(f"longitude: {source_point_longitude}\n")
            ofile.write(f"latitude: {source_point_latitude}\n")
            ofile.write(f"elevation: {source_point_elevation}\n")
            ofile.write(f"dip direction azimuth (geographic North): {dip_direction_as_north_azimuth}\n")
            ofile.write(f"dip angle: {source_dip_angle}")

        QMessageBox.information(
            self,
            self.module_name,
            f"Processing parameters exported as file '{export_file_path}'"
        )
        return

    def load_analysis_parameters(self):

        try:

            input_file_path, _ = QFileDialog.getOpenFileName(
                self,
                self.tr("Load processing parameters"),
                "*.txt",
                "txt (*.txt *.TXT)"
            )

            if not input_file_path:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    "No defined file path"
                )
                return

            with open(input_file_path, "r") as ifile:
                raw_params = ifile.readlines()

            params = list(map(lambda val: val.split(":")[-1].strip(), raw_params))

            self.lock_z_value_to_dem_surface_QCheckBox.setChecked(False)

            self.source_point_longitude_QLineEdit.setText(params[0])
            self.source_point_longitude = float(params[0])
            self.source_point_latitude_QLineEdit.setText(params[1])
            self.source_point_latitude = float(params[1])
            self.source_point_elevation_QLineEdit.setText(params[2])
            self.source_point_elevation = float(params[2])

            self.direction_azimuth_is_relative_to_geographic_north_QRadioButton.setChecked(True)
            self.dip_direction_QDoubleSpinBox.setValue(float(params[3]))
            self.dip_direction_as_north_azimuth = float(params[3])
            self.dip_angle_QDoubleSpinBox.setValue(float(params[4]))
            self.source_dip_angle = float(params[4])

            self.update_source_point_location()

            QMessageBox.information(
                self,
                self.module_name,
                "Parameters loaded"
            )
            return

        except Exception as e:

            QMessageBox.critical(
                self,
                self.module_name,
                f"Error while reading input processing parameters from file:\n{e!r}"
            )
            return

    def set_source_point_in_map(self):
        
        try:
            self.map_tool.canvasClicked.disconnect(self.update_source_point_location)
        except:
            pass            
         
        self.store_current_map_crs()

        # mouse listener
        self.map_tool = EmitPointFromCanvas(
            self.canvas,
            self.plugin
        )

        # save the standard map tool for restoring it at the end
        self.previous_tool = self.canvas.mapTool()
        self.map_tool.canvasClicked.connect(self.update_source_point_location)
        self.map_tool.setCursor(Qt.CrossCursor)
        self.canvas.setMapTool(self.map_tool)

    def get_chosen_dem(self, ndx_dem_file = 0):

        self.qgis_grid_layer = None
        self.reset_post_dem_change()
        if self.qgis_raster_layers is None or len(self.qgis_raster_layers) == 0:
            return          
                                
        # no DEM layer defined  
        if ndx_dem_file == 0:
            return             

        self.qgis_grid_layer = self.qgis_raster_layers[ndx_dem_file - 1]

        dem_src = self.qgis_grid_layer.source()

        results, err = read_raster_band_with_projection(
            raster_source=dem_src)

        if err:
            QMessageBox.critical(
                self,
                self.module_name,
                f"Exception while reading DEM content:\n{e!r}"
            )
            return

        geotransform, epsg_code, band_params, data, dem_projection = results

        self.elevation_grid = Grid(
            array=data,
            geotransform=geotransform,
            epsg_code=epsg_code,
            projection=dem_projection
        )

        self.set_source_point_in_map_QPushButton.setEnabled(True)
        self.reset_source_point_in_map_QPushButton.setEnabled(True)

    def create_marker(
        self,
            canvas,
            prj_crs_x,
            prj_crs_y,
            pen_width=2,
            icon_type=2,
            icon_size=18,
            icon_color: Union[str, QColor] ='limegreen'
    ) -> QgsVertexMarker:
        
        marker = QgsVertexMarker(canvas)
        marker.setIconType(icon_type)
        marker.setIconSize(icon_size)
        marker.setPenWidth(pen_width)
        if isinstance(icon_color, QColor):
            color = icon_color
        else:
            color = QColor(icon_color)
        marker.setColor(color)
        marker.setCenter(QgsPointXY(prj_crs_x, prj_crs_y))

        return marker        

    def update_marker_in_canvas(self) -> bool:

        try:

            self.remove_intersection_markers_from_canvas()
            self.remove_source_point_marker_from_canvas()

            if self.source_point_longitude is not None and isfinite(self.source_point_longitude) and \
                    self.source_point_latitude is not None and isfinite(self.source_point_latitude):
                color = QColor("blue")
                source_pt_x, source_pt_y = qgs_project_xy(
                    x=self.source_point_longitude,
                    y=self.source_point_latitude,
                    dest_crs=self.map_crs,
                )
                source_point_marker = self.create_marker(
                    self.canvas,
                    source_pt_x,
                    source_pt_y,
                    icon_type=1,
                    icon_color=color)

                self.source_point_marker_list = [source_point_marker]

            self.canvas.refresh()

            return False

        except Exception as e:

            QMessageBox.critical(
                self,
                self.module_name,
                f"Exception while updating marker:\n{err!r}"
            )

            return True

    def update_source_point_location(
        self,
        qgs_point=None,
    ):

        try:

            self.reset_source_point_in_map_QPushButton.setEnabled(True)

            if self.sender() == self.map_tool:

                self.source_point_longitude, self.source_point_latitude = qgs_project_xy(
                    x=qgs_point.x(),
                    y=qgs_point.y(),
                    src_crs=self.map_crs,
                )

                self.source_point_longitude_QLineEdit.setText(str(self.source_point_longitude))
                self.source_point_latitude_QLineEdit.setText(str(self.source_point_latitude))

            elif self.sender() == self.source_point_longitude_QLineEdit:

                if self.source_point_longitude_QLineEdit.text() == '':
                    QMessageBox.critical(
                        self,
                        self.module_name,
                        "Error: longitude value is undefined"
                    )
                    return

                try:

                    self.source_point_longitude = float(self.source_point_longitude_QLineEdit.text())

                except Exception as e:

                    QMessageBox.critical(
                        self,
                        self.module_name,
                        f"Longitude value is not correctly defined:\n{e!r}"
                    )
                    return

            elif self.sender() == self.source_point_latitude_QLineEdit:

                if self.source_point_latitude_QLineEdit.text() == '':
                    QMessageBox.critical(
                        self,
                        self.module_name,
                        "Error: latitude value is undefined"
                    )
                    return

                try:

                    self.source_point_latitude = float(self.source_point_latitude_QLineEdit.text())

                except Exception as e:
                    QMessageBox.critical(
                        self,
                        self.module_name,
                        f"Latitude value is not correctly defined:\n{e!r}"
                    )
                    return

            if self.lock_z_value_to_dem_surface_QCheckBox.isChecked():

                elevation_value_from_dem = self.update_source_point_elevation_value()

                if elevation_value_from_dem is None:
                    self.current_source_point_elevation_value = None
                    self.source_point_elevation_QLineEdit.setText("")
                else:
                    self.current_source_point_elevation_value = elevation_value_from_dem
                    self.source_point_elevation_QLineEdit.setText(str(self.current_source_point_elevation_value))
                    self.use_elevation_from_dem = True

            _ = self.update_marker_in_canvas()

        except Exception as e:

            QMessageBox.critical(
                self,
                self.module_name,
                f"Exception while updating source point location:\n{err!r}"
            )
            return

    def update_source_point_elevation_value (self):
        """
        Update z value.
        
        """

        try:

            # does nothing when the height source is not from DEM

            if not self.lock_z_value_to_dem_surface_QCheckBox.isChecked():
                return None

            # prevent action when the DEM is not read

            if self.elevation_grid is None:
                return None

            # get z value from ptLyr

            if self.source_point_longitude is None or self.source_point_latitude is None:
                return None

            srcpt_demcrs_x, srcpt_demcrs_y = qgs_project_xy(
                x=self.source_point_longitude,
                y=self.source_point_latitude,
                dest_crs=self.qgis_grid_layer.crs(),
            )

            # return None or a numeric value
            return self.elevation_grid.interpolate_bilinear(
                srcpt_demcrs_x,
                srcpt_demcrs_y)

        except Exception as e:

            QMessageBox.critical(
                self,
                self.module_name,
                f"Exception while updating z value:\n{err!r}"
            )

            return

    def restore_previous_map_tool(self,
                                  map_tool):

        if map_tool:
            self.canvas.unsetMapTool(map_tool)

        if self.previous_tool:
            self.canvas.setMapTool(self.previous_tool)

    def clear_source_point_qlineedit_forms(self):
        
        for qlineedit_form in (
                self.source_point_longitude_QLineEdit,
                self.source_point_latitude_QLineEdit,
                self.source_point_elevation_QLineEdit):

            qlineedit_form.clear()

    def update_dip_direction_slider(self):
        """
        Update the value of the dip direction in the slider.""
        """        
        transformed_dipdirection = self.dip_direction_QDoubleSpinBox.value() + 180.0
        if transformed_dipdirection > 360.0:
            transformed_dipdirection -= 360.0
        self.dip_direction_QDial.setValue(int(transformed_dipdirection))

    def update_dip_direction_spinbox(self):
        """
        Update the value of the dip direction in the spinbox.
        """        
        real_dipdirection = self.dip_direction_QDial.value() - 180.0
        if real_dipdirection < 0.0:
            real_dipdirection += 360.0
        self.dip_direction_QDoubleSpinBox.setValue(real_dipdirection)

    def update_dip_angle_slider(self):
        """
        Update the value of the dip angle in the slider.
        """
        
        self.DAngle_verticalSlider.setValue(int(self.dip_angle_QDoubleSpinBox.value()))

    def update_dip_angle_spinbox(self):
        """
        Update the value of the dip angle in the spinbox.
        """        
        
        self.dip_angle_QDoubleSpinBox.setValue(self.DAngle_verticalSlider.value())

    def check_input_source_point_parameters(self
                                            ) -> str:

        # check if all input data are correct

        if self.source_point_longitude_QLineEdit.text() == '':
            return "Define the longitude of the source point in 'Geographic parameters' section"

        if self.source_point_latitude_QLineEdit.text() == '':
            return "Define the latitude of the source point in 'Geographic parameters' section"

        if self.source_point_elevation_QLineEdit.text() == '':
            return "Define the elevation of the source point in 'Geographic parameters' section"

        return ''

    def extract_current_plane_parameters(self) -> Union[str, tuple]:

        try:

            source_point_longitude = float(self.source_point_longitude_QLineEdit.text())
            source_point_latitude = float(self.source_point_latitude_QLineEdit.text())
            source_point_elevation = float(self.source_point_elevation_QLineEdit.text())
            source_dip_direction = self.dip_direction_QDoubleSpinBox.value()
            source_dip_angle = self.dip_angle_QDoubleSpinBox.value()
            direction_azimuth_is_relative_to_map = self.direction_azimuth_is_relative_to_map_top_QRadioButton.isChecked()

            # Calculates dip direction correction with respect to map CRS top (y-axis) direction

            if direction_azimuth_is_relative_to_map:

                geographic_north_direction_azimuth_cw, err = calculate_local_geographic_north_azimuth_from_epsg_4326(
                    longitude=source_point_longitude,
                    latitude=source_point_latitude,
                    planar_qgis_crs=self.map_crs,
                )

                if err:
                    return None, err

                print(f"Azimuth correction (degrees, clockwise): {geographic_north_direction_azimuth_cw}")

                dip_direction_as_north_azimuth = (source_dip_direction - geographic_north_direction_azimuth_cw) % 360.0

            else:

                dip_direction_as_north_azimuth = source_dip_direction % 360.0

            print(f"North-related plane orientation: dip dir. = {dip_direction_as_north_azimuth}, dip ang. = {source_dip_angle}")

            return (
                source_point_longitude,
                source_point_latitude,
                source_point_elevation,
                dip_direction_as_north_azimuth,
                source_dip_angle,
            )

        except Exception as e:

            return f"Exception while extracting analysis parameters:\n{e!r}"


    def preprocess_plane_and_source_point(
        self,
        dest_crs: 'QGIS_CRS',
    ) -> Tuple[Union[type(None), Tuple[Plane, Point]], Error]:

        try:

            result = self.extract_current_plane_parameters()

            if isinstance(result, str):
                return None, Error(
                    True,
                    caller_name(),
                    Exception(result),
                    traceback.format_exc(),
                )

            source_point_longitude = result[0]
            source_point_latitude = result[1]
            source_point_elevation = result[2]
            dip_direction_as_north_azimuth = result[3]
            source_dip_angle = result[4]

            # Correct dip direction and angle by DEM crs

            dest_crs_plane_orientation, err = convert_plane_orientation_to_crs(
                dip_direction_as_north_azimuth=dip_direction_as_north_azimuth,
                source_dip_angle=source_dip_angle,
                local_pt_longitude=source_point_longitude,
                local_pt_latitude=source_point_latitude,
                dest_crs=dest_crs,
            )

            if err:
                return None, err

            dest_crs_src_pt_x, dest_crs_src_pt_y = qgs_project_xy(
                x=source_point_longitude,
                y=source_point_latitude,
                dest_crs=dest_crs,
            )

            dest_crs_src_pt_3d = Point(
                dest_crs_src_pt_x,
                dest_crs_src_pt_y,
                source_point_elevation,
            )

            return (
                dest_crs_plane_orientation,
                dest_crs_src_pt_3d
            ), Error()

        except Exception as e:

            return None, Error(
                True,
                caller_name(),
                e,
                traceback.format_exc(),
            )

    def get_distances_dem(self) -> Union[str, Tuple[Grid, 'QGIS_CRS']]:

        try:

            if self.qgis_raster_layers_for_elevations is None or len(self.qgis_raster_layers_for_elevations) == 0:
                return "No raster layer is available in current project"

            current_raster_layers_index = self.choose_distances_dem_QComboBox.currentIndex()

            # no DEM layer defined
            if current_raster_layers_index == 0:
                return "No raster layer chosen"

            dem_layer = self.qgis_raster_layers_for_elevations[current_raster_layers_index - 1]
            dem_crs = dem_layer.crs()
            dem_src = dem_layer.source()

            results, err = read_raster_band_with_projection(
                raster_source=dem_src)

            if err:
                return f"Exception while reading DEM file:\n{err!r}"

            geotransform, epsg_code, band_params, data, dem_projection = results

            distances_grid = Grid(
                array=data,
                geotransform=geotransform,
                epsg_code=epsg_code,
                projection=dem_projection
            )

            return distances_grid, dem_crs

        except Exception as e:

            return f"Exception while reading DEM for distances:\n{e!r}"

    def calculate_distances_of_dem_centers_from_plane(self):
        """
        Calculate distances of input DEM cell centers from geological plane.
        """

        try:

            # check validity of geographical and geological input parameters

            err_msg = self.check_input_source_point_parameters()

            if err_msg:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    err_msg
                )
                return

            # extracting plane parameters (source point plus plane orientation)

            current_plane_parameters = self.extract_current_plane_parameters()
            if isinstance(current_plane_parameters, str):
                warn_qgis(
                    self.module_name,
                    current_plane_parameters
                )
                return

            source_point_longitude, \
                source_point_latitude, \
                source_point_elevation, \
                dip_direction_as_north_azimuth, \
                source_dip_angle = current_plane_parameters

            # extract the input DEM for distances calculation

            result = self.get_distances_dem()

            if isinstance(result, str):
                QMessageBox.warning(
                    self,
                    self.module_name,
                    result
                )
                return

            distances_grid, dem_crs = result
            output_grid_path = self.output_grid_path_input_QLineEdit.text()

            # source point

            result, err = self.preprocess_plane_and_source_point(
                dest_crs=dem_crs
            )

            if err:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    f"Exception while preprocessing plane and source point:\n{err!r}"
                )
                return

            geological_plane_in_dem_crs, source_point_in_dem_crrs = result

            # define distance array limits

            below_threshold = float(self.lower_threshold_QDoubleSpinBox.value())
            above_threshold = float(self.above_thresold_QDoubleSpinBox.value())

            if below_threshold >= above_threshold:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    f"Below threshold {below_threshold} is not lower than upper threshold {above_threshold}"
                )
                return

            # populate array with distances from DEM

            cplane = CPlane3D.from_geol_plane(
                geol_plane=geological_plane_in_dem_crs,
                pt=source_point_in_dem_crrs
            )

            arr = distances_grid.array
            num_rows, num_cols = arr.shape
            distances = np.zeros((num_rows, num_cols)) * np.nan

            for i in range(num_rows):
                for j in range(num_cols):
                    val = arr[i, j]
                    if not np.isfinite(val):
                        continue
                    cc_pt_x, cc_pt_y = distances_grid.ijArrToxy(i, j)
                    dem_cell_center = Point(
                        cc_pt_x,
                        cc_pt_y,
                        val
                    )

                    distance = cplane.signed_distance_to_point(dem_cell_center)
                    if below_threshold <= distance <= above_threshold:
                        distances[i, j] = distance

            err = write_geotiff(
                file_path=output_grid_path,
                arr_out=distances,
                geotransform=GeoTransform.to_gdal_geotransform(distances_grid.geotransform),
                projection=distances_grid.projection
            )

            if err:
                QMessageBox.critical(
                    self,
                    self.module_name,
                    f"Exception while writing geotiff:\n{err!r}"
                )
                return

            if self.load_output_grid_in_map_QCheckBox.isChecked():

                try:

                    distances_grid_layer = QgsRasterLayer(
                        output_grid_path,
                        QFileInfo(output_grid_path).baseName()
                    )
                    QgsProject.instance().addMapLayer(distances_grid_layer)

                    if distances_grid_layer.isValid():
                        print("Grid layer was loaded successfully")
                    else:
                        QMessageBox.critical(
                            self,
                            self.module_name,
                            "Unable to read basename and file path.\nOutput string is possibly invalid"
                        )

                except Exception as e:

                    QMessageBox.critical(
                        self,
                        self.module_name,
                        f"Unable to load grid layer in project:\n{e!r}"
                    )
                    return

            QMessageBox.information(
                self,
                self.module_name,
                "Distance grid created"
            )

        except Exception as e:

            QMessageBox.critical(
                self,
                self.module_name,
                f"Exception while calculating distance grid:\n{e!r}")
            return

    def define_output_params_file_path(self):

        output_filename, _ = QFileDialog.getSaveFileName(
            self,
            self.tr("Save processing parameters"),
            "*.txt",
            "txt (*.txt *.TXT)"
        )

        if not output_filename:
            return

        self.export_params_file_path_QLineEdit.setText(output_filename)

    def select_output_grid_file(self):

        output_filename, __ = QFileDialog.getSaveFileName(
            self,
            self.tr("Save grid as geotiff"),
            "*.tif",
            "tif (*.tif *.TIF)"
        )

        if not output_filename:
            return

        self.output_grid_path_input_QLineEdit.setText(output_filename)


