Detectando las intersecciones de una determinada línea

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