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

Detectando intersecciones entre múltiples líneas

Comentarios desactivados junio 27th, 2011

Podemos detectar intersecciones entre líneas mediante el tipo Digi21.DigiNG.Topology.IntersectionDetector, que es una clase abstracta que define métodos de extensión sobre el tipo Digi21.DigiNG.ReadOnlyLine o sobre una secuencia de líneas IEnumerable<Digi21.DigiNG.ReadOnlyLine>.

Estos métodos de extensión disponen de varias sobrecargas que nos van a permitir filtrar el tipo de intersecciones que queremos detectar, de manera que podemos llegar a realizar consultas con un altísimo nivel de detalle, por ejemplo, podríamos realizar búsquedas tan extrañas como intersecciones entre dos líneas, la primera forzosamente con el código A y un número par de vértices y la segunda con código B y con un número par de vértices.
Las posibilidades son infinitas.

Todas las sobrecargas devuelven una secuencia objetos de Digi21.DigiNG.Entities.SegmentPointer como por ejemplo el siguiente recorte en el que te muestro la primera sobrecarga:

        public static IEnumerable<IGrouping<Point2D, SegmentPointer>> DetectIntersections(
            this IEnumerable<ReadOnlyLine> entities);

Y por su parte, el tipo SegmentPointer tiene el siguiente aspecto:

        public class SegmentPointer
        {
            public SegmentPointer(
                ReadOnlyLine line,
                int firstVertex,
                int secondVertex);

            public int FirstVertex { get; }
            public ReadOnlyLine Line { get; }
            public int SecondVertex { get; }
        }

que si te fijas tiene tres propiedades: Una que devuelve la línea implicada en la intersección, y otras dos que nos indican entre que vértices de esa línea se ha detectado la intersección. Si el valor almacenado en estas dos últimas propiedades coincide, significa que la intersección se detectó en ese vértice, y si no coinciden, las coordenadas de la intersección estarán en el segmento que une los dos vértices.

Utilizar estos métodos es trivial, tan solo tenemos que añadir una referencia al ensamblado Digi21.DigiNG.Topology, hacer visible en nuestro código el espacio de nombres Digi21.DigiNG.Topology y llamar al método de extensión que nos interese o sobre una línea o sobre una secuencia de líneas.

En este post nos vamos a centrar únicamente en los métodos de extensión que se aplican a secuencias de entidades. En el próximo post nos centraremos en las sobrecargas que se aplican a una línea en concreto.

Aquí el vídeo relacionado con este código

El siguiente código detecta las intersecciones de todas las entidades del archivo de dibujo activo, añadiendo una tarea en la ventana de tareas de la aplicación por cada intersección, de modo que cuando el usuario hace doble clic sobre la tarea se desplaza la ventana de dibujo a las coordenadas de la intersección:

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

namespace Pruebas
{
    [Command(Name = "detectar_intersecciones")]
    public class DetectarIntersecciones : Command
    {
        public DetectarIntersecciones()
        {
            this.Initialize += new EventHandler(DetectarIntersecciones_Initialize);
        }

        void DetectarIntersecciones_Initialize(object sender, EventArgs e)
        {
            var líneas = from entidad in DigiNG.DrawingFile
                         where entidad is ReadOnlyLine
                         select entidad as ReadOnlyLine;

            foreach (var intersección in líneas.DetectIntersections())
            {
                Digi3D.Tasks.Add(new TaskGotoPoint(
                    (Point3D)intersección.Key,
                    "Detectada una intersección",
                    TaskSeverity.Error,
                    DigiNG.DrawingFile.Path,
                    "detectar_intersecciones"));
            }

            Dispose();
        }
    }
}

Pero podemos hacer las cosas mejor, ya que el método lo que nos ha devuelto es una secuencia de grupos (cuya clave principal es la coordenada de la intersección), así que podríamos hacer que en la barra de tareas apareciera únicamente una tarea por cada punto en el que se ha detectado una intersección y que dicha tarea tenga como nodos hijos una subtarea por cada entidad. Al hacer doble clic sobre la tarea se desplazará la cámara a las coordenadas de la intersección, y al hacer doble clic sobre cada subtarea, se animará durante dos segundos la línea en cuestión:

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

