Archivo de la categoría: DigiNG

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);

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();
    

¿El TIFF contiene una imagen o por el contrario contiene un MDT/MDS?

TIFFDump mostrando la cabecera de un archivo MDS obtenido mediante un radar.

TIFFDump mostrando la cabecera de un archivo MDS obtenido mediante un radar.

Acabo de añadir en DigiNG la posibilidad de cargar archivos MDS/MDT contenidos en un archivo .tif.

El usuario carga el archivo .tif con la orden CARGA_F, y es DigiNG quien decide si carga el archivo como una imagen o como un MDT. En este post te voy a explicar el criterio que utiliza DigiNG para decidir cómo carga el archivo.

Para ello vamos a utilizar una herramienta denominada TIFFDUMP que viene junto con el código fuente de la librería libtiff (lo encontrarás en la carpeta bin de este enlace).

Esta herramienta (que es una aplicación de consola) nos muestra el contenido de la cabecera de un archivo .tif.

El formato TIFF es un formato muy bien pensado (exceptuando que no permite crear archivos de más de 4GB de tamaño, aunque ese pequeño problema ya está solucionado con el formato BigTiff), y tiene una cabecera muy completa que permite indicar todas las características de la imagen contenida dentro del archivo.

Esas características se almacenan en variables. Existen muchísimas según el estándar, y si no encuentras la que necesitas para almacenar metadatos de la imagen, eres libre de añadir la que quieras. Si además tienes la suerte de que se acepte por la comunidad podrías conseguir que en una próxima revisión del estándar se convirtiera en una variable estandarizada.

Las variables no tienen por qué aparecer en el archivo .tif. Si no aparecen, se asume que su valor es el que tienen por defecto.

El programa tiffdump.exe es muy sencillo de ejecutar. Tan solo tenemos que abrir una consola, y ejecutarlo pásandole como parámetro el nombre del archivo .tiff a analizar.

En la siguiente tabla tienes varias variables junto con su significado:

Nombre de variable Significado
ImageWidth Ancho de la imagen en píxeles
ImageLength Alto de la imagen en píxeles.
Photometric Valor que indica el tipo de imagen (tonos de gris, color RGB, paleta de color, imagen que es únicamente un canal alpha, CMYK, YCrCb,…)
BitsPerSample Indica el número de bits que tendrá cada canal de la imagen.
SamplesPerPixel Indica el número canales que tendrá cada pixel. Si la imagen es RGB lo normal es encontrarse aquí un 3, si es en tonos de gris, lo normal es encontrar aquí un 1. Si es una imagen RGB con canal alpha, aquí tendremos un 4, si es una imagen en tonos de gris con canal alpha, nos encontraremos con un 2, etc.
SampleFormat Indica el tipo de valor almacenado en cada pixel. Este parámetro casi nunca se encuentra en una imagen Tiff. Si creas una imagen Tiff con Photoshop por ejemplo no verás este valor almacenado en el tiff, y se asumirá que el valor por defecto es 1, que significa entero sin signo.

Ahora bien, ¿cómo decidimos si una imagen es un MDT/MDS o no?

Nunca conviene reinventar la rueda, y si hay alguna aplicación estándar que realice la operación que quieres implementar, lo ideal es analizar el comportamiento de esa aplicación y realizar una implementación parecida. De ese modo estarás reforzando ese estándar de facto.

La aplicación a seguir en este caso, y sin ningún tipo de dudas es Global Mapper.

Lo único que necesitaba son una serie de imágenes, y como se por donde van los tiros, solicité a un colega que me crease archivos TIFF con MDTs en los siguientes formatos:

Tipo de imagen Canales bits por canal tipo de valor
Tono de gris 1 8 entero sin signo
Tono de gris 1 8 entero con signo
Tono de gris 1 16 entero sin signo
Tono de gris 1 32 valor en coma flotante
Tono de gris 1 64 valor en coma flotante

Y como era de esperar, Global Mapper únicamente trata como MDT/MDS los archivos con valor en coma flotante.

