Consumption seems to be a relevant topic, coming after the long Easter weekend here in Switzerland… I was laid up with gastric flu on Monday: after having consumed large amounts of food with friends on Saturday and Sunday, I ended up eating nothing for the whole day. I managed to put the finishing touches on the previous post in this series, but beyond that I was pretty useless.
Anyway, back to the point. We’ve looked at the trend of moving to the cloud, and the steps for creating and implementing a RESTful web-service using the new ASP.NET Web API. Now we’re going to look at how we can call these web-services from within AutoCAD and use the data they provide to generate database geometry.
Let’s start with looking at why we chose JSON for our transport protocol…
RESTful web-services don’t mandate the use of JSON – we could also be returning XML, or even HTML if that made sense – but it’s a fairly common data protocol, these days. I chose it because it was the default served up by the ASP.NET Web API, and it’s generally a little less verbose than XML. Some web-services allow you to chose between JSON and XML (and even YAML, apparently, although I ‘m not familiar with that), but we’ve kept it simple by only returning JSON.
JSON is really easy to parse from Javascript, by all accounts, but we’re going to use C#. So what’s the best way to proceed? There are a couple of choices – perhaps more, but I know of a couple: we could map our JSON to physical classes and serialize into them, which wouldn’t be overly complex, in our case, or we could make use of the dynamic capabilities of .NET 4 to provide access to the data.
I ended up choosing the latter route – mainly for simplicity – although I suspect the former would probably end up being marginally quicker, performance-wise. The concept of using dynamic capabilities in this way was originally presented here, and then refined here and here.
Here’s the code I borrowed from the last of the above links for JSON serialization and access:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Dynamic;
using System.Linq;
using System.Text;
using System.Web.Script.Serialization;
internal sealed class DynamicJsonConverter : JavaScriptConverter
{
public override object Deserialize(
IDictionary<string, object> dictionary, Type type,
JavaScriptSerializer serializer
)
{
if (dictionary == null)
throw new ArgumentNullException("dictionary");
return
type == typeof(object) ?
new DynamicJsonObject(dictionary) :
null;
}
public override IDictionary<string, object> Serialize(
object obj, JavaScriptSerializer serializer
)
{
throw new NotImplementedException();
}
public override IEnumerable<Type> SupportedTypes
{
get {
return
new ReadOnlyCollection<Type>(
new List<Type>(new[] { typeof(object) })
);
}
}
#region Nested type: DynamicJsonObject
private sealed class DynamicJsonObject : DynamicObject
{
private readonly IDictionary<string, object> _dictionary;
public DynamicJsonObject(IDictionary<string, object> dictionary)
{
if (dictionary == null)
throw new ArgumentNullException("dictionary");
_dictionary = dictionary;
}
public override string ToString()
{
var sb = new StringBuilder("{");
ToString(sb);
return sb.ToString();
}
private void ToString(StringBuilder sb)
{
var firstInDictionary = true;
foreach (var pair in _dictionary)
{
if (!firstInDictionary)
sb.Append(",");
firstInDictionary = false;
var value = pair.Value;
var name = pair.Key;
if (value is string)
{
sb.AppendFormat("{0}:\"{1}\"", name, value);
}
else if (value is IDictionary<string, object>)
{
new
DynamicJsonObject(
(IDictionary<string, object>)value
).ToString(sb);
}
else if (value is ArrayList)
{
sb.Append(name + ":[");
var firstInArray = true;
foreach (var arrayValue in (ArrayList)value)
{
if (!firstInArray)
sb.Append(",");
firstInArray = false;
if (arrayValue is IDictionary<string, object>)
new DynamicJsonObject(
(IDictionary<string, object>)arrayValue
).ToString(sb);
else if (arrayValue is string)
sb.AppendFormat("\"{0}\"", arrayValue);
else
sb.AppendFormat("{0}", arrayValue);
}
sb.Append("]");
}
else
{
sb.AppendFormat("{0}:{1}", name, value);
}
}
sb.Append("}");
}
public override bool TryGetMember(
GetMemberBinder binder, out object result
)
{
if (!_dictionary.TryGetValue(binder.Name, out result))
{
// return null to avoid exception.
// Caller can check for null this way...
result = null;
return true;
}
var dictionary = result as IDictionary<string, object>;
if (dictionary != null)
{
result = new DynamicJsonObject(dictionary);
return true;
}
var arrayList = result as ArrayList;
if (arrayList != null && arrayList.Count > 0)
{
if (arrayList[0] is IDictionary<string, object>)
result =
new List<object>(
arrayList.Cast<IDictionary<string, object>>().Select(
x => new DynamicJsonObject(x)
)
);
else
result = new List<object>(arrayList.Cast<object>());
}
return true;
}
}
#endregion
}
Here’s our client code that makes use of it:
using Autodesk.AutoCAD.ApplicationServices.Core;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System.Web.Script.Serialization;
using System.Net;
using System.IO;
using System.Diagnostics;
using System.Collections.Generic;
using System;
namespace Packing
{
public class Commands
{
[CommandMethod("AGCWS")]
public static void ApollonianGasketWebService()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Ask the user to select a circle which we'll then
// fill with out Apollonian gasket
PromptEntityOptions peo =
new PromptEntityOptions("\nSelect circle");
peo.AllowNone = true;
peo.SetRejectMessage("\nMust be a circle.");
peo.AddAllowedClass(typeof(Circle), false);
PromptEntityResult per = ed.GetEntity(peo);
// If none is selected, we'll just use a default size
if (per.Status != PromptStatus.OK &&
per.Status != PromptStatus.None
)
return;
// Also prompt for the recursion level of our algorithm
PromptIntegerOptions pio =
new PromptIntegerOptions("\nEnter number of steps");
pio.LowerLimit = 1;
pio.UpperLimit = 11;
pio.DefaultValue = 8;
pio.UseDefaultValue = true;
PromptIntegerResult pir = ed.GetInteger(pio);
if (pir.Status != PromptStatus.OK)
return;
int steps = pir.Value;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
// Start by creating layers for each step/level
CreateLayers(db, tr, steps + 1);
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId, OpenMode.ForWrite
);
// If the user selected a circle, use it
Circle cir;
if (per.Status != PromptStatus.None && per.ObjectId != null)
{
cir =
tr.GetObject(per.ObjectId, OpenMode.ForRead) as Circle;
}
else
{
// Otherwise create a new one at the default location
cir = new Circle(Point3d.Origin, Vector3d.ZAxis, 10);
btr.AppendEntity(cir);
tr.AddNewlyCreatedDBObject(cir, true);
}
// Let's time the WS operation
Stopwatch sw = Stopwatch.StartNew();
dynamic res = ApollonianPackingWs(cir.Radius, steps, true);
sw.Stop();
doc.Editor.WriteMessage(
"\nWeb service call took {0} seconds.",
sw.Elapsed.TotalSeconds
);
// We're going to offset our geometry - which will be
// scaled appropriately by the web service - to fit
// within the selected/created circle
Vector3d offset =
cir.Center - new Point3d(cir.Radius, cir.Radius, 0.0);
// Go through our "dynamic" list, accessing each property
// dynamically
foreach (dynamic tup in res)
{
double curvature = System.Math.Abs((double)tup.Curvature);
if (1.0 / curvature > 0.0)
{
Circle c =
new Circle(
new Point3d(
(double)tup.X, (double)tup.Y, 0.0
) + offset,
Vector3d.ZAxis,
1.0 / curvature
);
// The Layer (and therefore the colour) will be based
// on the "level" of each sphere
c.Layer = (tup.Level + 1).ToString();
btr.AppendEntity(c);
tr.AddNewlyCreatedDBObject(c, true);
}
}
tr.Commit();
doc.Editor.WriteMessage(
"\nCreated {0} circles.", res.Count
);
}
}
[CommandMethod("AGSWS")]
public static void ApollonianGasket()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Rather than select a sphere (which may not have history)
// simply ask for the center and radius of our spherical
// volume
PromptPointOptions ppo =
new PromptPointOptions("\nSelect center point");
ppo.AllowNone = false;
PromptPointResult ppr = ed.GetPoint(ppo);
if (ppr.Status != PromptStatus.OK)
return;
Point3d center = ppr.Value;
ppo.BasePoint = center;
ppo.Message = "\nSelect point on radius";
ppo.UseBasePoint = true;
ppo.UseDashedLine = true;
ppr = ed.GetPoint(ppo);
if (ppr.Status != PromptStatus.OK)
return;
// Calculate the radius and the offset for our geometry
double radius = (ppr.Value - center).Length;
Vector3d offset = center - Point3d.Origin;
// Prompt for the recursion level of our algorithm
PromptIntegerOptions pio =
new PromptIntegerOptions("\nEnter number of steps");
pio.LowerLimit = 1;
pio.UpperLimit = 11;
pio.DefaultValue = 8;
pio.UseDefaultValue = true;
PromptIntegerResult pir = ed.GetInteger(pio);
if (pir.Status != PromptStatus.OK)
return;
int steps = pir.Value;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
// Start by creating layers for each step/level
CreateLayers(db, tr, steps + 1);
// We created our Apollonian gasket in the current space,
// for our 3D version we'll make sure it's in modelspace
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId, OpenMode.ForRead
);
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite
);
// Let's time the WS operation
Stopwatch sw = Stopwatch.StartNew();
dynamic res = ApollonianPackingWs(radius, steps, false);
sw.Stop();
doc.Editor.WriteMessage(
"\nWeb service call took {0} seconds.",
sw.Elapsed.TotalSeconds
);
// Go through our "dynamic" list, accessing each property
// dynamically
foreach (dynamic tup in res)
{
double rad = System.Math.Abs((double)tup.Radius);
if (rad > 0.0)
{
Solid3d s = new Solid3d();
s.CreateSphere(rad);
Point3d cen =
new Point3d(
(double)tup.X, (double)tup.Y, (double)tup.Z
);
Vector3d disp = cen - Point3d.Origin;
s.TransformBy(Matrix3d.Displacement(disp + offset));
// The Layer (and therefore the colour) will be based
// on the "level" of each sphere
s.Layer = tup.Level.ToString();
btr.AppendEntity(s);
tr.AddNewlyCreatedDBObject(s, true);
}
}
tr.Commit();
doc.Editor.WriteMessage(
"\nCreated {0} spheres.", res.Count
);
}
}
private static dynamic ApollonianPackingWs(
double p, int numSteps, bool circles
)
{
string json = null;
// Call our web-service synchronously (this isn't ideal, as
// it blocks the UI thread)
HttpWebRequest request =
WebRequest.Create(
"http://localhost:64114/api/" +
(circles ? "circles" : "spheres") +
"/" + p.ToString() +
"/" + numSteps.ToString()
) as HttpWebRequest;
// Get the response
using (
HttpWebResponse response =
request.GetResponse() as HttpWebResponse
)
{
// Get the response stream
StreamReader reader =
new StreamReader(response.GetResponseStream());
// Extract our JSON results
json = reader.ReadToEnd();
}
if (!String.IsNullOrEmpty(json))
{
// Use our dynamic JSON converter to populate/return
// our list of results
var serializer = new JavaScriptSerializer();
serializer.RegisterConverters(
new[] { new DynamicJsonConverter() }
);
// We need to make sure we have enough space for our JSON,
// as the default limit may well get exceeded
serializer.MaxJsonLength = 50000000;
return serializer.Deserialize(json, typeof(List<object>));
}
return null;
}
// A helper method to create layers for each of our
// levels/steps
private static void CreateLayers(
Database db, Transaction tr, int layers
)
{
LayerTable lt =
(LayerTable)tr.GetObject(
db.LayerTableId, OpenMode.ForWrite
);
for (short i = 1; i <= layers; i++)
{
// Each layer will simply be named after its index
string name = i.ToString();
if (!lt.Has(name))
{
// Our layer will have the color-index of our
// index, too
LayerTableRecord ltr = new LayerTableRecord();
ltr.Color =
Autodesk.AutoCAD.Colors.Color.FromColorIndex(
Autodesk.AutoCAD.Colors.ColorMethod.ByAci, i
);
ltr.Name = name;
// Add the layer to the layer table and transaction
lt.Add(ltr);
tr.AddNewlyCreatedDBObject(ltr, true);
}
}
}
}
}
A few comments on this implementation…
This code is still calling our local web-service, but it will be a one-string change to have it connect to our cloud-based service (which will simply have a different URL).
Right now the code uses a synchronous web-service call, which has the unfortunate behaviour of locking the UI thread for the duration of the call (and actually of the processing of the data – we’re not checking in to process events during those loops, either).
Asynchronous calling is clearly the way to go, at this stage, with respect to web-services. Even if we have to wait for AutoCAD to be ready to generate all our geometry, blocking the UI thread means AutoCAD becomes unresponsive and also won’t check for the user cancelling the operation.
I started implementing code based on the Async CTP to do this, but realised (and I think I have this right) that the capabilities that are likely to be introduced in .NET 4.5 – and can be used now in VS11 – work with different types/extension methods than those in the Async CTP. Rather than publish code that I already know will change, I’ve chosen to come back to this later, with some code that works inside VS11 rather than VS2010.
Let’s see the code in action. Here are the results at the command-line when we run AGCWS and AGSWS, accepting the default number of recursion steps for each:
Command: AGCWS
Select circle:
Enter number of steps <8>:
Web service call took 4.7617224 seconds.
Created 6549 circles.
Command: AGSWS
Select center point:
Select point on radius:
Enter number of steps <8>:
Web service call took 3.640632 seconds.
Created 12119 spheres.
And here are the graphical results:
Now that we have the various parts of our application working together on a local system, over the coming posts we’ll move our web-service to the cloud and update the client code to access it there.