Archivo de la categoría: DigiNG

Comportamiento de los importadores de archivos de dibujo sin tabla de códigos

El día 01/02/2013 hemos añadido a Digi3D.NET la posibilidad de no indicar en el cuadro de diálogo Nuevo Proyecto, en la pestaña Archivo de dibujo ninguna tabla de códigos en el campo Tabla de códigos.

En este post te voy a explicar como funciona esta característica dentro de Digi3D.NET.

Modificaciones realizadas al programa principal

Digi3D.NET necesita una tabla de códigos para poder trabajar. Es absolutamente fundamental. Todo el programa está desarrollando teniendo en cuenta de que en memoria hay una tabla de códigos. Ahí es donde aparece la representación (color, grosor, patrones,…) de cada código, parámetros de transformación, …

Se han realizado dos modificaciones a Digi3D.NET para permitir trabajar sin tabla de códigos:

  • Permitir al usuario dejar en blanco el campo correspondiente en el cuadro de diálogo de nuevo proyecto.
  • Analizar si el usuario deje ha dejado en blanco ese campo. En caso afirmativo, crear en memoria una tabla de códigos vacía, sin nombre asignado, sin ruta, sin nada (bueno, en realidad si hay algo, un código que es el código al que van a parar las entidades con códigos desconocidos). Esta tabla de códigos es la que se le pasa al importador/exportador encargado de cargar el archivo de dibujo.

Ahora bien, no solo ha habido que modificar el programa principal. También ha habido que modificar los distintos importadores/exportadores para que añadan códigos a esta tabla de códigos, y a la hora de desarrollar esa modificación surgió una duda: ¿En qué casos los importadores/exportadores van a añadir códigos?.

Decidiendo cuando se va a añadir un código nuevo en la tabla de códigos

Digi3D.NET nunca ha añadido códigos a la tabla de códigos activa si se encuentra con un código desconocido, por lo tanto no queremos que si alguien actualiza el programa, este siga comportándose así, es decir, si se encuentra un código desconocido, este no se añade a la tabla de códigos y el usuario verá ese código de color gris, lo que le hará saber que es un código desconocido.

Cuadro de diálogo de configuración

Cuadro de diálogo de configuración mostrando la opción de añadir códigos desconocidos para el importador de binarios de doble precisión.

Lo primero que se ha añadido a cada uno de los importadores/exportadores es una nueva opción en el cuadro de diálogo de Configuración que le va a permitir indicar al usuario si quiere que el importador/exportador añada automáticamente códigos a la tabla de códigos activa si no localiza en esa tabla el código con el cual traducir una determinada entidad localizada en el archivo de dibujo.

Si te fijas en la captura de pantalla, la opción por defecto es No. Es así por lo indicado anteriormente: queremos mantener la compatibilidad hacia atrás y no queremos que de repente se añadan códigos automáticamente a un usuario que indica una tabla de códigos activa. Si el usuario activa esta opción, se añadirán códigos, pero tiene que ser el usuario quien implícitamente indique que quiere que el programa se comporte de ese modo.

Sin embargo, no queremos que el usuario que se quiera aprovechar de esta funcionalidad tenga que cambiar esta opción obligatoriamente, y ahí es donde entra la posibilidad de dejar vacío el campo en el que se indica la tabla de códigos en el cuadro de diálogo de Nuevo Proyecto.

Si el usuario deja vacío ese campo, aunque tenga deshabilitada la opción de añadir códigos en el cuadro de diálogo de Configuración, los importadores van a crear códigos nuevos.

Comportamiento en función del importador/exportador

No todos los importadores/exportadores se comportan igual, por eso te pongo aquí una tabla para que veas las diferencias de comportamiento en función del formato del archivo de dibujo y en función de la configuración indicada.

Escenario 1: El usuario no ha rellenado el campo Tabla de códigos en el cuadro de diálogo Nuevo Proyecto, al cargar el archivo de dibujo o ha seleccionado una tabla de códigos pero ninguno de sus códigos se corresponden con los de las entidades del archivo de dibujo y además ha seleccionado NO AÑADIR CÓDIGOS DESCONOCIDOS en la configuración del programa para el importador en cuestión. .

Importador Comportamiento
BinD Se crearán tantos códigos como códigos se localicen en las entidades del archivo de dibujo.
Bin Se crearán tantos códigos como códigos se localicen en las entidades del archivo de dibujo.
Dwg Se crearán tantos códigos como capas se localicen en las entidades del archivo de dibujo.
Dgn Se crearán tantos códigos como códigos se localicen en las entidades del archivo de dibujo. El caso de Dgn es especial, pues la codificación varía en función del parámetro riterio para códigos seleccionado por el usuario a la hora de cargar el archivo de dibujo.
Shp Se crearán tantos códigos como tablas formen el shapefile.
Mdb (Geomedia Datawarehouse) Se crearán tantos códigos como tablas con geometrías se localicen en el .mdb.

Escenario 2: El usuario ha cargado una tabla de códigos pero ninguno de sus códigos se corresponden con los de las entidades del archivo de dibujo. El usuario ha seleccionado NO AÑADIR CÓDIGOS DESCONOCIDOS en la configuración del programa para el importador en cuestión.

Importador Comportamiento
BinD Se cargarán las entidades con código desconocido.
Bin Se cargarán las entidades con código desconocido.
Dwg Se cargarán las entidades con código desconocido.
Dgn Se cargarán las entidades con código desconocido,
Shp No se cargarán las entidades de las capas para las que no se localiza correspondencia en la tabla de códigos.
Mdb (Geomedia Datawarehouse) No se cargarán las entidades de las capas para las que no se localiza correspondencia en la tabla de códigos.

Es decir, que los importadores basados en bases de datos, como Shp y Geomedia no pueden cargar entidades con código desconocido.

Conectando con bases de datos SQL Server Compact

Digi3D.NET permite que almacenemos información alfanumérica en bases de datos basadas en archivo (como Access, o SQL Server Compact) o basadas en servidor, como Microsoft SQL Server, Oracle, MySQL, PostgreSQL, …

