A quick recap on the series so far… after introducing the benefits of moving application functionality to the cloud, we spent a couple of posts taking algorithms we’d previously hosted directly inside an AutoCAD and moving them to live behind a locally-hosted web-service. We then took a step back and talked about some issues around architecting applications for the cloud before we went on to make use of our web-service inside AutoCAD. And, most recently, we looked at how to move our web-service from a local system to be hosted in the cloud.
And a quick aside, as I watched it as I was finishing up this post… if you’re interested in the industry driving the software industry’s move to the cloud – especially related to the end of Moore’s Law – I strongly recommend watching this video of Herb Sutter’s recent “Welcome to the Jungle” presentation (which is based on this excellent article of Herb’s). Watching the full 2-hour video will require (free) registration on the site, but I found it well worth the trade.
Over the coming few posts, we’re going to look at consuming our web-service in a few different scenarios. In today’s post, we’re going to focus on the classic scenario of consuming the data inside AutoCAD (which will give us feature parity with where we were before we started the series, in a certain sense :-). This is really a minor adaptation of the approach we saw previously to consume a locally-hosted web-service. In the following few posts, we’re going to have some real fun: just to show the platform-related benefits from shifting platform-dependent code to the cloud, we’re going to consume our web-service in a Unity3D scene and then in an Android-hosted OpenGL ES-based mobile app. We’ll see where we go, after that… :-)
As mentioned previously, we really only need to change one line of our C# code to call our web-service in the cloud rather than on our local system – the one containing the URL – but I went ahead and made the code a bit more robust, while I was at it. I really had wanted to call the web-service asynchronously – as that’s certainly the recommended approach, these days, irrespective of the platform you’re working on – but as the simplified async capabilities have not yet been baked into the .NET Framework (and look likely to change from the posted Async CTP), I’ve kept the code synchronous, for now.
I did put a bit more error-handling in place, to help us report an error gracefully in case we can’t access the web-service (which we can test by changing the URL to something that doesn’t exist, to see how the code copes with that) or the web-service reports an error (which we’ll test by choosing 11 recursion steps in our client code, as we’re only supporting up to 10 in our cloud-based implementation).
Oh yes, and I did adjust the code to make use of the shortened labels for our JSON data fields (“R” instead of “Radius”, “C” instead of “Curvature”, “L” instead of “Level”), of course. The dependent JSON-related code remains the same as shown previously.
Here are our updated AutoCAD commands and their supporting functions:
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.Web;
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(ed, cir.Radius, steps, true);
sw.Stop();
if (res == null)
return;
ed.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.C);
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.L + 1).ToString();
btr.AppendEntity(c);
tr.AddNewlyCreatedDBObject(c, true);
}
}
tr.Commit();
ed.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(ed, radius, steps, false);
sw.Stop();
if (res == null)
return;
ed.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.R);
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.L.ToString();
btr.AppendEntity(s);
tr.AddNewlyCreatedDBObject(s, true);
}
}
tr.Commit();
ed.WriteMessage(
"\nCreated {0} spheres.", res.Count
);
}
}
private static dynamic ApollonianPackingWs(
Editor ed, 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://apollonian.cloudapp.net/api/" +
(circles ? "circles" : "spheres") +
"/" + p.ToString() +
"/" + numSteps.ToString()
) as HttpWebRequest;
// Get the response
try
{
using (
HttpWebResponse response =
request.GetResponse() as HttpWebResponse
)
{
// Get the response stream
StreamReader reader =
new StreamReader(response.GetResponseStream());
// Extract our JSON results
json = reader.ReadToEnd();
}
}
catch (System.Exception ex)
{
ed.WriteMessage(
"\nCannot access web-service: {0}", ex.Message
);
}
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 be 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);
}
}
}
}
}
If we run the code inside AutoCAD – having changed the code to try to call an incorrect URL – we can see the error is handled properly and reported to the user:
Command: AGCWS
Select circle:
Enter number of steps <8>:
Cannot access web-service: The remote name could not be resolved:
'napollonian.cloudapp.net'
If we then change the URL back and choose a recursion level of 11 – which isn’t supported by our service – we see that this is also handled:
Command: AGCWS
Select circle:
Enter number of steps <8>: 11
Cannot access web-service: The remote server returned an error:
(400) Bad Request.
And, of course, we can see the code work appropriately for the range of calls from levels 1 to 10 for both circles and spheres:
Command: AGCWS
Select circle:
Enter number of steps <8>: 1
Web service call took 4.1195031 seconds.
Created 4 circles.
Command: AGCWS
Select circle:
Enter number of steps <8>: 2
Web service call took 0.0920637 seconds.
Created 10 circles.
Command: AGCWS
Select circle:
Enter number of steps <8>: 3
Web service call took 0.0873717 seconds.
Created 28 circles.
Command: AGCWS
Select circle:
Enter number of steps <8>: 4
Web service call took 0.0999509 seconds.
Created 82 circles.
Command: AGCWS
Select circle:
Enter number of steps <8>: 5
Web service call took 0.1269908 seconds.
Created 244 circles.
Command: AGCWS
Select circle:
Enter number of steps <8>: 6
Web service call took 0.1969812 seconds.
Created 729 circles.
Command: AGCWS
Select circle:
Enter number of steps <8>: 7
Web service call took 0.3472026 seconds.
Created 2184 circles.
Command: AGCWS
Select circle:
Enter number of steps <8>:
Web service call took 0.9152223 seconds.
Created 6549 circles.
Command: AGCWS
Select circle:
Enter number of steps <8>: 9
Web service call took 2.3909918 seconds.
Created 19644 circles.
Command: AGCWS
Select circle:
Enter number of steps <8>: 10
Web service call took 6.9777157 seconds.
Created 58929 circles.
Command: AGSWS
Select center point:
Select point on radius:
Enter number of steps <8>: 1
Web service call took 0.1782483 seconds.
Created 9 spheres.
Command: AGSWS
Select center point:
Select point on radius:
Enter number of steps <8>: 2
Web service call took 0.1006492 seconds.
Created 29 spheres.
Command: AGSWS
Select center point:
Select point on radius:
Enter number of steps <8>: 3
Web service call took 0.1020872 seconds.
Created 89 spheres.
Command: AGSWS
Select center point:
Select point on radius:
Enter number of steps <8>: 4
Web service call took 0.126641 seconds.
Created 299 spheres.
Command: AGSWS
Select center point:
Select point on radius:
Enter number of steps <8>: 5
Web service call took 0.2640874 seconds.
Created 989 spheres.
Command: AGSWS
Select center point:
Select point on radius:
Enter number of steps <8>: 6
Web service call took 0.5017233 seconds.
Created 2837 spheres.
Command: AGSWS
Select center point:
Select point on radius:
Enter number of steps <8>: 7
Web service call took 0.9456658 seconds.
Created 6635 spheres.
Command: AGSWS
Select center point:
Select point on radius:
Enter number of steps <8>:
Web service call took 1.9347648 seconds.
Created 12119 spheres.
Command: AGSWS
Select center point:
Select point on radius:
Enter number of steps <8>: 9
Web service call took 2.3434653 seconds.
Created 16187 spheres.
Command: AGSWS
Select center point:
Select point on radius:
Enter number of steps <8>: 10
Web service call took 2.6593427 seconds.
Created 18107 spheres.
The timings shown are genuine but only reflect the call to the web-service – creation of the geometry will have made the commands take much longer to complete, of course. But it gives a reasonable idea of any overhead associated with transferring data from a remote service – we’re talking a few of seconds, at the most, for the majority of cases (the outlier being level 10 in 2D, which returns nearly 60K circles).
This number will have come down, given the fact we’re transferring so much less data (we’ve reduced the precision of each double down to four decimal places as well as the truncation of field names in the JSON data). So it wouldn’t be fair to compare these numbers with those we saw when looking at the local web-service, for instance.
Here are these results graphically:
Looking at this image, I’ve realised that the 3D results have new layers shown in an additional, new colour (which is based on the level), while in the 2D results that’s reversed (each new level takes red as its layer’s color). We could adjust for this in our client code, of course (or just change the code on the server), but I don’t see it as a significant issue, at this stage.
Just for fun, here are these results shown in a 3D view with the realistic visual style:
That’s it for today’s post. Next time, we’ll take a look at consuming spheres from our web-service in a Unity3D scene and then after that we’ll implement a simple model-viewer for the Android platform.