from qgis.core import QgsProject, QgsMapLayer, Qgis, QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsLayerTreeLayer, QgsLayerTree, QgsSingleBandPseudoColorRenderer, QgsRasterLayer, QgsRasterPipe, QgsRasterFileWriter,  QgsMapSettings, QgsMapRendererCustomPainterJob, QgsLayout, QgsLayoutItemLegend, QgsLayoutSize, QgsUnitTypes, QgsApplication
from qgis.PyQt.QtWidgets import QInputDialog, QStyledItemDelegate, QMenu, QDialog, QVBoxLayout, QPushButton,  QFileDialog, QStyle, QStyleOptionViewItem, QMessageBox, QProgressBar, QToolTip,  QRadioButton, QFrame, QDialogButtonBox, QSizePolicy
from qgis.PyQt.QtGui import QStandardItemModel, QStandardItem, QPainter, QColor, QFont, QImage, QCursor
from qgis.PyQt.QtCore import Qt, QRect, QEvent, QCoreApplication, QSettings, QSize, QRectF, QObject
from qgis.gui import QgsProjectionSelectionDialog
from qgis import utils as qgis_utils
from qgis.utils import iface
from osgeo import gdal, osr
from ezdxf import colors
import numpy as np
import processing
import simplekml
import tempfile
import zipfile
import shutil
import ezdxf
import time
import os