En ocasiones nos interesará crear una base de datos por cada archivo de dibujo. En este escenario, es posible que nos interese utilizar una base de datos de archivo que se asociará con cada archivo de dibujo.

Tenemos varias posibilidades, una de ellas (la opción clásica) es asociar un archivo Access a cada archivo de dibujo. Esta solución tiene dos inconvenientes:

  • Access no es gratuito, por lo tanto tendremos que adquirir una licencia de esta aplicación para poder crear la base de datos, realizar consultas, …
  • No es compatible con aplicaciones de 64 bits, por lo tanto queda descartado para Digi3D.NET si tenemos instalada la versión de 64 bits

Pero existen otras soluciones. Hoy vamos ha hablar de Microsoft SQL Server Compact.

Un archivo de base de datos Microsoft SQL Server Compact tiene la extensión .sdf.

Este tipo de bases de datos tienen muchas ventajas si las comparamos con Access, entre las que podríamos destacar (aunque hay muchas más):

  • Es compatible tanto con aplicaciones de 64 bits como de 32 bits.
  • Es gratuita.
  • Es espacial (permite almacenar geometrías y consultas espaciales).
  • Permite crear tablas de hasta 4 GB de información (frente a 4 de Access).
  • Permite almacenar cadenas de caracteres de hasta 4000 caracteres (frente a 255 de Access).

Si queremos utilizar una base de datos Microsoft SQL Server Compact tan solo tenemos que instalar un pequeño motor de base de datos en nuestro ordenador. A día de hoy existen dos versiones de este motor de bases de datos:

Si tienes instalado Digi3D.NET en tu equipo, ya tienes instalado Microsoft SQL Server Compact 3.5 SP2, pues es uno de los requisitos necesarios para instalar la aplicación.

Para crear estas bases de datos y realizar consultas SQL sobre ellas disponemos de varias herramientas en función de la versión:

Microsoft SQL Server Compact 3.5 SP2 Microsoft SQL Server Compact 4.0 SP1
SQL Server 2008 R2 Management Studio (Express) (3.5 SP2) Visual Studio 2010 (3.5 SP2)
Visual Studio 2010 (3.5 SP2)
Visual Studio 2010 SP1 + 4.0 Tools (3.5 SP2 + 4.0)
Visual Studio Express 2012 for Web
WebMatrix 2

Si te fijas hay muchas más opciones para la versión 4, pero a mi personalmente me encanta Microsoft SQL Server Management Studio para trabajar con bases de datos. Ni te molestes en intentar conectar con una base de datos Microsoft SQL Server Compact 4.0 con ninguna de las versiones Microsoft SQL Server Management Studio, ni siquiera con la versión 2012. Microsoft decidió que nunca jamás se podrán administrar estas bases de datos con este programa, y en parte tienen razón. Este programa está pensado para servidores de bases de datos completos, con Transact SQL completo, con procedimientos almacenados, con replicación, informes, y un sinfín de cosas que no admiten las bases de datos compact que son algo muy sencillo pensado inicialmente para aplicaciones de telefonía móvil.

NuevoProyectoMostrandoBotonSeleccionDataLinkProperties

Una vez que hemos creado una base de datos SQL Server Compact con cualquiera de estas herramientas (en el formato adecuado, lee el capítulo de Bases de datos del libro Digi3D en profundidad para aprender el esquema de una base de datos compatible con Digi3D.NET), lo único que nos queda es hacer que Digi3D.NET se conecte con nuestra base de datos.

La forma de conectar Digi3D.NET con una base de datos consiste en indicar en la pestaña Archivo de dibujo del cuadro de diálogo Nuevo Proyecto el tipo de Modelo de Datos (a día de hoy podemos seleccionar o CATDBS o Geographics) y en el siguiente campo, especificar la cadena de conexión a la base de datos.

La cadena de conexión es una línea de texto en un formato especial que indica por un lado el proveedor de base de datos (el motor de base de datos, runtime, o como lo queramos llamar), la ruta a la base de datos y opcionalmente una serie de parámetros.

Un ejemplo de cadena de conexión podría ser el siguiente:

Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=PruebaCatDBS;Data Source=.sqlexpress

Si te fijas no es trivial crear estas cadenas de conexión, por eso, existe un cuadro de diálogo común de Windows que nos va a facilitar la tarea de crear cadenas de conexiones. En Digi3D.NET podemos hacer que aparezca este cuadro de diálogo pulsando el botón de los tres puntos que aparece en la ventana en la que se nos solicita la cadena de conexión.

CuadroDialogoDataLinkProperties

Este cuadro de diálogo nos va a permitir seleccionar entre una lista de proveedores de bases de datos instalados en nuestro equipo. Esos proveedores pueden variar en función de la versión de Digi3D.NET, ya que en el caso de ejecutar la versión de 64 bits, no aparecerá el proveedor Microsoft Jet 4.0 OLE DB Provider, que es el utilizado por las bases de datos de Access, lo que significa que con las versiones de 64 bits no podríamos conectarnos con bases de datos de Access.

El único inconveniente es que no vamos a localizar aquí ningún proveedor para Microsoft SQL Server Compact 3.5 SP2 o Microsoft SQL Server Compact 4.0 SP1, de modo que no nos va a quedar más remedio que introducir manualmente la cadena de conexión, y ese es precisamente el objetivo de esta entrada en el blog.

El formato de las cadenas de conexión para estos dos proveedores es la siguiente:

Tipo de conexión Cadena de conexión
SQL Server Compact 3.5 SP2 sin contraseña Provider=Microsoft.SQLSERVER.CE.OLEDB.3.5;Data Source=rutaAlArchivonombreDelArchivo.sdf;
SQL Server Compact 3.5 SP2 con contraseña Provider=Microsoft.SQLSERVER.CE.OLEDB.3.5;Data Source=rutaAlArchivonombreDelArchivo.sdf;SSCE:Database Password=’contraseña’;
SQL Server Compact 4.0 SP1 sin contraseña Provider=Microsoft.SQLSERVER.CE.OLEDB.4.0;Data Source=rutaAlArchivonombreDelArchivo.sdf;
SQL Server Compact 4.0 SP1 con contraseña Provider=Microsoft.SQLSERVER.CE.OLEDB.4.0;Data Source=rutaAlArchivonombreDelArchivo.sdf;SSCE:Database Password=’contraseña’;

