Haciendo que Digi3D x64 pueda controlar Google Earth

Las versiones de Digi3D.NET de 64 bits nunca se han podido comunicar con Google Earth.

Ayer por la mañana me propuse solucionar el problema. Ha sido un día entero de frustración que te explico aquí. Hoy por la mañana por fin he dado con la solución.

Si quieres ordenar a Google Earth que haga cosas, como por ejemplo que desplace la cámara a una determinada posición, tienes que hacerlo mediante Component Object Model.

Hacer eso es trivial, primero necesitas tener acceso a los tipos que enumera el servidor COM, Visual Studio te lo facilita con la directiva #import.

Lo que no soluciona Visual Studio de forma automática es el problema que se presenta en Digi3D x64 al conectarse con el servidor COM. El problema es el siguiente:

Google Earth es una aplicación de 32 bits y se comporta como un servidor COM de tipo Inproc Server

Lo importante aquí es que se comporta como un Inproc Server, y eso significa que no deja de ser una DLL que se adjunta al proceso de la aplicación que la instancia (vamos que forma parte del mismo proceso). Si una aplicación es de 64 bits, las DLLs que cargue tienen que ser de 64 bits, no puede cargar una de 32 bits, independientemente de cómo se cargue esa DLL (LoadLibrary que sería la forma normal o mediante CoCreateInstance que es el método que se utiliza con COM).

La solución sería instalar una versión de 64 bits de Google Earth para que Digi3D.NET de 64 bits si que pueda localizar el servidor COM de 64 bits, pero no pierdas el tiempo (ya lo he perdido yo por ti), no existe una versión de 64 bits de Google Earth.

La única solución es hacer que Google Earth sea un Out-of-proc Server, y eso significa que el servidor COM sea un ejecutable y no una DLL. Cuando instancias un Out-of-proc Server, Windows se encarga de todo lo necesario para poder comunicar la aplicación cliente de 64 bits con el servidor de 32. En este caso es posible porque son dos procesos distintos, por lo tanto el servidor ya no forma parte del cliente, y por lo tanto existen mecanismos de comunicación entre procesos que compatibilizan ambas plataformas.

Como no puedo modificar Google Earth para que sea un Out-of-proc server (porque si tuviera el código fuente de Google Earth obviamente lo compilaría en 64 bits y me olvidaría del problema), no queda más remedio que desarrollar un intermediario (que llamaré Proxy) entre Digi3D.NET y Google Earth.

Este proxy va a ser un servidor COM Out-of-proc y va a implementar un interfaz con los métodos que requiere Digi3D.NET para poder ordenar a Google Earth a que se desplace a unas determinadas coordenadas.

Puedes descargar el código fuente del proyecto de nuestro repositorio de código fuente en nuestro repositorio de código fuente GitHub,  Básicamente creamos el servidor como un proyecto de tipo ATL, y luego hacemos una aplicación cliente que nos va a servir a comprobar que funciona.

…y como una imagen vale más que mil palabras, aquí tienes el vídeo en el que desarrollo esa solución. Espero que te guste.