namespace Pruebas
{
    [Command(Name = "detectar_intersecciones")]
    public class DetectarIntersecciones : Command
    {
        public DetectarIntersecciones()
        {
            this.Initialize += new EventHandler(DetectarIntersecciones_Initialize);
        }

        void DetectarIntersecciones_Initialize(object sender, EventArgs e)
        {
            var líneas = from entidad in DigiNG.DrawingFile
                         where entidad is ReadOnlyLine
                         select entidad as ReadOnlyLine;

            foreach (var intersección in líneas.DetectIntersections())
            {
                List<ITask> subtareas = new List<ITask>();
                foreach (var entidad in intersección)
                {
                    string títuloSubtarea = string.Format(
                        "Intersección entre los vértices {0} y {1}",
                        entidad.FirstVertex,
                        entidad.SecondVertex);

                    subtareas.Add(new TaskAnimateEntity(
                        entidad.Line,
                        2,
                        títuloSubtarea,
                        TaskSeverity.Error));
                }

                string títuloTarea = string.Format(
                    "Detectada una intersección en las coordenadas {0}",
                    intersección.Key);

                Digi3D.Tasks.Add(new TaskGotoPoint(
                    (Point3D)intersección.Key,
                    títuloTarea,
                    TaskSeverity.Error,
                    DigiNG.DrawingFile.Path,
                    "detectar_intersecciones",
                    subtareas.ToArray()));
            }

            Dispose();
        }
    }
}

Especializaciones

Los métodos de extensión DetectIntersections disponen de varias sobrecargas que nos van a permitir realizar búsquedas especializadas.

Aquí el vídeo relacionado con este código

La primera de ellas nos permite que pasemos un predicado de tipo Func<Point2D, bool> para indicar si queremos que se añada una determinada intersección al resultado.

Para ello vamos a modificar nuestra orden para hacer que únicamente detecte intersecciones dentro de un rango de coordenadas máximas y mínimas pasadas por parámetro.
Si el usuario no pasa parámetros a la orden, se mostrarán todas las intersecciones, y si pasa cuatro parámetros (xmin, ymin, xmax, ymax) se mostrarán únicamente las intersecciones dentro de este rango de coordenadas.

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

namespace Pruebas
{
    [Command(Name = "detectar_intersecciones")]
    public class DetectarIntersecciones : Command
    {
        private Window2D maxmin = Window2D.WholeWorld;

        public DetectarIntersecciones()
        {
            this.Initialize += new EventHandler(DetectarIntersecciones_Initialize);
        }

        void DetectarIntersecciones_Initialize(object sender, EventArgs e)
        {
            if (this.Args.Length == 4)
            {
                maxmin.Xmin = double.Parse(this.Args[0]);
                maxmin.Ymin = double.Parse(this.Args[1]);
                maxmin.Xmax = double.Parse(this.Args[2]);
                maxmin.Ymax = double.Parse(this.Args[3]);
            }

            var líneas = from entidad in DigiNG.DrawingFile
                         where entidad is ReadOnlyLine
                         select entidad as ReadOnlyLine;

            var intersecciones = líneas.DetectIntersections(
                (Point2D coordenada) => maxmin.Contains(coordenada));

            foreach (var intersección in intersecciones)
            {
                List<ITask> subtareas = new List<ITask>();
                foreach (var entidad in intersección)
                {
                    string títuloSubtarea = string.Format(
                        "Intersección entre los vértices {0} y {1}",
                        entidad.FirstVertex,
                        entidad.SecondVertex);

                    subtareas.Add(new TaskAnimateEntity(
                        entidad.Line,
                        2,
                        títuloSubtarea,
                        TaskSeverity.Error));
                }

                string títuloTarea = string.Format(
                    "Detectada una intersección en las coordenadas {0}",
                    intersección.Key);

                Digi3D.Tasks.Add(new TaskGotoPoint(
                    (Point3D)intersección.Key,
                    títuloTarea,
                    TaskSeverity.Error,
                    DigiNG.DrawingFile.Path,
                    "detectar_intersecciones",
                    subtareas.ToArray()));
            }

            Dispose();
        }
    }
}