class UiManagerR:
    """
    Gerencia a interface do usuário, interagindo com um QTreeView para listar e gerenciar camadas de rasters no QGIS.
    """
    def __init__(self, iface, dialog):
        """
        Inicializa a instância da classe UiManagerO, responsável por gerenciar a interface do usuário
        que interage com um QTreeView para listar e gerenciar camadas de rasters no QGIS.

        :param iface: Interface do QGIS para interagir com o ambiente.
        :param dialog: Diálogo ou janela que esta classe gerenciará.

        Funções e Ações Desenvolvidas:
        - Configuração inicial das variáveis de instância.
        - Associação do modelo de dados com o QTreeView.
        - Inicialização da configuração do QTreeView.
        - Seleção automática da última camada no QTreeView.
        - Conexão dos sinais do QGIS e da interface do usuário com os métodos correspondentes.
        """
        # Salva as referências para a interface do QGIS e o diálogo fornecidos
        self.iface = iface
        self.dlg = dialog

        # Aplica estilos CSS para aprimorar a interação visual com os itens do QTreeView
        self.dlg.treeViewListaRaster.setStyleSheet("""
            QTreeView::item:hover:!selected {
                background-color: #def2fc;
            }
            QTreeView::item:selected {
            }""")

        # Cria e configura o modelo de dados para o QTreeView
        self.treeViewModel = QStandardItemModel()
        self.dlg.treeViewListaRaster.setModel(self.treeViewModel)

        # Atualiza a visualização da lista de camadas no QTreeView
        self.atualizar_treeView_lista_raster()

        # Chama atualização dos botões após toda modificação
        self.atualizar_estado_botoes()

        # Adiciona o filtro de eventos ao treeView
        self.tree_view_event_filter = TreeViewEventFilter(self)
        self.dlg.treeViewListaRaster.viewport().installEventFilter(self.tree_view_event_filter)

        # Inicia a conexão de sinais para tratar a mudança de nome das camadas no projeto
        self.connect_name_changed_signals()

        # Conecta os sinais do QGIS e da interface do usuário para sincronizar ações e eventos
        self.connect_signals()

    def connect_signals(self):
        """
        Conecta os sinais do QGIS e do QTreeView para sincronizar a interface com o estado atual do projeto.
        Este método garante que mudanças no ambiente do QGIS se reflitam na interface do usuário e que ações na
        interface desencadeiem reações apropriadas no QGIS.

        Funções e Ações Desenvolvidas:
        - Conexão com sinais de adição e remoção de camadas para atualizar a visualização da árvore.
        - Sincronização do modelo do QTreeView com mudanças de seleção e propriedades das camadas no QGIS.
        - Tratamento da mudança de nome das camadas para manter consistência entre a interface e o estado interno.
        """
        # Conecta sinais do QGIS para lidar com a adição e remoção de camadas no projeto
        QgsProject.instance().layersAdded.connect(self.layers_added)

        # Conecta o evento de mudança em um item do QTreeView para atualizar a visibilidade da camada no QGIS
        self.treeViewModel.itemChanged.connect(self.on_item_changed)

        # Sincroniza o estado das camadas no QGIS com o checkbox do QTreeView sempre que as camadas do mapa mudam
        self.iface.mapCanvas().layersChanged.connect(self.sync_from_qgis_to_treeview)

        # Conecta mudanças na seleção do QTreeView para atualizar a camada ativa no QGIS
        self.dlg.treeViewListaRaster.selectionModel().selectionChanged.connect(self.on_treeview_selection_changed)

        # Sincroniza a seleção no QGIS com a seleção no QTreeView quando a camada ativa no QGIS muda
        self.iface.currentLayerChanged.connect(self.on_current_layer_changed)

        # Define e aplica um delegado personalizado para customização da exibição de itens no QTreeView
        self.dlg.treeViewListaRaster.setItemDelegate(CustomDelegate(self.dlg.treeViewListaRaster))

        # Configura a política de menu de contexto para permitir menus personalizados em cliques com o botão direito
        self.dlg.treeViewListaRaster.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.dlg.treeViewListaRaster.customContextMenuRequested.connect(self.open_context_menu)

        # Conecte o botão pushButtonRasterKML à função export_raster_to_kml
        self.dlg.pushButtonRasterKML.clicked.connect(self.export_raster_to_kml)

        # Conecte o botão pushButtonRasterDXF à função export_raster_to_dxf
        self.dlg.pushButtonRasterDXF.clicked.connect(self.exportar_raster_dxf)

        # Conectando o botão pushButtonFecharR à função que fecha o diálogo
        self.dlg.pushButtonFecharR.clicked.connect(self.close_dialog)

        # Conecta para abrir o Qgis2threejs 
        self.dlg.pushButton3DRaster.clicked.connect(self.abrir_qgis2threejs)

        # seleção via mouse (funciona mesmo com 1 raster)
        self.dlg.treeViewListaRaster.clicked.connect(lambda _: self.on_treeview_selection_changed(None, None))

        # Ação de controle ao remover Camadas de Rasters
        QgsProject.instance().layersRemoved.connect(self.on_layers_removed)

        # Conecta o botão para Zoom
        self.dlg.pushButtonVisualizarR.clicked.connect(self.visualizar_raster_selecionado)

        # Conectar o botão de renomear à função de renomear a camada
        self.dlg.pushButtonRenomeR.clicked.connect(self.renomear_camada_selecionada)

        # Conectar o botão de deletar à função de remover a camada
        self.dlg.pushButtonDelR.clicked.connect(self.remover_camada_selecionada)

        # Conectar o botão de salvar o raster
        self.dlg.pushButtonSalvarRaster.clicked.connect(self.salvar_raster_selecionado)

        # Conecta o botão de reprojetar
        self.dlg.pushButtonReprojetarR.clicked.connect(self.reprojetar_raster)

        # Conectar para clonar a camada
        self.dlg.pushButtonClonarRaster.clicked.connect(self.clonar_raster_selecionado)

        # Abrir diálogo de adição de camadas de imagens
        self.dlg.pushButtonAbrirR.clicked.connect(self.abrir_imagem_raster)

        # Salvar múltiplos rasters
        self.dlg.pushButtonSalvaMultiplosR.clicked.connect(self.abrir_salvar_multiplos_rasters)

    def abrir_imagem_raster(self):
        """
        Abre um diálogo para selecionar uma imagem raster (como .tif, .asc, .png, .jpg)
        e adiciona a camada ao projeto QGIS.
        """
        filtros = ("Imagens Raster (*.tif *.tiff *.asc *.jpg *.jpeg *.png);;"
                   "Todos os Arquivos (*)")

        file_path, _ = QFileDialog.getOpenFileName(self.dlg, "Abrir Imagem Raster", "", filtros)

        if not file_path:
            return

        nome_camada = os.path.splitext(os.path.basename(file_path))[0]
        layer = QgsRasterLayer(file_path, nome_camada)

        if not layer.isValid():
            self.mostrar_mensagem("Falha ao carregar a imagem raster.\nVerifique se o formato é suportado.", "Erro")
            return

        QgsProject.instance().addMapLayer(layer)
        self.mostrar_mensagem(f"Imagem raster '{nome_camada}' adicionada com sucesso!", "Sucesso")

    def abrir_salvar_multiplos_rasters(self):
        """
        Abre um diálogo com:
        - "Salvar todas as Camadas" (default)
        - "Salvar apenas as Camadas Visíveis"

        Ao confirmar (OK):
        - pede uma pasta (lembra via QSettings)
        - salva as camadas conforme a opção
        - mostra barra de progresso contando arquivos
        """
        dlg, rb_todas, rb_visiveis = self._criar_dialogo_salvar_multiplos_rasters()
        if dlg.exec() != QDialog.DialogCode.Accepted:
            return

        apenas_visiveis = rb_visiveis.isChecked()

        layers = self._coletar_camadas_raster_treeview(apenas_visiveis=apenas_visiveis)
        if not layers:
            self.mostrar_mensagem("Nenhuma camada raster para salvar.", "Aviso")
            return

        pasta = self._selecionar_pasta_saida_batch()
        if not pasta:
            return

        progressBar, progressMsg = self.iniciar_progress_bar(len(layers))

        ok_count = 0
        falhas = []

        for i, layer in enumerate(layers, start=1):
            try:
                path_saida = self._gerar_caminho_saida_unico(pasta, layer.name(), ext=".tif")

                # Local vs serviço
                if layer.providerType().lower() in ("wms", "xyz", "arcgismapserver", "arcgisfeatureserver"):
                    ok = self._salvar_raster_servico_renderizado(layer, path_saida)
                else:
                    ok = self._salvar_raster_bruto(layer, path_saida)

                if ok:
                    ok_count += 1
                else:
                    falhas.append(layer.name())

            except Exception as e:
                falhas.append(f"{layer.name()} ({e})")

            progressBar.setValue(i)
            QCoreApplication.processEvents()  # mantém UI “respirando”

        # remove o widget de progresso (sem limpar toda a messageBar)
        try:
            self.iface.messageBar().popWidget(progressMsg)
        except Exception:
            pass

        if falhas:
            self.mostrar_mensagem(f"Salvamento concluído com falhas: {ok_count}/{len(layers)}.\n"
                f"Falharam: {', '.join(falhas[:8])}" + ("..." if len(falhas) > 8 else ""), "Erro", duracao=3, caminho_pasta=pasta)
        else:
            self.mostrar_mensagem(f"Salvamento concluído: {ok_count}/{len(layers)} arquivos.", "Sucesso", duracao=3, caminho_pasta=pasta)

    def _criar_dialogo_salvar_multiplos_rasters(self):
        """
        Cria o diálogo solicitado:
        - QFrame (Box + Raised)
        - 2 RadioButtons (um abaixo do outro)
        - Botões padrão OK/Cancelar centralizados e ocupando a largura do diálogo
        """
        dlg = QDialog(self.dlg)
        dlg.setWindowTitle("Salvar múltiplos rasters")

        layout = QVBoxLayout(dlg)

        frame = QFrame(dlg)
        frame.setFrameShape(QFrame.Shape.Box)
        frame.setFrameShadow(QFrame.Shadow.Raised)

        frame_layout = QVBoxLayout(frame)

        rb_todas = QRadioButton("Salvar todas as Camadas", frame)
        rb_visiveis = QRadioButton("Salvar apenas as Camadas Visíveis", frame)
        rb_todas.setChecked(True)

        frame_layout.addWidget(rb_todas)
        frame_layout.addWidget(rb_visiveis)

        layout.addWidget(frame)

        buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, dlg)

        # ✅ centraliza os botões e faz a "barra" ocupar toda a largura até as bordas do layout
        buttonBox.setCenterButtons(True)
        buttonBox.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)

        buttonBox.accepted.connect(dlg.accept)
        buttonBox.rejected.connect(dlg.reject)

        layout.addWidget(buttonBox)

        return dlg, rb_todas, rb_visiveis

    def _selecionar_pasta_saida_batch(self) -> str | None:
        """
        Abre um diálogo de pasta e guarda em QSettings a última pasta.
        Reutiliza a mesma chave 'lastDir' que você já usa em escolher_local_para_salvar().
        """
        settings = QSettings()
        lastDir = settings.value("lastDir", "")

        pasta = QFileDialog.getExistingDirectory(self.dlg, "Selecione a pasta de destino", lastDir)
        if not pasta:
            return None

        settings.setValue("lastDir", pasta)
        return pasta

    def _coletar_camadas_raster_treeview(self, *, apenas_visiveis: bool) -> list:
        """
        Pega as camadas raster listadas no treeViewModel.
        Se 'apenas_visiveis' for True, filtra pelo checkState (visibilidade).
        """
        layers = []
        for row in range(self.treeViewModel.rowCount()):
            item = self.treeViewModel.item(row)
            if not item:
                continue

            if apenas_visiveis and item.checkState() != Qt.CheckState.Checked:
                continue

            layer_id = item.data(Qt.ItemDataRole.UserRole)
            layer = QgsProject.instance().mapLayer(layer_id)

            if isinstance(layer, QgsRasterLayer) and layer.isValid():
                layers.append(layer)

        return layers

    def _gerar_caminho_saida_unico(self, pasta: str, base_name: str, *, ext: str = ".tif") -> str:
        """
        Gera um caminho único na pasta (evita sobrescrever):
        Nome.tif, Nome_1.tif, Nome_2.tif...
        """
        safe = self._sanitizar_nome_arquivo(base_name)
        path = os.path.join(pasta, safe + ext)

        i = 1
        while os.path.exists(path):
            path = os.path.join(pasta, f"{safe}_{i}{ext}")
            i += 1

        return path

    def _sanitizar_nome_arquivo(self, name: str) -> str:
        """
        Remove caracteres ruins para nome de arquivo no Windows.
        """
        if not name:
            return "raster"
        permitido = " _-()[]{}."
        out = []
        for ch in name:
            if ch.isalnum() or ch in permitido:
                out.append(ch)
            else:
                out.append("_")
        safe = "".join(out).strip()
        return safe or "raster"

    def _salvar_raster_servico_renderizado(self, layer: QgsRasterLayer, path_saida: str) -> bool:
        """
        Para rasters de serviço (WMS/XYZ/ArcGIS):
        - Renderiza a camada isoladamente usando o renderer do QGIS
        - Grava um GeoTIFF RGB georreferenciado na extensão da camada

        Obs: serviço não tem "pixel grid original" para copiar 1:1, então isso é um “snapshot georreferenciado”.
        """
        try:
            extent = layer.extent()
            if extent.isEmpty():
                self.mostrar_mensagem(f"Extensão vazia: {layer.name()}", "Erro")
                return False

            # usamos um tamanho baseado no canvas para ter qualidade decente
            canvas = self.iface.mapCanvas()
            w = max(256, canvas.size().width())
            h = max(256, canvas.size().height())

            crs_out = layer.crs() if layer.crs().isValid() else QgsProject.instance().crs()

            ms = QgsMapSettings()
            ms.setLayers([layer])
            ms.setExtent(extent)
            ms.setDestinationCrs(crs_out)
            ms.setOutputSize(QSize(w, h))

            img = QImage(w, h, QImage.Format.Format_ARGB32_Premultiplied)
            img.fill(Qt.GlobalColor.white)

            painter = QPainter(img)
            job = QgsMapRendererCustomPainterJob(ms, painter)
            job.start()
            job.waitForFinished()
            painter.end()

            # QImage -> numpy (RGB)
            img2 = img.convertToFormat(QImage.Format.Format_RGBA8888)
            ptr = img2.bits()
            ptr.setsize(img2.byteCount())
            arr = np.array(ptr).reshape((h, w, 4))[:, :, :3]
            arr = np.moveaxis(arr, 2, 0)  # [3, rows, cols]

            # GeoTIFF
            drv = gdal.GetDriverByName("GTiff")
            ds = drv.Create(path_saida, w, h, 3, gdal.GDT_Byte, options=["COMPRESS=LZW"])
            for b in range(3):
                ds.GetRasterBand(b + 1).WriteArray(arr[b])

            gt = (extent.xMinimum(), extent.width() / w, 0, extent.yMaximum(), 0, -extent.height() / h)
            ds.SetGeoTransform(gt)

            srs = osr.SpatialReference()
            srs.ImportFromWkt(crs_out.toWkt())
            ds.SetProjection(srs.ExportToWkt())
            ds = None

            return True

        except Exception as e:
            self.mostrar_mensagem(f"Falha ao salvar serviço '{layer.name()}': {e}", "Erro")
            return False

    def clonar_raster_selecionado(self):
        """
        Clona a camada raster selecionada no treeViewListaRaster, criando uma camada idêntica
        (inclusive estilo) como camada temporária/memória no QGIS.
        Não funciona para rasters de provedores (WMS, XYZ etc).
        """
        idxs = self.dlg.treeViewListaRaster.selectedIndexes()
        if not idxs:
            self.mostrar_mensagem("Nenhuma camada selecionada.", "Aviso")
            return

        layer_id = self.treeViewModel.itemFromIndex(idxs[0]).data(Qt.ItemDataRole.UserRole)
        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer or layer.type() != QgsMapLayer.LayerType.RasterLayer:
            self.mostrar_mensagem("Selecione uma camada raster válida.", "Erro")
            return

        # Verifica se é camada de provedor (serviço online)
        provider_types_nao_clonaveis = ("wms", "xyz", "arcgismapserver", "arcgisfeatureserver")
        if layer.providerType().lower() in provider_types_nao_clonaveis:
            self.mostrar_mensagem(
                "Não é possível clonar rasters de provedores online (WMS, XYZ, ArcGIS, etc).\n"
                "Somente rasters locais (arquivos no disco) podem ser clonados.","Aviso")
            return

        # Gera nome do clone
        clone_name = f"{layer.name()}_Clone"

        # Cria arquivo temporário VRT
        tmp_dir = tempfile.mkdtemp()
        clone_path = os.path.join(tmp_dir, f"{clone_name}.vrt")

        # Usa GDAL para gerar VRT (virtual raster)
        ds = gdal.Open(layer.source())
        if ds is None:
            self.mostrar_mensagem(
                "Falha ao abrir o arquivo raster original.\n"
                "Verifique se o arquivo existe e está acessível.","Erro")
            shutil.rmtree(tmp_dir)
            return

        gdal.GetDriverByName("VRT").CreateCopy(clone_path, ds)

        # Cria nova camada raster no QGIS usando o arquivo VRT
        clone_layer = QgsRasterLayer(clone_path, clone_name, "gdal")

        if clone_layer.isValid():
            # Copia o estilo da camada original (inclui simbologia, colormap, etc.)
            clone_layer.setRenderer(layer.renderer().clone())
            clone_layer.setCrs(layer.crs())
            QgsProject.instance().addMapLayer(clone_layer)
            self.mostrar_mensagem("Clone criado com sucesso!", "Sucesso")
        else:
            self.mostrar_mensagem("Erro ao criar o clone!", "Erro")
            shutil.rmtree(tmp_dir)  # Limpa temp

    def reprojetar_raster(self):
        """
        Permite ao usuário reprojetar o raster selecionado usando o GDAL (warp/reproject),
        método Bilinear.

        Regras:
        - Se o raster não tem SRC (CRS inválido), pede para definir antes de reprojetar.
        - Remove a duplicidade de 'NODATA' no dicionário de parâmetros.
        - Define o NODATA de saída assim:
            * se o raster já tiver NODATA na banda 1, reutiliza;
            * senão, usa -9999.0 como fallback.
        """
        # Seleciona camada raster
        idxs = self.dlg.treeViewListaRaster.selectedIndexes()
        if not idxs:
            self.mostrar_mensagem("Nenhuma camada selecionada", "Aviso")
            return

        layer_id = self.treeViewModel.itemFromIndex(idxs[0]).data(Qt.ItemDataRole.UserRole)
        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer or layer.type() != QgsMapLayer.LayerType.RasterLayer:
            self.mostrar_mensagem("Selecione uma camada raster válida.", "Erro")
            return

        # Se a camada não tem SRC, pede para definir
        if not layer.crs().isValid():
            crs = self.selecionar_crs_dialogo("Definir SRC para a camada")
            if not crs:
                self.mostrar_mensagem("SRC não definido!", "Erro")
                return
            layer.setCrs(crs)
            self.mostrar_mensagem("SRC definido. Agora escolha o SRC de destino.", "Informação")

        # Escolhe o SRC de destino (diálogo)
        crs_destino = self.selecionar_crs_dialogo("Selecione o SRC de destino")
        if not crs_destino:
            return

        # Decide NODATA de saída: usa o NODATA original (se existir); senão, -9999
        nodata_out = None
        prov = layer.dataProvider()
        try:
            # banda 1 (padrão)
            if prov and prov.sourceHasNoDataValue(1):
                v = prov.sourceNoDataValue(1)
                if v is not None:
                    nodata_out = float(v)
        except Exception:
            nodata_out = None

        if nodata_out is None:
            nodata_out = -9999.0

        # Executa o algoritmo de reprojeção GDAL (método Bilinear, RESAMPLING=1)
        params = {
            'INPUT': layer.source(),
            'SOURCE_CRS': layer.crs().authid(),
            'TARGET_CRS': crs_destino.authid(),
            'RESAMPLING': 1,  # Bilinear
            'NODATA': nodata_out,
            'TARGET_RESOLUTION': None,
            'OPTIONS': '',
            'DATA_TYPE': 0,
            'TARGET_EXTENT': None,
            'TARGET_EXTENT_CRS': None,
            'MULTITHREADING': False,
            'EXTRA': '',
            'OUTPUT': 'TEMPORARY_OUTPUT'}

        resultado = processing.run("gdal:warpreproject", params)
        output_raster = resultado.get('OUTPUT')

        if output_raster:
            rlayer = QgsRasterLayer(output_raster, f"{layer.name()}_reprojetado", "gdal")
            if rlayer.isValid():
                QgsProject.instance().addMapLayer(rlayer)
                self.mostrar_mensagem("Raster reprojetado com sucesso!", "Sucesso")
            else:
                self.mostrar_mensagem("Falha ao carregar raster reprojetado!", "Erro")
        else:
            self.mostrar_mensagem("Erro na reprojeção GDAL!", "Erro")

    def selecionar_crs_dialogo(self, titulo="Selecionar SRC"):
        """
        Abre o diálogo padrão do QGIS para seleção de SRC e retorna um QgsCoordinateReferenceSystem.
        """
        dlg = QgsProjectionSelectionDialog()
        dlg.setWindowTitle(titulo)
        if dlg.exec():
            epsg = dlg.crs().authid()
            return QgsCoordinateReferenceSystem(epsg)
        return None

    def salvar_raster_selecionado(self):
        """
        Salva o raster selecionado em GeoTIFF.
          • Raster local → grava dados brutos no próprio SRC da camada.
          • Raster de serviço (WMS/XYZ) → grava visualização georreferenciada no SRC do projeto.
          • Se camada não tiver SRC, usa SRC do projeto e avisa o usuário.
        """
        idxs = self.dlg.treeViewListaRaster.selectedIndexes()
        if not idxs:
            self.mostrar_mensagem("Nenhuma camada selecionada", "Erro")
            return

        layer_id = self.treeViewModel.itemFromIndex(idxs[0]).data(Qt.ItemDataRole.UserRole)
        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer or layer.type() != QgsMapLayer.LayerType.RasterLayer:
            self.mostrar_mensagem("Camada inválida", "Erro")
            return

        path_saida = self.escolher_local_para_salvar(layer.name() + ".tif",
                                                     "GeoTIFF (*.tif)")
        if not path_saida:
            return

        if layer.providerType() in ("wms", "xyz", "arcgismapserver", "arcgisfeatureserver"):
            ok = self.salvar_visualizacao_canvas_geotiff(path_saida)
        else:
            ok = self._salvar_raster_bruto(layer, path_saida)

        if ok:
            self.mostrar_mensagem("Raster salvo com sucesso!", "Sucesso",
                                  caminho_arquivo=path_saida,
                                  caminho_pasta=os.path.dirname(path_saida))

    def _salvar_raster_bruto(self, layer: QgsRasterLayer, path_saida: str) -> bool:
        """
        Salva um raster local (não-serviço) como um arquivo GeoTIFF bruto, mantendo o SRC original da camada.
        
        Detalhamento do processo:
        1. Determina o sistema de referência de coordenadas (SRC) de saída:
           - Usa o SRC da camada, se estiver definido e válido.
           - Se o SRC não estiver definido, utiliza o SRC do projeto e informa o usuário.
        2. Cria um QgsRasterPipe e clona o provedor de dados do raster:
           - O QgsRasterPipe permite conectar as etapas de leitura, renderização e gravação dos dados raster.
        3. Instancia o QgsRasterFileWriter para escrever o arquivo GeoTIFF no caminho informado.
        4. Escreve o raster no arquivo de saída usando as dimensões, extensão espacial e SRC determinados.
        5. Se houver erro na escrita, informa ao usuário e retorna False.
        6. Se tudo ocorrer normalmente, retorna True.
        
        Parâmetros:
        - layer: QgsRasterLayer, camada raster a ser salva.
        - path_saida: str, caminho completo do arquivo GeoTIFF a ser criado.
        
        Retorno:
        - True se o raster foi salvo com sucesso.
        - False se houve qualquer falha no processo.
        """
        crs_out = layer.crs() if layer.crs().isValid() else QgsProject.instance().crs()
        if not layer.crs().isValid():
            self.mostrar_mensagem("SRC da camada não definido – usando SRC do projeto.", "Erro", duracao=4)

        pipe = QgsRasterPipe()
        # Clone apenas o provider: NÃO inclua o renderer!
        if not pipe.set(layer.dataProvider().clone()):
            self.mostrar_mensagem("Falha ao clonar provider", "Erro")
            return False

        # NÃO use pipe.set(layer.renderer().clone())

        writer = QgsRasterFileWriter(path_saida)
        res = writer.writeRaster(pipe, layer.width(), layer.height(), layer.extent(), crs_out)

        if res != QgsRasterFileWriter.NoError:
            self.mostrar_mensagem("Erro ao gravar GeoTIFF", "Erro")
            return False

        return True

    def salvar_visualizacao_canvas_geotiff(self, path_saida: str) -> bool:
        """
        Salva a visualização atual do canvas em GeoTIFF georreferenciado.
        Retorna True se tudo correr bem, False caso contrário.
        """
        try:
            canvas = self.iface.mapCanvas()
            rect   = canvas.extent()
            width  = canvas.size().width()
            height = canvas.size().height()
            crs    = QgsProject.instance().crs()

            # Renderiza o canvas em imagem RGBA
            img = QImage(width, height, QImage.Format.Format_ARGB32)
            img.fill(Qt.GlobalColor.white)
            painter = QPainter(img)
            canvas.render(painter)
            painter.end()

            # Converte QImage → numpy (RGB)
            img   = img.convertToFormat(QImage.Format.Format_RGBA8888)
            ptr   = img.bits()
            ptr.setsize(img.byteCount())
            arr   = np.array(ptr).reshape((height, width, 4))[:, :, :3]
            arr   = np.moveaxis(arr, 2, 0)           # [3, rows, cols]

            # Grava GeoTIFF
            drv = gdal.GetDriverByName('GTiff')
            ds  = drv.Create(path_saida, width, height, 3, gdal.GDT_Byte,
                             options=['COMPRESS=LZW'])
            for b in range(3):
                ds.GetRasterBand(b+1).WriteArray(arr[b])

            gt = (rect.xMinimum(), rect.width()/width, 0, rect.yMaximum(), 0, -rect.height()/height)
            ds.SetGeoTransform(gt)

            srs = osr.SpatialReference()
            srs.ImportFromWkt(crs.toWkt())
            ds.SetProjection(srs.ExportToWkt())
            ds = None  # fecha

            self.mostrar_mensagem("Visualização do canvas salva em GeoTIFF!", "Sucesso", caminho_arquivo=path_saida, caminho_pasta=os.path.dirname(path_saida))
            return True

        except Exception as e:
            self.mostrar_mensagem(f"Falha ao salvar GeoTIFF: {e}", "Erro")
            return False

    def remover_camada_selecionada(self):
        """
        Remove a camada raster selecionada no treeViewListaRaster sem perguntar nada ao usuário.
        """
        # Obtém os índices selecionados no treeView de rasters
        selected_indexes = self.dlg.treeViewListaRaster.selectedIndexes()
        if selected_indexes:
            selected_index = selected_indexes[0]
            # Usa o papel UserRole para pegar o ID real da camada
            layer_id = selected_index.model().itemFromIndex(selected_index).data(Qt.ItemDataRole.UserRole)
            layer_to_remove = QgsProject.instance().mapLayer(layer_id)
            if layer_to_remove:
                # Remove a camada do projeto QGIS sem qualquer confirmação ou salvamento
                QgsProject.instance().removeMapLayer(layer_id)
                self.atualizar_treeView_lista_raster()
                self.iface.mapCanvas().refresh()

    def renomear_camada_selecionada(self):
        """
        Permite ao usuário renomear uma camada de Raster selecionado no treeViewListaRaster,
        garantindo unicidade do nome no projeto.

        Fluxo:
        - Obtém o índice selecionado.
        - Recupera a camada associada ao item (usando Qt.UserRole).
        - Exibe um diálogo para o usuário digitar o novo nome.
        - Garante que o nome será único (ajusta se necessário).
        - Atualiza o nome da camada no QGIS e o texto do item no treeView.
        """
        # Obtém os índices atualmente selecionados no treeView de raster
        selected_indexes = self.dlg.treeViewListaRaster.selectedIndexes()
        if selected_indexes:
            # Pega o primeiro índice selecionado
            selected_index = selected_indexes[0]
            # Recupera o ID da camada usando o papel UserRole (garante que é o ID real da camada QGIS)
            layer_id = selected_index.model().itemFromIndex(selected_index).data(Qt.ItemDataRole.UserRole)
            # Busca a camada correspondente no projeto QGIS
            selected_layer = QgsProject.instance().mapLayer(layer_id)
            if selected_layer:
                # Exibe um diálogo para o usuário digitar o novo nome (pré-preenchido com o nome atual)
                novo_nome, ok = QInputDialog.getText(
                    self.dlg,
                    "Renomear Camada",
                    "Digite o novo nome da camada:",
                    text=selected_layer.name())
                # Se o usuário confirmou e digitou algo
                if ok and novo_nome:
                    # Gera um nome único para evitar duplicidade no projeto
                    novo_nome = self.gerar_nome_unico(novo_nome, selected_layer.id())
                    # Renomeia a camada no QGIS (isso pode disparar sinais que atualizam a interface automaticamente)
                    selected_layer.setName(novo_nome)
                    # Em vez de tentar alterar diretamente o item do modelo (risco de crash), reconstrói o treeView do zero
                    self.atualizar_treeView_lista_raster()

    def gerar_nome_unico(self, base_nome, current_layer_id):
        """
        Gera um nome único para uma camada raster dentro do projeto QGIS,
        assegurando que não haja conflitos de nomeação entre as camadas raster.

        Parâmetros:
        - base_nome (str): O nome base proposto para a camada.
        - current_layer_id (str): O ID da camada que está sendo renomeada, usado para excluir seu nome atual da verificação de duplicidade.

        Funcionalidades:
        - Cria um dicionário dos nomes de todas as camadas raster existentes no projeto QGIS, excluindo a camada que está sendo renomeada.
        - Verifica se o nome base já existe entre as camadas raster. Se não existir, retorna o nome base como válido.
        - Se o nome base já existir, gera variações do nome acrescentando um sufixo numérico (ex: "Nome_1", "Nome_2") até encontrar um nome não utilizado.
        - Retorna o novo nome único gerado.
        """
        # Restringe apenas para camadas raster
        existing_names = {layer.name(): layer.id()
            for layer in QgsProject.instance().mapLayers().values()
            if (layer.id() != current_layer_id and
                layer.type() == QgsMapLayer.LayerType.RasterLayer)}  # Filtro para rasters

        # Se o nome proposto não existe, retorna ele direto
        if base_nome not in existing_names:
            return base_nome
        else:
            # Gera nomes incrementando um sufixo até ser único
            i = 1
            novo_nome = f"{base_nome}_{i}"
            while novo_nome in existing_names:
                i += 1
                novo_nome = f"{base_nome}_{i}"
            return novo_nome

    def atualizar_estado_botoes(self):
        """
        Atualiza o estado (habilitado/desabilitado) dos botões da interface gráfica conforme a presença de camadas no treeView de Rasters.

        Detalhamento do Processo:
        1. Verifica se o modelo associado ao treeViewListaRaster está vazio, ou seja, sem nenhuma camada listada.
        2. Se o modelo estiver vazio, todos os botões que dependem de camada devem ser desabilitados para evitar operações inválidas.
        3. Caso contrário, os botões são habilitados, permitindo ao usuário interagir normalmente com as funções associadas a uma camada de raster.
        4. Os botões afetados incluem:
            - Deletar camada (`pushButtonDelR`)
            - Renomear camada (`pushButtonRenomeR`)
            - Tornar camada permanente (`pushButtonSalvarRaster`)
            - Salvar em múltiplos formatos (`pushButtonSalvaMultiplosR`)
            - Exportar para DXF (`pushButtonRasterDXF`)
            - Exportar para KML (`pushButtonRasterKML`)
            - Visualizar raster associados (`pushButtonVisualizarR`)
            - Vizzualizar 3D (`pushButton3DRaster`)
            - Reprojetar camada (`pushButtonReprojetarR`)
        5. Garante uma experiência segura e intuitiva, evitando erros por ações em contexto inválido.
        """
        # Verifica se o modelo do treeView está vazio
        modelo_vazio = self.dlg.treeViewListaRaster.model().rowCount() == 0

        # Atualiza o estado dos botões baseado na presença ou ausência de itens no modelo
        self.dlg.pushButtonDelR.setEnabled(not modelo_vazio)
        self.dlg.pushButtonRenomeR.setEnabled(not modelo_vazio)
        self.dlg.pushButtonSalvarRaster.setEnabled(not modelo_vazio)
        self.dlg.pushButtonSalvaMultiplosR.setEnabled(not modelo_vazio)
        self.dlg.pushButtonRasterDXF.setEnabled(not modelo_vazio)
        self.dlg.pushButtonRasterKML.setEnabled(not modelo_vazio)
        self.dlg.pushButtonVisualizarR.setEnabled(not modelo_vazio)
        self.dlg.pushButtonClonarRaster.setEnabled(not modelo_vazio)
        self.dlg.pushButtonReprojetarR.setEnabled(not modelo_vazio)
        self.dlg.pushButton3DRaster.setEnabled(not modelo_vazio)

    def visualizar_raster_selecionado(self):
        """
        Aproxima a visualização do mapa para a camada de raster selecionada no treeView.

        - Obtém a camada de raster atualmente selecionada.
        - Centraliza e ajusta a extensão do mapa para mostrar toda a camada.
        - Se não houver feições ou seleção, não faz nada.
        """
        index = self.dlg.treeViewListaRaster.currentIndex()  # Obtém o índice selecionado
        if not index.isValid():
            return  # Não há seleção

        # Obtém o ID da camada do UserRole (melhor do que usar .data() sem argumentos)
        layer_id = index.model().itemFromIndex(index).data(Qt.ItemDataRole.UserRole)
        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer or layer.extent().isEmpty():
            return  # Camada não encontrada ou sem feições

        # Garante que a camada esteja ativa (opcional, mas recomendado)
        self.iface.setActiveLayer(layer)
        # Ajusta o zoom para a extensão da camada ativa
        self.iface.zoomToActiveLayer()

    def configurar_tooltip(self, index):
        """
        Exibe um tooltip detalhado da camada raster selecionada.

        O que a função faz (passos):
        1. Converte o índice do treeView em item, obtendo o ID da camada.
        2. Recupera a camada no projeto e garante que é RasterLayer.
        3. Coleta atributos principais (SRC, bandas, colunas, linhas).
        4. Calcula a **área** do raster como:
             área = (cols * rows) * (resX * resY)
           • resX/resY = resolução do pixel nas unidades do SRC.
           • cols/rows = largura/altura em número de pixels.
        5. Formata todas as infos em texto amigável.
        6. Mostra o tooltip na posição atual do cursor.

        Nota: Se o SRC for geográfico (graus) o valor é em graus²,
              podendo não representar área real em m².
        """
        item = index.model().itemFromIndex(index)          # item selecionado
        layer_id = item.data(Qt.ItemDataRole.UserRole)                  # ID da camada
        layer = QgsProject.instance().mapLayer(layer_id)   # camada no projeto

        if layer and layer.type() == QgsMapLayer.LayerType.RasterLayer:
            # Informações básicas
            crs_desc   = layer.crs().description() if layer.crs().isValid() \
                         else "Sem Georreferenciamento"
            num_bandas = layer.bandCount()
            cols       = layer.width()                     # nº colunas  (= pixels X)
            rows       = layer.height()                    # nº linhas   (= pixels Y)

            # Resolução do pixel (unidades / pixel)
            #   • rasterUnitsPerPixelX/Y já retornam a resolução do provider
            #   • fallback: extent / dimensões
            resX = layer.rasterUnitsPerPixelX() or \
                   (layer.extent().width()  / cols)
            resY = layer.rasterUnitsPerPixelY() or \
                   (layer.extent().height() / rows)

            # Málculo da área
            pixel_area = abs(resX * resY)                  # área de 1 pixel
            area_total = cols * rows * pixel_area          # área total

            # unidade textual ─ metros², graus² etc.
            unidade = layer.crs().mapUnits().name \
                      if layer.crs().isValid() else ""

            # Monta texto do tooltip
            tooltip_text = (
                f"SRC: {crs_desc}\n"
                f"Bandas: {num_bandas}\n"
                f"Dimensões: {cols} x {rows} pixels\n"
                f"Área: {area_total:,.3f} {unidade}²")

            QToolTip.showText(QCursor.pos(), tooltip_text) # exibe tooltip

    def obter_tipo_de_raster(self, layer):
        """
        Retorna uma string que descreve o tipo de geometria da camada fornecida.

        A função obtém o tipo de geometria WKB (Well-Known Binary) da camada e converte esse tipo
        em uma string legível, como 'raster', etc.

        Parâmetros:
        - layer: Objeto QgsVectorLayer representando a camada de onde o tipo de ponto será extraído.

        Retorno:
        - tipo_raster (str): Uma string que descreve o tipo de geometria da camada.
        """
        if layer.type() == QgsMapLayer.LayerType.RasterLayer:
            # Aqui você pode personalizar o tipo conforme a origem do raster, se quiser
            return "Raster"
        return "Desconhecido"

    def on_layers_removed(self, removed_ids):
        """
        removed_ids → lista de strings (layer.id()) que saíram do projeto.
        Se algum deles pertencer a um raster listado no treeView,
        elimina somente a respectiva linha, sem tocar nos demais.
        """
        if self.treeViewModel.rowCount() == 0:
            return

        # percorre cada ID removido
        for rid in removed_ids:
            # procura a linha cujo Qt.UserRole == rid
            for row in range(self.treeViewModel.rowCount()):
                item = self.treeViewModel.item(row)
                if item and item.data(Qt.ItemDataRole.UserRole) == rid:
                    self.treeViewModel.removeRow(row)    # remove a linha
                    break                                # sai do for interno

        # Garante que sempre reste uma seleção válida
        model = self.dlg.treeViewListaRaster.model()
        if model.rowCount() > 0 and not self.dlg.treeViewListaRaster.currentIndex().isValid():
            # seleciona a linha que ficou na mesma posição do antigo row
            target_row = min(row, model.rowCount() - 1)
            idx = model.index(target_row, 0)
            self.dlg.treeViewListaRaster.setCurrentIndex(idx)
            self.dlg.treeViewListaRaster.scrollTo(idx)

        # Chama atualização dos botões após toda modificação
        self.atualizar_estado_botoes()

    def close_dialog(self):
        """
        Fecha o diálogo associado a este UiManagerR:
        """
        self.dlg.close()

    def on_treeview_selection_changed(self, selected, deselected):
        """
        Define a camada ativa no QGIS com base na seleção do TreeView.

        Funcionalidades:
        - Acessa os índices selecionados no TreeView.
        - Recupera o ID da camada associado ao item selecionado.
        - Define a camada correspondente como ativa no QGIS (iface.setActiveLayer).
        """
        # Obtém os índices selecionados no TreeView
        indexes = self.dlg.treeViewListaRaster.selectionModel().selectedIndexes()
        if indexes:
            # Recupera o ID da camada a partir do primeiro item selecionado
            layer_id = self.treeViewModel.itemFromIndex(indexes[0]).data(Qt.ItemDataRole.UserRole)
            # Obtém a camada no projeto QGIS através do ID
            layer = QgsProject.instance().mapLayer(layer_id)
            if layer:
                # Define a camada como ativa no QGIS
                self.iface.setActiveLayer(layer)

    def atualizar_treeView_lista_raster(self):
        """
        Atualiza o TreeView que lista as camadas de rasters no QGIS.

        Funcionalidades:
        1) Salva o ID da camada atualmente selecionada no TreeView, se houver.
        2) Limpa e reconstrói o modelo do TreeView:
           - Define o cabeçalho centralizado.
           - Itera sobre todas as camadas do projeto e adiciona ao modelo apenas as camadas rasters.
           - Para cada camada encontrada:
             * Cria um item com o nome da camada.
             * Torna o item checkable para controlar visibilidade.
             * Define flags para impedir edição direta do texto.
             * Armazena o ID único da camada no Qt.UserRole.
             * Ajusta o estado do checkbox de acordo com a visibilidade atual da camada no projeto.
             * Formata a fonte do item (itálico para memória, negrito para outras).
        3) Tenta restaurar a seleção anterior, buscando no modelo o item cujo ID corresponda ao salvo.
        4) Caso não haja seleção anterior ou a camada não exista mais, seleciona o primeiro item do modelo.
        5) Move o scroll para garantir que o item selecionado esteja visível.
        """
        # Guarda a linha selecionada ANTES de recriar o modelo
        current_row   = -1                     # -1 → nada selecionado
        current_index = self.dlg.treeViewListaRaster.currentIndex()
        if current_index.isValid():
            current_row = current_index.row()  # linha que estava selecionada

        # Reconstrói o modelo
        self.treeViewModel.clear()
        header_item = QStandardItem("Lista de Camadas de Rasters")
        header_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
        self.treeViewModel.setHorizontalHeaderItem(0, header_item)

        root = QgsProject.instance().layerTreeRoot()
        for layer in QgsProject.instance().mapLayers().values():
            if layer.type() == QgsMapLayer.LayerType.RasterLayer:
                item = QStandardItem(layer.name())
                item.setCheckable(True)
                # item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                item.setEditable(False)
                item.setData(layer.id(), Qt.ItemDataRole.UserRole)

                node = root.findLayer(layer.id())
                item.setCheckState(Qt.CheckState.Checked if node and node.isVisible()
                                               else Qt.CheckState.Unchecked)

                self.adjust_item_font(item, layer)
                self.treeViewModel.appendRow(item)

        # Decide qual linha selecionar DEPOIS da atualização
        model = self.dlg.treeViewListaRaster.model()
        row_count = model.rowCount()

        if row_count == 0:
            return  # não há nada para selecionar

        # Tenta selecionar a mesma linha anterior, ou a primeira
        target_row = current_row if 0 <= current_row < row_count else 0

        target_index = model.index(target_row, 0)
        self.dlg.treeViewListaRaster.setCurrentIndex(target_index)
        self.dlg.treeViewListaRaster.scrollTo(target_index)

    def on_current_layer_changed(self, layer):
        """
        Sincroniza a seleção do treeView com a camada ativa no QGIS.

        Funcionalidades:
        - Verifica se a nova camada ativa é uma camada de rasters.
        - Percorre o modelo do treeView para encontrar o item correspondente ao ID da nova camada.
        - Se encontrar, seleciona e rola até o item correspondente no treeView.
        - Se a camada ativa for raster → seleciona o item correspondente.
        - Caso contrário → limpa a seleção do treeView.
        • Quando a camada ativa for raster → seleciona o item correspondente.
        • Quando a camada ativa NÃO for raster → mantém a seleção atual.
          (Se por acaso não houver seleção, força a primeira linha.)
        """
        model = self.dlg.treeViewListaRaster.model()
        if model is None or model.rowCount() == 0:
            return  # não há rasters no treeView

        # Caso a camada ativa seja RASTER
        if layer and layer.type() == QgsMapLayer.LayerType.RasterLayer:
            layer_id = layer.id()

            # Percorre as linhas do modelo do treeView
            for row in range(model.rowCount()):
                item = model.item(row, 0)
                if item and item.data(Qt.ItemDataRole.UserRole) == layer_id:
                    index = model.indexFromItem(item)
                    # força a seleção mesmo que já estivesse selecionado
                    self.dlg.treeViewListaRaster.setCurrentIndex(index)
                    self.dlg.treeViewListaRaster.scrollTo(index)
                    break

        # Caso a camada ativa NÃO seja raster
        else:
            # Se por algum motivo não houver item selecionado,
            # garante que a primeira linha fique marcada.
            if not self.dlg.treeViewListaRaster.currentIndex().isValid():
                first_index = model.index(0, 0)
                self.dlg.treeViewListaRaster.setCurrentIndex(first_index)
                self.dlg.treeViewListaRaster.scrollTo(first_index)

    def adjust_item_font(self, item, layer):
        """
        Esta função ajusta a fonte do item no QTreeView com base no tipo de camada.
        Se a camada for temporária (dados em memória), ajusta a fonte para itálico.
        Se a camada for permanente, ajusta a fonte para negrito.

        Detalhes:
        - Cria um objeto QFont para ajustar a fonte do item.
        - Verifica se a camada é temporária usando o método isTemporary().
        - Se a camada for temporária, ajusta a fonte para itálico.
        - Se a camada for permanente, ajusta a fonte para negrito.
        - Aplica a fonte ajustada ao item no QTreeView.
        - Retorna o item com a fonte ajustada para uso posterior, se necessário.
        """
        # Cria um objeto QFont para ajustar a fonte do item
        fonte_item = QFont()

        # Verifica se a camada é temporária (dados em memória) e ajusta a fonte para itálico
        if layer.isTemporary():
            fonte_item.setItalic(True)
        # Se não for temporária, ajusta a fonte para negrito, indicando uma camada permanente
        else:
            fonte_item.setBold(True)

        # Aplica a fonte ajustada ao item no QTreeView
        item.setFont(fonte_item)

        # Retorna o item com a fonte ajustada para uso posterior se necessário
        return item

    def connect_name_changed_signals(self):
        """
        Conecta o sinal nameChanged de todas as camadas reais do projeto.
        Ignora grupos e nós sem camada associada.
        Evita conexões duplicadas.
        """
        root = QgsProject.instance().layerTreeRoot()

        for layerNode in root.findLayers():
            if isinstance(layerNode, QgsLayerTreeLayer):
                layer = layerNode.layer()
                if layer is not None:
                    try:
                        # desconecta primeiro para evitar duplicidade
                        layer.nameChanged.disconnect(self.on_layer_name_changed)
                    except TypeError:
                        # se não estava conectado, ignora o erro
                        pass

                    # conecta de forma segura
                    layer.nameChanged.connect(self.on_layer_name_changed)

    def layers_added(self, layers):
        """
        Responde ao evento de adição de camadas no projeto QGIS, atualizando a lista de camadas no QTreeView
        e conectando sinais de mudança de nome para camadas de rasters recém-adicionadas.

        Este método verifica cada camada adicionada para determinar se é uma camada de vetor de rasters.
        Se for, ele atualiza a lista de camadas no QTreeView e conecta o sinal de mudança de nome à função
        de callback apropriada.

        :param layers: Lista de camadas recém-adicionadas ao projeto.

        Funções e Ações Desenvolvidas:
        - Verificação do tipo e da geometria das camadas adicionadas.
        - Atualização da visualização da lista de camadas no QTreeView para incluir novas camadas de rasters.
        - Conexão do sinal de mudança de nome da camada ao método de tratamento correspondente.
        """
        # Itera por todas as camadas adicionadas
        for layer in layers:
            # Verifica se a camada é do tipo vetor e se sua geometria é de raster
            if layer.type() == QgsMapLayer.LayerType.RasterLayer:
                # Atualiza a lista de camadas no QTreeView
                self.atualizar_treeView_lista_raster()
                # Conecta o sinal de mudança de nome da nova camada ao método on_layer_name_changed
                layer.nameChanged.connect(self.on_layer_name_changed)
                # Interrompe o loop após adicionar o sinal à primeira camada de raster encontrada
                break

        # Chama atualização dos botões após toda modificação
        self.atualizar_estado_botoes()

    def on_layer_name_changed(self):
        """
        Responde ao evento de mudança de nome de qualquer camada no projeto QGIS. 
        Este método é chamado automaticamente quando o nome de uma camada é alterado,
        e sua função é garantir que a lista de camadas no QTreeView seja atualizada para refletir
        essa mudança.

        Funções e Ações Desenvolvidas:
        - Atualização da lista de camadas no QTreeView para assegurar que os nomes das camadas estejam corretos.
        """
        # Atualiza a lista de camadas no QTreeView para refletir a mudança de nome
        self.atualizar_treeView_lista_raster()

    def on_layer_was_renamed(self, layerId, newName):
        """
        Responde ao evento de renomeação de uma camada no QGIS, atualizando o nome da camada no QTreeView.
        Este método garante que as mudanças de nome no projeto sejam refletidas na interface do usuário.

        :param layerId: ID da camada que foi renomeada.
        :param newName: Novo nome atribuído à camada.

        Funções e Ações Desenvolvidas:
        - Pesquisa no modelo do QTreeView pelo item que corresponde à camada renomeada.
        - Atualização do texto do item no QTreeView para refletir o novo nome.
        """
        # Itera sobre todos os itens no modelo do QTreeView para encontrar o item correspondente
        for i in range(self.treeViewModel.rowCount()):
            item = self.treeViewModel.item(i)
            layer = QgsProject.instance().mapLayer(layerId)
            # Verifica se o item corresponde à camada que foi renomeada
            if layer and item.text() == layer.name():
                item.setText(newName)  # Atualiza o nome do item no QTreeView
                break  # Sai do loop após atualizar o nome

    def on_item_changed(self, item):
        """
        Atualiza a visibilidade da camada no QGIS quando o checkbox correspondente
        no treeView é marcado ou desmarcado.

        Funcionalidades:
        - Recupera o ID da camada associada ao item alterado no treeView.
        - Obtém a camada correspondente no projeto.
        - Atualiza a visibilidade no painel de camadas do QGIS conforme o estado do checkbox.

        Parâmetro:
        - item (QStandardItem): Item do treeView que foi modificado (normalmente um checkbox marcado/desmarcado).
        """
        # Obtém o ID da camada armazenado no item do treeView
        layer_id = item.data(Qt.ItemDataRole.UserRole)

        # Recupera a camada correspondente no projeto QGIS
        layer = QgsProject.instance().mapLayer(layer_id)
        if not layer:
            return  # Se a camada não existir mais, sai da função

        # Acessa a estrutura da árvore de camadas (Layer Tree)
        root = QgsProject.instance().layerTreeRoot()

        # Procura o nó da camada dentro da árvore
        node = root.findLayer(layer_id)
        if node:
            # Atualiza a visibilidade da camada com base no estado do checkbox (checked ou unchecked)
            node.setItemVisibilityChecked(item.checkState() == Qt.CheckState.Checked)

    def sync_from_qgis_to_treeview(self):
        """
        Sincroniza o estado de visibilidade das camadas no treeView com o estado real
        das camadas no painel de camadas do QGIS.

        Funcionalidades:
        - Percorre todos os itens do modelo do treeView.
        - Obtém o ID da camada associado a cada item.
        - Busca o nó correspondente na árvore de camadas do QGIS.
        - Atualiza o checkbox de visibilidade no treeView de acordo com a visibilidade da camada no QGIS.
        """
        # Acessa a raiz da árvore de camadas do QGIS
        root = QgsProject.instance().layerTreeRoot()

        # Itera sobre todos os itens do modelo do treeView
        for i in range(self.treeViewModel.rowCount()):
            item = self.treeViewModel.item(i)

            # Obtém o ID da camada associado ao item, se existir
            layer_id = item.data(Qt.ItemDataRole.UserRole) if item else None

            # Obtém o nó da camada correspondente na árvore do QGIS
            node = root.findLayer(layer_id) if layer_id else None

            # Se o nó for válido, atualiza o estado do checkbox do item conforme a visibilidade do nó
            if node:
                item.setCheckState(Qt.CheckState.Checked if node.isVisible() else Qt.CheckState.Unchecked)

    def open_context_menu(self, position):
        """
        Esta função abre um menu de contexto ao clicar com o botão direito na árvore de visualização (QTreeView).
        Se um item for selecionado, cria e exibe um menu de contexto com a opção de abrir as propriedades da camada.
        
        Detalhes:
        - Obtém os índices dos itens selecionados no QTreeView.
        - Verifica se há algum item selecionado.
        - Se houver um item selecionado:
            - Cria um novo menu de contexto.
            - Adiciona a opção "Abrir Propriedades da Camada" ao menu de contexto.
            - Exibe o menu de contexto na posição do cursor.
            - Executa a ação correspondente se a opção "Abrir Propriedades da Camada" for selecionada.
        """
        # Obtém os índices dos itens selecionados na árvore de visualização
        indexes = self.dlg.treeViewListaRaster.selectedIndexes()
        
        # Verifica se algum item foi selecionado
        if indexes:
            # Cria um novo menu de contexto
            menu = QMenu()
            
            # Adiciona uma ação ao menu de contexto
            layer_properties_action = menu.addAction("Abrir Propriedades da Camada")
            
            # Exibe o menu de contexto na posição do cursor e obtém a ação selecionada pelo usuário
            action = menu.exec(self.dlg.treeViewListaRaster.viewport().mapToGlobal(position))
            
            # Verifica se a ação selecionada foi "Abrir Propriedades da Camada"
            if action == layer_properties_action:
                # Abre as propriedades da camada para o item selecionado
                self.abrir_layer_properties(indexes[0])

    def abrir_layer_properties(self, index):
        """
        Abre a janela de propriedades da camada selecionada no QTreeView. Este método é chamado quando o usuário deseja
        ver ou editar as propriedades de uma camada específica, como símbolos, campos e outras configurações.

        Funções e Ações Desenvolvidas:
        - Obtenção do ID da camada a partir do item selecionado no QTreeView.
        - Recuperação da camada correspondente no projeto QGIS.
        - Exibição da janela de propriedades da camada se ela for encontrada.

        :param index: O índice do modelo que representa o item selecionado no QTreeView.
        """
        # Obtém o ID da camada do item selecionado no treeView
        layer_id = index.model().itemFromIndex(index).data(Qt.ItemDataRole.UserRole)
        # Busca a camada correspondente no projeto QGIS usando o ID
        layer = QgsProject.instance().mapLayer(layer_id)
        # Se a camada for encontrada, exibe a janela de propriedades da camada
        if layer:
            self.iface.showLayerProperties(layer)

    def escolher_local_para_salvar(self, nome_padrao, tipo_arquivo):
        """
        Permite ao usuário escolher um local e um nome de arquivo para salvar uma camada, usando uma caixa de diálogo.
        O método também gerencia nomes de arquivos para evitar sobreposição e lembra o último diretório utilizado.

        Funções e Ações Desenvolvidas:
        - Recuperação do último diretório utilizado através das configurações do QGIS.
        - Geração de um nome de arquivo único para evitar sobreposição.
        - Exibição de uma caixa de diálogo para escolha do local de salvamento.
        - Atualização do último diretório utilizado nas configurações do QGIS.

        :param nome_padrao: Nome padrão proposto para o arquivo a ser salvo.
        :param tipo_arquivo: Descrição do tipo de arquivo para a caixa de diálogo (ex. "Arquivos DXF (*.dxf)").

        :return: O caminho completo do arquivo escolhido para salvar ou None se nenhum arquivo foi escolhido.
        """
        # Acessa as configurações do QGIS para recuperar o último diretório utilizado
        settings = QSettings()
        lastDir = settings.value("lastDir", "")  # Usa uma string vazia como padrão se não houver último diretório

        # Configura as opções da caixa de diálogo para salvar arquivos
        options = QFileDialog.Options()
        
        # Gera um nome de arquivo com um sufixo numérico caso o arquivo já exista
        base_nome_padrao, extensao = os.path.splitext(nome_padrao)
        numero = 1
        nome_proposto = base_nome_padrao
        
        # Incrementa o número no nome até encontrar um nome que não exista
        while os.path.exists(os.path.join(lastDir, nome_proposto + extensao)):
            nome_proposto = f"{base_nome_padrao}_{numero}"
            numero += 1

        # Propõe o nome completo no último diretório utilizado
        nome_completo_proposto = os.path.join(lastDir, nome_proposto + extensao)

        # Exibe a caixa de diálogo para salvar arquivos com o nome proposto
        fileName, _ = QFileDialog.getSaveFileName(
            self.dlg,
            "Salvar Camada",
            nome_completo_proposto,
            tipo_arquivo,
            options=options)

        # Verifica se um nome de arquivo foi escolhido
        if fileName:
            # Atualiza o último diretório usado nas configurações do QGIS
            settings.setValue("lastDir", os.path.dirname(fileName))

            # Assegura que o arquivo tenha a extensão correta
            if not fileName.endswith(extensao):
                fileName += extensao

        return fileName  # Retorna o caminho completo do arquivo escolhido ou None se cancelado

    def mostrar_mensagem(self, texto, tipo, duracao=3, caminho_pasta=None, caminho_arquivo=None):
        """
        Exibe uma mensagem na barra de mensagens do QGIS, proporcionando feedback ao usuário baseado nas ações realizadas.
        As mensagens podem ser de erro ou de sucesso, com uma duração configurável e uma opção de abrir uma pasta.

        :param texto: Texto da mensagem a ser exibida.
        :param tipo: Tipo da mensagem ("Erro" ou "Sucesso") que determina a cor e o ícone da mensagem.
        :param duracao: Duração em segundos durante a qual a mensagem será exibida (padrão é 3 segundos).
        :param caminho_pasta: Caminho da pasta a ser aberta ao clicar no botão (padrão é None).
        :param caminho_arquivo: Caminho do arquivo a ser executado ao clicar no botão (padrão é None).
        """
        # Obtém a barra de mensagens da interface do QGIS
        bar = iface.messageBar()  # Acessa a barra de mensagens da interface do QGIS

        # Exibe a mensagem com o nível apropriado baseado no tipo
        if tipo == "Erro":
            # Mostra uma mensagem de erro na barra de mensagens com um ícone crítico e a duração especificada
            bar.pushMessage("Erro", texto, level=Qgis.MessageLevel.Critical, duration=duracao)
        elif tipo == "Sucesso":
            # Cria o item da mensagem
            msg = bar.createMessage("Sucesso", texto)
            
            # Se o caminho da pasta for fornecido, adiciona um botão para abrir a pasta
            if caminho_pasta:
                botao_abrir_pasta = QPushButton("Abrir Pasta")
                botao_abrir_pasta.clicked.connect(lambda: os.startfile(caminho_pasta))
                msg.layout().insertWidget(1, botao_abrir_pasta)  # Adiciona o botão à esquerda do texto
            
            # Se o caminho do arquivo for fornecido, adiciona um botão para executar o arquivo
            if caminho_arquivo:
                botao_executar = QPushButton("Executar")
                botao_executar.clicked.connect(lambda: os.startfile(caminho_arquivo))
                msg.layout().insertWidget(2, botao_executar)  # Adiciona o botão à esquerda do texto
            
            # Adiciona a mensagem à barra com o nível informativo e a duração especificada
            bar.pushWidget(msg, level=Qgis.MessageLevel.Info, duration=duracao)

    def iniciar_progress_bar(self, total_steps):
        """
        Inicia e exibe uma barra de progresso na interface do usuário para o processo de exportação.

        Parâmetros:
        - total_steps (int): O número total de etapas a serem concluídas no processo de exportação.

        Funcionalidades:
        - Cria uma mensagem personalizada na barra de mensagens para acompanhar o progresso.
        - Configura e estiliza uma barra de progresso.
        - Adiciona a barra de progresso à barra de mensagens e a exibe na interface do usuário.
        - Define o valor máximo da barra de progresso com base no número total de etapas.
        - Retorna os widgets de barra de progresso e de mensagem para que possam ser atualizados durante a exportação.
        """
        progressMessageBar = self.iface.messageBar().createMessage("Exportando camada...")
        progressBar = QProgressBar()  # Cria uma instância da QProgressBar
        progressBar.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)  # Alinha a barra de progresso à esquerda e verticalmente ao centro
        progressBar.setFormat("%p% - %v de %m etapas concluídas")  # Define o formato da barra de progresso
        progressBar.setMinimumWidth(300)  # Define a largura mínima da barra de progresso

        # Estiliza a barra de progresso
        progressBar.setStyleSheet("""
            QProgressBar {
                border: 1px solid grey;
                border-radius: 2px;
                background-color: #cddbde;
                text-align: center;
            }
            QProgressBar::chunk {
                background-color: #55aaff;
                width: 5px;
                margin: 1px;
            }
            QProgressBar {
                min-height: 5px;}""")

        # Adiciona a progressBar ao layout da progressMessageBar e exibe na interface
        progressMessageBar.layout().addWidget(progressBar)
        self.iface.messageBar().pushWidget(progressMessageBar, Qgis.MessageLevel.Info)

        # Define o valor máximo da barra de progresso com base no número total de etapas
        progressBar.setMaximum(total_steps)

        return progressBar, progressMessageBar

    def export_raster_to_kml(self):
        """
        Exporta uma camada raster selecionada para o formato KMZ (KML + Imagens).

        Passos detalhados:
        1. Captura o tempo de início do processo.
        2. Verifica se uma camada raster está selecionada na treeView.
        3. Obtém o nome da camada selecionada e verifica se a camada existe no projeto.
        4. Inicia a barra de progresso para monitorar o processo de exportação.
        5. Reprojeta a camada raster e exporta as imagens necessárias.
        6. Cria o arquivo KML usando as imagens exportadas.
        7. Escolhe o local para salvar o arquivo KMZ.
        8. Cria o arquivo KMZ (ZIP) contendo o KML e as imagens.
        9. Atualiza a barra de progresso e limpa a barra de mensagem.
        10. Exibe uma mensagem informando o sucesso da exportação e o tempo de execução.

        Retorna:
        - None
        """
        start_time = time.time()  # Capturar o tempo de início

        # Verificar se alguma camada está selecionada na treeView
        indexes = self.dlg.treeViewListaRaster.selectedIndexes()
        if not indexes:
            self.mostrar_mensagem("Nenhuma camada selecionada", "Erro")
            return

        # Obter o nome da camada selecionada
        selected_layer_name = self.treeViewModel.itemFromIndex(indexes[0]).text()
        # Obter a camada pelo nome
        layers = QgsProject.instance().mapLayersByName(selected_layer_name)
        if not layers:
            self.mostrar_mensagem("Camada não encontrada", "Erro")
            return

        layer = layers[0]  # Obter a primeira camada encontrada

        # Iniciar a barra de progresso
        total_steps = 5  # Ajuste o número total de etapas conforme necessário
        progressBar, progressMessageBar = self.iniciar_progress_bar(total_steps)
        step = 0  # Inicializar o contador de etapas

        # Reprojetar a camada raster e exportar as imagens necessárias
        reprojected_layer, transformed_extent, output_image_path, legend_image_path = self.reproject_and_export_raster(layer, progressBar, step)
        if not reprojected_layer:
            return

        step += 2  # Atualizar o contador de etapas
        # Criar o arquivo KML usando as imagens exportadas
        kml_output_path = self.create_kml(reprojected_layer, transformed_extent, output_image_path, legend_image_path, progressBar, step)
        if not kml_output_path:
            return

        step += 2  # Atualizar o contador de etapas
        progressBar.setValue(step)  # Atualizar a barra de progresso

        end_time = time.time()  # Capturar o tempo de fim
        execution_time = end_time - start_time  # Calcular o tempo de execução

        # Escolher local para salvar o arquivo KMZ
        kmz_output_path = self.escolher_local_para_salvar(layer.name() + ".kmz", "Arquivos KMZ (*.kmz)")
        if not kmz_output_path:
            progressBar.reset()
            self.iface.messageBar().clearWidgets()
            
            return

        # Criar o arquivo KMZ (ZIP)
        with zipfile.ZipFile(kmz_output_path, 'w') as kmz:
            kmz.write(kml_output_path, os.path.basename(kml_output_path))
            kmz.write(output_image_path, os.path.basename(output_image_path))
            kmz.write(legend_image_path, os.path.basename(legend_image_path))

        step += 1  # Atualizar o contador de etapas
        progressBar.setValue(step)  # Atualizar a barra de progresso

        self.iface.messageBar().clearWidgets()  # Limpar a barra de mensagem

        # Exibir mensagem de sucesso com o tempo de execução e caminhos dos arquivos
        self.mostrar_mensagem(
            f"Camada exportada para KMZ em {execution_time:.2f} segundos", 
            "Sucesso", 
            caminho_pasta=os.path.dirname(kmz_output_path), 
            caminho_arquivo=kmz_output_path)

    def reproject_and_export_raster(self, layer, progressBar, step):
        """
        Reprojeta uma camada raster para WGS84 e exporta uma imagem PNG da camada e sua legenda.

        Passos detalhados:
        1. Verifica se a camada está em WGS84 (EPSG:4326).
        2. Se não estiver em WGS84, reprojeta a camada para WGS84.
        3. Cria um caminho temporário para salvar o raster reprojetado.
        4. Configura a transformação de coordenadas e transforma a extensão da camada.
        5. Usa o GDAL para reprojetar o raster.
        6. Cria uma nova camada raster a partir do arquivo reprojetado.
        7. Se a camada já estiver em WGS84, usa a camada original.
        8. Atualiza a barra de progresso.
        9. Cria um arquivo temporário para a imagem PNG.
        10. Exporta a imagem raster para PNG, aplicando a renderização configurada.
        11. Atualiza a barra de progresso.
        12. Cria a imagem da legenda da camada.
        13. Retorna a camada reprojetada, a extensão transformada, o caminho da imagem PNG e o caminho da imagem da legenda.

        Retorna:
        - reprojected_layer: Camada raster reprojetada para WGS84.
        - transformed_extent: Extensão transformada da camada.
        - output_image_path: Caminho do arquivo PNG exportado.
        - legend_image_path: Caminho do arquivo de imagem da legenda.
        """
        # Verificar se a camada está em WGS84 (EPSG:4326)
        wgs84_crs = QgsCoordinateReferenceSystem('EPSG:4326')
        if layer.crs() != wgs84_crs:
            # Criar um caminho temporário para o raster reprojetado
            with tempfile.NamedTemporaryFile(suffix='.tif', delete=False) as tmp_file:
                reprojected_raster_path = tmp_file.name

            try:
                # Configurar a transformação de coordenadas
                transform = QgsCoordinateTransform(layer.crs(), wgs84_crs, QgsProject.instance())
                extent = layer.extent()
                transformed_extent = transform.transformBoundingBox(extent, QgsCoordinateTransform.ForwardTransform)
                
                # Usar o GDAL para reprojetar o raster
                gdal.Warp(reprojected_raster_path, layer.source(), dstSRS='EPSG:4326')
                
                # Criar uma nova camada raster a partir do arquivo reprojetado
                reprojected_layer = QgsRasterLayer(reprojected_raster_path, layer.name() + '_wgs84')
                if not reprojected_layer.isValid():
                    self.mostrar_mensagem("Erro ao reprojetar a camada para WGS84", "Erro")
                    return None, None, None, None
            except Exception as e:
                self.mostrar_mensagem("Erro ao reprojetar a camada: " + str(e), "Erro")
                return None, None, None, None
        else:
            reprojected_layer = layer  # Usar a camada original se já estiver em WGS84
            transformed_extent = reprojected_layer.extent()  # Obter a extensão transformada

        # Atualizar a barra de progresso
        step += 1  # Incrementar o contador de etapas
        progressBar.setValue(step)  # Atualizar o valor da barra de progresso

        # Criar um arquivo temporário para a imagem PNG
        with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_png_file:
            output_image_path = tmp_png_file.name  # Obter o caminho do arquivo PNG temporário

        # Exportar a imagem raster para PNG, aplicando a renderização configurada
        pipe = QgsRasterPipe()  # Criar um pipeline de raster
        pipe.set(layer.dataProvider().clone())  # Clonar o provedor de dados da camada
        pipe.set(layer.renderer().clone())  # Clonar o renderizador da camada

        file_writer = QgsRasterFileWriter(output_image_path)  # Criar um escritor de arquivos raster
        file_writer.writeRaster(
            pipe,  # Usar o pipeline de raster
            layer.width(),  # Largura da imagem
            layer.height(),  # Altura da imagem
            layer.extent(),  # Extensão da camada
            layer.crs()  # Sistema de referência de coordenadas da camada
        )

        # Atualizar a barra de progresso
        step += 1  # Incrementar o contador de etapas
        progressBar.setValue(step)  # Atualizar o valor da barra de progresso

        # Criar a imagem da legenda
        legend_image_path = self.gerar_legenda_como_imagem(layer)  # Gerar a imagem da legenda
        if not legend_image_path:
            self.mostrar_mensagem("Erro ao gerar a legenda da camada", "Erro")
            return None, None, None, None

        return reprojected_layer, transformed_extent, output_image_path, legend_image_path  # Retornar os resultados

    def create_kml(self, reprojected_layer, transformed_extent, output_image_path, legend_image_path, progressBar, step):
        """
        Cria um arquivo KML com a camada raster reprojetada e uma imagem de legenda.

        Passos detalhados:
        1. Obtém a extensão da camada transformada.
        2. Cria um arquivo KML temporário.
        3. Cria um KML com GroundOverlay para a imagem raster.
        4. Adiciona a legenda como ScreenOverlay no KML.
        5. Salva o arquivo KML.
        6. Atualiza a barra de progresso.
        7. Retorna o caminho do arquivo KML criado.

        Retorna:
        - kml_output_path: Caminho do arquivo KML criado.
        """
        # Obter a extensão da camada transformada
        bbox = [transformed_extent.xMinimum(), transformed_extent.yMinimum(), transformed_extent.xMaximum(), transformed_extent.yMaximum()]

        # Criar KML com GroundOverlay
        with tempfile.NamedTemporaryFile(suffix='.kml', delete=False) as tmp_kml_file:
            kml_output_path = tmp_kml_file.name  # Obter o caminho do arquivo KML temporário

        kml = simplekml.Kml()  # Criar um objeto KML
        ground = kml.newgroundoverlay(name=reprojected_layer.name())  # Criar um GroundOverlay no KML
        ground.icon.href = os.path.basename(output_image_path)  # Definir o ícone como a imagem raster exportada (apenas o nome do arquivo)
        ground.latlonbox.north = bbox[3]  # Definir o limite norte da extensão
        ground.latlonbox.south = bbox[1]  # Definir o limite sul da extensão
        ground.latlonbox.east = bbox[2]  # Definir o limite leste da extensão
        ground.latlonbox.west = bbox[0]  # Definir o limite oeste da extensão

        # Adicionar a legenda como ScreenOverlay
        screen = kml.newscreenoverlay(name="Legenda")  # Criar um ScreenOverlay no KML para a legenda
        screen.icon.href = os.path.basename(legend_image_path)  # Definir o ícone como a imagem da legenda (apenas o nome do arquivo)
        screen.overlayxy = simplekml.OverlayXY(x=0, y=0, xunits=simplekml.Units.fraction, yunits=simplekml.Units.fraction)  # Definir a posição do overlay
        screen.screenxy = simplekml.ScreenXY(x=0.01, y=0.05, xunits=simplekml.Units.fraction, yunits=simplekml.Units.fraction)  # Definir a posição na tela
        screen.size.x = 0.18  # Definir a largura do overlay
        screen.size.y = 0.18  # Definir a altura do overlay
        screen.size.xunits = simplekml.Units.fraction  # Definir a unidade de largura
        screen.size.yunits = simplekml.Units.fraction  # Definir a unidade de altura

        kml.save(kml_output_path)  # Salvar o arquivo KML

        # Atualizar a barra de progresso
        step += 2  # Incrementar o contador de etapas
        progressBar.setValue(step)  # Atualizar o valor da barra de progresso

        return kml_output_path  # Retornar o caminho do arquivo KML criado

    def gerar_legenda_como_imagem(self, layer, page_width=60, page_height=70):
        """
        Gera uma imagem de legenda para a camada raster especificada.

        Passos detalhados:
        1. Cria um layout temporário.
        2. Adiciona um item de legenda ao layout.
        3. Inclui apenas a camada raster especificada na legenda.
        4. Atualiza a legenda.
        5. Ajusta o tamanho da legenda.
        6. Calcula o tamanho da imagem com base no conteúdo da legenda.
        7. Renderiza a legenda em uma imagem.
        8. Salva a imagem temporária.
        9. Retorna o caminho da imagem da legenda.

        Retorna:
        - legend_image_path: Caminho do arquivo de imagem da legenda.
        """
        # Criar um layout temporário
        project = QgsProject.instance()
        layout = QgsLayout(project)
        layout.initializeDefaults()

        # Adicionar item de legenda ao layout
        legend = QgsLayoutItemLegend(layout)
        legend.setTitle("Legenda")
        legend.setLinkedMap(None)  # Sem mapa associado

        # Incluir apenas a camada raster especificada na legenda
        root = QgsLayerTree()
        root.addLayer(layer)
        legend.model().setRootGroup(root)

        legend.updateLegend()  # Atualizar a legenda

        # Ajustar tamanho da legenda
        layout.addLayoutItem(legend)
        legend.setMinimumSize(QgsLayoutSize(page_width, page_height, QgsUnitTypes.LayoutMillimeters))  # Tamanho configurável
        legend.attemptResize(QgsLayoutSize(page_width, page_height, QgsUnitTypes.LayoutMillimeters))  # Tamanho configurável

        # Calcular o tamanho da imagem com base no conteúdo da legenda
        legend_rect = legend.boundingRect()
        img_width = int(legend_rect.width() * 10)  # Ajustar fator de escala conforme necessário
        img_height = int(legend_rect.height() * 10)

        # Renderizar a legenda em uma imagem
        image = QImage(img_width, img_height, QImage.Format.Format_ARGB32)
        image.fill(Qt.GlobalColor.transparent)  # Fundo transparente

        # Configurar o QPainter para renderizar a legenda na imagem
        painter = QPainter(image)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        layout.render(painter, QRectF(0, 0, img_width, img_height), legend_rect)
        painter.end()

        # Salvar a imagem temporária
        with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_legend_file:
            legend_image_path = tmp_legend_file.name  # Obter o caminho do arquivo temporário
            image.save(legend_image_path, 'PNG')  # Salvar a imagem no arquivo temporário

        return legend_image_path  # Retornar o caminho da imagem da legenda

    def exportar_raster_dxf(self):
        """
        Exporta uma camada raster para o formato DXF. Dependendo do tipo da camada, 
        realiza uma exportação diferente:
        - Se a camada for um serviço de mapa (WMS/XYZ), exporta a visualização da área de trabalho.
        - Caso contrário, exporta a camada raster comum.

        Passos:
        1. Obtém a camada selecionada pelo usuário.
        2. Verifica se a camada é um serviço de mapa.
        3. Se for um serviço de mapa, exporta a visualização da área de trabalho.
        4. Se for uma camada raster comum, exporta a camada raster comum.

        """
        # Obtém os índices das camadas selecionadas na interface do usuário
        indexes = self.dlg.treeViewListaRaster.selectedIndexes()
        if not indexes:  # Verifica se alguma camada foi selecionada
            self.mostrar_mensagem("Nenhuma camada selecionada", "Erro")  # Mostra mensagem de erro
            return  # Encerra a função se nenhuma camada foi selecionada
        
        # Obtém o nome da camada selecionada
        selected_layer_name = self.treeViewModel.itemFromIndex(indexes[0]).text()
        
        # Obtém a camada pelo nome
        layers = QgsProject.instance().mapLayersByName(selected_layer_name)
        if not layers:  # Verifica se a camada foi encontrada
            self.mostrar_mensagem("Camada não encontrada", "Erro")  # Mostra mensagem de erro
            return  # Encerra a função se a camada não foi encontrada
        
        layer = layers[0]  # Obtém a primeira camada encontrada

        # Verificação para camadas de serviço de mapas
        if layer.providerType() in ['wms', 'xyz']:  # Verifica se a camada é um serviço de mapa (WMS/XYZ)
            self.exportar_visualizacao_para_dxf(layer)  # Exporta a visualização da área de trabalho
        else:  # Caso contrário
            self.exportar_raster_comum_para_dxf(layer)  # Exporta a camada raster comum

    def exportar_visualizacao_para_dxf(self, layer):
        """
        Exporta a visualização da área de trabalho do QGIS para o formato DXF.

        Passos:
        1. Inicia a barra de progresso.
        2. Captura a visualização do canvas do QGIS como uma imagem.
        3. Salva a imagem capturada como PNG.
        4. Solicita ao usuário o local para salvar o arquivo DXF.
        5. Cria um documento DXF e insere a imagem PNG.
        6. Salva o arquivo DXF.
        7. Atualiza a barra de progresso e mostra uma mensagem de sucesso.

        Parâmetros:
        - layer: A camada para a qual a visualização será exportada.
        """
        start_time = time.time()  # Inicia a contagem do tempo de execução
        total_steps = 5  # Define o número total de etapas para a barra de progresso
        progressBar, progressMessageBar = self.iniciar_progress_bar(total_steps)  # Inicia a barra de progresso
        
        canvas = self.iface.mapCanvas()  # Obtém o canvas do QGIS
        rect = canvas.extent()  # Obtém a extensão da visualização
        width = canvas.size().width()  # Obtém a largura do canvas
        height = canvas.size().height()  # Obtém a altura do canvas
        image = QImage(width, height, QImage.Format.Format_ARGB32)  # Cria uma imagem com as dimensões do canvas
        painter = QPainter(image)  # Cria um pintor para a imagem
        canvas.render(painter)  # Renderiza o canvas na imagem
        painter.end()  # Finaliza o pintor

        progressBar.setValue(1)  # Atualiza a barra de progresso

        # Salvar a imagem capturada
        temp_image_path = self.escolher_local_para_salvar("visualizacao_qgis.png", "Image Files (*.png)")  # Solicita o caminho para salvar a imagem
        if not temp_image_path:  # Verifica se o caminho foi fornecido
            self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
            return  # Encerra a função se o caminho não foi fornecido
        if not image.save(temp_image_path):  # Salva a imagem como PNG
            self.mostrar_mensagem("Erro ao salvar a imagem capturada", "Erro")  # Mostra mensagem de erro
            self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
            return  # Encerra a função se ocorrer um erro

        progressBar.setValue(2)  # Atualiza a barra de progresso

        # Escolher local para salvar o arquivo DXF
        dxf_file_path = self.escolher_local_para_salvar("visualizacao_qgis.dxf", "DXF Files (*.dxf)")  # Solicita o caminho para salvar o DXF
        if not dxf_file_path:  # Verifica se o caminho foi fornecido
            self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
            return  # Encerra a função se o caminho não foi fornecido

        progressBar.setValue(3)  # Atualiza a barra de progresso

        # Converter a imagem para DXF
        doc = ezdxf.new(dxfversion='R2013')  # Cria um novo documento DXF
        msp = doc.modelspace()  # Obtém o espaço do modelo
        image_def = doc.add_image_def(filename=os.path.abspath(temp_image_path), size_in_pixel=(width, height))  # Adiciona a definição da imagem
        insert_point = (rect.xMinimum(), rect.yMinimum())  # Define o ponto de inserção
        msp.add_image(insert=insert_point,  # Adiciona a imagem ao espaço do modelo
                      size_in_units=(rect.width(), rect.height()),  # Define o tamanho da imagem em unidades
                      image_def=image_def,  # Define a imagem
                      rotation=0)  # Define a rotação

        progressBar.setValue(4)  # Atualiza a barra de progresso

        # Salvar o arquivo DXF
        doc.saveas(dxf_file_path)  # Salva o documento DXF

        progressBar.setValue(5)  # Atualiza a barra de progresso

        end_time = time.time()  # Calcula o tempo de execução
        execution_time = end_time - start_time  # Calcula o tempo total de execução
        self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
        self.mostrar_mensagem(f"Visualização da área de trabalho exportada para DXF em {execution_time:.2f} segundos", "Sucesso", caminho_pasta=os.path.dirname(dxf_file_path), caminho_arquivo=dxf_file_path)

    def exportar_raster_comum_para_dxf(self, layer):
        """
        Exporta uma camada raster comum para o formato DXF.
        
        Passos:
        1. Inicia a barra de progresso.
        2. Solicita ao usuário para salvar o arquivo raster como TIFF.
        3. Solicita ao usuário para salvar o arquivo DXF.
        4. Configura os parâmetros do raster, incluindo resolução e extensão.
        5. Cria e salva o arquivo TIFF.
        6. Converte o TIFF para PNG.
        7. Deleta o arquivo TIFF.
        8. Cria um documento DXF e insere a imagem PNG.
        9. Salva o arquivo DXF.
        10. Atualiza a barra de progresso e mostra uma mensagem de sucesso.

        Parâmetros:
        - layer: A camada raster a ser exportada.
        """
        start_time = time.time()  # Inicia a contagem do tempo de execução
        total_steps = 100  # Define o número total de etapas para a barra de progresso
        progressBar, progressMessageBar = self.iniciar_progress_bar(total_steps)  # Inicia a barra de progresso

        progressBar.setValue(10)  # Atualiza a barra de progresso
        raster_file_path_tif = self.escolher_local_para_salvar(f"{layer.name()}.tif", "Image Files (*.tif)")  # Solicita o caminho para salvar o TIFF
        if not raster_file_path_tif:  # Verifica se o caminho foi fornecido
            self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
            return  # Encerra a função se o caminho não foi fornecido
        
        progressBar.setValue(20)  # Atualiza a barra de progresso
        dxf_file_path = self.escolher_local_para_salvar(f"{layer.name()}.dxf", "DXF Files (*.dxf)")  # Solicita o caminho para salvar o DXF
        if not dxf_file_path:  # Verifica se o caminho foi fornecido
            self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
            return  # Encerra a função se o caminho não foi fornecido
        
        progressBar.setValue(30)  # Atualiza a barra de progresso
        resolution_multiplier = 8  # Define o multiplicador de resolução
        provider = layer.dataProvider()  # Obtém o provedor de dados da camada
        extent = layer.extent()  # Obtém a extensão da camada
        width = layer.width() * resolution_multiplier  # Calcula a largura com base no multiplicador de resolução
        height = layer.height() * resolution_multiplier  # Calcula a altura com base no multiplicador de resolução
        file_writer = QgsRasterFileWriter(raster_file_path_tif)  # Cria o escritor de arquivos raster
        pipe = QgsRasterPipe()  # Cria o pipe raster
        if not pipe.set(provider.clone()):  # Clona o provedor de dados do raster
            self.mostrar_mensagem("Erro ao clonar o provedor de dados do raster", "Erro")  # Mostra mensagem de erro
            self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
            return  # Encerra a função se ocorrer um erro
        if not pipe.set(layer.renderer().clone()):  # Clona o renderizador do raster
            self.mostrar_mensagem("Erro ao clonar o renderizador do raster", "Erro")  # Mostra mensagem de erro
            self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
            return  # Encerra a função se ocorrer um erro
        if file_writer.writeRaster(pipe, width, height, extent, layer.crs()) != QgsRasterFileWriter.NoError:  # Escreve o raster no arquivo TIFF
            self.mostrar_mensagem("Erro ao salvar o arquivo raster", "Erro")  # Mostra mensagem de erro
            self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
            return  # Encerra a função se ocorrer um erro
        
        progressBar.setValue(50)  # Atualiza a barra de progresso
        if not os.path.exists(raster_file_path_tif):  # Verifica se o arquivo TIFF foi salvo
            self.mostrar_mensagem("Erro ao salvar o arquivo raster", "Erro")  # Mostra mensagem de erro
            self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
            return  # Encerra a função se o arquivo não foi salvo
        
        # Converter TIFF para PNG
        raster_file_path_png = raster_file_path_tif.replace(".tif", ".png")  # Define o caminho do arquivo PNG
        image = QImage()  # Cria uma instância de QImage
        if not image.load(raster_file_path_tif):  # Carrega o arquivo TIFF
            self.mostrar_mensagem("Erro ao carregar o arquivo TIFF para conversão", "Erro")  # Mostra mensagem de erro
            self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
            return  # Encerra a função se ocorrer um erro
        progressBar.setValue(60)  # Atualiza a barra de progresso durante o carregamento da imagem
        
        if not image.save(raster_file_path_png, "PNG"):  # Salva a imagem como PNG
            self.mostrar_mensagem("Erro ao salvar o arquivo como PNG", "Erro")  # Mostra mensagem de erro
            self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
            return  # Encerra a função se ocorrer um erro
        progressBar.setValue(70)  # Atualiza a barra de progresso durante a conversão para PNG
        
        # Deletar o arquivo TIFF
        os.remove(raster_file_path_tif)  # Deleta o arquivo TIFF
        
        progressBar.setValue(80)  # Atualiza a barra de progresso após a exclusão do TIFF
        doc = ezdxf.new(dxfversion='R2010')  # Cria um novo documento DXF
        msp = doc.modelspace()  # Obtém o espaço do modelo
        image_def = doc.add_image_def(filename=os.path.abspath(raster_file_path_png), size_in_pixel=(width, height))  # Adiciona a definição da imagem
        insert_point = (extent.xMinimum(), extent.yMinimum())  # Define o ponto de inserção
        msp.add_image(insert=insert_point,  # Adiciona a imagem ao espaço do modelo
                      size_in_units=(extent.width(), extent.height()),  # Define o tamanho da imagem em unidades
                      image_def=image_def,  # Define a imagem
                      rotation=0)  # Define a rotação
        
        progressBar.setValue(90)  # Atualiza a barra de progresso durante a criação do DXF
        doc.saveas(dxf_file_path)  # Salva o documento DXF
        progressBar.setValue(100)  # Atualiza a barra de progresso após salvar o DXF
        
        end_time = time.time()  # Calcula o tempo de execução
        execution_time = end_time - start_time  # Calcula o tempo total de execução
        self.iface.messageBar().clearWidgets()  # Limpa a barra de mensagens
        self.mostrar_mensagem(  # Mostra mensagem de sucesso
            f"Camada exportada para DXF em {execution_time:.2f} segundos",
            "Sucesso",
            caminho_pasta=os.path.dirname(dxf_file_path),
            caminho_arquivo=dxf_file_path)

    def abrir_qgis2threejs(self):
        """
        Aproxima para a camada selecionada no QTreeView e abre o plugin Qgis2threejs.
        Se o plugin não estiver instalado/carregado, oferece a opção de instalá-lo.
        """
        # Ajusta o zoom para a camada selecionada
        indexes = self.dlg.treeViewListaRaster.selectedIndexes()
        if indexes:
            selected_layer_name = self.treeViewModel.itemFromIndex(indexes[0]).text()
            layers = QgsProject.instance().mapLayersByName(selected_layer_name)

            if layers:
                selected_layer = layers[0]
                self.iface.mapCanvas().setExtent(selected_layer.extent())
                self.iface.mapCanvas().refresh()

        plugin_nome = "Qgis2threejs"

        try:
            plugin = qgis_utils.plugins.get(plugin_nome)

            # Se ainda não estiver carregado, tenta carregar/iniciar
            if plugin is None:
                qgis_utils.loadPlugin(plugin_nome)
                qgis_utils.startPlugin(plugin_nome)
                plugin = qgis_utils.plugins.get(plugin_nome)

            # Se continuou None, trata como não instalado/não disponível
            if plugin is None:
                resposta = QMessageBox.question(
                    self.dlg,
                    "Plugin não encontrado",
                    f"O complemento '{plugin_nome}' não está instalado.\nDeseja instalá-lo agora?",
                    QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                    QMessageBox.StandardButton.Yes)
                if resposta == QMessageBox.StandardButton.Yes:
                    self.iface.actionManagePlugins().trigger()
                else:
                    self.mostrar_mensagem(
                        f"O complemento '{plugin_nome}' é necessário para esta funcionalidade.",
                        "Erro",
                        duracao=3)
                return

            # API atual do plugin
            if hasattr(plugin, "openExporter"):
                plugin.openExporter()
            elif hasattr(plugin, "openExporterWebEng"):
                plugin.openExporterWebEng()
            else:
                QMessageBox.warning(
                    self.dlg, "Qgis2threejs",
                    "O plugin foi carregado, mas não expõe openExporter/openExporterWebEng.")

        except Exception as e:
            self.mostrar_mensagem(f"Erro ao abrir Qgis2threejs: {e}", "Erro", duracao=3)

