Archivo de la etiqueta: programación

Desarrollando un transformador universal de coordenadas

Transformador Universal de Coordenadas

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Para ello vamos a seguir los siguientes pasos:

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

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

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

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

    El método queda así pues:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fragments Shaders en Digi3D.NET

Digi3D.NET permite transformar los parámetros radiomátricos (brillo/color/contraste/gama/niveles/tonos de gris/positivo o negativo…) de visualización de las imágenes que muestra en la ventana fotogramétrica mediante los controles del panel Propiedades de la imagen de la ventana fotogramétrica.

Podemos configurar el programa para que esta transformación se realice o por software o por hardware, en la opción del menú Herramientas/Configuración/Estereoscopía/Método para Brillo y Contraste.

Disponemos de dos opciones:

  • Software
  • Fragment Shaders

Si instalamos Digi3D.NET por primera vez en un equipo la opción predeterminada será Fragment Shaders que es la implementación por hardware.

La opción ideal es hacer la transformación por hardware, pero no todas las tarjetas gráficas están preparadas para realizar operaciones por hardware. Las tarjetas gráficas que admiten esta operación son tarjetas que admiten el lenguaje de programación de Shaders.

Si nuestra tarjeta gráfica no admite shaders, si tenemos seleccionada la opción Fragment Shaders comprobaremos que al desplazarnos por la imagen, esta dará tirones, en cuyo caso tendremos que seleccionar la opción Software.

Mi recomendación es que si tu tarjeta gráfica lo admite, selecciones la opción Fragment Shaders, por varios motivos:

  • Velocidad: ya que si la operación se realiza por software, cada vez que Digi3D.NET cargue una tesela de la imagen, tendrá que modificar pixel por pixel su valor en función de la configuración de brillo/contraste/color… seleccionada por el usuario.

    Además, me atrevería a asegurar que el 99% de los equipos cuya tarjeta gráfica no admite shaders son antiguos y lentos, lo que provocará que la carga de la tesela se relentice, provocando que el usuario visualice por uno o varios segundos esta tesela de color negro.

  • Carga del procesador: Si el procesador está ocupado cambiando el color de la imagen, el scroll de la imagen se puede ralentizar.

    Y tal y como expongo en el punto anterior, como el procesador seguro que tiene pocos núcleos (dos, no más) esto afectará al rendimiento de Digi3D.NET.

  • Modificación real de la imagen en memoria: Si nos disponemos a realizar alguna operación con la imagen cargada en memoria, como por ejemplo correlar un modelo, y esta ha sido alterada por software, los resultados variarán en función del los parámetros radiométricos de la imagen.

Digi3D.NET programa los shaders de radiometría mediante el lenguaje de programación GLSL que son las siglas de OpenGL Shading Language, y este lenguaje se programación se caracteriza porque los programas no se pueden almacenar compilados, sino que los compila la tarjeta gráfica (el Driver de OpenGL más bien) en el mismo instante en el que cargamos el programa en la tarjeta gráfica.

Puedes localizar el código fuente del Fragment Shader que gestiona los parámetros radiométricos de visualización de Digi3D.NET en la ruta:

Sistema operativo Versión de Digi3D.NET Ruta
32 bits 32 bits %ProgramFiles%Digi21.netDigi3D2011FragmentShader.glsl
64 bits 64 bits %ProgramFiles%Digi21.netDigi3D2011FragmentShader.glsl
64 bits 32 bits %ProgramFiles (x86)%Digi21.netDigi3D2011FragmentShader.glsl

Este archivo es un archivo de texto que puedes editar con cualquier editor como el bloc de notas. Está programado en el lenguaje CLSL que es muy parecido al lenguaje de programación C.

Lo que más te va a sorprender si no sabes de programación de shaders es que el programa se ejecuta una vez por cada pixel de pantalla, si has leido bien, si nuestro monitor tiene 1920×1080 píxeles, cada vez que movamos la ventana de Digi3D.NET un único píxel, se ejecutará este programa 2073600 veces!!!!! y en tiempo real, seguro que si nuestra tarjeta no es muy lenta podremos llegar a los 140 frames por segundo sin ningún problema.

No es mi intención enseñarte a programar shaders, lo que si que puedo es recomendarte un libro, como es OpenGL Shading Language, pero si te puedo explicar un poquito el contenido del código y cómo modificarlo para hacer una broma o para investigar.

El código del shader es el siguiente:

// Fragment Shader para las transformaciones de color en Digi3D 2011-2012
// Desarrollado por José Ángel Martínez Torres - Dreaming With Objects S.L.
// www.digi21.net/digi3d
//

// Indica realizar una transformación por tabla de color. Si es verdadero no se ajustarán ni brillo, 
// ni contraste ni gamma, sino que se transformará el color de entrada
// en un color de salida.
// Lo activa por ejemplo la casilla de Niveles Automáticos. Si el usuario activa esta casilla, se 
// calcula una tabla de transformación de colores mediante el algoritmo
// indicado en http://en.wikipedia.org/wiki/Histogram_equalization
uniform bool aplicarTablaColor;
uniform vec3 tablaColor[256];