Pero hay más sobrecargas: La siguiente nos permite indicar si queremos o no incluir una línea (mediante un predicado de tipo Func<Entity, bool>) en el proceso.

Podemos utilizar esta sobrecarga para filtrar por códigos por ejemplo.

Aquí el vídeo relacionado con este código
Veámoslo creando una nueva orden que detecta intersecciones entre las líneas con los códigos pasados por parámetro.

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 UtilidadesDigi;
using Digi21.Digi3D;
using Digi21.Math;

namespace Acme
{
    [Command(Name = "detectar_intersecciones_cod")]
    public class DetectarInterseccionesCod : Command
    {
        public DetectarInterseccionesCod()
        {
            this.Initialize += new EventHandler(DetectarInterseccionesCod_Initialize);
        }

        //bool MeInteresaEstaLínea(ReadOnlyLine línea)
        //{
        //    return línea.TieneAlgúnCódigo(this.Args);
        //}

        void DetectarInterseccionesCod_Initialize(object sender, EventArgs e)
        {
            try
            {
                if (this.Args.Length == 0)
                {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "detectar_intersecciones_cod",
                        "No has indicado los códigos de las entidades para la detección de intersecciones",
                        2,
                        BallonIcon.Error);
                    return;
                }

                var intersecciones = DigiNG.DrawingFile.SoloLíneas().DetectIntersections(
                    línea => línea.TieneAlgúnCódigo(this.Args));

                foreach (var intersección in intersecciones)
                {
                    List<ITask> subtareas = new List<ITask>();
                    foreach (var entidad in intersección)
                    {
                        string títuloSubtarea = string.Format(
                            "Intersección entre los vértices {0} y {1}",
                            entidad.FirstVertex,
                            entidad.SecondVertex);

                        subtareas.Add(new TaskAnimateEntity(
                            entidad.Line,
                            2,
                            títuloSubtarea,
                            TaskSeverity.Error));
                    }

                    Digi3D.Tasks.Add(new TaskGotoPoint(
                        (Point3D)intersección.Key,
                        "Detectada una intersección",
                        TaskSeverity.Error,
                        DigiNG.DrawingFile.Path,
                        "detectar_intersecciones",
                        subtareas.ToArray()));
                }
                DigiNG.RenderScene();

            }
            finally
            {
                Dispose();
            }
        }
    }
}

La siguiente sobrecarga nos permite especificar si permitimos analizar las intersecciones entre dos líneas. Cada vez que el algoritmo detecta que dos líneas son candidatas a tener intersecciones, el algoritmo llama a nuestro predicado para que le indiquemos si le damos permiso para analizar las intersecciones entre esas dos líneas.

Supón que quieres que únicamente se analicen las intersecciones entre línas de tipo A con líneas de tipo B, pero nunca intersecciones de líneas de tipo A con líneas de tipo A y de tipo B con tipo B

Aquí el vídeo relacionado con este código

Vamos a crear una orden nueva que esperae dos códigos y analice únicamente las intersecciones entre líneas que tengan una el primer código y otra el segundo o viceversa, pero nunca intersecciones de dos líneas con el mismo código.

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 UtilidadesDigi;
using Digi21.Digi3D;
using Digi21.Math;

namespace Acme
{
    [Command(Name = "detectar_intersecciones_códigos_distintos")]
    public class DetectarInterseccionesCódigosDistintos : Command
    {
        public DetectarInterseccionesCódigosDistintos()
        {
            this.Initialize += new EventHandler(DetectarInterseccionesCódigosDistintos_Initialize);
        }

        //private bool PermitoCalcularInterseccionesEntreEstasDosLíneas(ReadOnlyLine líneaA, ReadOnlyLine líneaB)
        //{
        //    return líneaA.TieneElCódigo(this.Args[0]) && líneaB.TieneElCódigo(this.Args[1]) ||
        //        líneaA.TieneElCódigo(this.Args[1]) && líneaB.TieneElCódigo(this.Args[0]);
        //}

