Monday, December 30, 2019

My Class in Autodesk University 2019, Las Vegas

I attended Autodesk University 2019, Las Vegas in Nov. 2019 and presented my class (SD321455). The class' is titled as "Creating Plugable Custom Document Window Content To Enhance  AutoCAD UI".

Here is the link to the class' materials in Autodesk University website:

https://www.autodesk.com/autodesk-university/class/Creating-Plugable-Custom-Document-Window-Content-Enhance-AutoCAD-Softwares-UI-2019

The presentation itself was not as good as I wanted, because after a few minutes into it, I realized that the time (60 minutes) was much shorter than I needed as planned. So, the recorded video would not be much of good for learning. But the 48-page handout and the download-able code projects, in conjunction with PowerPoint presentation cover all details for any AutoCAD .NET API programmer to know on the topic.

By the way, to tell the truth, one of the factors that motivated me to go to Autodesk University, Las Vegas is the Rock'n Roll Las Vegas Marathon held on the Sunday right before AU (No, 17th, 2019), which was the 7th marathon races I completed in 2019😀

Friday, December 20, 2019

Retrieving Catchment Information from Its Label in Civil 3D

A post in Autodesk's Civil 3D customization discussion forum here asked how to retrieve Catchment data for exporting to external file and I suggested to use Catchment label as the source of Catchment data, as long as the label style used for the label contains the wanted information of the Catchment. Upon the request of the original poster, I worked out some code and post here in my blog, so it can be read easier, or downloaded as complete project for test run.

While code is rather simple and quite self-explanatory, I also placed a bit comments here and there.

These are 2 major classes:

using System.Collections.Generic;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace CatchmentLabelData
{
    public class CatchmentAreaInfo
    {
        public string AreaName { setget; } = "";
        public double Area { setget; } = 0.0;
        public double Coefficient { setget; } = 1.0;
        public double ConcentrationTime { setget; } = 0.0;
 
        public override string ToString()
        {
            return AreaName + "," +
                Area.ToString("#####0.000") + "," +
                Coefficient.ToString("#0.00") + "," +
                ConcentrationTime.ToString("##0.0");
        }
    }
 
    public class CatchmentLabelDataExtractor
    {
        public Document ThisDwg { private setget; }
 
        public void ExtractDataFromLabels(Document dwg)
        {
            ThisDwg = dwg;
 
            IEnumerable<CatchmentAreaInfodata = ExtractData();
            if (data!=null)
            {
                SaveData(data);
            }
            else
            {
                CadApp.ShowAlertDialog(
                    "No catchment data found!");
            }
        }
 
        #region private methods: extract data from Catchment labels
 
        private IEnumerable<CatchmentAreaInfoExtractData()
        {
            var lblIds = FindCatchmentLabels();
            if (lblIds.Count == 0) return null;
 
            var data = new List<CatchmentAreaInfo>();
            foreach (var id in lblIds)
            {
                CatchmentAreaInfo area = GetCatchmentAreaDataFromLabel(id);
                if(area!=null)
                {
                    data.Add(area);
                }
            }
 
            return data.Count > 0 ? data : null; ;
        }
 
        private CatchmentAreaInfo GetCatchmentAreaDataFromLabel(ObjectId lblId)
        {
            CatchmentAreaInfo area = null;
 
            using (var tran = lblId.Database.TransactionManager.StartTransaction())
            {
                var ent = (Entity)tran.GetObject(lblIdOpenMode.ForRead);
 
                var lblText = ExplodeLabel(ent);
 
                if (!string.IsNullOrEmpty(lblText))
                {
                    area = ParseLabelText(lblText);
                }
 
                tran.Commit();
            }
 
            return area;
        }
 
        private string ExplodeLabel(Entity ent)
        {
            var lblText = "";
            var exploded = new List<DBObject>();
 
            // first round of explosion: the result is a BlockReference
            BlockReference blk = null;
            var outEnts = new DBObjectCollection();
            ent.Explode(outEnts);
            foreach (DBObject obj in outEnts)
            {
                exploded.Add(obj);
                if (obj is BlockReference)
                {
                    blk = (BlockReference)obj;
                }
            }
 
            // second round of explosion, the result is an MText
            if (blk!=null)
            {
                outEnts = new DBObjectCollection();
                blk.Explode(outEnts);
                foreach (DBObject obj in outEnts)
                {
                    exploded.Add(obj);
                    if (obj is MText)
                    {
                        lblText = ((MText)obj).Text;
                    }
                }
            }
 
            // disposed entities resulted in by explosion
            foreach (DBObject obj in exploded)
            {
                obj.Dispose();
            }
            
            return lblText;
        }
 
        /// <summary>
        /// The text parsing logic here would heavily be depended on 
        /// the label style's content
        /// </summary>
        /// <param name="lblText"></param>
        /// <returns></returns>
        private CatchmentAreaInfo ParseLabelText(string lblText)
        {
            var txts = lblText.Replace("\r\n""|").Split('|');
            if (txts.Length!=4)
            {
                // Invalid label content
                // Since we target particular label style
                // we should know the lable content has 4 pieces of data
                return null;
            }
 
            var name = txts[0];
            var area = ParseForNumber(txts[1]);
            var coe = ParseForNumber(txts[2]);
            var time = ParseForNumber(txts[3]);
 
            CatchmentAreaInfo catchment = new CatchmentAreaInfo
            {
                AreaName = name,
                Area = area,
                Coefficient = coe,
                ConcentrationTime = time
            };
 
            return catchment;
        }
 
        private double ParseForNumber(string txt)
        {
            double num = 0.0;
 
            int start = -1;
            int end = -1;
            forint i=0; i<txt.Length; i++)
            {
                if (char.IsNumber(txt,i))
                {
                    if (start == -1) start = i;
                    break;
                }
            }
 
            if (start>=0)
            {
                for (int i=start+1; i<txt.Length;i++)
                {
                    if (!char.IsNumber(txti) &&
                        !char.IsPunctuation(txt,i))
                    {
                        end = i;
                        break;
                    }
                }
 
                if (end <= 0) end = txt.Length;
            }
 
            if (start>=0 && end>start)
            {
                var s = txt.Substring(startend - start).Trim();
                num = double.Parse(s);
            }
 
            return num;
        }
 
        private List<ObjectIdFindCatchmentLabels()
        {
            var ids = new List<ObjectId>();
            using (var tran = ThisDwg.TransactionManager.StartTransaction())
            {
                var model = (BlockTableRecord)tran.GetObject(
                    SymbolUtilityServices.GetBlockModelSpaceId(ThisDwg.Database), OpenMode.ForRead);
                foreach (ObjectId id in model)
                {
                    //ThisDwg.Editor.WriteMessage($"\nEntity => {id.ObjectClass.DxfName}");
                    if (id.ObjectClass.DxfName.ToUpper()==
                        "AECC_CATCHMENTAREA_LABEL")
                    {
                        //===========================================================
                        // we may want to further examine the label's LabelStyle
                        // in order to make sure this label does contain the data
                        // as expected according to the LabelStyle desgin.
                        // for simplicity of this sample code, I skip it
                        //===========================================================
 
                        ids.Add(id);
                    }
                }
 
                tran.Commit();
            }
 
            return ids;
        }
 
        #endregion
 
        #region private methods: save extracted data
 
        private void SaveData(IEnumerable<CatchmentAreaInfodata)
        {
            var txts = new List<string>();
            foreach (var catchment in data)
            {
                txts.Add(catchment.ToString());
            }
 
            var header = "Name,Area,Runoff Coefficient,Time of Concentration\r\n";
            var outputText = header + string.Join("\r\n"txts.ToArray());
 
            var fileName = GetFileName();
            if (!string.IsNullOrEmpty(fileName))
            {
                if (System.IO.File.Exists(fileName))
                {
                    System.IO.File.Delete(fileName);
                }
 
                System.IO.File.WriteAllText(fileNameoutputText);
            }
        }
 
        private string GetFileName()
        {
            var fileName = "";
            using (var dlg = new System.Windows.Forms.SaveFileDialog())
            {
                dlg.Title = "Save Catchment Area Information To CSV File";
                if (ThisDwg.IsNamedDrawing)
                {
                    dlg.InitialDirectory = System.IO.Path.GetDirectoryName(
                        ThisDwg.Name);
                    dlg.FileName = System.IO.Path.GetFileNameWithoutExtension(
                        ThisDwg.Name) + ".csv";
                }
                dlg.Filter = "CSV File *.csv|*.csv";
                dlg.OverwritePrompt = true;
                if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
                {
                    fileName = dlg.FileName;
                }
            }
 
            return fileName;
        }
 
        #endregion
    }
}

