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

Comentarios desactivados abril 25th, 2012

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.

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.

Actualizando la base de datos Epsg v7.9 para añadir transformaciones de cambio de datum ED50 -> ETRS89 y ED50 -> WGS84

Comentarios desactivados abril 17th, 2012

Este post es avanzado, y supone que acabas de descargar una versión nueva de la base de datos EPSG y la has exportado al formato Microsoft SQL Server Compact tal y como se explica en el post Cómo se ha creado la base de datos geográfica EPSG en formato SQL Server Compact que se instala con Digi3D 2011.

La última versión de los archivos de rejilla de mínima curvatura NTv2 suministrados por el Instituto Geográfico Nacional data del 2009, sin embargo no existe ninguna transformación en la base de datos EPSG v7.9 (la mas moderna a día de hoy 17/04/2012) que utilice este archivo de mínima curvatura.

En este post te explico cómo modificar la base de datos para añadir una transformación que utilice el archivo de mínima curvatura actualizado.

La base de datos EPSG define 12 transformaciones para transformar entre los sistemas de coordenadas geográficos ED50 y ETRS89 y 40 transformaciones para transformar entre ED50 y WGS 84.

Puedes comprobarlo mediante las siguientes consultas SQL:

SELECT COUNT(*)
FROM Coordinate_Operation
WHERE SOURCE_CRS_CODE=4230 and TARGET_CRS_CODE=4258 and COORD_OP_TYPE='transformation';

SELECT COUNT(*)
FROM [Coordinate_Operation]
WHERE SOURCE_CRS_CODE=4230 and TARGET_CRS_CODE=4326 AND COORD_OP_TYPE='transformation';

Muchas de ellas consisten simplemente en desplazamientos geográficos (es decir, transformar de coordenadas geográficas a geocéntricas y realizar un desplazamiento en geocéntricas para luego volver a transformar a geográficas), por lo que tienen poca precisión.

De entre todos los algoritmos para transformar entre sistemas geográficos, el más preciso es el método interpolación mediante una rejilla de mínima curvatura. Esta rejilla le indica al programa: “Para este par de coordenadas latitud, longitud en el sistema de coordenadas de referencia ED50, le corresponde este otro par de coordenadas latitud, longitud en el sistema de coordenadas de referencia ETRS89”. Luego el programa tan solo tiene que interpolar mediante una interpolación bilineal. El resultado es muy preciso (20 cm para el caso de España).

Es responsabilidad de cada país/organismo crear su archivo de rejilla y de publicarlo. Cualquiera podría inventarse su propio formato de rejilla (lo que obligaría a que los desarrolladores de programas de cartografía y sistemas de información geográfica tuvieran que implementar un importador específico para cada formato de rejilla) pero existe uno estandarizado denominado NTv2.

En el caso de España, el organismo encargado de crear y actualizar ese archivo de rejilla es el Instituto Geográfico Nacional que publica este archivo a través de su página en internet: www.ign.es.

Descarga los archivos de rejilla entrando en www.ign.es: en el menú Herramientas de la página web, puedes seleccionar la opción: Rejilla para cambio de Datum entre ED50 y ETRS89 (en formato NTV2). Verás que puedes descargar dos rejillas: una para Península y otra para Baleares. Descarga el archivo Península, comprobarás que el nombre del archivo descargado es PENR2009.gsb.

Como la creación y mantenimiento de los archivos NTv2 depende del centro geográfico nacional de cada país, la base de datos EPSG no distribuye estos datos, sin embargo, Digi3D 2011 incorpora una copia de estos archivos en la carpeta %ProgramData%\Digi3D 2011\OpenGis, de modo que en ese directorio encontrarás el archivo PENR2009.gsb.