        void DetectarInterseccionesCódigosDistintos_Initialize(object sender, EventArgs e)
        {
            try
            {
                if (this.Args.Length != 2)
                {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "detectar_intersecciones_códigos_distintos",
                        "No has indicado los dos códigos con los que trabajar",
                        2,
                        BallonIcon.Error);
                    return;
                }

                var intersecciones = DigiNG.DrawingFile.SoloLíneas().DetectIntersections(
                    (líneaA, líneaB) =>
                        líneaA.TieneElCódigo(this.Args[0]) && líneaB.TieneElCódigo(this.Args[1]) ||
                        líneaA.TieneElCódigo(this.Args[1]) && líneaB.TieneElCódigo(this.Args[0]));

                foreach (var intersección in intersecciones)
                {
                    List<ITask> subtareas = new List<ITask>();
                    foreach (var entidad in intersección)
                    {
                        string títuloSubtarea = string.Format(
                            "Intersección entre los vértices {0} y {1}",
                            entidad.FirstVertex,
                            entidad.SecondVertex);

                        subtareas.Add(new TaskAnimateEntity(
                            entidad.Line,
                            2,
                            títuloSubtarea,
                            TaskSeverity.Error));
                    }

                    Digi3D.Tasks.Add(new TaskGotoPoint(
                        (Point3D)intersección.Key,
                        "Detectada una intersección",
                        TaskSeverity.Error,
                        DigiNG.DrawingFile.Path,
                        "detectar_intersecciones",
                        subtareas.ToArray()));
                }
                DigiNG.RenderScene();

            }
            finally
            {
                Dispose();
            }
        }
    }
}

Pero podríamos haber optimizado el proceso anterior, ya que el algoritmo va a llamar a nuestro predicado por todas las entidades que tengan posibilidad de cruzarse, independientemente de sus códigos.

El resto de sobrecargas son combinaciones las sobrecargas anteriores, de modo que existe una que nos permite pasarle dos predicados: uno para indicar si analizar las intersecciones de una línea y otro para indicar si permitimos analizar las intersecciones entre un par de líneas, de modo que la siguiente modificación mejora sustancialmente la velocidad de proceso del algoritmo con idéntico resultado:

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 UtilidadesDigi;
using Digi21.Digi3D;
using Digi21.Math;

namespace Acme
{
    [Command(Name = "detectar_intersecciones_códigos_distintos")]
    public class DetectarInterseccionesCódigosDistintos : Command
    {
        public DetectarInterseccionesCódigosDistintos()
        {
            this.Initialize += new EventHandler(DetectarInterseccionesCódigosDistintos_Initialize);
        }

        //private bool PermitoCalcularInterseccionesEntreEstasDosLíneas(ReadOnlyLine líneaA, ReadOnlyLine líneaB)
        //{
        //    return líneaA.TieneElCódigo(this.Args[0]) && líneaB.TieneElCódigo(this.Args[1]) ||
        //        líneaA.TieneElCódigo(this.Args[1]) && líneaB.TieneElCódigo(this.Args[0]);
        //}

        void DetectarInterseccionesCódigosDistintos_Initialize(object sender, EventArgs e)
        {
            try
            {
                if (this.Args.Length != 2)
                {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "detectar_intersecciones_códigos_distintos",
                        "No has indicado los dos códigos con los que trabajar",
                        2,
                        BallonIcon.Error);
                    return;
                }

                var intersecciones = DigiNG.DrawingFile.SoloLíneas().DetectIntersections(
                    línea => línea.TieneAlgúnCódigo(this.Args),
                    (líneaA, líneaB) =>
                        líneaA.TieneElCódigo(this.Args[0]) && líneaB.TieneElCódigo(this.Args[1]) ||
                        líneaA.TieneElCódigo(this.Args[1]) && líneaB.TieneElCódigo(this.Args[0]));

                foreach (var intersección in intersecciones)
                {
                    List<ITask> subtareas = new List<ITask>();
                    foreach (var entidad in intersección)
                    {
                        string títuloSubtarea = string.Format(
                            "Intersección entre los vértices {0} y {1}",
                            entidad.FirstVertex,
                            entidad.SecondVertex);

                        subtareas.Add(new TaskAnimateEntity(
                            entidad.Line,
                            2,
                            títuloSubtarea,
                            TaskSeverity.Error));
                    }

                    Digi3D.Tasks.Add(new TaskGotoPoint(
                        (Point3D)intersección.Key,
                        "Detectada una intersección",
                        TaskSeverity.Error,
                        DigiNG.DrawingFile.Path,
                        "detectar_intersecciones",
                        subtareas.ToArray()));
                }
                DigiNG.RenderScene();

            }
            finally
            {
                Dispose();
            }
        }
    }
}

