# -*- coding: utf-8 -*-

"""
Qualy_Accuracy_3D.py
***************************************************************************
*                                                                         *
*   This program is free software; you can redistribute it and/or modify  *
*   it under the terms of the GNU General Public License as published by  *
*   the Free Software Foundation; either version 2 of the License, or     *
*   (at your option) any later version.                                   *
*                                                                         *
***************************************************************************
"""
__author__ = 'Leandro França'
__date__ = '2025-08-06'
__copyright__ = '(C) 2025, Leandro França'

from qgis.PyQt.QtCore import QVariant
from qgis.core import *
from numpy import sqrt, array, mean, std, pi, sin
from lftools.geocapt.imgs import *
from lftools.translations.translate import translate
from lftools.geocapt.topogeo import str2HTML
from lftools.geocapt.cartography import PEC
import os
from qgis.PyQt.QtGui import QIcon


class Accuracy_3D(QgsProcessingAlgorithm):

    TEST = 'TEST'
    REF = 'REF'
    OUTPUT = 'OUTPUT'
    DECIMAL = 'DECIMAL'
    HTML = 'HTML'
    CRS = 'CRS'
    FIELD = 'FIELD'

    def createInstance(self):
        return Accuracy_3D()

    def name(self):
        return 'Accuracy_3D'.lower()

    def displayName(self):
        return self.tr('3D positional accuracy', 'Acurácia posicional 3D')

    def group(self):
        return self.tr('Qualidade')

    def groupId(self):
        return 'quality'
    
    LOC = QgsApplication.locale()[:2]

    def tr(self, *string):
        return translate(string, self.LOC)

    def tags(self):
        return 'GeoOne,PEC,qualidade,padrão,rmse,remq,exactness,precision,precisão,tendência,tendency,correctness,accuracy,acurácia,discrepância,discrepancy,vector,deltas,3d,planialtimetrico,cqdg,asprs'.split(',')

    def icon(self):
        return QIcon(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'images/quality.png'))

    txt_en = '''This tool can be used to evaluate the <b>planialtimetric (3D) positional accuracy</b> of digital cartographic products.

<b>Outputs:</b>
1. Discrepancy calculations: differences in X, Y, and Z coordinates, including the planimetric (XY) and altimetric (Z) resultants.
2. Accuracy report: Cartographic Accuracy Standard report containing RMSE values and classification according to PEC-PCD Planimetric and PEC-PCD Altimetric.

<b>Input Requirements:</b>
 - Input layers must match <b>PointZ</b> pairs.
 - Each vector is generated by two vertices (homologous points):
   1. The first vertex corresponds to the test point.
   2. The second vertex corresponds to the check point.'''
    
    txt_pt = '''Esta ferramenta pode ser utilizada para avaliar a acurácia posicional planialtimétrica (3D) de produtos cartográficos digitais.

<b>Saídas</b>
1. <b>Cálculo das discrepâncias</b>: diferenças nas coordenadas X, Y e Z, incluindo as resultantes planimétrica (XY) e altimétrica (Z).
2. <b>Relatório de acurácia</b>: relatório do Padrão de Exatidão Cartográfica contendo valores de REMQ e classificação conforme o PEC-PCD Planimétrico e o PEC-PCD Altimétrico.

<b>Requisitos de Entrada</b>
- As camadas de entrada devem formar pares de <b>PointZ</b>.
- Cada vetor é gerado por dois vértices (pontos homólogos):
    1. O primeiro vértice corresponde ao ponto de teste.
    2. O segundo vértice corresponde ao ponto de checagem.'''
    
    figure = 'images/tutorial/qualy_3d.jpg'

    def shortHelpString(self):
        social_BW = Imgs().social_BW
        footer = '''<div align="center">
                      <img src="'''+ os.path.join(os.path.dirname(os.path.dirname(__file__)), self.figure) +'''">
                      </div>
                      <div align="left">
                      <p>
                      <b><a href="'''+ self.tr('https://portal.geoone.com.br/m/lessons/drone-webodm?classId=5990') + '''" target="_blank">'''+ self.tr('Click here to watch a full class on this tool',
                                    'Clique aqui para assistir uma aula completa sobre esta ferramenta') +'''</a></b>
                      </p>
                      <p>
                      <b><a href="'''+ self.tr('https://geoone.com.br/pvdrone/') + '''" target="_blank">'''+ self.tr('Sign up for the WebODM and QGIS course',
                                    'Inscreva-se no curso de WebODM e QGIS') +'</a><br><br>'+ self.tr('Author: Leandro Franca', 'Autor: Leandro França')+'''</b>
                      </p>'''+ social_BW + '''
                    </div>'''
        return self.tr(self.txt_en, self.txt_pt) + footer
    

    def initAlgorithm(self, config=None):
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.TEST,
                self.tr('Test Points', 'Pontos de Teste'),
                [QgsProcessing.TypeVectorPoint]
            )
        )
        
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.REF,
                self.tr('Reference Points', 'Pontos de Referência'),
                [QgsProcessing.TypeVectorPoint]
            )
        )

        self.addParameter(
            QgsProcessingParameterField(
                self.FIELD,
                self.tr('Name Field', 'Campo para nome'),
                parentLayerParameterName=self.REF,
                optional = True
            )
        )
        
        self.addParameter(
            QgsProcessingParameterCrs(
                self.CRS,
                self.tr('CRS', 'SRC'),
                # QgsProject.instance().crs(),
                optional = True
                )
            )

        self.addParameter(
            QgsProcessingParameterNumber(
                self.DECIMAL,
                self.tr('Decimal places', 'Casas decimais'),
                QgsProcessingParameterNumber.Type.Integer,
                defaultValue = 3,
                minValue = 0
                )
            )
        
        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr('3D Planialtimetric Discrepancies', 'Discrepâncias planialtimétricas 3D')
            )
        )
        
        self.addParameter(
            QgsProcessingParameterFileDestination(
                self.HTML,
                self.tr('3D Planialtimetric Accuracy Report', 'Relatório do PEC-PCD planialtimétrico 3D'),
                self.tr('HTML files (*.html)')
            )
        )


    def processAlgorithm(self, parameters, context, feedback):
        
        teste = self.parameterAsSource(
            parameters,
            self.TEST,
            context
        )
        if teste is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.TEST))
            
        ref = self.parameterAsSource(
            parameters,
            self.REF,
            context
        )
        if ref is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.REF))
        
        
        decimal = self.parameterAsInt(
            parameters,
            self.DECIMAL,
            context
        )
        
        out_CRS = self.parameterAsCrs(
            parameters,
            self.CRS,
            context
        )
        
        campo = self.parameterAsFields(
            parameters,
            self.FIELD,
            context
        )
        if campo:
            columnName = campo[0]

        
        format_num = '{:,.Xf}'.replace('X', str(decimal))
        
        Fields = QgsFields()
        itens  = {
                     'name' : QVariant.String,
                     'discrep_x' : QVariant.Double,
                     'discrep_y' : QVariant.Double,
                     'discrep_z' : QVariant.Double,
                     'discrep_xy' : QVariant.Double,
                     'discrep_xyz' : QVariant.Double,
                     }
        for item in itens:
            Fields.append(QgsField(item, itens[item]))
            
        
        html_output = self.parameterAsFileOutput(
            parameters,
            self.HTML,
            context
        )
        
        dicionario = {'0.5k': '1:500', '1k': '1:1.000', '2k': '1:2.000', '5k': '1:5.000', '10k': '1:10.000', '25k': '1:25.000', '50k': '1:50.000', '100k': '1:100.000', '250k': '1:250.000'}
        
        valores = ['A', 'B', 'C', 'D']
        
        Escalas = [ esc for esc in dicionario]
        
        # VALIDAÇÕES
        num_teste = teste.featureCount()
        num_ref = ref.featureCount()
        # Validação do número mínimo de feições em cada camada de entrada
        if num_teste < 3 or num_teste < 3:
            raise QgsProcessingException(self.tr('Insufficient number of features for quality evaluation!', 'Número de feições insuficiente para avaliação de qualidade!'))
        
        # O número de feições deve ser extamente o mesmo nas duas camadas
        if num_ref != num_teste:
            raise QgsProcessingException(self.tr('The number of features must be the same in both layers!', 'O número de feições deve ser igual nas duas camadas!'))
            
        # As camadas de teste e de referência devem ser distintas
        if teste.sourceName() == ref.sourceName():
            raise QgsProcessingException(self.tr('The test and reference layers must be distinct!','As camadas de teste e de referência devem ser distintas!'))
        
        
        # SRC definido deve ser projetado
        msg = self.tr('Define a projected CRS for the calculations!', 'Defina um SRC projetado para os cálculos!')
        coordTransf = False
        if out_CRS.isValid():
            if out_CRS.isGeographic():
                raise QgsProcessingException(msg)
            else:
                # Transformação de coordenadas
                coordinateTransfTest = QgsCoordinateTransform(teste.sourceCrs(), out_CRS, QgsProject.instance())
                coordinateTransfRef = QgsCoordinateTransform(ref.sourceCrs(), out_CRS, QgsProject.instance())
                coordTransf = True
                SRC = out_CRS
        elif teste.sourceCrs() == ref.sourceCrs():
            if teste.sourceCrs().isGeographic():
                raise QgsProcessingException(msg)
            else:
                SRC = ref.sourceCrs()
        else:
            raise QgsProcessingException(msg)
            
            
        # Verificar se possui coordenada Z e se existe valor igual a zero
        def ValidaZ (pontos, nome):
            for feat in pontos.getFeatures():
                geom = feat.geometry()
                if geom.isMultipart():
                    raise QgsProcessingException(self.tr('A camada {} deve ser do tipo PointZ!'.format(nome)))
                coord = geom.constGet()
                if coord.z() == 'nan':
                    raise QgsProcessingException(self.tr('A camada {} deve ser do tipo PointZ!'.format(nome)))
                if coord.z() == 0:
                    feedback.reportError(self.tr('A feição de id {} da camada {} tem coordenada Z igual a zero!'.format(feat.id(), nome)))
                    
        ValidaZ(teste, self.tr('Teste'))
        ValidaZ(ref, self.tr('Referência'))
        
        
        (sink, dest_id) = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            Fields,
            QgsWkbTypes.LineStringZ,
            SRC
        )
        if sink is None:
            raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))
            
        
        # Pegar pares de pontos homólogos
        dic_test = {}
        for f in teste.getFeatures():
            geom = f.geometry()
            if coordTransf and teste.sourceCrs() != SRC:
                geom.transform(coordinateTransfTest)
            dic_test[f.id()] = geom
            
        
        def PontoMaisProximo(geom):
            distMin = 1e10
            IdMin = None
            for k in dic_test:
                pnt1 = geom.asPoint()
                pnt2 = dic_test[k].asPoint()
                dist = sqrt((pnt1.x()- pnt2.x())**2 + (pnt1.y()- pnt2.y())**2)
                if dist < distMin:
                    distMin = dist
                    IdMin = k
            return IdMin
                
        
        dic_pares = {}
        for feat in ref.getFeatures():
            if campo:
                nome = feat[ columnName ]
            else:
                nome = str(feat.id())
            geomRef = feat.geometry()
            
            if coordTransf and ref.sourceCrs() != SRC:
                geomRef.transform(coordinateTransfRef)
            
            geomTest = dic_test[PontoMaisProximo(geomRef)]
            
            dic_pares[nome] = [ geomTest , geomRef ]
        
        # Cálculo das discrepâncias
        DISCREP = []
        DISCREP_X = []
        DISCREP_Y = []
        DISCREP_Z = []
        DISCREP_XY = []
        DISCREP_XYZ = []
        total = 100.0 / len(dic_pares)
        for current, nome in enumerate(dic_pares):
            p1 = dic_pares[nome][0].constGet()
            p2 = dic_pares[nome][1].constGet()
            x1 = p1.x()
            x2 = p2.x()
            y1 = p1.y()
            y2 = p2.y()
            z1 = p1.z()
            z2 = p2.z()
            # Discrep = Coordendada de Teste - Coord. de Referência
            deltax = x1 - x2
            deltay = y1 - y2
            deltaz = z1 - z2
            DISCREP_X += [deltax]
            DISCREP_Y += [deltay]
            DISCREP_Z += [deltaz]
            discrep_xy = sqrt( (x2 - x1)**2 + (y2 - y1)**2)
            discrep_xyz = sqrt( (x2 - x1)**2 + (y2 - y1)**2 + (z2 - z1)**2)
            DISCREP_XY += [discrep_xy]
            DISCREP_XYZ += [discrep_xyz]
            feature = QgsFeature(Fields)
            feature.setGeometry(QgsLineString([p1, p2]))
            feature.setAttributes([nome] + [float(deltax), float(deltay), float(deltaz), float(discrep_xy), float(discrep_xyz)])
            sink.addFeature(feature, QgsFeatureSink.FastInsert)
            if feedback.isCanceled():
                break
            feedback.setProgress(int((current+1) * total))
        
        
        # Cálculo planimétrico
        feedback.pushInfo(self.tr('Planimetric calculation...', 'Cálculo planimétrico...'))
        
        DISCREP_XY = array(DISCREP_XY)
        RMSE_XY = sqrt((DISCREP_XY*DISCREP_XY).sum()/len(DISCREP_XY))
        RESULTADOS_XY = {}
        for escala in Escalas:
            mudou = False
            for valor in valores[::-1]:
                EM = PEC[escala]['planim'][valor]['EM']
                EP = PEC[escala]['planim'][valor]['EP']
                if (sum(DISCREP_XY<EM)/len(DISCREP_XY))>0.9 and (RMSE_XY < EP):
                    RESULTADOS_XY[escala] = valor
                    mudou = True
            if not mudou:
                RESULTADOS_XY[escala] = 'R'
        
        feedback.pushInfo('RMSE_XY: {:,.[X]f}'.replace('[X]', str(decimal)).format(RMSE_XY))
        for result in RESULTADOS_XY:
            feedback.pushInfo('{} ➜ {}'.format(dicionario[result],RESULTADOS_XY[result]))
        
        # Criacao do arquivo html com os resultados
        DISCREP_X = array(DISCREP_X)
        DISCREP_Y = array(DISCREP_Y)
        
        
        # Cálculo altimétrico
        feedback.pushInfo(self.tr('Altimetric calculation...', 'Cálculo altimétrico...'))
        
        DISCREP_Z = array(DISCREP_Z)
        RMSE_Z = sqrt((DISCREP_Z*DISCREP_Z).sum()/len(DISCREP_Z))
        RESULTADOS_Z = {}
        for escala in Escalas:
            mudou = False
            for valor in valores[::-1]:
                EM = PEC[escala]['altim'][valor]['EM']
                EP = PEC[escala]['altim'][valor]['EP']
                if (sum(DISCREP_Z<EM)/len(DISCREP_Z))>0.9 and (RMSE_Z < EP):
                    RESULTADOS_Z[escala] = valor
                    mudou = True
            if not mudou:
                RESULTADOS_Z[escala] = 'R'
        
        feedback.pushInfo('RMSE_Z: {}'.format(round(RMSE_Z,decimal)))
        for result in RESULTADOS_Z:
            feedback.pushInfo('{} ➜ {}'.format(dicionario[result],RESULTADOS_Z[result]))
        
        # Criacao do arquivo html com os resultados
        DISCREP_X = array(DISCREP_X)
        DISCREP_Y = array(DISCREP_Y)
        DISCREP_Z = array(DISCREP_Z)
        
  
        # Gerar relatorio
        feedback.pushInfo(self.tr('Generating accuracy report...', 'Gerando relatório do PEC-PCD...'))
        arq = open(html_output, 'w')
        
        texto = '''<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
  <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
  <title>''' + str2HTML(self.tr('PLANIALTIMETRIC POSITIONAL ACCURACY', 'ACURÁCIA POSICIONAL PLANIALTIMÉTRICA')) + '''</title>
  <link rel = "icon" href = "https://github.com/LEOXINGU/lftools/blob/main/images/lftools.png?raw=true" type = "image/x-icon">
  <meta name="qrichtext" content="1">
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body style="background-color: rgb(229, 233, 166);">
<div style="text-align: center;"><span style="font-weight: bold;"><br>
    <img height="80" src="data:image/'''+ 'png;base64,' + lftools_logo + '''">
</div>
<div style="text-align: center;"><span style="font-weight: bold; text-decoration: underline;">''' + str2HTML(self.tr('PLANIALTIMETRIC POSITIONAL ACCURACY REPORT (3D)', 'RELATÓRIO DE ACURÁCIA POSICIONAL PLANIALTIMÉTRICA (3D)')) + '''</span><br>
</div>
<br>
<span style="font-weight: bold;"><br>''' + str2HTML(self.tr('EVALUATED DATA', 'DADOS AVALIADOS')) + '''</span><br>
&nbsp;&nbsp;&nbsp; a. ''' + str2HTML(self.tr('Test Points', 'Pontos de Teste')) + ''': [test_points]<br>
&nbsp;&nbsp;&nbsp; b. ''' + str2HTML(self.tr('Reference Points', 'Pontos de Referência')) + ''': [ref_points]<br>
&nbsp;&nbsp;&nbsp; c. ''' + str2HTML(self.tr('Total Number of Homologous Point Pairs', 'Total de pares de pontos homólogos')) + ''': [layer_count]<br>
<span style="font-weight: bold;"><br>
</span>
<p class="MsoNormal"><b>''' + str2HTML(self.tr('PLANIMETRIC POSITIONAL ACCURACY', 'ACURÁCIA POSICIONAL PLANIMÉTRICA')) + ''' (XY)</b><o:p></o:p></p>
&nbsp;&nbsp;&nbsp; <span style="font-weight: bold;">
1. ''' + str2HTML(self.tr('X Discrepancies', 'Discrepâncias em X')) + ''':</span><br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; a. ''' + str2HTML(self.tr('average (tendency)', 'média (tendência)')) + ''': [discrepX_mean] m<br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; b. ''' + str2HTML(self.tr('standard deviation (precision)', 'desvio-padrão (precisão)')) + ''':&nbsp;[discrepX_std] m<br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; c. ''' + str2HTML(self.tr('maximum', 'máxima')) + ''':&nbsp;[discrepX_max] m<br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; d. ''' + str2HTML(self.tr('minimum', 'mínima')) + ''':&nbsp;[discrepX_min] m<br>
&nbsp;<span style="font-weight: bold;">&nbsp;&nbsp;
2. ''' + str2HTML(self.tr('Y Discrepancies', 'Discrepâncias em Y')) + ''':</span><br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; a. ''' + str2HTML(self.tr('average (tendency)', 'média (tendência)')) + ''': [discrepY_mean] m<br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; b. ''' + str2HTML(self.tr('standard deviation (precision)', 'desvio-padrão (precisão)')) + ''':&nbsp;[discrepY_std] m<br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; c. ''' + str2HTML(self.tr('maximum', 'máxima')) + ''':&nbsp;[discrepY_max] m<br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; d. ''' + str2HTML(self.tr('minimum', 'mínima')) + ''':&nbsp;[discrepY_min] m<br>
&nbsp;&nbsp;&nbsp; <span style="font-weight: bold;">
3. ''' + str2HTML(self.tr('RMSE', 'REMQ')) + ''': [RMSE_XY] m</span><br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; a. ''' + str2HTML(self.tr('maximum discrepancy', 'discrepância máxima')) + ''' (XY):&nbsp;[discrepXY_max] m<br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; b. ''' + str2HTML(self.tr('minimum discrepancy', 'discrepância mínima')) + ''' (XY):&nbsp;[discrepXY_min] m<br>
&nbsp;&nbsp;&nbsp; <span style="font-weight: bold;">
4. ''' + str2HTML(self.tr('Cartographic Accuracy Standard', 'Padrão de Exatidão Cartográfica')) + ''' (</span><span  style="font-weight: bold;">PEC-PCD)<br>
</span>&nbsp;&nbsp;&nbsp; <br> [PEC_YX]<br>
<br>
<hr><br>
<p class="MsoNormal"><b>''' + str2HTML(self.tr('VERTICAL POSITIONAL ACCURACY', 'ACURÁCIA POSICIONAL ALTIMÉTRICA')) + ''' (Z)</b><o:p></o:p></p>
&nbsp;&nbsp;&nbsp; <span style="font-weight: bold;">
1. ''' + str2HTML(self.tr('Z Discrepancies', 'Discrepâncias em Z')) + ''':</span><br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; a. ''' + str2HTML(self.tr('average (tendency)', 'média (tendência)')) + ''': [discrepZ_mean] m<br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; b. ''' + str2HTML(self.tr('standard deviation (precision)', 'desvio-padrão (precisão)')) + ''':&nbsp;[discrepZ_std] m<br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; c. ''' + str2HTML(self.tr('maximum', 'máxima')) + ''':&nbsp;[discrepZ_max] m<br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; d. ''' + str2HTML(self.tr('minimum', 'mínima')) + ''':&nbsp;[discrepZ_min] m<br>
&nbsp;<span style="font-weight: bold;"></span>&nbsp;&nbsp;&nbsp; <span style="font-weight: bold;">
2. ''' + str2HTML(self.tr('RMSE', 'REMQ')) + ''':&nbsp;</span><span  style="font-weight: bold;">[RMSE_Z]</span><span  style="font-weight: bold;"> m</span><br>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp; <span style="font-weight: bold;">
3. ''' + str2HTML(self.tr('Cartographic Accuracy Standard', 'Padrão de Exatidão Cartográfica')) + ''' (</span><span  style="font-weight: bold;">PEC-PCD)<br>
</span><br>
[PEC_Z]<br>
<br>
<hr>''' + str2HTML(self.tr('Leandro Franca', 'Leandro França')) + ''' 2025<br>
<address><font size="+l">''' + str2HTML(self.tr('Cartographic Engineer', 'Eng. Cartógrafo')) + '''<br>
email: contato@geoone.com.br<br>
</font>
</address>
</body>
</html>

        '''
        
        def TabelaPEC(RESULTADOS):
            tabela = '''<table style="margin: 0px;" border="1" cellpadding="4"
     cellspacing="0">
      <tbody>
        <tr>'''
            for escala in Escalas:
                tabela += '    <td style="text-align: center; font-weight: bold;">{}</td>'.format(dicionario[escala])
            
            tabela +='''
            </tr>
            <tr>'''
            for escala in Escalas:
                tabela += '    <td style="text-align: center;">{}</td>'.format(RESULTADOS[escala])
            
            tabela +='''
        </tr>
      </tbody>
    </table>'''
            return tabela
        
        valores = {'[test_points]': str2HTML(teste.sourceName()),
                   '[ref_points]': str2HTML(ref.sourceName()),
                   '[layer_count]': str(teste.featureCount()),
                   '[discrepX_mean]': format_num.format(float(DISCREP_X.mean())),
                   '[discrepX_std]': format_num.format(DISCREP_X.std()),
                   '[discrepX_max]': format_num.format(DISCREP_X.max()),
                   '[discrepX_min]': format_num.format(DISCREP_X.min()),
                   
                   '[discrepY_mean]': format_num.format(DISCREP_Y.mean()),
                   '[discrepY_std]': format_num.format(DISCREP_Y.std()),
                   '[discrepY_max]': format_num.format(DISCREP_Y.max()),
                   '[discrepY_min]': format_num.format(DISCREP_Y.min()),
                   
                   '[discrepZ_mean]': format_num.format(DISCREP_Z.mean()),
                   '[discrepZ_std]': format_num.format(DISCREP_Z.std()),
                   '[discrepZ_max]': format_num.format(DISCREP_Z.max()),
                   '[discrepZ_min]': format_num.format(DISCREP_Z.min()),
                   
                   '[RMSE_XY]': format_num.format(RMSE_XY),
                   '[discrepXY_max]': format_num.format(DISCREP_XY.max()),
                   '[discrepXY_min]': format_num.format(DISCREP_XY.min()),
                   
                   '[RMSE_Z]': format_num.format(RMSE_Z),
                   '[discrepZ_max]': format_num.format(DISCREP_Z.max()),
                   '[discrepZ_min]': format_num.format(DISCREP_Z.min()),
                   
                   '[PEC_YX]': TabelaPEC(RESULTADOS_XY),
                   '[PEC_Z]': TabelaPEC(RESULTADOS_Z),
                   }
        
        for valor in valores:
            texto = texto.replace(valor, valores[valor])

        arq.write(texto)
        arq.close()
        
        feedback.pushInfo(self.tr('Operation completed successfully!', 'Operação finalizada com sucesso!'))
        feedback.pushInfo(self.tr('Leandro Franca - Cartographic Engineer', 'Leandro França - Eng Cart'))
        return {self.OUTPUT: dest_id,
                self.HTML: html_output}