El único inconveniente es que la base de datos original EPSG (la que acabas de descargar de http://www.epsg.org/Geodetic.html no dispone de ninguna transformación que utilice este archivo, únicamente tiene transformaciones que utilizan versiones anteriores de ese archivo.

Vamos a comprobarlo:

La base de datos EPSG dispone de una tabla denominada ‘Coordinate_Operation Method’ que registra los códigos, nombres y descripciones de las distintas transformaciones.

Consultemos el código asociado al algoritmo NTv2 mediante la siguiente consulta SQL:

SELECT * FROM [Coordinate_Operation Method]
WHERE COORD_OP_METHOD_NAME='NTv2';

Puedes comprobar que el algoritmo NTv2 tiene asociado el código 9615.

Podemos consultar también los parámetros que recibe un determinado algoritmo. Comprobemos los parámetros que recibe el algoritmo 9615:

La base de datos EPSG dispone de una tabla ‘Coordinate_Operation Parameter Usage’ que nos indica los parámetros de cada algoritmo. Estos parámetros (como todo en la base de datos EPSG) tienen asignado un código. Disponemos de otra tabla ‘Coordinate_Operation Parameter’ que nos muestra una descripción de cada tipo de parámetro.

Para averiguar los parámetros que recibe la transformación con código 9615 podemos ejecutar la siguiente consulta SQL:

SELECT *
FROM [Coordinate_Operation Parameter Usage]
WHERE COORD_OP_METHOD_CODE=9615;

Eso nos devolverá un único registro indicando que el único parámetro que recibe el algoritmo 9615 es el parámetro cuyo código es el 8656.

Podemos consultar el significado de ese parámetro con la siguiente consulta SQL:

SELECT *
FROM [Coordinate_Operation Parameter]
WHERE PARAMETER_CODE=8656;

Por último, si quieres hacerlo bien, puedes hacerlo con una consulta JOIN de la siguiente manera:

SELECT *
FROM [Coordinate_Operation Parameter Usage] AS A
JOIN [Coordinate_Operation Parameter] AS B
ON A.PARAMETER_CODE = B.PARAMETER_CODE
WHERE A.COORD_OP_METHOD_CODE=9615;

De entre todas las posibles transformaciones (te recuerdo que había 12) entre ED50 y ETRS89 que define la base de datos EPSG, únicamente hay dos que utilicen el algoritmo 9615: La transformación 15895: ED50 to ETRS89 (11) y la operación 15932: ED50 to ETRS89 (12).

Puedes comprobarlo mediante la siguiente consulta SQL:

SELECT *
FROM Coordinate_Operation
WHERE SOURCE_CRS_CODE=4230 and TARGET_CRS_CODE=4258 and COORD_OP_TYPE='transformation' AND COORD_OP_METHOD_CODE=9615;

Si nos leemos los comentarios de estas dos transformaciones, rápidamente averiguamos que la transformación 15895 ha sido sustituida por la 15932, de modo que la que nos interesará utilizar es la 15932, pero tal y como vamos a comprobar muy pronto, esta transformación no nos sirve, pues el archivo de rejilla a que utiliza esta transformación no es “PENR2009.gsb”.

Los parámetros de las transformaciones se almacenan en la tabla ‘Coordinate_Operation Parameter Value’.

Esta tabla almacena los parámetros de todos los algoritmos utilizados en todas las posibles transformaciones especificadas en la base de datos EPSG. Actualmente tiene 15743 registros.

Cada registro tiene varios campos:

COORD_OP_CODE COORD_OP_METHOD_CODE PARAMETER_CODE PARAMETER_VALUE PARAM_VALUE_FILE_REF UOM_CODE

Si comprobamos los registros de esta tabla para el código de operación 15932, comprobaremos que el nombre del archivo de rejilla asignado para esa operación es “SPED2ETV2.gsb”.

SELECT * FROM [Coordinate_Operation Parameter Value] WHERE COORD_OP_CODE=15932;

Por lo tanto esta operación no nos sirve. El archivo “SPED2ETV2.gsb” es el nombre que tenía la versión antigua de los archivos de rejilla, de hecho, la operación 15932 se añadió a la base de datos EPSG con fecha 27-03-2007, y el nombre de archivo “PENR2009” nos hace pensar que este archivo se creó en el año 2009.

Podrías pensar que una solución sería modificar este registro de la base de datos y cambiar la palabra “SPED2ETV2.gsb” por “PENR2009.gsb”, pero esto no se puede hacer porque la base de datos EPSG es inmutable. No se permiten cambios. Podemos añadir información, transformaciones, sistemas de coordenadas, pero nunca cambiar nada.

Así que no nos queda más remedio que crear una transformación nueva.

En la documentación de EPSG te indican que si quieres añadir información, utilices siempre códigos con valores superiores a 32767, por lo tanto vamos a crear una transformación con código 32768 que nos servirá para transformar entre los sistemas de coordenadas geográficos ED50 (4230) y ETRS89 (4326), para la zona de España con el algoritmo 9615 y utilizando como archivo de rejilla NTv2 el archivo “PENR2009.gsb”.

Para ello ejecutaremos la consulta:

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, INFORMATION_SOURCE, DATA_SOURCE, REVISION_DATE, SHOW_OPERATION, DEPRECATED)
VALUES (32768, 'ED50 a ETRS89 (14)', 'transformation', 4230, 4258, 'IGN-Esp v3', 14, 3429, 'For applications to an accuracy of 10-15cm (95% confidence) for Spain mainland and about 4cm (95%) for Balearic Islands.',0.2,9615,'Reemplaza a ED50 to ETRS89(12)','Instituto Geográfico Nacional, www.ign.es', 'OGP', 2009-01-01, 1, 0);

Y ya puestos, y considerando que el sistema de coordenadas geográfico ETRS89 es idéntico al WGS84, vamos a crear una transformación para transformar entre los sistemas de coordenadas geográficos ED50 (4230) y WGS84 (4326), esta vez con código 32769 con la siguiente consulta:

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, INFORMATION_SOURCE, DATA_SOURCE, REVISION_DATE, SHOW_OPERATION, DEPRECATED)
VALUES (32769, 'ED50 a WGS84 (43)', 'transformation', 4230, 4326, 'IGN-Esp v3', 43, 3429, 'For applications to an accuracy of 10-15cm (95% confidence) for Spain mainland and about 4cm (95%) for Balearic Islands.',0.2,9615,'Valor de los parámetros de la transformación ED50 a ETRS89 (14). Asume que ETRS89 y WGS 84 se consideran iguales dentro de la precisión de la transformación. Reemplaza ED50 to WGS 84 (41)','Instituto Geográfico Nacional, www.ign.es', 'OGP', 2009-01-01, 1, 0);

Tan solo nos queda añadir en la tabla ‘Coordinate_Operation Parameter Value’ los parámetros para estas dos transformaciones.

Para ello tan solo tenemos que ejecutar las consultas:

INSERT INTO [Coordinate_Operation Parameter Value]
VALUES (32768, 9615, 8656, NULL, 'PENR2009.gsb', NULL);

INSERT INTO [Coordinate_Operation Parameter Value]
VALUES (32769, 9615, 8656, NULL, 'PENR2009.gsb', NULL);

y ya lo tenemos. Ahora cuando Digi3D detecte que tiene que transformar entre los sistemas de coordenadas de referencia ED50 y ETRS89 o ED50 y WGS 84 nos mostrará esta transformacion en el cuadro de diálogo de posibles transformaciones.