De modo que si por ejemplo nuestro archivo de dibujo está almacenado en la ruta C:Trabajos20131h1.bind podríamos tener una base de datos asociada en la ruta C:Trabajos20131h1.sdf mediante la cadena de conexión (suponiendo que la versión de la base de datos es la 3.5): Provider=Microsoft.SQLSERVER.CE.OLEDB.3.5;Data Source=C:Trabajos20131h1.sdf;

Esto tiene el inconveniente de que cada vez que cambiemos de archivo de dibujo, tendremos que cambiar la cadena de conexión para que la conexión sea con la base de datos correspondiente.

Si queremos evitar tener esto, podemos aprovecharnos de los sustituidores de Digi3D.NET para hacer que el programa seleccione automáticamente el nombre de la base de datos en función del nombre del archivo de dibujo mediante la siguiente cadena de conexión: Provider=Microsoft.SQLSERVER.CE.OLEDB.3.5;Data Source=$(RutaArchivoDibujo)$(NombreArchivoDibujoSinExtension).sdf;

¿Latitud/Longitud o Longitud/Latitud? El caso de los Shapefiles

La verdad es que no me imaginaba la complejidad que tiene todo lo que tenga que ver con sistemas de coordenadas de referencia: a pesar de existir estándares, casi nadie los respeta.

Un caso muy curioso es el de los Shapefiles. Los archivos Shapefile llevan asociado un archivo .prj que define la proyección cartográfica en la que se almacenan las coordenadas de las entidades en el archivo.

El contenido de este archivo es una cadena Well Known Text, que forma parte del estándar Simple Feature Access del Open Geospatial Consortium.

Este estándar define que el orden en el que se almacenan las coordenadas lo define el sistema de coordenadas de referencia, luego si el sistema de coordenadas de referencia es un sistema de coordenadas geográfico con los ejes (Longitud, Latitud), las coordenadas a almacenar deben ir en el orden (Longitud, Latitud). Si por el contrario el sistema de coordenadas es también geográfico pero con los ejes (Latitud, Longitud), las coordenadas se deben almacenar en el orden (Latitud, Longitud).

Como el archivo .prj asociado a los Shapefiles es una cadena Well Kown Text todo hacía pensar que los programas que cargan archivos Shapefile y tienen asociado un sistema de coordenadas de referencia, deberían ser lo suficientemente inteligentes como para saber el orden de los ejes.

Digi3D 2011 cumple al 100% con el estándar. Además obtiene los parámetros de los sistemas de coordenadas de referencia de la base de datos EPSG Geodetic Parameter Dataset, y en esta base de datos todos los sistemas de coordenadas geográficos tienen sus ejes ordenados de la forma (Latitud, Longitud).

Por lo tanto, como Digi3D 2011 es estricto cumpliendo el estándar, si seleccionamos como sistema de coordenadas de salida uno geográfico (basado en EPSG), el archivo generado tendrá las coordenadas ordenadas (Latitud, Longitud).

En teoría, y confiando en que todos los programas cumplieran con el estándar tal y como lo hace Digi3D 2011, decidí exportar el famoso modelo de Bronchales a Shapefile para cargarlo con ArcGIS Explorer Desktop y cual es mi sorpresa que el modelo aparece con los ejes cambiados y además en Kenia.

Captura de pantalla de ArcGIS Explorer mostrando el modelo de bronchales con los ejes cambiados y en Kenia

El contenido del archivo .prj del Shapefile cargado con ArcGIS Explorer en la captura de pantalla anterior es el siguiente:

GEOGCS["WGS 84",DATUM["World Geodetic System 1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree (supplier to define representation)",0.01745329251994328,AUTHORITY["EPSG","9122"]],AXIS["Lat", North],AXIS["Long", East],AUTHORITY["EPSG","4326"]]

que es una cadena Well Known Text en la que se comprueba que los parámetros se han obtenido de la base de datos EPSG (el sistema de coordenadas geográfico WGS 84 tiene asociado el número 4326).

Si te fijas en la cadena Well Kown Text se están definiendo los ejes del sistema geográfico, se indica que el primero es Latitud y que el segundo es Longitud.

De todos modos el estándar define que si una cadena Well Kown Text incluye una autoridad (EPSG en este caso), y el sofware que carga el archivo en cuestión implementa esa autoridad, debe hacer caso omiso a la información que aparece en la georeferenciación y crear el sistema de coordenadas basándose en el código (4326 en este caso). De forma que pensé: Quizás ArcGIS explorer trate de forma especial el sistema de coordenadas 4326, de modo que quité todas las referencias a la autoridad EPSG del archivo .prj creando lo siguiente:

GEOGCS["WGS 84",DATUM["World Geodetic System 1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree (supplier to define representation)",0.01745329251994328],AXIS["Lat",North],AXIS["Long",East]]

Pero desafortunadamente el resultado fue el mismo: modelo representado con los ejes cambiados y ubicado en Kenia.

Parece que que ArcGIS Explorer espera siempre (Longitud/Latitud) independientemente de lo que se indique en el sistema de coordenadas.

Por último, y ya para confirmar mis sospechas, cambié el sentido de los ejes para ver si afectaba en algo a la representación/ubicación con la siguiente cadena:

GEOGCS["WGS 84",DATUM["World Geodetic System 1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree (supplier to define representation)",0.01745329251994328],AXIS["Long",East], AXIS["Lat",North]]

pero tampoco.

El siguiente conjunto de pruebas lo hice con Global Mapper con idénticos resultados: seleccionando como sistema de coordenadas de visualización Geographic (Latitude/Longitude) y con los tres archivos .prj asociados el resultado es el mismo.