class TreeViewEventFilter(QObject):
    """
    Filtro de eventos personalizado para detectar movimentos do mouse sobre itens em um treeView.

    Esta classe herda de QObject e implementa um filtro de eventos que detecta quando o mouse se move
    sobre itens específicos em um treeView. Quando o mouse se move sobre um item, a classe chama um 
    método no UiManagerR para exibir um tooltip com informações sobre o item.

    Parâmetros:
    - ui_manager: Referência à instância do objeto UiManagerR, que gerencia a interface do usuário.
    """

    def __init__(self, ui_manager):
        """
        Inicializa o filtro de eventos com uma referência ao UiManagerR.

        Parâmetros:
        - ui_manager: Instância do UiManager que será usada para acessar e manipular a interface do usuário.
        """
        super().__init__()  # Inicializa a classe base QObject
        self.ui_manager = ui_manager  # Armazena a referência ao UiManagerR para uso posterior

    def eventFilter(self, obj, event):
        """
        Filtra os eventos de movimentação do mouse sobre o treeView e exibe tooltips quando aplicável.

        Esta função intercepta eventos que ocorrem no treeView especificado. Se o evento for de movimento
        do mouse (QEvent.MouseMove) e o mouse estiver sobre um item válido no treeView, a função chama
        o método 'configurar_tooltip' do UiManager para exibir um tooltip com informações sobre o item.

        Parâmetros:
        - obj: O objeto que está sendo monitorado (neste caso, o viewport do treeView).
        - event: O evento que está sendo filtrado (como QEvent.MouseMove).

        Retorno:
        - bool: O resultado da chamada à função 'eventFilter' da classe base, indicando se o evento foi processado.
        """
        # Verifica se o objeto é o viewport do treeView e se o evento é de movimento do mouse
        if obj == self.ui_manager.dlg.treeViewListaRaster.viewport() and event.type() == QEvent.Type.MouseMove:
            # Obtém o índice do item no treeView sob o cursor do mouse
            index = self.ui_manager.dlg.treeViewListaRaster.indexAt(event.pos())
            if index.isValid():  # Verifica se o índice é válido (se o mouse está sobre um item)
                self.ui_manager.configurar_tooltip(index)  # Chama o método para configurar e exibir o tooltip
        # Retorna o resultado padrão do filtro de eventos
        return super().eventFilter(obj, event)  # Chama a implementação da classe base para continuar o processamento normal