Cómo se ha creado la base de datos geográfica EPSG en formato SQL Server Compact que se instala con Digi3D 2011

Comentarios desactivados enero 23rd, 2012

Estoy terminando de implementar los sistemas de cordenadas de referencia en Digi3D 2011, y me he encontrado con un problema inesperado: Las consultas SQL que debe ejecutar Digi3D 2011 para realizar determinadas consultas sobre la base de datos geográfica varían en función de si la base de datos está almacenada en un archivo .mdb (de Microsoft Access) o está alojada en una instancia de Microsoft SQL Server.

La base de datos geográfica en formato .mdb se puede utilizar en las versiones de 32 bits de Digi3D 2011 sin ningún problema. Las versiones de 64 bits no tienen esa suerte, ya que no es posible que un programa de 64 bits se conecte con un archivo en formato .mdb.

La única manera que tenían las versiones de 64 bits de conectarse con esa base de datos era creando una base de datos SQL Server y configurar el programa para que se conecte con esta base de datos en vez de con el archivo local .mdb. Tienes instrucciones de cómo realizar esa tarea en el post Base de datos EPSG en Digi3D 2011 edición de 64 Bits.

El problema ha surgido porque con la versión que liberaré esta semana (me imagino que el día 25 a 27 de enero de 2012) en ciertas consultas es necesario saber si el usuario ha marcado alguna operación como visible o no.

Si en un determinado momento Digi3D 2011 necesita transformar una coordenada de un sistema de coordenadas de referencia a otro, tiene que realizar una consulta SQL sobre la base de datos geográfica y ésta le indica de todas las operaciones (cada una con mejor o peor precisión) que existen para transformar coordenadas entre los dos sistemas.

Si se localiza más de una operación, Digi3D 2011 muestra un cuadro de diálogo al usuario indicando que se han localizado múltiples métodos, invitándole a que seleccione el más apropiado.

En ocasiones no queremos que Digi3D 2011 muestre todas las posibles opciones, como por ejemplo en el caso de transformar entre los sistemas de coordenadas de referencia ED50 y ETRS89: siempre nos interesará utilizar la operación que utiliza el algoritmo NTv2 y nunca las que utilizan desplazamientos geocéntricos.

La base de datos dispone de un campo denominado SHOW_OPERATION que en Access es de tipo booleano, pero que en SQL Server se convierte a campo de tipo bit.

Y el problema surgue aquí: Si queremos localizar todas las transformaciones entre los sistemas de coordenadas 4230 y 4258 que sean visibles, con SQL Server tenemos que ejecutar la siguiente consulta:

SELECT *
FROM Coordinate_Operation
WHERE SOURCE_CRS_CODE=4230
AND TARGET_CRS_CODE=4258
AND COORD_OP_TYPE='transformation'
AND SHOW_OPERATION=1

Sin embargo, si la base de datos no está ubicada en un SQL Server, sino que estamos conectados con el .mdb la consulta se realiza de esta otra forma:

SELECT *
FROM Coordinate_Operation
WHERE SOURCE_CRS_CODE=4230
AND TARGET_CRS_CODE=4258
AND COORD_OP_TYPE='transformation'
AND SHOW_OPERATION=True

Si te fijas hay una pequeña diferencia: en SQL Server utilizamos un 1 para filtrar los registros que tienen el campo SHOW_OPERATION a activo, sin embargo con Access tenemos que utilizar un True.

La solución más obvia es olvidarme del archivo .mdb que está desfasado (fíjate cómo de desfasado está que los señores de Microsoft directamente no te dejan conectarte con él en las versiones de 64 bits), pero eso obligaría a instalar un SQL Server Express junto con cada Digi3D 2011. Hasta ahora, así ha sido para las versiones de 64 bits de Digi3D 2011: había que instalar un SQL Server Express (o mejor) en el equipo o en la red de la empresa para que Digi3D 2011 se pudiera conectar con la base de datos geográfica.

Como no quiero forzar a instalar un SQL Server en cada equipo donde se instale Digi3D 2011 (porque habrá entornos en los que sencillamente no se puede, como centros geográficos nacionales con mucha seguridad), no queda más remedio que probar una tercera opción: Convertir la base de datos EPSG a formato Microsoft SQL Server Compact Edition, que es digámoslo así el sustituto de los obsoletos .mdb.

Este formato es un SQL Server en un archivo (con extensión .sdf) y está pensado para mono usuario y tiene la ventaja de que no hay que instalar un servidor, con copiar un par de DLLs junto con la instalación de Digi3D 2011 sobra.

El problema es que no existe ningún asistente, importador o exportador en Microsoft SQL Server Management Studio que convierta o un .mdb o una base de datos .mdf (las de Microsoft SQL Server) en un archivo .sdf.

Se me ocurren tres soluciones:

  1. Crear un guión (pulsando con el botón derecho del ratón en la base de datos EPSG en el panel Object Explorer de Microsoft SQL Server Management Studio, y seleccionando la opción Tasks/Generate scripts… del menú contextual. Crear una nueva base de datos en formato Microsoft SQL Server Compact y ejecutar sobre esta nueva base de datos el guión.
  2. Localizar alguna herramienta por Internet que convierta una base de datos .mdf (la extensión que tienen las bases de datos Microsoft SQL Server) en una con extensión .sdf.
  3. Programar una utilidad que realice la tarea.

La primera solución es compleja, pues el SQL que se genera en el guión no es compatible con SQL Server Compact y habría que corregirlo: añadir por puntos y comas al final de cada entrada, eliminar las cláusulas GO, el comando print que aparece en el guión cada X líneas, …).