GlobalMapperMostrandoBronchalesMal

Es posible que Digi3D 2011 no estuviera generando bien el archivo .prj, así que decidí estropearlo, y al intentar cargarlo en Global Mapper este mostró un cuadro de diálogo indicando que el Shapefile no tiene información de proyección (por lo tanto si que se estaba generando bien, obviamente).

Si se acepta el cuadro de diálogo que indica que el Shapefile no tiene asociado un sistema de coordenadas, se muestra otro que nos permite seleccionar el sistema de coordenadas en el que están las coordenadas del archivo Shapefile, y ¡sorpresa! existe un botón titulado: Init From EPSG…. Genial, tan solo tenemos que pulsarlo y seleccionar el sistema de coordenadas 4326, que según EPSG, tiene los ejes ordenados de la forma (Latitud, Longitud) por lo tanto debe mostrar correctamente el modelo de Bronchales

Pues no. El resultado es el mismo, lo que me hizo pensar que nadie sigue el estándar (al menos en el caso de Shapefiles).

Investigando un poco, llegué a una discusión muy interesante entre usuarios de Microsoft SQL Server Spatial y los ingenieros que han desarrollado ese producto (dentro de muy poco hablaré mucho sobre SQL Server espacial y Digi3D). Ahí se dice que SQL Server Espacial cumple al 100% con el estándar (igual que Digi3D) y que por lo tanto las coordenadas para el caso del sistema de coordenadas 4326 se almacenan como Latitud/Longitud. Un determinado usuario sin embargo me dió la pista sobre cómo se comportan el resto de programas:

Coordinates in SHP (ESRI “shapefiles”), MIF (MapInfo) and other files produced by current GIS packages may be in many different coordinate systems. For lat/lon coordinate systems the order of coordinates is lon/lat in those systems.

y un poco más adelante dice lo siguiente (lo que te hace pensar que realmente sabe del tema)

Whew! … that’s what I mean by “universal” usage, as the above formats comprise, what? 99.99%? of the data out there. If anyone can point to even a single repository of “Latitude / Longitude” data that uses (lat, lon) ordering instead of (lon, lat) ordering, I’d be very grateful to see the URL to that repository.

Lo que significa que el resto de programas hacen caso omiso del estándar. En realidad me imagino que será porque existen archivos Shapefile en coordenadas geográficas desde mucho antes que este estándar. Piensa que el estándar es de 2006 (pero aún no he encontrado nadie que lo respete al 100% en lo referente a sistemas de coordenadas de referencia, quitando Digi3D 2011) y sin embargo el formato Shapefile data de 1998.

Aquí llegamos a un compromiso: Digi3D 2011 cumple al 100% el estándar y creo personalmente que si existe un estándar es para cumplirlo, pero no puedes dar la espalda a lo que en teoría es el estándar de facto: toda esa cartografía que ya existe, que está almacenada en coordenadas geográficas y generada antes de que existiera el estándar, por programas que posiblemente no tengan ni idea de que existe un estándar, y que símplemente asocian un archivo .prj a cada Shapefile generado copiando una plantilla con el mismo nombre que el archivo Shapefile y con extensión .prj.

Así que la única solución que se me ha ocurrido ha sido permitir configurar en Herramientas/Configuración/Importador Exportador de Shapefiles una opción para indicar el orden de las coordenadas en caso de sistemas de coordenadas geográficos. Por defecto (y para seguir el estándar de facto, que no es el que se debería seguir) la opción es Longitud/Latitud. De esta manera podemos saltarnos el estándar en el caso de Shapefiles. También podrás encontrar estas opciones para DWG y DGN.

El resto de exportadores importantes .bin, .bind, .kml, … no permiten esta configuración pues los binarios de Digi3D (tanto .bin como .bind) cumplen al 100% con el estándar y los .kml tienen asociado de forma implícita el sistema de coordenadas geográfico WGS 84 y además lo hace bien: primero se almacena la Latitud y luego la Longitud.

Imprimir la configuración de teclado

Como ya sabes, el formato del archivo de teclas Teclas.mnu ha cambiado en Digi3D 2011.

El formato antiguo tenía una serie de limitaciones como era que únicamente permitía añadir una orden a una pulsación de tecla (lo que te obligaba a crear archivos de macroinstrucciones externos si querías asignar más de una orden a una pulsación de tecla, cosa que personalmente nunca me ha gustado) y peor aún, no había forma saber a que tecla o combinación de teclas se refería un determinado comando, ya que cada pulsación de tecla o combinación de teclas generaba un número único imposible de identificar. Recuerda que existe un post en este blog que te explica cómo convertir un archivo de teclas antiguo al nuevo formato https://desarrollodigi3d.digi21.net/2011/01/25/archivos-de-teclas-de-digi3d-2011/

En este post vamos a aprender cómo convertir un archivo de teclas en formato xml en algo que sea legible por el ser humano, la idea es modificar el archivo xml para que al hacer doble clic sobre él, el navegador/explorador web que utilices nos muestre una tabla con la tecla, indicando además si estaba pulsada la tecla Alt, Mayúsculas o Control y la descripción asociada a la pulsación de tecla.

La idea es convertir esto:

