Friday, October 20, 2017

Validate Polyline (LighWeightPolyline) As Polygon - Updated

In our CAD designing/drafting practice we often Auto CAD Civil3D/AutoCAD Map to draw polygons with Polyine (LwPolyline) to present as an area on land. When exchange data with other business process (ofter GIS business), we often need to export the closed polylines as GIS polygons (export as shapes, for example). Thus, it is required that the closed polylines generated by our CAD users cannot have duplicate vertices, nor they can be self-intersecting.

AutoCAD Map comes with tool to check/fix polyline's duplicate vertices, self-intersecting, as polygon. However, if we develop our own CAD application that needs to do such check, we'll need our own code to do it. 

To determine if a polyline has duplicate vertices is fairly simple. I can loop through the polyline's vertices from  the first one and compare its position with the next vertex' position (for the last vertex, compare it with the first one, regardless the polyline is closed or not). If the distance of the 2 points is 0.0, or better yet, smaller than a given tolerance, then the polyline has duplicate vertices.

As for determine if the polyline is self-intersecting, originally I thought it would be tricky to code. Back to a few years ago, ADN's Philippe Leefsma posted an article on this topic. But it was one of the comment to that article, made by "Cincir" (very late thanks to you!) provided a much simpler solution. Here I simply materialize it in actual .NET code, for anyone to use, if interested in.

Here is the code:

using System;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
 
namespace ValidatePolyline
{
    public enum PolygonValidateResult
    {
        OK = 0,
        DuplicateVertices = 1,
        SelfIntersection = 2,
        BothDuplicateAndIntersection = 3,
    }
 
    public static class PolylineValidation
    {
        private const double TOLERANCE = 0.001;
 
        public static PolygonValidateResult IsValidPolygon(this Polyline pline)
        {
            var result = PolygonValidateResult.OK;
 
            var t = new Tolerance(TOLERANCE, TOLERANCE);
            using (var curve1 = pline.GetGeCurve(t))
            {
                using (var curve2 = pline.GetGeCurve(t))
                {
                    using (var curveInter = new CurveCurveIntersector3d(
                        curve1, curve2, pline.Normal, t))
                    {
                        if (curveInter.NumberOfIntersectionPoints != pline.NumberOfVertices)
                        {
                            int overlaps = curveInter.OverlapCount();
 
                            if (curveInter.NumberOfIntersectionPoints < pline.NumberOfVertices)
                                result = PolygonValidateResult.DuplicateVertices;
                            else
                                result = overlaps == pline.NumberOfVertices ?
                                    PolygonValidateResult.SelfIntersection :
                                    PolygonValidateResult.BothDuplicateAndIntersection;
                        }
                    }
                }
            }
 
            return result;
        }
    }
}

As you can see, the code is very simple to use, as an extension method of Polyline class. Here is the code to use it:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(ValidatePolyline.MyCommands))]
 
namespace ValidatePolyline
{
    public class MyCommands
    {
        [CommandMethod("TestPoly")]
        public static void RunDocCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var polyId = SelectPolyline(ed);
                if (!polyId.IsNull)
                {
                    TestPolyline(polyId, ed);
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError: {0}", ex.Message);
                ed.WriteMessage("\n*Cancel*");
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        private static ObjectId SelectPolyline(Editor ed)
        {
            var opt = new PromptEntityOptions(
                "\nPick a polyline:");
            opt.SetRejectMessage("\nNot a polyline!");
            opt.AddAllowedClass(typeof(Polyline), true);
 
            var res = ed.GetEntity(opt);
            if (res.Status == PromptStatus.OK)
            {
                return res.ObjectId;
            }
            else
            {
                return ObjectId.Null;
            }
        }
 
        private static void TestPolyline(ObjectId polyId, Editor ed)
        {
            var res = PolygonValidateResult.OK;
            using (var tran = polyId.Database.TransactionManager.StartTransaction())
            {
                var poly = (Polyline)tran.GetObject(polyId, OpenMode.ForRead);
 
                res = poly.IsValidPolygon();
 
                tran.Commit();
            }
 
            string msg = string.Format(
                "\nSelected polyline status: {0}", res.ToString());
 
            ed.WriteMessage(msg);
        }
    }
}

This video clip shows the result of running this code.

UPDATE

I probably posted prematurely without doing enough test run. Anyway, I update the code with a couple of change:

1. Since being "Closed" is the very basic requirement when treating a polyline as polygon, I modified the code to report back if the polyline is close.

2. This also leads to the change of PolygonValidationResult enum type, so that Bitwise result can come out from the test return value to show all the comination of invalid states of a polygon.

Here is the update:

using System;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
 
namespace ValidatePolyline
{
    public enum PolygonValidateResult
    {
        OK = 0,
        NotClosed = 1,
        DuplicateVertices = 2,
        SelfIntersection = 4,
    }
 