La segunda opción es mucho más sencilla siempre y cuando exista alguna utilidad por internet.

La tercera es la última opción, pues debe de existir alguna utilidad por internet, ¡y la hay!, si buscas en Google localizarás un programa denominado SDF Viewer que nos va a solucionar la papeleta. Esta herramienta es de pago, pero permite que la utilicemos 15 días (tiempo más que de sobra para realizar esta práctica).

Vamos a ver cómo he construido la base de datos con extensión .sdf que se copia junto con la instalación de Digi3D 2011 (en la carpeta %ProgramData%\Digi3D 2011\epsg.sdf).

  1. Descárga e instala la aplicación SDF Viewer.
  2. Ejecuta la aplicación SDF Viewer.
  3. Aparecerá un cuadro de diálogo indicando que quedan X días de prueba. Pulsa la opción Ask Later.
  4. En Tools pulsa la opción From SQL Server.
  5. En el cuadro de diálogo Import From SQL Server, en el campo Server name: teclea la dirección de tu servidor Microsoft SQL Server en el cual tienes la base de datos EPSG (aquella que importaste si seguiste los pasos del post Base de datos EPSG en Digi3D 2011 edición de 64 Bits). Usualmente aquí tienes que poner .\sqlexpress
  6. En Authentication: selecciona el método de autenticación de tu instancia de SQL Server. Por defecto es Windows Authentication.
  7. Pulsa el botón Connect.
  8. En Database selecciona la base de datos EPSG.
  9. En Tables pulsa el botón Select All.
  10. Ásegúrate de que está seleccionado el checkbox titulado Include Data.
  11. Pulsa el botón Script.
  12. En el cuadro de diálogo común Guardar Como indica la ubicación al archivo .sqlce que va a generar el programa SDF Viewer con el SQL necesario para crear las tablas y los datos.
  13. Cierra el programa SDF Viewer.

Bien, ahora vamos a crear una base de datos nueva en formato Microsoft SQL Server Compact y la rellenaremos con la información generara con el programa SDF Viewer.

  1. Ejecuta Microsoft SQL Server Management Studio.
  2. En el cuadro de diálogo Connect to Server, en el campo Server type, selecciona SQL Server Compact.
  3. En el campo Database file despliega la lista y selecciona <New database>.
  4. En el cuadro de diálogo Create New SQL Server Compact Database, selecciona la ruta donde crear la base de datos SQL Server Compact y pulsa OK.
  5. Aparecerá un cuadro de diálogo indicando que si estás seguro de crear la base de datos sin protección por contraseña. Confirma pulsando Yes.
  6. Ya está creada la base de datos, pero volverá a aparecer el cuadro de diálogo Create New SQL Server Compact Database. Pulsa Cancel.
  7. Ahora aparecerá el cuadro de diálogo Connect to Server con la ruta a la base de datos que acabas de crear. Pulsa el botón Connect para conectarte a ella.
  8. Arrastra y suelta el archivo .sqlce que creaste en la sección anterior.
  9. Pulsa el botón Execute.

Ya tienes creada la base de datos EPSG en formato Microsoft SQL Server Compact.

El instalador no solo copia esta base de datos en %ProgramData%\Digi3D 2011\EPSG, sino que además almacena en el registro la ubicación de esta base de datos.

En función de si la versión de Digi3D 2011 es de 32 o de 64 bits la ubicación de la ruta a la base de datos se almacenará en una u otra entradas del registo.
A continuación tienes una tabla que indica dónde se almacena en el registro la cadena de conexión a la base de datos geográfica.

Digi3D 2011 edición de 32 bits Digi3D 2011 edición de 64 bits
HKEY_LOCAL_MACHINE\SOFTWARE\Digi21\Digi3D2011\App\Configuration\EPSGConnectionString HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Digi21\Digi3D2011\App\Configuration\EPSGConnectionString

Imprimir la configuración de teclado

Comentarios desactivados septiembre 21st, 2011

Como ya sabes, el formato del archivo de teclas Teclas.mnu ha cambiado en la versión 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 http://blogdigi3d.digi21.net/?p=192

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

Comentarios desactivados junio 29th, 2011

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

Comentarios desactivados junio 29th, 2011

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

Comentarios desactivados junio 28th, 2011

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

Secuencias infinitas con Yield return y creación de una única secuencia de entidades de todos los archivos de referencia

Comentarios desactivados junio 28th, 2011

Aquí el vídeo relacionado con este post

En ocasiones nos interesará crear un enumerador que enumere las entidades de todos los archivos de referencia como un todo, es decir, como si todos los archivos de referencia fueran un único archivo, por ejemplo cuando realicemos un control de bordes: nos interesará analizar la continuidad de las entidades del archivo de dibujo activo con respecto a todos los archivos de dibujo de referencia cargados.

Para ello, vamos a aprovecharnos de una de las maravillas de C# que es la palabra clave yield. Esta palabra clave básicamente nos permite devolver un valor al llamador, pero almacenando el estado en el que hemos devuelvo el control para continuar por el mismo sitio y con los mismos valores si se vuelve a llamar al método que utiliza el yield return.

Por ejemplo, (te voy a explicar el ejemplo más común, pero es que viene de perlas) si queremos hacer un método que devuelva una secuencia de 10 números aleatorios, podríamos implementarlo de la siguiente manera:

       public IEnumerable<int> SecuenciaNúmerosAleatorios()
        {
            Random r = new Random();

            int[] array = new int[10];
            for (int i = 0; i < 10; i++)
                array[i] = r.Next();

            return array;
        }