Here is the CommandClass that runs process:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(CatchmentLabelData.MyCommands))]
 
namespace CatchmentLabelData
{
    public class MyCommands
    {
        [CommandMethod("CatchmentData")]
        public static void RunMyCommand()
        {
            var doc = CadApp.DocumentManager.MdiActiveDocument;
            var ed = doc.Editor;
 
            try
            {
                var extractor = new CatchmentLabelDataExtractor();
                extractor.ExtractDataFromLabels(doc);
            }
            catch(System.Exception ex)
            {
                ed.WriteMessage(
                    $"\nError: \n{ex.Message}\n");
            }
        }
    }
}


For simplicity, I let the output to be saved as CSV file. The primary goal of this article is about retrieving Catchment data from its label.

Some thoughts on this topic:

It is quite frustrating that Autodesk seems does not make much effort to update/enhance Civil 3D's API. Label is most powerful thing in Civil 3D: it can designed to automatically obtain data from Civil 3D objects (as LabelStyle's Content). Obviously, there is internal API to do this, because user can edit the label style's content. But these APIs are not exposed to outside at all (maybe, because they are too complicated. I am always amazed how labels in Civil 3D work).

Through this discussion, I can see, with lack of API support, one possible workaround of getting Civil 3D object information is to:

1. Define a label style to let its content expose the wanted information. That is, let Civil 3D to do the work of collecting data from its objects into particular labels;
2. We then can use similar approach shown in this article to obtain the data from labels.

The source code with built DLL files can be downloaded here. Note, if you want to run the DLL from this download without building from the project\s source code, you need to unlock it first (because of the executable DLL is downloaded via Internet). The project is built against Civil 3D 2020 and .NET Framework 4.72.



















Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.