<?xml version="1.0" encoding="utf-8"?>
<Keyboard xmlns="http://schemas.digi21.net/Digi3D/keyboard/v1.0">
  <Key Name="NUMERO 0" Description="Cambia el tamaño del cursor"><![CDATA[CURSOR]]></Key>
  <Key Name="NUMERO 1" Description="Borra entidades por ventana"><![CDATA[BORRA_V]]></Key>
  <Key Name="TECLA DE ADICION" Description="Selecciona el código activo"><![CDATA[COD]]></Key>
  <Key Name="NUMERO 9" Description="Dibuja un cruce de caminos"><![CDATA[CRUCE]]></Key>
  <Key Name="NUMERO 6" Description="Dibuja una paralela a una línea existente"><![CDATA[PARALELA_DINAMICA]]></Key>
  <Key Name="NUMERO 4" Description="Asigna como código activo el de la línea seleccionada"><![CDATA[clonar]]></Key>
  <Key Name="NUMERO 5" Description="Finaliza la línea que está dibujando cerrándola"><![CDATA[cierra_ent]]></Key>
  <Key Name="NUMERO 7" Description="Cambia el sentido de la línea seleccionada"><![CDATA[CAMB_SEN]]></Key>
  <Key Name="NUMERO 8" Description="Une las dos líneas seleccionadas"><![CDATA[UNIR]]></Key>
  <Key Name="P" Shift="true" Description="Matorral"><![CDATA[cod=58]]></Key>
  <Key Name="A" Description="Construcción"><![CDATA[cod=30
modob=2 0]]></Key>
  <Key Name="G" Description="Muro malla"><![CDATA[cod=35
modob=2 0]]></Key>
  <Key Name="H" Description="Cerca de alambre"><![CDATA[cod=37
modob=2 0]]></Key>
  <Key Name="I" Description="Presa dique malecón"><![CDATA[COD=11
modob=2 5]]></Key>
  <Key Name="K" Description="Antena"><![CDATA[cod=34
modob=5]]></Key>
  <Key Name="5" Description="Tentativo a línea en 3D"><![CDATA[modob=5]]></Key>
  <Key Name="'" Description="Punto de cota fotogramétrica"><![CDATA[cod=41
punto
modob=5
cota]]></Key>
</Keyboard>

…en esto otro:

Listado de teclas

Tecla Mayúsculas Control Descripción
NUMERO 0 Cambia el tamaño del cursor
NUMERO 1 Borra entidades por ventana
TECLA DE ADICION Selecciona el código activo
NUMERO 9 Dibuja un cruce de caminos
NUMERO 6 Dibuja una paralela a una línea existente
NUMERO 4 Asigna como código activo el de la línea seleccionada
NUMERO 5 Finaliza la línea que está dibujando cerrándola
NUMERO 7 Cambia el sentido de la línea seleccionada
NUMERO 8 Une las dos líneas seleccionadas
P X Matorral
A Construcción
G Muro malla
H Cerca de alambre
I Presa dique malecón
K Antena
5 Tentativo a línea en 3D
Punto de cota fotogramétrica

Para realizar nuestra tarea lo único que tenemos que hacer es crear una transformación XML que transforme nuestro documento original en un archivo .html básicamente con una tabla con cuatro columnas, en la primera pondremos la tecla en cuestión, en la segunda una X si estaba pulsada la tecla mayúsculas, en la tercera una X si estaba pulsada la tecla control y por último la descripción.

Estas transformaciones de un documento .xml a otro tipo de documento (que podría ser otro .xml, un archivo .html, o un archivo .txt por ejemplo) se realizan mediente Transformaciones XSL que es un estándar precisamente para esto, transformar un archivo .xml en otra cosa.

No te voy a enseñar aquí XSLT, para eso existen multitud de tutoriales en internet. Yo no soy amigo de los tutoriales, prefiero comprarme un buen libro. Personalmente aprendí XSLT (y todo lo relacionado con XML) con el libro Essential XML Quick Reference: A Programmer’s Reference to XML, XPath, XSLT, XML Schema, SOAP, and More (DevelopMentor)

Primer paso: Indicar que nuestro archivo de teclas debe ser transformado mediante una transformación XSL

Para ello, lo único que tenemos que hacer es añadir la instrucción de preprocesador xml-stylesheet en nuestro archivo de teclas haciendo una referencia al archivo .xls en el que vamos a implementar la transformación:

<?xml-stylesheet type="text/xsl" href="keyboard.xsl" ?>

Vamos a poner nuestra instrucción al comienzo del documento, después de la instrucción xml, y antes del nodo raiz, de modo que las primeras líneas de nuestro archivo .xml original son las siguientes:

<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="keyboard.xsl" ?>
<Keyboard xmlns="http://schemas.digi21.net/Digi3D/keyboard/v1.0">
  <Key Name="NUMERO 0" Description="Cambia el tamaño del cursor"><![CDATA[CURSOR]]></Key>
  <Key Name="NUMERO 1" Description="Borra entidades por ventana"><![CDATA[BORRA_V]]></Key>
  <Key Name="TECLA DE ADICION" Description="Selecciona el código activo"><![CDATA[COD]]></Key>

Segundo paso: Crear la transformación

Ahora únicamente nos queda crear nuestro archivo de transformación. Creamos el archivo keyboard.xls con la siguiente información:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
</xsl:stylesheet>

A continuación añadimos la plantilla de transformación indicando sobre que nodo o conjunto de nodos queremos realizar la transformación, para ello indicamos la cadena XPath:

/Keyboard

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/Keyboard">
  </xsl:template>
</xsl:stylesheet>

y a continuación creamos el cuerpo del archivo html a crear:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/Keyboard">
    <html xmlns="http://www.w3.org/1999/xhtml">
      <title>Listado de teclas</title>
      <body>
        <h3>Listado de teclas</h3>

        <table border="1">
          <tr>
            <td>Tecla</td>
            <td>Mayúsculas</td>
            <td>Control</td>
            <td>Descripción</td>
          </tr>

        </table>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

Tan solo nos queda añadir una entrada por cada nodo hijo Key tenga el nodo Keyboard del archivo de teclas original.

Para ello utilizaremos la instrucción for-each para añadir tantas filas como nodos hijos tenga el nodo Keyboard de la siguiente manera:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/Keyboard">
    <html xmlns="http://www.w3.org/1999/xhtml">
      <title>Listado de teclas</title>
      <body>
        <h3>Listado de teclas</h3>

        <table border="1">
          <tr>
            <td>Tecla</td>
            <td>Mayúsculas</td>
            <td>Control</td>
            <td>Descripción</td>
          </tr>

          <xsl:for-each select="Key">
            <tr>
              <td>
              </td>

              <td>
              </td>

              <td>
              </td>

              <td>
              </td>
            </tr>
          </xsl:for-each>
        </table>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

