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

 Processing of geological planes and surfaces

                              -------------------
        start                : 2011-12-21
        copyright            : (C) 2011-2026 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.                                   *
 *                                                                         *
 ***************************************************************************/
"""


import numbers

import pickle

from PyQt5 import uic

from qgis.core import (
    QgsCoordinateReferenceSystem,
    QgsCoordinateTransform,
    QgsProject,
    QgsRenderContext
)
context = QgsRenderContext()

from geogst.core.geometries.lines import *
from geogst.core.profiles.project_onto_profiles import *
from geogst.core.profiles.structures import *
from geogst.core.utils.strings import *
from geogst.io.geopackages.geopackage import *
from geogst.io.rasters.gdal import *
from geogst.io.rasters.gdal import *
from geogst.io.vectors.ogr import *
from geogst.plots.profiles import *
from geogst.plots.utils import *

from ..apps.qgis.canvas import *
from ..apps.qgis.lines import *
from ..apps.qgis.messages import *
from ..apps.qgis.polygons import *
from ..apps.qgis.rasters import *
from ..apps.qgis.vectors import *

from ..apps.qt.filesystem import *
from ..apps.qt.messages import *
from ..apps.qt.plots import *
from ..apps.qt.tools import *
from ..apps.qt.colors import *

from ..utils.geoprofiler.export import *

from .config.structures import *
from .config.qt_windows import *


class GeoProfilerWidget(QWidget):

    def __init__(
        self,
        current_directory,
        plugin_name,
        canvas,
    ):

        super(GeoProfilerWidget, self).__init__()

        self.init_basic_params(
            current_directory,
            plugin_name,
            canvas,
        )
        self.init_actions()
        self.init_geostorage()
        self.init_profiles_grid_parameters()
        self.init_profile_lines_parameters()
        self.init_geoprofiles_parameters()
        self.init_graphics_parameters()

    def init_basic_params(
        self,
        current_directory,
        plugin_name,
        canvas,
    ):

        self.plugin_name = plugin_name
        self.canvas = canvas
        self.current_directory = current_directory

    def init_geostorage(self):

        self.storage_geopackage_path = None
        self.storage_layer_name = None

    def init_actions(self):

        self.processings = {
            "Create new geopackage with empty 3D line layer": self.create_geopackage_storage,
            "Create new 3D line layer in existing geopackage": self.create_3d_layer_storage,
            "Append to existing 3D line layer in geopackage": self.load_existing_geopackage_storage,
            "Load trace from 2D line layer": self.define_profile_lines_from_qgis_layer,
            "Digitize 2D trace in canvas": self.define_profile_line_by_rubberband_line,
            "Clear last trace": self.clear_rubberband_line,
            "Clear all traces": self.clear_all_traces,
            "Define DEM as elevation source": self.define_dem_as_elevation_source,
            "Generate 3D profile lines": self.generate_3d_profiles_lines,
            "Choose working 3D profiles layer": self.define_3d_line_layer,
            "Point projection": self.define_point_layer_for_projection,
            "Geological attitude projection": self.define_attitude_layer_for_projection,
            "Line intersection": self.define_line_layer_for_intersection,
            "Polygon intersection": self.define_polygon_layer_for_intersection,
            "Remove all": self.set_geological_parameters_to_none,
            "Remove point projection": self.set_points_projection_parameters_to_none,
            "Remove geological attitude projection": self.set_attitudes_projection_parameters_to_none,
            "Remove line intersection": self.set_lines_intersection_parameters_to_none,
            "Remove polygon intersection": self.set_polygons_intersection_parameters_to_none,
            "Define graphical parameters": self.define_total_graphical_parameters,
            "Save graphical parameters": self.save_graphic_parameters,
            "Load graphical parameters": self.load_graphic_parameters,
            "Plot": self.plot_geoprofiles,
            "Help": self.open_help,
        }

        uic.loadUi(f"{self.current_directory}/ui/GeoProfiler.ui", self)

        self.actions_qtreewidget = self.actionsTreeWidget
        self.actions_qtreewidget.itemDoubleClicked.connect(self.do_processing)

    def init_profile_lines_parameters(self):

        self.newly_defined_profile_baselines = []  # line instances, in the specific CRS, undensified

    def init_profiles_grid_parameters(self):

        self.profiles_grid = None

    def set_points_projection_parameters_to_none(self, warn=True):

        self.points_projection_parameters = None

        if warn:
            warn_qgis(
                self.plugin_name,
                "Points projection parameters reset"
            )

    def set_attitudes_projection_parameters_to_none(self, warn=True):

        self.attitudes_projection_parameters = None

        if warn:
            warn_qgis(
                self.plugin_name,
                "Attitudes projection parameters reset"
            )

    def set_lines_intersection_parameters_to_none(self, warn=True):

        self.lines_intersection_parameters = None

        if warn:
            warn_qgis(
                self.plugin_name,
                "Lines intersection parameters reset"
            )

    def set_polygons_intersection_parameters_to_none(self, warn=True):

        self.polygons_intersection_parameters = None

        if warn:
            warn_qgis(
                self.plugin_name,
                "Polygons intersection parameters reset"
            )

    def set_geological_parameters_to_none(self):

        self.set_points_projection_parameters_to_none(warn=False)
        self.set_attitudes_projection_parameters_to_none(warn=False)
        self.set_lines_intersection_parameters_to_none(warn=False)
        self.set_polygons_intersection_parameters_to_none(warn=False)

    def init_geoprofiles_parameters(self):

        self.qgis_3d_line_layer = None
        self.geoprofiles = None  # GeoProfiles()  # main instance for the geoprofiles

        self.set_geological_parameters_to_none()

    def init_graphics_parameters(self):

        self.graphical_params = None
        self.polygon_classification_colors = dict()

    def do_processing(self):

        current_item_text = self.actions_qtreewidget.currentItem().text(0)

        processing = self.processings.get(current_item_text)

        if processing is not None:
            processing()

    def create_geopackage_storage(self):

        dialog = CreateNewGeopackageDialog(self.plugin_name)
        if dialog.exec_():
            geopackage_path = dialog.new_geopackage_QLineEdit.text()
            if len(geopackage_path) == 0:
                warn_qgis(
                    self.plugin_name,
                    "Output geopackage path is undefined"
                )
                return
            layer_name = dialog.new_layer_QLineEdit.text()
            if len(layer_name) == 0:
                warn_qgis(
                    self.plugin_name,
                    "Output layer name is undefined"
                )
                return
        else:
            warn_qgis(
                self.plugin_name,
                "No geopackage defined"
            )
            return

        success = create_geopackage(
            path=geopackage_path
        )
        if not success:
            error_qgis(
                header=self.plugin_name,
                msg=f"\nError with creating {geopackage_path}",
            )
            return

        success = add_vector_layer(
            geopackage_path=geopackage_path,
            layer_name=layer_name,
            geometry_type="LineString25D",
            epsg_code=4326,
        )

        if not success:
            error_qgis(
                header=self.plugin_name,
                msg=f"\nError with adding vector layer {layer_name}",
            )
            return

        self.storage_geopackage_path = geopackage_path
        self.storage_layer_name = layer_name

    def create_3d_layer_storage(self):

        dialog = OpenGeopackageDialog(self.plugin_name)
        if dialog.exec_():
            geopackage_path = dialog.existing_geopackage_QLineEdit.text()
            if len(geopackage_path) == 0:
                warn_qgis(
                    self.plugin_name,
                    "Output geopackage path is undefined"
                )
                return
            layer_name = dialog.existing_layer_QLineEdit.text()
            if len(layer_name) == 0:
                warn_qgis(
                    self.plugin_name,
                    "Output layer name is undefined"
                )
                return
        else:
            warn_qgis(
                self.plugin_name,
                "No geopackage defined"
            )
            return

        success = add_vector_layer(
            geopackage_path=geopackage_path,
            layer_name=layer_name,
            geometry_type="LineString25D",
            epsg_code=4326,
        )

        if not success:
            error_qgis(
                header=self.plugin_name,
                msg=f"\nError with adding vector layer {layer_name}",
            )
            return

        self.storage_geopackage_path = geopackage_path
        self.storage_layer_name = layer_name

    def load_existing_geopackage_storage(self):

        dialog = OpenGeopackageDialog(self.plugin_name)
        if dialog.exec_():
            geopackage_path = dialog.existing_geopackage_QLineEdit.text()
            if len(geopackage_path) == 0:
                warn_qgis(
                    self.plugin_name,
                    "Output geopackage path is undefined"
                )
                return
            layer_name = dialog.existing_layer_QLineEdit.text()
            if len(layer_name) == 0:
                warn_qgis(
                    self.plugin_name,
                    "Output layer name is undefined"
                )
                return
        else:
            warn_qgis(
                self.plugin_name,
                "No geopackage defined"
            )
            return

        self.storage_geopackage_path = geopackage_path
        self.storage_layer_name = layer_name

    def define_profile_lines_from_qgis_layer(self):
        """
        Should define:
         - source type -> self.profile_track_source = ProfileSource.LINE_LAYER
         - list of undensified, inverted-in-case, CRS-projected lines

        """

        self.init_profile_lines_parameters()
        self.init_geoprofiles_parameters()

        self.clear_rubberband_line()

        line_layers_in_project = loaded_line_layers()

        if len(line_layers_in_project) == 0:
            warn_qgis(
                self.plugin_name,
                "No available line layers in current project"
            )
            return

        dialog = DefineSourceLineLayerDialog(
            self.plugin_name,
            line_layers_in_project,
        )

        if dialog.exec_():
            qgs_line_layer = line_layers_in_project[dialog.LineLayers_comboBox.currentIndex()]
        else:
            warn_qgis(
                self.plugin_name,
                "No defined line source"
            )
            return

        layer_crs_wkt = get_layer_crs_as_wtk(qgs_line_layer)
        if layer_crs_wkt is None:
            warn_qgis(
                self.plugin_name,
                "Unable to extract layer CRS as WKT"
            )
            return
        
        self.new_profiles_crs_code = layer_crs_wkt

        qgis_line_geometries = extract_selected_geometries(
            layer=qgs_line_layer,
        )

        if qgis_line_geometries is None:
            return

        profile_lines = []
        for qgis_line_geometry in qgis_line_geometries:
            lines = extract_lines_23d_from_qgslinestring_multipart(
                qgis_geometry=qgis_line_geometry
            )
            if lines is None:
                warn_qt(self,
                        self.plugin_name,
                        f"Line geometry '{qgis_line_geometry}' could not be extracted")
                continue
            profile_lines.extend(lines)

        #print(f"profile_lines: {profile_lines}")

        if not profile_lines:
            warn_qt(
                self,
                self.plugin_name,
                f"No profile line could be extracted"
            )
            return

        for profile_line in profile_lines:
            self.newly_defined_profile_baselines.append(
                ProfileBaseLineParameters(
                    "profile",
                    ProfileSource.LINE_LAYER,
                    profile_line,
                    layer_crs_wkt,
                )
            )

        #print(f"self.newly_defined_profile_baselines: {self.newly_defined_profile_baselines}")

        ok_qgis(
            self.plugin_name,
            "Line layer read"
        )

    def extract_line2d_from_point_list(
        self,
         dialog,
    ) -> Union[None, Ln]:

        try:

            raw_point_string = dialog.point_list_qtextedit.toPlainText()
            raw_point_list = raw_point_string.split("\n")
            raw_point_list = [clean_string(str(unicode_txt)) for unicode_txt in raw_point_list]
            data_list = [rp for rp in raw_point_list if rp != ""]

            point_list = [to_float(xy_pair.split(",")) for xy_pair in data_list]
            line_2d = xytuple_list_to_line2d(point_list)

            return line_2d

        except Exception as e:

            error_qgis(
                self.plugin_name,
                f"{e!r}"
            )

            return None

    def qgis_rasterlayer_parameters(self,
        dem: 'qgis.core.QgsRasterLayer',
    ) -> Union[None, QGisRasterParameters]:

        result, err = extract_dem_parameters_from_qgs_raster_layer(dem)

        if err:
            msg = f"Error with {dem.name()} as source: {err!r} "
            warn_qgis(
                self.plugin_name,
                msg
            )
            return None

        return result

    def extract_selected_dem(
        self,
        dialog,
    ) -> Union[None, 'qgis.core.QgsRasterLayer']:

        try:
            return dialog.singleband_raster_layers_in_project[dialog.listDEMs_treeWidget.currentIndex().row()]
        except:
            return None

    def read_dem_layer(self) -> Union[None, Grid]:

        try:

            current_raster_layers = loaded_monoband_raster_layers()
            if len(current_raster_layers) == 0:
                raise Exception("No available raster layers in current project")

            dialog = SourceDEMSelectionDialog(
                self.plugin_name,
                current_raster_layers
            )

            if dialog.exec_():
                selected_dem = self.extract_selected_dem(dialog)
            else:
                raise Exception("No chosen DEM")

            if selected_dem is None:
                raise Exception("No selected DEM")

            dem_pth = selected_dem.dataProvider().dataSourceUri()

            result, err = read_raster_band(raster_source=dem_pth)

            if err:
                raise Exception(f"Unable to read raster band: {err!r}")

            geotransform, wkt_crs, band_params, data = result

            return Grid(
                array=data,
                geotransform=geotransform,
                wkt_crs=wkt_crs
            )

        except Exception as e:

            warn_qgis(
                self.plugin_name,
                f"{e!r}",
            )

            return None

    def define_dem_as_elevation_source(self):

        self.init_profiles_grid_parameters()
        self.init_geoprofiles_parameters()

        profiles_grid = self.read_dem_layer()

        if profiles_grid is None:
            return None

        self.profiles_grid = profiles_grid

        ok_qgis(
            self.plugin_name,
            f"Read DEM with z range {self.profiles_grid.z_range()}"
        )

    def init_topo_labels(self):
        """
        Initialize topographic label and order parameters.

        :return:
        """

        self.profiles_labels = None
        self.profiles_order = None

    def check_pre_profile(self):

        for geoprofile in self.geoprofiles.geoprofiles:
            if not geoprofile.statistics_calculated:
                warn_qgis(
                    self.plugin_name,
                    "Profile statistics not yet calculated"
                )
                return False

        return True

    ''' 20241222: apparently unused
    def calculate_profile_statistics(self,
        geoprofiles
    ):

        for geoprofile in geoprofiles:

            for name, line3d in geoprofile:

                statistics_elev = [get_statistics(p) for p in line3d.z_array()]
                statistics_dirslopes = [get_statistics(p) for p in line3d.dir_slopes()]
                statistics_slopes = [get_statistics(p) for p in np.absolute(line3d.dir_slopes())]

                profile_length = line3d.incremental_length_2d()[-1] - line3d.incremental_length_2d()[0]
                natural_elev_range = (
                    np.nanmin(np.array([ds_stats["min"] for ds_stats in statistics_elev])),
                    np.nanmax(np.array([ds_stats["max"] for ds_stats in statistics_elev])))
    '''

    def extract_graphical_params(
        self,
        dialog,
    ) -> None:
        # get profile plot parameters

        try:

            width = float(dialog.figure_width_qlineedit.text())
            if width > 0.0:
                self.graphical_params.figure.width = width

        except:

            pass

        try:

            height = float(dialog.figure_height_qlineedit.text())
            if height > 0:
                self.graphical_params.figure.height = height

        except:

            pass

        try:

            self.graphical_params.axis.z_min = float(dialog.z_min_value_qlineedit.text())

        except:

            print(f"Undefined Z min")
            pass

        try:

            self.graphical_params.axis.z_max = float(dialog.z_max_value_qlineedit.text())

        except:

            print(f"Undefined Z max")
            pass

        try:
            vertical_exaggeration = float(dialog.vertical_exxageration_ratio_qlineedit.text())
            if vertical_exaggeration > 0.0:
                self.graphical_params.axis.vertical_exaggeration = vertical_exaggeration

        except:

            print(f"Undefined vertical exxageration")
            pass

        if hasattr(dialog, 'elevation_color') and dialog.elevation_color is not None:
            self.graphical_params.elevations.color = qcolor2rgbmpl(dialog.elevation_color)

        if hasattr(dialog, 'point_projections_style'):
            self.graphical_params.point_projections = dialog.point_projections_style

        if hasattr(dialog, 'attitude_projections_style'):
            self.graphical_params.attitude_projections = dialog.attitude_projections_style

        if hasattr(dialog, 'line_intersections_style'):
            self.graphical_params.line_intersections = dialog.line_intersections_style

        if hasattr(dialog, "polygon_classification_colors"):
            self.graphical_params.polygon_intersections = dialog.polygon_classification_colors

    def generate_3d_profiles_lines(
            self,
            profiles_arrangement="central",
    ):

        if self.newly_defined_profile_baselines == []:
            warn_qt(self,
                    self.plugin_name,
                    f"Warning: Profile line(s) not defined")
            return

        if self.profiles_grid is None:
            warn_qt(self,
                    self.plugin_name,
                    f"Warning: DEM source not defined")
            return

        dialog = BaseDataProfilerSettingsDialog(
            self.plugin_name
        )

        if dialog.exec_():

            profiles_spacing = dialog.spacing_wdgt.value()
            print(f"Spacing between profiles: {profiles_spacing} (type: {type(profiles_spacing)})")
            num_parallel_profiles = dialog.num_parallel_profiles_wdgt.value()
            print(f"Number of parallel profiles: {num_parallel_profiles} (type: {type(num_parallel_profiles)})")

        else:

            warn_qgis(
                self.plugin_name,
                "Warning: parameters are not yet defined for parallel profiles "
            )
            return

        if num_parallel_profiles <= 0 or num_parallel_profiles % 2 != 1:
            warn_qgis(
                self.plugin_name,
                "Error: number of parallel profiles must be a positive odd number"
            )
            return

        if num_parallel_profiles > 1 and profiles_spacing <= 0.0:
            warn_qgis(
                self.plugin_name,
                "Error: spacing between parallel profiles cannot be zero when parallel profiles defined"
            )
            return

        newly_defined_profilers = Profilers.make_parallel_from_lines(
                src_traces=self.newly_defined_profile_baselines,
                num_profiles=num_parallel_profiles,
                offset=profiles_spacing,
                profiles_arrangement=profiles_arrangement,
            )

        # defines points 3D for one or more profilers, based on a grid

        points_3d_profiles, err = profile_grid_as_pts3d(
            newly_defined_profilers,
            grid=self.profiles_grid,
        )

        if err:
            error_qt(
                self,
                header=self.plugin_name,
                msg=f"Error in grid sampling: {err!r} "
            )
            return

        profiles_coords_4326 = []
        for line_pts in points_3d_profiles:
            line_coords_4326 = []
            for x, y, z in line_pts:
                xy_4326 = project_xy_to_geographic(
                    x=x,
                    y=y,
                    src_crs=self.new_profiles_crs_code,
                )
                if xy_4326 is not None:
                    line_coords_4326.append([*xy_4326, z])
                else:
                    print(f"Error in {x}-{y}-{z} projection to EPSG:4326 from {self.new_profiles_crs_code}")

            profiles_coords_4326.append(line_coords_4326)

        # write as lines in storage fc

        for line_coords in profiles_coords_4326:
            success = insert_3d_line(
                file_path=self.storage_geopackage_path,
                layer_name=self.storage_layer_name,
                line_coordinates=line_coords,
            )
            if not success:
                print(f"Error in {line_coords!r}")

        ok_qgis(
            self.plugin_name,
            "Inserted profiles"
        )

    def define_total_graphical_parameters(self):
        """
        Updates in-place graphical parameters.
        """

        if self.graphical_params is None:
            self.graphical_params = GraphicalParameters()

        dialog = StylesTotalGraphicalParamsDialog(
            self.plugin_name,
            self.geoprofiles,
            self.graphical_params,
            self.polygons_intersection_parameters,
        )

        if dialog.exec_():
            self.extract_graphical_params(dialog)
        else:
            return

        ok_qgis(
            self.plugin_name,
            "Graphic parameters for profiles defined"
        )

    def save_graphic_parameters(self):

        try:

            file_pth = define_path_new_file(
                self,
                "Save graphical parameters as file",
                "*.pkl",
                "pickle (*.pkl *.PKL)"
            )

            with open(file_pth, 'wb') as handle:
                pickle.dump(
                    self.graphical_params,
                    handle,
                    protocol=pickle.HIGHEST_PROTOCOL
                )

            info_qgis(
                self.plugin_name,
                f"Graphical parameters saved in {file_pth}"
            )

        except Exception as e:

            warn_qgis(
                self.plugin_name,
                f"Error: {e!r}"
            )

    def load_graphic_parameters(self):

        try:

            file_pth = old_file_path(
                self,
                "Load graphical parameters from file",
                "*.pkl",
                "pickle (*.pkl *.PKL)"
            )

            with open(file_pth, 'rb') as handle:
                self.graphical_params = pickle.load(handle)

            info_qgis(
                self.plugin_name,
                f"Graphical parameters loaded from {file_pth}"
            )

        except Exception as e:

            warn_qgis(
                self.plugin_name,
                f"Error: {e!r}"
            )

    def plot_geoprofiles(self):

        err = self.convert_3d_lines_into_geoprofiles()
        if err:
            return

        if self.polygons_intersection_parameters is not None:

            err = self.intersect_polygons()
            if err:
                error_qt(
                    self,
                    self.plugin_name,
                    f"while intersecting polygons: {err!r}",
                )
                return

        if self.attitudes_projection_parameters is not None:

            err = self.project_attitudes()
            if err:
                error_qt(
                    self,
                    self.plugin_name,
                    f"while projecting attitudes: {err!r}",
                )
                return

        if self.points_projection_parameters is not None:

            err = self.project_points()
            if err:
                error_qt(
                    self,
                    self.plugin_name,
                    f"while projecting points: {err!r}",
                )
                return

        if self.lines_intersection_parameters is not None:

            err = self.intersect_lines()
            if err:
                error_qt(
                    self,
                    self.plugin_name,
                    f"while intersecting lines: {err!r}",
                )
                return

        if self.graphical_params is None:
            self.graphical_params = GraphicalParameters()
            zmin, zmax, ve = self.precalculate_axis_parameters_suggestions()
            self.graphical_params.axis.z_min = zmin
            self.graphical_params.axis.z_max = zmax
            self.graphical_params.axis.vertical_exaggeration = ve

        '''
        if self.polygons_intersection_parameters is not None:
            if self.graphical_params.polygon_intersections is None:
                self.graphical_params.initialize_polygons_intersections_style()
        '''

        # plot the profile

        figure = profiles(
            self.geoprofiles,
            axis_params=self.graphical_params.axis,
            linewidth=0.8,
            width=self.graphical_params.figure.width,
            height=self.graphical_params.figure.height,
            elevation_params=self.graphical_params.elevations,
            points=self.graphical_params.point_projections,
            attitudes=self.graphical_params.attitude_projections,
            line_intersections=self.graphical_params.line_intersections,
            polygon_intersections=self.graphical_params.polygon_intersections,
        )

        graphs_win = FigureWindow(
            figure=figure,
        )
        graphs_win.setWindowTitle("Geoprofiles")
        graphs_win.exec_()

    def convert_profile_mline_to_mline(self,
        profile_mline: Tuple[numbers.Integral, str, List[Tuple[numbers.Real, numbers.Real, numbers.Real]]],
    ) -> Union[Error, Union[Ln, MultiLine]]:

        try:

            fid, line_type, coords = profile_mline
            if line_type == "line":
                return (fid, Ln.fromCoordinates(coords))
            elif line_type == "multiline":
                lines = []
                for line in coords:
                    lines.append(Ln.fromCoordinates(line))
                return (fid, MultiLine.fromLines(lines))
            else:
                raise Exception(f"Unknown line type: {line_type!r}")

        except Exception as e:

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

    def project_profiles_3d(self,
        lines: List[
            Tuple[
                numbers.Integral,  # the feature fids
                str,  # "line" or "multiline"
                Union[
                    List[Tuple[float, float, float]],  # 3D coordinates of single line
                    List[List[Tuple[float, float, float]]]  # 3D coordinates of multiple lines
                ]
            ]
        ],
        dest_crs,
    ) -> Union[Error, List]:

        projected_profiles = []

        for fid, line_type, line_coords in lines:

            if line_type == "line":

                projected_line = qgis_project_3d_coords_list(
                    coords_3d=line_coords,
                    dest_crs=dest_crs,
                )
                if isinstance(projected_line, Error):
                    return projected_line

                projected_profiles.append((fid, line_type, projected_line))

            elif line_type == "multiline":

                projected_multilines = []
                for coords in line_coords:
                    projected_line = qgis_project_3d_coords_list(
                        coords_3d=coords,
                        dest_crs=dest_crs,
                    )
                    if isinstance(projected_line, Error):
                        return projected_line
                    projected_multilines.append(projected_line)

                projected_profiles.append((fid, line_type, projected_multilines))

            else:

                return Error(
                    True,
                    caller_name(),
                    Exception(f"Unknown line type {line_type!r}"),
                    traceback.format_exc(),
                )

        return projected_profiles

    def define_profile_line_by_rubberband_line(self):

        self.init_profile_lines_parameters()
        self.init_geoprofiles_parameters()

        src_crs = QgsProject.instance().crs()

        if src_crs.isGeographic():
            warn_qt(self,
                    self.plugin_name,
                    f"Project CRS is geographic ('{src_crs}') but planar one required for profile digitation")
            return

        proj_wkt_string = src_crs.toWkt()

        self.new_profiles_crs_code = proj_wkt_string

        self.clear_rubberband_line()

        self.previous_maptool = self.canvas.mapTool()  # Save the standard map tool for restoring it at the end

        info_qgis(
            self.plugin_name,
            "Now you can digitize the trace on the map.\nLeft click: add point\nRight click: end adding point"
        )

        self.rubberband = QgsRubberBand(self.canvas)
        self.rubberband.setWidth(2)
        self.rubberband.setColor(QColor(Qt.red))

        self.digitize_maptool = MapDigitizeTool(self.canvas)
        self.canvas.setMapTool(self.digitize_maptool)

        self.digitize_maptool.moved.connect(self.canvas_refresh_profile_line)
        self.digitize_maptool.leftClicked.connect(self.profile_add_point)
        self.digitize_maptool.rightClicked.connect(self.canvas_end_profile_line)

    def canvas_refresh_profile_line(self, position):

        x, y = xy_from_canvas(self.canvas, position)

        self.refresh_rubberband(
            self.profile_canvas_points_x + [x],
            self.profile_canvas_points_y + [y]
        )

    def profile_add_point(self, position):

        x, y = xy_from_canvas(self.canvas, position)
        self.profile_canvas_points_x.append(x)
        self.profile_canvas_points_y.append(y)

    def canvas_end_profile_line(self):

        self.refresh_rubberband(
            self.profile_canvas_points_x,
            self.profile_canvas_points_y,
        )

        self.currently_digitized_line = None

        if len(self.profile_canvas_points_x) < 2:
            warn_qgis(
                self.plugin_name,
                "At least two non-coincident points are required"
            )
            return

        digitized_line = Ln(list(zip(self.profile_canvas_points_x, self.profile_canvas_points_y))).remove_coincident_points()

        if digitized_line.num_points() < 2:
            warn_qgis(
                self.plugin_name,
                "Just one non-coincident point"
            )
            return

        self.profile_canvas_points_x = []
        self.profile_canvas_points_y = []
        self.restore_previous_map_tool()

        project_crs_wkt = extract_project_crs_as_wkt()
        if project_crs_wkt is None:
            error_qgis(
                self.plugin_name,
                "Unable to extract GGIS project CRS as WKT"
            )
            return

        profile_baseline_params = ProfileBaseLineParameters(
            "Digitized line",
            ProfileSource.DIGITATION,
            digitized_line,
            project_crs_wkt
        )

        self.newly_defined_profile_baselines.append(profile_baseline_params)

    def restore_previous_map_tool(self):

        self.canvas.unsetMapTool(self.digitize_maptool)
        self.canvas.setMapTool(self.previous_maptool)

    def refresh_rubberband(self,
                           x_list,
                           y_list
                           ):

        self.rubberband.reset(QgsWkbTypes.LineGeometry)
        for x, y in zip(x_list, y_list):
            self.rubberband.addPoint(QgsPointXY(x, y))

    def clear_rubberband_line(self):

        self.profile_canvas_points_x = []
        self.profile_canvas_points_y = []

        self.currently_digitized_line = None

        try:
            self.rubberband.reset()
        except:
            pass

    def clear_all_traces(self):

        self.clear_rubberband_line()
        self.newly_defined_profile_baselines = []

    def output_profile_line(self,
            output_format,
            output_filepath,
            pts2dt,
            proj_sr
    ):

        points = [[n, pt2dt.x, pt2dt.y] for n, pt2dt in enumerate(pts2dt)]
        if output_format == "csv":
            success, msg = write_generic_csv(
                output_filepath,
                ['id', 'x', 'y'],
                points
            )
            if not success:
                warn_qgis(
                    self.plugin_name,
                    msg
                )
        elif output_format == "shapefile - line":
            success, msg = write_rubberband_profile_lnshp(
                output_filepath,
                ['id'],
                points,
                proj_sr)
            if not success:
                warn_qgis(
                    self.plugin_name,
                    msg
                )
        else:
            error_qt(
                self,
                self.plugin_name,
                "Debug: error in export format"
            )
            return

        if success:
            info_qgis(
                self.plugin_name,
                "Ln saved"
            )

    def extract_format_type(self,):

        if dialog.outtype_shapefile_line_QRadioButton.isChecked():
            return "shapefile - line"
        elif dialog.outtype_csv_QRadioButton.isChecked():
            return "csv"
        else:
            return ""

    def save_rubberband_line(self):


        if self.currently_digitized_line is None:

            warn_qgis(
                self.plugin_name,
                "No available line to save [1]"
            )
            return

        elif self.currently_digitized_line.num_points() < 2:

            warn_qgis(
                self.plugin_name,
                "No available line to save [2]"
            )
            return

        dialog = ExportLineDataDialog(self.plugin_name)
        if dialog.exec_():
            output_format = extract_format_type()
            if output_format == "":
                warn_qgis(
                    self.plugin_name,
                    "Error in output format"
                )
                return
            output_filepath = dialog.outpath_QLineEdit.text()
            if len(output_filepath) == 0:
                warn_qgis(
                    self.plugin_name,
                    "Error in output path"
                )
                return
            add_to_project = dialog.load_output_checkBox.isChecked()
        else:
            warn_qgis(
                self.plugin_name,
                "No export defined"
            )
            return

        # get project CRS information
        project_crs_osr = proj4str()

        output_profile_line(
            output_format,
            output_filepath,
            self.currently_digitized_line.pts(),
            project_crs_osr)

        # add theme to QGis project
        if output_format == "shapefile - line" and add_to_project:
            try:

                digitized_line_layer = QgsVectorLayer(output_filepath,
                                                      QFileInfo(output_filepath).baseName(),
                                                      "ogr")
                QgsProject.instance().addMapLayer(digitized_line_layer)

            except:

                error_qt(
                    self,
                    self.plugin_name,
                    "Unable to load layer in project"
                )
                return

    def define_point_layer_for_projection(self):

        try:

            # read input data

            current_point_layers = loaded_point_layers()

            if len(current_point_layers) == 0:
                warn_qgis(
                    self.plugin_name,
                    "No available point layers"
                )
                return

            dialog = GeologicalDataProjectPointsDialog(
                self.plugin_name,
                current_point_layers
            )

            if dialog.exec_():
                # read parameters
                self.points_projection_parameters = self.extract_point_projection_params(
                    dialog,
                    current_point_layers
                )
                info_qgis(
                    self.plugin_name,
                    "Points projections parameters defined",
                )
            else:
                warn_qgis(
                    self.plugin_name,
                    "No point projection defined"
                )

        except Exception as e:

            error_qt(
                self,
                self.plugin_name,
                f"Unable to define points layer to project: {e!r}"
            )

    def remove_point_layer_for_projection(self):

        self.points_projection_parameters = None

    def project_points(self) -> Error:
        """
        Project the points of a point layer as simple point locations.

        """

        try:

            if self.points_projection_parameters is None:
                raise Exception("Points projection parameters are yet to defined")

            # check CRS

            point_layer_wkt_crs = get_layer_crs_as_wtk(
                self.points_projection_parameters.point_layer
            )

            if point_layer_wkt_crs is None:
                raise Exception("Unable to extract layer CRS for attitude projections")

            # Define fields to read

            if self.points_projection_parameters.z_attribute_choice:
                field_list = [
                    self.points_projection_parameters.z_field_name,
                    self.points_projection_parameters.label_field_name,
                ]
            else:
                field_list = [
                    self.points_projection_parameters.label_field_name,
                ]

            # read input data

            data_points = read_qgis_pt_layer(
                pt_layer=self.points_projection_parameters.point_layer,
                field_list=field_list,
            )

            if not data_points:
                raise Exception(f"Unable to extract records for source layer")

            pts_3d = []

            for x, y, z, *_ in data_points:
                pts_3d.append(Point(x, y, z))

            if pts_3d:

                parsed_points = []

                for (*_, label), pt_3d in zip(data_points, pts_3d):
                    parsed_points.append((label, pt_3d))

            else:

                raise Exception(f"Points 3D are not defined")

            if parsed_points:

                projected_points = project_points(
                    self.geoprofiles,
                    parsed_points,
                    point_layer_wkt_crs,
                    self.points_projection_parameters.max_profile_distance,
                    ProjectionMethod.NEAREST,
                    PointsInput.POINTS,
                )

                if isinstance(projected_points, Error):
                    raise Exception(f"{projected_points!r}")

                self.geoprofiles._points_projections["points"] = projected_points
                return Error()

            else:

                raise Exception("No 3D valid points found")

        except Exception as e:

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

    def extract_point_projection_params(
            self,
            dialog,
            current_point_layers):

        point_layer = current_point_layers[dialog.point_layer_combobox.currentIndex()]
        z_attribute_choice = dialog.radio_attributi_z.isChecked()
        z_field_name = dialog.z_fld_comboBox.currentText()

        points_are_3d = dialog.radio_punti_3d.isChecked()

        max_profile_distance = dialog.max_profile_distance_qdoublespinbox.value()
        label_field_name = dialog.point_label_fld_comboBox.currentText()

        return PointsProjectionParameters(
            point_layer,
            label_field_name,
            z_attribute_choice,
            z_field_name,
            points_are_3d,
            max_profile_distance,
        )

    def extract_attitude_projections_params(
            self,
            dialog,
            current_point_layers) -> AttitudesProjectionParameters:

        point_layer = current_point_layers[dialog.point_layer_combobox.currentIndex()]
        label_field_name = dialog.point_label_fld_comboBox.currentText()
        use_dipdir = dialog.use_dipdir_qradiobuttom.isChecked()
        use_rhr_strike = dialog.use_rhr_strike_qradiobutton.isChecked()
        orient_fld_name = dialog.orient_fld_combobox.currentText()
        dipangle_fld_name = dialog.dipangle_fld_combobox.currentText()

        dem_choice = dialog.radio_dem.isChecked()
        dem_layer = dialog.current_raster_layers[dialog.dem_combobox.currentText()]

        z_attribute_choice = dialog.radio_attributi_z.isChecked()
        z_field_name = dialog.combobox_attributi_z.currentText()

        points_are_3d = dialog.radio_punti_3d.isChecked()

        max_profile_distance = dialog.max_profile_distance_qdoublespinbox.value()

        return AttitudesProjectionParameters(
            point_layer,
            label_field_name,
            use_dipdir,
            use_rhr_strike,
            orient_fld_name,
            dipangle_fld_name,
            dem_choice,
            dem_layer,
            z_attribute_choice,
            z_field_name,
            points_are_3d,
            max_profile_distance,
        )

    def define_attitude_layer_for_projection(self):
        """
        Project the points of a point layer as simple point locations.

        """

        try:

            current_point_layers = loaded_point_layers()

            if len(current_point_layers) == 0:
                warn_qgis(
                    self.plugin_name,
                    "No available point layers"
                )
                return

            dialog = GeologicalDataProjectAttitudesDialog(
                self.plugin_name,
                current_point_layers
            )

            if dialog.exec_():

                # read parameters

                self.attitudes_projection_parameters = self.extract_attitude_projections_params(
                    dialog,
                    current_point_layers
                )

                info_qgis(
                    self.plugin_name,
                    "Attitudes projections parameters defined",
                )

            else:
                warn_qgis(
                        self.plugin_name,
                        "No point attitudes projection defined")
                return

        except Exception as e:

            error_qt(
                self,
                self.plugin_name,
                f"Unable to define point layer to project: {e!r}"
            )

    def project_attitudes(self) -> Error:

        try:

            if self.attitudes_projection_parameters is None:
                raise Exception("Attitudes projection parameters are yet to be defined")

            # check CRS

            point_layer_wkt_crs = get_layer_crs_as_wtk(
                self.attitudes_projection_parameters.point_layer
            )

            if point_layer_wkt_crs is None:
                raise Exception("Unable to extract layer CRS for attitude projections")

            # Define fields to read

            if self.attitudes_projection_parameters.z_attribute_choice:
                field_list=[
                    self.attitudes_projection_parameters.z_field_name,
                    self.attitudes_projection_parameters.label_field_name,
                    self.attitudes_projection_parameters.orient_fld_name,
                    self.attitudes_projection_parameters.dipangle_fld_name,
                ]
            else:
                field_list=[
                    self.attitudes_projection_parameters.label_field_name,
                    self.attitudes_projection_parameters.orient_fld_name,
                    self.attitudes_projection_parameters.dipangle_fld_name,
                ]

            # read input data

            data_points = read_qgis_pt_layer(
                pt_layer=self.attitudes_projection_parameters.point_layer,
                field_list=field_list,
            )

            if not data_points:
                raise Exception(f"Unable to extract records for source layer")

            parsed_points = None

            if self.attitudes_projection_parameters.dem_choice:

                pts_3d = []

                for x, y, *_ in data_points:

                    source_dem = self.attitudes_projection_parameters.dem_layer

                    grid = grid_from_raster_band(
                        raster_source=source_dem.source()
                    )

                    pt3d = grid.interpolate_bilinear_point_with_nan(
                        pt=Point(x, y),
                        pt_wkt_crs=point_layer_wkt_crs,
                    )

                    pts_3d.append(pt3d)

            elif self.attitudes_projection_parameters.z_attribute_choice or self.attitudes_projection_parameters.points_are_3d:

                pts_3d = []

                for x, y, z, *_ in data_points:
                    pts_3d.append(Point(x, y, z))

            else:

                raise Exception(f"Got no valid choice for elevation source")

            use_rhr_strike = self.attitudes_projection_parameters.use_rhr_strike

            if pts_3d:

                parsed_points = []

                for (*_, label, orientation, dipangle), pt_3d in zip(data_points, pts_3d):

                    plane = Plane(
                        azim=orientation,
                        dip_ang=dipangle,
                        is_rhr_strike=use_rhr_strike)

                    parsed_points.append((label, pt_3d, plane))

            else:

                raise Exception(f"Points 3D are not defined")

            if parsed_points:

                projected_attitudes = project_points(
                    self.geoprofiles,
                    parsed_points,
                    point_layer_wkt_crs,
                    self.attitudes_projection_parameters.max_profile_distance,
                    ProjectionMethod.NEAREST,
                    PointsInput.ATTITUDES,
                )

                if isinstance(projected_attitudes, Error):
                    raise Exception(f"{projected_attitudes!r}")

                self.geoprofiles._points_projections["attitudes"] = projected_attitudes

            else:

                raise Exception("No 3D valid points found")

        except Exception as e:

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

    def define_line_layer_for_intersection(self):
        """
        Intersects a line layer (e.g., faults) with the profiles.
        """

        try:

            current_line_layers = loaded_line_layers()

            if len(current_line_layers) == 0:
                warn_qgis(
                    self.plugin_name,
                    "No available line layers"
                )
                return

            dialog = GeologicalDataIntersectLinesDialog(
                self.plugin_name,
                current_line_layers
            )

            if dialog.exec_():
                # line_layer, id_field_ndx
                self.lines_intersection_parameters = self.extract_intersection_line_layer_params(dialog, current_line_layers)
                info_qgis(
                    self.plugin_name,
                    "Line intersection parameters defined",
                )

            else:
                warn_qgis(
                    self.plugin_name,
                    "No defined line source"
                )

        except Exception as e:

            error_qt(
                self,
                self.plugin_name,
                f"Unable to define line layer to intersect: {e!r}"
            )

    def intersect_lines(self) -> Error:

        try:

            if self.lines_intersection_parameters is None:
                raise Exception("Lines intersection parameters are yet to be defined")

            # check CRS

            lines_layer_crs_wkt = get_layer_crs_as_wtk(
                self.lines_intersection_parameters.line_layer,
            )

            if lines_layer_crs_wkt is None:
                raise Exception(f"Unable to extract CRS as WKT from chosen line layer")

            # process data

            success, results = extract_coords_2d_with_order_from_selected_features_in_layer_multipart(
                line_layer=self.lines_intersection_parameters.line_layer,
                order_field_ndx=self.lines_intersection_parameters.id_field_ndx
            )

            if not success:
                raise Exception(results)

            line_layer_crs_coordinates, line_layer_ids = results

            line_layer_crs_loaded_lines = defaultdict(list)

            for (id, (type, data)) in zip(line_layer_ids, line_layer_crs_coordinates):
                if type == "multiline":
                    lines = []
                    for ln_data in data:
                        ln = Ln.fromCoordinates(ln_data)
                        lines.append(ln)
                elif type == "line":
                    lines = [Ln.fromCoordinates(data)]
                else:
                    raise Exception(f"Line type is {type}, that is unhandled")

                line_layer_crs_loaded_lines[id].extend(lines)

            err = self.geoprofiles.intersect_lines(
                line_layer_crs_loaded_lines,
                lines_layer_crs_wkt,
            )

            if err:
                raise Exception(f"{err!r}")

        except Exception as e:

            error_qt(
                self,
                self.plugin_name,
                f"Error with line layer intersection: {e!r}"
            )

    def extract_intersection_line_layer_params(
            self,
            dialog,
            current_line_layers
    ) -> LinesIntersectionParameters:

        line_layer = current_line_layers[dialog.LineLayers_comboBox.currentIndex()]
        id_field_ndx = dialog.inters_input_id_fld_line_comboBox.currentIndex()

        return LinesIntersectionParameters(
            line_layer,
            id_field_ndx,
        )

    def intersect_polygons(self) -> Error:

        try:

            if self.polygons_intersection_parameters is None:
                raise Exception("Polygons intersection parameters are yet to be defined")

            # check CRS

            polygons_layer_crs_wkt = get_layer_crs_as_wtk(
                self.polygons_intersection_parameters.polygon_layer
            )

            if polygons_layer_crs_wkt is None:
                raise Exception("Unable to extract CRS as WKT from chosen polygon layer")

            # process

            polygon_lyr = self.polygons_intersection_parameters.polygon_layer

            renderer = polygon_lyr.renderer()

            # 1) Costruisci una mappa {legend_key -> label} una sola volta
            items = renderer.legendSymbolItems()  # lista di QgsLegendSymbolItem
            key_to_label = {it.ruleKey(): it.label() for it in items}

            polygons = defaultdict(list)

            renderer.startRender(context, polygon_lyr.fields())

            try:

                for f in polygon_lyr.getFeatures():

                    if f.hasGeometry():

                        keys = list(renderer.legendKeysForFeature(f, context))  # set -> lista
                        labels = [key_to_label.get(k) for k in keys if key_to_label.get(k)]
                        if labels:
                            name = ', '.join(labels)
                        else:
                            # fallback: nessuna key (es. NullSymbolRenderer). Prova etichetta predefinita
                            name = items[0].label() if items else renderer.type()

                        geom = f.geometry()

                        feature_polygons = qgismpolygon_to_polygons(geom)

                        polygons[name].extend(feature_polygons)

            finally:

                renderer.stopRender(context)

            # regular code for determining intersections

            err = self.geoprofiles.intersect_polygons(
                polygons,
                polygons_layer_crs_wkt,
            )

            if err:
                raise Exception(f"{err!r}")

        except Exception as e:

            error_qt(
                self,
                self.plugin_name,
                f"Error with polygon layer intersection: {e!r}"
            )

    def define_polygon_layer_for_intersection(self):
        """
        Intersects a polygonal layer (e.g., outcrops) with the profiles.
        """

        try:

            current_polygonal_layers = loaded_polygon_layers()

            if len(current_polygonal_layers) == 0:
                warn_qgis(
                    self.plugin_name,
                    "No available polygon layers"
                )
                return

            dialog = GeologicalDataIntersectPolygonsDialog(
                self.plugin_name,
                current_polygonal_layers
            )

            if dialog.exec_():

                # polygon_layer, id_fld_name
                self.polygons_intersection_parameters = self.extract_intersection_polygon_layer_params(dialog, current_polygonal_layers)

                info_qgis(
                    self.plugin_name,
                    "Polygon intersection parameters defined",
                )

            else:

                warn_qgis(
                    self.plugin_name,
                    "No defined polygonal source"
                )
                return

        except Exception as e:

            error_qt(
                self,
                self.plugin_name,
                f"Unable to define polygon layer to intersect: {e!r}"
            )

    def extract_intersection_polygon_layer_params(
            self,
            dialog,
            current_polygon_layers) -> PolygonsIntersectionParameters:

        polygon_layer = current_polygon_layers[dialog.PolygonLayers_comboBox.currentIndex()]

        return PolygonsIntersectionParameters(
            polygon_layer,
        )

    def define_3d_line_layer(self):

        line_layers_in_project = loaded_line_layers()

        if len(line_layers_in_project) == 0:
            warn_qgis(
                self.plugin_name,
                "No available line layers in current project"
            )
            return

        dialog = DefineSourceLineLayerDialog(
            self.plugin_name,
            line_layers_in_project,
        )

        if dialog.exec_():
            qgs_3d_line_layer = line_layers_in_project[dialog.LineLayers_comboBox.currentIndex()]
        else:
            warn_qgis(
                self.plugin_name,
                "No defined 3D line source"
            )
            return

        info_qgis(
            self.plugin_name,
            "3D line source defined",
        )

        self.qgis_3d_line_layer = qgs_3d_line_layer

    def convert_3d_lines_into_geoprofiles(self) -> bool:

        try:

            if self.qgis_3d_line_layer is None:
                msg = f"Source 3D line layer not defined"
                error_qt(
                    self,
                    self.plugin_name,
                    f"{msg}",
                )
                return True

            # read selected features from source line layer

            topographic_lines = extract_coords_3d_with_fid_from_selected_features_in_layer_multipart(
                line_layer=self.qgis_3d_line_layer,
            )

            if isinstance(topographic_lines, str):
                raise Exception(topographic_lines)

            # project EPSG-4326 lines into project CRS

            project_crs_topographic_lines = self.project_profiles_3d(
                topographic_lines,
                dest_crs=QgsProject.instance().crs(),
            )

            if isinstance(project_crs_topographic_lines, Error):
                raise Exception(f"Error: {project_crs_topographic_lines!r}")

            # convert each selected feature into a Ln/MultiLine instance

            qgis_project_crs_parsed_mlines = []

            for project_crs_topographic_line in project_crs_topographic_lines:

                qgis_project_crs_parsed_mline = self.convert_profile_mline_to_mline(
                    project_crs_topographic_line,
                )

                if isinstance(qgis_project_crs_parsed_mline, Error):
                    raise Exception(f"{qgis_project_crs_parsed_mline!r}")

                qgis_project_crs_parsed_mlines.append(qgis_project_crs_parsed_mline)

            # convert the Ln/MultiLine instances into ZTrace instances

            qgis_project_crs_z_traces = []

            qgis_project_wkt_crs = QgsProject.instance().crs().toWkt()

            for rec_id, qgis_project_crs_parsed_mline in qgis_project_crs_parsed_mlines:

                qgis_project_crs_z_trace = ZTrace.from_mline(
                    mline=qgis_project_crs_parsed_mline,
                    wkt_crs=qgis_project_wkt_crs,
                    rec_id=rec_id,
                )

                qgis_project_crs_z_traces.append(qgis_project_crs_z_trace)

            self.geoprofiles = GeoProfiles(
                    topo_profiles=qgis_project_crs_z_traces,
                    wkt_crs=qgis_project_wkt_crs,
                )

            '''
            self.profile_3d_lines_crs = qgs_3d_line_layer.sourceCrs()
    
            qgis_line_geometries = extract_selected_geometries(
                layer=qgs_3d_line_layer,
            )
    
            if qgis_line_geometries is None:
                raise Exception("qgis_line_geometries is None")
    
            profile_lines = []
            for qgis_line_geometry in qgis_line_geometries:
                lines = extract_lines_from_qgis_geometry(
                    qgis_geometry=qgis_line_geometry
                )
                if lines is None:
                    warn_qt(self,
                            self.plugin_name,
                            f"Line geometry '{qgis_line_geometry}' could not be extracted")
                    continue
                profile_lines.extend(lines)
    
            if not profile_lines:
                raise Exception(f"No profile line could be extracted")
   
            self.profile_lines = profile_lines  # MultiLine(profile_lines).to_line().remove_coincident_points()
    
            self.profile_track_source_type = ProfileSource.LINE_LAYER
            '''

            return False

        except Exception as e:

            error_qt(
                self,
                self.plugin_name,
                f"{e!r}",
            )
            return True

    def precalculate_axis_parameters_suggestions(self):

        # pre-process input data for profiles parameters

        profile_length = self.geoprofiles.s_max()
        natural_elev_min = self.geoprofiles.z_min()
        natural_elev_max = self.geoprofiles.z_max()

        if np.isnan(profile_length) or profile_length == 0.0:
            warn_qt(self,
                    self.plugin_name,
                    f"Max profile length is {profile_length}.\nCheck profile trace"
                    )
            return Error()

        if np.isnan(natural_elev_min) or np.isnan(natural_elev_max):
            warn_qt(self,
                    self.plugin_name,
                    f"Max elevation in profile(s) is {natural_elev_max} and min is {natural_elev_min}.\nCheck profile trace location vs. DEM(s)"
                    )
            return Error()

        if natural_elev_max <= natural_elev_min:
            warn_qt(self,
                    self.plugin_name,
                    f"Min elevation {natural_elev_min} is greater than max elevation {natural_elev_max}"
                    )
            return Error()

        # calculate proposed plot elevation range

        z_padding = 0.5
        delta_z = natural_elev_max - natural_elev_min

        if delta_z == 0.0:

            plot_z_min = floor(natural_elev_min) - 10
            plot_z_max = ceil(natural_elev_max) + 10

        else:

            plot_z_min = floor(natural_elev_min - delta_z * z_padding)
            plot_z_max = ceil(natural_elev_max + delta_z * z_padding)

        delta_plot_z = plot_z_max - plot_z_min

        # suggested exaggeration value

        w_to_h_rat = float(profile_length) / float(delta_plot_z)
        sugg_ve = 0.2 * w_to_h_rat

        return plot_z_min, plot_z_max, sugg_ve

    def open_help(self):

        dialog = HelpDialog(
            self.plugin_name,
            self.current_directory
        )
        dialog.exec_()

    def closeEvent(self, evnt):

        self.clear_rubberband_line()

        """
        try:
            
            self.digitize_maptool.moved.disconnect(self.canvas_refresh_profile_line)
            self.digitize_maptool.leftClicked.disconnect(self.profile_add_point)
            self.digitize_maptool.rightClicked.disconnect(self.canvas_end_profile_line)
            self.restore_previous_map_tool()

        except:

            pass
        """


