from qgis.core import QgsProject, QgsRasterLayer, QgsMapSettings, QgsMapRendererCustomPainterJob, Qgis, QgsMessageLog, QgsWkbTypes, QgsVectorLayer, QgsPoint, QgsField, QgsFeature, QgsGeometry, QgsPointXY, QgsRaster, QgsMarkerSymbol, QgsLineSymbol, QgsArrowSymbolLayer, QgsVectorLayerSimpleLabeling, QgsPalLayerSettings, QgsTextFormat, QgsProperty, QgsCoordinateTransform, QgsLineString, QgsTextBackgroundSettings, QgsSpatialIndex, QgsRuleBasedRenderer, QgsMapLayer, QgsRectangle, QgsMeshLayer, QgsCoordinateReferenceSystem, QgsSymbolLayer
from PyQt5.QtWidgets import QDialog, QCheckBox, QComboBox, QPushButton, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGraphicsTextItem, QScrollArea, QLabel, QLineEdit, QDoubleSpinBox, QVBoxLayout, QHBoxLayout, QLabel, QListWidgetItem, QListWidget, QFileDialog, QStyledItemDelegate, QStyle, QAbstractSpinBox
from qgis.PyQt.QtCore import Qt, QRectF, QPointF, QSize, QVariant, QRect, QPoint, QEvent, QRegularExpression
from PyQt5.QtGui import QImage, QPainter, QPixmap, QColor, QFont, QPen, QBrush, QRegularExpressionValidator
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
from openpyxl.utils import get_column_letter
from openpyxl.styles import Border, Side
from PyQt5.QtGui import QDoubleValidator
from matplotlib.patches import Circle
import matplotlib.patches as patches
from typing import Optional, Tuple
from openpyxl import load_workbook
from qgis.gui import QgsMapCanvas
from PyQt5.QtCore import QLocale
import matplotlib.pyplot as plt
from functools import partial
from qgis.utils import iface
from qgis.PyQt import uic
import pandas as pd
import numpy as np
import processing
import tempfile
import typing
import math
import os
import re

FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'platoMDT.ui'))