y para terminar tendremos que rellenar cada uno de los campos con el valor.

Para añadir el nombre de la tecla en el primer campo, utilizaremos la instrucción value-of, indicando que queremos almacenar el valor del atributo Name. Haremos lo mismo con el último campo, donde añadiremos la información del atributo Description.

Para los campos Mayúsculas y Control podemos utilizar la instrucción if, indicando que nos introduzca el carácter X únicamente si los atributos Shift o Control tienen asignado el valor verdadero.

Nuestra transformación tiene la siguiente forma:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/Keyboard">
    <html xmlns="http://www.w3.org/1999/xhtml">
      <title>Listado de teclas</title>
      <body>
        <h3>Listado de teclas</h3>

        <table border="1">
          <tr>
            <td>Tecla</td>
            <td>Mayúsculas</td>
            <td>Control</td>
            <td>Descripción</td>
          </tr>

          <xsl:for-each select="Key">
            <tr>
              <td>
                <xsl:value-of select="@Name"/>
              </td>

              <td>
                <xsl:if test="@Shift">X</xsl:if>
              </td>

              <td>
                <xsl:if test="@Control">X</xsl:if>
              </td>

              <td>
                <xsl:value-of select="@Description"/>
              </td>
            </tr>
          </xsl:for-each>
        </table>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

Pero aún no funcionará, ya que todos los nodos del archivo de teclas están almacenados en el espacio de nombres http://schemas.digi21.net/Digi3D/keyboard/v1.0, por lo tanto las consultas XPath a la hora de realizar la transformación no localizarán ni el nodo Keyboard ni el nodo Key.

Para solucionar nuestro problema y finalizar el ejercicio, tan solo nos queda añadir una definición de espacio de nombres a nuestro archivo de transformación xsl que denominaremos teclasMnu y que apuntará a la url: http://schemas.digi21.net/Digi3D/keyboard/v1.0 y sustituiremos las consultas XPath /Keyboard por teclasMnu:Keyboard y Key por teclasMnu:Key:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:teclasMnu="http://schemas.digi21.net/Digi3D/keyboard/v1.0">
  <xsl:template match="/teclasMnu:Keyboard">
    <html xmlns="http://www.w3.org/1999/xhtml">
      <title>Listado de teclas</title>
      <body>
        <h3>Listado de teclas</h3>

        <table border="1">
          <tr>
            <td>Tecla</td>
            <td>Mayúsculas</td>
            <td>Control</td>
            <td>Descripción</td>
          </tr>

          <xsl:for-each select="teclasMnu:Key">
            <tr>
              <td>
                <xsl:value-of select="@Name"/>
              </td>

              <td>
                <xsl:if test="@Shift">X</xsl:if>
              </td>

              <td>
                <xsl:if test="@Control">X</xsl:if>
              </td>

              <td>
                <xsl:value-of select="@Description"/>
              </td>
            </tr>
          </xsl:for-each>
        </table>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

Y ya hemos terminado. Te propongo que añadas un campo más, para indicar si estaba pulsada la tecla Alt y que además utilices una hoja de estilos en cascada para darle un aspecto más moderno.

Descarga los archivos de ejemplo de este ejercicio.

Detectando líneas sin continuidad

Aquí el vídeo relacionado con esta entrada

En ocasiones nos interesa realizar análisis de continuidad geométrica.

Quizás queramos detectar como error líneas que no tienen continuidad, es decir, que en las coordenadas de alguno de sus extremos no nace otra línea, o quizás justo lo contrario, una línea que continúa con otra con un código que las hace incompatibles, como curva de nivel fina con curva de nivel maestra, o dos curvas de nivel finas pero con distinta coordenada Z, …

Si queremos hacer un control de bordes, quizás nos interese detectar como errores líneas que en la práctica no tienen la obligación de continuar con otra línea, pero que finalizan en un marco de hoja y no continúan en otro archivo.

Todos estos análisis los podemos realizar mediante el tipo Digi21.DigiNG.Topology.NodeDetector que implementa una serie de métodos de extensión que se ejecutan sobre una secuencia de líneas y que nos devolverá una secuencia de IGrouping<Point2D, VertexPointer>, es decir, una secuencia de entidades agrupadas por el punto en el que coinciden esas entidades.

El tipo Digi21.DigiNG.Entities.VertexPointer es muy parecido al tipo Digi21.DigiNG.Entities.SegmentPointer, dispone de dos propiedades: una para indicar la línea que llega al nodo y otra para indicar el vértice de esa línea que llega al nodo.

A continuación la definición de este tipo:

namespace Digi21.DigiNG.Entities
{
    public struct VertexPointer
    {
        public VertexPointer(ReadOnlyLine line, int vertex);

        public ReadOnlyLine Line { get; }
        public int Vertex { get; }
    }
}

Al igual que en el caso del detector de intersecciones, dispondemos de varias sobrecargas del método de extensión que nos permitirán o detectar todos los nodos, o especificar si permitimos analizar una determinada entidad o si nos interesa un nodo en unas determinadas coordenadas o una combinación de estas opciones.

Veamos un ejemplo muy sencillo: vamos a crear una orden que añade tantas tareas en la ventana de tareas como entidades que no tienen continuidad.
Lo que vamos a hacer es detectar todos los nodos y crear una consulta Linq que se quede únicamente con la secuencia de nodos que estén formados únicamente por una entidad.

Si tenemos dos líneas, A y B, ambas formadas por dos puntos, y con las siguientes coordenadas: A: (100, 100) – (200, 200) y B: (200, 200) – (300, 100) tendremos tres nodos, uno en las coordenadas (100, 100) al que llega únicamente una entidad, la entidad A, un segundo nodo (200, 200) al que llegan dos entidades, la A y la B y por último el tercer nodo (300, 100) al que llega únicamente la entidad B.

Está claro que los nodos A y B son nodos en los que no hay continuidad pues únicamente llega a ellos una línea.