Como un array de int es siempre un IEnumerable<int> pues tan solo tenemos que crear un array de 10 posiciones, rellenarlo y devolverlo, y estaremos devolviendo una secuencia de 10 números aleatorios. Perfecto.

Ahora, ¿que pasa si te digo que quiero que esa secuencia sea infinita?

No podemos hacer un array infinito porque nuestro ordenador no tiene una cantidad infinita de memoria.

Aquí es donde viene a ayudarnos la palabra clave yield. Mira cómo podríamos hacer ese método como un método que devuelve un número infinito de valores:

        public IEnumerable<int> SecuenciaNúmerosAleatorios()
        {
            Random r = new Random();

            while(true)
                yield return r.Next();
        }

¿no te parece precioso?

Yield lo que hace es implementar una secuencia perezosa, únicamente se generará un nuevo número aleatorio en el momento en el que ordenemos al iterador que se mueva a la siguiente posición, de modo que no hay consumo de memoria ni tiempo de espera para que se rellene el ¿array? ¿que array? de valores.

Bien, pues ahora que conoces la palabra clave yield, vamos a crear un método de extensión que se ejecute sobre una secuenciua de ReadOnlyDrawingFile y que nos devuelva una secuencia única de entidades que devuelva una a una todas las entidades de todos los archivos de referencia cargados.

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<Entity> EnumeraEntidades(this IEnumerable<ReadOnlyDrawingFile> archivos)
        {
            foreach (var archivo in archivos)
                foreach (var entidad in archivo)
                    yield return entidad;
        }

    }
}

No se a tí, pero a mí me encanta C#, por cosas como esta.

A continuación vamos a hacer una orden que nos muestre en la ventana de resultados la suma de entidades de cada tipo (líneas, puntos, …) de todos los archivos de referencia cargados utilizando el método de extensión que acabamos de desarrollar:

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().
                    CuantasEntidadesDeTipo<ReadOnlyLine>());

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

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

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

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

    }
}

Archivos de referencia

Comentarios desactivados junio 27th, 2011

Aquí el vídeo relacionado con este post

Digi3D 2011 expone las entidades de los archivos de referencia (aquellos archivos que no son el archivo de dibujo activo) a través de la propiedad DigiNG.ReferenceFiles que devuelve un array de tipo ReadOnlyDrawingFile.

El array devuelto tendrá tantos valores como archivos de referencia cargados, y tal y como su nombre indica, no podemos realizar ninguna modificación sobre estos archivos, pues nos devuelve enumeradores de solo lectura.

En el siguiente ejemplo, vamos a desarrollar una orden que nos muestra en la ventana de resultados el número de entidades de cada tipo que tiene cada uno de los archivos de referencia cargados.

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

namespace Pruebas
{
    public static class UtilidadesSecuenciaEntity
    {
        public static int CuantasEntidadesDeTipo<T>(this IEnumerable<Entity> entidades)
        {
            var secuenciaDeT = from entidad in entidades
                               where entidad is T
                               select entidad;

            return secuenciaDeT.Count();
        }
    }

    [Command(Name="cuenta_entidades_archivos_referencia")]
    public class CuentaEntidadesDeArchivosReferencia : Command
    {
        public CuentaEntidadesDeArchivosReferencia()
        {
            this.Initialize +=
                new EventHandler(CuentaEntidadesDeArchivosReferencia_Initialize);
        }

        void CuentaEntidadesDeArchivosReferencia_Initialize(object sender, EventArgs e)
        {
            try
            {
                var archivosReferencia = DigiNG.ReferenceFiles;

                if (archivosReferencia.Length == 0)
                {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "cuenta_entidades_archivos_referencia",
                        "No hay ningún archivo de referencia cargado",
                        2,
                        BallonIcon.Error);
                    return;
                }

                foreach (var archivo in DigiNG.ReferenceFiles)
                {
                    Digi3D.OutputWindow.WriteLine(archivo.Path);

                    Digi3D.OutputWindow.WriteLine(
                        "{0} líneas",
                        archivo.CuantasEntidadesDeTipo<ReadOnlyLine>());

                    Digi3D.OutputWindow.WriteLine(
                        "{0} puntos",
                        archivo.CuantasEntidadesDeTipo<ReadOnlyPoint>());

                    Digi3D.OutputWindow.WriteLine(
                        "{0} textos",
                        archivo.CuantasEntidadesDeTipo<ReadOnlyText>());

                    Digi3D.OutputWindow.WriteLine(
                        "{0} polígonos",
                        archivo.CuantasEntidadesDeTipo<ReadOnlyPolygon>());

                    Digi3D.OutputWindow.WriteLine(
                        "{0} complejos",
                        archivo.CuantasEntidadesDeTipo<ReadOnlyComplex>());
                }
            }
            finally
            {
                Dispose();
            }
        }
    }
}

Detectando las intersecciones de una determinada línea

Comentarios desactivados junio 27th, 2011

En el post anterior nos centramos en los métodos de extensión implementados en el tipo Digi21.DigiNG.Topology.IntersectionDetector que extendían secuencias de líneas.

En este post vamos a centrarnos en las sobrecargas que actúan únicamente sobre una determinada línea.

Aquí tienes el vídeo relacionado con este bloque de código

Si queremos averiguar únicamente las intersecciones que tiene una línea con el resto de líneas del modelo, podemos utilizar cualquiera de los métodos del post anterior y luego centrarnos únicamente en las intersecciones entre las que esté involucrada la línea que nos interesa.

Veámoslo con un ejemplo: Vamos a hacer una orden que solicita al usuario que se seleccione una línea y luego añadirá tantas tareas como intersecciones tenga esa línea con el resto de líneas del archivo de dibujo.

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