Tipos de tareas

Comentarios desactivados junio 24th, 2011

Podemos añadir tareas en el interfaz de usuario de Digi3D 2011 mediante el método Digi21.DigiNG.Tasks.Add, que espera recibir un objeto que implemente el interfaz Digi21.Digi3D.ITask.

Este interfaz tiene el siguiente aspecto:

    public interface ITask
    {
        ITask[] Childs { get; }
        string Description { get; }
        string DrawingFile { get; }
        string Module { get; }
        TaskSeverity Severity { get; }
        string Title { get; }

        void Execute();
    }

Las dos propiedades más importantes son Title que indica el título que se mostrará al usuario en la ventana de tareas, Severity que indica en que subpanel de la barra de tareas mostrar la tarea (Errores, Advertencias o Mensajes).
Si el usuario hace doble clic en una determinada tarea, se ejecutará su método Execute, que obrará en consecuencia.

No es necesario que creemos tipos que implementen el interfaz ITask, pues el framework de Digi3D.NET incluye varios tipos que somos libres de utilizar.

Tarea que muestra un cuadro de mensajes al usuario

Si queremos añadir una tarea de información al usuario que muestre un MessageBox a este cuando haga doble clic sobre la tarea, podemos utilizar el tipo Digi21.Digi3D.TaskMessageBox que implementa los siguientes constructores:

        public TaskMessageBox();
        public TaskMessageBox(
            string title,
            string description,
            TaskSeverity severity,
            string drawingFile,
            string module);
        public TaskMessageBox(
            string title,
            string description,
            TaskSeverity severity,
            string drawingFile,
            string module,
            ITask[] childs);

El primer constructor no admite comentarios, los ostros dos se diferencian únicamente porque el segundo admite que le pasemos un array de subtareas.

  • En el parámetro title debes poner el texto que quieres que aparezca como título de la tarea en el panel de tareas.
  • En el parámetro description debes poner el cuerpo del texto que quieres que aparezca en el MessageBox.
  • En el parámetro severity selecciona el panel en el que quieres que aparezca la tarea en cuestión.
  • En el parámetro drawingFile te recomiendo que pongas siempre el nombre del archivo donde se ha localizado la causa por la cual estás añadiendo una tarea. En la mayoría de los casos pondrás DigiNG.DrawingFile.Path
  • En el parámetro module te recomiendo que pongas el nombre de la orden que está añadiendo la tarea, para que el usuario sepa quién ha añadido esa tarea.

Veámoslo en acción: A continuación te presento una orden que salude al usuario. Puedes ver el vídeo Ventana de tareas para ver cómo crear el proyecto del cual te presento aquí un recorte de código.

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

        void SaludaVentanaTareas_Initialize(object sender, EventArgs e)
        {
            Digi3D.Tasks.Add(new TaskMessageBox(
                "¡Saludos!",
                "Hola usuario de Digi3D",
                TaskSeverity.Message,
                DigiNG.DrawingFile.Path,
                "SaludaVentanaTareas"));
            Dispose();
        }
    }

Si ejecutamos esta orden, aparecerá la siguiente tarea en el panel de tareas:

y si hacemos doble clic sobre la tarea, esta nos mostrará el siguiente MessageBox

Más fácil imposible.

Tarea que anima una entidad durante cierta cantidad de tiempo

El tipo Digi21.DigiNG.TaskAnimateEntity nos va a permitir animar una entidad para que el usuario sepa a qué entidad nos estamos refiriendo en la tarea. Esta animación se realiza por una cantidad de tiempo y además se realiza sin desplazar la cámara.