Aquí el código de la orden:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG.Topology;
using Digi21.DigiNG;
using Digi21.DigiNG.Entities;
using Digi21.Digi3D;
using Digi21.Math;

namespace Acme
{
    [Command(Name="detectar_líneas_sin_continuidad")]
    public class DetectarLíneasSinContinuidad : Command
    {
        public DetectarLíneasSinContinuidad()
        {
            this.Initialize += new EventHandler(DetectarLíneasSinContinuidad_Initialize);
        }

        void DetectarLíneasSinContinuidad_Initialize(object sender, EventArgs e)
        {
            try
            {
                var todosLosNodos = DigiNG.DrawingFile.OfType<ReadOnlyLine>().DetectNodes();

                var nodosConUnaÚnicaLínea = from nodo in todosLosNodos
                                            where nodo.Count() == 1
                                            select nodo;

                foreach (var nodo in nodosConUnaÚnicaLínea)
                    Digi3D.Tasks.Add(new TaskEntityGotoPoint(
                        (Point3D)nodo.Key,
                        nodo.ElementAt(0).Line,
                        2,
                        "Extremo de línea sin continuidad",
                        TaskSeverity.Error,
                        DigiNG.DrawingFile.Path,
                        "detectar_líneas_sin_continuidad"));
                DigiNG.RenderScene();
            }
            finally
            {
                Dispose();
            }
        }
    }
}

Muy sencillo, ¿no?. Con esto hemos simulado la opción de Bintram que marca como errores entidades sin continuidad.

Vamos a añadir una tabla de códigos a nuestra orden. Ahora la orden va tener en cuenta únicamente las líneas que tengan alguno de los códigos pasados por parámetros:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG.Topology;
using Digi21.DigiNG;
using Digi21.DigiNG.Entities;
using Digi21.Digi3D;
using Digi21.Math;
using UtilidadesDigi;

namespace Acme
{
    [Command(Name="detectar_líneas_sin_continuidad")]
    public class DetectarLíneasSinContinuidad : Command
    {
        public DetectarLíneasSinContinuidad()
        {
            this.Initialize += new EventHandler(DetectarLíneasSinContinuidad_Initialize);
        }

        void DetectarLíneasSinContinuidad_Initialize(object sender, EventArgs e)
        {
            try
            {
                if (this.Args.Length == 0)
                {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "detectar_líneas_sin_continuidad",
                        "No has indicado los códigos de las entidades a analizar",
                        2,
                        BallonIcon.Error);
                    return;
                }

//                var todosLosNodos = DigiNG.DrawingFile.QueTenganAlgúnCódigo(this.Args).SoloLíneas().DetectNodes();
                var todosLosNodos = DigiNG.DrawingFile.
                    SoloLíneasSinEliminar().DetectNodes(
                    entidad => entidad.TieneAlgúnCódigo(this.Args));


                var nodosConUnaÚnicaLínea = from nodo in todosLosNodos
                                            where nodo.Count() == 1
                                            select nodo;

                foreach (var nodo in nodosConUnaÚnicaLínea)
                    Digi3D.Tasks.Add(new TaskEntityGotoPoint(
                        (Point3D)nodo.Key,
                        nodo.ElementAt(0).Line,
                        2,
                        "Extremo de línea sin continuidad",
                        TaskSeverity.Error,
                        DigiNG.DrawingFile.Path,
                        "detectar_líneas_sin_continuidad"));
                DigiNG.RenderScene();
            }
            finally
            {
                Dispose();
            }
        }
    }
}

Evitando "código duro" en nuestra orden contabilizadora de entidades

Aquí el vídeo relacionado con este post

En el post anterior desarrollamos una orden que contabilizaba todos los tipos de entidad de todos los archivos cargados.

Esta orden tenía programado mediante código duro los distintos casos a contabilizar, como líneas, puntos, textos, …

¿Que pasaría si nuestra orden en un futuro se ejecuta en una versión más moderna de Digi3D.NET que admite nuevos tipos de entidades, como por ejemplo esferas?

Pues sencillamente que no sería capaz de contabilizar el número de esferas porque no contempla ese caso.

Siempre que desarrollemos este tipo de órdenes, deberíamos intentar evitar el código duro y hacer que la orden esté preparada para un futuro.