De modo que las pruebas que se realizan para comprobar si un TIFF es un MDT/MDS o no son las siguientes:

  1. Se comprueba que la imagen tenga un único canal
  2. Se comprueba que el número de bits del canal sea 32 o 64
  3. Se comprueba que la imagen informe del tipo de valor almacenado en cada pixel (te recuerdo que este campo habitualmente no aparece en la mayoria de archivos .tif)
  4. Se comprueba que el tipo de valor almacenado en cada pixel es valor en coma flotante (o lo que es lo mismo, un 3, que significa IEEE floating point data

Si se superan todas estas pruebas, DigiNG considera el archivo como un MDT/MDS. Si no, se considerará como una imagen.

Convirtiendo los métodos de extensión sobre secuencias de entidades en métodos genéricos

Vamos a realizar un cambio a la biblioteca de clases UtilidadesDigi que hemos ido construyendo en nuestro curso de programación de Digi3D:

Si te fijas en el código de los métodos de extensión UtilidadesSecuenciaEntity.QueTenganElCódigo, UtilidadesSecuenciaEntity.QueTenganElCódigoConComodín, UtilidadesSecuenciaEntity.QueTenganAlgúnCódigo y UtilidadesSecuenciaEntity.QueTenganAlgúnCódigoConComodín:

    public static IEnumerable<Entity> QueTenganElCódigo(this IEnumerable<Entity> secuenciaOriginal, string código)
    {
        return from entidad in secuenciaOriginal
               where entidad.TieneElCódigo(código)
               select entidad;
    }
    public static IEnumerable<Entity> QueTenganElCódigoConComodín(this IEnumerable<Entity> secuenciaOriginal, string código)
    {
        return from entidad in secuenciaOriginal
               where entidad.TieneElCódigoConComodín(código)
               select entidad;
    }
    public static IEnumerable<Entity> QueTenganAlgúnCódigo(this IEnumerable<Entity> secuenciaOriginal, IEnumerable<string> códigos)
    {
        return from entidad in secuenciaOriginal
               where entidad.TieneAlgúnCódigo(códigos)
               select entidad;
    }
    public static IEnumerable<Entity> QueTenganAlgúnCódigoConComodín(this IEnumerable<Entity> secuenciaOriginal, IEnumerable<string> códigos)
    {
        return from entidad in secuenciaOriginal
               where entidad.TieneAlgúnCódigoConComodín(códigos)
               select entidad;
    }

Siempre devuelven una secuencia de IEntity, aunque esa secuencia esté particularizada para un determinado tipo de entidad, y eso deshabilita todos los filtros que se hubieran aplicado antes, como en el siguiente ejemplo:

            var líneasDeMarcoHoja = DigiNG.DrawingFile.OfType<ReadOnlyLine>().QueTenganElCódigo(códigoDelMarco);

Vamos a estudiar que pasa con la línea de código anterior:

  • DigiNG.DrawingFile devuelve una secuencia de IEnumerable<IEntity>.
  • Generamos una secuencia de ReadOnlyLine mediante el método de extensión de Linq: OfType<T>. A partir de este momento tenemos una secuencia: IEnumerable<ReadOnlyLine>.
  • Luego llamamos al método de extensión UtilidadesDigiNG.QueTenganElCódigo que extuende IEnumerable<IEntity> y que devuelve una secuencia: IEnumerable<IEntity>.

Lo que resulta en que al final lo que tenemos es un IEnumerable<IEntity>, y está claro por el contexto que lo que queremos es un IEnumerable<ReadOnlyLine>.

En este caso particular, podríamos haberlo solucionado de la siguiente forma:

            var líneasDeMarcoHoja = DigiNG.DrawingFile.QueTenganElCódigo(códigoDelMarco).OfType<ReadOnlyLine>();

Pero esa no es la solución. Tenemos que modificar nuestros métodos de extensión para que sean genéricos, de forma que si están subclasificando una secuencia de X, que lo que devuelvan sea una secuencia de X y no una secuencia de otra cosa.

Éstos métodos van a extender secuencias de tipos cuya base sea IEntity, así que cuando las convirtamos en métodos genéricos tendrán que tener como restricción que el tipo genérico herede de IEntity.

Veamos cómo quedan nuestros métodos de extensión una vez convertidos en métodos genéricos:

    public static IEnumerable<T> QueTenganElCódigo<T>(this IEnumerable<T> secuenciaOriginal, string código)
        where T : Entity
    {
        return from entidad in secuenciaOriginal
               where entidad.TieneElCódigo(código)
               select entidad;
    }

    public static IEnumerable<T> QueTenganElCódigoConComodín<T>(this IEnumerable<T> secuenciaOriginal, string código)
        where T : Entity
    {
        return from entidad in secuenciaOriginal
               where entidad.TieneElCódigoConComodín(código)
               select entidad;
    }

    public static IEnumerable<T> QueTenganAlgúnCódigo<T>(this IEnumerable<T> secuenciaOriginal, IEnumerable<string> códigos)
        where T : Entity
    {
        return from entidad in secuenciaOriginal
               where entidad.TieneAlgúnCódigo(códigos)
               select entidad;
    }

    public static IEnumerable<T> QueTenganAlgúnCódigoConComodín<T>(this IEnumerable<T> secuenciaOriginal, IEnumerable<string> códigos)
        where T : Entity
    {
        return from entidad in secuenciaOriginal
               where entidad.TieneAlgúnCódigoConComodín(códigos)
               select entidad;
    }

De esta manera estos métodos de extensión devuelven subconjuntos de secuencias sin cambiar su tipo.