namespace Pruebas
{
    [CommandInMenu(
        "Mostrar las intersecciones de la línea seleccionada",
        MenuItemGroup.GeometricAnalysisGroup1)]
    [Command(Name="intersecciones_de_línea")]
    public class InterseccionesDeLínea : Command
    {
        public InterseccionesDeLínea()
        {
            this.Initialize += new EventHandler(InterseccionesDeLínea_SetFocus);
            this.SetFocus += new EventHandler(InterseccionesDeLínea_SetFocus);
            this.DataUp += new EventHandler<Digi21.Math.Point3DEventArgs>(InterseccionesDeLínea_DataUp);
            this.EntitySelected += new EventHandler<EntitySelectedEventArgs>(InterseccionesDeLínea_EntitySelected);
        }

        void InterseccionesDeLínea_SetFocus(object sender, EventArgs e)
        {
            Digi3D.StatusBar.Text = "Selecciona la línea para mostrar sus intersecciones";
        }

        void InterseccionesDeLínea_DataUp(object sender, Digi21.Math.Point3DEventArgs e)
        {
            DigiNG.SelectEntity(
                e.Coordinates,
                entidad => entidad is ReadOnlyLine);
        }

        void InterseccionesDeLínea_EntitySelected(object sender, EntitySelectedEventArgs e)
        {
            ReadOnlyLine líneaSeleccionada = (ReadOnlyLine)e.Entity;

            var todasLasIntersecciones = DigiNG.DrawingFile.SoloLíneas().DetectIntersections();
            foreach (var intersección in todasLasIntersecciones)
            {
                foreach (var entidadImplicadaEnLaIntersección in intersección)
                {
                    if (líneaSeleccionada == entidadImplicadaEnLaIntersección.Line)
                    {
                        Digi3D.Tasks.Add(new TaskGotoPoint(
                            (Point3D)intersección.Key,
                            "Intersección",
                            TaskSeverity.Error,
                            DigiNG.DrawingFile.Path,
                            "intersecciones_de_línea"));
                        DigiNG.RenderScene();
                        break;
                    }
                }
            }

            Dispose();
        }
    }
}

El problema que tiene este sistema es que estamos analizando las intersecciones existentes en todo el archivo de dibujo para luego centrarnos únicamente en un subconjunto en teoría muy pequeño de todas estas intersecciones de modo que estamos desaprovechando recursos (tiempo y memoria).

Afortunadamente, el tipo Digi21.DigiNG.Topology.IntersectionDetector expone métodos de extensión que afectan a una única línea. Estos métodos de extensión son idénticos a aquellos que se aplican a una secuencia de líneas, pero centrándose únicamente en las intersecciones de una determinada línea, de modo que es mucho más rápido, consume menos memoria y además ya no tenemos que buscar los nodos a los que llega nuestra línea, pues todos ellos cumplirán esta función.

La sobrecarga más sencilla es aquella que recibe como único parámetro el conjunto de líneas con las que queremos comprobar sus intersecciones.

Veamos entonces como queda nuestra orden optimizada con el método de extensión aplicado a líneas:

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

namespace Pruebas
{
    [CommandInMenu(
        "Mostrar las intersecciones de la línea seleccionada",
        MenuItemGroup.GeometricAnalysisGroup1)]
    [Command(Name="intersecciones_de_línea")]
    public class InterseccionesDeLínea : Command
    {
        public InterseccionesDeLínea()
        {
            this.Initialize += new EventHandler(InterseccionesDeLínea_SetFocus);
            this.SetFocus += new EventHandler(InterseccionesDeLínea_SetFocus);
            this.DataUp += new EventHandler<Digi21.Math.Point3DEventArgs>(InterseccionesDeLínea_DataUp);
            this.EntitySelected += new EventHandler<EntitySelectedEventArgs>(InterseccionesDeLínea_EntitySelected);
        }

        void InterseccionesDeLínea_SetFocus(object sender, EventArgs e)
        {
            Digi3D.StatusBar.Text = "Selecciona la línea para mostrar sus intersecciones";
        }

        void InterseccionesDeLínea_DataUp(object sender, Digi21.Math.Point3DEventArgs e)
        {
            DigiNG.SelectEntity(
                e.Coordinates,
                entidad => entidad is ReadOnlyLine);
        }

        void InterseccionesDeLínea_EntitySelected(object sender, EntitySelectedEventArgs e)
        {
            ReadOnlyLine líneaSeleccionada = (ReadOnlyLine)e.Entity;

            var todasLasIntersecciones =
                líneaSeleccionada.DetectIntersections(DigiNG.DrawingFile.SoloLíneas());

            foreach (var intersección in todasLasIntersecciones)
            {
                foreach (var entidadImplicadaEnLaIntersección in intersección)
                {
                    if (líneaSeleccionada == entidadImplicadaEnLaIntersección.Line)
                    {
                        Digi3D.Tasks.Add(new TaskGotoPoint(
                            (Point3D)intersección.Key,
                            "Intersección",
                            TaskSeverity.Error,
                            DigiNG.DrawingFile.Path,
                            "intersecciones_de_línea"));
                        DigiNG.RenderScene();
                        break;
                    }
                }
            }

            Dispose();
        }
    }
}

Y como este método es tan rápido, podemos permitirnos el lujo de ejecutarlo en tiempo real.