Linq nos permite agrupar (mediante la cláusula group) una secuencia de objetos mediante una clave, y gracias a que mediante Reflexión podemos obtener en tiempo de ejecución el tipo de un determinado objeto (llamando al método Object.GetType(), podríamos modificar nuestra orden y convertirla en algo como lo siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG;
using Digi21.Digi3D;
using UtilidadesDigi;
using Digi21.DigiNG.Entities;

namespace Acme
{
    [Command(Name = "suma_todas_las_entidades")]
    public class SumaTodasLasEntidades
        : Command
    {
        public SumaTodasLasEntidades()
        {
            this.Initialize += new EventHandler(SumaTodasLasEntidades_Initialize);
        }

        void SumaTodasLasEntidades_Initialize(object sender, EventArgs e)
        {
            try
            {
                var agrupación = from entidad in UtilidadesDigiNG.EnumeraTodasLasEntidades()
                                 group entidad by entidad.GetType().Name;

                foreach(var tiposDeEntidad in agrupación)
                    Digi3D.OutputWindow.WriteLine(
                        "{0}: {1}",
                        tiposDeEntidad.Key,
                        tiposDeEntidad.Count());
            }
        }
    }
}

De esta manera hemos desvinculado completamente nuestra orden de unos tipos de entidad fijos y está preparada para el futuro.

Secuencias de entidades de un tipo y enumerando entidades de todos los archivos cargados

Aquí el vídeo relacionado con este post

En el post anterior creamos un método de extensión denominado EnumeraEntidades que extendía una secuencia de archivos de dibujo de solo lectura y nos devolvía una secuencia de todas las entidades de todos los archivos de referencia cargados dando la impresión que únicamente había un único archivo.

Esta secuencia devolvía todas las entidades de los archivos de referencia cargados, independientemente de su tipo.

En este post vamos a crear una sobrecarga de este método de extensión para hacerlo genérico y poder indicar mediante su parámetro genérico el tipo de entidad en el que estamos interesados:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Entities;
using Digi21.DigiNG.IO;

namespace UtilidadesDigi
{
    public static class UtilidadesSecuenciaReadOnlyDrawingFile
    {
        public static IEnumerable<T> EnumeraEntidades<T>(this IEnumerable<ReadOnlyDrawingFile> archivos)
            where T:class
        {
            var entidadesADevolver = from archivo in archivos
                                     from entidad in archivo
                                     where entidad is T
                                     select entidad as T;

            return entidadesADevolver;
        }

    }
}

Si te fijas, he cambiado completamente el algoritmo. Ahora ya no utilizamos yield return, y toda la lógica está expresada como una consulta Linq. Podríamos haberlo hecho así en el post anterior, pero quería enseñarte el funcionamiento de yield return.

Ahora tenemos que cambiar el código de la orden, que ya no va a llamar a CuantasEntidadesDeTipo<T>:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Entities;
using Digi21.DigiNG.IO;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG;
using Digi21.Digi3D;
using UtilidadesDigi;

namespace Acme
{
    [Command(Name="suma_entidades_archivos_referencia")]
    public class SumaEntidadesArchivosReferencia
        : Command
    {
        public SumaEntidadesArchivosReferencia()
        {
            this.Initialize += new EventHandler(SumaEntidadesArchivosReferencia_Initialize);
        }

        void SumaEntidadesArchivosReferencia_Initialize(object sender, EventArgs e)
        {
            try
            {
                if (DigiNG.ReferenceFiles.Length == 0)
                {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "suma_entidades_archivos_referencia",
                        "No hay ningún archivo de referencia cargado",
                        2,
                        BallonIcon.Error);
                    return;
                }

                Digi3D.OutputWindow.WriteLine(
                    "Suma de todos los tipos de entidades de los archivos de referencia cargados:");


                Digi3D.OutputWindow.WriteLine(
                    "Número de líneas: {0}",
                    DigiNG.ReferenceFiles.EnumeraEntidades<ReadOnlyLine>().Count());

                Digi3D.OutputWindow.WriteLine(
                    "Número de puntos: {0}",
                    DigiNG.ReferenceFiles.EnumeraEntidades<ReadOnlyPoint>().Count());

                Digi3D.OutputWindow.WriteLine(
                    "Número de textos: {0}",
                    DigiNG.ReferenceFiles.EnumeraEntidades<ReadOnlyText>().Count());

                Digi3D.OutputWindow.WriteLine(
                    "Número de polígonos: {0}",
                    DigiNG.ReferenceFiles.EnumeraEntidades<ReadOnlyPolygon>().Count());

                Digi3D.OutputWindow.WriteLine(
                    "Número de complejos: {0}",
                    DigiNG.ReferenceFiles.EnumeraEntidades<ReadOnlyComplex>().Count());
            }
            finally
            {
                Dispose();
            }
        }
    }
}

Ahora vamos a hacer un método más que nos va a enumerar todas las entidades de todos los archivos cargados, tanto del archivo de dibujo como de los archivos de referencia.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Entities;
using Digi21.DigiNG;

namespace UtilidadesDigi
{
    public static class UtilidadesDigiNG
    {
        public static IEnumerable<Entity> EnumeraTodasLasEntidades()
        {
            foreach (var entidad in DigiNG.DrawingFile)
                yield return entidad;

            foreach (var archivoReferencia in DigiNG.ReferenceFiles)
                foreach (var entidad in archivoReferencia)
                    yield return entidad;
        }

        public static IEnumerable<T> EnumeraTodasLasEntidades<T>()
            where T:class
        {
            foreach (var entidad in DigiNG.DrawingFile)
            {
                if( entidad is T)
                    yield return entidad as T;
            }

            foreach (var archivoReferencia in DigiNG.ReferenceFiles)
            {
                foreach (var entidad in archivoReferencia)
                {
                    if( entidad is T )
                        yield return entidad as T;
                }
            }
        }
    }
}

y para probar su funcionamiento, vamos a crear una orden nueva que va a contabilizar las entidades de cada tipo de todos los archivos cargados:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG;
using Digi21.Digi3D;
using UtilidadesDigi;
using Digi21.DigiNG.Entities;

namespace Acme
{
    [Command(Name = "suma_todas_las_entidades")]
    public class SumaTodasLasEntidades
        : Command
    {
        public SumaTodasLasEntidades()
        {
            this.Initialize += new EventHandler(SumaTodasLasEntidades_Initialize);
        }

        void SumaTodasLasEntidades_Initialize(object sender, EventArgs e)
        {
            try
            {
                Digi3D.OutputWindow.WriteLine(
                    "Suma de todos los tipos de entidades de todos los archivos cargados:");

                Digi3D.OutputWindow.WriteLine(
                    "Número de líneas: {0}",
                    UtilidadesDigiNG.
                    EnumeraTodasLasEntidades<ReadOnlyLine>().Count());

                Digi3D.OutputWindow.WriteLine(
                    "Número de puntos: {0}",
                    UtilidadesDigiNG.
                    EnumeraTodasLasEntidades<ReadOnlyPoint>().Count());

                Digi3D.OutputWindow.WriteLine(
                    "Número de textos: {0}",
                    UtilidadesDigiNG.
                    EnumeraTodasLasEntidades<ReadOnlyText>().Count());

                Digi3D.OutputWindow.WriteLine(
                    "Número de polígonos: {0}",
                    UtilidadesDigiNG.
                    EnumeraTodasLasEntidades<ReadOnlyPolygon>().Count());

                Digi3D.OutputWindow.WriteLine(
                    "Número de complejos: {0}",
                    UtilidadesDigiNG.
                    EnumeraTodasLasEntidades<ReadOnlyComplex>().Count());
            }
            finally
            {
                Dispose();
            }
        }
    }
}