Este tipo implementa los siguientes constructores:

        public TaskAnimateEntity();
        public TaskAnimateEntity(
            Entity entity,
            int seconds,
            string title,
            TaskSeverity severity);
        public TaskAnimateEntity(
            Entity entity,
            int seconds,
            string title,
            TaskSeverity severity,
            string drawingFile, string
            module);
        public TaskAnimateEntity(
            Entity entity,
            int seconds,
            string title,
            TaskSeverity severity,
            string drawingFile,
            string module,
            ITask[] childs);

y si te fijas la única diferencia con el tipo anterior es que esta vez debemos pasar como parámetro la entidad que queremos animar así como el número de segundos que queremos que dure la animación.

Veámoslo en acción: A continuación te presento una orden a la que le pasamos como parámetro un número de entidad, y esta, si se superan todos los tests añade finalmente una tarea en el subpanel de mensajes de la barra de tareas que al hacer doble clic anima la entidad en cuestión.

    [Command(Name = "anima_entidad_número")]
    public class AnimaEntidadNúmero : Command
    {
        public AnimaEntidadNúmero()
        {
            this.Initialize += new EventHandler(AnimaEntidadNúmero_Initialize);
        }

        void AnimaEntidadNúmero_Initialize(object sender, EventArgs e)
        {
            try
            {
                if (this.Args.Length == 0)
                {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "anima_entidad_número",
                        "No has indicado el número de entidad a seleccionar",
                        2,
                        BallonIcon.Error);

                    return;
                }

                int posición;
                if( !int.TryParse(this.Args[0], out posición)) {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "anima_entidad_número",
                        "El parámetro pasado no es un número",
                        2,
                        BallonIcon.Error);

                    return;
                }

                if( posición < 0 || posición > DigiNG.DrawingFile.Count() ) {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "anima_entidad_número",
                        "No existe ninguna entidad en la posición pasada por parámetros",
                        2,
                        BallonIcon.Error);

                    return;
                }

                string título = string.Format(
                    "Entidad en la posición número {0}",
                    posición);

                Digi3D.Tasks.Add(
                    new TaskAnimateEntity(
                        DigiNG.DrawingFile.ElementAt(posición),
                        2,
                        título,
                        TaskSeverity.Message,
                        DigiNG.DrawingFile.Path,
                        "anima_entidad_número"));
            }
            finally
            {
                Dispose();
            }
        }
    }

Tarea que hace un zoom extendido en una determinada entidad

Existe un tipo muy parecido al anterior cuya finalidad es la de hacer un zoom extendido en una determinada entidad. Este tipo es el tipo Digi21.DigiNG.TaskZoomEntity y tiene los mismos constructores que el tipo anterior.
Si modificamos el código del ejemplo anterior y sustituimos TaskAnimateEntity por TaskZoomEntity podrás comprobar que el programa hace un zoom extendido en la entidad a parte de animarla.
Avanzado: El tipo TaskZoomEntity hereda de TaskAnimateEntity.

Tarea que desplaza la cámara a un punto y además muestra un triángulo al regenerar la pantalla

Disponemos además de una tarea Digi21.DigiNG.TaskGotoPoint que desplaza la cámara a una determinada posición y además dibuja una marca (un triángulo) en las coordenadas donde se ha desplazado la cámara.
Esta tarea permite además configurar el tamaño y color (incluido el factor de opacidad) del triángulo de modo que podríamos crear tareas con distintos colores en función de un determinado criterio como verde un error poco importante y rojo uno grave.

A continuación el constructor con más parámetros de este tipo:

        public TaskGotoPoint(
            Point3D coordinates,
            Color color,
            int size,
            string title,
            TaskSeverity severity,
            string drawingFile,
            string module,
            ITask[] childs);

Como puedes observar, puedes indicar las coordenadas a las que desplazar la cámara cuando el usuario haga doble clic, el color (en el espacio de nombres System.Drawing) con el que representar en la ventana de dibujo el triángulo cuyo centro coincide en las coordenadas del primer parámetro, el tamaño en píxeles del lado del triángulo, el resto de parámetros no es necesario que te los explique.