Vamos a desarrollar una orden que se ejecutará cada vez que el usuario digitaliza una línea. Si esta línea es una curva de nivel, se comprobará mediante este método si esta ha interseccionado con otra curva de nivel, en cuyo caso se mostrará al usuario una tarea, un globo y un sonido de error, con “sorpresa final”, de modo que en el mismo instante en el que el usuario digitaliza la línea, el programa ya le informa de que ha cometido un error.

Si te fijas en el código, esta orden no hace una llamada al método Dispose, por lo tanto, nunca se auto destruye. Al ejecutarla se queda residente, a la escucha, esperando que se digitalice una línea. En nomenclatura UNIX esta orden sería un demonio, en nomenclatura Windows sería una especia de proceso.
La única manera de destruir la orden es al finalizar Digi3D, el propio CLR de Windows se encargará de destruirla.

El truco para convertir esta orden en un proceso consiste en la llamada al método DigiNG.Commands.Pop(), que elimina la orden de la pila de órdenes de DigiNG, de modo que aunque el usuario pulse la tecla Escape, no puede destruir la orden, pues la tecla Escape destruye las órdenes que están en la pila de órdenes.

Y ¿cuándo entra en acción la orden?

Cada vez que DigiNG almacena una entidad en el archivo de dibujo, lanza el evento DigiNG.EntityAddes, evento al cual se puede conectar cualquier orden.

Veamos 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;
using Digi21.Digi3D;
using Digi21.DigiNG.Entities;
using UtilidadesDigi;
using Digi21.DigiNG.Topology;
using Digi21.Math;

namespace Acme
{
    [Command(Name="servicio_curvas_nivel")]
    public class ServicioCurvasNivel : Command
    {
        public ServicioCurvasNivel()
        {
            this.Initialize += new EventHandler(ServicioCurvasNivel_Initialize);
            DigiNG.EntityAdded += new EventHandler<EntityAddedEventArgs>(DigiNG_EntityAdded);
        }

        void DigiNG_EntityAdded(object sender, EntityAddedEventArgs e)
        {
            if (e.Entity is ReadOnlyLine &&
                e.Entity.TieneAlgúnCódigo(this.Args))
                ControlCalidadCurvasNivel(e.Entity as ReadOnlyLine);
        }

        private void ControlCalidadCurvasNivel(ReadOnlyLine línea)
        {
            var curvasDeNivelExistentes = from entidad in DigiNG.DrawingFile
                                          where entidad != línea
                                          where entidad.TieneAlgúnCódigo(this.Args)
                                          select entidad as ReadOnlyLine;

            var intersecciones = línea.DetectIntersections(curvasDeNivelExistentes);

            List<ITask> tareasAAñadir = new List<ITask>();
            foreach (var intersección in intersecciones)
            {
                tareasAAñadir.Add(new TaskEntityGotoPoint(
                    (Point3D)intersección.Key,
                    línea,
                    2,
                    "Curva de nivel que cruza con otras curvas de nivel",
                    TaskSeverity.Error,
                    DigiNG.DrawingFile.Path,
                    "servicio_curvas_nivel"));
            }

            foreach (var tarea in tareasAAñadir)
                Digi3D.Tasks.Add(tarea);

            if (tareasAAñadir.Count != 0)
            {
                Digi3D.Music(MusicType.Error);
                DigiNG.RenderScene();
            }
        }

        void ServicioCurvasNivel_Initialize(object sender, EventArgs e)
        {
            if (this.Args.Length == 0)
            {
                Digi3D.Music(MusicType.Error);
                Digi3D.ShowBallon(
                    "servicio_curvas_nivel",
                    "No se han indicado los códigos de las curvas de nivel",
                    2,
                    BallonIcon.Error);
                Dispose();
                return;
            }

            DigiNG.Commands.Pop();
        }
    }
}

Y ahora la “sorpresa final”, ¿Por qué no hacer que en vez de sonar el sonido de error de Digi, una voz sintética hable indicándole al usuario en perfecto castellano que la curva de nivel que acaba de digitalizar se cruza X veces con otra curva de nivel?

Si hacemos que nuestro proyecto referencie el ensamblado System.Speech.dll, en el espacio de nombres System.Speech.Systhesis tenemos el tipo SpeechSynthesizer.

Lo único que tenemos que hacer es crear una instancia de este tipo y llamar a su método SpeakAsync pasando como parámetro la cadena que queremos que sea sintetizada por el motor de texto a voz.

Los sistemas operativos Windows vienen de serie con un motor de texto a voz para frases en inglés. Si quieres que el sintetizador funcione con frases en castellano tendrás que comprarte una voz en castellano. Envíame un correo si quieres que te diga dónde comprar voces en castellano.

A continuación la orden pero esta vez con voz sintética.

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

namespace Acme
{
    [Command(Name="servicio_curvas_nivel")]
    public class ServicioCurvasNivel : Command
    {
        public ServicioCurvasNivel()
        {
            this.Initialize += new EventHandler(ServicioCurvasNivel_Initialize);
            DigiNG.EntityAdded += new EventHandler<EntityAddedEventArgs>(DigiNG_EntityAdded);
        }

        void DigiNG_EntityAdded(object sender, EntityAddedEventArgs e)
        {
            if (e.Entity is ReadOnlyLine &&
                e.Entity.TieneAlgúnCódigo(this.Args))
                ControlCalidadCurvasNivel(e.Entity as ReadOnlyLine);
        }

