November 2014

Sun Mon Tue Wed Thu Fri Sat
            1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30            










« DevCamps 2008 - Customization and Application Development Conferences | Main | Deleting unused annotation scales in AutoCAD using .NET »

May 16, 2008

An automatic numbering system for AutoCAD blocks using .NET - Part 4

In the original post in this series, we introduced a basic application to number AutoCAD objects, specifically blocks with attributes. In the second post we extended this to make use of a generic numbering system for drawing-resident AutoCAD objects, and in the third post we implemented additional commands to take advantage of this new "kernel".

In this post we're going to extend the application in a few ways: firstly we're going to support duplicates, so that the LNS command which parses the current drawing to understand its numbers will support automatic and semi-automatic renumbering of objects with duplicate numbers. In addition there are a number of new event handlers that have been introduced to automatically renumber objects on creation/insertion/copy, and also to clear the numbering system when a user undoes any action in the drawing (just to be safe :-).

While introducing these event handlers I decide to switch the approach for associating data with a drawing: rather than declaring the variables at a class level and assuming they would be duplicated instantiated appropriately per-document, as shown in this previous post, I decided to encapsulate the variables in a class and specifically instantiate that class and store it per-document, as shown in this previous post.

Here's the updated C# code, with the changed & new lines in red, and here is the complete source file to save you having to strip line numbers:

    1 using Autodesk.AutoCAD.ApplicationServices;

    2 using Autodesk.AutoCAD.Runtime;

    3 using Autodesk.AutoCAD.DatabaseServices;

    4 using Autodesk.AutoCAD.EditorInput;

    5 using Autodesk.AutoCAD.Geometry;

    6 using System.Collections.Generic;

    7 using System.Collections;

    8

    9 namespace AutoNumberedBubbles

   10 {

   11   public class Commands : IExtensionApplication

   12   {

   13     // Strings identifying the block

   14     // and the attribute name to use

   15

   16     const string blockName = "BUBBLE";

   17     const string attbName = "NUMBER";

   18

   19     // A string to identify our application's

   20     // data in per-document UserData

   21

   22     const string dataKey = "TTIFBubbles";

   23

   24     // Define a class for our custom data

   25

   26     public class BubbleData

   27     {

   28       // A separate object to manage our numbering

   29

   30       private NumberedObjectManager m_nom;

   31       public NumberedObjectManager Nom

   32       {

   33         get { return m_nom; }

   34       }

   35

   36       // A "base" index (for the start of the list)

   37

   38       private int m_baseNumber;

   39       public int BaseNumber

   40       {

   41         get { return m_baseNumber; }

   42         set { m_baseNumber = value; }

   43       }

   44

   45       // A list of blocks added to the database

   46       // which we will then renumber

   47

   48       private List<ObjectId> m_blocksAdded;

   49       public List<ObjectId> BlocksToRenumber

   50       {

   51         get { return m_blocksAdded; }

   52       }

   53

   54       // Constructor

   55

   56       public BubbleData()

   57       {

   58         m_baseNumber = 0;

   59         m_nom = new NumberedObjectManager();

   60         m_blocksAdded = new List<ObjectId>();

   61       }

   62

   63       // Method to clear the contents

   64

   65       public void Reset()

   66       {

   67         m_baseNumber = 0;

   68         m_nom.Clear();

   69         m_blocksAdded.Clear();

   70       }

   71     }

   72

   73     // Constructor

   74

   75     public Commands()

   76     {

   77     }

   78

   79     // Functions called on initialization & termination

   80

   81     public void Initialize()

   82     {

   83       try

   84       {

   85         DocumentCollection dm =

   86           Application.DocumentManager;

   87         Document doc = dm.MdiActiveDocument;

   88         Database db = doc.Database;

   89         Editor ed = doc.Editor;

   90

   91         ed.WriteMessage(

   92           "\nLNS  Load numbering settings by analyzing the current drawing" +

   93           "\nDMP  Print internal numbering information" +

   94           "\nBAP  Create bubbles at points" +

   95           "\nBIC  Create bubbles at the center of circles" +

   96           "\nMB  Move a bubble in the list" +

   97           "\nDB  Delete a bubble" +

   98           "\nRBS  Reorder the bubbles, to close gaps caused by deletion" +

   99           "\nHLB  Highlight a particular bubble"

  100         );

  101

  102         // Hook into some events, to detect and renumber

  103         // blocks added to the database

  104

  105         db.ObjectAppended +=

  106           new ObjectEventHandler(

  107             db_ObjectAppended

  108           );

  109         dm.DocumentCreated +=

  110           new DocumentCollectionEventHandler(

  111             dm_DocumentCreated

  112           );

  113         dm.DocumentLockModeWillChange +=

  114           new DocumentLockModeWillChangeEventHandler(

  115             dm_DocumentLockModeWillChange

  116           );

  117

  118         doc.CommandEnded +=

  119           delegate(object sender, CommandEventArgs e)

  120           {

  121             if (e.GlobalCommandName == "UNDO" ||

  122                 e.GlobalCommandName == "U")

  123             {

  124               ed.WriteMessage(

  125                 "\nUndo invalidates bubble numbering: call" +

  126                 " LNS to reload the numbers for this drawing"

  127               );

  128               GetBubbleData((Document)sender).Reset();

  129             }

  130           };

  131       }

  132       catch

  133       { }

  134     }

  135

  136     public void Terminate()

  137     {

  138     }

  139

  140     // Method to retrieve (or create) the

  141     // BubbleData object for a particular

  142     // document

  143

  144     private BubbleData GetBubbleData(Document doc)

  145     {

  146       Hashtable ud = doc.UserData;

  147       BubbleData bd =

  148         ud[dataKey] as BubbleData;

  149

  150       if (bd == null)

  151       {

  152         object obj = ud[dataKey];

  153         if (obj == null)

  154         {

  155           // Nothing there

  156

  157           bd = new BubbleData();

  158           ud.Add(dataKey, bd);

  159         }

  160         else

  161         {

  162           // Found something different instead

  163

  164           Editor ed = doc.Editor;

  165           ed.WriteMessage(

  166             "Found an object of type \"" +

  167             obj.GetType().ToString() +

  168             "\" instead of BubbleData.");

  169         }

  170       }

  171       return bd;

  172     }

  173

  174     // Do the same for a particular database

  175

  176     private BubbleData GetBubbleData(Database db)

  177     {

  178       DocumentCollection dm =

  179         Application.DocumentManager;

  180       Document doc =

  181         dm.GetDocument(db);

  182       return GetBubbleData(doc);

  183     }

  184

  185     // When a new document is created, attach our

  186     // ObjectAppended event handler to the new

  187     // database

  188

  189     void dm_DocumentCreated(

  190       object sender,

  191       DocumentCollectionEventArgs e

  192     )

  193     {

  194       e.Document.Database.ObjectAppended +=

  195         new ObjectEventHandler(

  196           db_ObjectAppended

  197         );

  198     }

  199

  200     // When an object is appended to a database,

  201     // add it to a list we care about if it's a

  202     // BlockReference

  203

  204     void db_ObjectAppended(

  205       object sender,

  206       ObjectEventArgs e

  207     )

  208     {

  209       BlockReference br =

  210         e.DBObject as BlockReference;

  211       if (br != null)

  212       {

  213         BubbleData bd =

  214           GetBubbleData(e.DBObject.Database);

  215         bd.BlocksToRenumber.Add(br.ObjectId);

  216       }

  217     }

  218

  219     // When the command (or action) is over,

  220     // take the list of blocks to renumber and

  221     // go through them, renumbering each one

  222

  223     void dm_DocumentLockModeWillChange(

  224       object sender,

  225       DocumentLockModeWillChangeEventArgs e

  226     )

  227     {

  228       Document doc = e.Document;

  229       BubbleData bd =

  230         GetBubbleData(doc);

  231

  232       if (bd.BlocksToRenumber.Count > 0)

  233       {

  234         Database db = doc.Database;

  235         Transaction tr =

  236           db.TransactionManager.StartTransaction();

  237         using (tr)

  238         {

  239           foreach (ObjectId bid in bd.BlocksToRenumber)

  240           {

  241             try

  242             {

  243               BlockReference br =

  244                 tr.GetObject(bid, OpenMode.ForRead)

  245                 as BlockReference;

  246               if (br != null)

  247               {

  248                 BlockTableRecord btr =

  249                   (BlockTableRecord)tr.GetObject(

  250                     br.BlockTableRecord,

  251                     OpenMode.ForRead

  252                 );

  253                 if (btr.Name == blockName)

  254                 {

  255                   AttributeCollection ac =

  256                     br.AttributeCollection;

  257

  258                   foreach (ObjectId aid in ac)

  259                   {

  260                     DBObject obj =

  261                       tr.GetObject(aid, OpenMode.ForRead);

  262                     AttributeReference ar =

  263                       obj as AttributeReference;

  264

  265                     if (ar.Tag == attbName)

  266                     {

  267                       // Change the one we care about

  268

  269                       ar.UpgradeOpen();

  270

  271                       int bubbleNumber =

  272                         bd.BaseNumber +

  273                         bd.Nom.NextObjectNumber(bid);

  274                       ar.TextString =

  275                         bubbleNumber.ToString();

  276

  277                       break;

  278                     }

  279                   }

  280                 }

  281               }

  282             }

  283             catch { }

  284           }

  285           tr.Commit();

  286           bd.BlocksToRenumber.Clear();

  287         }

  288       }

  289     }

  290

  291     // Command to extract and display information

  292     // about the internal numbering

  293

  294     [CommandMethod("DMP")]

  295     public void DumpNumberingInformation()

  296     {

  297       Document doc =

  298         Application.DocumentManager.MdiActiveDocument;

  299       Editor ed = doc.Editor;

  300       BubbleData bd =

  301         GetBubbleData(doc);

  302       bd.Nom.DumpInfo(ed);

  303     }

  304

  305     // Command to analyze the current document and

  306     // understand which indeces have been used and

  307     // which are currently free

  308

  309     [CommandMethod("LNS")]

  310     public void LoadNumberingSettings()

  311     {

  312       Document doc =

  313         Application.DocumentManager.MdiActiveDocument;

  314       Database db = doc.Database;

  315       Editor ed = doc.Editor;

  316       BubbleData bd =

  317         GetBubbleData(doc);

  318

  319       // We need to clear any internal state

  320       // already collected

  321

  322       bd.Reset();

  323

  324       // Select all the blocks in the current drawing

  325

  326       TypedValue[] tvs =

  327         new TypedValue[1] {

  328             new TypedValue(

  329               (int)DxfCode.Start,

  330               "INSERT"

  331             )

  332           };

  333       SelectionFilter sf =

  334         new SelectionFilter(tvs);

  335

  336       PromptSelectionResult psr =

  337         ed.SelectAll(sf);

  338

  339       // If it succeeded and we have some blocks...

  340

  341       if (psr.Status == PromptStatus.OK &&

  342           psr.Value.Count > 0)

  343       {

  344         Transaction tr =

  345           db.TransactionManager.StartTransaction();

  346         using (tr)

  347         {

  348           // First get the modelspace and the ID

  349           // of the block for which we're searching

  350

  351           BlockTableRecord ms;

  352           ObjectId blockId;

  353

  354           if (GetBlock(

  355                 db, tr, out ms, out blockId

  356             ))

  357           {

  358             // For each block reference in the drawing...

  359

  360             foreach (SelectedObject o in psr.Value)

  361             {

  362               DBObject obj =

  363                 tr.GetObject(o.ObjectId, OpenMode.ForRead);

  364               BlockReference br = obj as BlockReference;

  365               if (br != null)

  366               {

  367                 // If it's the one we care about...

  368

  369                 if (br.BlockTableRecord == blockId)

  370                 {

  371                   // Check its attribute references...

  372

  373                   int pos = -1;

  374                   AttributeCollection ac =

  375                     br.AttributeCollection;

  376

  377                   foreach (ObjectId id in ac)

  378                   {

  379                     DBObject obj2 =

  380                       tr.GetObject(id, OpenMode.ForRead);

  381                     AttributeReference ar =

  382                       obj2 as AttributeReference;

  383

  384                     // When we find the attribute

  385                     // we care about...

  386

  387                     if (ar.Tag == attbName)

  388                     {

  389                       try

  390                       {

  391                         // Attempt to extract the number from

  392                         // the text string property... use a

  393                         // try-catch block just in case it is

  394                         // non-numeric

  395

  396                         pos =

  397                           int.Parse(ar.TextString);

  398

  399                         // Add the object at the appropriate

  400                         // index

  401

  402                         bd.Nom.NumberObject(

  403                           o.ObjectId, pos, false, true

  404                         );

  405                       }

  406                       catch { }

  407                     }

  408                   }

  409                 }

  410               }

  411             }

  412           }

  413           tr.Commit();

  414         }

  415

  416         // Once we have analyzed all the block references...

  417

  418         int start = bd.Nom.GetLowerBound(true);

  419

  420         // If the first index is non-zero, ask the user if

  421         // they want to rebase the list to begin at the

  422         // current start position

  423

  424         if (start > 0)

  425         {

  426           ed.WriteMessage(

  427             "\nLowest index is {0}. ",

  428             start

  429           );

  430           PromptKeywordOptions pko =

  431             new PromptKeywordOptions(

  432               "Make this the start of the list?"

  433             );

  434           pko.AllowNone = true;

  435           pko.Keywords.Add("Yes");

  436           pko.Keywords.Add("No");

  437           pko.Keywords.Default = "Yes";

  438

  439           PromptResult pkr =

  440             ed.GetKeywords(pko);

  441

  442           if (pkr.Status != PromptStatus.OK)

  443             bd.Reset();

  444           else

  445           {

  446             if (pkr.StringResult == "Yes")

  447             {

  448               // We store our own base number

  449               // (the object used to manage objects

  450               // always uses zero-based indeces)

  451

  452               bd.BaseNumber = start;

  453               bd.Nom.RebaseList(bd.BaseNumber);

  454             }

  455           }

  456         }

  457

  458         // We found duplicates in the numbering...

  459

  460         if (bd.Nom.HasDuplicates())

  461         {

  462           // Ask how to fix the duplicates

  463

  464           PromptKeywordOptions pko =

  465             new PromptKeywordOptions(

  466               "Blocks contain duplicate numbers. " +

  467               "How do you want to renumber?"

  468             );

  469           pko.AllowNone = true;

  470           pko.Keywords.Add("Automatically");

  471           pko.Keywords.Add("Individually");

  472           pko.Keywords.Add("Not");

  473           pko.Keywords.Default = "Automatically";

  474

  475           PromptResult pkr =

  476             ed.GetKeywords(pko);

  477

  478           bool bAuto = false;

  479           bool bManual = false;

  480

  481           if (pkr.Status != PromptStatus.OK)

  482             bd.Reset();

  483           else

  484           {

  485             if (pkr.StringResult == "Automatically")

  486               bAuto = true;

  487             else if (pkr.StringResult == "Individually")

  488               bManual = true;

  489

  490             // Whether fixing automatically or manually

  491             // we will iterate through the duplicate list

  492

  493             if (bAuto || bManual)

  494             {

  495               ObjectIdCollection idc =

  496                 new ObjectIdCollection();

  497

  498               // Get each entry in the duplicate list

  499

  500               SortedDictionary<int,List<ObjectId>> dups =

  501                 bd.Nom.Duplicates;

  502               foreach (

  503                 KeyValuePair<int,List<ObjectId>> dup in dups

  504               )

  505               {

  506                 // The position is the key in the entry

  507                 // and the list of IDs is the value

  508                 // (we take a copy, so we can modify it

  509                 // without affecting the original)

  510

  511                 int pos = dup.Key;

  512                 List<ObjectId> ids =

  513                   new List<ObjectId>(dup.Value);

  514

  515                 // For automatic renumbering there's no

  516                 // user interaction

  517

  518                 if (bAuto)

  519                 {

  520                   foreach (ObjectId id in ids)

  521                   {

  522                     bd.Nom.NextObjectNumber(id);

  523                     idc.Add(id);

  524                   }

  525                 }

  526                 else // bManual

  527                 {

  528                   // For manual renumbering we ask the user

  529                   // to select the block to keep, then

  530                   // we renumber the rest automatically

  531

  532                   ed.UpdateScreen();

  533

  534                   ids.Add(bd.Nom.GetObjectId(pos));

  535                   HighlightBubbles(db, ids, true);

  536

  537                   ed.WriteMessage(

  538                     "\n\nHighlighted blocks " +

  539                     "with number {0}. ",

  540                     pos + bd.BaseNumber

  541                   );

  542

  543                   bool finished = false;

  544                   while (!finished)

  545                   {

  546                     PromptEntityOptions peo =

  547                       new PromptEntityOptions(

  548                         "Select block to keep (others " +

  549                         "will be renumbered automatically): "

  550                       );

  551                     peo.SetRejectMessage(

  552                       "\nEntity must be a block."

  553                     );

  554                     peo.AddAllowedClass(

  555                       typeof(BlockReference), false);

  556                     PromptEntityResult per =

  557                       ed.GetEntity(peo);

  558

  559                     if (per.Status != PromptStatus.OK)

  560                     {

  561                       bd.Reset();

  562                       return;

  563                     }

  564                     else

  565                     {

  566                       // A block has been selected, so we

  567                       // make sure it is one of the ones

  568                       // we highlighted for the user

  569

  570                       if (ids.Contains(per.ObjectId))

  571                       {

  572                         // Leave the selected block alone

  573                         // by removing it from the list

  574

  575                         ids.Remove(per.ObjectId);

  576

  577                         // We then renumber each block in

  578                         // the list

  579

  580                         foreach (ObjectId id in ids)

  581                         {

  582                           bd.Nom.NextObjectNumber(id);

  583                           idc.Add(id);

  584                         }

  585                         RenumberBubbles(db, idc);

  586                         idc.Clear();

  587

  588                         // Let's unhighlight our selected

  589                         // block (renumbering will do this

  590                         // for the others)

  591

  592                         List<ObjectId> redraw =

  593                           new List<ObjectId>(1);

  594                         redraw.Add(per.ObjectId);

  595                         HighlightBubbles(db, redraw, false);

  596

  597                         finished = true;

  598                       }

  599                       else

  600                       {

  601                         ed.WriteMessage(

  602                           "\nBlock selected is not " +

  603                           "numbered with {0}. ",

  604                           pos + bd.BaseNumber

  605                         );

  606                       }

  607                     }

  608                   }

  609                 }

  610               }

  611               RenumberBubbles(db, idc);

  612             }

  613             bd.Nom.Duplicates.Clear();

  614             ed.UpdateScreen();

  615           }

  616         }

  617       }

  618     }

  619

  620     // Take a list of objects and either highlight

  621     // or unhighlight them, depending on the flag

  622

  623     private void HighlightBubbles(

  624       Database db, List<ObjectId> ids, bool highlight)

  625     {

  626       Transaction tr =

  627         db.TransactionManager.StartTransaction();

  628       using (tr)

  629       {

  630         foreach (ObjectId id in ids)

  631         {

  632           Entity ent =

  633             (Entity)tr.GetObject(

  634               id,

  635               OpenMode.ForRead

  636             );

  637           if (highlight)

  638             ent.Highlight();

  639           else

  640             ent.Draw();

  641         }

  642         tr.Commit();

  643       }

  644     }

  645

  646     // Command to create bubbles at points selected

  647     // by the user - loops until cancelled

  648

  649     [CommandMethod("BAP")]

  650     public void BubblesAtPoints()

  651     {

  652       Document doc =

  653         Application.DocumentManager.MdiActiveDocument;

  654       Database db = doc.Database;

  655       Editor ed = doc.Editor;

  656       Autodesk.AutoCAD.ApplicationServices.

  657       TransactionManager tm =

  658         doc.TransactionManager;

  659

  660       Transaction tr =

  661         tm.StartTransaction();

  662       using (tr)

  663       {

  664         // Get the information about the block

  665         // and attribute definitions we care about

  666

  667         BlockTableRecord ms;

  668         ObjectId blockId;

  669         AttributeDefinition ad;

  670         List<AttributeDefinition> other;

  671

  672         if (GetBlock(

  673               db, tr, out ms, out blockId

  674           ))

  675         {

  676           GetBlockAttributes(

  677             tr, blockId, out ad, out other

  678           );

  679

  680           // By default the modelspace is returned to

  681           // us in read-only state

  682

  683           ms.UpgradeOpen();

  684

  685           // Loop until cancelled

  686

  687           bool finished = false;

  688           while (!finished)

  689           {

  690             PromptPointOptions ppo =

  691               new PromptPointOptions("\nSelect point: ");

  692             ppo.AllowNone = true;

  693

  694             PromptPointResult ppr =

  695               ed.GetPoint(ppo);

  696             if (ppr.Status != PromptStatus.OK)

  697               finished = true;

  698             else

  699               // Call a function to create our bubble

  700               CreateNumberedBubbleAtPoint(

  701                 db, ms, tr, ppr.Value,

  702                 blockId, ad, other

  703               );

  704             tm.QueueForGraphicsFlush();

  705             tm.FlushGraphics();

  706           }

  707         }

  708         tr.Commit();

  709       }

  710     }

  711

  712     // Command to create a bubble at the center of

  713     // each of the selected circles

  714

  715     [CommandMethod("BIC")]

  716     public void BubblesInCircles()

  717     {

  718       Document doc =

  719         Application.DocumentManager.MdiActiveDocument;

  720       Database db = doc.Database;

  721       Editor ed = doc.Editor;

  722

  723       // Allow the user to select circles

  724

  725       TypedValue[] tvs =

  726         new TypedValue[1] {

  727             new TypedValue(

  728               (int)DxfCode.Start,

  729               "CIRCLE"

  730             )

  731           };

  732       SelectionFilter sf =

  733         new SelectionFilter(tvs);

  734

  735       PromptSelectionResult psr =

  736         ed.GetSelection(sf);

  737

  738       if (psr.Status == PromptStatus.OK &&

  739           psr.Value.Count > 0)

  740       {

  741         Transaction tr =

  742           db.TransactionManager.StartTransaction();

  743         using (tr)

  744         {

  745           // Get the information about the block

  746           // and attribute definitions we care about

  747

  748           BlockTableRecord ms;

  749           ObjectId blockId;

  750           AttributeDefinition ad;

  751           List<AttributeDefinition> other;

  752

  753           if (GetBlock(

  754                 db, tr, out ms, out blockId

  755             ))

  756           {

  757             GetBlockAttributes(

  758               tr, blockId, out ad, out other

  759             );

  760

  761             // By default the modelspace is returned to

  762             // us in read-only state

  763

  764             ms.UpgradeOpen();

  765

  766             foreach (SelectedObject o in psr.Value)

  767             {

  768               // For each circle in the selected list...

  769

  770               DBObject obj =

  771                 tr.GetObject(o.ObjectId, OpenMode.ForRead);

  772               Circle c = obj as Circle;

  773               if (c == null)

  774                 ed.WriteMessage(

  775                   "\nObject selected is not a circle."

  776                 );

  777               else

  778                 // Call our numbering function, passing the

  779                 // center of the circle

  780                 CreateNumberedBubbleAtPoint(

  781                   db, ms, tr, c.Center,

  782                   blockId, ad, other

  783                 );

  784             }

  785           }

  786           tr.Commit();

  787         }

  788       }

  789     }

  790

  791     // Command to delete a particular bubble

  792     // selected by its index

  793

  794     [CommandMethod("MB")]

  795     public void MoveBubble()

  796     {

  797       Document doc =

  798         Application.DocumentManager.MdiActiveDocument;

  799       Editor ed = doc.Editor;

  800       BubbleData bd =

  801         GetBubbleData(doc);

  802

  803       // Use a helper function to select a valid bubble index

  804

  805       int pos =

  806         GetBubbleNumber(

  807           ed, bd,

  808           "\nEnter number of bubble to move: "

  809         );

  810

  811       if (pos >= bd.BaseNumber)

  812       {

  813         int from = pos - bd.BaseNumber;

  814

  815         pos =

  816           GetBubbleNumber(

  817             ed, bd,

  818             "\nEnter destination position: "

  819           );

  820

  821         if (pos >= bd.BaseNumber)

  822         {

  823           int to = pos - bd.BaseNumber;

  824

  825           ObjectIdCollection ids =

  826             bd.Nom.MoveObject(from, to);

  827

  828           RenumberBubbles(doc.Database, ids);

  829         }

  830       }

  831     }

  832

  833     // Command to delete a particular bubbler,

  834     // selected by its index

  835

  836     [CommandMethod("DB")]

  837     public void DeleteBubble()

  838     {

  839       Document doc =

  840         Application.DocumentManager.MdiActiveDocument;

  841       Database db = doc.Database;

  842       Editor ed = doc.Editor;

  843       BubbleData bd =

  844         GetBubbleData(doc);

  845

  846       // Use a helper function to select a valid bubble index

  847

  848       int pos =

  849         GetBubbleNumber(

  850           ed, bd,

  851           "\nEnter number of bubble to erase: "

  852         );

  853

  854       if (pos >= bd.BaseNumber)

  855       {

  856         // Remove the object from the internal list

  857         // (this returns the ObjectId stored for it,

  858         // which we can then use to erase the entity)

  859

  860         ObjectId id =

  861           bd.Nom.RemoveObject(pos - bd.BaseNumber);

  862         Transaction tr =

  863           db.TransactionManager.StartTransaction();

  864         using (tr)

  865         {

  866           DBObject obj =

  867             tr.GetObject(id, OpenMode.ForWrite);

  868           obj.Erase();

  869           tr.Commit();

  870         }

  871       }

  872     }

  873

  874     // Command to reorder all the bubbles in the drawing,

  875     // closing all the gaps between numbers but maintaining

  876     // the current numbering order

  877

  878     [CommandMethod("RBS")]

  879     public void ReorderBubbles()

  880     {

  881       Document doc =

  882         Application.DocumentManager.MdiActiveDocument;

  883       BubbleData bd =

  884         GetBubbleData(doc);

  885

  886       // Re-order the bubbles - the IDs returned are

  887       // of the objects that need to be renumbered

  888

  889       ObjectIdCollection ids =

  890         bd.Nom.ReorderObjects();

  891

  892       RenumberBubbles(doc.Database, ids);

  893     }

  894

  895     // Command to highlight a particular bubble

  896

  897     [CommandMethod("HLB")]

  898     public void HighlightBubble()

  899     {

  900       Document doc =

  901         Application.DocumentManager.MdiActiveDocument;

  902       Database db = doc.Database;

  903       Editor ed = doc.Editor;

  904       BubbleData bd =

  905         GetBubbleData(doc);

  906

  907       // Use our function to select a valid bubble index

  908

  909       int pos =

  910         GetBubbleNumber(

  911           ed, bd,

  912           "\nEnter number of bubble to highlight: "

  913         );

  914

  915       if (pos >= bd.BaseNumber)

  916       {

  917         ObjectId id =

  918           bd.Nom.GetObjectId(pos - bd.BaseNumber);

  919

  920         if (id == ObjectId.Null)

  921         {

  922           ed.WriteMessage(

  923             "\nNumber is not currently used -" +

  924             " nothing to highlight."

  925           );

  926           return;

  927         }

  928         List<ObjectId> ids =

  929           new List<ObjectId>(1);

  930         ids.Add(id);

  931         HighlightBubbles(db, ids, true);

  932       }

  933     }

  934

  935     // Internal helper function to open and retrieve

  936     // the model-space and the block def we care about

  937

  938     private bool

  939       GetBlock(

  940         Database db,

  941         Transaction tr,

  942         out BlockTableRecord ms,

  943         out ObjectId blockId

  944       )

  945     {

  946       BlockTable bt =

  947         (BlockTable)tr.GetObject(

  948           db.BlockTableId,

  949           OpenMode.ForRead

  950         );

  951

  952       if (!bt.Has(blockName))

  953       {

  954         Document doc =

  955           Application.DocumentManager.MdiActiveDocument;

  956         Editor ed = doc.Editor;

  957         ed.WriteMessage(

  958           "\nCannot find block definition \"" +

  959           blockName +

  960           "\" in the current drawing."

  961         );

  962

  963         blockId = ObjectId.Null;

  964         ms = null;

  965         return false;

  966       }

  967

  968       ms =

  969         (BlockTableRecord)tr.GetObject(

  970           bt[BlockTableRecord.ModelSpace],

  971           OpenMode.ForRead

  972         );

  973

  974       blockId = bt[blockName];

  975

  976       return true;

  977     }

  978

  979     // Internal helper function to retrieve

  980     // attribute info from our block

  981     // (we return the main attribute def

  982     // and then all the "others")

  983

  984     private void

  985       GetBlockAttributes(

  986         Transaction tr,

  987         ObjectId blockId,

  988         out AttributeDefinition ad,

  989         out List<AttributeDefinition> other

  990       )

  991     {

  992       BlockTableRecord blk =

  993         (BlockTableRecord)tr.GetObject(

  994           blockId,

  995           OpenMode.ForRead

  996         );

  997

  998       ad = null;

  999       other =

1000         new List<AttributeDefinition>();

1001

1002       foreach (ObjectId attId in blk)

1003       {

1004         DBObject obj =

1005           (DBObject)tr.GetObject(

1006             attId,

1007             OpenMode.ForRead

1008           );

1009         AttributeDefinition ad2 =

1010           obj as AttributeDefinition;

1011

1012         if (ad2 != null)

1013         {

1014           if (ad2.Tag == attbName)

1015           {

1016             if (ad2.Constant)

1017             {

1018               Document doc =

1019                 Application.DocumentManager.MdiActiveDocument;

1020               Editor ed = doc.Editor;

1021

1022               ed.WriteMessage(

1023                 "\nAttribute to change is constant!"

1024               );

1025             }

1026             else

1027               ad = ad2;

1028           }

1029           else

1030             if (!ad2.Constant)

1031               other.Add(ad2);

1032         }

1033       }

1034     }

1035

1036     // Internal helper function to create a bubble

1037     // at a particular point

1038

1039     private Entity

1040       CreateNumberedBubbleAtPoint(

1041         Database db,

1042         BlockTableRecord btr,

1043         Transaction tr,

1044         Point3d pt,

1045         ObjectId blockId,

1046         AttributeDefinition ad,

1047         List<AttributeDefinition> other

1048       )

1049     {

1050       BubbleData bd =

1051         GetBubbleData(db);

1052

1053       //  Create a new block reference

1054

1055       BlockReference br =

1056         new BlockReference(pt, blockId);

1057

1058       // Add it to the database

1059

1060       br.SetDatabaseDefaults();

1061       ObjectId blockRefId = btr.AppendEntity(br);

1062       tr.AddNewlyCreatedDBObject(br, true);

1063

1064       // Create an attribute reference for our main

1065       // attribute definition (where we'll put the

1066       // bubble's number)

1067

1068       AttributeReference ar =

1069         new AttributeReference();

1070

1071       // Add it to the database, and set its position, etc.

1072

1073       ar.SetDatabaseDefaults();

1074       ar.SetAttributeFromBlock(ad, br.BlockTransform);

1075       ar.Position =

1076         ad.Position.TransformBy(br.BlockTransform);

1077       ar.Tag = ad.Tag;

1078

1079       // Set the bubble's number

1080

1081       int bubbleNumber =

1082         bd.BaseNumber +

1083         bd.Nom.NextObjectNumber(blockRefId);

1084

1085       ar.TextString = bubbleNumber.ToString();

1086       ar.AdjustAlignment(db);

1087

1088       // Add the attribute to the block reference

1089

1090       br.AttributeCollection.AppendAttribute(ar);

1091       tr.AddNewlyCreatedDBObject(ar, true);

1092

1093       // Now we add attribute references for the

1094       // other attribute definitions

1095

1096       foreach (AttributeDefinition ad2 in other)

1097       {

1098         AttributeReference ar2 =

1099           new AttributeReference();

1100

1101         ar2.SetAttributeFromBlock(ad2, br.BlockTransform);

1102         ar2.Position =

1103           ad2.Position.TransformBy(br.BlockTransform);

1104         ar2.Tag = ad2.Tag;

1105         ar2.TextString = ad2.TextString;

1106         ar2.AdjustAlignment(db);

1107

1108         br.AttributeCollection.AppendAttribute(ar2);

1109         tr.AddNewlyCreatedDBObject(ar2, true);

1110       }

1111       return br;

1112     }

1113

1114     // Internal helper function to have the user

1115     // select a valid bubble index

1116

1117     private int

1118       GetBubbleNumber(

1119         Editor ed,

1120         BubbleData bd,

1121         string prompt

1122       )

1123     {

1124       int upper = bd.Nom.GetUpperBound();

1125       if (upper <= 0)

1126       {

1127         ed.WriteMessage(

1128           "\nNo bubbles are currently being managed."

1129         );

1130         return upper;

1131       }

1132

1133       PromptIntegerOptions pio =

1134         new PromptIntegerOptions(prompt);

1135       pio.AllowNone = false;

1136

1137       // Get the limits from our manager object

1138

1139       pio.LowerLimit =

1140         bd.BaseNumber +

1141         bd.Nom.GetLowerBound(false);

1142       pio.UpperLimit =

1143         bd.BaseNumber +

1144         upper;

1145

1146       PromptIntegerResult pir =

1147         ed.GetInteger(pio);

1148       if (pir.Status == PromptStatus.OK)

1149         return pir.Value;

1150       else

1151         return -1;

1152     }

1153

1154     // Internal helper function to open up a list

1155     // of bubbles and reset their number to the

1156     // position in the list

1157

1158     private void RenumberBubbles(

1159       Database db, ObjectIdCollection ids)

1160     {

1161       BubbleData bd =

1162         GetBubbleData(db);

1163       Transaction tr =

1164         db.TransactionManager.StartTransaction();

1165       using (tr)

1166       {

1167         // Get the block information

1168

1169         BlockTableRecord ms;

1170         ObjectId blockId;

1171

1172         if (GetBlock(

1173               db, tr, out ms, out blockId

1174           ))

1175         {

1176           // Open each bubble to be renumbered

1177

1178           foreach (ObjectId bid in ids)

1179           {

1180             if (bid != ObjectId.Null)

1181             {

1182               DBObject obj =

1183                 tr.GetObject(bid, OpenMode.ForRead);

1184               BlockReference br = obj as BlockReference;

1185               if (br != null)

1186               {

1187                 if (br.BlockTableRecord == blockId)

1188                 {

1189                   AttributeCollection ac =

1190                     br.AttributeCollection;

1191

1192                   // Go through its attributes

1193

1194                   foreach (ObjectId aid in ac)

1195                   {

1196                     DBObject obj2 =

1197                       tr.GetObject(aid, OpenMode.ForRead);

1198                     AttributeReference ar =

1199                       obj2 as AttributeReference;

1200

1201                     if (ar.Tag == attbName)

1202                     {

1203                       // Change the one we care about

1204

1205                       ar.UpgradeOpen();

1206

1207                       int bubbleNumber =

1208                         bd.BaseNumber +

1209                         bd.Nom.GetNumber(bid);

1210                       ar.TextString =

1211                         bubbleNumber.ToString();

1212

1213                       break;

1214                     }

1215                   }

1216                 }

1217               }

1218             }

1219           }

1220         }

1221         tr.Commit();

1222       }

1223     }

1224   }

1225

1226   // A generic class for managing groups of

1227   // numbered (and ordered) objects

1228

1229   public class NumberedObjectManager

1230   {

1231     // Store the IDs of the objects we're managing

1232

1233     private List<ObjectId> m_ids;

1234

1235     // A list of free positions in the above list

1236     // (allows numbering gaps)

1237

1238     private List<int> m_free;

1239

1240     // A map of duplicates - blocks detected with

1241     // the number of an existing block

1242

1243     private SortedDictionary<int,List<ObjectId>> m_dups;

1244     public SortedDictionary<int, List<ObjectId>> Duplicates

1245     {

1246       get { return m_dups; }

1247     }

1248

1249     // Constructor

1250

1251     public NumberedObjectManager()

1252     {

1253       m_ids =

1254         new List<ObjectId>();

1255

1256       m_free =

1257         new List<int>();

1258

1259       m_dups =

1260         new SortedDictionary<int, List<ObjectId>>();

1261     }

1262

1263     // Clear the internal lists

1264

1265     public void Clear()

1266     {

1267       m_ids.Clear();

1268       m_free.Clear();

1269       m_dups.Clear();

1270     }

1271

1272     // Does the duplicate list contain anything?

1273

1274     public bool HasDuplicates()

1275     {

1276       return m_dups.Count > 0;

1277     }

1278

1279     // Return the first entry in the ObjectId list

1280     // (specify "true" if you want to skip

1281     // any null object IDs)

1282

1283     public int GetLowerBound(bool ignoreNull)

1284     {

1285       if (ignoreNull)

1286         // Define an in-line predicate to check

1287         // whether an ObjectId is null

1288         return

1289           m_ids.FindIndex(

1290             delegate(ObjectId id)

1291             {

1292               return id != ObjectId.Null;

1293             }

1294           );

1295       else

1296         return 0;

1297     }

1298

1299     // Return the last entry in the ObjectId list

1300

1301     public int GetUpperBound()

1302     {

1303       return m_ids.Count - 1;

1304     }

1305

1306     // Store the specified ObjectId in the next

1307     // available location in the list, and return

1308     // what that is

1309

1310     public int NextObjectNumber(ObjectId id)

1311     {

1312       int pos;

1313       if (m_free.Count > 0)

1314       {

1315         // Get the first free position, then remove

1316         // it from the "free" list

1317

1318         pos = m_free[0];

1319         m_free.RemoveAt(0);

1320         m_ids[pos] = id;

1321       }

1322       else

1323       {

1324         // There are no free slots (gaps in the numbering)

1325         // so we append it to the list

1326

1327         pos = m_ids.Count;

1328         m_ids.Add(id);

1329       }

1330       return pos;

1331     }

1332

1333     // Go through the list of objects and close any gaps

1334     // by shuffling the list down (easy, as we're using a

1335     // List<> rather than an array)

1336

1337     public ObjectIdCollection ReorderObjects()

1338     {

1339       // Create a collection of ObjectIds we'll return

1340       // for the caller to go and update

1341       // (so the renumbering will gets reflected

1342       // in the objects themselves)

1343

1344       ObjectIdCollection ids =

1345         new ObjectIdCollection();

1346

1347       // We'll go through the "free" list backwards,

1348       // to allow any changes made to the list of

1349       // objects to not affect what we're doing

1350

1351       List<int> rev =

1352         new List<int>(m_free);

1353       rev.Reverse();

1354

1355       foreach (int pos in rev)

1356       {

1357         // First we remove the object at the "free"

1358         // position (in theory this should be set to

1359         // ObjectId.Null, as the slot has been marked

1360         // as blank)

1361

1362         m_ids.RemoveAt(pos);

1363

1364         // Now we go through and add the IDs of any

1365         // affected objects to the list to return

1366

1367         for (int i = pos; i < m_ids.Count; i++)

1368         {

1369           ObjectId id = m_ids[pos];

1370

1371           // Only add non-null objects

1372           // not already in the list

1373

1374           if (!ids.Contains(id) &&

1375               id != ObjectId.Null)

1376             ids.Add(id);

1377         }

1378       }

1379

1380       // Our free slots have been filled, so clear

1381       // the list

1382

1383       m_free.Clear();

1384

1385       return ids;

1386     }

1387

1388     // Get the ID of an object at a particular position

1389

1390     public ObjectId GetObjectId(int pos)

1391     {

1392       if (pos < m_ids.Count)

1393         return m_ids[pos];

1394       else

1395         return ObjectId.Null;

1396     }

1397

1398     // Get the position of an ObjectId in the list

1399

1400     public int GetNumber(ObjectId id)

1401     {

1402       if (m_ids.Contains(id))

1403         return m_ids.IndexOf(id);

1404       else

1405         return -1;

1406     }

1407

1408     // Store an ObjectId in a particular position

1409     // (shuffle == true will "insert" it, shuffling

1410     // the remaining objects down,

1411     // shuffle == false will replace the item in

1412     // that slot)

1413

1414     public void NumberObject(

1415       ObjectId id, int index, bool shuffle, bool dups)

1416     {

1417       // If we're inserting into the list

1418

1419       if (index < m_ids.Count)

1420       {

1421         if (shuffle)

1422           // Insert takes care of the shuffling

1423           m_ids.Insert(index, id);

1424         else

1425         {

1426           // If we're replacing the existing item, do

1427           // so and then make sure the slot is removed

1428           // from the "free" list, if applicable

1429

1430           if (!dups ||

1431               m_ids[index] == ObjectId.Null)

1432           {

1433             m_ids[index] = id;

1434             if (m_free.Contains(index))

1435               m_free.Remove(index);

1436           }

1437           else

1438           {

1439             // If we're tracking duplicates, add our new

1440             // object to the duplicate list for that index

1441

1442             if (dups)

1443             {

1444               List<ObjectId> ids;

1445               if (m_dups.ContainsKey(index))

1446               {

1447                 ids = m_dups[index];

1448                 m_dups.Remove(index);

1449               }

1450               else

1451                 ids = new List<ObjectId>();

1452

1453               ids.Add(id);

1454               m_dups.Add(index, ids);

1455             }

1456           }

1457         }

1458       }

1459       else

1460       {

1461         // If we're appending, shuffling is irrelevant,

1462         // but we may need to add additional "free" slots

1463         // if the position comes after the end

1464

1465         while (m_ids.Count < index)

1466         {

1467           m_ids.Add(ObjectId.Null);

1468           m_free.Add(m_ids.LastIndexOf(ObjectId.Null));

1469           m_free.Sort();

1470         }

1471         m_ids.Add(id);

1472       }

1473     }

1474

1475     // Move an ObjectId already in the list to a

1476     // particular position

1477     // (ObjectIds between the two positions will

1478     // get shuffled down automatically)

1479

1480     public ObjectIdCollection MoveObject(

1481       int from, int to)

1482     {

1483       ObjectIdCollection ids =

1484         new ObjectIdCollection();

1485

1486       if (from < m_ids.Count &&

1487           to < m_ids.Count)

1488       {

1489         if (from != to)

1490         {

1491           ObjectId id = m_ids[from];

1492           m_ids.RemoveAt(from);

1493           m_ids.Insert(to, id);

1494

1495           int start = (from < to ? from : to);

1496           int end = (from < to ? to : from);

1497

1498           for (int i = start; i <= end; i++)

1499           {

1500             ids.Add(m_ids[i]);

1501           }

1502         }

1503         // Now need to adjust/recreate "free" list

1504         m_free.Clear();

1505         for (int j = 0; j < m_ids.Count; j++)

1506         {

1507           if (m_ids[j] == ObjectId.Null)

1508             m_free.Add(j);

1509         }

1510       }

1511       return ids;

1512     }

1513

1514     // Remove an ObjectId from the list

1515

1516     public int RemoveObject(ObjectId id)

1517     {

1518       // Check it's non-null and in the list

1519

1520       if (id != ObjectId.Null &&

1521           m_ids.Contains(id))

1522       {

1523         int pos = m_ids.IndexOf(id);

1524         RemoveObject(pos);

1525         return pos;

1526       }

1527       return -1;

1528     }

1529

1530     // Remove the ObjectId at a particular position

1531

1532     public ObjectId RemoveObject(int pos)

1533     {

1534       // Get the ObjectId in the specified position,

1535       // making sure it's non-null

1536

1537       ObjectId id = m_ids[pos];

1538       if (id != ObjectId.Null)

1539       {

1540         // Null out the position and add it to the

1541         // "free" list

1542

1543         m_ids[pos] = ObjectId.Null;

1544         m_free.Add(pos);

1545         m_free.Sort();

1546       }

1547       return id;

1548     }

1549

1550     // Dump out the object list information

1551     // as well as the "free" slots

1552

1553     public void DumpInfo(Editor ed)

1554     {

1555       if (m_ids.Count > 0)

1556       {

1557         ed.WriteMessage("\nIdx ObjectId");

1558

1559         int index = 0;

1560         foreach (ObjectId id in m_ids)

1561           ed.WriteMessage("\n{0} {1}", index++, id);

1562       }

1563

1564       if (m_free.Count > 0)

1565       {

1566         ed.WriteMessage("\n\nFree list: ");

1567

1568         foreach (int pos in m_free)

1569           ed.WriteMessage("{0} ", pos);

1570       }

1571       if (HasDuplicates())

1572       {

1573         ed.WriteMessage("\n\nDuplicate list: ");

1574

1575         foreach (

1576           KeyValuePair<int, List<ObjectId>> dup

1577           in m_dups

1578         )

1579         {

1580           int pos = dup.Key;

1581           List<ObjectId> ids = dup.Value;

1582

1583           ed.WriteMessage("\n{0}    ", pos);

1584

1585           foreach (ObjectId id in ids)

1586           {

1587             ed.WriteMessage("{0} ", id);

1588           }

1589         }

1590       }

1591     }

1592

1593     // Remove the initial n items from the list

1594

1595     public void RebaseList(int start)

1596     {

1597       // First we remove the ObjectIds

1598

1599       for (int i=0; i < start; i++)

1600         m_ids.RemoveAt(0);

1601

1602       // Then we go through the "free" list...

1603

1604       int idx = 0;

1605       while (idx < m_free.Count)

1606       {

1607         if (m_free[idx] < start)

1608           // Remove any that refer to the slots

1609           // we've removed

1610           m_free.RemoveAt(idx);

1611         else

1612         {

1613           // Subtracting the number of slots

1614           // we've removed from the other items

1615           m_free[idx] -= start;

1616           idx++;

1617         }

1618       }

1619

1620       // The duplicate list is more tricky, as we store

1621       // our list of objects against the index

1622       // (we need to remove and re-add each entry)

1623

1624       if (HasDuplicates())

1625       {

1626         // First we get all the indeces (which we use

1627         // to iterate through the list)

1628

1629         SortedDictionary<int, List<ObjectId>>.

1630         KeyCollection kc =

1631           m_dups.Keys;

1632

1633         // Copy the KeyCollection into a list of ints,

1634         // to make it easier to iterate

1635         // (this also allows us to modify the m_dups

1636         // object while we're iterating)

1637

1638         List<int> idxs = new List<int>(kc.Count);

1639         foreach (int pos in kc)

1640           idxs.Add(pos);

1641

1642         foreach (int pos in idxs)

1643         {

1644           List<ObjectId> ids;

1645           m_dups.TryGetValue(pos, out ids);

1646

1647           if (m_dups.ContainsKey(pos - start))

1648             throw new Exception(

1649               ErrorStatus.DuplicateKey,

1650               "\nClash detected - " +

1651               "duplicate list may be corrupted."

1652             );

1653           else

1654           {

1655             // Remove the old entry and add the new one

1656

1657             m_dups.Remove(pos);

1658             m_dups.Add(pos - start, ids);

1659           }

1660         }

1661       }

1662     }

1663   }

1664 }