    public static class PolylineValidationExtension
    {
        private const double TOLERANCE = 0.001;
 
        public static PolygonValidateResult IsValidPolygon(this Polyline pline)
        {
            var result = PolygonValidateResult.OK;
 
            if (!pline.Closed)
            {
                result += 1;
            }
 
            var t = new Tolerance(TOLERANCE, TOLERANCE);
            using (var curve1 = pline.GetGeCurve(t))
            {
                using (var curve2 = pline.GetGeCurve(t))
                {
                    using (var curveInter = new CurveCurveIntersector3d(
                        curve1, curve2, pline.Normal, t))
                    {
                        int interCount = curveInter.NumberOfIntersectionPoints;
                        int overlaps = curveInter.OverlapCount();
                        if (!pline.Closed) overlaps += 1;
 
                        if (overlaps < pline.NumberOfVertices)
                        {
                            result += 2;
                        }
 
                        if (interCount > overlaps)
                        {
                            result += 4;
                        }
                    }
                }
            }
 
            return result;
        }
 
        public static PolygonValidateResult IsValidPolygon(this ObjectId polyId)
        {
            var result = PolygonValidateResult.OK;
 
            if (polyId.ObjectClass.DxfName.ToUpper() != "LWPOLYLINE")
            {
                throw new ArgumentException("Not a Lightweight Polyline!");
            }
 
            using (var tran = polyId.Database.TransactionManager.StartTransaction())
            {
                var poly = (Polyline)tran.GetObject(polyId, OpenMode.ForRead);
                result = poly.IsValidPolygon();
                tran.Commit();
            }
 
            return result;
        }
 
        public static string ToResultString(this PolygonValidateResult res)
        {
            string msg = "";
            if (res == PolygonValidateResult.OK)
            {
                msg = "valid polyline.";
            }
            else
            {
                if ((res & PolygonValidateResult.NotClosed) == PolygonValidateResult.NotClosed)
                {
                    msg = msg + "Polyline is not closed";
                }
 
                if ((res & PolygonValidateResult.DuplicateVertices) == PolygonValidateResult.DuplicateVertices)
                {
                    if (msg.Length > 0) msg = msg + "; ";
                    msg = msg + "Polyline has duplicate vertices";
                }
 
                if ((res & PolygonValidateResult.SelfIntersection) == PolygonValidateResult.SelfIntersection)
                {
                    if (msg.Length > 0) msg = msg + "; ";
                    msg = msg + "Polyline is self-intersecting";
                }
            }
 
            return msg;
        }
    }
}

and the command to run a test:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(ValidatePolyline.MyCommands))]
 
namespace ValidatePolyline
{
    public class MyCommands
    {
        [CommandMethod("TestPoly")]
        public static void RunDocCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var polyId = SelectPolyline(ed);
                if (!polyId.IsNull)
                {
                    TestPolyline(polyId, ed);
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError: {0}", ex.Message);
                ed.WriteMessage("\n*Cancel*");
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        private static ObjectId SelectPolyline(Editor ed)
        {
            var opt = new PromptEntityOptions(
                "\nPick a polyline:");
            opt.SetRejectMessage("\nNot a polyline!");
            opt.AddAllowedClass(typeof(Polyline), true);
 
            var res = ed.GetEntity(opt);
            if (res.Status == PromptStatus.OK)
            {
                return res.ObjectId;
            }
            else
            {
                return ObjectId.Null;
            }
        }
 
        private static void TestPolyline(ObjectId polyId, Editor ed)
        {
            var res = polyId.IsValidPolygon();
            ed.WriteMessage("\nTest Result: " + res.ToResultString());
        }
    }
}