        private void ControlCalidadCurvasNivel(ReadOnlyLine línea)
        {
            var curvasDeNivelExistentes = from entidad in DigiNG.DrawingFile
                                          where entidad != línea
                                          where entidad.TieneAlgúnCódigo(this.Args)
                                          select entidad as ReadOnlyLine;

            var intersecciones = línea.DetectIntersections(curvasDeNivelExistentes);

            List<ITask> tareasAAñadir = new List<ITask>();
            foreach (var intersección in intersecciones)
            {
                tareasAAñadir.Add(new TaskEntityGotoPoint(
                    (Point3D)intersección.Key,
                    línea,
                    2,
                    "Curva de nivel que cruza con otras curvas de nivel",
                    TaskSeverity.Error,
                    DigiNG.DrawingFile.Path,
                    "servicio_curvas_nivel"));
            }

            foreach (var tarea in tareasAAñadir)
                Digi3D.Tasks.Add(tarea);

            if (tareasAAñadir.Count != 0)
            {
                string mensaje = string.Format(
                    "La curva de nivel que acaba de registrar se cruza {0} veces con otra curva de nivel",
                    tareasAAñadir.Count);

                SpeechSynthesizer motorVoz = new SpeechSynthesizer();
                motorVoz.SpeakAsync(mensaje);
                DigiNG.RenderScene();
            }
        }

        void ServicioCurvasNivel_Initialize(object sender, EventArgs e)
        {
            if (this.Args.Length == 0)
            {
                Digi3D.Music(MusicType.Error);
                Digi3D.ShowBallon(
                    "servicio_curvas_nivel",
                    "No se han indicado los códigos de las curvas de nivel",
                    2,
                    BallonIcon.Error);
                Dispose();
                return;
            }

            DigiNG.Commands.Pop();
        }
    }
}

Aquí tienes el vídeo de esta sección de código

y para terminar, vamos a hacer que la orden no detecte como erróneas intersecciones en las que una curva de nivel continúa a otra curva de nivel (es decir, si el nodo de intersección coincide con el comienzo o final de ambas curvar) y además vamos a solucionar un problema gramatical para evitar que la voz sintética diga “… se ha cruzado una veces con…”

Para comprobar si una intersección se ha realizado al comienzo o al final de una determinada línea, vamos a crear un método de extensión para el tipo SegmentPointer que nos devolverá verdadero si se cumple esta condición, facilitando mucho la lectura del código principal

Aquí tienes el código definitivo:

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

namespace Acme
{
    public static class UtilidadesSegmentPointer
    {
        public static bool IntersecciónAlComienzoOFinalDeLínea(this SegmentPointer entidad)
        {
            if (entidad.FirstVertex == entidad.SecondVertex &&
                (entidad.FirstVertex == 0 ||
                entidad.FirstVertex == entidad.Line.Points.Count - 1))
                return true;

            return false;
        }
    }

    [Command(Name="servicio_curvas_nivel")]
    public class ServicioCurvasNivel : Command
    {
        public ServicioCurvasNivel()
        {
            this.Initialize += new EventHandler(ServicioCurvasNivel_Initialize);
            DigiNG.EntityAdded += new EventHandler<EntityAddedEventArgs>(DigiNG_EntityAdded);
        }

        void DigiNG_EntityAdded(object sender, EntityAddedEventArgs e)
        {
            if (e.Entity is ReadOnlyLine &&
                e.Entity.TieneAlgúnCódigo(this.Args))
                ControlCalidadCurvasNivel(e.Entity as ReadOnlyLine);
        }

        private void ControlCalidadCurvasNivel(ReadOnlyLine línea)
        {
            var curvasDeNivelExistentes = from entidad in DigiNG.DrawingFile
                                          where entidad != línea
                                          where entidad.TieneAlgúnCódigo(this.Args)
                                          select entidad as ReadOnlyLine;

            var intersecciones = línea.DetectIntersections(curvasDeNivelExistentes).ToArray();

            if (intersecciones.Length == 1)
            {
                if (intersecciones[0].Count() == 2)
                {
                    SegmentPointer líneaA = intersecciones[0].ElementAt(0);
                    SegmentPointer líneaB = intersecciones[0].ElementAt(1);

                    if (líneaA.IntersecciónAlComienzoOFinalDeLínea() &&
                        líneaB.IntersecciónAlComienzoOFinalDeLínea())
                        return;
                }
            }

            List<ITask> tareasAAñadir = new List<ITask>();
            foreach (var intersección in intersecciones)
            {
                tareasAAñadir.Add(new TaskEntityGotoPoint(
                    (Point3D)intersección.Key,
                    línea,
                    2,
                    "Curva de nivel que cruza con otras curvas de nivel",
                    TaskSeverity.Error,
                    DigiNG.DrawingFile.Path,
                    "servicio_curvas_nivel"));
            }

            foreach (var tarea in tareasAAñadir)
                Digi3D.Tasks.Add(tarea);

            if (tareasAAñadir.Count != 0)
            {
                string mensaje;

                if( tareasAAñadir.Count == 1 )
                    mensaje = "La curva de nivel que acaba de registrar se cruza una vez con otra curva de nivel";
                else
                    mensaje = string.Format(
                        "La curva de nivel que acaba de registrar se cruza {0} veces con otra curva de nivel",
                        tareasAAñadir.Count);

                SpeechSynthesizer motorVoz = new SpeechSynthesizer();
                motorVoz.SpeakAsync(mensaje);
                DigiNG.RenderScene();
            }
        }

        void ServicioCurvasNivel_Initialize(object sender, EventArgs e)
        {
            if (this.Args.Length == 0)
            {
                Digi3D.Music(MusicType.Error);
                Digi3D.ShowBallon(
                    "servicio_curvas_nivel",
                    "No se han indicado los códigos de las curvas de nivel",
                    2,
                    BallonIcon.Error);
                Dispose();
                return;
            }

            DigiNG.Commands.Pop();
        }
    }
}