class PlatoManager(QDialog, FORM_CLASS):
    def __init__(self, parent=None):
        """Constructor."""
        super(PlatoManager, self).__init__(parent)
        # Configura a interface do usuário a partir do Designer.
        self.setupUi(self)

        self.iface = iface

        # Altera o título da janela
        self.setWindowTitle("Gerar Platô sobre o MDT")

        # Inicializa o sinalizador para controlar a execução do gráfico
        self.is_generating_graph = False

        # Cria uma cena gráfica para o QGraphicsView
        self.scene = QGraphicsScene()
        self.graphicsViewRaster.setScene(self.scene)

        self.scenePoligono = QGraphicsScene()
        self.graphicsViewPoligono.setScene(self.scenePoligono)

        # Inicializa o ComboBox de Raster
        self.init_combo_box_raster()
        self.init_combo_box_poligono()

        self.scroll_widgets = []  # Armazena os widgets da scrollArea

        # Cria uma instância do GraficoManager e passa a instância atual de PlatoManager
        self.grafico_manager = GraficoManager(self)

        # Inicializa o caminho do logotipo como None
        self.logo_path = None

        # Configurações iniciais
        self.check_logo_status()  # Verifica e ajusta o estado do botão de logo

        # Conecta os sinais aos slots
        self.connect_signals()

        # Configura a janela para permitir minimizar, maximizar e fechar
        self.setWindowFlags(self.windowFlags() | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint)

        # Ao passar o mouse (hover) com um azul claro
        style = """QListWidget::item:hover {background: #cceeff;}   /* Azul claro no hover */"""
        self.listWidgetSegmentos.setStyleSheet(style)
        self.listWidgetEstacas.setStyleSheet(style)
        self.listWidgetTalude.setStyleSheet(style)

    def connect_signals(self):

        # Conecta os sinais existentes
        self.comboBoxRaster.currentIndexChanged.connect(self.display_raster)
        self.comboBoxPoligono.currentIndexChanged.connect(self.display_polygon)  # Conexão para atualizar o graphicsViewPoligono
        QgsProject.instance().layersAdded.connect(self.handle_layers_added)

        # Conectar o novo método que lida com a remoção de camadas
        QgsProject.instance().layersRemoved.connect(self.handle_layers_removed)

        for layer in QgsProject.instance().mapLayers().values():
            layer.nameChanged.connect(self.update_combo_boxes)

        # Conectar sinais relacionados a outras verificações que podem afetar o botão
        for layer in QgsProject.instance().mapLayers().values():
            layer.nameChanged.connect(self.update_pushButtonVertices_state)

        # Conecta o botão pushButtonCalcular para atualizar as camadas
        self.pushButtonCalcular.clicked.connect(self.calculate_and_create_segment_layers)

        # Atualiza a lista de segmentos ao abrir o projeto ou ao carregar camadas
        QgsProject.instance().layersAdded.connect(self.update_list_widget_segmentos)
        QgsProject.instance().layerWillBeRemoved.connect(self.update_list_widget_segmentos)

        # Atualiza a lista de Estaqueamento ao abrir o projeto ou ao carregar camadas
        QgsProject.instance().layersAdded.connect(self.update_list_widget_estaqueamentos)
        QgsProject.instance().layerWillBeRemoved.connect(self.update_list_widget_estaqueamentos)

        # Atualiza a lista de Taludes ao abrir o projeto ou ao carregar camadas
        QgsProject.instance().layersAdded.connect(self.update_list_widget_talude)
        QgsProject.instance().layerWillBeRemoved.connect(self.update_list_widget_talude)

        # Conecta o botão para calcular e gerar os pontos intermediários
        self.pushButtonEstaqueamento.clicked.connect(self.calculate_intermediate_points)

        # Conecta o botão para calcular e gerar os Taludes
        self.pushButtonTalude.clicked.connect(self.calculate_talude)

        # Conecta a seleção de itens no listWidgetSegmentos
        self.listWidgetSegmentos.itemSelectionChanged.connect(self.check_estaqueamento_button_state)

        # Conecta o valor do doubleSpinBoxEquidistante para ativar/desativar o botão
        self.doubleSpinBoxEquidistante.valueChanged.connect(self.check_estaqueamento_button_state)

        # Conectar o sinal do doubleSpinBoxEquidistante para atualizar o toolTip
        self.doubleSpinBoxEquidistante.valueChanged.connect(self.calculate_and_update_tooltip)

        # Conectar o sinal de seleção do listWidgetSegmentos para recalcular o toolTip
        self.listWidgetSegmentos.currentItemChanged.connect(self.calculate_and_update_tooltip)

        # Conectar o sinal de seleção do listWidgetSegmentos para recalcular o toolTip e ajustar o estado do doubleSpinBox
        self.listWidgetSegmentos.currentItemChanged.connect(self.update_doubleSpinBoxEquidistante_state)

        # Conectar o sinal de seleção do listWidgetEstacas para ajustar o estado do pushButtonTalude
        self.listWidgetEstacas.currentItemChanged.connect(self.update_pushButtonTalude_state)

        # Conecta as mudanças no listWidgetEstacas
        self.listWidgetEstacas.itemSelectionChanged.connect(self.update_radio_buttons_state)

        # Conecta as mudanças no listWidgetEstacas para ativar ou desativar o pushButtonGrafico
        self.listWidgetEstacas.itemSelectionChanged.connect(self.update_pushButtonGrafico_state)

        # Atualiza o estado do pushButtonVertices ao mudar a seleção no comboBoxPoligono
        self.comboBoxPoligono.currentIndexChanged.connect(self.update_pushButtonVertices_state)
        self.comboBoxRaster.currentIndexChanged.connect(self.update_pushButtonVertices_state)

        # Atualiza o estado do pushButtonJuntar na alteração do listWidgetEstacas e listWidgetTalude
        self.listWidgetEstacas.itemSelectionChanged.connect(self.update_pushButtonJuntar_state)
        self.listWidgetTalude.itemSelectionChanged.connect(self.update_pushButtonJuntar_state)

        # Conectar eventos relacionados a mudanças no projeto
        QgsProject.instance().layersAdded.connect(self.update_pushButtonVertices_state)
        QgsProject.instance().layersRemoved.connect(self.update_pushButtonVertices_state)

        # Desconectar quaisquer conexões existentes para evitar múltiplas conexões
        try:
            self.pushButtonGrafico.clicked.disconnect()
            self.pushButtonJuntar.clicked.disconnect()
            self.pushButtonLogo.clicked.disconnect()
            self.pushButtonVertices.clicked.disconnect()
        except TypeError:
            pass  # Nenhuma conexão existente

        # Conecta o botão para criar a camada de pontos a partir dos vértices
        self.pushButtonVertices.clicked.connect(self.create_point_layer_from_polygon)

        # Conecta o botão para calcular e gerar o Gráfico
        self.pushButtonGrafico.clicked.connect(self.on_pushButtonGrafico_clicked)

        # Conecta o botão para juntar as camadas
        self.pushButtonJuntar.clicked.connect(self.on_pushButtonJuntar_clicked)

        # Conectar o botão pushButtonCancelar para fechar o diálogo
        self.pushButtonCancelar.clicked.connect(self.close)

        # Conectar o botão pushButtonLogo para abrir a pasta
        self.pushButtonLogo.clicked.connect(self.on_pushButtonLogo_clicked)

        # Inicializar o botão "Calcular" como desativado
        self.pushButtonCalcular.setEnabled(False)

        # Conectar o botão "Vertices" ao método que ativará o botão "Calcular"
        self.pushButtonVertices.clicked.connect(self.on_pushButtonVertices_clicked)

        # Certifique-se de que a scrollAreaWidgetContents tenha um layout
        if not self.scrollAreaWidgetContents.layout():
            layout = QVBoxLayout()  # ou outro tipo de layout conforme necessário
            self.scrollAreaWidgetContents.setLayout(layout)

        # Instanciar o delegado e definir nos listWidgets
        self.delegateSegmentos = ListDeleteButtonDelegate(self.listWidgetSegmentos)
        self.listWidgetSegmentos.setItemDelegate(self.delegateSegmentos)

        self.delegateEstacas = ListDeleteButtonDelegate(self.listWidgetEstacas)
        self.listWidgetEstacas.setItemDelegate(self.delegateEstacas)

        self.delegateTalude = ListDeleteButtonDelegate(self.listWidgetTalude)
        self.listWidgetTalude.setItemDelegate(self.delegateTalude)

        # Conecte o evento de duplo clique ao listWidgetEstacas
        self.listWidgetEstacas.itemDoubleClicked.connect(self.open_excel_with_attributes)

    def init_combo_box_raster(self):
        """
        Inicializa o comboBox de raster, populando-o com as camadas raster do projeto
        e ajusta a seleção e visualização conforme necessário.
        """
        # Armazenar o índice atual selecionado
        current_raster_id = self.comboBoxRaster.currentData()

        layers = QgsProject.instance().mapLayers().values()
        raster_layers = [layer for layer in layers if layer.type() == layer.RasterLayer]
        self.comboBoxRaster.clear()
        for raster_layer in raster_layers:
            self.comboBoxRaster.addItem(raster_layer.name(), raster_layer.id())

        if current_raster_id:
            index = self.comboBoxRaster.findData(current_raster_id)
            if index != -1:
                self.comboBoxRaster.setCurrentIndex(index)  # Restaura a seleção anterior
            else:
                self.comboBoxRaster.setCurrentIndex(0)  # Seleciona o primeiro item se o anterior não existir
        else:
            if raster_layers:
                self.comboBoxRaster.setCurrentIndex(0)

        self.display_raster()

    def init_combo_box_poligono(self):
        """
        Inicializa o comboBox de polígonos, listando todas as camadas de polígono do projeto
        e conecta sinais para atualização automática da visualização.
        """
        # Inicializa o ComboBox de Polígono
        layers = QgsProject.instance().mapLayers().values()
        polygon_layers = [layer for layer in layers if layer.type() == layer.VectorLayer and layer.geometryType() == QgsWkbTypes.PolygonGeometry]
        self.comboBoxPoligono.clear()
        for polygon_layer in polygon_layers:
            self.comboBoxPoligono.addItem(polygon_layer.name(), polygon_layer.id())

            # Conecta os sinais featuresAdded e featuresDeleted para camadas de polígono existentes
            polygon_layer.featureAdded.connect(self.display_polygon)
            polygon_layer.featureDeleted.connect(self.display_polygon)

        if polygon_layers:
            self.comboBoxPoligono.setCurrentIndex(0)
            self.display_polygon()

    def display_raster(self):
        """
        Renderiza a camada raster selecionada no QGraphicsView, ajustando a visualização
        conforme o tamanho da viewport e a extensão do raster.
        """
        # Limpa a cena antes de adicionar um novo item
        self.scene.clear()

        # Obtém o ID da camada raster selecionada
        selected_raster_id = self.comboBoxRaster.currentData()

        # Busca a camada raster pelo ID
        selected_layer = QgsProject.instance().mapLayer(selected_raster_id)

        if isinstance(selected_layer, QgsRasterLayer):
            # Configurações do mapa
            map_settings = QgsMapSettings()
            map_settings.setLayers([selected_layer])  # Definimos a camada a ser renderizada
            map_settings.setBackgroundColor(QColor(255, 255, 255))
            
            # Define o tamanho da imagem a ser renderizada
            width = self.graphicsViewRaster.viewport().width()
            height = self.graphicsViewRaster.viewport().height()
            map_settings.setOutputSize(QSize(width, height))
            
            # Define a extensão do mapa (extensão do raster)
            map_settings.setExtent(selected_layer.extent())

            # Cria a imagem para renderizar
            image = QImage(width, height, QImage.Format_ARGB32)
            image.fill(Qt.transparent)

            # Configura o pintor e a tarefa de renderização
            painter = QPainter(image)
            render_job = QgsMapRendererCustomPainterJob(map_settings, painter)

            # Executa a renderização
            render_job.start()
            render_job.waitForFinished()
            painter.end()

            # Cria um pixmap a partir da imagem renderizada
            pixmap = QPixmap.fromImage(image)
            pixmap_item = QGraphicsPixmapItem(pixmap)

            # Adiciona o item à cena
            self.scene.addItem(pixmap_item)

            # Ajusta a cena ao QGraphicsView, garantindo que o modo de ajuste preserve a proporção
            self.graphicsViewRaster.setSceneRect(pixmap_item.boundingRect())
            self.graphicsViewRaster.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio)

    def display_polygon(self):
        """Atualiza a exibição no QGraphicsView:
           - se houver seleção: enquadra TODAS as selecionadas;
           - caso contrário: enquadra TODAS as feições do layer."""
        self.scenePoligono.clear()

        selected_polygon_id = self.comboBoxPoligono.currentData()
        selected_layer = QgsProject.instance().mapLayer(selected_polygon_id)

        if not selected_layer or not isinstance(selected_layer, QgsVectorLayer):
            return

        # garante que os sinais estão conectados para atualizar ao vivo
        self._ensure_polygon_signals_connected(selected_layer)

        # coleta feições poligonais válidas
        all_polys = []
        for f in selected_layer.getFeatures():
            g = f.geometry()
            if g and not g.isEmpty() and g.type() == QgsWkbTypes.PolygonGeometry:
                all_polys.append(f)

        if not all_polys:
            if self.isVisible():
                self.mostrar_mensagem("A camada de polígono não contém feições.", "Erro")
            return

        # selecionadas (apenas polígonos)
        selected_polys = []
        for f in selected_layer.selectedFeatures():
            g = f.geometry()
            if g and not g.isEmpty() and g.type() == QgsWkbTypes.PolygonGeometry:
                selected_polys.append(f)

        # decide conjunto a enquadrar
        feats_to_frame = selected_polys if selected_polys else all_polys

        # calcula extent unindo os bounding boxes
        extent = None
        for f in feats_to_frame:
            bb = f.geometry().boundingBox()
            if bb and not bb.isNull():
                if extent is None:
                    extent = QgsRectangle(bb)
                else:
                    extent.combineExtentWith(bb)

        if extent is None or extent.isNull():
            if self.isVisible():
                self.mostrar_mensagem("Não foi possível determinar o enquadramento das feições.", "Erro")
            return

        # prepara renderização
        map_settings = QgsMapSettings()
        map_settings.setLayers([selected_layer])
        map_settings.setBackgroundColor(QColor(255, 255, 255))

        width  = max(2, self.graphicsViewPoligono.viewport().width())
        height = max(2, self.graphicsViewPoligono.viewport().height())
        map_settings.setOutputSize(QSize(width, height))
        map_settings.setExtent(extent)

        image = QImage(width, height, QImage.Format_ARGB32)
        image.fill(Qt.transparent)

        painter = QPainter(image)
        render_job = QgsMapRendererCustomPainterJob(map_settings, painter)
        render_job.start()
        render_job.waitForFinished()
        painter.end()

        pixmap = QPixmap.fromImage(image)
        pixmap_item = QGraphicsPixmapItem(pixmap)
        self.scenePoligono.addItem(pixmap_item)
        self.graphicsViewPoligono.setSceneRect(pixmap_item.boundingRect())
        self.graphicsViewPoligono.fitInView(self.scenePoligono.sceneRect(), Qt.KeepAspectRatio)

    def _ensure_polygon_signals_connected(self, layer: QgsVectorLayer):
        """Conecta sinais para atualizar o preview ao alterar/selecionar/adicionar feições."""
        # evita reconectar no mesmo layer
        if getattr(self, "_poly_preview_layer_id", None) == layer.id():
            return
        self._poly_preview_layer_id = layer.id()

        # mudanças estruturais/seleção
        try:
            layer.featureAdded.connect(lambda fid: self.display_polygon())
        except Exception:
            pass
        try:
            layer.featuresDeleted.connect(lambda fids: self.display_polygon())
        except Exception:
            pass
        try:
            layer.geometryChanged.connect(lambda fid, geom: self.display_polygon())
        except Exception:
            pass
        try:
            layer.attributeValueChanged.connect(lambda fid, idx, val: self.display_polygon())
        except Exception:
            pass
        try:
            layer.selectionChanged.connect(lambda *_: self.display_polygon())
        except Exception:
            pass
        try:
            layer.extentsChanged.connect(lambda: self.display_polygon())
        except Exception:
            pass

    def get_z_value(self, x, y):
        """
        Retorna o valor de elevação (Z) do raster selecionado para as coordenadas (x, y).
        Exibe mensagem de erro se não for possível identificar o valor.
        """
        selected_raster_id = self.comboBoxRaster.currentData()
        raster_layer = QgsProject.instance().mapLayer(selected_raster_id)

        if not raster_layer or not isinstance(raster_layer, QgsRasterLayer):
            self.mostrar_mensagem("Camada raster inválida ou não selecionada.","Erro")
            return None

        identify_result = raster_layer.dataProvider().identify(
            QgsPointXY(x, y), 
            QgsRaster.IdentifyFormatValue)

        if identify_result.isValid():
            results = identify_result.results()
            if results:
                band_key = list(results.keys())[0]
                z_value = results.get(band_key)  # <-- Pode ser None
                if z_value is None:
                    self.mostrar_mensagem(
                        f"Não há valor de elevação (Z) para o pixel em ({x}, {y}). "
                        "Verifique se o polígono está dentro do raster.", "Erro")
                    return None
                # Se `z_value` for válido, convertemos para float
                return round(float(z_value), 3)

        # Caso identify_result não seja válido ou esteja vazio
        self.mostrar_mensagem(
            f"Não foi possível identificar o valor Z em ({x}, {y}). "
            "Verifique se o polígono está dentro do raster.", "Erro")
        return None

    def _log_message(self, message, level=Qgis.Info):
        QgsMessageLog.logMessage(message, 'TALUDE', level=level)

    def mostrar_mensagem(self, texto, tipo, duracao=3, caminho_pasta=None, caminho_arquivos=None):
        """
        Exibe uma mensagem na barra de mensagens da interface do QGIS.

        Parâmetros:
        texto: str - O texto da mensagem a ser exibida.
        tipo: str - O tipo da mensagem, que pode ser "Erro" ou "Sucesso".
        duracao: int - A duração da mensagem em segundos (padrão é 3 segundos).

        Funções:
        - Obtém a barra de mensagens da interface do QGIS.
        - Exibe uma mensagem de erro ou sucesso com o nível apropriado baseado no tipo fornecido.
        """
        bar = self.iface.messageBar()  # Obtém a barra de mensagens do QGIS

        if tipo == "Erro":
            # Exibe mensagem de erro (ícone vermelho)
            bar.pushMessage("Erro", texto, level=Qgis.Critical, duration=duracao)

        elif tipo == "Sucesso":
            # Cria mensagem de sucesso (ícone azul/informativo)
            msg = bar.createMessage("Sucesso", texto)

            # Exibe a mensagem na barra
            bar.pushWidget(msg, level=Qgis.Info, duration=duracao)

        elif tipo == "Aviso":
            # Exibe mensagem de aviso (ícone amarelo)
            bar.pushMessage("Aviso", texto, level=Qgis.Warning, duration=duracao)

    def reset_scroll_area(self):
        """Reseta e limpa a área de rolagem (scrollArea) e a lista de widgets (scroll_widgets)."""
        # Limpa a lista de widgets armazenada
        self.scroll_widgets.clear()

        # Limpa o layout do scrollArea
        layout = self.scrollAreaWidgetContents.layout()
        
        if layout is not None:
            while layout.count() > 0:
                item = layout.takeAt(0)
                if item.widget() is not None:
                    item.widget().deleteLater()
                elif item.layout() is not None:
                    self.clear_layout(item.layout())  # Limpa layouts aninhados

    def clear_layout(self, layout):
        """Limpa todos os widgets e layouts de um layout específico."""
        while layout.count() > 0:
            item = layout.takeAt(0)
            if item.widget() is not None:
                item.widget().deleteLater()
            elif item.layout() is not None:
                self.clear_layout(item.layout())

    def showEvent(self, event):
        """
        Trata o evento de exibição do diálogo, atualizando visualizações e resetando a interface ao abrir.
        """
        super(PlatoManager, self).showEvent(event)

        # Limpa a lista de widgets no scrollArea
        self.reset_scroll_area()

        # Ajusta a visualização quando o diálogo é mostrado
        self.display_raster()
        self.display_polygon()

        # Atualiza o listWidgetSegmentos ao abrir a janela
        self.update_list_widget_segmentos()
        self.update_list_widget_estaqueamentos()
        self.update_list_widget_talude()

        # Reseta o estado do botão pushButtonEstaqueamento (desativado)
        self.pushButtonEstaqueamento.setEnabled(False)

        # Reseta o valor do doubleSpinBoxEquidistante
        self.doubleSpinBoxEquidistante.setValue(0)

        # Verifica e aplica as configurações de ativação do pushButtonVertices
        self.update_pushButtonVertices_state()

        # Atualiza o estado do doubleSpinBoxEquidistante baseado no listWidgetSegmentos
        self.update_doubleSpinBoxEquidistante_state()

        # Atualiza o estado do pushButtonTalude baseado no listWidgetEstacas
        self.update_pushButtonTalude_state()
        
        # Atualiza o estado do radioButtonEsquerda e radioButtonDireita baseado no listWidgetEstacas
        self.update_radio_buttons_state()
        
        # Atualiza o estado do pushButtonGrafico baseado no listWidgetEstacas
        self.update_pushButtonGrafico_state()

        # Atualiza o estado do pushButtonJuntar baseado no listWidgetEstacas ou listWidgetTalude
        self.update_pushButtonJuntar_state()

        # Atualiza o estado do pushButtonCalcular baseado no click do botão pushButtonVertices
        self.on_pushButtonVertices_clicked()

        self.connect_selection_signal() # Chama após carregar ou selecionar a camada

    def closeEvent(self, event):
        """
        Trata o evento de fechamento do diálogo, removendo referências e executando ações de limpeza.
        """
        parent = self.parent()
        if parent:
            parent.plato_mdt_dlg = None
        super(PlatoManager, self).closeEvent(event)

    def update_combo_box(self):
        """
        Atualiza o comboBox de raster ao adicionar ou remover camadas no projeto,
        tentando manter a seleção anterior quando possível.
        """
        # Armazena a seleção atual para restaurar após a atualização
        current_index = self.comboBoxRaster.currentIndex()
        current_layer_id = self.comboBoxRaster.itemData(current_index)

        # Atualiza o combo box quando camadas são adicionadas ou removidas
        self.init_combo_box_raster()

        # Tenta restaurar a seleção anterior
        if current_layer_id:
            index = self.comboBoxRaster.findData(current_layer_id)
            if index != -1:
                self.comboBoxRaster.setCurrentIndex(index)
            else:
                # Se a camada não existe mais, seleciona a primeira disponível
                if self.comboBoxRaster.count() > 0:
                    self.comboBoxRaster.setCurrentIndex(0)
                    self.display_raster()

    def update_combo_boxes(self):
        """Atualiza ambos os combo boxes"""
        self.init_combo_box_raster()
        self.init_combo_box_poligono()

    def handle_layers_added(self, layers):
        """
        Atualiza os comboBoxes ao adicionar camadas, restaurando seleções anteriores,
        e conecta sinais para atualização automática da visualização de polígonos.
        """
        # Salva qual polígono está selecionado no momento
        current_polygon_id = self.comboBoxPoligono.currentData()
        
        # Atualiza comboBoxes normalmente
        self.update_combo_boxes()
        
        # Tenta restaurar a seleção do polígono anterior
        if current_polygon_id is not None:
            index = self.comboBoxPoligono.findData(current_polygon_id)
            if index != -1:
                self.comboBoxPoligono.setCurrentIndex(index)

        # Agora verifica se foi adicionado algum layer de polígono
        polygon_layers = [layer for layer in layers 
                          if isinstance(layer, QgsVectorLayer) 
                          and layer.geometryType() == QgsWkbTypes.PolygonGeometry]

        if polygon_layers:
            self.display_polygon()

            # Conecta sinais para atualizar visualização sempre que 
            # feições forem adicionadas/removidas
            for polygon_layer in polygon_layers:
                polygon_layer.featureAdded.connect(self.display_polygon)
                polygon_layer.featureDeleted.connect(self.display_polygon)

    def handle_layers_removed(self, removed_layers_ids):
        """Função para tratar a remoção de camadas."""
        # Atualizar os ComboBoxes de Raster e Polígono
        self.update_combo_boxes()

        # Verificar e atualizar os grupos e widgets associados
        self.check_layers_in_group_and_update(removed_layers_ids)

    def apply_arrow_symbology(self, layer: QgsVectorLayer):
        """
        Seta sem borda:
          - preenchimento azul (#0066CC) se Inclinação > 0
          - preenchimento vermelho (#CC0000) se Inclinação < 0
          - preenchimento preto (#000000)   se Inclinação = 0
        """
        field_name = "Inclinação"

        # expressão para cor do preenchimento baseada no campo (fração)
        color_expr = (
            f'CASE '
            f'WHEN "{field_name}" > 0 THEN color_rgb(0, 85, 255) '   # -- azul
            f'WHEN "{field_name}" < 0 THEN color_rgb(255, 65, 65) '     # -- vermelho
            f'ELSE color_rgb(0, 0, 0) END')                           # -- preto

        # cria APENAS a camada de seta (sem base de linha)
        arrow_layer = QgsArrowSymbolLayer()
        arrow_layer.setHeadType(QgsArrowSymbolLayer.HeadSingle)
        arrow_layer.setHeadLength(6)
        arrow_layer.setHeadThickness(1.4)
        arrow_layer.setArrowStartWidth(0.4)
        arrow_layer.setArrowWidth(0.6)
        arrow_layer.setColor(QColor('#000000'))  # default; será sobrescrito por data-defined

        # aplica cor data-defined no preenchimento do sub-símbolo (fill)
        sub_symbol: QgsFillSymbol = arrow_layer.subSymbol()
        if sub_symbol:
            for sl in sub_symbol.symbolLayers():
                # preenchimento controlado pela expressão
                sl.setDataDefinedProperty(
                    QgsSymbolLayer.PropertyFillColor,
                    QgsProperty.fromExpression(color_expr))
                # remove COMPLETAMENTE a borda/contorno
                if hasattr(sl, "setStrokeStyle"):
                    sl.setStrokeStyle(Qt.NoPen)
                if hasattr(sl, "setStrokeWidth"):
                    sl.setStrokeWidth(0.0)
                if hasattr(sl, "setStrokeColor"):
                    sl.setStrokeColor(QColor(0, 0, 0, 0))  # transparente

        # símbolo de linha SEM layers-base (sem traço)
        line_symbol = QgsLineSymbol()
        while line_symbol.symbolLayerCount() > 0:
            line_symbol.deleteSymbolLayer(0)

        # adiciona somente a seta
        line_symbol.appendSymbolLayer(arrow_layer)

        # aplica na camada
        if layer.renderer():
            layer.renderer().setSymbol(line_symbol)
        else:
            layer.setRenderer(QgsSingleSymbolRenderer(line_symbol))

        layer.triggerRepaint()

    def activate_labels_for_points(self, point_layer):
        """Ativa os rótulos de 'NovoZ' na camada de pontos com formatação simples, em negrito e cor preta, com deslocamento."""
        layer_settings = QgsPalLayerSettings()
        text_format = QgsTextFormat()

        # Configurações básicas do rótulo
        layer_settings.isExpression = True  # Definimos como expressão para combinar 'ID' e 'NovoZ'
        layer_settings.fieldName = "'ID: ' || \"ID\" || '\nNovoZ: ' || \"NovoZ\""  # Expressão para exibir ID e NovoZ com quebra de linha
        layer_settings.placement = QgsPalLayerSettings.OverPoint  # Coloca o rótulo sobre o ponto

        # Configurações do texto do rótulo
        font = QFont("Arial", 10)
        font.setBold(True)  # Negrito simples
        text_format.setFont(font)
        text_format.setSize(10)  # Tamanho do texto

        # Definir a cor do texto (preto)
        text_format.setColor(QColor(0, 0, 0))  # Cor preta para o rótulo

        # Aplicar o formato de texto ao rótulo
        layer_settings.setFormat(text_format)

        # Configuração de deslocamento (offset) do rótulo
        layer_settings.xOffset = 6  # Deslocamento horizontal
        layer_settings.yOffset = 3  # Deslocamento vertical

        # Definir a rotulagem na camada
        labeling = QgsVectorLayerSimpleLabeling(layer_settings)
        point_layer.setLabeling(labeling)
        point_layer.setLabelsEnabled(True)
        point_layer.triggerRepaint()

    def activate_labels_on_layer(self, layer, field_name):
        """Ativa os rótulos em uma camada de linhas com símbolos de seta no rótulo e formatação condicional."""
        
        layer_settings = QgsPalLayerSettings()
        text_format = QgsTextFormat()

        # Configurações básicas do rótulo
        layer_settings.isExpression = True  # Definimos como expressão para incluir setas

        # use o nome do campo na var field_name (ex.: "Inclinação")
        layer_settings.fieldName = (
            f"with_variable('p', \"{field_name}\" * 100, "        # -- p = valor em %
            " with_variable('pr', round(@p, 3), "                 # -- pr = % arredondado a 3 casas
            "   CASE "
            "     WHEN abs(@pr) < 0.0005 THEN '→' || format_number(0, 3) || '%' "
            "     WHEN @pr < 0 THEN '⬇' || format_number(@pr, 3) || '%' "
            "     ELSE '⬆' || format_number(@pr, 3) || '%' "
            "   END""))")

        layer_settings.placement = QgsPalLayerSettings.Line  # Coloca o rótulo ao longo da linha

        # Configurações do texto do rótulo
        font = QFont("Arial", 11)
        font.setBold(True)
        font.setItalic(True)
        text_format.setFont(font)
        text_format.setSize(11)

        # Configuração do fundo branco para o rótulo
        background = QgsTextBackgroundSettings()
        background.setEnabled(True)
        background.setFillColor(QColor(255, 255, 0))  # Cor de fundo amarelo
        text_format.setBackground(background)

        # Adiciona formatação condicional (vermelho para negativo, azul para positivo, preto para zero)
        color_expression = """CASE
                                WHEN "{field}" < 0 THEN '255,0,0'  -- Vermelho para inclinações negativas
                                WHEN "{field}" > 0 THEN '0,0,255'  -- Azul para inclinações positivas
                                ELSE '0,0,0'  -- Preto para zero
                              END""".format(field=field_name)

        # Definir as propriedades dos rótulos (cor, tamanho)
        properties = layer_settings.dataDefinedProperties()
        properties.setProperty(QgsPalLayerSettings.Color, QgsProperty.fromExpression(color_expression))
        properties.setProperty(QgsPalLayerSettings.Size, QgsProperty.fromValue(11))  # Tamanho da fonte

        # Aplicar o formato de texto ao rótulo
        layer_settings.setFormat(text_format)
        layer_settings.setDataDefinedProperties(properties)

        # Definir a rotulagem na camada
        labeling = QgsVectorLayerSimpleLabeling(layer_settings)
        layer.setLabeling(labeling)
        layer.setLabelsEnabled(True)
        layer.triggerRepaint()

    def create_point_layer_from_polygon(self):
        """
        Cria uma camada de pontos a partir dos vértices de um polígono selecionado, 
        atribui valores de elevação do raster, ativa rótulos e gera camada de linhas dos segmentos.
        """
        selected_polygon_id = self.comboBoxPoligono.currentData()
        selected_layer = QgsProject.instance().mapLayer(selected_polygon_id)
        
        if selected_layer and isinstance(selected_layer, QgsVectorLayer):
            features = [f for f in selected_layer.getFeatures() if f.geometry().type() == QgsWkbTypes.PolygonGeometry]

            # Verifica se há apenas uma feição ou uma feição selecionada
            if len(features) == 1 or selected_layer.selectedFeatureCount() == 1:
                if selected_layer.selectedFeatureCount() == 1:
                    feature = selected_layer.selectedFeatures()[0]
                else:
                    feature = features[0]

                feature = self._get_active_polygon_feature(selected_layer)
                poly_id = self._get_poly_id_for_naming(selected_layer, feature)

                # Nome com ID da feição
                point_layer_name = f"{selected_layer.name()}_ID{poly_id}_Vértices"

                # Se já existir, remove
                existing_layers = QgsProject.instance().mapLayersByName(point_layer_name)
                if existing_layers:
                    try:
                        QgsProject.instance().removeMapLayer(existing_layers[0].id())
                    except Exception as e:
                        self.mostrar_mensagem(f"Não foi possível sobrescrever a camada existente: {str(e)}", "Erro")
                        return

                # Cria a camada de pontos
                point_layer = QgsVectorLayer('Point?crs=' + selected_layer.crs().authid(), point_layer_name, 'memory')
                pr = point_layer.dataProvider()

                # Adiciona campos "ID", "X", "Y", "Z", "NovoZ"
                pr.addAttributes([QgsField("ID", QVariant.Int),
                                  QgsField("X", QVariant.Double),
                                  QgsField("Y", QVariant.Double),
                                  QgsField("Z", QVariant.Double),
                                  QgsField("NovoZ", QVariant.Double)])
                point_layer.updateFields()

                # Obtém o raster selecionado para calcular o valor de Z
                selected_raster_id = self.comboBoxRaster.currentData()
                raster_layer = QgsProject.instance().mapLayer(selected_raster_id)

                if not raster_layer or not isinstance(raster_layer, QgsRasterLayer):
                    self.mostrar_mensagem("Camada raster inválida ou não selecionada.", "Erro")
                    return

                # Obtém a geometria da feição selecionada
                geom = feature.geometry()
                if geom.isMultipart():
                    multi = geom.asMultiPolygon()
                    if not multi or not multi[0]:
                        self.mostrar_mensagem("Não foi possível obter os vértices do MultiPolygon.", "Erro")
                        return
                    vertices = multi[0][0]
                else:
                    poly = geom.asPolygon()
                    if not poly or not poly[0]:
                        self.mostrar_mensagem("Não foi possível obter os vértices do Polígono.", "Erro")
                        return
                    vertices = poly[0]

                # Remove o último vértice se for igual ao primeiro para evitar duplicação
                if vertices[0] == vertices[-1]:
                    vertices.pop()

                points = []
                for i, vertex in enumerate(vertices):
                    x = round(vertex.x(), 3)
                    y = round(vertex.y(), 3)

                    # Identifica o valor no raster
                    identify_result = raster_layer.dataProvider().identify(QgsPointXY(x, y), QgsRaster.IdentifyFormatValue)

                    # Verifica se foi válido e se há resultados
                    if identify_result.isValid():
                        results = identify_result.results()
                        if results:
                            band_key = list(results.keys())[0]
                            z_val = results[band_key]
                            
                            # Aqui está o ponto principal: verificar se o z_val é None
                            if z_val is None:
                                self.mostrar_mensagem(
                                    f"Nenhum valor de elevação (Z) encontrado em ({x}, {y}). Verifique se seu polígono está dentro do raster.",
                                    "Erro")
                                # Decida se quer pular esse ponto (continue) ou encerrar o método (return).
                                continue  
                            
                            z = round(float(z_val), 3)
                        else:
                            self.mostrar_mensagem(
                                f"Não há resultados de elevação para ({x}, {y}). Verifique se seu polígono está dentro do raster.",
                                "Erro")
                            continue
                    else:
                        self.mostrar_mensagem(
                            f"Não foi possível identificar valor de elevação para ({x}, {y}). Polígono pode estar fora do raster.",
                            "Erro")
                        continue

                    # Cria o ponto
                    point = QgsFeature(point_layer.fields())
                    point.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))
                    point.setAttribute("ID", i + 1)
                    point.setAttribute("X", x)
                    point.setAttribute("Y", y)
                    point.setAttribute("Z", z)
                    point.setAttribute("NovoZ", z)
                    points.append(point)

                    # Adiciona o ponto à camada de pontos
                    pr.addFeature(point)

                # Em vez de addMapLayer simples, insere no topo:
                self._top_insert_layer(point_layer)
                self.mostrar_mensagem("Camada de pontos criada com sucesso.", "Sucesso")

                # Ativar os rótulos de 'NovoZ' na camada de pontos
                self.activate_labels_for_points(point_layer)

                # Criar a camada de linhas, incluindo a linha que fecha o polígono
                self.create_line_layer_from_points(points, selected_layer.crs())

                # Popula a scrollArea com os dados das camadas
                self.populate_scroll_area()

            else:
                self.mostrar_mensagem("Selecione apenas 1 feição", "Erro")

    def _get_active_polygon_feature(self, selected_layer):
        """Retorna a feição de polígono 'ativa' (selecionada; senão a primeira válida)."""
        features = [f for f in selected_layer.getFeatures()
                    if f.geometry() and not f.geometry().isEmpty()
                    and f.geometry().type() == QgsWkbTypes.PolygonGeometry]
        if selected_layer.selectedFeatureCount() == 1:
            return selected_layer.selectedFeatures()[0]
        return features[0] if features else None

    def _get_poly_id_for_naming(self, selected_layer, feature):
        """
        Se existir um campo 'ID' (ou 'Id'/'id') não nulo, usa esse valor.
        Caso contrário, usa a ID interna (feature.id()).
        Retorna string (ex.: '1', '23A', etc.).
        """
        if feature is None:
            return None
        field_names = [f.name() for f in selected_layer.fields()]
        for cand in ("ID", "Id", "id"):
            if cand in field_names:
                val = feature[cand]
                if val is not None and str(val).strip() != "":
                    return str(val)
        return str(feature.id())

    def _top_insert_layer(self, layer):
        """Garante que a camada entre no topo da lista de camadas."""
        root = QgsProject.instance().layerTreeRoot()
        QgsProject.instance().addMapLayer(layer, False)
        root.insertLayer(0, layer)

    def create_line_layer_from_points(self, points, crs):
        """
        Cria uma camada de linhas conectando pontos sequenciais (fechando o polígono),
        calcula comprimento e inclinação dos segmentos e aplica simbologia e rótulos.
        """
        # Obter o nome da camada de polígono selecionada
        selected_polygon_id = self.comboBoxPoligono.currentData()
        selected_layer = QgsProject.instance().mapLayer(selected_polygon_id)
        if not selected_layer:
            return
        
        feature = self._get_active_polygon_feature(selected_layer)
        poly_id = self._get_poly_id_for_naming(selected_layer, feature)

        # Definir o nome da camada de linhas baseado na camada de polígono
        line_layer_name = f"{selected_layer.name()}_ID{poly_id}_Segmentos"

        # Se já existir, remover
        existing_layers = QgsProject.instance().mapLayersByName(line_layer_name)
        if existing_layers:
            try:
                QgsProject.instance().removeMapLayer(existing_layers[0].id())
            except Exception as e:
                self.mostrar_mensagem(f"Não foi possível sobrescrever a camada existente: {str(e)}", "Erro")
                return

        # Cria a camada de linhas
        line_layer = QgsVectorLayer(f'LineString?crs={crs.authid()}', line_layer_name, 'memory')
        pr = line_layer.dataProvider()

        # # Adiciona campos "ID", "Comprimento", "Inclinação"
        pr.addAttributes([QgsField("ID", QVariant.Int),
                          QgsField("Comprimento", QVariant.Double, "", 20, 3),
                          QgsField("Inclinação",  QVariant.Double, "", 20, 6)])

        line_layer.updateFields()

        num_points = len(points)

        for i in range(num_points):
            point1 = points[i]
            point2 = points[(i + 1) % num_points]  # Conecta o último ponto ao primeiro

            # Cria a linha entre dois pontos consecutivos
            line = QgsFeature(line_layer.fields())
            line_geom = QgsGeometry.fromPolylineXY([point1.geometry().asPoint(), point2.geometry().asPoint()])
            comprimento = round(line_geom.length(), 3)

            # Calcula a inclinação
            z1 = point1['NovoZ']
            z2 = point2['NovoZ']
            inclinacao = round((float(z2) - float(z1)) / comprimento if comprimento != 0 else 0, 5)

            # Atribui os valores à feição
            line.setGeometry(line_geom)
            line.setAttribute("ID", i + 1)
            line.setAttribute("Comprimento", comprimento)
            line.setAttribute("Inclinação", inclinacao)

            # Adiciona a linha à camada de linhas
            pr.addFeature(line)

        # Atribui a simbologia de seta à camada de linhas
        self.apply_arrow_symbology(line_layer)

        # Ativa os rótulos na camada de linhas (usando o campo "Inclinação" como exemplo)
        self.activate_labels_on_layer(line_layer, "Inclinação")

        # Em vez de addMapLayer simples:
        self._top_insert_layer(line_layer)
        self.mostrar_mensagem("Camada de linhas criada com sucesso.", "Sucesso")

    def populate_scroll_area(self):
        """
        Preenche a área de rolagem com widgets contendo informações dos pontos e linhas
        das camadas geradas, permitindo edição visual dos valores.
        """
        # Reset the scroll area before setting it up again
        self.reset_scroll_area()

        # Obter o nome da camada de polígono selecionada
        selected_polygon_id = self.comboBoxPoligono.currentData()
        selected_layer = QgsProject.instance().mapLayer(selected_polygon_id)

        if not selected_layer:
            return

        feature = self._get_active_polygon_feature(selected_layer)
        poly_id = self._get_poly_id_for_naming(selected_layer, feature)

        # Definir o nome da camada de pontos baseada na camada de polígono
        point_layer_name = f"{selected_layer.name()}_ID{poly_id}_Vértices"
        line_layer_name  = f"{selected_layer.name()}_ID{poly_id}_Segmentos"

        # Obter as camadas de pontos e linhas pelo nome dinâmico
        point_layer = QgsProject.instance().mapLayersByName(point_layer_name)
        line_layer  = QgsProject.instance().mapLayersByName(line_layer_name)

        if not point_layer or not line_layer:
            self.mostrar_mensagem(f"Camadas '{point_layer_name}' ou 'Linhas' não encontradas.", "Erro")
            return

        point_layer = point_layer[0]  # Obtém a primeira camada correspondente (espera-se que só haja uma)
        line_layer = line_layer[0]

        points = []
        lines = []

        # Extrair dados da camada de pontos
        for feature in point_layer.getFeatures():
            point_data = {
                'ID': feature['ID'],
                'Z': feature['Z'],
                'NovoZ': feature['NovoZ']}
            points.append(point_data)

        # Extrair dados da camada de linhas
        for feature in line_layer.getFeatures():
            line_data = {
                'ID': feature['ID'],
                'Inclinação': feature['Inclinação'],
                'Comprimento': feature['Comprimento']}
            lines.append(line_data)

        # Popula a scroll area com os dados obtidos
        self.setup_scroll_area(points, lines)

    def setup_scroll_area(self, points, lines):
        """
        Monta os widgets e layouts da scroll area para exibir e editar informações
        dos pontos e segmentos, conectando sinais de atualização da interface.
        """
        self.points = points  # Armazena os pontos para uso posterior
        self.lines = lines  # Armazena as linhas para uso posterior

        # Certifica-se de que o layout da scrollArea está configurado
        layout = self.scrollAreaWidgetContents.layout()
        if layout is None:
            layout = QVBoxLayout(self.scrollAreaWidgetContents)
            self.scrollAreaWidgetContents.setLayout(layout)
        else:
            # Limpa a scroll area
            while layout.count() > 0:
                item = layout.takeAt(0)
                if item.widget():
                    item.widget().deleteLater()

        self.scroll_widgets = []  # Armazena referências aos widgets para fácil acesso

        # Adiciona cabeçalhos
        header_layout = QHBoxLayout()
        headers = ["ID's", "Z", "NovoZ", "Inclinação", "Desnível"]
        
        for header in headers:
            label = QLabel(header)
            label.setAlignment(Qt.AlignLeft)  # Centraliza o texto do cabeçalho
            label.setStyleSheet("font-weight: bold;")  # Negrito para o cabeçalho
            header_layout.addWidget(label)
        
        layout.addLayout(header_layout)

        # Cria as linhas de inputs para cada ponto
        for i, point in enumerate(points):
            hbox = QHBoxLayout()

            # QLabel para o ID
            label_id = QLabel(f"ID: {i + 1}")
            label_id.setAlignment(Qt.AlignLeft)  # Centraliza o texto do ID
            label_id.setFixedHeight(18)     # altura em pixels
            label_id.setFixedWidth(30)      # largura em pixels (opcional)
            hbox.addWidget(label_id)

            # QLineEdit para o Z (não editável)
            line_edit_z = QLineEdit()
            line_edit_z.setText(str(point['Z']))
            line_edit_z.setReadOnly(True)
            line_edit_z.setAlignment(Qt.AlignLeft)  # Centraliza o texto
            line_edit_z.setFixedHeight(21)     # altura em pixels
            line_edit_z.setFixedWidth(50)      # largura em pixels (opcional)
            
            hbox.addWidget(line_edit_z)

            # QLineEdit para o NovoZ (editável)
            line_edit_novoz = QLineEdit()
            line_edit_novoz.setText(str(point['NovoZ']))
            line_edit_novoz.setAlignment(Qt.AlignLeft)  # Centraliza o texto à esquerda
            line_edit_novoz.setFixedHeight(21)     # altura em pixels
            line_edit_novoz.setFixedWidth(55)      # largura em pixels (opcional)
            # Aplica o estilo (fundo, negrito, itálico, tamanho da fonte)
            line_edit_novoz.setStyleSheet("""background-color: #e0f7ff; font-style: italic; font-weight: bold; font-size: 7.8pt""")

            # # Configura o QDoubleValidator para aceitar apenas números com , ou .
            regex = QRegularExpression(r"-?\d+(?:[.,]\d{0,3})?")   # aceita -123, 123.45 ou 123,45
            validator = QRegularExpressionValidator(regex, line_edit_novoz)
            line_edit_novoz.setValidator(validator)

            line_edit_novoz.editingFinished.connect(lambda idx=i: self.update_novoz(idx))
            hbox.addWidget(line_edit_novoz)

            # QLineEdit para a Inclinação (EDITÁVEL)
            line_edit_inclinacao = QLineEdit()
            if i < len(lines):
                inc = lines[i].get('Inclinação', 0.0)  # fração (0.05 -> 5%)
                line_edit_inclinacao.setText(f"{inc*100:.3f}%")
                self._apply_inclinacao_style(line_edit_inclinacao, inc*100)  # <<< AQUI
            else:
                line_edit_inclinacao.setText("0.000%")
                self._apply_inclinacao_style(line_edit_inclinacao, 0.0)      # <<< AQUI
            line_edit_inclinacao.setReadOnly(False)
            line_edit_inclinacao.setAlignment(Qt.AlignLeft)
            line_edit_inclinacao.setFixedHeight(21)
            line_edit_inclinacao.setFixedWidth(60)

            regex_inc = QRegularExpression(r"^-?\d+(?:[.,]\d{1,3})?\s*%?$")
            line_edit_inclinacao.setValidator(QRegularExpressionValidator(regex_inc, line_edit_inclinacao))

            line_edit_inclinacao.setProperty("user_edited", False)  # inicializa a flag

            # Sinal que indica edição real do usuário (NÃO é emitido em setText programático)
            line_edit_inclinacao.textEdited.connect(lambda _t, le=line_edit_inclinacao: le.setProperty("user_edited", True))

            # No editingFinished, passamos o próprio QLineEdit para checar a flag
            line_edit_inclinacao.editingFinished.connect(lambda idx=i, le=line_edit_inclinacao: self._on_inclinacao_edit_finished(idx, le))

            hbox.addWidget(line_edit_inclinacao)

            # QDoubleSpinBox para o Desnível (editável)
            spin_box_desnivel = QDoubleSpinBox()
            spin_box_desnivel.setDecimals(3)
            spin_box_desnivel.setMinimum(-9999.99)
            spin_box_desnivel.setMaximum(9999.99)
            spin_box_desnivel.setSingleStep(0.5)
            desnivel = point['NovoZ'] - point['Z']

            spin_box_desnivel.setValue(desnivel)  # Valor inicial baseado no NovoZ
            self._apply_desnivel_style(spin_box_desnivel, desnivel)   # Controla as cores
            spin_box_desnivel.setAlignment(Qt.AlignLeft) # Centraliza o texto
            # Ajusta o tamanho da caixa
            spin_box_desnivel.setFixedHeight(21)     # altura em pixels
            spin_box_desnivel.setFixedWidth(60)      # largura em pixels (opcional)
            # fundo azul claro
            spin_box_desnivel.setStyleSheet("background-color: #e0f7ff;")
            # não dispara valueChanged a cada tecla → só quando sai do campo (ou usa as setas)
            spin_box_desnivel.setKeyboardTracking(False)
            # corrige/formata somente ao finalizar edição/perder foco
            spin_box_desnivel.setCorrectionMode(QAbstractSpinBox.CorrectToNearestValue)

            # quando o usuário terminar a edição, aplicamos o cálculo
            spin_box_desnivel.editingFinished.connect(lambda idx=i: self._on_desnivel_edit_finished(idx))

            hbox.addWidget(spin_box_desnivel)

            # distância do segmento i (i -> i+1). Tenta em 'lines'; se não houver, 0.0
            dist_i = 0.0
            if i < len(lines):
                dist_i = (lines[i].get('Dist') or
                          lines[i].get('Distancia') or
                          lines[i].get('Distância') or
                          lines[i].get('Comprimento') or 0.0)

            self.scroll_widgets.append({
                'line_edit_novoz': line_edit_novoz,
                'line_edit_inclinacao': line_edit_inclinacao,
                'spin_box_desnivel': spin_box_desnivel,
                'z_val': float(point['Z']),
                'dist': float(
                    (lines[i].get('Dist') if i < len(lines) else 0.0) or
                    (lines[i].get('Distancia') if i < len(lines) else 0.0) or
                    (lines[i].get('Distância') if i < len(lines) else 0.0) or
                    (lines[i].get('Comprimento') if i < len(lines) else 0.0) or 0.0)})

            # Agora que o dicionário já está na lista, conecte o sinal
            spin_box_desnivel.valueChanged.connect(lambda val, idx=i: self.update_desnivel(val, idx))

            # E o mesmo para o line_edit_novoz, se necessário
            line_edit_novoz.editingFinished.connect(lambda idx=i: self.update_novoz(idx))

            layout.addLayout(hbox)

        layout.addStretch()  # Adiciona um esticador para garantir que o conteúdo fique no topo

    def _apply_desnivel_style(self, spin: QDoubleSpinBox, value: float):
        """Aplica cor ao texto do Desnível (+ azul, - vermelho, 0 preto), mantendo o estilo atual."""
        if value > 0:
            color = "#0000ff"   # azul
        elif value < 0:
            color = "#CC0000"   # vermelho
        else:
            color = "#000000"   # preto
        # mantém seu fundo/estilo; só garante a cor do texto
        spin.setStyleSheet("background-color: #e0f7ff; color: %s;" % color)

    def _apply_inclinacao_style(self, le: QLineEdit, pct_value: float):
        """Aplica o estilo do campo de Inclinação com cor por sinal (+ azul, - vermelho, 0 preto)."""
        if pct_value > 0:
            color = "#0066CC"   # azul
        elif pct_value < 0:
            color = "#CC0000"   # vermelho
        else:
            color = "#000000"   # preto

        base = "background-color: #e0f7ff; font-style: italic; font-weight: bold; font-size: 7.9pt;"
        le.setStyleSheet(base + f" color: {color};")

    def _on_desnivel_edit_finished(self, idx: int):
        """Aplica o desnível apenas quando o usuário sai do spin (editingFinished)."""
        if idx < 0 or idx >= len(self.scroll_widgets):
            return
        val = self.scroll_widgets[idx]['spin_box_desnivel'].value()
        self.update_desnivel(val, idx)

    def _fmt(self, v: float, prec: int = 3) -> str:
        """Formata número com casas decimais, usando ponto."""
        try:
            return f"{float(v):.{prec}f}"
        except Exception:
            return ""

    def _on_inclinacao_changed(self, i: int):
        """Ao editar a Inclinação da ID i: recalcula Desnível[i], NovoZ[i] e o segmento de fechamento (última ID)."""
        if getattr(self, "_updating_ui", False):
            return
        self._updating_ui = True
        try:
            row = self._rows[i]
            inc_txt = row["line_edit_inclinacao"].text()
            inc_pct = self._parse_float(inc_txt, 0.0)         # porcentagem (%)
            dist = self._parse_float(row["dist"], 0.0)

            # Desnível[i] = (Inclinação%/100)*dist
            dz_i = (inc_pct / 100.0) * dist if dist != 0.0 else 0.0
            self._set_desnivel_e_novoz(i, dz_i)

            # Recalcula a ÚLTIMA ID (fechamento)
            self._recalc_last_segment_from_novoz()
        finally:
            self._updating_ui = False

    def _set_desnivel_e_novoz(self, i: int, desnivel: float):
        """Aplica Desnível[i] e recalcula NovoZ[i]; mantém Inclinação[i] coerente."""
        row = self._rows[i]
        dist  = self._parse_float(row["dist"], 0.0)
        z0    = self._parse_float(row["z_val"], 0.0)

        # Desnível
        spin_dz = row["spin_desnivel"]
        spin_dz.blockSignals(True)
        spin_dz.setValue(desnivel)
        spin_dz.blockSignals(False)

        # NovoZ = Zorig + Desnível   (ajuste aqui se sua lógica for diferente)
        nz = z0 + desnivel
        spin_nz = row["spin_novoz"]
        spin_nz.blockSignals(True)
        spin_nz.setValue(nz)
        spin_nz.blockSignals(False)

        # Inclinação calculada = (Desnível/Dist)*100
        inc_calc = (desnivel / dist) * 100.0 if dist != 0.0 else 0.0
        le_inc = row["line_edit_inclinacao"]
        le_inc.blockSignals(True)
        le_inc.setText(self._fmt_pct(inc_calc, 3))
        le_inc.blockSignals(False)
        le_inc.setModified(False)   # ← importante

    def _recalc_last_segment_from_novoz(self):
        """Recalcula Desnível e Inclinação da ÚLTIMA ID (n-1 → 0) para fechar coerente."""
        n = len(self._rows)
        if n < 2:
            return

        i_last = n - 1
        row_last = self._rows[i_last]
        dist_last = self._parse_float(row_last["dist"], 0.0)

        nz0   = self._rows[0]["spin_novoz"].value()
        nzlast= row_last["spin_novoz"].value()
        dz_last = nz0 - nzlast

        # Desnível[last]
        sdz = row_last["spin_desnivel"]
        sdz.blockSignals(True)
        sdz.setValue(dz_last)
        sdz.blockSignals(False)

        # Inclinação[last]
        inc_last = (dz_last / dist_last) * 100.0 if dist_last != 0.0 else 0.0
        le_inc_last = row_last["line_edit_inclinacao"]
        le_inc_last.blockSignals(True)
        le_inc_last.setText(self._fmt_pct(inc_last, 3))
        le_inc_last.blockSignals(False)
        le_inc_last.setModified(False)  # ← importante

    def _parse_float(self, v, default=0.0) -> float:
        """Converte texto para float aceitando vírgula/ponto e '%' opcional."""
        try:
            s = str(v).strip().replace("%", "").replace(" ", "")
            s = s.replace(",", ".")
            return float(s)
        except Exception:
            return default

    def _fmt_pct(self, v: float, prec: int = 3) -> str:
        """Formata porcentagem com sufixo %."""
        try:
            return f"{float(v):.{prec}f}%"
        except Exception:
            return f"{v}%"

    def _on_inclinacao_edit_finished(self, idx: int, le: QLineEdit):
        """
        Nova lógica:
        - Ao editar a Inclinação do segmento i (linha i), ajusta-se NovoZ[i+1],
          mantendo NovoZ[i] fixo, de modo que:
             (NovoZ[i+1] - NovoZ[i]) / dist_i  ==  inclinação_digitada
        - Em seguida, recalcula as inclinações e desníveis (o que atualiza também a
          Inclinação da linha i+1 e quaisquer segmentos afetados).
        """
        if idx < 0 or idx >= len(self.scroll_widgets):
            return
        # Só executa se houve edição REAL do usuário
        if not le.property("user_edited"):
            return

        # Distância do segmento i (i -> i+1)
        dist = float(self.scroll_widgets[idx].get('dist', 0.0) or 0.0)
        if dist == 0.0:
            le.setProperty("user_edited", False)
            return

        # % digitado
        pct_novo = self._parse_float(le.text(), 0.0)

        # ΔZ do SEGMENTO i
        dz_seg = (pct_novo / 100.0) * dist

        n = len(self.points)
        i_next = (idx + 1) % n

        # Mantém NovoZ[i], ajusta NovoZ[i+1] = NovoZ[i] + ΔZ_seg
        novoz_i = float(self.points[idx]['NovoZ'])
        novo_novoz_next = round(novoz_i + dz_seg, 3)

        # Atualiza modelo e UI do NovoZ do PRÓXIMO vértice
        self.points[i_next]['NovoZ'] = novo_novoz_next
        le_nz_next = self.scroll_widgets[i_next]['line_edit_novoz']
        le_nz_next.blockSignals(True)
        le_nz_next.setText(f"{novo_novoz_next:.3f}")
        le_nz_next.blockSignals(False)

        # Atualiza Desnível do próximo: NovoZ[i+1] - Z[i+1]
        z_orig_next = float(self.points[i_next]['Z'])
        desnivel_next = round(novo_novoz_next - z_orig_next, 3)
        sb_next = self.scroll_widgets[i_next]['spin_box_desnivel']
        sb_next.blockSignals(True)
        sb_next.setValue(desnivel_next)
        sb_next.blockSignals(False)

        # Aplica a cor (azul >0, vermelho <0, preto =0)
        self._apply_desnivel_style(sb_next, desnivel_next)

        # Normaliza o texto digitado e limpa a flag de edição
        le.blockSignals(True)
        le.setText(f"{pct_novo:.3f}%")
        le.setProperty("user_edited", False)
        self._apply_inclinacao_style(le, pct_novo)   # <<< AQUI
        le.blockSignals(False)

        # Recalcula TODAS as inclinações/desníveis a partir dos NovoZ atualizados
        # (Isso ajusta também a Inclinação da linha i+1 e mantém tudo consistente.)
        self.recalculate_inclination_and_desnivel()

    def recalculate_inclination_and_desnivel(self):
        """
        Recalcula inclinação dos segmentos e desnível de cada ponto,
        atualizando a UI sem disparar sinais de edição.
        """
        num_points = len(self.points)
        if num_points == 0:
            return

        for i in range(num_points):
            # Segmento anterior (i-1 -> i), escrevendo na linha (i-1)
            prev_idx = num_points - 1 if i == 0 else i - 1
            z1 = float(self.points[prev_idx]['NovoZ'])
            z2 = float(self.points[i]['NovoZ'])
            comp_prev = float(self.lines[prev_idx]['Comprimento']) if self.lines else 0.0
            inc_prev = (z2 - z1) / comp_prev if comp_prev != 0.0 else 0.0

            # Linha (i-1): inclinação do segmento anterior
            le_prev = self.scroll_widgets[prev_idx]['line_edit_inclinacao']
            le_prev.blockSignals(True)
            le_prev.setText(f"{inc_prev*100:.3f}%")
            le_prev.setProperty("user_edited", False)  # <<< zera a flag após atualização programática
            self._apply_inclinacao_style(le_prev, inc_prev*100)   # <<< AQUI
            le_prev.blockSignals(False)

            # Segmento posterior (i -> i+1), escrevendo na linha i
            next_idx = 0 if i == num_points - 1 else i + 1
            z1p = float(self.points[i]['NovoZ'])
            z2p = float(self.points[next_idx]['NovoZ'])
            comp_i = float(self.lines[i]['Comprimento']) if self.lines else 0.0
            inc_post = (z2p - z1p) / comp_i if comp_i != 0.0 else 0.0

            # Linha i: inclinação do segmento posterior
            le_i = self.scroll_widgets[i]['line_edit_inclinacao']
            le_i.blockSignals(True)
            le_i.setText(f"{inc_post*100:.3f}%")
            le_i.setProperty("user_edited", False)     # <<< idem
            self._apply_inclinacao_style(le_i, inc_post*100)
            le_i.blockSignals(False)

            # Desnível do ponto i
            z_orig = float(self.points[i]['Z'])
            z_new  = float(self.points[i]['NovoZ'])
            desn = round(z_new - z_orig, 3)

            sb_dz = self.scroll_widgets[i]['spin_box_desnivel']
            sb_dz.blockSignals(True)
            sb_dz.setValue(desn)
            self._apply_desnivel_style(sb_dz, desn) # Altera as cores
            sb_dz.blockSignals(False)

    def update_desnivel(self, val, idx):
        """
        Atualiza o valor de NovoZ de um ponto a partir do desnível informado.
        Também sincroniza o campo NovoZ na interface e recalcula inclinações e desníveis.
        """

        try:
            desnivel = float(val)   # converte o valor recebido em número
        except ValueError:
            return  # se não for número válido, sai da função

        # calcula o NovoZ: Z original + desnível
        novo_z = round(self.points[idx]['Z'] + desnivel, 3)
        self.points[idx]['NovoZ'] = novo_z  # atualiza no modelo de dados

        # atualiza o QLineEdit de NovoZ na interface
        le_nz = self.scroll_widgets[idx]['line_edit_novoz']
        le_nz.blockSignals(True)  # evita disparar sinais recursivos
        le_nz.setText(f"{novo_z:.3f}")  # mostra sempre com 3 casas decimais
        le_nz.blockSignals(False)

        # recalcula todas as inclinações e desníveis para manter a consistência
        self.recalculate_inclination_and_desnivel()

    def update_novoz(self, idx):
        """
        Atualiza o valor de NovoZ de um ponto a partir do valor digitado no QLineEdit.
        Também sincroniza o dado com a lista de pontos e recalcula inclinações/desníveis.
        """

        # obtém o QLineEdit correspondente ao NovoZ do ponto
        le_nz = self.scroll_widgets[idx]['line_edit_novoz']
        # substitui vírgula por ponto para permitir entrada com vírgula decimal
        text = le_nz.text().replace(',', '.')

        try:
            # converte o texto para float e arredonda para 3 casas
            novo_z = round(float(text), 3)
        except ValueError:
            return  # se não for um número válido, não faz nada

        # atualiza o valor no modelo de dados (self.points)
        self.points[idx]['NovoZ'] = novo_z

        # atualiza o QLineEdit programaticamente (sem disparar sinais)
        le_nz.blockSignals(True)
        le_nz.setText(f"{novo_z:.3f}")  # garante formato com 3 casas decimais
        le_nz.blockSignals(False)

        # recalcula todas as inclinações e desníveis para manter consistência
        self.recalculate_inclination_and_desnivel()

    def update_layers(self):
        """
        Atualiza os valores dos campos 'NovoZ' na camada de pontos e 'Inclinação' na camada de linhas,
        sincronizando-os com os dados editados na interface.
        """
        # Obter o nome da camada de polígono selecionada
        selected_polygon_id = self.comboBoxPoligono.currentData()
        selected_layer = QgsProject.instance().mapLayer(selected_polygon_id)
        if not selected_layer:
            # self.mostrar_mensagem("Camada de polígono não encontrada.", "Erro")
            return

        feature = self._get_active_polygon_feature(selected_layer)
        poly_id = self._get_poly_id_for_naming(selected_layer, feature)

        # Definir os nomes das camadas de pontos e linhas baseados na camada de polígono
        point_layer_name = f"{selected_layer.name()}_ID{poly_id}_Vértices"
        line_layer_name  = f"{selected_layer.name()}_ID{poly_id}_Segmentos"

        # Obter as camadas pelo nome dinâmico
        point_layer = QgsProject.instance().mapLayersByName(point_layer_name)
        line_layer  = QgsProject.instance().mapLayersByName(line_layer_name)

        if not point_layer or not line_layer:
            self.mostrar_mensagem(f"Camadas '{point_layer_name}' ou '{line_layer_name}' não encontradas.", "Erro")
            return

        point_layer = point_layer[0]  # Espera-se que só haja uma camada correspondente
        line_layer = line_layer[0]

        # Atualizar NovoZ na camada de pontos
        point_layer.startEditing()
        for i, feature in enumerate(point_layer.getFeatures()):
            feature['NovoZ'] = self.points[i]['NovoZ']
            point_layer.updateFeature(feature)
        point_layer.commitChanges()

        # Atualizar Inclinação na camada de linhas
        line_layer.startEditing()

        num_points = len(self.points)

        for i, feature in enumerate(line_layer.getFeatures()):
            # Conectar o último ponto ao primeiro
            point1 = self.points[i]
            point2 = self.points[(i + 1) % num_points]  # Usa o operador % para fechar o polígono

            # Recalcular a inclinação entre os pontos
            z1 = point1['NovoZ']
            z2 = point2['NovoZ']
            comprimento = feature.geometry().length()

            # DEPOIS (guarda a fração com 6 casas; o rótulo formata)
            inclinacao = (float(z2) - float(z1)) / comprimento if comprimento != 0 else 0.0
            feature['Inclinação'] = round(inclinacao, 6)

            line_layer.updateFeature(feature)

        line_layer.commitChanges()

        self.mostrar_mensagem("Camadas de pontos e linhas atualizadas com sucesso.", "Sucesso")

    def calculate_and_create_segment_layers(self):
        """
        Atualiza as camadas de pontos e linhas existentes, cria novas camadas de pontos para cada segmento
        e atualiza a lista de segmentos na interface.
        """
        # Primeiro, atualize as camadas de pontos e linhas existentes
        self.update_layers()

        # Em seguida, crie as camadas de pontos para cada segmento
        selected_polygon_id = self.comboBoxPoligono.currentData()
        selected_layer = QgsProject.instance().mapLayer(selected_polygon_id)

        if not selected_layer:
            return

        feature = self._get_active_polygon_feature(selected_layer)
        poly_id = self._get_poly_id_for_naming(selected_layer, feature)

        # Obter a camada de pontos
        point_layer_name = f"{selected_layer.name()}_ID{poly_id}_Vértices"
        point_layer = QgsProject.instance().mapLayersByName(point_layer_name)

        if not point_layer:
            self.mostrar_mensagem(f"Camada '{point_layer_name}' não encontrada.", "Erro")
            return

        point_layer = point_layer[0]

        # Obter os pontos da camada de pontos
        points = [f for f in point_layer.getFeatures()]
        
        # Criar camadas de pontos para cada segmento
        self.create_point_layer_for_segments(points)

        # Atualizar o listWidgetSegmentos após criar as camadas de segmentos
        self.update_list_widget_segmentos()

        self.mostrar_mensagem("Camadas de segmentos criadas com sucesso.", "Sucesso")

    def create_point_layer_for_segments(self, points):
        """
        Cria camadas de pontos temporárias para cada segmento do polígono, 
        agrupando-as e adicionando ao projeto para análise individual dos segmentos.
        """
        # Obter o nome da camada de polígono selecionada
        selected_polygon_id = self.comboBoxPoligono.currentData()
        selected_layer = QgsProject.instance().mapLayer(selected_polygon_id)
        if not selected_layer:
            # self.mostrar_mensagem("Camada de polígono não encontrada.", "Erro")
            return

        # Cria ou acessa o grupo baseado no nome da camada de polígono
        root = QgsProject.instance().layerTreeRoot()
        group = root.findGroup("Segmentos")
        if not group:
            group = root.addGroup("Segmentos")
        else:
            # Limpar camadas existentes no grupo antes de adicionar novas camadas
            group.removeAllChildren()

        # Iterar sobre os segmentos dos pontos
        num_points = len(points)
        for i in range(num_points):
            # Definir o nome da camada de pontos para o segmento
            feature = self._get_active_polygon_feature(selected_layer)
            poly_id = self._get_poly_id_for_naming(selected_layer, feature)

            segment_id = f"Segmento_{i+1}_{(i+2) if i+1 < num_points else 1}"
            point_layer_name = f"{selected_layer.name()}_ID{poly_id}_{segment_id}"

            point_layer = QgsVectorLayer('Point?crs=' + selected_layer.crs().authid(), point_layer_name, 'memory')
            pr = point_layer.dataProvider()

            # Adicionar campos "ID", "X", "Y", "Z", "NovoZ"
            pr.addAttributes([QgsField("ID", QVariant.Int),
                              QgsField("X", QVariant.Double),
                              QgsField("Y", QVariant.Double),
                              QgsField("Z", QVariant.Double),
                              QgsField("NovoZ", QVariant.Double)])
            point_layer.updateFields()

            # Adicionar os dois pontos do segmento à nova camada de pontos
            point1 = points[i]
            point2 = points[(i + 1) % num_points]  # Conecta o último ponto ao primeiro

            for j, point in enumerate([point1, point2]):
                feature = QgsFeature(point_layer.fields())
                x = point.geometry().asPoint().x()
                y = point.geometry().asPoint().y()

                # Definir valores para os atributos
                feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))
                feature.setAttribute("ID", j + 1)
                feature.setAttribute("X", round(x, 3))
                feature.setAttribute("Y", round(y, 3))
                feature.setAttribute("Z", point['Z'])
                feature.setAttribute("NovoZ", point['NovoZ'])

                pr.addFeature(feature)

            # Adicionar a nova camada de pontos ao projeto
            QgsProject.instance().addMapLayer(point_layer, False)  # Adiciona a camada sem mostrar na árvore de camadas
            group.insertLayer(0, point_layer)  # Adiciona a camada ao grupo
            self.mostrar_mensagem(f"Camada de pontos '{point_layer_name}' criada com sucesso.", "Sucesso")

    def calculate_distance_and_update_spinbox(self):
        """
        Calcula a distância entre dois pontos na camada selecionada e ajusta
        o valor máximo do spinbox de equidistância conforme o resultado.
        """
        # Obter a camada selecionada no listWidgetSegmentos
        selected_item = self.listWidgetSegmentos.currentItem()
        if not selected_item:
            self.mostrar_mensagem("Nenhuma camada selecionada no listWidgetSegmentos.", "Erro")
            return

        # Obter a camada de pontos a partir do nome da camada
        layer_name = selected_item.text()
        layer = QgsProject.instance().mapLayersByName(layer_name)
        if not layer:
            self.mostrar_mensagem(f"Camada '{layer_name}' não encontrada.", "Erro")
            return
        layer = layer[0]

        # Verificar se a camada possui exatamente dois pontos
        points = [f for f in layer.getFeatures()]
        if len(points) != 2:
            self.mostrar_mensagem("A camada selecionada deve conter exatamente dois pontos.", "Erro")
            return

        # Calcular a distância entre os dois pontos
        point1 = points[0].geometry().asPoint()
        point2 = points[1].geometry().asPoint()
        distance = point1.distance(point2)

        # Atualizar o valor máximo do doubleSpinBoxEquidistante
        self.doubleSpinBoxEquidistante.setMinimum(0.1)
        self.doubleSpinBoxEquidistante.setMaximum(distance)
        self.mostrar_mensagem(f"Distância entre os pontos: {distance:.3f}", "Sucesso")

    def calculate_intermediate_points(self):
        """
        Gera pontos intermediários equidistantes entre dois pontos selecionados,
        cria uma nova camada de estaqueamento e aplica simbologia e rótulos.
        """
        # Armazena o índice atual da seleção e define o próximo índice (se houver)
        current_row = self.listWidgetSegmentos.currentRow()
        if current_row != -1:
            self._next_segment_index = current_row + 1
        else:
            self._next_segment_index = None

        # Obter a camada selecionada no listWidgetSegmentos
        selected_item = self.listWidgetSegmentos.currentItem()
        if not selected_item:
            self.mostrar_mensagem("Nenhuma camada selecionada no listWidgetSegmentos.", "Erro")
            return

        # Obter a camada de pontos a partir do nome da camada
        layer_name = selected_item.text()
        layer = QgsProject.instance().mapLayersByName(layer_name)
        if not layer:
            self.mostrar_mensagem(f"Camada '{layer_name}' não encontrada.", "Erro")
            return
        layer = layer[0]

        # Verificar se a camada possui exatamente dois pontos
        points = [f for f in layer.getFeatures()]
        if len(points) != 2:
            self.mostrar_mensagem("A camada selecionada deve conter exatamente dois pontos.", "Erro")
            return

        # Obter o valor selecionado no doubleSpinBoxEquidistante
        equidistance = self.doubleSpinBoxEquidistante.value()

        # Verificar se o valor da equidistância é maior que zero
        if equidistance <= 0:
            self.mostrar_mensagem("O valor da equidistância deve ser maior que zero.", "Erro")
            return

        # Calcular a distância entre os dois pontos
        point1 = points[0].geometry().asPoint()
        point2 = points[1].geometry().asPoint()
        total_distance = point1.distance(point2)

        # Verificar se a distância total entre os pontos é maior que zero
        if total_distance == 0:
            self.mostrar_mensagem("Os dois pontos são coincidentes. A distância entre eles é zero.", "Erro")
            return

        # Calcular quantos pontos intermediários podem ser gerados
        num_points = int(total_distance // equidistance)

        if num_points == 0:
            self.mostrar_mensagem("Nenhum ponto intermediário pode ser criado com o valor atual.", "Erro")
            return

        # Obter o raster selecionado para capturar o valor de Z
        selected_raster_id = self.comboBoxRaster.currentData()
        raster_layer = QgsProject.instance().mapLayer(selected_raster_id)
        if not raster_layer or not isinstance(raster_layer, QgsRasterLayer):
            self.mostrar_mensagem("Camada raster inválida ou não selecionada.", "Erro")
            return

        # Obter o NovoZ dos dois pontos
        novo_z1 = points[0]['NovoZ']
        novo_z2 = points[1]['NovoZ']

        # Calcular a inclinação entre os dois pontos
        inclination = (novo_z2 - novo_z1) / total_distance if total_distance != 0 else 0

        # Criar ou acessar o grupo "Estaqueamento"
        root = QgsProject.instance().layerTreeRoot()
        group = root.findGroup("Estaqueamentos")
        if not group:
            group = root.addGroup("Estaqueamentos")

        # (1) Extrai base e segmento de forma robusta
        try:
            base_name, segment_part = layer_name.rsplit('_Segmento_', 1)
        except ValueError:
            self.mostrar_mensagem("Nome da camada fora do padrão: '..._Segmento_i_j'.", "Erro")
            return

        new_layer_name = f"{base_name}_Estacas_{segment_part}"

        # (2) Se já existir uma camada com esse nome, remover antes de recriar
        exist = QgsProject.instance().mapLayersByName(new_layer_name)
        if exist:
            QgsProject.instance().removeMapLayer(exist[0].id())

        # (3) Agora sim, cria a nova camada de pontos
        crs = layer.crs()
        new_point_layer = QgsVectorLayer(f"Point?crs={crs.authid()}", new_layer_name, "memory")
        pr = new_point_layer.dataProvider()
        pr.addAttributes([QgsField("ID", QVariant.Int),
                          QgsField("X", QVariant.Double),
                          QgsField("Y", QVariant.Double),
                          QgsField("Z", QVariant.Double),
                          QgsField("NovoZ", QVariant.Double),
                          QgsField("Desnível", QVariant.Double),
                          QgsField("Acumula_dist", QVariant.Double)])  # Adiciona o campo Acumula_dist
        new_point_layer.updateFields()

        # Inicia a distância acumulada
        acumula_dist = 0

        # Adicionar o ponto inicial (ponto 1)
        feature1 = QgsFeature(new_point_layer.fields())
        feature1.setGeometry(QgsGeometry.fromPointXY(point1))
        feature1.setAttribute("ID", 1)
        feature1.setAttribute("X", round(point1.x(), 3))
        feature1.setAttribute("Y", round(point1.y(), 3))
        z_value1 = self.get_z_value(point1.x(), point1.y())  # Obter o Z da camada raster
        feature1.setAttribute("Z", z_value1 if z_value1 is not None else 0.0)
        feature1.setAttribute("NovoZ", novo_z1)
        desnivel1 = novo_z1 - z_value1 if z_value1 is not None else 0.0
        feature1.setAttribute("Desnível", round(desnivel1, 3))
        feature1.setAttribute("Acumula_dist", acumula_dist)  # Define a distância acumulada como 0 para o primeiro ponto
        pr.addFeature(feature1)

        # Calcular e adicionar os pontos intermediários
        for i in range(1, num_points + 1):
            t = i * equidistance / total_distance
            intermediate_x = (1 - t) * point1.x() + t * point2.x()
            intermediate_y = (1 - t) * point1.y() + t * point2.y()

            # Calcular o NovoZ para os pontos intermediários usando a inclinação
            novo_z_intermediate = novo_z1 + inclination * (i * equidistance)

            z_value_intermediate = self.get_z_value(intermediate_x, intermediate_y)  # Obter o Z da camada raster
            desnivel_intermediate = novo_z_intermediate - z_value_intermediate if z_value_intermediate is not None else 0.0

            # Atualizar a distância acumulada
            acumula_dist += equidistance

            # Criar a feição do ponto intermediário
            feature_intermediate = QgsFeature(new_point_layer.fields())
            feature_intermediate.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(intermediate_x, intermediate_y)))
            feature_intermediate.setAttribute("ID", i + 1)
            feature_intermediate.setAttribute("X", round(intermediate_x, 3))
            feature_intermediate.setAttribute("Y", round(intermediate_y, 3))
            feature_intermediate.setAttribute("Z", z_value_intermediate if z_value_intermediate is not None else 0.0)
            feature_intermediate.setAttribute("NovoZ", round(novo_z_intermediate, 3))
            feature_intermediate.setAttribute("Desnível", round(desnivel_intermediate, 3))
            feature_intermediate.setAttribute("Acumula_dist", acumula_dist)  # Adiciona a distância acumulada

            pr.addFeature(feature_intermediate)

        # Adicionar o ponto final (ponto 2)
        acumula_dist = total_distance  # Atualiza corretamente a distância acumulada no ponto final
        feature2 = QgsFeature(new_point_layer.fields())
        feature2.setGeometry(QgsGeometry.fromPointXY(point2))
        feature2.setAttribute("ID", num_points + 2)
        feature2.setAttribute("X", round(point2.x(), 3))
        feature2.setAttribute("Y", round(point2.y(), 3))
        z_value2 = self.get_z_value(point2.x(), point2.y())  # Obter o Z da camada raster
        feature2.setAttribute("Z", z_value2 if z_value2 is not None else 0.0)
        feature2.setAttribute("NovoZ", novo_z2)
        desnivel2 = novo_z2 - z_value2 if z_value2 is not None else 0.0
        feature2.setAttribute("Desnível", round(desnivel2, 3))
        feature2.setAttribute("Acumula_dist", acumula_dist)  # Adiciona a distância acumulada correta para o último ponto
        pr.addFeature(feature2)

        # Adicionar a nova camada de pontos ao grupo "Estaqueamento"
        QgsProject.instance().addMapLayer(new_point_layer, False)  # Adiciona a camada sem mostrar na árvore de camadas
        group.insertLayer(0, new_point_layer)  # Adiciona a camada ao grupo

        # Colapsa (fecha) a camada recém-criada no painel de camadas
        layer_node = group.findLayer(new_point_layer.id())
        if layer_node is not None:
            layer_node.setExpanded(False)

        self.mostrar_mensagem(f"Camada '{new_layer_name}' criada e adicionada ao grupo 'Estaqueamentos'.", "Sucesso")

        # Aplicar os rótulos à nova camada
        self.set_label_for_layer(new_point_layer, "Desnível")

        # Aplica a simbologia de desnível imediatamente após criar a camada
        self.apply_desnivel_symbology(new_point_layer)

        # Atualizar o listWidgetEstacas logo após a criação da camada
        self.update_list_widget_estaqueamentos()

    def set_label_for_layer(self, layer, field_name):
        """
        Configura o rótulo para uma camada no QGIS, com base em um campo específico e formatações adicionais.
        """
        label_settings = QgsPalLayerSettings()
        label_settings.drawBackground = True  # Ativa o fundo do rótulo
        label_settings.fieldName = field_name  # Define o campo a ser rotulado

        # Configura a formatação do texto
        text_format = label_settings.format()
        font = text_format.font()
        font.setItalic(True)
        font.setBold(True)
        text_format.setFont(font)

        # Configura a cor do rótulo com base em uma expressão condicional
        color_expression = """CASE
                                WHEN "Desnível" < 0 THEN '255,0,0'  -- Vermelho
                                WHEN "Desnível" > 0 THEN '0,0,255'  -- Azul
                                ELSE '0,0,0'  -- Preto
                              END"""

        properties = label_settings.dataDefinedProperties()
        properties.setProperty(QgsPalLayerSettings.Color, QgsProperty.fromExpression(color_expression))

        # Defina o tamanho da fonte usando propriedades definidas por dados
        properties.setProperty(QgsPalLayerSettings.Size, QgsProperty.fromValue(8))
        label_settings.setDataDefinedProperties(properties)

        # Configura o fundo branco para os rótulos
        background_color = text_format.background()
        background_color.setEnabled(True)
        background_color.setFillColor(QColor(255, 255, 255))
        text_format.setBackground(background_color)

        label_settings.setFormat(text_format)

        # Ativa o rótulo para a camada
        layer.setLabelsEnabled(True)
        layer.setLabeling(QgsVectorLayerSimpleLabeling(label_settings))
        layer.triggerRepaint()

    def apply_desnivel_symbology(self, layer):
        """
        Colore uma camada de estaqueamento conforme o valor do campo 'Desnível',
        e aplica borda da mesma cor:
          > 0  → marcador azul com borda azul
          < 0  → marcador vermelho com borda vermelha
          = 0  → marcador cinza com borda cinza
        """

        # garante que seja camada de pontos
        if not layer or layer.geometryType() != QgsWkbTypes.PointGeometry:
            return

        # cria símbolos com cor de fill e contorno (outline) iguais
        sym_blue = QgsMarkerSymbol.createSimple({'color': 'blue', 'outline_color': 'blue', 'outline_width': '0.1','size': '2'})
        sym_red = QgsMarkerSymbol.createSimple({'color': 'red', 'outline_color': 'red', 'outline_width': '0.1', 'size': '2'})
        sym_gray = QgsMarkerSymbol.createSimple({'color': 'gray', 'outline_color': 'gray', 'outline_width': '0.1', 'size': '2'})

        # nó raiz para o renderer baseado em regras
        root = QgsRuleBasedRenderer.Rule(None)

        # regra Aterro > 0
        rule_blue = QgsRuleBasedRenderer.Rule(sym_blue)
        rule_blue.setFilterExpression('"Desnível" > 0')
        rule_blue.setLabel('Aterro > 0')
        root.appendChild(rule_blue)

        # regra Corte < 0
        rule_red = QgsRuleBasedRenderer.Rule(sym_red)
        rule_red.setFilterExpression('"Desnível" < 0')
        rule_red.setLabel('Corte < 0')
        root.appendChild(rule_red)

        # regra Sem movimento = 0
        rule_gray = QgsRuleBasedRenderer.Rule(sym_gray)
        rule_gray.setFilterExpression('"Desnível" = 0')
        rule_gray.setLabel('Nada = 0')
        root.appendChild(rule_gray)

        # aplica o renderer e repinta
        layer.setRenderer(QgsRuleBasedRenderer(root))
        layer.triggerRepaint()

    def get_z_value(self, x, y):
        """
        Obtém o valor de elevação (Z) do raster selecionado nas coordenadas (x, y).
        Exibe mensagem de erro caso não seja possível obter o valor.
        """
        selected_raster_id = self.comboBoxRaster.currentData()
        raster_layer = QgsProject.instance().mapLayer(selected_raster_id)
        
        if not raster_layer or not isinstance(raster_layer, QgsRasterLayer):
            self.mostrar_mensagem("Camada raster inválida ou não selecionada.", "Erro")
            return None
        
        identify_result = raster_layer.dataProvider().identify(QgsPointXY(x, y), QgsRaster.IdentifyFormatValue)
        
        if identify_result.isValid():
            results = identify_result.results()
            if results:
                band_key = list(results.keys())[0]
                z_value = results.get(band_key)
                if z_value is not None:
                    return round(float(z_value), 3)

        # Se não conseguir identificar o valor Z, retorna None
        self.mostrar_mensagem(f"Não foi possível identificar o valor Z nas coordenadas ({x}, {y}).", "Erro")
        return None

    def find_matching_talude_layers(self, estacas_layer_name, talude_layers_dict):
        """Encontra as camadas de Talude correspondentes com base no nome da camada de Estacas."""
        matching_layers = []
        for talude_layer_name in talude_layers_dict.keys():
            # Verificar se o nome da camada de Talude contém o nome da camada de Estacas
            if estacas_layer_name in talude_layer_name:
                matching_layers.append(talude_layer_name)
        return matching_layers

    def create_connections_between_layers(self, estacas_points, talude_points, crs, base_layer_name, line_layer, feature_id_start):
        """Cria conexões entre os pontos das camadas de Estacas e Talude correspondentes e adiciona à camada de linhas única."""
        if not estacas_points or not talude_points:
            self.mostrar_mensagem(f"Não há pontos suficientes em Estacas ou Talude para criar conexões para '{base_layer_name}'.", "Erro")
            return feature_id_start

        pr_line = line_layer.dataProvider()
        feature_id = feature_id_start  # Atualiza o ID com base no valor passado

        line_features = []

        # Ordenar os pontos se necessário
        estacas_points_sorted = sorted(estacas_points, key=lambda x: x[0]['ID'])
        talude_points_sorted = sorted(talude_points, key=lambda x: x[0]['ID'])

        # Coletar os pontos para formar o contorno externo
        contour_points = []

        # Reprojetar e coletar pontos de Estacas
        for estaca_feature, estaca_geom, estaca_crs in estacas_points_sorted:
            if estaca_crs != crs:
                transform = QgsCoordinateTransform(estaca_crs, crs, QgsProject.instance())
                estaca_geom = estaca_geom.clone()
                estaca_geom.transform(transform)
            estaca_point = estaca_geom.asPoint()
            contour_points.append(estaca_point)

        # Reprojetar e coletar pontos de Talude em ordem reversa
        for talude_feature, talude_geom, talude_crs in reversed(talude_points_sorted):
            if talude_crs != crs:
                transform = QgsCoordinateTransform(talude_crs, crs, QgsProject.instance())
                talude_geom = talude_geom.clone()
                talude_geom.transform(transform)
            talude_point = talude_geom.asPoint()
            contour_points.append(talude_point)

        # Fechar o contorno conectando o último ponto ao primeiro, se necessário
        if contour_points[0] != contour_points[-1]:
            contour_points.append(contour_points[0])

        # Criar a geometria da linha do contorno
        contour_geom = QgsGeometry.fromPolylineXY(contour_points)

        # Criar a feição de linha para o contorno
        contour_feature = QgsFeature(line_layer.fields())
        contour_feature.setGeometry(contour_geom)
        contour_feature.setAttribute('ID', feature_id)
        contour_feature.setAttribute('Tipo', 'Contorno')
        contour_feature.setAttribute('Nome', base_layer_name)
        line_features.append(contour_feature)
        feature_id += 1

        # Criar as linhas inclinadas conectando os pontos correspondentes de Estacas e Talude
        min_length = min(len(estacas_points_sorted), len(talude_points_sorted))
        for i in range(min_length):
            estaca_feature, estaca_geom, _ = estacas_points_sorted[i]
            talude_feature, talude_geom, _ = talude_points_sorted[i]

            estaca_point = estaca_geom.asPoint()
            talude_point = talude_geom.asPoint()

            # Criar a geometria da linha inclinada
            inclined_line_geom = QgsGeometry.fromPolylineXY([estaca_point, talude_point])

            # Criar a feição de linha inclinada
            inclined_line_feature = QgsFeature(line_layer.fields())
            inclined_line_feature.setGeometry(inclined_line_geom)
            inclined_line_feature.setAttribute('ID', int(feature_id))
            inclined_line_feature.setAttribute('Tipo', 'Ligação')
            inclined_line_feature.setAttribute('Nome', base_layer_name)
            line_features.append(inclined_line_feature)
            feature_id += 1

            # só cria hatchs se não for o último segmento:
            if i < min_length - 1:
                prox_estaca_point = estacas_points_sorted[i + 1][1].asPoint()
                h_feats, feature_id = self._create_alignment_hatches(
                    estaca_point, prox_estaca_point, talude_point,
                    line_layer.fields(), base_layer_name, feature_id)
                line_features.extend(h_feats)

        # Adicionar as feições à camada de linhas única
        pr_line.addFeatures(line_features)

        # Retornar o próximo ID disponível
        return feature_id

    def _create_alignment_hatches(self, p_est: QgsPointXY, p_next_est: QgsPointXY, p_tal: QgsPointXY, layer_fields, nome, first_id: int) -> tuple:
        """
        Cria 3 hatchs no alinhamento Estaca-Estaca:

        * bases em ¼, ½, ¾ do alinhamento;
        * direção = vetor Estaca→Talude (colinear);
        * comprimentos = 0.4, 0.6, 0.4 do comprimento da ligação.

        Retorna (lista_de_features, próximo_ID).
        """
        # comprimento do alinhamento (entre estacas)
        align_len = p_est.distance(p_next_est)
        if align_len == 0:
            return [], first_id

        # ponto inicial (estaca atual) + vetor unitário do alinhamento
        ax, ay = (p_next_est.x() - p_est.x()) / align_len, (p_next_est.y() - p_est.y()) / align_len

        # vetor para o Talude (direção do hatch)
        lig_len = p_est.distance(p_tal) or 1.0
        vx, vy  = (p_tal.x() - p_est.x()) / lig_len, (p_tal.y() - p_est.y()) / lig_len

        # frações da posição e frações de comprimento
        pos_fractions   = (0.25, 0.50, 0.75)
        size_fractions  = (0.40, 0.60, 0.40)   # hatch do meio maior

        feats, fid = [], first_id
        for t, s in zip(pos_fractions, size_fractions):
            # base do hatch
            bx = p_est.x() + ax * align_len * t
            by = p_est.y() + ay * align_len * t
            base_pt = QgsPointXY(bx, by)

            # ponto final (para dentro do platô)
            end_pt = QgsPointXY(bx + vx * lig_len * s, by + vy * lig_len * s)

            f = QgsFeature(layer_fields)
            f.setGeometry(QgsGeometry.fromPolylineXY([base_pt, end_pt]))
            f.setAttributes([fid, "Hatch", nome])
            feats.append(f)
            fid += 1

        return feats, fid

    def process_matching_layers(self, estacas_layer, talude_layers, crs, line_layer, pr_line, feature_id):
        """Processa as camadas de Estacas e Talude correspondentes e cria as conexões."""
        estacas_points = []
        # Coletar os pontos da camada de Estacas
        for feature in estacas_layer.getFeatures():
            geom = feature.geometry()
            if geom is None or geom.isEmpty():
                continue
            if geom.type() != QgsWkbTypes.PointGeometry:
                self.mostrar_mensagem(f"A camada '{estacas_layer.name()}' não é uma camada de pontos.", "Erro")
                continue
            estacas_points.append((feature, geom, estacas_layer.crs()))

        # Coletar os pontos das camadas de Talude correspondentes
        talude_points = []
        for talude_layer in talude_layers:
            for feature in talude_layer.getFeatures():
                geom = feature.geometry()
                if geom is None or geom.isEmpty():
                    continue
                if geom.type() != QgsWkbTypes.PointGeometry:
                    self.mostrar_mensagem(f"A camada '{talude_layer.name()}' não é uma camada de pontos.", "Erro")
                    continue
                talude_points.append((feature, geom, talude_layer.crs()))

        # Criar as conexões e adicionar as feições à camada de linhas
        feature_id = self.create_connections_between_layers(estacas_points, talude_points, crs, estacas_layer.name(), line_layer, pr_line, feature_id)

        return feature_id

    def check_layers_in_group_and_update(self, removed_layers_ids):
        """
        Verifica e atualiza a interface e os grupos de camadas após remoções,
        garantindo consistência visual e funcional no projeto.
        """
        # Obter o nome da camada de polígono selecionada
        selected_polygon_id = self.comboBoxPoligono.currentData()
        selected_layer = QgsProject.instance().mapLayer(selected_polygon_id)

        if not selected_layer:
            # self.mostrar_mensagem("Camada de polígono não encontrada.", "Erro")
            self.clear_all_list_widgets()
            return

        # Verifica os grupos e atualiza os listWidgets
        self.check_group_and_update("Segmentos", self.listWidgetSegmentos, removed_layers_ids)
        self.check_group_and_update("Estaqueamentos", self.listWidgetEstacas, removed_layers_ids)
        self.check_group_and_update("Taludes", self.listWidgetTalude, removed_layers_ids)

    def check_group_and_update(self, group_name, list_widget, removed_layers_ids):
        """
        Verifica e atualiza a interface e os grupos de camadas após remoções,
        garantindo consistência visual e funcional no projeto.
        """
        root = QgsProject.instance().layerTreeRoot()
        group = root.findGroup(group_name)

        if not group:
            list_widget.clear()
            self._ensure_header(list_widget, group_name.capitalize())
            return

    def clear_all_list_widgets(self):
        """
        Limpa todos os listWidgets da interface, removendo itens exibidos nas listas.
        """
        self.listWidgetSegmentos.clear()
        self.listWidgetEstacas.clear()
        self.listWidgetTalude.clear()

        self._ensure_header(self.listWidgetSegmentos, "Segmentos")
        self._ensure_header(self.listWidgetEstacas,  "Estaqueamentos")
        self._ensure_header(self.listWidgetTalude,    "Taludes")

    def update_list_widget_talude(self):
        """
        Atualiza o listWidget de taludes, exibindo as camadas relacionadas disponíveis no projeto.
        """
        self.update_list_widget(self.listWidgetTalude, "Taludes", "Taludes")

        # Chama a função para atualizar o estado do pushButtonJuntar
        self.update_pushButtonJuntar_state()

    def update_list_widget_estaqueamentos(self):
        """
        Atualiza o listWidget de estaqueamentos, listando as camadas criadas e disponíveis no projeto.
        """
        self.update_list_widget(self.listWidgetEstacas, "Estaqueamentos", "Estaqueamentos")

        # Chama a função para atualizar o estado dos botões de rádio
        self.update_radio_buttons_state()

        # Chama a função para atualizar o estado do pushButtonJuntar
        self.update_pushButtonJuntar_state()

    def update_list_widget_segmentos(self):
        """
        Atualiza o listWidget de segmentos, listando as camadas de segmentos presentes no projeto.
        """
        # Sempre cria o cabeçalho – mesmo que não haja polígono/layer selecionado
        self.update_list_widget(self.listWidgetSegmentos, "Segmentos", "Segmentos")

    def _ensure_header(self, list_widget: QListWidget, header_text: str):
        """
        Garante que o list_widget tenha um cabeçalho não-selecionável com o texto dado.
        Se já existir, não cria outro.
        """
        if list_widget.count() and list_widget.item(0).flags() == Qt.NoItemFlags:
            # já há um cabeçalho
            return

        header_lbl = QLabel(header_text)
        header_lbl.setAlignment(Qt.AlignCenter)
        header_lbl.setFont(QFont("Arial", 8, QFont.Bold))
        header_lbl.setStyleSheet("""
            QLabel {
                background-color: #C0C0C0;
                color: #000000;
                padding: 1px;
                border: 1px solid #A0A0A0;
                border-left-color: #E0E0E0;
                border-top-color:  #E0E0E0;
                border-right-color:#808080;
                border-bottom-color:#808080;
            }
        """)

        header_item = QListWidgetItem(list_widget)
        header_item.setFlags(Qt.NoItemFlags)            # não selecionável
        list_widget.addItem(header_item)
        list_widget.setItemWidget(header_item, header_lbl)
        header_item.setSizeHint(header_lbl.sizeHint())

    def update_list_widget(self, list_widget, group_name, header_text):
        """
        Atualiza o listWidget informado com as camadas pertencentes ao grupo especificado,
        exibindo um cabeçalho personalizado conforme o parâmetro.
        """
        # Zera o conteúdo
        list_widget.clear()

        # Sempre garante o cabeçalho
        self._ensure_header(list_widget, header_text)

        # Agora verifica (ou não) se há um grupo no projeto
        root  = QgsProject.instance().layerTreeRoot()
        group = root.findGroup(group_name)
        if not group:
            return     # cabeçalho permanece mesmo que o grupo não exista

        # adiciona itens das camadas se o grupo existir
        for layer in group.findLayers():
            layer_obj = layer.layer()
            if isinstance(layer_obj, QgsVectorLayer) and layer_obj.geometryType() == QgsWkbTypes.PointGeometry:
                item = QListWidgetItem(layer_obj.name())
                item.setData(Qt.UserRole, layer_obj.id())
                item.setToolTip(layer_obj.name())
                list_widget.addItem(item)

        # Se estivermos atualizando o listWidgetSegmentos e houver uma seleção "guardada", define a nova seleção
        if list_widget == self.listWidgetSegmentos and hasattr(self, "_next_segment_index") and self._next_segment_index is not None:
            total = list_widget.count()
            # Se houver mais itens do que o índice desejado, seleciona-o (caso contrário, limpa a seleção)
            if self._next_segment_index < total:
                list_widget.setCurrentRow(self._next_segment_index)
                # self._log_message(f"Seleção movida para o item {self._next_segment_index}.", Qgis.Info)
            else:
                list_widget.setCurrentRow(-1)
                # self._log_message("Não há próximo item: seleção removida.", Qgis.Info)
            self._next_segment_index = None

        elif list_widget == self.listWidgetEstacas and hasattr(self, "_next_estaca_index") and self._next_estaca_index is not None:
            total = list_widget.count()
            if self._next_estaca_index < total:
                list_widget.setCurrentRow(self._next_estaca_index)
                # self._log_message(f"Seleção movida para o item {self._next_estaca_index} em Estaqueamentos.", Qgis.Info)
            else:
                list_widget.setCurrentRow(-1)
                # self._log_message("Não há próximo item em Estaqueamentos: seleção removida.", Qgis.Info)
            self._next_estaca_index = None

    def check_estaqueamento_button_state(self):
        """Verifica se o botão Estaqueamento deve ser ativado."""
        # Verifica se há um item selecionado no listWidgetSegmentos e se o valor do doubleSpinBoxEquidistante é maior que 0
        has_selection = self.listWidgetSegmentos.selectedItems()
        equidistance_value = self.doubleSpinBoxEquidistante.value()

        if has_selection and equidistance_value > 0:
            self.pushButtonEstaqueamento.setEnabled(True)
        else:
            self.pushButtonEstaqueamento.setEnabled(False)

    def update_doubleSpinBoxEquidistante_state(self):
        """
        Ativa ou desativa o doubleSpinBoxEquidistante com base na seleção no listWidgetSegmentos
        e ajusta o valor máximo do doubleSpinBoxEquidistante para ser o comprimento da linha selecionada.
        """
        # Verificar se há itens no listWidgetSegmentos
        if self.listWidgetSegmentos.count() == 0:
            self.doubleSpinBoxEquidistante.setEnabled(False)  # Desativa o doubleSpinBoxEquidistante se não houver itens
            self.doubleSpinBoxEquidistante.setToolTip("Nenhuma camada de segmento disponível.")
            return

        selected_item = self.listWidgetSegmentos.currentItem()

        # Verificar se há uma camada selecionada no listWidgetSegmentos
        if not selected_item:
            self.doubleSpinBoxEquidistante.setEnabled(False)  # Desativa o doubleSpinBoxEquidistante se não houver seleção
            self.doubleSpinBoxEquidistante.setToolTip("Selecione uma camada de segmento primeiro.")
            return

        # Obter a camada de pontos a partir do nome da camada
        layer_name = selected_item.text()
        layer = QgsProject.instance().mapLayersByName(layer_name)
        if not layer:
            self.mostrar_mensagem(f"Camada '{layer_name}' não encontrada.", "Erro")
            return
        layer = layer[0]

        # Verificar se a camada possui exatamente dois pontos
        points = [f for f in layer.getFeatures()]
        if len(points) != 2:
            self.mostrar_mensagem("A camada selecionada deve conter exatamente dois pontos.", "Erro")
            return

        # Calcular a distância entre os dois pontos
        point1 = points[0].geometry().asPoint()
        point2 = points[1].geometry().asPoint()
        total_distance = point1.distance(point2)

        # Ativar o doubleSpinBoxEquidistante e definir o valor máximo
        self.doubleSpinBoxEquidistante.setEnabled(True)
        self.doubleSpinBoxEquidistante.setMaximum(total_distance)
        self.doubleSpinBoxEquidistante.setToolTip(f"Distância total: {total_distance:.2f} m. Selecione uma equidistância menor ou igual.")

        # Atualizar o valor do toolTip para mostrar a quantidade de pontos que será criada
        self.calculate_and_update_tooltip()

    def calculate_and_update_tooltip(self):
        """
        Calcula a quantidade de pontos que será criada com base no valor da equidistância
        e na linha selecionada no listWidgetSegmentos, e atualiza o toolTip.
        """
        selected_item = self.listWidgetSegmentos.currentItem()

        # Verificar se há uma camada selecionada no listWidgetSegmentos
        if not selected_item:
            self.doubleSpinBoxEquidistante.setToolTip("Nenhuma camada de segmento selecionada.")  # Atualizar o toolTip
            return

        # Obter a camada de pontos a partir do nome da camada
        layer_name = selected_item.text()
        layer = QgsProject.instance().mapLayersByName(layer_name)
        if not layer:
            self.mostrar_mensagem(f"Camada '{layer_name}' não encontrada.", "Erro")
            return
        layer = layer[0]

        # Verificar se a camada possui exatamente dois pontos
        points = [f for f in layer.getFeatures()]
        if len(points) != 2:
            self.mostrar_mensagem("A camada selecionada deve conter exatamente dois pontos.", "Erro")
            return

        # Calcular a distância entre os dois pontos
        point1 = points[0].geometry().asPoint()
        point2 = points[1].geometry().asPoint()
        total_distance = point1.distance(point2)

        # Obter o valor de equidistância
        equidistance = self.doubleSpinBoxEquidistante.value()

        # Verificar se o valor da equidistância é válido
        if equidistance > 0 and total_distance > 0:
            # Calcular a quantidade de pontos
            num_points = int(total_distance // equidistance)
            tooltip_text = f"Distância total: {total_distance:.2f} m | Pontos a criar: {num_points}"
        else:
            tooltip_text = "Equidistância inválida ou distância zero."

        # Atualizar o toolTip do doubleSpinBox
        self.doubleSpinBoxEquidistante.setToolTip(tooltip_text)

    def update_pushButtonTalude_state(self):
        """
        Ativa ou desativa o pushButtonTalude com base na seleção no listWidgetEstacas.
        """
        selected_item = self.listWidgetEstacas.currentItem()

        # Verificar se há uma camada selecionada no listWidgetEstacas
        if not selected_item:
            self.pushButtonTalude.setEnabled(False)  # Desativa o botão se nenhuma camada estiver selecionada
            return

        # Se houver uma camada selecionada, ativa o botão
        self.pushButtonTalude.setEnabled(True)

    def update_pushButtonVertices_state(self):
        """
        Ativa ou desativa o pushButtonVertices com base nas seguintes condições:
        - Caso não haja camada de polígono no comboBoxPoligono.
        - Caso não haja camada no comboBoxRaster.
        - Caso as camadas de polígono e raster tenham georreferências diferentes.
        - Caso alguma das camadas esteja em coordenadas geográficas.
        - Se a camada de polígono tenha mais de uma feição e não haja uma única selecionada.
        - Se alguma das camadas não tiver projeção.
        """
        selected_polygon_index = self.comboBoxPoligono.currentIndex()
        selected_raster_index = self.comboBoxRaster.currentIndex()

        # Verifica se há uma camada de polígono e raster selecionada
        if selected_polygon_index == -1 or selected_raster_index == -1:
            self.pushButtonVertices.setEnabled(False)
            return

        # Obtém a camada de polígono e raster selecionada
        selected_polygon_id = self.comboBoxPoligono.currentData()
        selected_raster_id = self.comboBoxRaster.currentData()
        polygon_layer = QgsProject.instance().mapLayer(selected_polygon_id)
        raster_layer = QgsProject.instance().mapLayer(selected_raster_id)

        # Verifica se ambas as camadas são válidas
        if not polygon_layer or not raster_layer:
            self.pushButtonVertices.setEnabled(False)
            return

        # Verifica se as camadas têm projeção
        if not polygon_layer.crs().isValid() or not raster_layer.crs().isValid():
            if self.isVisible():
                self.mostrar_mensagem("Uma ou ambas as camadas não têm projeção definida.", "Aviso")
            self.pushButtonVertices.setEnabled(False)
            return

        # Verifica se as camadas têm sistemas de coordenadas diferentes
        if polygon_layer.crs() != raster_layer.crs():
            if self.isVisible() and selected_polygon_index != -1 and selected_raster_index != -1:
                self.mostrar_mensagem("As camadas de polígono e raster têm georreferências diferentes.", "Aviso")
            self.pushButtonVertices.setEnabled(False)
            return

        # Verifica se alguma das camadas está em coordenadas geográficas (ex.: EPSG:4326)
        if polygon_layer.crs().isGeographic() or raster_layer.crs().isGeographic():
            if self.isVisible():
                self.mostrar_mensagem("Uma ou ambas as camadas estão em coordenadas geográficas.", "Aviso")
            self.pushButtonVertices.setEnabled(False)
            return

        # Verifica se a camada de polígono tem mais de uma feição e se não há uma única selecionada
        if polygon_layer.featureCount() > 1 and polygon_layer.selectedFeatureCount() != 1:
            if self.isVisible():
                self.mostrar_mensagem("A camada de polígono tem mais de uma feição e nenhuma feição única está selecionada.", "Aviso")
            self.pushButtonVertices.setEnabled(False)
            return

        # Se todas as condições forem atendidas, ativa o botão
        self.pushButtonVertices.setEnabled(True)

    def on_pushButtonGrafico_clicked(self):
        """Método para ser chamado quando o botão de gráfico for clicado."""
        # Obter a camada selecionada no listWidgetEstacas
        selected_item = self.listWidgetEstacas.currentItem()
        if not selected_item:
            self.mostrar_mensagem("Nenhuma camada de estacas selecionada.", "Erro")
            return

        estacas_layer_name = selected_item.text()
        estacas_layer = self.grafico_manager.find_layer(estacas_layer_name, QgsVectorLayer)
        if not estacas_layer:
            self.mostrar_mensagem(f"Camada de estacas '{estacas_layer_name}' não encontrada.", "Erro")
            return

        # Obter a camada raster selecionada
        raster_layer_id = self.comboBoxRaster.currentData()
        raster_layer = QgsProject.instance().mapLayer(raster_layer_id)
        if not raster_layer:
            self.mostrar_mensagem("Camada raster não encontrada.", "Erro")
            return

        # Criar a camada de pontos de apoio temporariamente para uso no gráfico
        pontos_apoio_layer = self.grafico_manager.create_support_points_layer(estacas_layer, raster_layer)

        # Passar o caminho do logotipo para o GraficoManager
        self.grafico_manager.logo_path = self.logo_path

        # Gerar o gráfico com base nas camadas de estacas e pontos de apoio (sem adicionar ao projeto)
        self.grafico_manager.plot_layers(estacas_layer, pontos_apoio_layer, 45, 45)

    def on_pushButtonJuntar_clicked(self):
        """Une camadas do listWidget Estacas e Talude em uma única camada de pontos e cria conexões de linhas."""
        unique_coords = set()
        unique_features = []  # [(attributes_dict, geom, crs), ...]
        crs_from_inputs = None

        estacas_points_global = []  # [(feature, geom, crs, layer_name), ...]
        talude_points_global  = []

        def _safe_attr(feat, name, default=None):
            try:
                if name in feat.fields().names():
                    v = feat[name]
                    return v if v is not None else default
                return default
            except Exception:
                return default

        def process_list_widget(list_widget, cota_field_name, points_list, layer_type):
            nonlocal crs_from_inputs
            for index in range(list_widget.count()):
                item = list_widget.item(index)
                if not item or not item.text() or item.text() in ("Estaqueamentos", "Taludes"):
                    continue

                layer_name = item.text()
                layers = QgsProject.instance().mapLayersByName(layer_name)
                if not layers:
                    self.mostrar_mensagem(f"Camada '{layer_name}' não encontrada.", "Erro")
                    continue
                layer = layers[0]
                if not isinstance(layer, QgsVectorLayer):
                    continue

                if crs_from_inputs is None:
                    crs_from_inputs = layer.crs()

                for feature in layer.getFeatures():
                    geom = feature.geometry()
                    if not geom or geom.isEmpty():
                        continue
                    if geom.type() != QgsWkbTypes.PointGeometry:
                        self.mostrar_mensagem(f"A camada '{layer_name}' não é de pontos.", "Erro")
                        continue

                    pt = geom.asPoint()
                    coord_tuple = (round(pt.x(), 6), round(pt.y(), 6))
                    if coord_tuple not in unique_coords:
                        unique_coords.add(coord_tuple)

                        # ID como INTEIRO (se não existir/for inválido → usa FID)
                        src_id_val = _safe_attr(feature, 'ID', None)
                        try:
                            id_int = int(src_id_val) if src_id_val is not None and str(src_id_val).strip() != '' else int(feature.id())
                        except Exception:
                            id_int = int(feature.id())

                        at_x = _safe_attr(feature, 'X', pt.x())
                        at_y = _safe_attr(feature, 'Y', pt.y())

                        cota = _safe_attr(feature, cota_field_name, None)
                        if cota is None:
                            try:
                                cota = pt.z()
                            except Exception:
                                cota = None

                        attributes = {'ID': id_int, 'X': at_x, 'Y': at_y, 'Cota': cota}
                        unique_features.append((attributes, geom, layer.crs()))

                    points_list.append((feature, geom, layer.crs(), layer_name))

        # Estacas (NovoZ) e Talude (Z)
        process_list_widget(self.listWidgetEstacas, 'NovoZ', estacas_points_global, 'Estacas')
        process_list_widget(self.listWidgetTalude, 'Z',     talude_points_global,  'Talude')

        if not unique_features:
            self.mostrar_mensagem("Nenhuma feição encontrada nas camadas selecionadas.", "Erro")
            return

        # Camada de polígono + ID para nome
        selected_polygon_id = self.comboBoxPoligono.currentData()
        polygon_layer = QgsProject.instance().mapLayer(selected_polygon_id)
        if not polygon_layer or not isinstance(polygon_layer, QgsVectorLayer):
            self.mostrar_mensagem("Selecione uma camada de polígono válida.", "Erro")
            return

        feat_poly = self._get_active_polygon_feature(polygon_layer)
        if not feat_poly:
            self.mostrar_mensagem("Não encontrei uma feição válida no polígono selecionado.", "Erro")
            return

        poly_id = self._get_poly_id_for_naming(polygon_layer, feat_poly)

        # CRS destino = do polígono (fallback EPSG:4326 se inválido)
        crs = polygon_layer.crs()
        if not crs.isValid():
            crs = QgsCoordinateReferenceSystem('EPSG:4326')
            self.mostrar_mensagem("CRS do polígono inválido. Usando EPSG:4326.", "Erro")
        crs_auth = crs.authid()

        # CAMADA DE PONTOS (MERGE)
        merged_name = f"{polygon_layer.name()}_ID{poly_id}_Pontos"

        for lyr in QgsProject.instance().mapLayersByName(merged_name):
            QgsProject.instance().removeMapLayer(lyr.id())

        merged_points_layer = QgsVectorLayer(f"Point?crs={crs_auth}", merged_name, "memory")
        pr = merged_points_layer.dataProvider()

        # ID como INTEIRO
        fields = [
            QgsField('ID',   QVariant.Int),
            QgsField('X',    QVariant.Double),
            QgsField('Y',    QVariant.Double),
            QgsField('Cota', QVariant.Double)]
        pr.addAttributes(fields)
        merged_points_layer.updateFields()

        # Reprojeção e criação de feições
        new_features = []
        for attributes, geom, feat_crs in unique_features:
            # cópia profunda SEM usar .clone()
            g = QgsGeometry(geom)

            # reprojeta, se necessário
            if feat_crs != crs:
                try:
                    transform = QgsCoordinateTransform(feat_crs, crs, QgsProject.instance())
                    g.transform(transform)
                except Exception:
                    pass  # se falhar, segue com a geometria na origem

            f = QgsFeature(merged_points_layer.fields())
            f.setGeometry(g)

            # ID inteiro (pode ser None → NULL)
            id_val = attributes.get('ID', None)
            try:
                id_val = int(id_val) if id_val is not None else None
            except Exception:
                id_val = None
            f.setAttribute('ID', id_val)

            # X/Y double
            try:
                f.setAttribute('X', float(attributes.get('X', 0.0)))
            except Exception:
                f.setAttribute('X', None)
            try:
                f.setAttribute('Y', float(attributes.get('Y', 0.0)))
            except Exception:
                f.setAttribute('Y', None)

            # Cota double (tenta Z do ponto se vier None)
            cota_val = attributes.get('Cota', None)
            if cota_val is None:
                try:
                    p = g.asPoint()
                    cota_val = p.z()
                except Exception:
                    cota_val = None
            try:
                cota_val = float(cota_val) if cota_val is not None else None
            except Exception:
                cota_val = None
            f.setAttribute('Cota', cota_val)

            new_features.append(f)

        pr.addFeatures(new_features)
        self._top_insert_layer(merged_points_layer)

        # >>> Geração opcional da MALHA TIN a partir da camada unificada
        if self.checkBoxMalha.isChecked():
            mesh_name = f"{polygon_layer.name()}_ID{poly_id}_TIN"
            self._create_mesh_from_points_layer(merged_points_layer, mesh_name)

        # CAMADA DE LINHAS (TALUDE)
        line_layer_name = f"{polygon_layer.name()}_ID{poly_id}_Linhas_Talude"

        for lyr in QgsProject.instance().mapLayersByName(line_layer_name):
            QgsProject.instance().removeMapLayer(lyr.id())

        line_layer = QgsVectorLayer(f"LineString?crs={crs_auth}", line_layer_name, "memory")
        lpr = line_layer.dataProvider()

        # Mantive ID como string aqui para compatibilidade (posso mudar para Int se quiser)
        line_fields = [
            QgsField('ID',   QVariant.Int),
            QgsField('Tipo', QVariant.String),
            QgsField('Nome', QVariant.String)]
        lpr.addAttributes(line_fields)
        line_layer.updateFields()
        self._top_insert_layer(line_layer)

        feature_id = 1  # contador para as linhas

        # Agrupa pontos por camada de origem
        estacas_layers = {}
        for feature, geom, feature_crs, layer_name in estacas_points_global:
            estacas_layers.setdefault(layer_name, []).append((feature, geom, feature_crs))

        talude_layers = {}
        for feature, geom, feature_crs, layer_name in talude_points_global:
            talude_layers.setdefault(layer_name, []).append((feature, geom, feature_crs))

        # Conecta Estacas ↔ Talude
        for estacas_layer_name, estacas_points in estacas_layers.items():
            matching_talude_layers = self.find_matching_talude_layers(estacas_layer_name, talude_layers)
            if not matching_talude_layers:
                self.mostrar_mensagem(f"Nenhuma camada de Talude correspondente para '{estacas_layer_name}'.", "Erro")
                continue

            talude_points = []
            for t_name in matching_talude_layers:
                talude_points.extend(talude_layers[t_name])

            feature_id = self.create_connections_between_layers(estacas_points, talude_points, crs, estacas_layer_name, line_layer, feature_id)

        self.mostrar_mensagem(f"Camadas criadas: '{merged_name}' e '{line_layer_name}'.", "Sucesso")

    def update_radio_buttons_state(self):
        """
        Habilita/desabilita e (des)seleciona os radio buttons
        de acordo com o conteúdo do listWidgetEstacas.
        """
        # cabeçalho ocupa o índice 0; conta itens “reais”
        total_itens = self.listWidgetEstacas.count() - 1  # −1 para ignorar o cabeçalho
        tem_estacas    = total_itens > 0

        # habilita / desabilita
        self.radioButtonEsquerda.setEnabled(tem_estacas)
        self.radioButtonDireita.setEnabled(tem_estacas)

        if not tem_estacas:
            # 👉 nenhum dado → nenhum botão marcado
            for rb in (self.radioButtonEsquerda, self.radioButtonDireita):
                rb.setAutoExclusive(False)   # permite “desmarcar”
                rb.setChecked(False)
                rb.setAutoExclusive(True)    # restaura exclusividade
        else:
            # 👉 há dados → garante que pelo menos um esteja marcado
            if not (self.radioButtonEsquerda.isChecked() or self.radioButtonDireita.isChecked()):
                self.radioButtonEsquerda.setChecked(True)

    def update_pushButtonGrafico_state(self):
        """
        Ativa o pushButtonGrafico somente se houver algum item real
        (isto é, não-cabeçalho) selecionado no listWidgetEstacas.
        """
        # Filtra os itens selecionados ignorando o cabeçalho
        selecionados_validos = [it for it in self.listWidgetEstacas.selectedItems()
            if it.flags() != Qt.NoItemFlags]

        # Habilita se existir pelo menos um
        self.pushButtonGrafico.setEnabled(bool(selecionados_validos))

    def update_pushButtonJuntar_state(self):
        """
        Habilita ou desabilita o pushButtonJuntar com base
        no conteúdo real dos listWidgets (ignora o cabeçalho).
        """
        # total de “itens de dados” em cada widget = total - 1 (o cabeçalho)
        estacas_count = max(0, self.listWidgetEstacas.count() - 1)
        talude_count = max(0, self.listWidgetTalude.count() - 1)

        # se quiser habilitar quando houver algo em *ambos*
        # self.pushButtonJuntar.setEnabled(estacas_count > 0 and talude_count > 0)

        # se quiser habilitar quando houver em *qualquer um*:
        self.pushButtonJuntar.setEnabled(estacas_count > 0 or talude_count > 0)

    def on_pushButtonLogo_clicked(self):
        """Método chamado ao clicar no botão para selecionar o logo."""
        # Abrir um diálogo para selecionar a imagem
        logo_path, _ = QFileDialog.getOpenFileName(self, "Selecione o logo", "", "Image Files (*.png *.jpg *.jpeg)")

        if logo_path:
            # Armazenar o caminho da imagem selecionada na memória
            self.logo_path = logo_path
            self.mostrar_mensagem(f"Logotipo selecionado: {logo_path}", "Sucesso")

            # Atualizar o texto e a cor do botão, sem alterar o estilo original
            self.pushButtonLogo.setText("Logo ✓")
            self.pushButtonLogo.setStyleSheet(self.pushButtonLogo.styleSheet() + "color: blue;")
        else:
            self.mostrar_mensagem("Nenhum logotipo selecionado.", "Erro")

            # Resetar o botão para o estado inicial se não houver logo
            self.logo_path = None
            self.pushButtonLogo.setText("Logo")
            self.pushButtonLogo.setStyleSheet(self.pushButtonLogo.styleSheet() + "color: black;")

    def check_logo_status(self):
        """Verifica se existe um logo armazenado e ajusta o botão pushButtonLogo adequadamente."""
        if hasattr(self, 'logo_path') and self.logo_path:
            # Se houver um logo armazenado, exibe o texto "Logo ✓" e estilo azul
            self.pushButtonLogo.setText("Logo ✓")
            self.pushButtonLogo.setStyleSheet(self.pushButtonLogo.styleSheet() + "color: blue;")
        else:
            # Caso contrário, exibe o texto "Logo" com o estilo padrão
            self.pushButtonLogo.setText("Logo")
            self.pushButtonLogo.setStyleSheet(self.pushButtonLogo.styleSheet() + "color: black;")

    def on_pushButtonVertices_clicked(self):
        """
        Método para ser chamado ao clicar no botão "Vertices".
        Verifica se há widgets adicionados na scrollArea e ativa o botão "Calcular" se houver.
        """
        # Verifique se a scrollArea tem um layout configurado
        layout = self.scrollAreaWidgetContents.layout()

        if layout and layout.count() > 0:
            # Ativa o botão Calcular se houver widgets
            self.pushButtonCalcular.setEnabled(True)
        else:
            # Desativa o botão Calcular se não houver widgets
            self.pushButtonCalcular.setEnabled(False)

    def connect_selection_signal(self):
        """Conecta o sinal selectionChanged para detectar mudanças na seleção de feições e controlar o botão."""
        selected_polygon_id = self.comboBoxPoligono.currentData()
        selected_layer = QgsProject.instance().mapLayer(selected_polygon_id)

        if selected_layer and isinstance(selected_layer, QgsVectorLayer):
            selected_layer.selectionChanged.connect(self.on_selection_changed)

    def on_selection_changed(self):
        """Função chamada quando a seleção de feições é alterada. Atualiza o polígono e controla o estado do botão."""
        self.display_polygon()  # Atualiza a exibição do polígono
        selected_polygon_id = self.comboBoxPoligono.currentData()
        selected_layer = QgsProject.instance().mapLayer(selected_polygon_id)
        
        # Verifica se há feições selecionadas
        if selected_layer and selected_layer.selectedFeatureCount() > 0:
            self.pushButtonVertices.setEnabled(True)  # Ativa o botão
        else:
            self.pushButtonVertices.setEnabled(False)  # Desativa o botão

    def open_excel_with_attributes(self, item):
        """
        Abre um arquivo Excel com as informações da tabela de atributos da camada selecionada.
        """
        if not item or not item.text():
            self.mostrar_mensagem("Nenhuma camada selecionada.", "Erro")
            return

        # Obtém o nome da camada a partir do texto do item selecionado
        layer_name = item.text()
        layer = QgsProject.instance().mapLayersByName(layer_name)

        if not layer:
            self.mostrar_mensagem(f"Camada '{layer_name}' não encontrada.", "Erro")
            return

        layer = layer[0]

        if not isinstance(layer, QgsVectorLayer):
            self.mostrar_mensagem(f"'{layer_name}' não é uma camada de vetor.", "Erro")
            return

        # Extrai os atributos da camada
        fields = [field.name() for field in layer.fields()]
        features = [feat for feat in layer.getFeatures()]

        # Verifica se há feições na camada
        if not features:
            self.mostrar_mensagem(f"A camada '{layer_name}' não contém feições.", "Erro")
            return

        # Cria uma lista para armazenar os dados
        dados_tabela = []

        # Percorre todas as feições e campos para extrair os dados
        for feature in features:
            linha_dados = []
            for field_name in fields:
                valor_celula = feature[field_name]
                # Tenta converter para número se possível
                try:
                    valor_celula = float(valor_celula)
                except (ValueError, TypeError):
                    pass  # Mantém o valor como string se não for um número
                linha_dados.append(valor_celula)
            dados_tabela.append(linha_dados)

        # Cria um DataFrame a partir dos dados
        df = pd.DataFrame(dados_tabela, columns=fields)

        # Cria um arquivo temporário para o Excel
        temp_fd, fileName = tempfile.mkstemp(suffix=".xlsx")
        os.close(temp_fd)  # Fecha o arquivo temporário para evitar conflito de acesso

        try:
            # Salva o DataFrame no arquivo Excel
            df.to_excel(fileName, index=False)

            # Adiciona bordas a todas as células no Excel
            self.add_borders_to_excel(fileName)

            # Abre o Excel com o arquivo temporário
            os.startfile(fileName)  # Funciona no Windows
            self.mostrar_mensagem("A tabela foi exportada e aberta no Excel temporariamente.", "Sucesso")

        except Exception as e:
            self.mostrar_mensagem(f"Erro ao salvar o arquivo Excel: {str(e)}", "Erro")

    def add_borders_to_excel(self, file_path):
        """
        Adiciona bordas a todas as células do arquivo Excel especificado.
        """
        # Abre o workbook existente
        workbook = load_workbook(file_path)
        sheet = workbook.active

        # Define o estilo da borda
        thin_border = Border(
            left=Side(style='thin'),
            right=Side(style='thin'),
            top=Side(style='thin'),
            bottom=Side(style='thin'))

        # Aplica a borda a todas as células usadas na planilha
        for row in sheet.iter_rows(min_row=1, max_row=sheet.max_row, min_col=1, max_col=sheet.max_column):
            for cell in row:
                cell.border = thin_border

        # Ajusta a largura das colunas automaticamente
        for col in sheet.columns:
            max_length = 0
            column = col[0].column_letter  # Pega a letra da coluna
            for cell in col:
                try:
                    if len(str(cell.value)) > max_length:
                        max_length = len(str(cell.value))
                except:
                    pass
            adjusted_width = (max_length + 2)
            sheet.column_dimensions[column].width = adjusted_width

        # Salva as alterações no arquivo
        workbook.save(file_path)
        workbook.close()

    def _intersect_talude_mdt(self, p0: QgsPointXY, novo_z0: float, perp_dx: float, perp_dy: float, direction_factor: int, raster_layer: QgsRasterLayer, max_dist: float = 250.0) -> tuple:
        """
        Caminha num plano 45° (1:1) a partir de p0, perpendicular à aresta,
        até o MDT cruzar a reta do talude.  Retorna (QgsPointXY, z_mdt, desnivel).

        Se não cruzar dentro de max_dist, devolve o último ponto avaliado.
        """
        # passo: use a resolução média do raster (ou 1 m mínimo)
        passo = max(raster_layer.rasterUnitsPerPixelX(), raster_layer.rasterUnitsPerPixelY(), 1.0)

        # sinal: +1 se estamos cortando (NovoZ maior que MDT no vértice)
        z0_terreno = self.get_z_value(p0.x(), p0.y()) or novo_z0
        sinal =  1 if novo_z0 >= z0_terreno else -1    # +1 = corte, -1 = aterro

        dist = 0.0
        while dist <= max_dist:
            dist += passo
            x = p0.x() + direction_factor * perp_dx * dist
            y = p0.y() + direction_factor * perp_dy * dist

            z_mdt = self.get_z_value(x, y)
            if z_mdt is None:          # fora do raster ⇒ para aqui
                break

            # cota da reta 45°: z_reta = NovoZ0  -/+  dist   (sinal define)
            z_reta = novo_z0 - sinal * dist

            cruzou =  (sinal > 0 and z_mdt >= z_reta) or \
                      (sinal < 0 and z_mdt <= z_reta)
            if cruzou:
                return QgsPointXY(x, y), z_mdt, (novo_z0 - z_mdt)

        # fallback se não cruzar
        return QgsPointXY(x, y), z_mdt if z_mdt is not None else 0.0, (novo_z0 - (z_mdt or 0.0))

    def _prepare_talude_input(self):
        """
        Faz todas as verificações/leituras necessárias.
        Retorna uma tupla com (layer, points, raster_layer, lado, direction_factor, talude_layer_name, crs)
        ou None caso algum erro ocorra (as mensagens já são exibidas).
        """
        # Gerenciamento de seleção
        current_row = self.listWidgetEstacas.currentRow()
        self._next_estaca_index = current_row + 1 if current_row != -1 else None

        selected_item = self.listWidgetEstacas.currentItem()
        if not selected_item:
            self.mostrar_mensagem("Nenhuma camada selecionada no listWidgetEstacas.", "Erro")
            return None

        layer_name = selected_item.text()
        layer_list = QgsProject.instance().mapLayersByName(layer_name)
        if not layer_list:
            self.mostrar_mensagem(f"Camada '{layer_name}' não encontrada.", "Erro")
            return None
        layer = layer_list[0]

        points = [f for f in layer.getFeatures()]
        if len(points) < 2:
            self.mostrar_mensagem("A camada selecionada deve conter pelo menos dois pontos.", "Erro")
            return None

        selected_raster_id = self.comboBoxRaster.currentData()
        raster_layer = QgsProject.instance().mapLayer(selected_raster_id)
        if not raster_layer or not isinstance(raster_layer, QgsRasterLayer):
            self.mostrar_mensagem("Camada raster inválida ou não selecionada.", "Erro")
            return None

        if self.radioButtonEsquerda.isChecked():
            lado, direction_factor = "esquerda", 1
        elif self.radioButtonDireita.isChecked():
            lado, direction_factor = "direita", -1
        else:
            self.mostrar_mensagem("Selecione se o talude será criado à esquerda ou à direita.", "Erro")
            return None

        talude_layer_name = f"{layer_name}_Talude_{lado.capitalize()}"
        crs = layer.crs()

        return layer, points, raster_layer, lado, direction_factor, talude_layer_name, crs

    def calculate_talude(self):
        """
        Calcula e gera as camadas de talude com base nas estacas selecionadas,
        criando novas camadas e aplicando simbologia específica.
        """
        dados = self._prepare_talude_input()
        if not dados:
            return  # já houve mensagem de erro

        (layer, points, raster_layer,
         lado, direction_factor,
         talude_layer_name, crs) = dados

        self._create_talude_layer(layer, points, raster_layer, lado, direction_factor, talude_layer_name, crs)

    def _create_talude_layer(self, layer, points, raster_layer, lado, direction_factor, talude_layer_name, crs):
        """
        1) Remove qualquer camada antiga de talude com o mesmo nome.
        2) Cria uma nova camada de pontos em memória para armazenar o talude.
        3) Define os campos: ID, X, Y, Z, NovoZ e Desnível.
        4) Obtém os vértices do polígono original selecionado (fecha o anel e remove duplicação).
        5) Calcula a normal bissetriz nos vértices correspondentes ao primeiro e último ponto de estacas:
           - Para cada vértice, obtém os vetores das duas arestas adjacentes.
           - Gera os normais “à esquerda” de cada aresta.
           - Faz a média vetorial e normaliza para obter o vetor unitário diagonal.
        6) Associa cada ponto de estaca ao seu vértice mais próximo do polígono original.
        7) No loop de criação de pontos de talude:
           a) Para i == 0 e i == n-1, usa a normal bissetriz calculada.
           b) Para demais i, calcula a perpendicular simples ao segmento de estacas.
           c) Chama _intersect_talude_mdt() para projetar o ponto a 45°.
           d) Cria a feição com geometria e atributos e adiciona ao provedor.
        8) Insere a camada dentro do grupo "Talude" na árvore de camadas.
        9) Exibe mensagem de sucesso e atualiza o listWidget de taludes.
        """
        # Remove camada antiga, se existir
        existing = QgsProject.instance().mapLayersByName(talude_layer_name)
        if existing:
            QgsProject.instance().removeMapLayer(existing[0].id())

        # Cria a camada de pontos em memória
        talude_layer = QgsVectorLayer(f"Point?crs={crs.authid()}", talude_layer_name, "memory")

        # Adiciona atributos necessários
        pr = talude_layer.dataProvider()
        pr.addAttributes([
            QgsField("ID", QVariant.Int),
            QgsField("X", QVariant.Double),
            QgsField("Y", QVariant.Double),
            QgsField("Z", QVariant.Double),
            QgsField("NovoZ", QVariant.Double),
            QgsField("Desnível", QVariant.Double)])
        talude_layer.updateFields()

        # Extrai os vértices do polígono original
        poly_layer = QgsProject.instance().mapLayer(self.comboBoxPoligono.currentData())
        feat = next(poly_layer.getFeatures())
        geom = feat.geometry()
        rings = geom.asPolygon() if not geom.isMultipart() else geom.asMultiPolygon()[0]
        verts = rings[0]
        if verts[0] == verts[-1]:
            verts = verts[:-1]

        # Função interna para calcular normal bissetriz num vértice do polígono
        def _vertex_bisector_normal(idx):
            """Retorna o normal unitário bissetriz no vértice idx do polígono."""
            p_cur  = verts[idx]
            p_prev = verts[idx-1] if idx>0 else verts[-1]
            p_next = verts[(idx+1) % len(verts)]

            # vetores de aresta
            v1x, v1y = p_cur.x() - p_prev.x(), p_cur.y() - p_prev.y()
            v2x, v2y = p_next.x() - p_cur.x(), p_next.y() - p_cur.y()

            # normais “à esquerda”
            n1x, n1y = -v1y, v1x
            n2x, n2y = -v2y, v2x

            # normaliza
            l1 = math.hypot(n1x, n1y)
            if l1>0: n1x, n1y = n1x/l1, n1y/l1
            l2 = math.hypot(n2x, n2y)
            if l2>0: n2x, n2y = n2x/l2, n2y/l2

            # média e normalização final
            ax, ay = n1x + n2x, n1y + n2y
            la = math.hypot(ax, ay)
            if la>0:
                return ax/la, ay/la
            else:
                return n2x, n2y

        # Localiza qual vértice do polígono corresponde ao primeiro/último estaqueamento
        def _find_nearest_vertex(pt):
            """
            Retorna o índice e a distância do vértice mais próximo ao ponto informado.
            """
            dists = [pt.distance(v) for v in verts]
            return dists.index(min(dists))

        p0 = points[0].geometry().asPoint()
        pN = points[-1].geometry().asPoint()
        idx0 = _find_nearest_vertex(p0)
        idxN = _find_nearest_vertex(pN)

        # Calcula normais bissetriz para início e fim
        perp_start = _vertex_bisector_normal(idx0)
        perp_end   = _vertex_bisector_normal(idxN)

        # Loop de criação dos pontos de talude
        n = len(points)
        for i in range(n):
            if i == 0:
                perp_dx, perp_dy = perp_start
            elif i == n - 1:
                perp_dx, perp_dy = perp_end
            else:
                # Perpendicular ao próprio segmento de estaqueamento
                p1 = points[i].geometry().asPoint()
                p2 = points[(i+1) % n].geometry().asPoint()
                dx, dy = p2.x() - p1.x(), p2.y() - p1.y()
                L = math.hypot(dx, dy) or 1.0
                perp_dx, perp_dy = -dy / L, dx / L

            # Opcional: log para conferir
            if i in (0, n-1):
                ang = math.degrees(math.atan2(perp_dy, perp_dx))
                # self._log_message(f"[Talude] vértice {i} → perp ({perp_dx:.3f},{perp_dy:.3f}) = {ang:.1f}°")

            # interseção 45°
            p1 = points[i].geometry().asPoint()
            z1 = points[i]['NovoZ']
            p_talude, z_mdt, desn_t = self._intersect_talude_mdt(
                p1, z1, perp_dx, perp_dy, direction_factor, raster_layer)

            # Cria e adiciona a feição
            feat = QgsFeature(talude_layer.fields())
            feat.setGeometry(QgsGeometry.fromPointXY(p_talude))
            feat.setAttributes([
                i+1,
                round(p_talude.x(), 3),
                round(p_talude.y(), 3),
                z_mdt,
                round(z1, 3),
                desn_t])
            pr.addFeature(feat)

        # Insere a camada no grupo "Taludes"
        root  = QgsProject.instance().layerTreeRoot()
        group = root.findGroup("Taludes") or root.addGroup("Taludes")
        QgsProject.instance().addMapLayer(talude_layer, False)
        group.insertLayer(0, talude_layer)

        self.mostrar_mensagem(
            f"Camada '{talude_layer_name}' de talude criada com sucesso.", "Sucesso")
        self.update_list_widget_talude()

    def _create_mesh_from_points_layer(self, source_layer: QgsVectorLayer, mesh_name: str):
        """
        Gera TIN (TEMPORARY_OUTPUT) a partir de 'source_layer' (pontos), usando SEMPRE o campo 'Cota'.
        Insere a malha no TOPO do painel. Respeita checkBoxSeleciona (usa só selecionados, se marcado).
        """
        try:
            if not source_layer or source_layer.geometryType() != QgsWkbTypes.PointGeometry:
                self.mostrar_mensagem("Camada inválida para gerar TIN (deve ser de pontos).", "Erro")
                return

            # Apenas selecionados?
            use_selected = getattr(self, "checkBoxSeleciona", None).isChecked() if hasattr(self, "checkBoxSeleciona") else False
            src_for_processing = source_layer

            if use_selected:
                if source_layer.selectedFeatureCount() == 0:
                    self.mostrar_mensagem("Não há feições selecionadas para gerar a TIN.", "Erro")
                    return
                tmp_sel = QgsVectorLayer(f"Point?crs={source_layer.crs().authid()}", "mesh_selected_points", "memory")
                tmp_sel_dp = tmp_sel.dataProvider()
                tmp_sel_dp.addAttributes(source_layer.fields())
                tmp_sel.updateFields()
                tmp_sel_dp.addFeatures(list(source_layer.getSelectedFeatures()))
                tmp_sel.updateExtents()
                src_for_processing = tmp_sel

            # (Obrigatório) usar apenas pontos com Cota não nula
            if "Cota" not in src_for_processing.fields().names():
                self.mostrar_mensagem("A camada de pontos não possui o campo 'Cota'.", "Erro")
                return

            tmp = QgsVectorLayer(f"Point?crs={src_for_processing.crs().authid()}", "mesh_points_nonnull", "memory")
            tmp_dp = tmp.dataProvider()
            tmp_dp.addAttributes(src_for_processing.fields())
            tmp.updateFields()

            feats_ok = [f for f in src_for_processing.getFeatures() if f["Cota"] is not None]
            if not feats_ok:
                self.mostrar_mensagem("Nenhum ponto com 'Cota' definida para gerar TIN.", "Erro")
                return
            tmp_dp.addFeatures(feats_ok)
            tmp.updateExtents()
            src_for_processing = tmp  # garante Cota não nula

            # Índice do campo 'Cota' (na layer final de processamento)
            idx_cota = src_for_processing.fields().indexFromName("Cota")

            # Garantir que o Processing consiga resolver a camada pelo ID
            added_tmp = False
            if QgsProject.instance().mapLayer(src_for_processing.id()) is None:
                # adiciona sem exibir no painel
                QgsProject.instance().addMapLayer(src_for_processing, False)
                added_tmp = True

            try:
                # Monta SOURCE_DATA corretamente: lista de dicts {source, type, attributeIndex}
                source_data = [{
                    "source": src_for_processing.id(),  # string ID da camada
                    "type": 0,                          # 0 = Vertices (para pontos)
                    "attributeIndex": idx_cota}]        # índice do campo 'Cota'

                result = processing.run(
                    "native:tinmeshcreation", {
                        "SOURCE_DATA": source_data,
                        "MESH_FORMAT": 0,                         # 2DM
                        "CRS_OUTPUT": QgsCoordinateReferenceSystem(),
                        "OUTPUT_MESH": "TEMPORARY_OUTPUT"})
            finally:
                # remove a camada temporária do projeto, se tiver sido adicionada só para o run
                if added_tmp:
                    QgsProject.instance().removeMapLayer(src_for_processing.id())

            mesh_path = result["OUTPUT_MESH"]
            mesh_layer = QgsMeshLayer(mesh_path, mesh_name, "mdal")
            if not mesh_layer.isValid():
                raise Exception("Falha ao carregar a malha gerada.")

            self._top_insert_layer(mesh_layer)
            self.mostrar_mensagem("Malha TIN gerada com sucesso!", "Sucesso")

        except Exception as e:
            self.mostrar_mensagem(f"Erro ao gerar a TIN: {e}", "Erro")

class GraficoManager:
    def __init__(self, plato_manager):
        """
        Inicializa o GraficoManager com uma referência para a classe PlatoManager.

        Parameters:
        - plato_manager: A instância de PlatoManager que contém o método mostrar_mensagem.
        """
        self.plato_manager = plato_manager
        self.logo_path = None  # Inicialmente, não há logo armazenado

    def find_layer(self, layer_name, layer_type):
        """
        Retorna a camada do projeto que corresponde ao nome e tipo especificados.
        """
        for layer in QgsProject.instance().mapLayers().values():
            if layer.name() == layer_name and isinstance(layer, layer_type):
                return layer
        return None

    def sample_raster_value(self, point, raster_layer):
        """Amostra o valor Z do raster baseado nas coordenadas do ponto."""
        identify_result = raster_layer.dataProvider().identify(QgsPointXY(point.x(), point.y()), QgsRaster.IdentifyFormatValue)
        
        if identify_result.isValid():
            results = identify_result.results()
            if results:
                band_key = list(results.keys())[0]
                z_value = results.get(band_key)  # Use get para evitar KeyError
                if z_value is not None:  # Verifica se o valor é válido
                    return round(float(z_value), 3)
        return None  # Retorna None se não houver valor válido

    def create_support_points_layer(self, estacas_layer, raster_layer):
        """
        Cria uma camada de pontos de apoio a partir das estacas fornecidas,
        atribuindo valores de elevação a partir do raster selecionado.
        """
        # Obter a resolução do pixel da camada raster
        raster_extent = raster_layer.extent()
        raster_width = raster_layer.width()
        raster_height = raster_layer.height()
        pixel_resolution_x = raster_extent.width() / raster_width
        pixel_resolution_y = raster_extent.height() / raster_height

        # Definir o espaçamento de suporte como a resolução do pixel
        support_spacing = min(pixel_resolution_x, pixel_resolution_y)

        # Obter o CRS da camada de estacas
        crs = estacas_layer.sourceCrs().authid()
        support_layer = QgsVectorLayer(f"Point?crs={crs}", "", "memory")  # Criar a camada sem nome

        prov = support_layer.dataProvider()

        # Adiciona os campos necessários, incluindo "Acumula_dist"
        prov.addAttributes([QgsField("ID", QVariant.Int), QgsField("Original_ID", QVariant.Int),
                            QgsField("X", QVariant.Double), QgsField("Y", QVariant.Double),
                            QgsField("Znovo", QVariant.Double), QgsField("Acumula_dist", QVariant.Double)])
        support_layer.updateFields()

        estacas_features = [feat for feat in estacas_layer.getFeatures()]
        estacas_points = [feat.geometry().asPoint() for feat in estacas_features]
        all_points = []
        support_point_id = 0
        last_coord = None
        acumula_dist = 0

        # Calcular o desnível para a extensão (1 metro além do desnível)
        first_desnivel = estacas_features[0]['Desnível']
        last_desnivel = estacas_features[-1]['Desnível']
        extend_by_start = min(abs(first_desnivel) + 3, 3)  # No máximo 10 metros
        extend_by_end = min(abs(last_desnivel) + 3, 3)  # No máximo 10 metros

        # Adicionar pontos extras antes do primeiro ponto de estacas
        first_length = estacas_points[0].distance(estacas_points[1])
        if first_length > 0:
            first_segment_dir = estacas_points[1] - estacas_points[0]
            for i in range(int(extend_by_start // support_spacing), 0, -1):
                extra_point = QgsPointXY(
                    estacas_points[0].x() - (first_segment_dir.x() * i * support_spacing) / first_length,
                    estacas_points[0].y() - (first_segment_dir.y() * i * support_spacing) / first_length)

                z_value = self.sample_raster_value(extra_point, raster_layer)
                if z_value is None:
                    # Interrompe a extensão se o valor de Z for None
                    break
                
                support_point_id += 1
                all_points.append((extra_point, -i, support_point_id))

        # Gerar os pontos de apoio ao longo dos segmentos
        for i, start_point in enumerate(estacas_points[:-1]):
            end_point = estacas_points[i + 1]
            segment_length = start_point.distance(end_point)
            if segment_length == 0:        #  ➜ pular segmento nulo
                continue

            num_intermediate_points = int(segment_length / support_spacing)

            for j in range(num_intermediate_points + 1):
                # aqui segment_length é seguro (>0)
                x = start_point.x() + (end_point.x() - start_point.x()) * (j * support_spacing) / segment_length
                y = start_point.y() + (end_point.y() - start_point.y()) * (j * support_spacing) / segment_length

                inter_point = QgsPointXY(x, y)
                support_point_id += 1
                all_points.append((inter_point, estacas_features[i]['ID'], support_point_id))

        # Adicionar pontos extras após o último ponto de estacas
        last_length = estacas_points[-1].distance(estacas_points[-2])
        if last_length > 0:
            last_segment_dir = estacas_points[-1] - estacas_points[-2]
            for i in range(0, int(extend_by_end // support_spacing) + 1):
                extra_point = QgsPointXY(
                    estacas_points[-1].x() + (last_segment_dir.x() * i * support_spacing) / last_length,
                    estacas_points[-1].y() + (last_segment_dir.y() * i * support_spacing) / last_length)

                z_value = self.sample_raster_value(extra_point, raster_layer)
                if z_value is None:
                    # Interrompe a extensão se o valor de Z for None
                    break

                support_point_id += 1
                all_points.append((extra_point, estacas_features[-1]['ID'], support_point_id))

        # Criar os recursos e adicionar à camada com a distância acumulada
        first_positive_id_encountered = False
        for point, original_id, support_point_id in all_points:
            current_coord = (point.x(), point.y())
            if not first_positive_id_encountered:
                if original_id >= 0:
                    acumula_dist = 0
                    first_positive_id_encountered = True
                else:
                    acumula_dist = -extend_by_start + (support_point_id - 1) * support_spacing
            else:
                if last_coord:
                    segment_distance = QgsPointXY(*last_coord).distance(QgsPointXY(*current_coord))
                    acumula_dist += segment_distance
            last_coord = current_coord

            # Atributos do recurso, incluindo acumulação de distância
            feat = QgsFeature()
            feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(*current_coord)))
            feat.setAttributes([support_point_id, original_id, round(point.x(), 3), round(point.y(), 3), None, round(acumula_dist, 3)])
            prov.addFeature(feat)

         # Amostra os valores do MDT para os pontos de apoio e atualiza o campo "Znovo"
        support_layer.startEditing()
        z_value_previous = None

        for feature in support_layer.getFeatures():
            point = feature.geometry().asPoint()
            z_value = self.sample_raster_value(point, raster_layer)
            if z_value is None and z_value_previous is not None:
                z_value = z_value_previous
            if z_value is not None:
                z_value_previous = z_value
            feature['Znovo'] = round(z_value, 3) if z_value is not None else None
            support_layer.updateFeature(feature)

        support_layer.commitChanges()

        # Retorna a camada sem adicionar ao projeto
        return support_layer

    def find_slope_intersection(self, x, y, apoio_distances, apoio_elevations, angle_degrees):
        """
        Calcula a posição de interseção do talude com base nas coordenadas iniciais,
        distâncias e elevações dos pontos de apoio e ângulo do talude.
        """
        angle_radians = math.radians(angle_degrees)
        slope = math.tan(angle_radians)
        
        # Calcula o ponto final da linha de talude (apenas como referência se não houver interseção)
        x_end = x + math.cos(angle_radians)
        y_end = y + math.sin(angle_radians)

        # Encontrar a interseção com a linha "Znovo"
        for i in range(len(apoio_distances) - 1):
            x1, y1 = apoio_distances[i], apoio_elevations[i]
            x2, y2 = apoio_distances[i + 1], apoio_elevations[i + 1]

            if x1 == x2:  # Ignora segmentos verticais, pois não haverá interseção
                continue

            # Cálculo da inclinação e interceptação da linha entre os pontos de Znovo
            slope_znovo = (y2 - y1) / (x2 - x1)
            intercept_znovo = y1 - slope_znovo * x1

            # Calcula o ponto de interseção
            intersect_x = (intercept_znovo - y + slope * x) / (slope - slope_znovo)
            intersect_y = slope * (intersect_x - x) + y

            # Verifica se a interseção está dentro do segmento de Znovo
            if min(x1, x2) <= intersect_x <= max(x1, x2) and min(y1, y2) <= intersect_y <= max(y1, y2):
                return intersect_x, intersect_y

        # Se não houver interseção, retorna o ponto final presumido
        return x_end, y_end

    def plot_layers(self, estacas_layer, pontos_apoio_layer, angle_degrees_ti, angle_degrees_tf):
        """
        Exibe um gráfico com as camadas de estacas e pontos de apoio e desenha os taludes com base nos ângulos fornecidos.
        
        Parameters:
        - estacas_layer: Camada de estacas.
        - pontos_apoio_layer: Camada de pontos de apoio.
        - angle_degrees_ti: Ângulo do talude inicial.
        - angle_degrees_tf: Ângulo do talude final.
        """
        # Coletar dados das camadas
        estacas_data = [(f['Acumula_dist'], f['NovoZ'], f['Z'], f['Desnível'], f.geometry().asPoint().x(), f.geometry().asPoint().y()) 
                        for f in estacas_layer.getFeatures()]
        pontos_apoio_data = [(f['Acumula_dist'], f['Znovo']) for f in pontos_apoio_layer.getFeatures()]

        # Ordenar os dados
        estacas_data.sort(key=lambda x: x[0])
        pontos_apoio_data.sort(key=lambda x: x[0])

        # Desempacotar dados
        estacas_distances, estacas_novoz, z_values, desnivels, x_coords, y_coords = zip(*estacas_data)
        apoio_distances, apoio_elevations = zip(*pontos_apoio_data)

        # Criar o gráfico principal
        fig, ax = plt.subplots(figsize=(15, 9))

        # Ajusta o título da figura com o nome da camada
        nome_camada = estacas_layer.name()
        fig.canvas.manager.set_window_title(f'{nome_camada}')

        fig.subplots_adjust(left=0.08, right=0.98, bottom=0.12, top=0.9)

        ax.set_xlabel('Distância Acumulada (m)')
        ax.set_ylabel('Elevação (m)')
        ax.set_title('Perfil Longitudinal')

        # Plotar a linha "NovoZ"
        ax.plot(estacas_distances, estacas_novoz, color='blue', linewidth=2, label='Perfil do Platô')

        # Plotar a linha "Znovo"
        ax.plot(apoio_distances, apoio_elevations, color='orange', linewidth=1.5, label='Terreno')

        # Pegar os pontos iniciais e finais da linha "NovoZ"
        start_x, start_y = estacas_distances[0], estacas_novoz[0]
        end_x, end_y = estacas_distances[-1], estacas_novoz[-1]

        # Elevações correspondentes em Znovo
        start_znovo = apoio_elevations[0]
        end_znovo = apoio_elevations[-1]

        # Ajustar o ângulo de inclinação para o talude de entrada (TI)
        if start_y > start_znovo:
            angle_degrees_ti = angle_degrees_ti
        else:
            angle_degrees_ti = 180 - angle_degrees_ti

        # Ajustar o ângulo de inclinação para o talude de saída (TF)
        if end_y > end_znovo:
            angle_degrees_tf = 180 - angle_degrees_tf
        else:
            angle_degrees_tf = angle_degrees_tf

        # Calcular e plotar a linha de talude para o ponto inicial (TI)
        talude_start_intersection = self.find_slope_intersection(start_x, start_y, apoio_distances, apoio_elevations, angle_degrees_ti)
        if talude_start_intersection:
            if estacas_novoz[0] > start_znovo:
                ti_color = 'blue'
            else:
                ti_color = 'red'
            ax.plot([start_x, talude_start_intersection[0]], [start_y, talude_start_intersection[1]], linestyle='--', linewidth=2.5, color=ti_color)

        # Calcular e plotar a linha de talude para o ponto final (TF)
        talude_end_intersection = self.find_slope_intersection(end_x, end_y, apoio_distances, apoio_elevations, angle_degrees_tf)
        if talude_end_intersection:
            if estacas_novoz[-1] > end_znovo:
                tf_color = 'blue'
            else:
                tf_color = 'red'
            ax.plot([end_x, talude_end_intersection[0]], [end_y, talude_end_intersection[1]], linestyle='--', linewidth=2.5, color=tf_color)

        # Adicionar linhas verticais nas extremidades da linha Talude
        if talude_start_intersection:
            ax.vlines(x=talude_start_intersection[0], ymin=0, ymax=talude_start_intersection[1], colors='grey', linestyles='dashed', linewidth=0.5)

        if talude_end_intersection:
            ax.vlines(x=talude_end_intersection[0], ymin=0, ymax=talude_end_intersection[1], colors='grey', linestyles='dashed', linewidth=0.5)

        # Criar os valores combinados de X para interpolação
        corte_adjusted_x = [talude_start_intersection[0]] + list(estacas_distances) + [talude_end_intersection[0]]
        corte_adjusted_y = [talude_start_intersection[1]] + list(estacas_novoz) + [talude_end_intersection[1]]

        # Preparar as listas estendidas de x e y para 'Znovo' e 'Corte Ajustado'
        x_combined = sorted(set(corte_adjusted_x + list(apoio_distances)))
        y_corte_interpolated = np.interp(x_combined, corte_adjusted_x, corte_adjusted_y)
        y_znovo_interpolated = np.interp(x_combined, apoio_distances, apoio_elevations)

        # Limitar o intervalo de x para o preenchimento
        x_fill_min = talude_start_intersection[0]
        x_fill_max = talude_end_intersection[0]

        # Ajustes nos limites do eixo Y
        y_min0 = min(apoio_elevations) - 2  # Valor mínimo da elevação para a linha de base com uma margem
        y_min1 = min(min(estacas_novoz), y_min0) - 3  # Valor mínimo com margem adicional
        y_max = max(max(estacas_novoz), max(apoio_elevations)) + 3  # Valor máximo com margem adicional
        ax.set_ylim(y_min1, y_max)

        # Plotar a linha "Znovo"
        # ax.plot(apoio_distances, apoio_elevations, color='orange', linewidth=1.5, label='Znovo (Terreno Natural)')

        # Preencher abaixo da linha "Terreno Natural" até a linha de base 'y_min0'
        ax.fill_between(apoio_distances, apoio_elevations, y_min0, color='lightgreen', alpha=0.3, hatch='***')

        # Filtrar os valores x e y para o intervalo de interesse
        x_fill = [x for x in x_combined if x_fill_min <= x <= x_fill_max]
        y_corte_fill = np.interp(x_fill, corte_adjusted_x, corte_adjusted_y)
        y_znovo_fill = np.interp(x_fill, apoio_distances, apoio_elevations)

        # Preenchimento vermelho abaixo de "Znovo" e acima de "Corte Ajustado"
        ax.fill_between(x_fill, y_corte_fill, y_znovo_fill, where=(y_corte_fill < y_znovo_fill), 
                        interpolate=True, color='red', alpha=0.5, hatch='///', label='Área Cortada')

        # Preenchimento azul acima de "Znovo" e abaixo de "Corte Ajustado"
        ax.fill_between(x_fill, y_znovo_fill, y_corte_fill, where=(y_corte_fill > y_znovo_fill), 
                        interpolate=True, color='blue', alpha=0.5, hatch='\\\\', label='Área Aterrada')

        # Adicionar a legenda para Talude Cortado e Aterrado
        ax.plot([], [], linestyle='--', linewidth=2.5, color='red', label='Talude Cortado')
        ax.plot([], [], linestyle='--', linewidth=2.5, color='blue', label='Talude Aterrado')

        # Função auxiliar para adicionar textos ao gráfico
        def add_text(ax, text, x_pos, y_pos, text_color, bbox_props):
            """Adiciona texto ao gráfico."""
            ax.text(x_pos, y_pos, text, ha="left", va="bottom", transform=ax.transAxes,
                    fontsize=9, weight='bold', fontstyle='italic', color=text_color, bbox=bbox_props)

        # Propriedades da caixa de anotação
        bbox_props = dict(facecolor='white', alpha=1, boxstyle="round,pad=0.5")

        # Calcular as áreas cortada e aterrada (manualmente)
        area_cortada = np.trapz(y_znovo_fill[y_znovo_fill > y_corte_fill] - y_corte_fill[y_znovo_fill > y_corte_fill], x=np.array(x_fill)[y_znovo_fill > y_corte_fill])
        area_aterrada = np.trapz(y_corte_fill[y_corte_fill > y_znovo_fill] - y_znovo_fill[y_corte_fill > y_znovo_fill], x=np.array(x_fill)[y_corte_fill > y_znovo_fill])

        # Calcular as inclinações
        inclinacao_novoz = (estacas_novoz[-1] - estacas_novoz[0]) / (estacas_distances[-1] - estacas_distances[0])
        inclinacao_znovo = (apoio_elevations[-1] - apoio_elevations[0]) / (apoio_distances[-1] - apoio_distances[0])

        # Adicionar textos ao gráfico
        add_text(ax, f'Área de Aterro: {abs(area_aterrada):.2f} m²', 0.85, 1.06, 'blue', bbox_props)
        add_text(ax, f'Área de Corte: {abs(area_cortada):.2f} m²', 0.85, 1.016, 'magenta', bbox_props)
        add_text(ax, f'Inclinação do Corte/Aterro: {inclinacao_novoz * 100:.3f}%', 0.005, 1.016, 'blue', bbox_props)
        add_text(ax, f'Inclinação média do Terreno: {inclinacao_znovo * 100:.3f}%', 0.005, 1.06, 'orange', bbox_props)

        # Adicionar anotações e linhas verticais para 'Desnivel' sobre a linha "NovoZ"
        for dist, corte, z, desnivel, x, y in estacas_data:
            # Definir a cor com base no valor de 'Desnivel'
            if desnivel > 0:
                color = 'blue'
            elif desnivel < 0:
                color = 'red'
            else:
                color = 'black'
            
            # Adicionar a anotação com a cor correspondente e uma caixa ao redor do texto
            ax.annotate(f'{desnivel}', xy=(dist, corte), textcoords="offset points",
                        xytext=(0, 7), ha='center', va='bottom', color=color, weight='bold',
                        fontsize=8, bbox=dict(boxstyle="round,pad=0.3", facecolor='white', edgecolor="black", alpha=1))

            # Desenhar a linha vertical da linha "NovoZ" até o Eixo X
            ax.vlines(x=dist, ymin=0, ymax=corte, colors='grey', linestyles='dashed', linewidth=0.5)

        # Coletar todas as posições x onde as linhas verticais tocam o eixo X
        vertical_line_positions = list(estacas_distances)

        if talude_start_intersection:
            vertical_line_positions.append(talude_start_intersection[0])

        if talude_end_intersection:
            vertical_line_positions.append(talude_end_intersection[0])

        # Criar uma lista de xticks incluindo as posições das linhas verticais
        xticks = sorted(set(vertical_line_positions))

        # Formatar os rótulos dos xticks
        formatted_labels = [f"{tick:.0f}" for tick in xticks]

        # Configurar os xticks e seus rótulos
        ax.set_xticks(xticks)
        ax.set_xticklabels(formatted_labels, fontsize=8)

        # Personalizar os rótulos do eixo x
        for i, label in enumerate(ax.get_xticklabels()):
            if xticks[i] in vertical_line_positions:
                # Rótulos onde as linhas verticais tocam o eixo x em negrito e itálico
                label.set_fontweight('bold')
                label.set_fontstyle('italic')

        # Posicionar a legenda no canto superior esquerdo
        ax.legend(loc='upper left')
        ax.grid(axis='y', color='gray', alpha=0.5, linestyle='-', linewidth=0.4)

        # Configurar os ticks do eixo Y para variar a cada 1 metro
        ax.yaxis.set_major_locator(plt.MultipleLocator(1))

        # Definir uma altura fixa para as anotações em relação ao eixo y do gráfico
        altura_fixa_para_anotacoes = y_min1 + (y_max - y_min1) * 0.1 # 5% acima do limite inferior

        # Adicionar anotações para cada ponto na linha de "NovoZ"
        for dist, corte, z, desnivel in zip(estacas_distances, estacas_novoz, z_values, desnivels):
            # Determinar a cor da anotação com base no valor de "Desnível"
            if desnivel < 0:
                cor_anotacao = 'red'
            elif desnivel > 0:
                cor_anotacao = 'blue'
            else:
                cor_anotacao = 'black'

            # Anotação para "NovoZ" (Corte/Aterro)
            ax.annotate(f'{corte:.3f} m',
                        xy=(dist, altura_fixa_para_anotacoes),
                        xytext=(8, 0),
                        textcoords="offset points", ha='center', va='center', color=cor_anotacao, fontstyle='italic', fontweight='bold',
                        fontsize=8, rotation=90, bbox=dict(boxstyle="round,pad=0.3", facecolor='white', edgecolor=cor_anotacao))

            # Anotação para "Z" (Terreno Natural)
            ax.annotate(f'{z:.3f} m',
                        xy=(dist, altura_fixa_para_anotacoes),
                        xytext=(-10.5, 0),
                        textcoords="offset points", ha='left', va='center', color='black', fontstyle='italic', fontweight='bold',
                        fontsize=8, rotation=90, bbox=dict(boxstyle="round,pad=0.3", facecolor='white', edgecolor='orange'))

        # Adicionar rótulos representativos para "Z" e "NovoZ" alinhados com as anotações
        ax.text(0.042, (altura_fixa_para_anotacoes - y_min1) / (y_max - y_min1), 'Terreno Natural', ha='right', va='center',
                color='black', fontstyle='italic', fontweight='bold', fontsize=9, rotation=90,
                transform=ax.transAxes, bbox=dict(boxstyle="round,pad=0.3", facecolor='white', edgecolor='orange'))

        ax.text(0.06, (altura_fixa_para_anotacoes - y_min1) / (y_max - y_min1), 'Corte/Aterro', ha='right', va='center',
                color='blue', fontstyle='italic', fontweight='bold', fontsize=9, rotation=90,
                transform=ax.transAxes, bbox=dict(boxstyle="round,pad=0.5", facecolor='white', edgecolor='red'))

        # Exemplo de uso:
        fig = plt.gcf()
        self.adicionar_rosa_dos_ventos(fig, x_coords, y_coords)

        # Chama a função para adicionar o logotipo ao gráfico
        self.adicionar_logo(fig)

        plt.show()  # Exibe o gráfico

    def adicionar_rosa_dos_ventos(self, fig, x_coords, y_coords):
        """
        Adiciona uma rosa dos ventos ao gráfico.

        Parameters:
        - fig: a figura na qual a rosa dos ventos será desenhada.
        - x_coords: lista de coordenadas x do perfil.
        - y_coords: lista de coordenadas y do perfil.
        """
        # Defina o tamanho e a posição da Rosa dos Ventos
        radius = 0.035  # Raio do círculo (em unidades de figura)
        center = (0.50, 0.82)  # Posição do centro do círculo (em unidades de figura)

        # Desenha um círculo para a rosa dos ventos
        circle = Circle(center, radius, transform=fig.transFigure, edgecolor="black", facecolor="none", clip_on=False)
        fig.add_artist(circle)

        # Desenha linhas com setas para indicar as direções
        for angle in [0, 90, 180, 270]:
            x_start = center[0]
            y_start = center[1]
            x_end = center[0] + 1.0 * radius * np.cos(np.radians(angle))
            y_end = center[1] + 1.0 * radius * np.sin(np.radians(angle))

            # Adiciona uma seta ao final da linha
            arrowprops = dict(arrowstyle="->", lw=1.5, mutation_scale=10)
            fig.add_artist(patches.FancyArrowPatch((x_start, y_start), (x_end, y_end),
                                                   connectionstyle="arc3,rad=0.0",
                                                   transform=fig.transFigure, clip_on=False, **arrowprops))

        # Calcula o ângulo com base nas coordenadas fornecidas
        delta_x = x_coords[-1] - x_coords[0]
        delta_y = y_coords[-1] - y_coords[0]
        angle = math.atan2(delta_y, delta_x)

        # Converte o ângulo de radianos para graus para facilitar a interpretação
        angle_degrees = math.degrees(angle)

        # Determina a direção principal com base no ângulo
        if -45 < angle_degrees <= 45:
            directions = ["E", "N", "W", "S"]
        elif 45 < angle_degrees <= 135:
            directions = ["N", "W", "S", "E"]
        elif -135 < angle_degrees <= -45:
            directions = ["S", "E", "N", "W"]
        else:
            directions = ["W", "N", "E", "S"]

        # Adiciona rótulos para as direções
        label_distance = 1.4  # Ajuste essa variável para mudar a distância dos rótulos ao círculo
        for i, label in enumerate(directions):
            angle = 90 * i
            x = center[0] + label_distance * radius * np.cos(np.radians(angle))
            y = center[1] + label_distance * radius * np.sin(np.radians(angle))
            fig.text(x, y, label, ha="center", va="center")

    def adicionar_logo(self, fig):
        """
        Função para adicionar o logotipo ao gráfico, se houver um logotipo selecionado.
        
        A imagem é redimensionada proporcionalmente dentro de uma caixa de 250x150 pixels.
        
        Parameters:
        - fig: O objeto Figure do matplotlib onde o logotipo será adicionado.
        """
        if hasattr(self, 'logo_path') and self.logo_path:
            try:
                # Carregar o logotipo
                logo = plt.imread(self.logo_path)

                # Tamanho máximo da caixa onde o logo deve ser ajustado
                max_width, max_height = 150, 75

                # Obtenha a largura e a altura da imagem original
                img_height, img_width = logo.shape[:2]

                # Calcular o fator de escala para largura e altura
                width_scale = max_width / img_width
                height_scale = max_height / img_height

                # Escolher o menor fator de escala para garantir que a imagem caiba na caixa
                scale_factor = min(width_scale, height_scale)

                # Cria uma instância de OffsetImage com o logotipo redimensionado
                imagebox = OffsetImage(logo, zoom=scale_factor)

                # Opções personalizadas para a caixa
                bboxprops = dict(boxstyle="round,pad=0.5", facecolor="white", edgecolor="black", linewidth=1.5)

                # Cria uma instância de AnnotationBbox para posicionar o logotipo
                ab = AnnotationBbox(imagebox, (0.975, 0.89), frameon=True, xycoords='figure fraction',
                                    box_alignment=(1, 1), bboxprops=bboxprops)

                # Adiciona o logotipo ao gráfico
                ax = plt.gca()  # Obtém o eixo atual
                ax.add_artist(ab)

            except Exception as e:
                self.plato_manager.mostrar_mensagem(f"Erro ao carregar o logotipo: {str(e)}", "Erro")
        else:
            self.plato_manager.mostrar_mensagem("Nenhum logotipo disponível para adicionar ao gráfico.", "Sucesso")

class ListDeleteButtonDelegate(QStyledItemDelegate):
    def __init__(self, parent=None):
        """
        Inicializa a interface e os componentes da classe, configura sinais,
        widgets e estados iniciais do diálogo ou objeto.
        """
        super(ListDeleteButtonDelegate, self).__init__(parent)
        self.parent = parent

    def paint(self, painter, option, index):
        """
        Personaliza a renderização dos itens do listWidget, incluindo botões de exclusão
        e estilos visuais customizados na interface.
        """
        if index.isValid():
            # Ignora o cabeçalho (primeiro item)
            if index.row() == 0:
                super(ListDeleteButtonDelegate, self).paint(painter, option, index)
                return

            # Desenha o fundo com base no estado
            if option.state & QStyle.State_Selected:
                painter.fillRect(option.rect, option.palette.highlight())
            elif option.state & QStyle.State_MouseOver:
                # Estado de hover (passar o mouse sobre o item)
                hover_color = QColor("#00ffff")  # Cor definida no styleSheet
                painter.fillRect(option.rect, hover_color)
            else:
                painter.fillRect(option.rect, option.palette.base())

            # Calcula as posições
            rect = option.rect
            icon_size = 10
            icon_margin = 4
            icon_rect = QRect(
                rect.left() + icon_margin,
                rect.top() + (rect.height() - icon_size) // 2,
                icon_size,
                icon_size)
            text_rect = QRect(
                icon_rect.right() + icon_margin,
                rect.top(),
                rect.width() - icon_size - 3 * icon_margin,
                rect.height())

            # Desenha o quadradinho com borda arredondada para exclusão
            painter.save()
            painter.setRenderHint(QPainter.Antialiasing)
            painter.setPen(QPen(QColor(0, 0, 255), 1))  # Cor da borda do quadrado
            painter.setBrush(QBrush(QColor(255, 0, 0, 200)))  # Fundo vermelho claro
            radius = 2  # Raio das bordas arredondadas
            painter.drawRoundedRect(icon_rect, radius, radius)  # Desenha o quadrado com bordas arredondadas

            # Desenha o "x" dentro do quadrado
            painter.setPen(QPen(QColor(255, 255, 255), 2))  # Cor e espessura do "x"
            painter.drawLine(icon_rect.topLeft() + QPoint(3, 3), icon_rect.bottomRight() - QPoint(3, 3))
            painter.drawLine(icon_rect.topRight() + QPoint(-3, 3), icon_rect.bottomLeft() + QPoint(3, -3))
            painter.restore()

            # Desenha o texto
            painter.save()
            # Ajusta a cor do texto com base no estado
            if option.state & QStyle.State_Selected:
                text_color = option.palette.highlightedText().color()
            else:
                text_color = option.palette.text().color()
            painter.setPen(text_color)
            font = painter.font()
            font.setPointSize(8)  # Ajusta o tamanho da fonte se necessário
            painter.setFont(font)
            text = index.data(Qt.DisplayRole)
            painter.drawText(text_rect, Qt.AlignVCenter | Qt.TextSingleLine, text)
            painter.restore()

    @staticmethod
    def remove_layer_and_empty_groups(layer_id, group_names=("Estaqueamentos", "Segmentos", "Taludes")):
        """
        Remove a camada pelo id e, caso qualquer grupo listado fique vazio,
        remove o grupo correspondente.
        """
        from qgis.core import QgsProject
        project = QgsProject.instance()
        layer = project.mapLayer(layer_id)
        if layer:
            project.removeMapLayer(layer_id)
            root = project.layerTreeRoot()
            for group_name in group_names:
                group = root.findGroup(group_name)
                if group and len(group.children()) == 0:
                    parent = group.parent()
                    parent.removeChildNode(group)

    def editorEvent(self, event, model, option, index):
        """
        Trata eventos de interação do usuário nos itens do listWidget,
        como cliques em botões de exclusão ou seleção.
        """
        if index.row() == 0:
            return False  # Ignora eventos no cabeçalho

        if event.type() == QEvent.MouseButtonRelease:
            # Calcula as posições
            rect = option.rect
            icon_size = 10
            icon_margin = 4
            icon_rect = QRect(
                rect.left() + icon_margin,
                rect.top() + (rect.height() - icon_size) // 2,
                icon_size,
                icon_size)
            if icon_rect.contains(event.pos()):
                # Remove a camada correspondente do QGIS e do listWidget
                layer_id = index.data(Qt.UserRole)  # Obtém o ID da camada armazenado no UserRole do item
                # Delete a camada, e caso seja a última delete o grupo
                ListDeleteButtonDelegate.remove_layer_and_empty_groups(layer_id)
                # Não é necessário remover o item manualmente; o slot de atualização cuidará disso

                iface.mapCanvas().refresh()  # Atualiza o canvas imediatamente

                return True
        return super(ListDeleteButtonDelegate, self).editorEvent(event, model, option, index)