Veámoslo en acción: A continuación te presento una orden que analiza las líneas almacenadas en el archivo de dibujo con un determinado código y que superen una determinada distancia sin decrementar la coordenada Z unas determinadas unidades.

Supongamos que nuestro pliego de condiciones nos obliga a que las líneas de hidrografía no puedan superar una distancia de 30 metros sin decrementar la coordenada z al menos 30 centímetros. Vamos a implementar una orden que analice el archivo de dibujo en busca de esta situación y añadiremos una tarea que desplazará la cámara a la posición donde se ha descubierto el error y además animará la entidad para que el usuario sepa a que entidad nos estamos refiriendo.

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

namespace Acme
{
    [Command(Name = "detectar_lineas_con_tramos_largos_sin_modificar_z")]
    public class DetectarLíneasConTramosSinModificarZ : Command
    {
        public DetectarLíneasConTramosSinModificarZ()
        {
            this.Initialize += new EventHandler(DetectarLíneasConTramosSinModificarZ_Initialize);
        }

        void DetectarLíneasConTramosSinModificarZ_Initialize(object sender, EventArgs e)
        {
            try
            {
                String código;
                double distancia;
                AsignaParámetros(out código, out distancia);

                var líneasDeInterés = from entidad in DigiNG.DrawingFile
                                      where entidad is ReadOnlyLine
                                      where entidad.TieneElCódigo(código)
                                      let línea = entidad as ReadOnlyLine
                                      where línea.Perimeter > distancia
                                      select línea;

                foreach (var línea in líneasDeInterés)
                {
                    double últimaZ = línea.Points[0].Z;
                    double distanciaRecorridaConÚltimaZ = 0.0;

                    for (int i = 1; i < línea.Points.Count; i++)
                    {
                        if (línea.Points[i].Z == últimaZ)
                        {
                            distanciaRecorridaConÚltimaZ +=
                                Point2D.CalculateModule(línea.Points[i - 1], línea.Points[i]);

                            if (distanciaRecorridaConÚltimaZ > distancia)
                            {
                                Digi3D.Tasks.Add(new TaskGotoPoint(
                                    línea.Points[i - 1],
                                    Color.Aquamarine,
                                    6,
                                    "Línea con un tramo largo sin modificar su coordenada Z",
                                    TaskSeverity.Error,
                                    DigiNG.DrawingFile.Path,
                                    "detectar_lineas_con_tramos_largos_sin_modificar_z"));
                                break;
                            }
                        }
                        else
                        {
                            últimaZ = línea.Points[i].Z;
                            distanciaRecorridaConÚltimaZ = 0.0;
                        }
                    }

                }

            }
            catch (Exception)
            {
            }
            finally
            {
                Dispose();
            }
        }

        private void AsignaParámetros(out string código, out double distancia)
        {
            if (this.Args.Length != 2)
            {
                Digi3D.Music(MusicType.Error);
                Digi3D.ShowBallon(
                    "Detector de líneas con tramos sin modificar Z",
                    "No has indicado los parámetros.\nFormato: [código] [distancia]",
                    2,
                    BallonIcon.Error);
                throw new Exception();
            }

            código = this.Args[0];

            if (!double.TryParse(this.Args[1], out distancia))
            {
                Digi3D.Music(MusicType.Error);
                Digi3D.ShowBallon(
                    "Detector de líneas con tramos sin modificar Z",
                    "El segundo parámetro no indica una distancia válida",
                    2,
                    BallonIcon.Error);
                throw new Exception();
            }
        }
    }
}