[youtube:https://youtu.be/6KSbIZUgJVs%5D

Marcando entidades eliminadas en archivos .KML

En la entrada KML pasa a ser un ciudadano de primera categoría en Digi3D.NET en el blog de Digi21 informamos que ahora podemos trabajar con archivos .kml de forma nativa.

Una de las características más importantes en Digi3D.NET es la posibilidad de marcar entidades como eliminadas. Gracias a podemos recuperar entidades eliminadas y podemos deshacer operaciones mediante la orden UNDO.

Los archivos .kml no tienen el concepto de Entidad marcada como eliminada. Si quieres eliminar la entidad, la eliminas del archivo .kml.

Pero afortunadamente los archivos .kml son archivos .xml, y existe una cosa maravillosa llamada Espacios de nombres y es gracias a esto como conseguimos eliminar entidades en un archivo .kml.

Este es el comienzo de un archivo .kml puro:

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://earth.google.com/kml/2.2">
    <Document>

Y este otro es el comienzo de un archivo .kml en el cual hemos eliminado una entidad con Digi3D.NET:

<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://earth.google.com/kml/2.2" 
     xmlns:d3d="http://schemas.digi21.net/digi3d/extensions/1.0">
    <Document>

Como puedes comprobar, Digi3D.NET ha añadido la declaración del espacio de nombres http://schemas.digi21.net/digi3d/extensions/1.0 (no intentes entrar en esa dirección porque no es una URL, no vas a encontrar nada en esa dirección) y se indica que en el documento se va a referir a ese espacio de nombres mediante el prefijo: d3d.

Veamos ahora el comienzo de una entidad en el archivo .kml:

        <Placemark>
            <name>160101name>
            <description>LINEA DE ACERAdescription>

y ahora cómo marca Digi3D.NET esa misma entidad como eliminada:

        <Placemark d3d:Deleted="-1">
            <name>160101name>
            <description>LINEA DE ACERAdescription>

Como puedes comprobar, Digi3D.NET añade al nodo Placemark un atributo de tipo d3d:Deleted. Como este atributo pertenece al espacio de nombres http://schemas.digi21.net/digi3d/extensions/1.0 para Google Earth es como si no existiera. Hemos añadido información sin afectar en absoluto al formato original.

Para recuperar una entidad con Digi3D.NET tenemos dos posibilidades, o cambiamos el valor del atributo de -1 (en realidad sería cualquier número distinto de 0) a 0 o directamente eliminamos el atributo. Digi3D.NET se decanta por esta última opción: elimina el atributo.

Cuando comprimes el archivo de se eliminan del archivo .kml los nodos que tengan asociado un atributo de tipo d3d:Deleted con valor distinto de cero.

A continuación te muestro cómo añadimos el atributo con la definición del espacio de nombres en el nodo raíz del documento .xml (esta parte de Digi3D.NET está programada en C++), y se utiliza MSXML para manipular archivos .xml:

auto atributo = docPtr->createNode(MSXML2::NODE_ATTRIBUTE, _bstr_t("xmlns:d3d"), _bstr_t(""));
atributo->nodeValue = _variant_t("http://schemas.digi21.net/digi3d/extensions/1.0");
docPtr->documentElement->Getattributes()->setNamedItem(atributo);

De la siguiente manera añadimos a una entidad un atributo para indicar que la entidad está eliminada:

auto atributo = docPtr->createNode(MSXML2::NODE_ATTRIBUTE, _bstr_t("d3d:Deleted"), _bstr_t("http://schemas.digi21.net/digi3d/extensions/1.0"));
atributo->nodeValue = _variant_t(true);
nodo->Getattributes()->setNamedItem(atributo);

De la siguiente manera eliminamos un atributo:

nodo->Getattributes()->removeQualifiedItem(_bstr_t("Deleted"), _bstr_t("http://schemas.digi21.net/digi3d/extensions/1.0"));

…y por último, de esta manera comprobamos si la entidad está eliminada cuando cargamos el archivo de dibujo:

auto eliminado = placemark->selectSingleNode(_bstr_t("@d3d:Deleted"));
if( eliminado != NULL )
  entidad->Borrado = (bool)eliminado->nodeValue;

El extraño caso del archivo de dibujo que se duplicaba al comprimir

Un usuario de Digi3D.NET se puso en contacto conmigo porque al comprimir un archivo se le duplican las entidades.

En realidad lo que me quería decir (lo averigüé tras conectarme a su ordenador con TeamViewer) es que se le duplicaba el archivo de dibujo físicamente, se multiplicaba por dos. No que se le duplicaban las entidades en memoria, pero si cerraba el archivo y lo volvía a abrir, efectivamente sí que estaban las entidades duplicadas. Inmediatamente vino a mi cabeza la palabra Karspersky y le pregunté qué antivirus tenía. Efectivamente, el antivirus era Karspersky.

Le dije que deshabilitara unos días el antivirus porque me imaginaba que sería el culpable. Ese antivirus siempre ha molestado a Ortobatch y estaba convencido de que este era el culpable de que se duplicara el archivo. Obviamente cuando sueltas una respuesta como esta el usuario piensa que estás equivocado, pero no es así. Le expliqué lo siguiente:

Cuando ordenas a Digi3D.NET que comprima, éste realiza las siguientes tareas:

  1. Elimina de la memoria principal las entidades marcadas como borradas.
  2. Le ordena al importador/exportador que elimine físicamente del archivo de dibujo las entidades marcadas como borradas.

En el caso de ser un archivo de dibujo de Geomedia por ejemplo, el importador ordena a la base de datos que elimine los registros correspondientes. Aquí el gestor de bases de datos es el responsable de eliminar.

En el caso de ser un archivo de dibujo en con extensiones .bind o .bin se realizan los siguientes pasos y en este orden:

  1. Se cierra el archivo de dibujo.
  2. Se elimina el archivo de dibujo.
  3. Se crea un nuevo archivo de dibujo donde se almacenan las entidades no eliminadas.

El problema es que el antivirus no permitía que Digi3D.NET eliminara el archivo, porque al descubrir que Digi3D.NET lo había cerrado, lo abría para analizarlo antes de que Digi3D.NET ordenase que se elimine, haciendo que el archivo en realidad no se eliminase. En el siguiente punto, el exportador abre el archivo para escritura, pero como ya existía no se truncaba. Cuando el exportador almacenaba las entidades no eliminadas, lo hacía al final del archivo, de modo que efectivamente estas se duplicaban.

Si el problema era este, había que solucionarlo en Digi3D.NET haciendo que no fuera tan confiado a la hora de eliminar el archivo y ordenándole comprobar que el archivo ha sido eliminado en vez de confiar en que éste se ha eliminado.

Para comprobar que efectivamente era el antivirus, así que instalé éste en una máquina virtual y ejecuté el siguiente programa

while(true) {
        CFile is;
        if( !is.Open(_T("c:\\prueba.bin"), CFile::modeCreate | CFile::modeWrite) ) {
            _tprintf(_T("Error al crear el archivo"));
            return 1;
        }

        TCHAR* cadena = new TCHAR[256];
        is.Write(cadena, 256);
        delete[] cadena;

        is.Close();
        if( -1 == _tunlink(_T("c:\\prueba.bin"))) {
            _tprintf(_T("Error al eliminar el archivo"));
            return 2;
        }

        _tprintf(_T("*"));
    }

Que crea un archivo, almacena información, lo cierra y lo elimina infinitas veces.

Puedes comprobar el resultado de la prueba en el siguiente vídeo:

[youtube:http://youtu.be/Vzne7nMJMkU%5D

Como puedes comprobar, efectivamente la llamada a _tunlink devuelve -1 y porque Karspersky tiene bloqueado el archivo.

Obviamente hay que buscar una solución en Digi3D.NET que ha consistido en hacer que todas las llamadas a _tunlink estén dentro de un bucle infinito cuya manera de salir es únicamente cuando se ha podido eliminar el archivo sin ningún problema. En caso de que no se pueda eliminar el archivo, se muestra el siguiente cuadro de tareas informando al usuario del problema

TaskDialog mostrando error al comprimir (no se pudo eliminar archivo)

Desarrollando un transformador universal de coordenadas

Transformador Universal de Coordenadas

Hemos incorporado a Digi3D.NET un programa que permite transformar coordenadas de cualquier sistema de referencia de coordenadas a cualquier sistema de referencia de coordenadas utilizando los objetos .NET proporcionados por Digi3D.NET.

Puedes localizar el programa en Inicio/Todos los programas/Digi21.net/Digi3D.NET/Transformador universal de coordenadas.
Puedes descargar el código fuente del programa para estudiarlo y manipularlo a tu antojo de nuestro repositorio de código fuente en GitHub, en https://github.com/digi21/TransformadorUniversalCoordenadas

Aquí te voy a explicar a grandes rasgos los pasos que he seguido para desarrollar este programa:

  1. He creado un proyecto con Visual Studio 2010 denominado TransformadorUniversalCoordenadas. Es un proyecto desarrollado en el lenguaje C#, para Windows y de tipo WPF (Windows Presentation Foundation).
  2. He creado el interfaz de usuario, que es muy muy sencillo, tan solo tiene unos controles para solicitar el sistema de coordenadas de referencia origen, el destino, unos botones para seleccionarlos, y por último dos TextBlock, uno a la izquierda en el que el usuario podrá teclear o pegar coordenadas y otro a la derecha que es de solo lectura y que mostrará el resultado de la transformación.
  3. He añadido una nueva ventana para mostrar los posibles mensajes de error que pueden suceder al intentar localizar una transformación entre dos sistemas de referencia de coordenadas.
  4. He añadido una referencia al ensamblado Digi21.Epsg. Este ensamblado básicamente proporciona métodos para mostrar cuadros de diálogo para que el usuario seleccione un sistema de referencia de coordenadas.
  5. He añadido una referencia al ensamblado Digi21.OpenGis. Este ensamblado proporciona la implementación de Open Geospatial Consortium Coordinate Transformation Service.
  6. He añadido un archivo de configuración al proyecto (App.config) para añadir la cadena de conexión a la base de datos EPSG y para especificar el directorio donde ubicar los archivos necesarios para hacer transformaciones (rejillas, …).
  7. Y por último he añadido la lógica del programa que es muy sencilla. Tan solo hay que instanciar sistemas de referencia de coordenadas, instanciar una transformación y transformar coordenadas al pegar el usuario un listado de coordenadas en la parte izquierda del programa.

    El ensamblado Digi21.OpenGis implementa dos fábricas, una para instanciar sistemas de referencia de coordenadas y otra para instanciar transformaciones.

    El programa almacena en dos campos las dos fábricas tal y como puedes ver a continuación:

    private CoordinateSystemFactory fábricaSrc = new CoordinateSystemFactory();
    private CoordinateTransformationFactory fábricaTransformaciones = new CoordinateTransformationFactory();
    

    Solicitar al usuario que seleccione un sistema de referencia de coordenadas es muy sencillo, tan solo tienes que llamar a EpsgManager.DialogSelectCrs.
    Luego puedes instanciar un sistema de referencia de coordenadas mediante una llamada al método CreateFromWkt de la fábrica de sistemas de referencia de coordenadas.

    
            private void BotonLocalizarSrcOrigen_Click(object sender, RoutedEventArgs e)
            {
                var wktOrigen = EpsgManager.DialogSelectCrs("Selecciona el sistema de referencia de coordenadas origen", origen);
    
                var nuevoOrigen = fábricaSrc.CreateFromWkt(wktOrigen);
    
                if (AsignaTransformación(nuevoOrigen, destino))
                {
                    origen = nuevoOrigen;
                    TransformaPuntos();
                }
            }
    

    Una vez que tienes los dos sistemas de referencia de coordenadas, tan solo tienes que localizar una transformación entre ambos mediante el método CreateFromCoordinateSystems de la fábrica de transformaciones:

            private bool AsignaTransformación(CoordinateSystem origen, CoordinateSystem destino)
            {
                try
                {
                    transformación = fábricaTransformaciones.CreateFromCoordinateSystems(
                        origen,
                        destino,
                        SelectTransformationHelper.DialogSelectTransformation,
                        CreateVerticalTransformationHelper.DialogCreateVerticalTransformation);
                    return true;
                }
                catch (Exception e)
                {
                    MostrarExcepción dlg = new MostrarExcepción
                    {
                        Origen = origen.Name,
                        Destino = destino.Name,
                        Mensaje = e.Message
                    };
    
                    dlg.ShowDialog();
                    return false;
                }
            }
    

    Y por último sólo tienes que transformar las coordenadas llamando al método Transform de la transformación matemática obtenida en el punto anterior:

    var transformado = transformación.MathTransform.Transform(coordenadas);

Nueva orientación absoluta serializada como una cadena Well Known Text

Tal y como te explico en el post Novedades en el cálculo de orientaciones absolutas en el blog de novedades de Digi3D.NET, ahora el programa realiza el cálculo de las orientaciones absolutas en un sistema intermedio.

La decisión de en qué sistema se calcula la orientación explicada en el enlace anterior. Lo que te voy a explicar aquí es cómo se almacenan en el archivo .abs.xml las operaciones matemáticas necesarias para transformar un punto Terreno a Local y de Local a Modelo.

Una opción hubiera sido no almacenar en el archivo .abs.xml ninguna transformación, sino haber almacenado simplemente una cadena WKT con la definición del sistema de coordenadas de referencia de los puntos de apoyo por un lado y del sistema intermedio por otro y que luego la orientación absoluta al cargarse en memoria crease las transformaciones necesarias en función de esos datos, pero esa solución no es válida, pues no existen sistemas de coordenadas de referencia topocéntricos. Puedes crear un sistema topocéntrico en cualquier sitio, con origen en cualquier coordenada, de modo que hay infinitos sistemas topocéntricos y lógicamente no aparece ninguna en la base de datos EPSG.

De modo que la solución no es almacenar en el archivo de orientación el sistema de coordenadas de referencia intermedio, sino almacenar la serie de operaciones necesarias para transformar del sistema de coordenadas de referencia de los puntos de apoyo (que siempre será conocido, aunque sea local, pero es conocido) al sistema intemedio, que puede ser o no uno estándar, nos da igual, porque no nos interesa almacenar el sistema, sino las operaciones para llegar a él, y lo mismo del sistema intermedio al sistema modelo.

Podríamos haber almacenado estas transformaciones en un formato inventado, pero Digi3D.NET implementa el estándar Coordinate Transformation Services del Open Geospatial Consortium y eso significa que Digi3D.NET puede crear una cadena de texto Well Known Text a partir de una serie de transformaciones y al revés, puede crear en memoria una serie de transformaciones a partir de la información de una cadena Well Known Text.

Las operaciones matemáticas definidas en el estándar Coordinate Transformation Services son muy restrictivas, y todas ellas cumplen una serie de propiedades:

  1. Las operaciones que trabajan con coordenadas geográficas, siempre esperan recibir primero una longitud y luego la latitud, nunca al revés.
  2. Las operaciones que trabajan con coordenadas geográficas, siempre reciben las coordenadas en grados sexagesimales, y devuelves sexagesimales. Nunca reciben ni radianes ni centesimales.
  3. Cada operación informa del número coordenadas que recibe y del número de coordenadas que devuelve.

    Hay operaciones que reciben una coordenada y devuelven otra, como por ejemplo la operación vertical_offset que recibe una coordenada Z y devuelve otra coordenada Z, hay otras operaciones que reciben tres coordenadas y devuelven una, como por ejemplo la operación Geographic3DToGravityRelatedHeightEGM, que recibe una longitud/latitud/altitud y que devuelven una coordenada Z ortométrica, y así sucesivamente.

No se pueden pasar tres coordenadas a una operación que espera recibir dos, pues se lanzará una excepción. Digi3D.NET tiene en cuenta todos estos factores, y eso se ve reflejado en la cadena que aparece en el archivo .abs.xml>.

El estándar Coordinate Transformation Services da nombres a las distintas operaciones, ya hemos visto alguna, como vertical_offset o Geographic3DToGravityRelatedHeightEGM. Hay muchas, algunas para realizar operaciones de proyecciones de coordenadas, como transverse_mercator, cassini_soldner, … y existen dos en particular que utiliza mucho Digi3D.NET para resolver el problema de llamar a una función que recibe dos coordenadas cuando tengo tres por ejemplo.

Affine

Esta operación básicamente crea una transformación afín a partir de una matriz. Si el número de dimensiones de entrada es M y el número de dimensiones de salida es N, entonces la matriz tendrá un tamaño de [N+1][M+1]. El +1 en las dimensiones de la matriz permiten a la matriz realizar un desplazamiento además de una rotación.

Podemos utilizar una afín para mover una coordenada de posición. Digi3D.NET en ocasiones decide que tiene que cambiar el orden de la coordenada Z de X,Y,Z a Z,X,Y. Para ello utiliza la siguiente matriz:

0 0 1 0
1 0 0 0
0 1 0 0
0 0 0 1

Y al revés, transformar de Z,X,Y a X,Y,Z utilizando la siguiente matriz:

0 1 0 0
0 0 1 0
1 0 0 0
0 0 0 1

La orientación afín nos permite cambiar de dimensiones, de manera que si queremos una matriz que transforme las coordenadas X,Y,Z en X,Y, es decir, eliminar una dimensión, podemos aplicar la siguiente afín:

1 0 0 0
0 1 0 0
0 0 0 1

o justo lo contrario, en ocasiones, Digi3D.NET tiene que duplicar alguna coordenada, como por ejemplo pasar de X,Y,Z a X,Y,X,Y,Z mediante la siguiente matriz:

1 0 0 0
0 1 0 0
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1

Passthrough

Esta operación permite ejecutar una operación matemática sobre un subconjunto de coordenadas de entre las que recibe como parámetros.

Esta operación matemática recibe dos parámetros:

  1. El número de coordenadas a saltar de entre las coordenadas recibidas para hacer la transformación.
  2. Una operación matemática a realizar con el resto de coordenadas recibidas para hacer la transformación.

Con un ejemplo lo vas a entender mejor:

Supongamos que tenemos las coordenadas X,Y,Z y queremos llamar a la operación matemática lambert_to_elipsoid (que debe recibir únicamente las coordenadas X,Y y devuelve lontigud, latitud.

No podemos llamar a lambert_to_elipsoid y pasarle nuestras coordenadas X,Y,Z, pues esta operación matemática lanzará una excepción si le pasamos tres parámetros. Recibe 2 y devuelve 2. No es posible saltarse esta restricción.

Si Digi3D.NET se encuentra con este caso realizará las siguientes operaciones:

  1. Convertir la coordenada X,Y,Z en Z,X,Y (mediante una afín de las que hemos visto en el punto anterior).
  2. Llamar a la operación PASSTHROUGH(1, lambert_to_elipsoid). Esto devolverá Z,longitud,latitud.
  3. Convertir la coordenada Z,longitud,latitud en longitud,latitud,Z (mediante una afín de las que hemos visto en el punto anterior).

En ocasiones hay que transformar la coordenada Z (por ejemplo transformarla de elipsoidal a ortométrica). Existen dos tipos de operaciones matemáticas en lo referente a la coordenada Z:

  1. Las que reciben una coordenada Z y devuelven una coordenada Z’
  2. Las que reciben tres coordenadas X,Y,Z y devuelven una coordenada Z’.

El primer caso es trivial: si tenemos X,Y,Z y queremos X,Y,Z’, tan solo tenemos que llamar a la operación matemática que transforma la coordenada Z en Z’ mediante un PASSTHROUGH(2, operación_matemática), pero el otro caso requiere duplicar las coordenadas XY mediante el siguiente algoritmo:

  1. Convertimos X,Y,Z en X,Y,X,Y,Z mediante una operación afín.
  2. Llamamos a la operación que convierte de X,Y,Z a Z’ mediante un PASSTHROUGH(2, operación matemática)

A continuación tienes el contenido de una orientación absoluta hipotética de un sensor satelital en el que las coordenadas de los puntos de apoyo están en el sistema de coordenadas de referencia Merchich / Sud Maroc + EGM2008 geoid height y las coordenadas del sensor obviamente en WGS 84 3D.

<?xml version='1.0' encoding='ISO-8859-1'?>
<absolute xmlns='http://schemas.digi21.net/Digi3D/AbsoluteOrientation/v1.0' translationX='14013454.46506193800000000000' translationY='-1477144.60782388760000000000' translationZ='9031256.06252797320000000000' controlPointCoordinateSystem='COMPD_CS["Merchich / Sud Maroc + EGM2008 geoid height",PROJCS["Merchich / Sud Maroc",GEOGCS["Merchich",DATUM["Merchich",SPHEROID["Clarke 1880 (IGN)",6378249.2,293.4660212936269,AUTHORITY["EPSG","7011"]],AUTHORITY["EPSG","6261"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["°",0.01745329251994328,AUTHORITY["EPSG","9122"]],AXIS["Lat",North],AXIS["Long",East],AUTHORITY["EPSG","4261"]],PROJECTION["Lambert_Conformal_Conic_1SP"],PARAMETER["latitude_of_origin",29.69999999999997],PARAMETER["central_meridian",-5.399999999999994],PARAMETER["scale_factor",0.9996155960000001],PARAMETER["false_easting",500000],PARAMETER["false_northing",300000],PARAMETER["semi_major",6378249.2],PARAMETER["semi_minor",6356515],UNIT["metros",1,AUTHORITY["EPSG","9001"]],AXIS["X",East],AXIS["Y",North],AUTHORITY["EPSG","26192"]],VERT_CS["EGM2008 geoid height",VERT_DATUM["EGM2008 geoid",2005,AUTHORITY["EPSG","1027"]],UNIT["metros",1,AUTHORITY["EPSG","9001"]],AXIS["H",Up],AUTHORITY["EPSG","3855"]]]' mathTransformGroundToLocal='CONCAT_MT[PARAM_MT["Affine", PARAMETER["num_row",4], PARAMETER["num_col", 4],PARAMETER["elt_0_0", 0],PARAMETER["elt_0_1", 0],PARAMETER["elt_0_2", 1],PARAMETER["elt_0_3", 0],PARAMETER["elt_1_0", 1],PARAMETER["elt_1_1", 0],PARAMETER["elt_1_2", 0],PARAMETER["elt_1_3", 0],PARAMETER["elt_2_0", 0],PARAMETER["elt_2_1", 1],PARAMETER["elt_2_2", 0],PARAMETER["elt_2_3", 0],PARAMETER["elt_3_0", 0],PARAMETER["elt_3_1", 0],PARAMETER["elt_3_2", 0],PARAMETER["elt_3_3", 1]],PASSTHROUGH_MT[1,CONCAT_MT[INVERSE_MT[PARAM_MT["Lambert_Conformal_Conic_1SP",PARAMETER["latitude_of_origin",29.69999999999997],PARAMETER["central_meridian",-5.399999999999994],PARAMETER["scale_factor",0.9996155960000001],PARAMETER["false_easting",500000],PARAMETER["false_northing",300000],PARAMETER["semi_major",6378249.2],PARAMETER["semi_minor",6356515]]],PARAM_MT["Affine", PARAMETER["num_row",3], PARAMETER["num_col", 3],PARAMETER["elt_0_0", 0],PARAMETER["elt_0_1", 1],PARAMETER["elt_0_2", 0],PARAMETER["elt_1_0", 1],PARAMETER["elt_1_1", 0],PARAMETER["elt_1_2", 0],PARAMETER["elt_2_0", 0],PARAMETER["elt_2_1", 0],PARAMETER["elt_2_2", 1]]]],PARAM_MT["Affine", PARAMETER["num_row",4], PARAMETER["num_col", 4],PARAMETER["elt_0_0", 0],PARAMETER["elt_0_1", 1],PARAMETER["elt_0_2", 0],PARAMETER["elt_0_3", 0],PARAMETER["elt_1_0", 0],PARAMETER["elt_1_1", 0],PARAMETER["elt_1_2", 1],PARAMETER["elt_1_3", 0],PARAMETER["elt_2_0", 1],PARAMETER["elt_2_1", 0],PARAMETER["elt_2_2", 0],PARAMETER["elt_2_3", 0],PARAMETER["elt_3_0", 0],PARAMETER["elt_3_1", 0],PARAMETER["elt_3_2", 0],PARAMETER["elt_3_3", 1]],PARAM_MT["Affine", PARAMETER["num_row",4], PARAMETER["num_col", 4],PARAMETER["elt_0_0", 0],PARAMETER["elt_0_1", 1],PARAMETER["elt_0_2", 0],PARAMETER["elt_0_3", 0],PARAMETER["elt_1_0", 1],PARAMETER["elt_1_1", 0],PARAMETER["elt_1_2", 0],PARAMETER["elt_1_3", 0],PARAMETER["elt_2_0", 0],PARAMETER["elt_2_1", 0],PARAMETER["elt_2_2", 1],PARAMETER["elt_2_3", 0],PARAMETER["elt_3_0", 0],PARAMETER["elt_3_1", 0],PARAMETER["elt_3_2", 0],PARAMETER["elt_3_3", 1]],PASSTHROUGH_MT[2,INVERSE_MT[PARAM_MT["vertical_offset", PARAMETER["vertical_offset",0.000000]]]],PARAM_MT["Ellipsoid_To_Geocentric", PARAMETER["semi_major",6378249.2], PARAMETER["semi_minor", 6356515]]]' mathTransformLocalToGround='CONCAT_MT[PARAM_MT["Geocentric_To_Ellipsoid", PARAMETER["semi_major",6378249.2], PARAMETER["semi_minor", 6356515]],PASSTHROUGH_MT[2,PARAM_MT["vertical_offset", PARAMETER["vertical_offset",0.000000]]],PARAM_MT["Affine", PARAMETER["num_row",4], PARAMETER["num_col", 4],PARAMETER["elt_0_0", 0],PARAMETER["elt_0_1", 1],PARAMETER["elt_0_2", 0],PARAMETER["elt_0_3", 0],PARAMETER["elt_1_0", 1],PARAMETER["elt_1_1", 0],PARAMETER["elt_1_2", 0],PARAMETER["elt_1_3", 0],PARAMETER["elt_2_0", 0],PARAMETER["elt_2_1", 0],PARAMETER["elt_2_2", 1],PARAMETER["elt_2_3", 0],PARAMETER["elt_3_0", 0],PARAMETER["elt_3_1", 0],PARAMETER["elt_3_2", 0],PARAMETER["elt_3_3", 1]],PARAM_MT["Affine", PARAMETER["num_row",4], PARAMETER["num_col", 4],PARAMETER["elt_0_0", 0],PARAMETER["elt_0_1", 0],PARAMETER["elt_0_2", 1],PARAMETER["elt_0_3", 0],PARAMETER["elt_1_0", 1],PARAMETER["elt_1_1", 0],PARAMETER["elt_1_2", 0],PARAMETER["elt_1_3", 0],PARAMETER["elt_2_0", 0],PARAMETER["elt_2_1", 1],PARAMETER["elt_2_2", 0],PARAMETER["elt_2_3", 0],PARAMETER["elt_3_0", 0],PARAMETER["elt_3_1", 0],PARAMETER["elt_3_2", 0],PARAMETER["elt_3_3", 1]],PASSTHROUGH_MT[1,CONCAT_MT[PARAM_MT["Affine", PARAMETER["num_row",3], PARAMETER["num_col", 3],PARAMETER["elt_0_0", 0],PARAMETER["elt_0_1", 1],PARAMETER["elt_0_2", 0],PARAMETER["elt_1_0", 1],PARAMETER["elt_1_1", 0],PARAMETER["elt_1_2", 0],PARAMETER["elt_2_0", 0],PARAMETER["elt_2_1", 0],PARAMETER["elt_2_2", 1]],PARAM_MT["Lambert_Conformal_Conic_1SP",PARAMETER["latitude_of_origin",29.69999999999997],PARAMETER["central_meridian",-5.399999999999994],PARAMETER["scale_factor",0.9996155960000001],PARAMETER["false_easting",500000],PARAMETER["false_northing",300000],PARAMETER["semi_major",6378249.2],PARAMETER["semi_minor",6356515]]]],PARAM_MT["Affine", PARAMETER["num_row",4], PARAMETER["num_col", 4],PARAMETER["elt_0_0", 0],PARAMETER["elt_0_1", 1],PARAMETER["elt_0_2", 0],PARAMETER["elt_0_3", 0],PARAMETER["elt_1_0", 0],PARAMETER["elt_1_1", 0],PARAMETER["elt_1_2", 1],PARAMETER["elt_1_3", 0],PARAMETER["elt_2_0", 1],PARAMETER["elt_2_1", 0],PARAMETER["elt_2_2", 0],PARAMETER["elt_2_3", 0],PARAMETER["elt_3_0", 0],PARAMETER["elt_3_1", 0],PARAMETER["elt_3_2", 0],PARAMETER["elt_3_3", 1]]]' mathTransformModelToLocal='CONCAT_MT[PARAM_MT["Affine", PARAMETER["num_row",4], PARAMETER["num_col", 4],PARAMETER["elt_0_0", 0],PARAMETER["elt_0_1", 1],PARAMETER["elt_0_2", 0],PARAMETER["elt_0_3", 0],PARAMETER["elt_1_0", 1],PARAMETER["elt_1_1", 0],PARAMETER["elt_1_2", 0],PARAMETER["elt_1_3", 0],PARAMETER["elt_2_0", 0],PARAMETER["elt_2_1", 0],PARAMETER["elt_2_2", 1],PARAMETER["elt_2_3", 0],PARAMETER["elt_3_0", 0],PARAMETER["elt_3_1", 0],PARAMETER["elt_3_2", 0],PARAMETER["elt_3_3", 1]],PARAM_MT["Affine", PARAMETER["num_row",4], PARAMETER["num_col", 4],PARAMETER["elt_0_0", 1],PARAMETER["elt_0_1", 0],PARAMETER["elt_0_2", 0],PARAMETER["elt_0_3", 0],PARAMETER["elt_1_0", 0],PARAMETER["elt_1_1", 1],PARAMETER["elt_1_2", 0],PARAMETER["elt_1_3", 0],PARAMETER["elt_2_0", 0],PARAMETER["elt_2_1", 0],PARAMETER["elt_2_2", 1],PARAMETER["elt_2_3", 0],PARAMETER["elt_3_0", 0],PARAMETER["elt_3_1", 0],PARAMETER["elt_3_2", 0],PARAMETER["elt_3_3", 1]],PARAM_MT["Ellipsoid_To_Geocentric", PARAMETER["semi_major",6378137], PARAMETER["semi_minor", 6356752.314245179]]]' mathTransformLocalToModel='CONCAT_MT[PARAM_MT["Geocentric_To_Ellipsoid", PARAMETER["semi_major",6378137], PARAMETER["semi_minor", 6356752.314245179]],PARAM_MT["Affine", PARAMETER["num_row",4], PARAMETER["num_col", 4],PARAMETER["elt_0_0", 1],PARAMETER["elt_0_1", 0],PARAMETER["elt_0_2", 0],PARAMETER["elt_0_3", 0],PARAMETER["elt_1_0", 0],PARAMETER["elt_1_1", 1],PARAMETER["elt_1_2", 0],PARAMETER["elt_1_3", 0],PARAMETER["elt_2_0", 0],PARAMETER["elt_2_1", 0],PARAMETER["elt_2_2", 1],PARAMETER["elt_2_3", 0],PARAMETER["elt_3_0", 0],PARAMETER["elt_3_1", 0],PARAMETER["elt_3_2", 0],PARAMETER["elt_3_3", 1]],PARAM_MT["Affine", PARAMETER["num_row",4], PARAMETER["num_col", 4],PARAMETER["elt_0_0", 0],PARAMETER["elt_0_1", 1],PARAMETER["elt_0_2", 0],PARAMETER["elt_0_3", 0],PARAMETER["elt_1_0", 1],PARAMETER["elt_1_1", 0],PARAMETER["elt_1_2", 0],PARAMETER["elt_1_3", 0],PARAMETER["elt_2_0", 0],PARAMETER["elt_2_1", 0],PARAMETER["elt_2_2", 1],PARAMETER["elt_2_3", 0],PARAMETER["elt_3_0", 0],PARAMETER["elt_3_1", 0],PARAMETER["elt_3_2", 0],PARAMETER["elt_3_3", 1]]]'>
    <matrix omega='3.56928506875955740000' phi='-6.94752310306991740000' kappa='-156.36142599315596000000' scale='1.62608831756480400000'>
        <m row='0' col='0'>-0.76951445000824426000</m>
        <m row='0' col='1'>-0.62731999435274888000</m>
        <m row='0' col='2'>-0.11965423483427851000</m>
        <m row='1' col='0'>0.62927342674164544000</m>
        <m row='1' col='1'>-0.77676685562447978000</m>
        <m row='1' col='2'>0.02545989788050484500</m>
        <m row='2' col='0'>-0.10891494674899546000</m>
        <m row='2' col='1'>-0.05570347106353324600</m>
        <m row='2' col='2'>0.99248912220040875000</m>
    </matrix>
    <points mp='122.183975'>
        <point id='PC101' useXY='1' useZ='1'>
            <ground>
                <x>460320.184000</x>
                <y>626570.439000</y>
                <z>10.627000</z>
            </ground>
            <model>
                <x>-25.93169811989125200000</x>
                <y>32.57041742280095800000</y>
                <z>107.54832105804235000000</z>
            </model>
            <residual>
                <x>-164.295424</x>
                <y>52.099229</y>
                <z>-0.240689</z>
            </residual>
        </point>
        <point id='PC102' useXY='1' useZ='1'>
            <ground>
                <x>457916.886000</x>
                <y>624247.954000</y>
                <z>12.069000</z>
            </ground>
            <model>
                <x>-25.94416234828861400000</x>
                <y>32.58570876952164500000</y>
                <z>107.54832105804235000000</z>
            </model>
            <residual>
                <x>-28.990795</x>
                <y>-74.345370</y>
                <z>0.679271</z>
            </residual>
        </point>
        <point id='PC103' useXY='1' useZ='1'>
            <ground>
                <x>459179.334000</x>
                <y>623462.274000</y>
                <z>20.536000</z>
            </ground>
            <model>
                <x>-25.94941176839595100000</x>
                <y>32.57867438360941700000</y>
                <z>107.54837954837430000000</z>
            </model>
            <residual>
                <x>106.274481</x>
                <y>109.752252</y>
                <z>-0.886566</z>
            </residual>
        </point>
        <point id='PC104' useXY='1' useZ='1'>
            <ground>
                <x>463822.512000</x>
                <y>626685.132000</y>
                <z>10.102000</z>
            </ground>
            <model>
                <x>-25.92991593921098900000</x>
                <y>32.55051554021122200000</y>
                <z>117.54652568697929000000</z>
            </model>
            <residual>
                <x>87.007349</x>
                <y>-87.509945</y>
                <z>0.453673</z>
            </residual>
        </point>
    </points>
</absolute>

Implementando EGM08_REDNAP en Digi3D.NET

Introducción

Digi3D.NET se basa en la base de datos del EPSG para implementar todos sus cálculos geodésicos.

El problema de la base de datos EPSG es que no contempla ningún sistema de coordenadas vertical para la rejilla EGM08_REDNAP proporcionada por el Instituto Geográfico Nacional de España.

Aquí te voy a contar las modificaciones que he realizado en la base de datos para añadir el sistema de coordenadas vertical EGM08_REDNAP a Digi3D.NET.

El instalador de Digi3D.NET instala una copia (transformada a formato .sdf) de la base de datos del EPSG en la carpeta %ProgramData%\Digi3D.NET\OpenGis.

Conectándonos a la base de datos

Podemos conectarnos a esta base de datos con herramientas como Microsoft Visual Studio, WebMatrix o Microsoft SQL Server Management Studio. Yo lo he hecho con Visual Studio 2010.

He seguido los siguientes pasos:

  1. En el panel Server Explorer he pulsado el botón Connect to database. Aparece el cuadro de diálogo Choose Data Source.
  2. Selecciono la opción Microsoft SQL Server Compact 3.5
  3. Pulso el botón Continue. Aparece el cuadro de diálogo Add Connection.
  4. Pulso el botón Browse… Aparece el cuadro de diálogo Select SQL Server Compact 3.5 Database File.
  5. Localizo la base de datos y acepto el cuadro de diálogo Select SQL Server Compact 3.5 Database File.
  6. Pulso Ok en el cuadro de diálogo Add Connection. Aparece la base de datos como una rama de Data Connections en el panel Server Explorer.
  7. Pulso con el botón derecho del ratón en el nombre de la base de datos y aparece un menú contextual. Selecciono la opción New Query para crear una nueva consulta en la base de datos. Se abre una ventana para introducir la consulta y aparece un cuadro de diálogo Add Table. Lo cancelo.
  8. A partir de ahora podemos hacer consultas tecleándolas en la tercera fila. Para ejecutarlas pulsamos Ctrl+R

Añadiendo los sistemas de coordenadas de referencia verticales

El objetivo es añadir dos sistemas de coordenadas de referencia verticales: Uno para península (que denominaremos EGM08_REDNAP Península) y otro para Canarias (que denominaremos EGM08_REDNAP Canarias) para que el usuario de Digi3D.NET pueda seleccionar ese sistema de coordenadas.

Los sistemas de coordenadas de referencia se extraen de la tabla Coordinate Reference System.
Hago la siguiente consulta para averiguar los parámetros que rellenan todos los sistemas de coordenadas de referencia verticales que aparecen en la base de datos:

SELECT *
FROM [Coordinate Reference System]
WHERE COORD_REF_SYS_KIND = 'vertical'

Y compruebo que todos ellos rellenan únicamente los siguientes campos: COORD_REF_SYS_CODE, COORD_REF_SYS_NAME, AREA_OF_USE_CODE, COORD_REF_SYS_KIND, COORD_REF_SYS_CODE, DATUM_CODE, CRS_SCOPE, algunos rellenan REMARKS, INFORMATION_SOURCE, DATA_SOURCE, REVISION_DATE, SHOW_CRS y por último DEPRECATED.

Vamos a averiguar qué valores poner en cada campo.

COORD_REF_SYS_CODE:

Aquí tenemos que poner un código único para nuestro sistema de coordenadas. En el documento G7-1 del EPSG Users who wish to augment the EPSG, en el tema 5.9.1, podemos leer el siguiente párrafo: data with their own information should utilise codes greater than 32767, por lo tanto vamos a localizar el sistema de coordenadas de referencia cuyo código sea mayor de entre los mayores de 32767 con la siguiente consulta:

SELECT MAX(COORD_REF_SYS_CODE) AS Expr1
FROM [Coordinate Reference System]
WHERE (COORD_REF_SYS_CODE > 32767)

Nos sale el número 69036405, de modo que voy a añadir un sistema nuevo cuyo código va a ser 69036406 para el sistema de coordenadas a aplicar en la península y 69036407 para el de Canarias.

COORD_REF_SYS_NAME:

Aquí vamos a poner EGM08_REDNAP Península para el caso de península y EGM08_REDNAP Canarias para el caso de Canarias.

AREA_OF_USE_CODE:

Si hacemos la siguiente consulta:

SELECT *
FROM Area
WHERE (AREA_NAME LIKE '%spain%')

Nos encontramos con que el código a utilizar para la península es: 2366 y el de Canarias: 3199

COORD_REF_SYS_KIND:

Aquí vamos a poner vertical

COORD_REF_SYS_CODE:

Si realizamos la siguiente consulta:

SELECT *
FROM [Coordinate System]
WHERE (COORD_SYS_TYPE = 'vertical')

Comprobamos rápidamente que tendremos que introducir el código 6499 (ya que mide alturas, con orientación hacia arriba y con metros como unidad de medida).

DATUM_CODE:

Si ejecutamos la siguiente consulta:

SELECT *
FROM Datum
WHERE (DATUM_NAME LIKE '%EGM2008%')

Comprobamos que el valor a introducir en este campo es 1027.

CRS_SCOPE:

Aquí vamos a basarnos en el de EGM2008, que si lo consultamos con la siguiente consulta:

SELECT *
FROM [Coordinate Reference System]
WHERE (COORD_REF_SYS_NAME LIKE '%egm2008%')

comprobamos que tenemos que almacenar el valor Geodesy.

REMARKS:

Aquí vamos a poner:

Height Surface resulting from the application of the EGM08_REDNAP geoid model to the WGS 84 ellipsoid. Replaces EGM2008 geoid height (CRS code 3855) in Spain mainland

para el sistema de coordenadas de referencia vertical para la península y:

Height Surface resulting from the application of the EGM08_REDNAP geoid model to the WGS 84 ellipsoid. Replaces EGM2008 geoid height (CRS code 3855) in Spain – Canary islands

para el de Canarias.

INFORMATION_SOURCE:

Aquí vamos a poner la url del Instituto Geográfico Nacional España: http://www.ign.es/ign/layoutIn/actividadesGeodesiaRedn.do

DATA_SOURCE:

Aquí si hacemos la siguiente consulta:

SELECT DISTINCT DATA_SOURCE
FROM [Coordinate Reference System]

Resulta que únicamente existe un Data Source oficial, que es OGP, así que vamos a poner OGP (podríamos poner IGN pero como desconozco el valor a poner...).

REVISION_DATE:

Aquí vamos a poner la fecha de hoy: 26/11/2013

SHOW_CRS

Aquí vamos a poner True

DEPRECATED:

Aquí vamos a poner False

De modo que las consultas que debemos añadir para nuestros dos sistemas de coordenadas de referencia verticales son a la base de datos EPSG son:

INSERT INTO [Coordinate Reference System] (COORD_REF_SYS_CODE, COORD_REF_SYS_NAME, AREA_OF_USE_CODE, COORD_REF_SYS_KIND, COORD_SYS_CODE, DATUM_CODE, CRS_SCOPE, REMARKS, INFORMATION_SOURCE, DATA_SOURCE, 
                         REVISION_DATE, SHOW_CRS, DEPRECATED)
VALUES (69036406, 'EGM08_REDNAP Península', 2366, 'vertical', 6499, 1027, 'Geodesy', 'Height Surface resulting from the application of the EGM08_REDNAP geoid model to the WGS 84 ellipsoid. Replaces EGM2008 geoid height (CRS code 3855) in Spain mainland', 'http://www.ign.es/ign/layoutIn/actividadesGeodesiaRedn.do', 'OGP', GETDATE(), 1, 0)

y

INSERT INTO [Coordinate Reference System] (COORD_REF_SYS_CODE, COORD_REF_SYS_NAME, AREA_OF_USE_CODE, COORD_REF_SYS_KIND, COORD_SYS_CODE, DATUM_CODE, CRS_SCOPE, REMARKS, INFORMATION_SOURCE, DATA_SOURCE, REVISION_DATE, SHOW_CRS, DEPRECATED)
VALUES (69036407, 'EGM08_REDNAP Canarias', 3199, 'vertical', 6499, 1027, 'Geodesy', 'Height Surface resulting from the application of the EGM08_REDNAP geoid model to the WGS 84 ellipsoid. Replaces EGM2008 geoid height (CRS code 3855) in Spain - Canary islands', 'http://www.ign.es/ign/layoutIn/actividadesGeodesiaRedn.do', 'OGP', GETDATE(), 1, 0)

Con estos cambios ya podemos seleccionar el sistema de coordenadas de referencia vertical EGM08_REDNAP Península en Digi3D.NET tal y como se puede ver en la siguiente captura de pantalla:

Digi3D mostrando sistema vertical EGM08_REDNAP Península

A continuación la cadena Well Known Text generada automáticamente por Digi3D.NET al seleccionar el sistema de coordenadas de referencia que hemos creado para la península:

VERT_CS["EGM08_REDNAP Península",VERT_DATUM["EGM2008 geoid",2005,AUTHORITY["EPSG","1027"]],UNIT["metros",1,AUTHORITY["EPSG","9001"]],AXIS["H",Up],AUTHORITY["EPSG","69036406"]]

y a continuación el de Baleares:

VERT_CS["EGM08_REDNAP Baleares",VERT_DATUM["EGM2008 geoid",2005,AUTHORITY["EPSG","1027"]],UNIT["metros",1,AUTHORITY["EPSG","9001"]],AXIS["H",Up],AUTHORITY["EPSG","69036407"]]

Ahora nos quedan añadir entradas a la base de datos para indicar a Digi3D.NET la operación matemática a realizar para transformar coordenadas elipsoidales WGS 84 a ortométricas EGM08_REDNAP.

Para ello vamos a basarnos en la información que aparece en la base de datos EPGS para el sistema de coordenadas de referencia vertical EGM2008, pues EGM08_REDNAP no es más que una especialización de EGM2008:

Si realizamos la siguiente consulta:

SELECT *
FROM Coordinate_Operation
WHERE (SOURCE_CRS_CODE=4979 AND TARGET_CRS_CODE = 3855)

Comprobamos que hay dos variantes de la misma transformación. Vamos a introducir pues en esta tabla dos transformaciones, una cuyo destino será el sistema de coordenada de referencia 69036406 y otra el 69036407 y el resto de valores vamos a copiarlos de la consulta anterior.

Los números de operaciones serán también números mayores que 32767, así que vamos a averiguar primero el número mayor de entre los mayores de 32767 en esta tabla:

SELECT MAX(COORD_OP_CODE) AS Expr1
FROM Coordinate_Operation
WHERE (COORD_OP_CODE > 32767)

La respuesta es 32769, de modo que utilizaremos 32770 y 32771:

INSERT INTO Coordinate_Operation (COORD_OP_CODE, COORD_OP_NAME, COORD_OP_TYPE, SOURCE_CRS_CODE, TARGET_CRS_CODE, COORD_TFM_VERSION, COORD_OP_VARIANT, AREA_OF_USE_CODE, COORD_OP_SCOPE, COORD_OP_ACCURACY, COORD_OP_METHOD_CODE, REMARKS, DATA_SOURCE, REVISION_DATE, SHOW_OPERATION, DEPRECATED)
VALUES (32770, 'WGS 84 to EGM08_REDNAP Península Geoid height', 'transformation', 4979, 69036406, 'IGN-Spain mainland', 1, 2366, 'Derivation of gravity-related heights from GPS observations.', 0.5, 1025, 'Replaces WGS 84 to EGM2008 geoid height (2) (tfm code 3859) in Spain - mainland. This tfm uses a grid with node spacing of 1 arc-minute.', 'OGP', GETDATE(), 1, 0)

y este otro:

INSERT INTO Coordinate_Operation (COORD_OP_CODE, COORD_OP_NAME, COORD_OP_TYPE, SOURCE_CRS_CODE, TARGET_CRS_CODE, COORD_TFM_VERSION, COORD_OP_VARIANT, AREA_OF_USE_CODE, COORD_OP_SCOPE, COORD_OP_ACCURACY, COORD_OP_METHOD_CODE, REMARKS, DATA_SOURCE, REVISION_DATE, SHOW_OPERATION, DEPRECATED)
VALUES (32771, 'WGS 84 to EGM08_REDNAP Canary islands geoid height', 'transformation', 4979, 69036407, 'IGN-Spain Balearic', 1, 3199, 'Derivation of gravity-related heights from GPS observations.', 0.5, 1025, 'Replaces WGS 84 to EGM2008 geoid height (2) (tfm code 3859) in Spain - Balearic islands. This tfm uses a grid with node spacing of 1 arc-minute.', 'OGP', GETDATE(), 1, 0)

Añadiendo los parámetros a las transformaciones

Por último sólo nos queda por añadir los parámetros de nuestras transformaciones (que básicamente consisten en indicar el nombre del archivo a cargar).
Los parámetros de las transformaciones se almacenan en la tabla Coordinate_Operation Parameter Value.

Podemos ver los parámetros de la transformación EGM2008:

SELECT *
FROM [Coordinate_Operation Parameter Value]
WHERE (COORD_OP_CODE = 3858)

Comprobamos que únicamente tendremos que añadir dos entradas a esta tabla: una con el COORD_OP_CODE 32769, con el COORD_OP_METHOD_CODE 1025, con el PARAMETER_CODE 8666 y con PARAM_VALUE_FILE_REF EGM08_REDNAP.txt para península y COORD_OP_CODE 32770, con el COORD_OP_METHOD_CODE 1025, con el PARAMETER_CODE 8666 y con PARAM_VALUE_FILE_REF EGM08_REDNAP_Canarias.txt para Canarias:

INSERT INTO [Coordinate_Operation Parameter Value] (COORD_OP_CODE, COORD_OP_METHOD_CODE, PARAMETER_CODE, PARAM_VALUE_FILE_REF)
VALUES (32770, 1025, 8666, 'EGM08_REDNAP.txt')

y

INSERT INTO [Coordinate_Operation Parameter Value] (COORD_OP_CODE, COORD_OP_METHOD_CODE, PARAMETER_CODE, PARAM_VALUE_FILE_REF)
VALUES (32771, 1025, 8666, 'EGM08_REDNAP_Canarias.txt')

Por último he tenido que modificar el algoritmo Geographic3DToGravityRelatedHeightEGM2008 para que pueda cargar archivos .txt en el formato en el que lo publica el IGN que no tiene nada que ver con el que se publica el EGM2008 y ya está, Digi3D.NET es compatible con estos sistemas de coordenadas de referencia verticales.

Insertando vértices a una línea seleccionada gráficamente en sus intersecciones con otras líneas

Hoy vamos a añadir una nueva orden a nuestro proyecto OrdenesDigiNG (cuyo código fuente puedes descargar de nuestro repositorio en GitHub

Esta orden va a solicitar al usuario que seleccione una línea e insertará en esa línea tantos vértices como intersecciones tenga esa línea con el resto de entidades visibles.

Para ello vamos a seguir los siguientes pasos:

  1. Cargamos el proyecto OrdenesDigiNG con Visual Studio 2010
  2. Añadimos una clase que denominaremos TramificaInsertandoEntidadSeleccionada
  3. Hacemos que la clase sea pública para que Digi3D.NET la pueda instanciar.
  4. Añadimos el atributo CommandAttribute asignando como nombre público de la orden tramifica_insertando_entidad_sel
  5. Hacemos que la clase herede del tipo Command (recuerda añadir la cláusula using Digi21.DigiNG.Plugin.Command)
  6. Añadimos un constructor público en el que vamos a añadir manejadores de eventos para los eventos Initialize, SetFocus, DataUp y EntitySelected. Los eventos Initialize y SetFocus hacemos que sean manejados por un único método que vamos a denominar InvitaAlUsuarioASeleccionarLínea que tendrá la firma de un manejador de eventos clásico.

    El constructor y el manejador de eventos tendrán el siguiente aspecto:

    public TramificaInsertandoEntidadSeleccionada()
    {
        this.Initialize += InvitaAlUsuarioASeleccionarLínea;
        this.SetFocus += InvitaAlUsuarioASeleccionarLínea;
        this.DataUp += new EventHandler<Digi21.Math.Point3DEventArgs>(TramificaInsertandoEntidadSeleccionada_DataUp);
        this.EntitySelected += new EventHandler<EntitySelectedEventArgs>(TramificaInsertandoEntidadSeleccionada_EntitySelected);
    }
    
    void TramificaInsertandoEntidadSeleccionada_DataUp(object sender, Digi21.Math.Point3DEventArgs e)
    {
        throw new NotImplementedException();
    }
    
  7. Ahora vamos iniciamos el proceso de selección de líneas en el manejador de eventos del evento DataUp.

    Para ello llamaremos a la sobrecarga del método DigiNG.SelectEntity que dispone de un predicado en el segundo parámetro e indicaremos mediante una expresión Lambda que únicamente vamos a permitir seleccionar entidades de tipo ReadOnlyLine.

    El método queda así pues:

    void TramificaInsertandoEntidadSeleccionada_DataUp(object sender, Point3DEventArgs e)
    {
        DigiNG.SelectEntity(e.Coordinates, entidad => entidad is ReadOnlyLine);
    }
    

Ahora vamos a realizar nuestra tarea con la línea seleccionada.

  1. Vamos a crear dos conjuntos: uno con la línea que queremos tramificar y otro con el resto de líneas del archivo de dibujo teniendo en cuenta que la orden únicamente debe trabajar con las entidades que sean visibles en este momento. Para esto último podemos utilizar el método de extensión Visibles del proyecto UtilidadesDigi que puedes descargar de nuestro repositorio de GitHub.

    var líneaATramificar = e.Entity as ReadOnlyLine;
    
    // Seleccionamos las líneas visibles del archivo de dibujo (excluyendo la línea que vamos a tramificar)
    var líneasContraLasCualesTramificar = from entidad in DigiNG.DrawingFile.OfType<ReadOnlyLine>().Visibles()
                                            where entidad != e.Entity
                                            select entidad;
    
  2. Después vamos a obtener una lista de las intersecciones de la línea seleccionada con el resto de líneas utilizando el método de extensión DetectIntersections:

    // Obtenemos las intersecciones de la línea seleccionada con el reto de líneas visibles
    var intersecciones = líneaATramificar.DetectIntersections(líneasContraLasCualesTramificar);
    

    Este método devuelve una agrupación de coordenadas y de entidades que llegan a estas coordenadas. Te recuerdo que cada coordenada es en sí mismo una secuencia cuyo contenido son valores de tipo SegmentPointer que indican por un lado la lína que llega a esa coordenadas y entre que vértices de esa línea se ha localizado la intersección.

  3. Ahora vamos a construir nuestra línea nueva.
    La línea nueva tendrá al menos tantos vértices como la línea original. Cada vértice es el comienzo de un segmento. El vértice 0 es el primer vértice del segmento 0. El vértice 1 es el primer segmento del segmento 1 y así sucesivamente.
    Lo que vamos a hacer es copiar el primer vértice del primer segmento de la línea original a la nueva. Luego buscaremos todas las intersecciones localizadas en ese determinado segmento de y las añadiremos a la línea destino, pero atención, tendremos que ordenar las intersecciones, porque nadie nos garantiza que se nos estén devolviendo ordenadas.

    for (int vértice = 0; vértice < líneaATramificar.Points.Count - 1; vértice++)
    {
        // Añadimos el vértice de la línea original
        líneaNueva.Points.Add(líneaATramificar.Points[vértice]);
    
        // Ahora localizamos únicamente las intersecciones localizadas para el segmento actual en la línea a tramificar
        var vérticesAAñadirEnEsteSegmento = intersecciones.SoloDeSegmento(líneaATramificar, vértice);
    
        // Tenemos una lista de vértices, pero pueden venir desordenados. Vamos a ordenarlos calculando su distancia a la coordenada del primer vértice de este segmento
        Point2D vérticeComienzoSegmento = (Point2D)líneaATramificar.Points[vértice];
    
        var vérticesOrdenados = from v in vérticesAAñadirEnEsteSegmento
                                let distancia = (v.Key - vérticeComienzoSegmento).Module
                                orderby distancia
                                select v.Key;
    
        // Ahora insertamos estos vértices en la línea nueva. Los vértices son 2D (los métodos de extensión proporcionados por el tipo IntersectionDetector trabajan con Point2D
        // así que tendremos que ínterpolar la coordenada Z
        foreach (var v in vérticesOrdenados)
        {
            var segmento = new Segment(líneaATramificar.Points[vértice], líneaATramificar.Points[vértice + 1]);
            double z = segmento.InterpolatedZ(new Point3D(v));
    
            líneaNueva.Points.Add(new Point3D(v.X, v.Y, z));
        }
    }
    
    // Por último añadimos el último vértice
    líneaNueva.Points.Add(líneaATramificar.Points.Last());
    
  4. Y ¡ya hemos terminado casi!
    Tan solo nos queda añadir la entidad nueva al archivo de dibujo, eliminar la anterior y destruir la orden:

    DigiNG.DrawingFile.Add(líneaNueva);
    DigiNG.DrawingFile.Delete(e.Entity);
    Dispose();