# -*- coding: utf-8 -*-
"""
/***************************************************************************
RAVIDialog
A QGIS plugin
Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
-------------------
begin                : 2024-10-24
git sha              : $Format:%H$
copyright            : (C) 2024 by Caio Arantes
email                : caiosimplicioarante@gmail.com
***************************************************************************/

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

import re
import os
import sys
import importlib
import platform
import subprocess
import zipfile
import json
import webbrowser
import io
import array
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from qgis.core import QgsWkbTypes

import zipfile
import geopandas as gpd
from shapely.geometry import shape
import tempfile
import os

import os
import tempfile
from qgis.core import (
    QgsVectorLayer,
    QgsVectorFileWriter,
    QgsFeatureRequest,
    QgsProject,
)

from PyQt5.QtGui import QColor

# PyQt5 and QGIS imports / Importações PyQt5 e QGIS
from PyQt5.QtCore import QDate, Qt, QVariant, QSettings
from PyQt5.QtWidgets import (
    QApplication,
    QMainWindow,
    QLabel,
    QMessageBox,
    QFileDialog,
    QGridLayout,
    QWidget,
    QDesktopWidget,
    QDialog,
    QVBoxLayout,
    QCheckBox,
    QDialogButtonBox,
    QDateEdit,
    QScrollArea,
    QPushButton,
    QHBoxLayout,
    QToolButton,
    QTextBrowser,
)
from qgis.PyQt import uic, QtWidgets
from qgis.core import (
    QgsProject,
    QgsRasterLayer,
    QgsRasterShader,
    QgsColorRampShader,
    QgsSingleBandPseudoColorRenderer,
    QgsStyle,
    QgsRasterBandStats,
    QgsMapLayer,
    QgsVectorLayer,
    QgsColorRamp,
    QgsLayerTreeLayer,
    QgsCoordinateReferenceSystem,
    QgsCoordinateTransform,
    QgsMultiBandColorRenderer,
    QgsContrastEnhancement,
    QgsProcessingFeedback,
    QgsApplication,
    QgsRectangle,
    QgsFeature,
    QgsGeometry,
    QgsField,
    QgsVectorFileWriter,
)

from qgis.gui import QgsMapToolEmitPoint, QgsRubberBand

from qgis.utils import iface
import qgis

from PyQt5.QtWidgets import QSizePolicy

# Scientiffic and data processing libraries / Bibliotecas científicas e de
# processamento de dados
import pandas as pd
import numpy as np
from scipy.signal import savgol_filter
import geopandas as gpd
import requests
import os
import platform
import shutil
import processing


# Plotly imports for visualization / Importações Plotly para visualização
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots

import importlib
import urllib.request
import json
import sys
import os
from PyQt5 import uic

from qgis.core import QgsProject, QgsMapLayer, QgsWkbTypes


# =============================================================================
# Earth Engine API Installation and Version Check / Instalação da API Earth
# Engine e verificação de versão
# =============================================================================

from .modules import (
    ee_utils,
    map_tools,
    nasa_power,
    vegetation_index_info,
    save_utils,
    authentication
)


from .modules.coordinate_capture import CoordinateCaptureTool

try:
    import ee
except:
    pass

# =============================================================================
# RAVIDialog Class Definition / Definição da Classe RAVIDialog
# =============================================================================

# Load the .ui file based on the language setting / Carrega o arquivo .ui com
# base na configuração de idioma
language = QSettings().value("locale/userLocale", "en")[0:2]

if language == "pt":
    ui_file = "ravi_dialog_base_pt.ui"
else:
    ui_file = "ravi_dialog_base.ui"

FORM_CLASS, _ = uic.loadUiType(os.path.join(os.path.dirname(__file__), ui_file))


class RAVIDialog(QtWidgets.QDialog, FORM_CLASS):
    def __init__(self, parent=None):
        """Constructor."""
        super(RAVIDialog, self).__init__(parent)
     
        self.setWindowFlags(Qt.Window | Qt.WindowMinimizeButtonHint)
        super().__init__(
            None,
            Qt.WindowStaysOnTopHint
            | Qt.WindowMinimizeButtonHint
            | Qt.WindowCloseButtonHint,
        )

        self.setupUi(self)  # #widgets-and-dialogs-with-auto-connect

        authentication.loadProjectId(self)

        self.coordinate_capture_tool = None

        # Find the checkbox in the UI / Encontra a checkbox na UI
        self.checkBox_captureCoordinates = self.findChild(
            QtWidgets.QCheckBox, "checkBox_captureCoordinates"
        )

        if self.checkBox_captureCoordinates:
            self.checkBox_captureCoordinates.stateChanged.connect(
                self.toggle_coordinate_capture_tool
            )
        else:
            print("Error: Capture coordinates checkbox not found in the UI.")

        # Determine language for UI elements / Determina o idioma para os
        # elementos da UI
        self.language = QSettings().value("locale/userLocale", "en")[0:2]

        # Initialize variables / Inicializa variáveis
        self.plot1 = None
        self.autentication = False
        self.folder_set = False
        self.inicio = None
        self.final = None
        self.nuvem = None
        self.vector_path = None
        self.aoi = None
        self.aoi_ckecked = False
        self.df = None
        self.recorte_datas = None
        self.df_aux = None
        self.selected_dates = []
        self.output_folder = None
        self.df_nasa = None
        self.df_points = None
        self.daily_precipitation = None
        self.selected_aio_layer_path = None

        # UI setup and signal connections / Configuração da UI e conexões de sinal
        self.setup_ui()
        self.connect_signals()
        authentication.loadProjectId(self)

        self.combo_year.addItems(
            [str(year) for year in range(2017, datetime.now().year + 1)]
        )

        # Set default values / Define valores padrão
        self.last_clicked(3)
        self.index_explain()
        self.tabWidget.setCurrentIndex(0)
        self.resizeEvent("small")

    def setup_ui(self):
        """Initial UI setup."""
        """Configuração inicial da UI."""
        self.textEdit.setReadOnly(True)  # Prevent editing / Impede a edição
        self.textEdit.setTextInteractionFlags(Qt.TextBrowserInteraction)
        self.textBrowser_valid_pixels.setReadOnly(True)
        self.textBrowser_valid_pixels.setTextInteractionFlags(
            Qt.TextBrowserInteraction
        )
        self.project_QgsPasswordLineEdit.setEchoMode(QtWidgets.QLineEdit.Normal)


    def connect_signals(self):
        """Connect UI signals to their respective slots."""
        """Conecta os sinais da UI aos seus respectivos slots."""

        self.autenticacao.clicked.connect(lambda: authentication.auth(self))
        self.desautenticacao.clicked.connect(lambda: authentication.auth_clear(self))
        
        # Connect the textChanged signal to the autoSaveProjectId function
        self.project_QgsPasswordLineEdit.textChanged.connect(
            lambda new_text: authentication.autoSaveProjectId(self, new_text)
        )

        self.clear_button_points.clicked.connect(self.remove_all_dots)
        self.QPushButton_features.clicked.connect(self.QPushButton_features_clicked)
        self.update_vector.clicked.connect(self.update_vector_clicked)
        self.update_vector_2.clicked.connect(self.update_vector_clicked)
        self.tabWidget.currentChanged.connect(self.on_tab_changed)
        self.load_1index.clicked.connect(self.load_index)
        self.load_1rgb.clicked.connect(self.load_rgb)
        self.composicao.clicked.connect(self.composite_clicked)
        self.clear_raster.clicked.connect(self.clear_all_raster_layers)
        self.hybrid.clicked.connect(map_tools.hybrid_function)
        self.QPushButton_next.clicked.connect(self.next_clicked)
        self.QPushButton_next_2.clicked.connect(self.next_clicked)
        self.QPushButton_next_3.clicked.connect(self.next_clicked)
        self.QPushButton_next_4.clicked.connect(self.next_clicked)
        self.QPushButton_next_5.clicked.connect(self.next_clicked)
        self.QPushButton_next_6.clicked.connect(self.next_clicked)
        self.QPushButton_next_7.clicked.connect(self.next_clicked)
        self.QPushButton_back.clicked.connect(self.back_clicked)
        self.QPushButton_back_2.clicked.connect(self.back_clicked)
        self.QPushButton_back_3.clicked.connect(self.back_clicked)
        self.QPushButton_back_4.clicked.connect(self.back_clicked)
        self.QPushButton_back_5.clicked.connect(self.back_clicked)
        self.QPushButton_back_6.clicked.connect(self.back_clicked)
        self.QPushButton_back_7.clicked.connect(self.back_clicked)
        self.QPushButton_back_8.clicked.connect(self.back_clicked)
        self.loadtimeseries.clicked.connect(self.loadtimeseries_clicked)
        self.loadtimeseries_2.clicked.connect(self.loadtimeseries_clicked)
        self.navegador.clicked.connect(self.open_browser)
        self.navegador_2.clicked.connect(self.open_browser_2)
        self.navegador_3.clicked.connect(self.open_browser_3)
        self.datasrecorte.clicked.connect(self.datasrecorte_clicked)
        self.datasrecorte_2.clicked.connect(self.datasrecorte_clicked)
        self.datasrecorte_3.clicked.connect(self.datasrecorte_clicked)

        self.salvar.clicked.connect(self.salvar_clicked)
        self.salvar_2.clicked.connect(self.salvar_clicked_2)
        self.salvar_3.clicked.connect(self.salvar_clicked_3)
        self.salvar_nasa.clicked.connect(self.salvar_nasa_clicked)
        self.build_vector_layer.clicked.connect(self.build_vector_layer_clicked)
        self.QCheckBox_sav_filter.stateChanged.connect(self.plot_timeseries)
        self.filtro_grau.currentIndexChanged.connect(self.plot_timeseries)
        self.window_len.currentIndexChanged.connect(self.plot_timeseries)
        self.vector_layer_combobox.currentIndexChanged.connect(
            self.get_selected_layer_path
        )
        self.mQgsFileWidget.fileChanged.connect(self.on_file_changed)
        self.radioButton_all.clicked.connect(self.all_clicked)
        self.radioButton_3months.clicked.connect(lambda: self.last_clicked(3))
        self.radioButton_6months.clicked.connect(lambda: self.last_clicked(6))
        self.radioButton_12months.clicked.connect(lambda: self.last_clicked(12))
        self.radioButton_3years.clicked.connect(lambda: self.last_clicked(3 * 12))
        self.radioButton_5years.clicked.connect(lambda: self.last_clicked(5 * 12))
        self.combo_year.currentIndexChanged.connect(self.selected_year_clicked)
        self.horizontalSlider_local_pixel_limit.valueChanged.connect(
            self.update_labels
        )
        self.horizontalSlider_aio_cover.valueChanged.connect(self.update_labels)
        self.horizontalSlider_buffer.valueChanged.connect(self.update_labels)
        self.horizontalSlider_total_pixel_limit.valueChanged.connect(
            self.update_labels
        )
        self.series_indice_2.currentIndexChanged.connect(self.reload_update2)
        self.series_indice.currentIndexChanged.connect(self.reload_update)
        self.incioedit_2.dateChanged.connect(self.reload_update2)
        self.finaledit_2.dateChanged.connect(self.reload_update2)
        self.incioedit.dateChanged.connect(self.reload_update)
        self.finaledit.dateChanged.connect(self.reload_update)
        self.vector_layer_combobox_2.currentIndexChanged.connect(
            self.combobox_2_update
        )
        self.nasapower.clicked.connect(self.nasapower_clicked)
        self.clear_nasa.clicked.connect(self.clear_nasa_clicked)
        self.textEdit.anchorClicked.connect(self.open_link)
        self.textBrowser_valid_pixels.anchorClicked.connect(self.open_link)
        self.series_indice.currentIndexChanged.connect(self.index_explain)



    def nasapower_clicked(self):
        """Handles the event when the "NASA POWER" button is clicked."""
        """Manipula o evento quando o botão "NASA POWER" é clicado."""
        # Get the latitude and longitude from the UI / Obtém a latitude e
        # longitude da UI
        self.find_centroid()
        self.df_nasa, self.daily_precipitation = nasa_power.open_nasapower(
            str(self.lat),
            str(self.lon),
            self.df_aux.date.tolist()[0],
            self.df_aux.date.tolist()[-1],
        )
        self.plot_timeseries()

    def toggle_coordinate_capture_tool(self, state):
        """Toggles the coordinate capture tool based on the checkbox state."""
        """Ativa/desativa a ferramenta de captura de coordenadas com base no
        estado da checkbox."""
        canvas = iface.mapCanvas()
        if state == Qt.Checked:  # Checkbox is checked (active) / Checkbox
            # está marcada (ativa)
            # Deactivate any existing tool before activating the new one /
            # Desativa qualquer ferramenta existente antes de ativar a nova
            if canvas.mapTool():
                canvas.unsetMapTool(canvas.mapTool())
            if not self.coordinate_capture_tool:  # Only create if it doesn't exist / Cria somente se não existir
                self.coordinate_capture_tool = CoordinateCaptureTool(canvas, self)
                print("CoordinateCaptureTool created")
            canvas.setMapTool(self.coordinate_capture_tool)
            print("Coordinate capture tool activated.")
            print(f"self.coordinate_capture_tool: {self.coordinate_capture_tool}")
            self.sentinel2_selected_dates_update()
            #self.crs_transform()
        else:  # Checkbox is unchecked (inactive) / Checkbox não está marcada
            # (inativa)
            self.deactivate_coordinate_capture_tool()
            print("Coordinate capture tool deactivated.")
            print(f"self.coordinate_capture_tool: {self.coordinate_capture_tool}")

    def activate_coordinate_capture_tool(self):
        canvas = iface.mapCanvas()
        if (
            self.checkBox_captureCoordinates.isChecked()
        ):  # Button is checked (active)
            # Deactivate any existing tool before activating the new one
            if canvas.mapTool():
                canvas.unsetMapTool(canvas.mapTool())
            self.coordinate_capture_tool = CoordinateCaptureTool(canvas, self)
            canvas.setMapTool(self.coordinate_capture_tool)
            print("Coordinate capture tool activated.")
        else:  # Button is unchecked (inactive)
            self.deactivate_coordinate_capture_tool()
            print("Coordinate capture tool deactivated.")

    def deactivate_coordinate_capture_tool(self):
        """Deactivates the coordinate capture map tool."""
        """Desativa a ferramenta de mapa de captura de coordenadas."""
        print("deactivate_coordinate_capture_tool called")
        if (
            hasattr(self, "coordinate_capture_tool")
            and self.coordinate_capture_tool
        ):
            canvas = iface.mapCanvas()
            canvas.unsetMapTool(self.coordinate_capture_tool)

            # self.coordinate_capture_tool = None  # Deactivate the tool
            print("CoordinateCaptureTool deactivated")
            print(f"self.coordinate_capture_tool: {self.coordinate_capture_tool}")
        else:
            print("No coordinate capture tool to deactivate.")

    def remove_all_dots(self):
        """Removes all dots from the map canvas."""
        print("remove_all_dots called")
        canvas = iface.mapCanvas()

        if not self.coordinate_capture_tool:
            print("No coordinate capture tool active.")
            return  # Exit if no tool is active

        rubberBands = self.coordinate_capture_tool.rubber_bands

        if not rubberBands:
            print("No dots to remove.")
            return

        for rubberBand in list(rubberBands):
            try:
                canvas.scene().removeItem(rubberBand)
            except Exception as e:
                print(f"Error removing rubber band: {e}")

        iface.mapCanvas().refresh()  # Refresh the canvas

        # Clear dataframes and web view
        self.df_points = None
        self.df_aux_points = None
        self.webView_3.setHtml("")

        print("All dots removed.")

        # Clear the list in the tool
        self.coordinate_capture_tool.rubber_bands = []


    # =========================================================================
    # Project ID Management / Gerenciamento do ID do Projeto
    # =========================================================================

    def process_coordinates(self, longitude, latitude):
        """Processes the captured coordinates with Earth Engine."""
        """Processa as coordenadas capturadas com o Earth Engine."""
        QApplication.setOverrideCursor(Qt.WaitCursor)
        try:
            # 1. Define the point (longitude, latitude) / Define o ponto
            # (longitude, latitude)
            point = ee.Geometry.Point(longitude, latitude)

            # 2. Buffer the point to create a *very* small polygon (e.g., 2
            # meters) / Cria um buffer no ponto para criar um polígono *muito*
            # pequeno (ex: 2 metros)
            aoi = point.buffer(2)

            # Do something with the aoi (e.g., print its information) / Faz
            # algo com a aoi (ex: imprime suas informações)
            print(f"AOI (point) defined")
            name = f"({round(latitude,5)},{round(longitude,5)})"
            print("point:", name)
            # print(self.point_calculate_timeseries(aoi, name))

            if self.df_points is None:
                self.df_points = self.df_aux[["date", "AOI_average"]]
                self.df_points["date"] = pd.to_datetime(self.df_points["date"])

            new_df = self.point_calculate_timeseries(aoi, name)
            new_df["date"] = pd.to_datetime(new_df["date"])
            print(new_df)
            self.df_points = pd.merge(
                self.df_points, new_df, on="date", how="outer"
            )

            self.df_ajust_points()
            self.plot_timeseries_points()
        except Exception as e:
            QApplication.restoreOverrideCursor()
            print(f"Error processing with Earth Engine: {e}")
            QMessageBox.critical(
                self,
                "Earth Engine Error",
                f"An error occurred: {e}",
            )
        QApplication.restoreOverrideCursor()

    def plot_timeseries_points(self):
        print("Plotting time series for points...")

        df = self.df_aux_points
        print(df.shape)

        # Melt the dataframe to have a long format / Transforma o dataframe para
        # um formato longo
        df_melted = df.melt(
            id_vars="date",
            var_name="Points (lat, long)",
            value_name=self.series_indice.currentText(),
        )

        # Create the line plot with varied color and line dash / Cria o gráfico
        # de linha com cor e traço variados
        fig = px.line(
            df_melted,
            x="date",
            y=self.series_indice.currentText(),
            color="Points (lat, long)",
            line_dash="Points (lat, long)",
            title=f"Time Series - {self.series_indice.currentText()} - Points",
        )
        fig.update_layout(
            yaxis_title=self.series_indice.currentText(),
            title=f"Time Series - {self.series_indice.currentText()} - Points",
            xaxis_title=None,  # Remove x-axis label
        )
        self.fig_3 = fig
        # fig.show()

        self.webView_3.setHtml(
            fig.to_html(include_plotlyjs="cdn", config=self.config)
        )
        print("Feature info calculated and plotted.")

    # =========================================================================

    def load_fields(self, id_column=None):
        # Get the system's temporary directory / Obtém o diretório temporário do
        # sistema
        temp_folder = tempfile.gettempdir()
        print(f"Temporary folder: {temp_folder}")

        # Input shapefile path / Caminho do shapefile de entrada
        input_path = self.selected_aio_layer_path

        # Open the layer / Abre a camada
        layer = QgsVectorLayer(input_path, "Polygons", "ogr")
        if not layer.isValid():
            print("Failed to load the layer.")
            return

        # Populate the attributes_id combobox with unique field names /
        # Popula a combobox attributes_id com nomes de campos únicos
        unique_fields = [field.name() for field in layer.fields()]

        self.attributes_id.clear()
        self.attributes_id.addItems(sorted(unique_fields))

    def QPushButton_features_clicked(self):
        QApplication.setOverrideCursor(Qt.WaitCursor)
        self.sentinel2_selected_dates_update()
        feature_info = self.split_features(self.attributes_id.currentText())
        self.feature_info(feature_info)
        self.df_ajust_features()
        self.plot_timeseries_features()
        QApplication.restoreOverrideCursor()

    def split_features(self, id_column=None):
        """
        Splits each feature from the selected vector layer into individual
        shapefiles, using a specified attribute column for identification.

        Args:
            id_column (str): Name of the attribute column to use for
                identification. If None, feature ID will be used.

        Returns:
            dict: Dictionary mapping feature identifiers to their shapefile paths
        """
        """
        Divide cada feição da camada vetorial selecionada em shapefiles
        individuais, usando uma coluna de atributo especificada para
        identificação.

        Args:
            id_column (str): Nome da coluna de atributo a ser usada para
                identificação. Se None, o ID da feição será usado.

        Returns:
            dict: Dicionário mapeando identificadores de feição para seus
                caminhos de shapefile
        """

        # Get the system's temporary directory / Obtém o diretório temporário do
        # sistema
        temp_folder = tempfile.gettempdir()
        print(f"Temporary folder: {temp_folder}")

        # Input shapefile path / Caminho do shapefile de entrada
        input_path = self.selected_aio_layer_path

        # Open the layer / Abre a camada
        layer = QgsVectorLayer(input_path, "Polygons", "ogr")

        # Validate id_column if provided / Valida id_column se fornecido
        if (
            id_column
            and id_column not in [field.name() for field in layer.fields()]
        ):
            raise ValueError(
                f"Column '{id_column}' not found in layer attributes"
            )

        feature_count = layer.featureCount()
        print(f"Total features to process: {feature_count}")

        # Dictionary to store feature identifiers and their corresponding paths /
        # Dicionário para armazenar identificadores de feição e seus caminhos
        # correspondentes
        feature_info = {}

        # Process each feature / Processa cada feição
        for feature in layer.getFeatures():
            # Get feature identifier / Obtém o identificador da feição
            if id_column:
                feature_identifier = str(feature[id_column])
                # Clean the identifier for use in filename / Limpa o
                # identificador para uso no nome do arquivo
                feature_identifier = "".join(
                    c for c in feature_identifier if c.isalnum() or c in ("-", "_")
                )
            else:
                feature_identifier = str(feature.id())

            # Create output filename using the identifier / Cria o nome do
            # arquivo de saída usando o identificador
            output_path = os.path.join(
                temp_folder, f"feature_{feature_identifier}.shp"
            )

            # Create a memory layer with the same attributes / Cria uma camada
            # de memória com os mesmos atributos
            memory_layer = QgsVectorLayer(
                f"Polygon?crs={layer.crs().authid()}",
                f"feature_{feature_identifier}",
                "memory",
            )

            # Set up the memory layer / Configura a camada de memória
            memory_layer.startEditing()
            memory_provider = memory_layer.dataProvider()
            memory_provider.addAttributes(layer.fields())
            memory_layer.updateFields()
            memory_provider.addFeature(feature)
            memory_layer.commitChanges()

            try:
                # Save the memory layer as a new shapefile / Salva a camada de
                # memória como um novo shapefile
                save_options = QgsVectorFileWriter.SaveVectorOptions()
                save_options.driverName = "ESRI Shapefile"

                error = QgsVectorFileWriter.writeAsVectorFormat(
                    memory_layer,
                    output_path,
                    "UTF-8",
                    layer.crs(),
                    "ESRI Shapefile",
                )

                if error[0] != QgsVectorFileWriter.NoError:
                    print(f"Error saving feature {feature_identifier}: {error}")
                else:
                    print(
                        f"Feature {feature_identifier} saved to: {output_path}"
                    )
                    # Store the feature information / Armazena as informações da
                    # feição
                    feature_info[feature_identifier] = {
                        "path": output_path,
                        "attributes": {
                            field.name(): feature[field.name()]
                            for field in layer.fields()
                        },
                    }

            except Exception as e:
                print(f"Error saving feature {feature_identifier}: {str(e)}")
            finally:
                # Clean up memory layer / Limpa a camada de memória
                del memory_layer

        print("\nFinished processing all features.")
        print("Generated feature information:")
        for identifier, info in feature_info.items():
            print(f"\nFeature: {identifier}")
            print(f"Path: {info['path']}")
            print("Attributes:")
            for attr_name, attr_value in info["attributes"].items():
                print(f"  {attr_name}: {attr_value}")

        return feature_info

    def feature_info(self, feature_info):
        features = []

        for feature_id, info in feature_info.items():
            # Get the name from attributes, handle cases where 'NAME' might not
            # exist / Obtém o nome dos atributos, lida com casos onde 'NAME'
            # pode não existir
            print(f"Feature ID: {self.attributes_id.currentText()}")
            name = info["attributes"].get(
                self.attributes_id.currentText(), "N/A"
            )

            path = info["path"]

            self.aoi_feature = self.load_vector_function(path)
            df_feature = self.feature_calculate_timeseries(name)
            df_feature["date"] = pd.to_datetime(df_feature["date"])
            features.append(df_feature)

        # Merge all dataframes in the features list horizontally on the 'date'
        # key / Mescla todos os dataframes na lista de feições horizontalmente
        # na chave 'date'

        df_aux = self.df_aux[["date", "AOI_average"]]
        df_aux.date = pd.to_datetime(df_aux.date)

        features.append(df_aux)
        merged_df = features[0]
        for df in features[1:]:
            merged_df = pd.merge(merged_df, df, on="date", how="outer")

        print(f"Merged features calculated for {len(features)} features.")
        df = merged_df.copy()

        df["date"] = pd.to_datetime(df["date"])

        self.df_features = df

    def plot_timeseries_features(self):
        df = self.df_aux_features

        # Melt the dataframe to have a long format / Transforma o dataframe para
        # um formato longo
        df_melted = df.melt(
            id_vars="date",
            var_name="Polygon",
            value_name=self.series_indice.currentText(),
        )

        # Create the line plot with varied color and line dash / Cria o gráfico
        # de linha com cor e traço variados
        fig = px.line(
            df_melted,
            x="date",
            y=self.series_indice.currentText(),
            color="Polygon",
            line_dash="Polygon",
            title=f"Time Series - {self.series_indice.currentText()} - {self.vector_layer_combobox.currentText()}",
        )
        fig.update_layout(
            yaxis_title=self.series_indice.currentText(),
            title=f"Time Series - {self.series_indice.currentText()} - {self.vector_layer_combobox.currentText()}",
            xaxis_title=None,  # Remove x-axis label
        )
        self.fig_2 = fig

        self.webView_2.setHtml(
            fig.to_html(include_plotlyjs="cdn", config=self.config)
        )
        print("Feature info calculated and plotted.")

    def combobox_2_update(self):
        self.vector_layer_combobox.setCurrentIndex(
            self.vector_layer_combobox_2.currentIndex()
        )

    def reload_update2(self):
        self.finaledit.setDate(self.finaledit_2.date())
        self.incioedit.setDate(self.incioedit_2.date())
        self.series_indice.setCurrentIndex(self.series_indice_2.currentIndex())

    def reload_update(self):
        self.finaledit_2.setDate(self.finaledit.date())
        self.incioedit_2.setDate(self.incioedit.date())
        self.series_indice_2.setCurrentIndex(self.series_indice.currentIndex())

    def clear_nasa_clicked(self):
        """Clears the NASA data and updates the timeseries plot."""
        """Limpa os dados da NASA e atualiza o gráfico de séries temporais."""
        self.df_nasa = None
        self.plot_timeseries()

    def update_labels(self):
        """Updates the text of several labels based on the values of horizontal
        sliders."""
        """Atualiza o texto de vários rótulos com base nos valores dos
        sliders horizontais."""
        self.label_cloud_aoi.setText(
            f"{self.horizontalSlider_local_pixel_limit.value()}%"
        )
        self.label_coverage.setText(f"{self.horizontalSlider_aio_cover.value()}%")
        self.label_cloud.setText(
            f"{self.horizontalSlider_total_pixel_limit.value()}%"
        )
        self.label_buffer.setText(f"{self.horizontalSlider_buffer.value()}m")

    def custom_filter_clicked(self):
        """Slot method to handle the custom filter checkbox click event."""
        """Método slot para lidar com o evento de clique da checkbox de filtro
        personalizado."""
        if self.customfilter.isChecked():
            self.horizontalSlider.setEnabled(True)
        else:
            self.horizontalSlider.setEnabled(False)

    def open_link(self, url):
        """Open the clicked link in the default web browser."""
        """Abre o link clicado no navegador padrão."""
        print(f"Opening URL: {url.toString()}")
        webbrowser.open(url.toString())

    def last_clicked(self, months):
        today = datetime.today().strftime("%Y-%m-%d")
        one_month_ago = (datetime.today() - relativedelta(months=months)).strftime(
            "%Y-%m-%d"
        )
        self.finaledit.setDate(QDate.fromString(today, "yyyy-MM-dd"))
        self.incioedit.setDate(QDate.fromString(one_month_ago, "yyyy-MM-dd"))

    def all_clicked(self):
        today = datetime.today().strftime("%Y-%m-%d")
        since = "2017-03-28"
        self.finaledit.setDate(QDate.fromString(today, "yyyy-MM-dd"))
        self.incioedit.setDate(QDate.fromString(since, "yyyy-MM-dd"))

    def selected_year_clicked(self):
        """Sets the date range in the UI to the selected year from the combo
        box."""
        """Define o intervalo de datas na UI para o ano selecionado na combo
        box."""
        year = self.combo_year.currentText()
        start = f"{year}-01-01"
        end = f"{year}-12-31"
        self.incioedit.setDate(QDate.fromString(start, "yyyy-MM-dd"))
        self.finaledit.setDate(QDate.fromString(end, "yyyy-MM-dd"))

    def build_vector_layer_clicked(self):
        self.vector_builder()
        self.load_vector_layers()
        self.get_selected_layer_path()
        self.load_vector_function()
        self.find_area()


    def crs_transform(self):
        project = QgsProject.instance()
        project.setCrs(QgsCoordinateReferenceSystem("EPSG:4326"))
        print("CRS set to EPSG:4326")

    def vector_builder(self):
        """Handles the event when the "Build Vector Layer" button is clicked."""
        if self.output_folder is None:
            self.pop_warning("Please select a output folder first.")
            return

        existing_layers = QgsProject.instance().mapLayers().values()
        layer_names = [layer.name() for layer in existing_layers]
        if "Google Hybrid" not in layer_names:
            self.pop_warning("Please load the Google Hybrid layer first.")
            return

        # Get the canvas and its CRS
        canvas = iface.mapCanvas()
        canvas_crs = canvas.mapSettings().destinationCrs()
        target_crs = QgsCoordinateReferenceSystem("EPSG:4326")
        
        # Get the extent in canvas CRS
        extent = canvas.extent()
        
        # Create transformer from canvas CRS to EPSG:4326
        transform = QgsCoordinateTransform(
            canvas_crs,
            target_crs,
            QgsProject.instance()
        )
        
        # Transform the extent to EPSG:4326
        extent_4326 = transform.transformBoundingBox(extent)

        # Create a vector layer in EPSG:4326
        layer = QgsVectorLayer(
            "Polygon?crs=EPSG:4326",
            "canvas_extent",
            "memory"
        )
        pr = layer.dataProvider()

        # Add fields
        pr.addAttributes([QgsField("id", QVariant.Int)])
        layer.updateFields()

        # Create a feature with the transformed extent
        feature = QgsFeature()
        geometry = QgsGeometry.fromRect(extent_4326)
        feature.setGeometry(geometry)
        feature.setAttributes([1])
        pr.addFeature(feature)

        # Update layer extents
        layer.updateExtents()

        # Generate unique filename and save
        shp_path = self.get_unique_filename("canvas_extent.shp")
        print(f"Shapefile path: {shp_path}")
        shp_name = os.path.basename(shp_path).replace(".shp", "")
        print(f"Shapefile name: {shp_name}")

        # Write the layer to disk
        save_options = QgsVectorFileWriter.SaveVectorOptions()
        save_options.driverName = "ESRI Shapefile"
        save_options.fileEncoding = "UTF-8"
        
        QgsVectorFileWriter.writeAsVectorFormat(
            layer,
            shp_path,
            save_options
        )

        # Load the shapefile
        loaded_layer = QgsVectorLayer(shp_path, shp_name, "ogr")
        if loaded_layer.isValid():
            # Verify CRS
            if loaded_layer.crs().authid() != "EPSG:4326":
                print("Warning: Layer CRS is not EPSG:4326, reprojecting...")
                # Reproject if necessary
                params = {
                    'INPUT': loaded_layer,
                    'TARGET_CRS': target_crs,
                    'OUTPUT': 'memory:'
                }
                reprojected = processing.run("native:reprojectlayer", params)['OUTPUT']
                loaded_layer = reprojected

            QgsProject.instance().addMapLayer(loaded_layer)
            print(f"Layer added successfully with CRS: {loaded_layer.crs().authid()}")
        else:
            print("Failed to load the shapefile.")
            self.pop_warning("Failed to load the shapefile.")


    def salvar_clicked(self):
        """Handles the event when the save button is clicked."""
        """Manipula o evento quando o botão salvar é clicado."""
        df = self.df_aux
        try:
            df = df[["date", "AOI_average", "savitzky_golay_filtered", "image_id"]]
        except:
            df = df[["date", "AOI_average", "image_id"]]

        name = (
            f"{self.series_indice.currentText()}_{self.vector_layer_combobox.currentText()}_time_series.csv"
        )
        save_utils.save(df, name, self)

    def salvar_clicked_2(self):
        """Handles the event when the save button is clicked."""
        """Manipula o evento quando o botão salvar é clicado."""
        df = self.df_features
        name = (
            f"{self.series_indice.currentText()}_{self.vector_layer_combobox.currentText()}_time_series_features.csv"
        )
        save_utils.save(df, name, self)

    def salvar_clicked_3(self):
        """Handles the event when the save button is clicked."""
        """Manipula o evento quando o botão salvar é clicado."""
        df = self.df_points
        name = (
            f"{self.series_indice.currentText()}_{self.vector_layer_combobox.currentText()}_time_series_points.csv"
        )
        save_utils.save(df, name, self)

    def salvar_nasa_clicked(self):
        if self.df_nasa is None:
            self.pop_warning("No NASA data to save.")
            return
        name = (
            f"nasa_power_precipitation_{self.vector_layer_combobox.currentText()}.csv"
        )

        save_utils.save(self.daily_precipitation, name, self)

    def datasrecorte_clicked(self):
        """Opens a dialog for selecting specific dates for the time series."""
        """Abre um diálogo para selecionar datas específicas para a série
        temporal."""
        dialog = QDialog(self)
        dialog.setWindowTitle("Date Selection for Time Series")
        dialog.setGeometry(100, 100, 400, 500)

        layout = QVBoxLayout(dialog)

        # Scroll Area for Checkboxes / Área de Scroll para Checkboxes
        scroll_area = QScrollArea(dialog)
        scroll_area.setWidgetResizable(True)
        scroll_content = QWidget()
        scroll_layout = QVBoxLayout(scroll_content)

        self.checkboxes = []
        self.group_checkboxes = {}  # Store month group checkboxes
        self.year_checkboxes = {}  # Store year checkboxes
        self.group_widgets = {}  # Store month group content widgets

        # Group Dates by Year and Month / Agrupa Datas por Ano e Mês
        self.df["date"] = pd.to_datetime(
            self.df["date"]
        )  # Ensure dates are datetime objects
        grouped = self.df.groupby([self.df["date"].dt.year, self.df["date"].dt.month])

        # Organize by Year / Organiza por Ano
        years = self.df["date"].dt.year.unique()
        for year in sorted(years):
            # Create a year-level widget / Cria um widget de nível de ano
            year_widget = QWidget(dialog)
            year_layout = QVBoxLayout(year_widget)

            # Year-level checkbox (above all content for the year) / Checkbox
            # de nível de ano (acima de todo o conteúdo para o ano)
            year_checkbox = QCheckBox(f"Select All in {year}", dialog)
            year_checkbox.setChecked(
                True
                if self.recorte_datas is None
                else all(
                    str(date.date()) in self.recorte_datas
                    for date in self.df[self.df["date"].dt.year == year]["date"]
                )
            )
            year_checkbox.stateChanged.connect(
                lambda state, yr=year: self.toggle_year_checkboxes(yr, state)
            )
            scroll_layout.addWidget(year_checkbox)
            self.year_checkboxes[year] = year_checkbox

            # Indented content for the year / Conteúdo indentado para o ano
            year_content_widget = QWidget(dialog)
            year_content_layout = QVBoxLayout(year_content_widget)
            year_content_layout.setContentsMargins(
                20, 0, 0, 0
            )  # Add indentation for year content
            scroll_layout.addWidget(year_content_widget)

            # Add months under each year / Adiciona meses sob cada ano
            for (group_year, month), group in grouped:
                if group_year != year:
                    continue

                # Create a month-level widget / Cria um widget de nível de mês
                group_label = f"{group_year}-{month:02d}"
                month_widget = QWidget(dialog)
                month_layout = QVBoxLayout(month_widget)

                # Month toggle button / Botão de alternância do mês
                month_toggle_button = QToolButton(dialog)
                month_toggle_button.setText(f"▶ {group_label}")
                month_toggle_button.setCheckable(True)
                month_toggle_button.setChecked(True)
                month_toggle_button.setStyleSheet("text-align: left;")
                month_toggle_button.toggled.connect(
                    lambda checked, grp=month_widget, btn=month_toggle_button, lbl=group_label: self.toggle_group_visibility(
                        grp, btn, lbl
                    )
                )

                # Month-level checkbox / Checkbox de nível de mês
                group_checkbox = QCheckBox(f"Select All in {group_label}", dialog)
                group_checkbox.setChecked(
                    True
                    if self.recorte_datas is None
                    else all(
                        str(date.date()) in self.recorte_datas for date in group["date"]
                    )
                )
                group_checkbox.stateChanged.connect(
                    lambda state, grp=group_label: self.toggle_group_checkboxes(
                        grp, state
                    )
                )
                month_layout.addWidget(group_checkbox)
                self.group_checkboxes[group_label] = group_checkbox

                # Add individual checkboxes with further indentation / Adiciona
                # checkboxes individuais com mais indentação
                for date in group["date"]:
                    date_str = str(date.date())
                    checkbox = QCheckBox(date_str, dialog)
                    checkbox.setChecked(
                        True
                        if self.recorte_datas is None
                        else date_str in self.recorte_datas
                    )
                    month_layout.addWidget(checkbox)
                    checkbox.setContentsMargins(
                        20, 0, 0, 0
                    )  # Further indentation for dates
                    self.checkboxes.append((checkbox, group_label, group_year))

                # Add month layout to the year content layout / Adiciona o layout
                # do mês ao layout do conteúdo do ano
                year_content_layout.addWidget(month_toggle_button)
                year_content_layout.addWidget(month_widget)
                self.group_widgets[group_label] = month_widget

        scroll_content.setLayout(scroll_layout)
        scroll_area.setWidget(scroll_content)
        layout.addWidget(scroll_area)

        # Buttons / Botões
        button_box = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel, dialog
        )
        button_layout = QHBoxLayout()
        apply_button = QPushButton("Apply", dialog)
        select_button = QPushButton("Select All", dialog)
        deselect_button = QPushButton("Deselect All", dialog)
        button_layout.addWidget(apply_button)
        button_layout.addWidget(select_button)
        button_layout.addWidget(deselect_button)
        layout.addLayout(button_layout)
        layout.addWidget(button_box)

        # Signal Connections / Conexões de Sinal
        button_box.accepted.connect(dialog.accept)
        button_box.rejected.connect(dialog.reject)
        apply_button.clicked.connect(
            self.apply_changes
        )  # Apply without closing / Aplica sem fechar
        select_button.clicked.connect(self.select_all_checkboxes)
        deselect_button.clicked.connect(self.deselect_all_checkboxes)

        if dialog.exec_() == QDialog.Accepted:
            self.apply_changes()  # Ensure changes are applied before closing
        else:
            print("Time series dialog canceled. No changes made.")

    def apply_changes(self):
        """
        Apply changes to the selected dates without closing the dialog.

        This method updates the selected dates based on the checked checkboxes
        and adjusts the time series plot accordingly.
        """
        """
        Aplica as mudanças nas datas selecionadas sem fechar o diálogo.

        Este método atualiza as datas selecionadas com base nas checkboxes
        marcadas e ajusta o gráfico de séries temporais de acordo.
        """
        self.selected_dates = [
            cb.text() for cb, _, _ in self.checkboxes if cb.isChecked()
        ]
        self.recorte_datas = self.selected_dates
        print(f"Selected dates for time series (applied): {self.recorte_datas}")
        self.df_ajust()
        self.plot_timeseries()

        try:
            self.df_ajust_features()
            self.plot_timeseries_features()
            print("Feature info updated and ploted).")
        except:
            pass

        try:
            self.df_ajust_points()
            self.plot_timeseries_points()
            print("Points info updated and ploted).")
        except:
            pass

    def toggle_group_visibility(self, group_widget, toggle_button, group_label):
        """
        Toggle the visibility of a group widget.

        Args:
            group_widget (QWidget): The widget representing the group.
            toggle_button (QToolButton): The button used to toggle the
                visibility.
            group_label (str): The label of the group.
        """
        """
        Alterna a visibilidade de um widget de grupo.

        Args:
            group_widget (QWidget): O widget representando o grupo.
            toggle_button (QToolButton): O botão usado para alternar a
                visibilidade.
            group_label (str): O rótulo do grupo.
        """
        group_widget.setVisible(toggle_button.isChecked())
        toggle_button.setText(
            f"▶ {group_label}"
            if not toggle_button.isChecked()
            else f"▼ {group_label}"
        )

    def toggle_group_checkboxes(self, group_label, state):
        """
        Toggle all checkboxes in a month group.

        Args:
            group_label (str): The label of the group.
            state (int): The state of the checkbox (checked or unchecked).
        """
        """
        Alterna todas as checkboxes em um grupo de mês.

        Args:
            group_label (str): O rótulo do grupo.
            state (int): O estado da checkbox (marcado ou desmarcado).
        """
        for checkbox, group, _ in self.checkboxes:
            if group == group_label:
                checkbox.setChecked(state == Qt.Checked)

    def toggle_year_checkboxes(self, year, state):
        """
        Toggle all checkboxes in a year group.

        Args:
            year (int): The year to toggle.
            state (int): The state of the checkbox (checked or unchecked).
        """
        """
        Alterna todas as checkboxes em um grupo de ano.

        Args:
            year (int): O ano a ser alternado.
            state (int): O estado da checkbox (marcado ou desmarcado).
        """
        for checkbox, _, group_year in self.checkboxes:
            if group_year == year:
                checkbox.setChecked(state == Qt.Checked)
        for group_label, group_checkbox in self.group_checkboxes.items():
            if group_label.startswith(str(year)):
                group_checkbox.setChecked(state == Qt.Checked)

    def select_all_checkboxes(self):
        """Select all checkboxes."""
        """Seleciona todas as checkboxes."""
        for checkbox, _, _ in self.checkboxes:
            checkbox.setChecked(True)
        for group_checkbox in self.group_checkboxes.values():
            group_checkbox.setChecked(True)
        for year_checkbox in self.year_checkboxes.values():
            year_checkbox.setChecked(True)

    def deselect_all_checkboxes(self):
        """Deselect all checkboxes."""
        """Desmarca todas as checkboxes."""
        for checkbox, _, _ in self.checkboxes:
            checkbox.setChecked(False)
        for group_checkbox in self.group_checkboxes.values():
            group_checkbox.setChecked(False)
        for year_checkbox in self.year_checkboxes.values():
            year_checkbox.setChecked(False)

    def centralizar(self):
        """
        Centers the window on the screen without changing the screen the dialog
        is on.

        This method calculates the geometry of the window frame and moves it to
        the center of the available screen space on the current screen.
        """
        """
        Centraliza a janela na tela sem alterar a tela em que o diálogo está.

        Este método calcula a geometria do quadro da janela e a move para o
        centro do espaço de tela disponível na tela atual.
        """
        # 1. Get the current geometry of the window frame.
        qtRectangle = self.frameGeometry()
        # 2. Determine the center point of the available screen space on the
        # current screen.
        screen = QDesktopWidget().screenNumber(self)
        centerPoint = QDesktopWidget().availableGeometry(screen).center()
        # 3. Move the center of the window frame to the center point of the
        # screen.
        qtRectangle.moveCenter(centerPoint)
        # 4. Move the window to the new top-left position.
        self.move(qtRectangle.topLeft())

    def resizeEvent(self, size):
        self.setMinimumSize(0, 0)  # Remove minimum size constraint
        self.setMaximumSize(16777215, 16777215)  # Rem

        if size == "small":
            self.resize(743, 373)
            self.setFixedSize(self.width(), self.height())  # Lock to small size
        elif size == "big":
            self.resize(1145, 582)
            self.setFixedSize(self.width(), self.height())  # Lock to big size

    def on_tab_changed(self, index):
        # print(f"Tab changed to index: {index}")
        if not self.autentication:
            self.tabWidget.setCurrentIndex(0)
            return
        if index < 9:
            self.resizeEvent("small")

        if index >= 9:
            self.resizeEvent("big")

        if index >= 9 and self.df is None:
            self.tabWidget.setCurrentIndex(8)
            return

        if index == 1 and not hasattr(self, "path_suggestion_loaded"):
            self.load_path_sugestion()
            self.path_suggestion_loaded = True

        if index == 2 and not hasattr(self, "vector_layers_loaded"):
            self.load_vector_layers()
            self.vector_layers_loaded = True
            try:
                self.load_vector_layers()
                self.get_selected_layer_path()
            except:
                pass

        if index > 1 and not self.QPushButton_next_4.isEnabled():
            self.tabWidget.setCurrentIndex(1)
            return
        
        if index > 2 and not self.QPushButton_next.isEnabled():
            self.tabWidget.setCurrentIndex(2)
            return

        if index == 11 and self.plot1 is not None:
            self.load_fields()

        if index != 10:
            try:
                self.checkBox_captureCoordinates.setChecked(False)
            except:
                pass

    def next_clicked(self):
        self.tabWidget.setCurrentIndex(
            (self.tabWidget.currentIndex() + 1) % self.tabWidget.count()
        )

    def back_clicked(self):
        self.tabWidget.setCurrentIndex(
            (self.tabWidget.currentIndex() - 1) % self.tabWidget.count()
        )

    def get_dates(self):
        # Get the date from the QDateEdit widget / Obtém a data do widget
        # QDateEdit
        self.inicio = self.incioedit.date().toString("yyyy-MM-dd")

        self.final = self.finaledit.date().toString("yyyy-MM-dd")
        # print(f"Selected date: {self.inicio} to {self.final}")

        # Get the date from the QDateEdit widget / Obtém a data do widget
        # QDateEdit

        # Get the selected text from the combobox / Obtém o texto selecionado
        # da combobox
        self.nuvem = self.horizontalSlider_total_pixel_limit.value()
        # print(f"Nuvem limit: {self.nuvem}")

    def load_path_sugestion(self):
        """
        Load the path suggestion based on the user's operating system.
        """
        """
        Carrega a sugestão de caminho com base no sistema operacional do
        usuário.
        """
        system = platform.system()
        if system == "Windows":
            self.output_folder = os.path.join(os.environ["USERPROFILE"], "Downloads")
        elif system == "Linux":
            self.output_folder = os.path.join(os.environ["HOME"], "Downloads")
        elif system == "Darwin":  # MacOS
            self.output_folder = os.path.join(os.environ["HOME"], "Downloads")

        # Pre-configure with a suggested directory / Pré-configura com um
        # diretório sugerido
        self.mQgsFileWidget.setFilePath(self.output_folder)

    def pop_warning(self, aviso):
        QApplication.restoreOverrideCursor()
        msg = QMessageBox(self)
        msg.setWindowTitle("Warning!")
        msg.setIcon(QMessageBox.Warning)
        msg.setText(aviso)
        msg.setStandardButtons(QMessageBox.Ok)
        msg.button(QMessageBox.Ok).setText("OK")
        msg.exec_()

    def update_vector_clicked(self):
        self.load_vector_layers()
        self.get_selected_layer_path()
        self.load_vector_function()

    def load_vector_layers(self):
        # Get all layers in the current QGIS project / Obtém todas as camadas
        # no projeto QGIS atual
        layers = list(QgsProject.instance().mapLayers().values())

        # Filter polygon and multipolygon vector layers / Filtra camadas
        # vetoriais de polígono e multipolígono
        vector_layers = [
            layer
            for layer in layers
            if layer.type() == QgsMapLayer.VectorLayer
            and layer.geometryType() == QgsWkbTypes.PolygonGeometry
        ]

        # Get current layer names / Obtém os nomes das camadas atuais
        current_layer_names = set(
            self.vector_layer_combobox.itemText(i)
            for i in range(self.vector_layer_combobox.count())
        )

        # Clear the combobox and the dictionary / Limpa a combobox e o
        # dicionário
        self.vector_layer_combobox.clear()
        self.vector_layer_ids = {}

        # Find the new layer while populating the combobox / Encontra a nova
        # camada enquanto popula a combobox
        new_layer_name = None
        for layer in vector_layers:
            layer_name = layer.name()
            self.vector_layer_combobox.addItem(layer_name)
            self.vector_layer_ids[layer_name] = layer.id()

            # If this layer wasn't in the previous list, it's new / Se esta
            # camada não estava na lista anterior, é nova
            if layer_name not in current_layer_names:
                new_layer_name = layer_name

        # Update the second combobox / Atualiza a segunda combobox
        self.vector_layer_combobox_2.clear()
        self.vector_layer_combobox_2.addItems(
            self.vector_layer_combobox.itemText(i)
            for i in range(self.vector_layer_combobox.count())
        )
        self.vector_layer_combobox_2.setCurrentIndex(
            self.vector_layer_combobox.currentIndex()
        )

        # If we found a new layer, select it / Se encontramos uma nova camada,
        # selecione-a
        if new_layer_name:
            index = self.vector_layer_combobox.findText(new_layer_name)
            self.vector_layer_combobox.setCurrentIndex(index)

        if self.vector_layer_combobox.count() == 0:
            self.pop_warning("No vector layers found in the project.")

    def get_selected_layer_path(self):
        """
        Retrieves the path of the currently selected layer in the combobox and
        triggers further processing.
        """
        """
        Recupera o caminho da camada atualmente selecionada na combobox e
        aciona o processamento adicional.
        """
        # Get the currently selected layer name from the combobox / Obtém o nome
        # da camada atualmente selecionada da combobox
        layer_name = (
            self.vector_layer_combobox.currentText().strip()
        )  # Remove whitespace
        if layer_name == "":
            print("No layer selected.")
            self.aoi_area.setText("Total Area:")
            self.QPushButton_next.setEnabled(False)
            return None
        print(f"Layer name from combobox: '{layer_name}'")  # Debug
        self.zoom_to_layer(layer_name)

        # Get the corresponding layer ID / Obtém o ID da camada
        # correspondente
        layer_id = self.vector_layer_ids.get(layer_name)
        print(f"Layer ID from vector_layer_ids: {layer_id}")  # Debug

        if layer_id is None:
            print(
                f"Error: Layer ID is None for layer name '{layer_name}'.  Check vector_layer_ids."
            )
            print(
                f"Contents of vector_layer_ids: {self.vector_layer_ids}"
            )  # Debug
            return None

        # Get the layer using its ID / Obtém a camada usando seu ID
        layer = QgsProject.instance().mapLayer(layer_id)
        if layer:
            print(
                f"Layer found: {layer.name()}, ID: {layer_id}"
            )  # Debug: Confirm layer is found
            self.selected_aio_layer_path = (
                layer.dataProvider().dataSourceUri().split("|")[0]
            )
            print(
                f"Selected layer path: {self.selected_aio_layer_path}"
            )  # Debug: Show selected layer path

            # Trigger the processing function / Aciona a função de
            # processamento
            self.aoi = self.load_vector_function()
            area = self.find_area()
            if area > 100:
                self.QPushButton_next.setEnabled(False)
                self.pop_warning(
                    f"Area too large ({area:.2f} km²). The limit is 100 km²."
                )
                return None
            if area == 0:
                self.pop_warning("Selected layer is not valid")
                self.QPushButton_next.setEnabled(False)
                return None

            self.QPushButton_next.setEnabled(True)
            # self.load_vector_function()
            return None
        else:
            print(
                f"Layer '{layer_name}' with ID '{layer_id}' not found in the project."
            )
            return None

    def load_index(self):
        """
        Calculates a vegetation index, downloads the GeoTIFF, and adds it to
        the QGIS project as a styled raster layer, ensuring unique names for
        each layer.
        """
        """
        Calcula um índice de vegetação, baixa o GeoTIFF e o adiciona ao projeto
        QGIS como uma camada raster estilizada, garantindo nomes exclusivos
        para cada camada.
        """
        try:
            print("First index clicked")
            QApplication.setOverrideCursor(
                Qt.WaitCursor
            )  # Set wait cursor for user feedback

            # Retrieve vegetation index and date inputs / Recupera o índice de
            # vegetação e as entradas de data
            vegetation_index = self.imagem_unica_indice.currentText()
            date = [self.dataunica.currentText()]

            first_image = self.sentinel2.filter(
                ee.Filter.inList("date", date)
            ).first()

            # Clip image to AOI / Recorta a imagem para a AOI
            first_image = first_image.clip(self.aoi)

            # Calculate the selected vegetation index / Calcula o índice de
            # vegetação selecionado
            if vegetation_index == "NDVI":
                index_image = first_image.normalizedDifference(["B8", "B4"]).rename(
                    "NDVI"
                )
            elif vegetation_index == "GNDVI":
                index_image = first_image.normalizedDifference(["B8", "B3"]).rename(
                    "GNDVI"
                )
            elif vegetation_index == "EVI":
                index_image = first_image.expression(
                    "2.5 * ((NIR / 10000 - RED / 10000) / (NIR / 10000 + 6 * RED / 10000 - 7.5 * BLUE / 10000 + 1))",
                    {
                        "NIR": first_image.select("B8"),
                        "RED": first_image.select("B4"),
                        "BLUE": first_image.select("B2"),
                    },
                ).rename("EVI")
            elif vegetation_index == "SAVI":
                L = 0.5  # Soil brightness correction factor
                index_image = first_image.expression(
                    "(1 + L) * ((NIR / 10000) - (RED / 10000)) / ((NIR / 10000) + (RED / 10000) + L)",
                    {
                        "NIR": first_image.select("B8"),
                        "RED": first_image.select("B4"),
                        "L": L,
                    },
                ).rename("SAVI")

            # Prepare download URL and output filename / Prepara o URL de
            # download e o nome do arquivo de saída
            url = index_image.getDownloadUrl(
                {
                    "scale": 10,
                    "region": self.aoi.geometry().bounds().getInfo(),
                    "format": "GeoTIFF",
                }
            )
            base_output_file = f"{vegetation_index}_{date[0]}.tiff"
            output_file = self.get_unique_filename(base_output_file)

            # Download the image / Baixa a imagem
            response = requests.get(url)
            if response.status_code == 200:
                with open(output_file, "wb") as f:
                    f.write(response.content)
                print(f"{vegetation_index} image downloaded as {output_file}")
            else:
                print(f"Failed to download image. HTTP Status: {response.status_code}")
                return

            # Prepare unique layer name / Prepara o nome exclusivo da camada
            layer_name = f"{vegetation_index} {date[0]}"
            base_name = layer_name
            i = 1
            while QgsProject.instance().mapLayersByName(layer_name):
                layer_name = f"{base_name}_{i}"
                i += 1
            print(f"Layer name adjusted to '{layer_name}' to ensure uniqueness.")

            # Add raster layer with styling / Adiciona a camada raster com
            # estilo
            map_tools.load_raster_layer_colorful(
                output_file, layer_name, vegetation_index, None
            )

        except Exception as e:
            print(f"An error occurred: {e}")
        finally:
            QApplication.restoreOverrideCursor()  # Restore the default cursor

    def load_rgb(self):
        # Set the cursor to indicate processing
        QApplication.setOverrideCursor(Qt.WaitCursor)

        try:
            date = [self.dataunica.currentText()]
            print(f"Selected date: {date}")

            first_image = self.sentinel2.filter(
                ee.Filter.inList("date", date)
            ).first()

            # Clip image to AOI
            first_image = first_image.clip(self.aoi)

            # Available bands: [B1, B2, B3, B4, B5, B6, B7, B8, B8A, B9, B11,
            # B12, AOT, WVP, SCL, TCI_R, TCI_G, TCI_B, MSK_CLDPRB, MSK_SNWPRB,
            # QA10, QA20, QA60, MSK_CLASSI_OPAQUE, MSK_CLASSI_CIRRUS,
            # MSK_CLASSI_SNOW_ICE]
            bands = [
                "B1",
                "B2",
                "B3",
                "B4",
                "B5",
                "B6",
                "B7",
                "B8",
                "B8A",
                "B9",
                "B11",
                "B12",
            ]
            first_image = first_image.select(bands)

            # Get the acquisition date and define download region
            region = self.aoi.geometry().bounds().getInfo()["coordinates"]

            # Generate download URL
            try:
                url = first_image.getDownloadUrl(
                    {
                        "scale": 10,
                        "region": region,
                        "format": "GeoTIFF",
                    }
                )
            except Exception as e:
                self.pop_warning(f"Failed to generate download URL: {e}")
                return

            # Define output file
            base_output_file = f"Sentinel2_AllBands_{date[0]}.tiff"
            output_file = self.get_unique_filename(base_output_file)

            # Download the image
            try:
                response = requests.get(url, stream=True)
                response.raise_for_status()
                with open(output_file, "wb") as f:
                    for chunk in response.iter_content(chunk_size=8192):
                        if chunk:
                            f.write(chunk)
                print(f"Image downloaded to {output_file}")
            except requests.exceptions.RequestException as e:
                self.pop_warning(f"Error downloading image: {e}")
                return

            # Add the image as a raster layer in QGIS
            layer_name = f"Sentinel-2 RGB {date[0]}"
            existing_layers = QgsProject.instance().mapLayersByName(layer_name)

            layer = QgsRasterLayer(output_file, layer_name)
            if not layer.isValid():
                self.pop_warning(f"Failed to load the layer: {output_file}")
                return

            # Set min and max values for each band (Red, Green, Blue)
            min_val, max_val = 200, 2300

            # Create a new MultiBandColorRenderer for RGB
            renderer = QgsMultiBandColorRenderer(
                layer.dataProvider(),
                4,  # Red band (B4, 1-based index)
                3,  # Green band (B3)
                2,  # Blue band (B2)
            )

            # Set contrast enhancement for each band (Red, Green, Blue)
            try:
                red_ce = QgsContrastEnhancement(layer.dataProvider().dataType(4))
                red_ce.setMinimumValue(min_val)
                red_ce.setMaximumValue(max_val)
                red_ce.setContrastEnhancementAlgorithm(
                    QgsContrastEnhancement.StretchToMinimumMaximum
                )

                green_ce = QgsContrastEnhancement(layer.dataProvider().dataType(3))
                green_ce.setMinimumValue(min_val)
                green_ce.setMaximumValue(max_val)
                green_ce.setContrastEnhancementAlgorithm(
                    QgsContrastEnhancement.StretchToMinimumMaximum
                )

                blue_ce = QgsContrastEnhancement(layer.dataProvider().dataType(2))
                blue_ce.setMinimumValue(min_val)
                blue_ce.setMaximumValue(max_val)
                blue_ce.setContrastEnhancementAlgorithm(
                    QgsContrastEnhancement.StretchToMinimumMaximum
                )

                renderer.setRedContrastEnhancement(red_ce)
                renderer.setGreenContrastEnhancement(green_ce)
                renderer.setBlueContrastEnhancement(blue_ce)
            except AttributeError as ae:
                print(f"Error configuring renderer: {ae}")
                return
            except Exception as e:
                print(f"Unexpected error configuring renderer: {e}")
                return

            # Set the renderer to the layer
            layer.setRenderer(renderer)

            # Add the raster layer to the QGIS project and insert it at the top
            # of the layer tree
            QgsProject.instance().addMapLayer(layer, False)
            root = QgsProject.instance().layerTreeRoot()
            root.insertChildNode(0, QgsLayerTreeLayer(layer))
            iface.setActiveLayer(layer)
        except Exception as e:
            self.pop_warning(f"An error occurred: {e}")
        finally:
            QApplication.restoreOverrideCursor()

    def zoom_to_layer(self, layer_name, margin_ratio=0.1):
        """
        Zoom to the specified layer with an optional margin.

        :param layer_name: Name of the layer to zoom to.
        :param margin_ratio: Fraction of the extent to add as margin (default
            is 0.1, or 10%).
        """
        """
        Amplia para a camada especificada com uma margem opcional.

        :param layer_name: Nome da camada para ampliar.
        :param margin_ratio: Fração da extensão para adicionar como margem
            (o padrão é 0,1 ou 10%).
        """
        project = QgsProject.instance()
        layers = project.mapLayersByName(
            layer_name
        )  # Get layers matching the name

        if not layers:
            print(f"Layer '{layer_name}' not found.")
            return

        layer = layers[0]  # Use the first matching layer
        iface = qgis.utils.iface  # Access the QGIS interface
        canvas = iface.mapCanvas()  # Get the active map canvas

        # Ensure the canvas CRS matches the layer CRS
        canvas.setDestinationCrs(layer.crs())

        # Get the layer's extent and add a margin
        layer_extent = layer.extent()
        x_margin = layer_extent.width() * margin_ratio
        y_margin = layer_extent.height() * margin_ratio

        expanded_extent = QgsRectangle(
            layer_extent.xMinimum() - x_margin,
            layer_extent.yMinimum() - y_margin,
            layer_extent.xMaximum() + x_margin,
            layer_extent.yMaximum() + y_margin,
        )

        # Set the expanded extent to the canvas
        canvas.setExtent(expanded_extent)
        canvas.refresh()

        print(f"Zoomed to layer extent with margin: {expanded_extent.toString()}")

    def get_unique_filename(self, base_file_name):
        name, extension = os.path.splitext(base_file_name)
        output_file = os.path.join(self.output_folder, base_file_name)
        counter = 1

        while os.path.exists(output_file):
            output_file = os.path.join(
                self.output_folder, f"{name}_{counter}{extension}"
            )
            counter += 1

        print(f"Unique filename: {output_file}")
        return output_file

    def sentinel2_selected_dates_update(self):
        Date_list_selection = (
            [
                date.strftime("%Y-%m-%d")
                for date in pd.to_datetime(self.df_aux["date"]).tolist()
            ]
            if "date" in self.df_aux.columns
            else []
        )
        print(f"Selected dates for time series: {Date_list_selection}")

        print("Final number of images before:", self.sentinel2.size().getInfo())
        dates_in_collection = self.sentinel2.aggregate_array("date").getInfo()
        print(f"Dates in the collection: {dates_in_collection}")
        print(f"Selected dates (timestamps): {Date_list_selection}")

        # Filtra a coleção Sentinel-2 pelas datas selecionadas
        sentinel2_selected_dates = self.sentinel2.filter(
            ee.Filter.inList("date", ee.List(Date_list_selection))
        )
        print(
            "Final number of images after:", sentinel2_selected_dates.size().getInfo()
        )

        self.sentinel2_selected_dates = sentinel2_selected_dates

    def composite_clicked(self):
        QApplication.setOverrideCursor(Qt.WaitCursor)
        self.sentinel2_selected_dates_update()
        self.composite()
        QApplication.restoreOverrideCursor()

    def composite(self):
        print("Composição de índices vegetativos")
        indice_vegetacao = self.indice_composicao.currentText()
        metrica = self.metrica.currentText()

        # Função para calcular o índice vegetal desejado e preservar a data
        def calculate_index(image):
            if indice_vegetacao == "NDVI":
                return (
                    image.normalizedDifference(["B8", "B4"])
                    .rename("NDVI")
                    .copyProperties(image, ["system:time_start"])
                )
            elif indice_vegetacao == "EVI":
                return (
                    image.expression(
                        "2.5 * ((NIR/10000 - RED/10000) / (NIR/10000 + 6*RED/10000 - 7.5*BLUE/10000 + 1))",
                        {
                            "NIR": image.select("B8"),
                            "RED": image.select("B4"),
                            "BLUE": image.select("B2"),
                        },
                    )
                    .rename("EVI")
                    .copyProperties(image, ["system:time_start"])
                )
            elif indice_vegetacao == "SAVI":
                return (
                    image.expression(
                        "(1 + L) * ((NIR/10000 - RED/10000) / (NIR/10000 + RED/10000 + L))",
                        {"NIR": image.select("B8"), "RED": image.select("B4"), "L": 0.5},
                    )
                    .rename("SAVI")
                    .copyProperties(image, ["system:time_start"])
                )
            elif indice_vegetacao == "GNDVI":
                return (
                    image.normalizedDifference(["B8", "B3"])
                    .rename("GNDVI")
                    .copyProperties(image, ["system:time_start"])
                )
            else:
                raise ValueError(f"Invalid indice_vegetacao: {indice_vegetacao}")

        # Aplica o cálculo do índice à coleção filtrada
        index_collection = self.sentinel2_selected_dates.map(calculate_index)

        # Seleção da métrica de agregação
        if metrica in ["Mean", "Média"]:
            final_image = index_collection.mean()
        elif metrica in ["Max", "Máximo"]:
            final_image = index_collection.max()
        elif metrica in ["Min", "Mínimo"]:
            final_image = index_collection.min()
        elif metrica in ["Median", "Mediana"]:
            final_image = index_collection.median()
        elif metrica in ["Amplitude"]:
            final_image = index_collection.max().subtract(index_collection.min())
        elif metrica in ["Standard Deviation", "Desvio Padrão"]:
            final_image = index_collection.reduce(ee.Reducer.stdDev())
        elif metrica in ["Sum", "Soma"]:
            final_image = index_collection.sum()
        elif metrica in ["Area Under Curve (AUC)"]:
            # --- Cálculo da AUC por pixel via método de array ---
            count = index_collection.size().getInfo()
            if count < 2:
                raise ValueError("Número insuficiente de imagens para calcular a AUC.")

            # Cria uma "pilha" dos índices (cada imagem vira uma banda)
            indexStack = index_collection.toBands()
            # Define uma máscara válida (mínimo valor da máscara de todas as bandas)
            validMask = indexStack.mask().reduce(ee.Reducer.min())

            # Obtém os nomes das bandas (cada banda corresponde a uma data)
            bands = indexStack.bandNames()

            # Define a data inicial; certifique-se de que self.inicio esteja no formato "YYYY-MM-DD"
            start_date = ee.Date(self.inicio)
            # Cria uma lista de timestamps em dias relativos à data inicial
            timestamps = index_collection.aggregate_array("system:time_start").map(
                lambda date: ee.Number(ee.Date(date).difference(start_date, "day"))
            )

            # Alinha os timestamps com o número de bandas
            alignedTimestamps = ee.List(timestamps.slice(0, bands.size()))
            # Cria uma imagem com constantes baseadas nos timestamps e renomeia para os nomes das bandas
            timeImage = ee.Image.constant(alignedTimestamps).rename(bands).toArray()

            # Converte a pilha de índices para um array
            ndviArray = indexStack.toArray()

            # Calcula as diferenças entre timestamps consecutivos
            diffs = timeImage.arraySlice(0, 1).subtract(timeImage.arraySlice(0, 0, -1))
            # Soma os valores de NDVI de imagens consecutivas
            sums = ndviArray.arraySlice(0, 1).add(ndviArray.arraySlice(0, 0, -1))
            # Aplica a regra do trapézio: (diferença * soma) / 2 e reduz o array pela soma dos intervalos
            auc = diffs.multiply(sums).divide(2).arrayReduce(ee.Reducer.sum(), [0])
            # Extrai o resultado final, aplica a máscara e define como imagem final
            final_image = ee.Image(auc.arrayGet([0])).updateMask(validMask)
        else:
            raise ValueError(f"Invalid metric: {metrica}")

        # Recorta a imagem final à área de interesse (AOI)
        final_image = final_image.clip(self.aoi.geometry())

        # Gera a URL de download para a imagem final (GeoTIFF, escala de 10)
        url = final_image.getDownloadUrl(
            {
                "scale": 10,
                "region": self.aoi.geometry().bounds().getInfo(),
                "format": "GeoTIFF",
            }
        )

        base_output_file = f"{metrica}_{indice_vegetacao}.tiff"
        output_file = self.get_unique_filename(base_output_file)
        response = requests.get(url)
        with open(output_file, "wb") as f:
            f.write(response.content)
        print(f"{indice_vegetacao} image downloaded as {output_file}")

        # Recorta a imagem raster usando a camada vetorial (normalmente a AOI)
        layer_name = f"{indice_vegetacao} {metrica}"
        base_output_file = f"{metrica}_{indice_vegetacao}_clipped.tiff"
        output_file_clipped = self.get_unique_filename(base_output_file)
        self.clip_raster_by_vector(
            output_file, self.selected_aio_layer_path, output_file_clipped, layer_name
        )

        # Carrega a camada raster com estilo colorido no QGIS
        map_tools.load_raster_layer_colorful(
            output_file_clipped, layer_name, indice_vegetacao, self.metrica.currentText()
        )

    def clip_raster_by_vector(
        self, raster_path, shapefile_path, output_path, layer_name
    ):
        print(
            f"Clipping raster {raster_path} by vector {shapefile_path} to {output_path}"
        )
        # Load layers
        shapefile_layer = QgsVectorLayer(shapefile_path, "Clip Layer", "ogr")
        raster_layer = QgsRasterLayer(raster_path, "Raster Layer")

        # Check if layers loaded successfully
        if not shapefile_layer.isValid():
            print("Failed to load shapefile.")
        if not raster_layer.isValid():
            print("Failed to load raster.")

        # Clip raster using the shapefile
        result = processing.run(
            "gdal:cliprasterbymasklayer",
            {
                "INPUT": raster_layer,
                "MASK": shapefile_layer,
                "NODATA": -9999,  # Change to appropriate NoData value if needed
                "CROP_TO_CUTLINE": True,
                "KEEP_RESOLUTION": True,
                "OUTPUT": output_path,
            },
            feedback=QgsProcessingFeedback(),
        )

        print(f"Clipping result: {result}")

    def on_file_changed(self, file_path):
        """Slot called when the selected file changes."""
        """Slot chamado quando o arquivo selecionado muda."""
        
        if self.mQgsFileWidget.filePath():
            print(f"File selected: {file_path}")
            self.output_folder = file_path
            self.folder_set = True
            self.QPushButton_next_4.setEnabled(True)
        else:
            print("No file selected.")
            self.folder_set = False
            self.QPushButton_next_4.setEnabled(False)



    def index_explain(self):
        if self.language:
            explanation = vegetation_index_info.vegetation_indices_pt.get(
                self.series_indice.currentText()
            )
        else:
            explanation = vegetation_index_info.vegetation_indices.get(
                self.series_indice.currentText()
            )
        self.textBrowser_index_explain.setHtml(explanation)

    def load_vector_function(self, shapefile_path=None):
        """
        Loads the vector layer from the selected file path, reprojects it to
        EPSG:4326, dissolves multiple features if necessary, and converts it
        into an Earth Engine FeatureCollection representing the AOI.
        """
        """
        Carrega a camada vetorial do caminho do arquivo selecionado, a
        reprojeta para EPSG:4326, dissolve várias feições, se necessário, e a
        converte em um Earth Engine FeatureCollection representando a AOI.
        """
        if shapefile_path is None:
            shapefile_path = self.selected_aio_layer_path

        try:
            # Load the shapefile, handling both .zip archives and regular files.
            if shapefile_path.endswith(".zip"):
                with zipfile.ZipFile(shapefile_path, "r") as zip_ref:
                    shapefile_found = False
                    for file in zip_ref.namelist():
                        if file.endswith(".shp"):
                            shapefile_found = True
                            shapefile_within_zip = file
                            break
                    if not shapefile_found:
                        print("No .shp file found inside the zip archive.")
                        return

                    # Read shapefile directly from the zip archive.
                    aoi = gpd.read_file(
                        f"zip://{shapefile_path}/{shapefile_within_zip}"
                    )
            else:
                aoi = gpd.read_file(shapefile_path)

            # Reproject the GeoDataFrame to EPSG:4326 to ensure correct
            # coordinates for Earth Engine.
            aoi = aoi.to_crs(epsg=4326)

            if aoi.empty:
                print("The shapefile does not contain any geometries.")
                return

            # Dissolve multiple features into a single geometry if necessary.
            if len(aoi) > 1:
                aoi = aoi.dissolve()

            # Extract the first geometry.
            geometry = aoi.geometry.iloc[0]

            # Convert the geometry to GeoJSON format.
            geojson = geometry.__geo_interface__

            # Remove any third dimension from the coordinates.
            if geojson["type"] == "Polygon":
                geojson["coordinates"] = [
                    list(map(lambda coord: coord[:2], ring)) for ring in geojson["coordinates"]
                ]
            elif geojson["type"] == "MultiPolygon":
                geojson["coordinates"] = [
                    [list(map(lambda coord: coord[:2], ring)) for ring in polygon]
                    for polygon in geojson["coordinates"]
                ]

            # Create an Earth Engine geometry object.
            ee_geometry = ee.Geometry(geojson)
            feature = ee.Feature(ee_geometry)
            aoi = ee.FeatureCollection([feature])

            print("AOI defined successfully.")
            self.QPushButton_next.setEnabled(True)
            self.aio_set = True
            self.vector_layer_combobox_2.setCurrentIndex(
                self.vector_layer_combobox.currentIndex()
            )
            # self.next_clicked()

            self.aoi_ckecked = True
            self.aoi_ckecked_function()

            return aoi

        except Exception as e:
            print(f"Error in load_vector_function: {e}")
            return

    def find_centroid(self):
        centroid = self.aoi.geometry().centroid()
        self.lat = centroid.getInfo().get("coordinates")[1]
        self.lon = centroid.getInfo().get("coordinates")[0]
        print(f"{round(self.lat,4)},{round(self.lon,4)}")
        area = (
            self.aoi.geometry().area().getInfo() / 1e6
        )  # Convert from square meters to square kilometers
        print(f"Area: {area:.2f} km²")
        if area >= 100:
            self.aoi = None
            self.aoi_ckecked = False
            self.aoi_ckecked_function()
        return area

    def find_area(self):
        try:
            area_km2 = (
                self.aoi.geometry().area().getInfo() / 1e6
            )  # Convert from square meters to square kilometers
            area_ha = area_km2 * 100  # Convert from square kilometers to hectares
            print(f"Area: {area_km2:.2f} km² ({area_ha:.2f} ha)")
            self.aoi_area.setText(
                f"Total Area: {area_km2:.2f} km² ({area_ha:.2f} hectares)"
            )
            return area_km2
        except Exception as e:
            print(f"Error in find_area: {e}")
            self.aoi_area.setText(f"Total Area:")
            return 0

    def aoi_ckecked_function(self):
        if self.aoi_ckecked and self.folder_set:
            self.QPushButton_next.setEnabled(True)
        else:
            self.QPushButton_next.setEnabled(False)

    def resetting(self):
        self.recorte_datas = None
        self.aoi = self.load_vector_function()
        self.get_dates()
        # self.customfilter.setChecked(False)
        # self.remove_cloudy.setChecked(False)
        self.QCheckBox_sav_filter.setChecked(False)
        self.filtro_grau.setCurrentIndex(0)
        self.window_len.setCurrentIndex(0)
        self.df_nasa = None
        self.df_aux = None
        self.df_points = None

    def loadtimeseries_clicked(self):
        QApplication.setOverrideCursor(Qt.WaitCursor)
        try:
            self.resetting()
            self.ee_load_collection()
            self.calculate_timeseries()
            self.plot_timeseries()
            self.tabWidget.setCurrentIndex(9)
            self.centralizar()
            self.webView_2.setHtml("")
            self.webView_3.setHtml("")
            print("Time series loaded successfully.")
            
            if self.language == "pt":
                self.pop_warning("\n".join(self.collection_info_pt))
            else:
                self.pop_warning("\n".join(self.collection_info))
        
        except Exception as e:
            print(f"An error occurred: {e}")
            QApplication.restoreOverrideCursor()
            self.pop_warning(f"An error occurred: {e}")
        QApplication.restoreOverrideCursor()

    def ee_load_collection(self):

        self.collection_info = []
        self.collection_info_pt = []
        """Loads the Earth Engine image collection based on user-defined
        criteria."""
        """Carrega a coleção de imagens do Earth Engine com base nos critérios
        definidos pelo usuário."""

        # Find the centroid of the AOI and check if the area is within the limit

        # Reset settings and set the cursor to indicate processing

        # Retrieve user inputs for date range, cloud percentage, and AOI
        inicio = self.inicio
        final = self.final
        nuvem = self.nuvem
        aoi = self.aoi
        coverage_threshold = self.horizontalSlider_aio_cover.value() / 100
        local_pixel_limit = self.horizontalSlider_local_pixel_limit.value()
        print(f"Coverage threshold: {coverage_threshold}")

        # Define the Sentinel-2 image collection with filtering by date, bounds,
        # and cloud percentage
        sentinel2 = (
            ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
            .filterDate(inicio, final)
            .filterBounds(aoi)
            #.filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", nuvem))
            .map(lambda image: image.set("date", image.date().format("YYYY-MM-dd")))
        )

        # Check if the collection is empty
        original_count = sentinel2.size().getInfo()
        print(f"Collection size before any filtering: {original_count}")
        self.collection_info.append(f"Collection size before any filtering: {original_count}")
        self.collection_info_pt.append(f"Tamanho da coleção antes de qualquer filtro: {original_count}")

        if original_count == 0:
            QApplication.restoreOverrideCursor()
            self.pop_warning(
                "No images found for the selected criteria. Please select a larger date range or less strick filtering and try again."
            )
            return

        # Apply cloud percentage filter
        sentinel2 = sentinel2.filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", nuvem))

        # Check if the collection is empty after cloud filtering
        cloud_filtered_count = sentinel2.size().getInfo()
        print(f"Collection size after cloud filtering: {cloud_filtered_count}")
        self.collection_info.append(f"Collection size after tile cloud percentage filtering (Step 8): {cloud_filtered_count}")
        self.collection_info_pt.append(f"Tamanho da coleção após filtro de {nuvem}% de limite de nuvem na cena (Passo 8): {cloud_filtered_count}")

        if cloud_filtered_count == 0:
            QApplication.restoreOverrideCursor()
            self.pop_warning(
                "No images found for the selected criteria. Please select a larger date range or less strick filtering and try again."
            )
            return


        # Apply AOI coverage filter to the image collection
        if coverage_threshold > 0:
            sentinel2 = self.AOI_coverage_filter(sentinel2, aoi, coverage_threshold)
            if sentinel2.size().getInfo() == 0:
                QApplication.restoreOverrideCursor()
                self.pop_warning(
                    "No images found for the selected criteria. Please select a larger date range or less strick filtering and try again."
                )
                return

        if local_pixel_limit > 0:
            # Apply local pixel limit filter to the image collection
            sentinel2 = self.SCL_filter(sentinel2, aoi, local_pixel_limit)
            if sentinel2.size().getInfo() == 0:
                QApplication.restoreOverrideCursor()
                self.pop_warning(
                    "No images found for the selected criteria. Please select a larger date range or less strick filtering and try again."
                )
                return

        # Apply cloud and shadow mask if the checkbox is checked
        if self.mask.isChecked():
            sentinel2 = self.SCL_mask(sentinel2, aoi)
            if sentinel2.size().getInfo() == 0:
                QApplication.restoreOverrideCursor()
                self.pop_warning(
                    "No images found for the selected criteria. Please select a larger date range or less strick filtering and try again."
                )
                return

        sentinel2 = self.uniqueday_collection(sentinel2)

        # Store the filtered image collection in the instance variable
        self.sentinel2 = sentinel2

    def uniqueday_collection(self, sentinel2):
        """Filters the image collection to include only one image per day."""
        """Filtra a coleção de imagens para incluir apenas uma imagem por dia."""
        print("Filtering to unique days...")
        print("Original collection size:", sentinel2.size().getInfo())
        # Step 1: Aggregate timestamps from the ImageCollection
        original_timestamps = sentinel2.aggregate_array("system:time_start").getInfo()

        # Step 2: Convert timestamps to formatted dates
        formatted_dates = [
            datetime.fromtimestamp(ts / 1000).strftime("%Y-%m-%d")
            for ts in original_timestamps
        ]

        # Step 3: Identify unique dates and map them back to the original
        # timestamps
        df = pd.DataFrame(
            list(zip(original_timestamps, formatted_dates)),
            columns=["timestamp", "date"],
        )
        first_timestamps_per_date = df.groupby("date")["timestamp"].min().tolist()

        print(f"Collection size after keeping only unique dates: {len(first_timestamps_per_date)}")
        self.collection_info.append(f"Collection size after keeping only unique dates: {len(first_timestamps_per_date)}")
        self.collection_info_pt.append(f"Tamanho da coleção após manter apenas datas únicas: {len(first_timestamps_per_date)}")

        # Step 4: Filter the collection to include only the first image for each
        # unique date
        return sentinel2.filter(
            ee.Filter.inList("system:time_start", ee.List(first_timestamps_per_date))
        )

    def AOI_coverage_filter(self, sentinel2, aoi, coverage_threshold):
        """Filters the image collection based on the coverage of the Area of
        Interest (AOI)."""
        """Filtra a coleção de imagens com base na cobertura da Área de
        Interesse (AOI)."""
        print("Applying AOI coverage filter...")
        if coverage_threshold == 1:
            coverage_threshold = 0.9999  # Avoid floating-point comparison issues

        # Coverage Ratio Function
        aoi_geometry = aoi.first().geometry()
        aoi_area = aoi_geometry.area()

        def calculate_coverage_ratio(image):
            """
            Calculates the ratio of the AOI area covered by the image.

            Args:
                image (ee.Image): The Sentinel-2 image.

            Returns:
                ee.Image: The original image with an added 'coverage_ratio'
                    property.
            """
            # Compute the intersection geometry between AOI and image footprint
            intersection = aoi_geometry.intersection(image.geometry(), ee.ErrorMargin(1))

            # Calculate the area of the intersection
            intersection_area = intersection.area()

            # Calculate the coverage ratio (intersection area / AOI area)
            coverage_ratio = intersection_area.divide(aoi_area)

            # Set the coverage ratio as a property of the image
            return image.set("coverage_ratio", coverage_ratio)

        # -------------------------------
        # Step 6: Apply Coverage Ratio Calculation
        # -------------------------------

        # Map the coverage ratio function over the Sentinel-2 collection
        sentinel2_with_ratio = sentinel2.map(calculate_coverage_ratio)

        # -------------------------------
        # Step 7: Filter Based on Coverage Ratio
        # -------------------------------

        # Define a filter to keep images with coverage_ratio >=
        # coverage_threshold
        coverage_filter = ee.Filter.gte("coverage_ratio", coverage_threshold)

        # Apply the filter to get the final collection
        covering_colection = sentinel2_with_ratio.filter(coverage_filter)


        # Get the number of images after coverage filtering
        filtered_count = covering_colection.size().getInfo()

        print(
            f"Collection size after filtro >= {coverage_threshold*100}% AOI coverage: {filtered_count}"
        )
        self.collection_info.append(f"Collection size after filter >= {coverage_threshold*100}% AOI coverage (Step 6): {filtered_count}")
        self.collection_info_pt.append(f"Tamanho da coleção após filtro >= {coverage_threshold*100}% de cobertura da AOI (Passo 6): {filtered_count}")
        

        return covering_colection

    def SCL_filter(self, sentinel2, aoi, valid_pixel_threshold):
        """Filters the image collection based on the percentage of valid pixels
        within the AOI."""
        """Filtra a coleção de imagens com base na porcentagem de pixels
        válidos dentro da AOI."""

        print("Applying SCL filter...")
        #print("Original collection size:", sentinel2.size().getInfo())

        scl_classes_behavior = {
            0: self.mask_class0.isChecked(),  # No data
            1: self.mask_class1.isChecked(),  # Saturated/defective
            2: self.mask_class2.isChecked(),  # Dark features
            3: self.mask_class3.isChecked(),  # Cloud shadows
            4: self.mask_class4.isChecked(),  # Vegetation
            5: self.mask_class5.isChecked(),  # Bare soils
            6: self.mask_class6.isChecked(),  # Water
            7: self.mask_class7.isChecked(),  # Cloud low probability
            8: self.mask_class8.isChecked(),  # Cloud medium probability
            9: self.mask_class9.isChecked(),  # Cloud high probability
            10: self.mask_class10.isChecked(),  # Thin cirrus
            11: self.mask_class11.isChecked(),  # Snow or ice
        }

        def mask_cloud_and_shadows(image):
            scl = image.select("SCL")
            # Start with an all-inclusive mask
            mask = ee.Image.constant(1)
            # Apply exclusions
            for class_value, include in scl_classes_behavior.items():
                if include:
                    mask = mask.And(scl.neq(class_value))

            masked_image = image.updateMask(mask)

            # Calculate the percentage of valid pixels
            total_pixels = (
                image.select(0)
                .reduceRegion(reducer=ee.Reducer.count(), geometry=aoi, scale=10)
                .get("B1")
            )

            valid_pixels = (
                masked_image.select(0)
                .reduceRegion(reducer=ee.Reducer.count(), geometry=aoi, scale=10)
                .get("B1")
            )

            percentage_valid = (
                ee.Number(valid_pixels).divide(total_pixels).multiply(100)
            )

            # Add the percentage of valid pixels as a property
            return masked_image.set("percentage_valid_pixels", percentage_valid)

            # Apply the cloud and shadow mask function

        # Apply the cloud and shadow mask function to the image collection
        sentinel2_masked = sentinel2.map(mask_cloud_and_shadows)

        # Filter the collection based on the valid pixel threshold
        filtered_collection = sentinel2_masked.filter(
            ee.Filter.gte("percentage_valid_pixels", valid_pixel_threshold)
        )

        masked_timestamps = filtered_collection.aggregate_array("system:time_start").getInfo()

        sentinel2 =  sentinel2.filter(
            ee.Filter.inList("system:time_start", ee.List(masked_timestamps))
        )

        print("Collection size after SCL filter:", sentinel2.size().getInfo())
        self.collection_info.append(f"Collection size after SCL filter (Step 9): {sentinel2.size().getInfo()}")
        self.collection_info_pt.append(f"Tamanho da coleção após filtro SCL (Passo 9): {sentinel2.size().getInfo()}")
        return sentinel2

    def SCL_mask(self, sentinel2, aoi):
        """Applies a Scene Classification Layer (SCL) mask
        collection."""
        """Aplica uma mascara de Camada de Classificação de Cena (SCL) à coleção
        de imagens."""
        print("Applying SCL MASK...")
        scl_classes_behavior = {
            0: self.mask_class0.isChecked(),  # No data
            1: self.mask_class1.isChecked(),  # Saturated/defective
            2: self.mask_class2.isChecked(),  # Dark features
            3: self.mask_class3.isChecked(),  # Cloud shadows
            4: self.mask_class4.isChecked(),  # Vegetation
            5: self.mask_class5.isChecked(),  # Bare soils
            6: self.mask_class6.isChecked(),  # Water
            7: self.mask_class7.isChecked(),  # Cloud low probability
            8: self.mask_class8.isChecked(),  # Cloud medium probability
            9: self.mask_class9.isChecked(),  # Cloud high probability
            10: self.mask_class10.isChecked(),  # Thin cirrus
            11: self.mask_class11.isChecked(),  # Snow or ice
        }

        def mask_cloud_and_shadows(image):
            scl = image.select("SCL")
            # Start with an all-inclusive mask
            mask = ee.Image.constant(1)
            # Apply exclusions
            for class_value, include in scl_classes_behavior.items():
                if include:
                    mask = mask.And(scl.neq(class_value))

            return image.updateMask(mask)

            # Apply the cloud and shadow mask function to the image collection

        return sentinel2.map(mask_cloud_and_shadows)

    def calculate_timeseries(self):
        """Calculates the time series of the selected vegetation index for the
        AOI."""
        """Calcula a série temporal do índice de vegetação selecionado para a
        AOI."""
        vegetation_index = self.series_indice.currentText()

        # Buffer the AOI geometry inward by 10 meters (adjust distance as
        # needed)
        buffer_distance = self.horizontalSlider_buffer.value()
        if buffer_distance != 0:
            print(f"Buffer distance: {buffer_distance} meters")
            aoi = self.aoi.map(lambda feature: feature.buffer(buffer_distance))
        else:
            print("No buffer applied")
            aoi = self.aoi

        # Define the vegetation index calculation in a function
        def calculate_index(image):
            if vegetation_index == "NDVI":
                index_image = image.normalizedDifference(["B8", "B4"]).rename("index")
            elif vegetation_index == "EVI":
                index_image = image.expression(
                    "2.5 * ((NIR / 10000 - RED / 10000) / (NIR / 10000 + 6 * RED / 10000 - 7.5 * BLUE / 10000 + 1))",
                    {
                        "NIR": image.select("B8"),
                        "RED": image.select("B4"),
                        "BLUE": image.select("B2"),
                    },
                ).rename("index")
            elif vegetation_index == "SAVI":
                L = 0.5
                index_image = image.expression(
                    "(1 + L) * ((NIR / 10000) - (RED / 10000)) / ((NIR / 10000) + (RED / 10000) + L)",
                    {
                        "NIR": image.select("B8"),
                        "RED": image.select("B4"),
                        "L": L,
                    },
                ).rename("index")
            elif vegetation_index == "GCI":
                index_image = image.expression(
                    "NIR / GREEN - 1",
                    {"NIR": image.select("B8"), "GREEN": image.select("B3")},
                ).rename("index")
            elif vegetation_index == "GNDVI":
                index_image = image.normalizedDifference(["B8", "B3"]).rename("index")
            else:
                raise ValueError(f"Unsupported vegetation index: {vegetation_index}")

            # Calculate mean value for the index over AOI
            mean_index = (
                index_image.reduceRegion(
                    reducer=ee.Reducer.mean(), geometry=aoi, scale=10, bestEffort=True
                )
                .get("index")
            )

            return image.set({"mean_index": mean_index})

        # Map the calculation function over the collection and get results
        result = self.sentinel2.map(calculate_index)
        result = result.filter(ee.Filter.notNull(["mean_index"]))

        # Retrieve dates and mean index values separately using aggregate_array
        dates = result.aggregate_array("date").getInfo()
        mean_indices = result.aggregate_array("mean_index").getInfo()
        image_ids = result.aggregate_array("system:index").getInfo()

        # Combine the dates, mean indices, and image IDs into a DataFrame
        df = pd.DataFrame(
            {"date": dates, "AOI_average": mean_indices, "image_id": image_ids}
        )

        # Optional: Smoothing or further processing
        self.df = df.copy()
        self.df_aux = df.copy()
        self.load_dates()
        self.plot_timeseries()

    def feature_calculate_timeseries(self, name):
        """Calculates the time series of the selected vegetation index for a
        specific feature."""
        """Calcula a série temporal do índice de vegetação selecionado para uma
        feição específica."""
        vegetation_index = self.series_indice.currentText()

        # Buffer the AOI geometry inward by 10 meters (adjust distance as
        # needed)
        buffer_distance = self.horizontalSlider_buffer.value()
        if buffer_distance != 0:
            print(f"Buffer distance: {buffer_distance} meters")
            aoi = self.aoi_feature.map(lambda feature: feature.buffer(buffer_distance))
        else:
            print("No buffer applied")
            aoi = self.aoi_feature

        # Define the vegetation index calculation in a function
        def calculate_index(image):
            if vegetation_index == "NDVI":
                index_image = image.normalizedDifference(["B8", "B4"]).rename("index")
            elif vegetation_index == "EVI":
                index_image = image.expression(
                    "2.5 * ((NIR / 10000 - RED / 10000) / (NIR / 10000 + 6 * RED / 10000 - 7.5 * BLUE / 10000 + 1))",
                    {
                        "NIR": image.select("B8"),
                        "RED": image.select("B4"),
                        "BLUE": image.select("B2"),
                    },
                ).rename("index")
            elif vegetation_index == "SAVI":
                L = 0.5
                index_image = image.expression(
                    "(1 + L) * ((NIR / 10000) - (RED / 10000)) / ((NIR / 10000) + (RED / 10000) + L)",
                    {
                        "NIR": image.select("B8"),
                        "RED": image.select("B4"),
                        "L": L,
                    },
                ).rename("index")
            elif vegetation_index == "GCI":
                index_image = image.expression(
                    "NIR / GREEN - 1",
                    {"NIR": image.select("B8"), "GREEN": image.select("B3")},
                ).rename("index")
            elif vegetation_index == "GNDVI":
                index_image = image.normalizedDifference(["B8", "B3"]).rename("index")
            else:
                raise ValueError(f"Unsupported vegetation index: {vegetation_index}")

            # Calculate mean value for the index over AOI
            mean_index = (
                index_image.reduceRegion(
                    reducer=ee.Reducer.mean(), geometry=aoi, scale=10, bestEffort=True
                )
                .get("index")
            )

            return image.set({"mean_index": mean_index})

        # Map the calculation function over the collection and get results
        result = self.sentinel2_selected_dates.map(calculate_index)
        result = result.filter(ee.Filter.notNull(["mean_index"]))

        # Retrieve dates and mean index values separately using aggregate_array
        dates = result.aggregate_array("date").getInfo()
        mean_indices = result.aggregate_array("mean_index").getInfo()

        # Combine the dates, mean indices
        print(f"Creating DataFrame for {name}")
        return pd.DataFrame({"date": dates, name: mean_indices})

    def point_calculate_timeseries(self, aoi, name):
        """Calculates the time series of the selected vegetation index for a
        specific point."""
        """Calcula a série temporal do índice de vegetação selecionado para um
        ponto específico."""
        vegetation_index = self.series_indice.currentText()

        # Define the vegetation index calculation in a function
        def calculate_index(image):
            if vegetation_index == "NDVI":
                index_image = image.normalizedDifference(["B8", "B4"]).rename("index")
            elif vegetation_index == "EVI":
                index_image = image.expression(
                    "2.5 * ((NIR / 10000 - RED / 10000) / (NIR / 10000 + 6 * RED / 10000 - 7.5 * BLUE / 10000 + 1))",
                    {
                        "NIR": image.select("B8"),
                        "RED": image.select("B4"),
                        "BLUE": image.select("B2"),
                    },
                ).rename("index")
            elif vegetation_index == "SAVI":
                L = 0.5
                index_image = image.expression(
                    "(1 + L) * ((NIR / 10000) - (RED / 10000)) / ((NIR / 10000) + (RED / 10000) + L)",
                    {
                        "NIR": image.select("B8"),
                        "RED": image.select("B4"),
                        "L": L,
                    },
                ).rename("index")
            elif vegetation_index == "GCI":
                index_image = image.expression(
                    "NIR / GREEN - 1",
                    {"NIR": image.select("B8"), "GREEN": image.select("B3")},
                ).rename("index")
            elif vegetation_index == "GNDVI":
                index_image = image.normalizedDifference(["B8", "B3"]).rename("index")
            else:
                raise ValueError(f"Unsupported vegetation index: {vegetation_index}")

            # Calculate mean value for the index over AOI
            mean_index = (
                index_image.reduceRegion(
                    reducer=ee.Reducer.mean(), geometry=aoi, scale=10, bestEffort=True
                )
                .get("index")
            )

            return image.set({"mean_index": mean_index})

        # Map the calculation function over the collection and get results
        result = self.sentinel2_selected_dates.map(calculate_index)
        result = result.filter(ee.Filter.notNull(["mean_index"]))

        # Retrieve dates and mean index values separately using aggregate_array
        dates = result.aggregate_array("date").getInfo()
        mean_indices = result.aggregate_array("mean_index").getInfo()

        # Combine the dates, mean indices
        print(f"Creating DataFrame for {name}")
        return pd.DataFrame({"date": dates, name: mean_indices})

    def clear_all_raster_layers(self):
        """Removes all raster layers from the QGIS project, except for the
        Google Hybrid layer."""
        """Remove todas as camadas raster do projeto QGIS, exceto a camada
        Google Hybrid."""
        # Get the current project instance
        project = QgsProject.instance()

        # Create a copy of the layer list to avoid issues with removing during
        # iteration
        layers_to_remove = list(project.mapLayers().values())

        # Iterate over the copied list
        for layer in layers_to_remove:
            if (
                layer.type() == QgsMapLayer.RasterLayer
                and layer.name() != "Google Hybrid"
            ):
                layer_name = layer.name()  # Store the layer name before removing it
                project.removeMapLayer(layer.id())  # Use layer.id() for removal
                print(f"Removed raster layer: {layer_name}")
                iface.mapCanvas().refresh()

    def df_ajust(self):
        """Adjusts the main DataFrame based on the selected dates."""
        """Ajusta o DataFrame principal com base nas datas selecionadas."""
        df = self.df.copy()
        if self.recorte_datas:
            df = df[df["date"].isin(self.recorte_datas)]
            self.df_aux = df.copy()
        else:
            self.df_aux = df.copy()

    def df_ajust_features(self):
        """Adjusts the features DataFrame based on the selected dates."""
        """Ajusta o DataFrame de feições com base nas datas selecionadas."""
        df = self.df_features.copy()
        if self.recorte_datas:
            df = df[df["date"].isin(self.recorte_datas)]
            self.df_aux_features = df.copy()
        else:
            self.df_aux_features = df.copy()

    def df_ajust_points(self):
        """Adjusts the points DataFrame based on the selected dates."""
        """Ajusta o DataFrame de pontos com base nas datas selecionadas."""
        df = self.df_points.copy()
        if self.recorte_datas:
            df = df[df["date"].isin(self.recorte_datas)]
            self.df_aux_points = df.copy()
        else:
            self.df_aux_points = df.copy()

    def df_run_filter(self):
        """Applies the Savitzky-Golay filter to smooth the time series data."""
        """Aplica o filtro Savitzky-Golay para suavizar os dados da série
        temporal."""
        df = self.df_aux.copy()
        try:
            if self.window_len.count() == 0:
                self.window_len.clear()
                self.window_len.addItems(
                    list(map(str, list(range(7, len(df) + 1))))
                )
                self.window_len.setCurrentIndex(0)

            window_length = int(self.window_len.currentText())
            polyorder = int(self.filtro_grau.currentText().split("%")[0])
            print(f"Window length: {window_length}, Polyorder: {polyorder}")

            if window_length > len(df):
                window_length = len(df)
                self.window_len.setCurrentIndex(len(df) - 5)
                print(f"Window length too large. Using maximum value: {window_length}")

            # Apply Savitzky-Golay filter to smooth the time series

            df["savitzky_golay_filtered"] = savgol_filter(
                df["AOI_average"], window_length=window_length, polyorder=polyorder
            )
            self.df_aux = df.copy()
            return True
        except Exception as e:
            self.pop_warning(
                f"Not enough images to apply the Savitzky-Golay filter. Please select a larger date range or less strick filtering."
            )
            self.QCheckBox_sav_filter.setChecked(False)
            self.plot_timeseries()
            return False

    def plot_timeseries(self):
        """Plots the time series data, optionally applying the Savitzky-Golay
        filter."""
        """Plota os dados da série temporal, opcionalmente aplicando o filtro
        Savitzky-Golay."""
        print("plot1 started")

        # Prepare to plot
        myFile = io.StringIO()
        if self.QCheckBox_sav_filter.isChecked() and self.df_run_filter():
            df = self.df_aux
            try:
                self.fig = go.Figure()
                self.fig.add_trace(
                    go.Scatter(
                        x=df["date"],
                        y=df["AOI_average"],
                        mode="lines",
                        name=self.series_indice.currentText(),
                        line=dict(color="green"),
                    )
                )
                self.fig.add_trace(
                    go.Scatter(
                        x=df["date"],
                        y=df["savitzky_golay_filtered"],
                        mode="lines",
                        name=f"{self.series_indice.currentText()} filtered",
                        line=dict(color="purple"),
                    )
                )
            except Exception as e:
                self.pop_warning(f"An error occurred while plotting: {e}")
                self.QCheckBox_sav_filter.setChecked(False)
        else:
            df = self.df_aux
            self.fig = go.Figure()
            self.fig.add_trace(
                go.Scatter(
                    x=df["date"],
                    y=df["AOI_average"],
                    mode="lines",
                    name=self.series_indice.currentText(),
                    line=dict(color="green"),
                )
            )

        self.fig.update_layout(
            # xaxis_title='Date',
            yaxis_title=self.series_indice.currentText(),
            title=f"Time Series - {self.series_indice.currentText()} - {self.vector_layer_combobox.currentText()}               Image count: {len(df)}",
        )

        self.fig.update_traces(
            hovertemplate="date = %{x|%Y-%m-%d}<br>average_ndvi = %{y:.2f}<extra></extra>"
        )

        # Configurations for the Plotly plot
        self.config = {
            "displaylogo": False,
            "modeBarButtonsToRemove": [
                "toImage",
                "sendDataToCloud",
                "zoom2d",
                "pan2d",
                "select2d",
                "lasso2d",
                "zoomIn2d",
                "zoomOut2d",
                "autoScale2d",
                "resetScale2d",
                "hoverClosestCartesian",
                "hoverCompareCartesian",
                "zoom3d",
                "pan3d",
                "orbitRotation",
                "tableRotation",
                "resetCameraLastSave",
                "resetCameraDefault3d",
                "hoverClosest3d",
                "zoomInGeo",
                "zoomOutGeo",
                "resetGeo",
                "hoverClosestGeo",
                "hoverClosestGl2d",
                "hoverClosestPie",
                "toggleHover",
                "toggleSpikelines",
                "resetViews",
            ],
        }

        if isinstance(self.df_nasa, pd.DataFrame):
            # Add bar plot (set below the line explicitly)
            self.fig.add_trace(
                go.Bar(
                    x=self.df_nasa.index,
                    y=self.df_nasa["PRECTOTCORR"],
                    name="Monthly Precipitation",
                    yaxis="y2",
                    marker_color="blue",
                    opacity=0.4,
                )
            )

            # Ensure correct layout and layering
            self.fig.update_layout(
                yaxis=dict(
                    title=self.series_indice.currentText(),
                ),
                yaxis2=dict(
                    title="Precipitation (mm)",
                    overlaying="y",
                    side="right",
                ),
                xaxis=None,
            )

        # Update layout and render the plot
        self.webView.setHtml(
            self.fig.to_html(include_plotlyjs="cdn", config=self.config)
        )

        self.plot1 = True

    def open_browser(self):
        """Opens the plot in a web browser."""
        """Abre o gráfico em um navegador da web."""
        self.fig.show()

    def open_browser_2(self):
        """Opens the feature plot in a web browser."""
        """Abre o gráfico de feições em um navegador da web."""
        try:
            self.fig_2.show()
        except:
            self.pop_warning("No data to plot")

    def open_browser_3(self):
        """Opens the points plot in a web browser."""
        """Abre o gráfico de pontos em um navegador da web."""
        try:
            self.fig_3.show()
        except:
            self.pop_warning("No data to plot")

    def load_dates(self):
        """Loads the unique dates from the DataFrame into the date selection
        combobox."""
        """Carrega as datas exclusivas do DataFrame na combobox de seleção de
        data."""
        datas = self.df.date.unique().astype(str).tolist()
        self.dataunica.clear()
        self.dataunica.addItems(datas)
        self.dataunica.setCurrentIndex(self.dataunica.count() - 1)