As for the specific changes...

Lines 19-71 encapsulate the information we need to store per-document, with lines 140-183 allowing retrieval of this data.

Line 102-130 and 185-289 add event handlers to the application. Note that we watch Database.ObjectAppended event to find when numbered objects are added to a drawing, but we do the actually work of renumbering the objects during DocumentCollection.DocumentLockWillChange - the safe place to do so.

A lot of the additional line changes are simply to access the new per-document data via the BubbleData class: 300-302, 316-317, 322, 402, 418, 452-453, 800-801, 811, 813, 821, 823, 826, 843-844, 854,861, 883-884, 890, 904-905, 915, 1050-1051, 1083-1084, 1124, 1140-1141, 1143, 1161-1162, 1208-1209.

Lines 458-616 add the ability to renumber bubbles while scanning the drawing, whether automatically (with no user intervention) or semi-automatically (allowing the user to choose a specific bubble not to renumber). This latter process uses a new HighlightBubbles() function (lines 620-644) to highlight a list of bubbles (it's generic, so could be called HighlightObjects(), thinking about it). This is then also used by the HLB command, replacing the previous implementation (lines 917-931).

We now pass the BubbleData class through to the GetBubbleNumber() function, in line 1120. This is then used in lines 807, 817, 850 & 911.

The NumberedObjectManager class has required infrastructure changes to support duplicates: 1240-1247, 1259-1260, 1269 & 1272-1277. We're using a SortedDictionary to maintain a list of ObjectIds per position. This is a standard "overflow" data structure, and is only added to when a duplicate is found.

DumpInfo() now displays duplicate data in the DMP command (lines 1571-1590).

RebaseList() has been changed to move entries in the duplicate list. This ended up being quite complicated, as we're mapping a list of objects against a position (the dictionary key) and so it's the key that changes when we move the list's base.

To try out this additional functionality, try this:

  1. Open a drawing with a bunch of bubbles, or create new ones.
  2. Copy & paste these bubbles one or more times, to seed the drawing with duplicate-numbered objects.
  3. Load the application and run the LNS command to understand the numbers in the drawing. Try the different options for renumbering duplicates (Automatically, Individually or Not).
  4. Next try copying and pasting again, once the numbering system is active, and see how the copied numbers are changed.
  5. INSERT a few bubbles: new blocks will also be renumbered - even if the user selects a particular number via the attribute editing dialog - but that's somewhat inevitable with this approach.

OK - now that we're up at nearly 1700 lines of code, it's getting time to bring this series to a close... (or at least to stop including the entire code in each post. :-)

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e200e5522ac37f8833

Listed below are links to weblogs that reference An automatic numbering system for AutoCAD blocks using .NET - Part 4:

blog comments powered by Disqus

Feed/Share

10 Random Posts