Pero lo podemos mejorar más aún, pues las tareas pueden tener a su vez subtareas, y estaría muy bien que esta tarea tuviera como subtarea una del tipo TaskAnimateEntity para que el usuario sepa la línea en la que se ha detectado el error (podría suceder que en esa ubicación se localicen muchas líneas.

A continuación el recorte de código correspondiente para añadir esa funcionalidad:

                             ...

                            if( distanciaRecorridaConÚltimaZ > distancia )
                            {
                                ITask[] subtareas = new[] {
                                    new TaskAnimateEntity(
                                        línea,
                                        2,
                                        "Animar la línea",
                                        TaskSeverity.Error)
                                };

                                Digi3D.Tasks.Add(new TaskGotoPoint(
                                    línea.Points[i - 1],
                                    Color.Aquamarine,
                                    6,
                                    "Línea con un tramo largo sin modificar su coordenada Z",
                                    TaskSeverity.Error,
                                    DigiNG.DrawingFile.Path,
                                    "detectar_lineas_con_tramos_largos_sin_modificar_z",
                                    subtareas));

                           ...

y como una imagen vale más que mil palabras, aquí tienes una captura de la tarea con subtareas:

Captura de pantalla del panel de tareas con una tarea con subtareas

Tarea que selecciona una entidad y además desplaza la cámara (sin cambiar el factor de zoom) a una determinada posición

De todos modos podemos obtener un comportamiento parecido utilizando el tipo Digi21.DigiNG.TaskEntityGotoPoint que es una mezcla de los tipos
TaskGotoPoint y TaskAnimateEntity

Su constructor mas completo es el siguiente:

        public TaskEntityGotoPoint(
            Point3D coordinates,
            Color color,
            int size,
            Entity entity,
            int seconds,
            string title,
            TaskSeverity severity,
            string drawingFile,
            string module,
            ITask[] childs);

que no requiere de ninguna explicación.

Aquí tienes el recorte de nuestra orden utilizando este tipo:

                            if (distanciaRecorridaConÚltimaZ > distancia)
                            {
                                Digi3D.Tasks.Add(new TaskEntityGotoPoint(
                                    línea.Points[i - 1],
                                    Color.Red,
                                    6,
                                    línea,
                                    2,
                                    "Línea con un tramo largo sin modificar su coordenada Z",
                                    TaskSeverity.Error,
                                    DigiNG.DrawingFile.Path,
                                    "detectar_líneas_con_tramos_sin_modificar_z"));

                                DigiNG.RenderScene();
                                break;
                            }

Crea tu propia tarea

Si con las tareas que te proporciona el framework no tienes suficiente, siempre eres libre de implementar tu propia tarea.
Lo único que tienes que hacer crear un tipo que implemente el interfaz Digi21.Digi3D.ITask.

Para verlo en acción, vamos a crear un tipo de tarea que activa únicamente la visualización de un grupo de códigos, de manera que si el usuario hace doble clic, se ocultarán el resto de códigos, para que el usuario se centre únicamente en el código o grupos de códigos que han desencadenado que aparezca la tarea en la barra de tareas.

    public class TareaActivarSoloCódigosDeEntidad : ITask
    {
        string[] códigos;

        public TareaActivarSoloCódigosDeEntidad(Entity entidad)
        {
            this.códigos = (from código in entidad.Codes select código.Name).ToArray();
        }

        public ITask[] Childs
        {
            get { return null; }
        }

        public string DrawingFile
        {
            get { return ""; }
        }

        public void Execute()
        {
            DigiNG.Codes.ShowCode( nombreCódigo => códigos.Contains(nombreCódigo) );
            DigiNG.RenderScene();
        }

        public string Module
        {
            get { return ""; }
        }

        public TaskSeverity Severity
        {
            get { return TaskSeverity.Error; }
        }

        public string Title
        {
            get { return "Activar únicamente los códigos de esta entidad"; }
        }
    }

y podemos añadir tareas de este tipo como subtareas de las tareas del ejemplo anterior:

                            if( distanciaRecorridaConÚltimaZ > distancia )
                            {
                                TareaActivarSoloCódigosDeEntidad[] subtareas = new[] {
                                    new TareaActivarSoloCódigosDeEntidad(línea)
                                };

                                Digi3D.Tasks.Add( new TaskEntityGotoPoint(
                                    línea.Points[i-1],
                                    Color.Aquamarine,
                                    6,
                                    línea,
                                    2,
                                    "Línea con un tramo largo sin modificar su coordenada Z",
                                    TaskSeverity.Error,
                                    DigiNG.DrawingFile.Path,
                                    "detectar_lineas_con_tramos_largos_sin_modificar_z",
                                    subtareas));
                                break;
                            }