class CustomDelegate(QStyledItemDelegate):
    """
    Esta classe `CustomDelegate` personaliza a aparência dos itens no QTreeView, 
    especialmente para camadas raster, desenhando ícones específicos baseados no tipo de renderização.

    Detalhes:
    - O método `paint` personaliza a pintura dos itens no QTreeView.
        - Inicializa a opção de estilo do item.
        - Usa o estilo do widget pai.
        - Calcula a posição e o tamanho do ícone de seleção.
        - Obtém a camada correspondente pelo nome e verifica seu tipo de renderização.
        - Desenha um quadrado dividido em 4 partes com cores baseadas no tipo de renderização da camada.
        - Desenha o item padrão após personalização.
    - O método `sizeHint` ajusta o tamanho do item para acomodar o ícone personalizado.
    - O método `is_false_color` verifica se a camada usa renderização de falsa cor.

    Métodos:
    - `__init__(self, parent=None)`: Inicializa a classe com o pai opcional.
    - `paint(self, painter, option, index)`: Personaliza a pintura dos itens no QTreeView.
    - `sizeHint(self, option, index)`: Ajusta o tamanho do item para acomodar o ícone personalizado.
    - `is_false_color(self, layer)`: Verifica se a camada usa renderização de falsa cor.
    """
    def __init__(self, parent=None):
        super().__init__(parent) # Inicializa a classe base com o pai opcional

    def paint(self, painter, option, index):
        # Inicializa a opção de estilo do item
        options = QStyleOptionViewItem(option)
        self.initStyleOption(options, index)
        
        # Usa o estilo do widget pai
        style = option.widget.style()
        
        checkBoxRect = style.subElementRect(QStyle.SubElement.SE_ItemViewItemCheckIndicator, options, option.widget)
        iconSize = QSize(14, 14)
        iconRect = QRect(checkBoxRect.left() - 20, checkBoxRect.top(), iconSize.width(), iconSize.height())
        
        # Obter a camada correspondente pelo nome
        layer_name = index.data()
        layers = QgsProject.instance().mapLayersByName(layer_name)
        if layers:
            layer = layers[0]
            if self.is_false_color(layer):
                # Desenha o quadrado dividido em 4 partes com cores de falsa cor
                colors = [QColor(255, 0, 255), QColor(255, 0, 0), QColor(0, 255, 0), QColor(255, 255, 0)]
            else:
                # Desenha o quadrado dividido em 4 partes em escala de cinza
                colors = [QColor(200, 200, 200), QColor(50, 50, 50), QColor(120, 120, 120), QColor(150, 150, 150)]
            
            size = iconRect.width() // 2
            for i in range(2):
                for j in range(2):
                    partRect = QRect(iconRect.left() + i*size, iconRect.top() + j*size, size, size)
                    painter.fillRect(partRect, colors[i + j*2])
        
        # Desenha o item padrão
        super().paint(painter, option, index)

    def sizeHint(self, option, index):
        """
        Ajusta o tamanho do item para acomodar o ícone personalizado.
        
        Detalhes:
        - Chama o método base `sizeHint` para obter o tamanho padrão.
        - Aumenta a largura para acomodar o ícone personalizado.
        
        Retorna:
        - `QSize`: O tamanho ajustado do item.
        """
        size = super().sizeHint(option, index)
        size.setWidth(size.width() + 15)  # Ajusta o tamanho para o ícone
        return size

    def is_false_color(self, layer):
        """
        Verifica se a camada usa renderização de falsa cor.
        
        Detalhes:
        - Verifica se a camada é do tipo Raster.
        - Obtém o renderizador da camada e verifica se é uma instância de `QgsSingleBandPseudoColorRenderer`.
        
        Retorna:
        - `bool`: `True` se a camada usar renderização de falsa cor, caso contrário `False`.
        """
        # Método auxiliar para verificar se a camada usa renderização de falsa cor
        if layer.type() == QgsMapLayer.LayerType.RasterLayer:
            renderer = layer.renderer()
            if isinstance(renderer, QgsSingleBandPseudoColorRenderer):
                return True
        return False