// En la variable brillo se almacena el desplazamiento a realizar en el brillo. Se admiten valores entre [-1,+1]
uniform float brillo;

// En la variable brillo se almacena el desplazamiento de contraste a aplicar. Se admiten valores entre [-1,+1]
uniform float contraste;

// La variable transformarTonoGris controla si se debe transformar la imagen a tonos de gris.
uniform bool transformarTonosGris;

// La variable negativo controla si se debe mostrar la imagen en negativo.
uniform bool negativo;

// El valor escalaColor indica el factor (que será potencia de 2) por el cual se debe escalar el color.
uniform float escalaColor;

// Indica si el usuario ha desplazado el control de Gamma.
// Documentación de Gamma en http://www.ivl.disco.unimib.it/Teaching/AIC-2010-specialistica/proof_paper_JEI.pdf
uniform float gamma;


// Fórmula del brillo contraste de The Gimp tal y como aparece en el artículo de la wikipedia
// http://en.wikipedia.org/wiki/Image_editing#Contrast_change_and_brightening
// Modificada para no calcular la tangente por cada componente
float CalculaValorBrilloContrasteRapido(float value, float brightness, float tangentePiCeroCinco)
{
    if (brightness < 0.0)
        value = value * ( 1.0 + brightness);
    else
        value = value + ((1.0 - value) * brightness);

    value = (value - 0.5) * tangentePiCeroCinco + 0.5;
    return value;
}

void main(void)
{
    vec4 colorTransformado = gl_Color;

    if( aplicarTablaColor ) {
        colorTransformado.r = tablaColor[int(gl_Color.r*255.0)].r;
        colorTransformado.g = tablaColor[int(gl_Color.g*255.0)].g;
        colorTransformado.b = tablaColor[int(gl_Color.b*255.0)].b;
    } else {
        if( brillo != 0.0 || contraste != 0.0) {
            float tangentePiCeroCinco = tan((contraste + 1.0) * 3.14159265358979323/4.0);
            colorTransformado.r = CalculaValorBrilloContrasteRapido(gl_Color.r, brillo, tangentePiCeroCinco);
            colorTransformado.g = CalculaValorBrilloContrasteRapido(gl_Color.g, brillo, tangentePiCeroCinco);
            colorTransformado.b = CalculaValorBrilloContrasteRapido(gl_Color.b, brillo, tangentePiCeroCinco);
        }

        if( 1.0 != escalaColor )
            colorTransformado *= vec4(escalaColor, escalaColor, escalaColor, 1.0);

        if( gamma != 1.0 )
            colorTransformado = pow(colorTransformado, vec4(1.0/gamma, 1.0/gamma, 1.0/gamma, 1.0));
    }

    if( negativo )
        colorTransformado = vec4(1.0, 1.0, 1.0, 0.0) - colorTransformado;

    if( transformarTonosGris ) {
        float iluminacion = colorTransformado.r * 0.299 + colorTransformado.g * 0.587 + colorTransformado.b * 0.114;
        colorTransformado = vec4(iluminacion, iluminacion, iluminacion, 1.0);
    }

    gl_FragColor = colorTransformado;
}

Como puedes comprobar, al principio se declaran una serie de variables (aplicarTablaColor, tablaColor, brillo, …) que son las que se modifican cuando el usuario interactua con los controles, y dentro de la función main tienes el código que realiza las correspondientes transformaciones.

Te recuerdo que este programa se ejecuta una vez por cada pixel a mostrar, y el objetivo de cualquier Fragment Shader es el de asignar la variable global gl_FragColor que es un tipo con cuatro componentes (rojo, verde, azul, alfa). El color original lo tenemos almacenado en la viable global gl_Color.

Vamos a modificar el Fragment Shader para que no se muestren colores ni verde ni azul, únicamente rojo, de esta manera conseguiremos ver el modelo de Bronchales (que es el que utilizo para la captura de pantalla del comienzo de la entrada) de color rojo. Para ello, voy a obtener el valor de iluminación (que como puedes ver en el código fuente original se obtiene multiplicando la componente roja del pixel por 0,299, la componente verde por 0,587 y la componente azul por 0,114, y vamos a indicar que este valor será el color a utilizar en la componente roja del color de salida.

A continuación el código fuente de nuestro Fragment Shader modificado…

void main(void)
{
    float iluminacion = gl_Color.r * 0.299 + gl_Color.g * 0.587 + gl_Color.b * 0.114;
    gl_FragColor = vec4(iluminacion, 0.0, 0.0, 1.0);
}

Detectando líneas sin continuidad

Aquí el vídeo relacionado con esta entrada

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

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

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

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

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

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

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

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

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

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

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

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

Aquí el código de la orden:

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

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

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

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

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

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

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

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

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

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

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


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

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

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

Aquí el vídeo relacionado con este post

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

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

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

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

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

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

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

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

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

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

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

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

Aquí el vídeo relacionado con este post

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

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

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

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

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

            return entidadesADevolver;
        }

    }
}

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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