Turning AutoCAD into an RSS reader with F#

OK, OK, you are probably thinking "why would anyone ever want to use AutoCAD as an RSS reader?". The answer is, of course, "they wouldn't". The point of the next few posts is not actually to enable AutoCAD to be used to read RSS, but to show how it is possible to use F# and .NET to extract information from RSS feeds and create corresponding AutoCAD entities.

The reason I came onto this subject will also become more clear when you see my next post: I have been researching Asynchronous Workflows in F# - an uber-cool mechanism for managing concurrent, asynchronous tasks - and this seemed like a valid place to start. The problem I was looking for was one where I could simultaneously query and manipulate data from multiple sources, and then use that data to create AutoCAD entities. So, ultimately, the choice of RSS was both logical and completely irrelevant. ๐Ÿ™‚

Today I'm going to present code that works synchronously: in a single thread we are going to query website after website to download individual RSS feeds and to process them, extracting information on the various posts listed in the RSS, and create HyperLink objects in AutoCAD attached to DBText entities. These will be laid out such that - if you really, really wanted to - you could use these entities to open the various posts in your internet browser.

The reason I chose F# was really the ability to succinctly launch and coordinate asynchronous tasks - something you'll see in the next post, of course. While I could have used C# or VB.NET, F# is also well suited to dealing with lists of data - such as we'll be extracting from the various RSS feeds.

I used F# 1.9.3.7 to run this code: you will certainly need this version to run the code in the following post, as the asynchronous HTTP request functionality is new to the 1.9.3.7 release.

A few additional notes on the implementation... The below code somehow manages to support various RSS standards: Atom, RSS 1.0, RSS 2.0. But some of it feels like a bit of a "hack". The code queries for the titles, links and descriptions contained in each feed, and does some programmatic manipulation to end up - in the cases I've tested - with equal numbers of each. Feeds that use Feedburner, for instance, contain various types of link, which made this very tricky, but the below code appears to work for most cases. The point of this exercise is not to implement an "all singing, all dancing" implementation for RSS consumption: I simply did what was needed to get a number of different blogs working. If a particular feed you add doesn't work, you will just get a single entry created inside AutoCAD. Please don't expect me to debug why it doesn't work for that feed, as that was never the point of this exercise (and I wasted far too long getting to this point, believe me :-).

Here's the F# code:

// Use lightweight F# syntax

#light

// Declare a specific namespace and module name

module MyNamespace.MyApplication

// Import managed assemblies

#I @"C:\Program Files\Autodesk\AutoCAD 2008"

#r "acdbmgd.dll"

#r "acmgd.dll"

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.Geometry

open System.Xml

open System.Collections

open System.Collections.Generic

open System.IO

open System.Net

open Microsoft.FSharp.Control.CommonExtensions

// The RSS feeds we wish to get. The first two values are

// only used if our code is not able to parse the feed's XML

let feeds =

  [ ("Through the Interface",

    "http://blogs.autodesk.com/through-the-interface",

    "http://through-the-interface.typepad.com/through_the_interface/rss.xml");

    ("Don Syme's F# blog",

    "http://blogs.msdn.com/dsyme/",

    "http://blogs.msdn.com/dsyme/rss.xml");

    ("Shaan Hurley's Between the Lines",

    "http://autodesk.blogs.com/between_the_lines",

    "http://autodesk.blogs.com/between_the_lines/rss.xml");

    ("Scott Sheppard's It's Alive in the Lab",

    "http://blogs.autodesk.com/labs",

    "http://labs.blogs.com/its_alive_in_the_lab/rss.xml");

    ("Lynn Allen's Blog",

    "http://blogs.autodesk.com/lynn",

    "http://lynn.blogs.com/lynn_allens_blog/index.rdf");

    ("Heidi Hewett's AutoCAD Insider",

    "http://blogs.autodesk.com/autocadinsider",

    "http://heidihewett.blogs.com/my_weblog/index.rdf") ]

// Fetch the contents of a web page, synchronously

let httpSync (url:string) =

  let req = WebRequest.Create(url)

  use resp = req.GetResponse()

  use stream = resp.GetResponseStream()

  use reader = new StreamReader(stream)

  reader.ReadToEnd()

// Load an RSS feed's contents into an XML document object

// and use it to extract the titles and their links

// Hopefully these always match (this could be coded more

// defensively)

let titlesAndLinks (name, url, xml) =

  let xdoc = new XmlDocument()

  xdoc.LoadXml(xml)

  let titles =

    [ for n in xdoc.SelectNodes("//*[name()='title']")

        -> n.InnerText ]

  let links =

    [ for n in xdoc.SelectNodes("//*[name()='link']") ->

        let inn = n.InnerText

        if  inn.Length > 0 then

          inn

        else

          let href = n.Attributes.GetNamedItem("href").Value

          let rel = n.Attributes.GetNamedItem("rel").Value

          if href.Contains("feedburner") then

              ""

          else

            href ]

  let descs =

    [ for n in xdoc.SelectNodes

        ("//*[name()='description' or name()='content' or name()='subtitle']")

          -> n.InnerText ]

  // A local function to filter out duplicate entries in

  // a list, maintaining their current order.

  // Another way would be to use:

  //    Set.of_list lst |> Set.to_list

  // but that results in a sorted (probably reordered) list.

  let rec nub lst =

    match lst with

    | a::[] -> [a]

    | a::b ->

      if a = List.hd b then

        nub b

      else

        a::nub b

    | [] -> []

  // Filter the links to get (hopefully) the same number

  // and order as the titles and descriptions

  let real = List.filter (fun (x:string) -> x.Length > 0) 

  let lnks = real links |> nub

  // Return a link to the overall blog, if we don't have

  // the same numbers of titles, links and descriptions

  let lnum = List.length lnks

  let tnum = List.length titles

  let dnum = List.length descs

  if tnum = 0 || lnum = 0 || lnum <> tnum || dnum <> tnum then

    [(name,url,url)]

  else

    List.zip3 titles lnks descs

// For a particular (name,url) pair,

// create an AutoCAD HyperLink object

let hyperlink (name,url,desc) =

  let hl = new HyperLink()

  hl.Name <- url

  hl.Description <- desc

  (name, hl)

// Download an RSS feed and return AutoCAD HyperLinks for its posts

let hyperlinksSync (name, url, feed) =

  let xml = httpSync feed

  let tl = titlesAndLinks (name, url, xml)

  List.map hyperlink tl

// Now we declare our command

[<CommandMethod("rss")>]

let createHyperlinksFromRss() =

  // Let's get the usual helpful AutoCAD objects

  let doc =

    Application.DocumentManager.MdiActiveDocument

  let db = doc.Database

  // "use" has the same effect as "using" in C#

  use tr =

    db.TransactionManager.StartTransaction()

  // Get appropriately-typed BlockTable and BTRs

  let bt =

    tr.GetObject

      (db.BlockTableId,OpenMode.ForRead)

    :?> BlockTable

  let ms =

    tr.GetObject

      (bt.[BlockTableRecord.ModelSpace],

      OpenMode.ForWrite)

    :?> BlockTableRecord

  // Add text objects linking to the provided list of

  // HyperLinks, starting at the specified location

  // Note the valid use of tr and ms, as they are in scope

  let addTextObjects pt lst =

    // Use a for loop, as we care about the index to

    // position the various text items

    let len = List.length lst

    for index = 0 to len - 1 do

      let txt = new DBText()

      let (name:string,hl:HyperLink) = List.nth lst index

      txt.TextString <- name

      let offset =

        if index = 0 then

          0.0

        else

          1.0

      // This is where you can adjust:

      //  the initial outdent (x value)

      //  and the line spacing (y value)

      let vec =

        new Vector3d

          (1.0 * offset,

          -0.5 * (Int32.to_float index),

          0.0)

      let pt2 = pt + vec

      txt.Position <- pt2

      ms.AppendEntity(txt) |> ignore

      tr.AddNewlyCreatedDBObject(txt,true)

      txt.Hyperlinks.Add(hl) |> ignore

  // Here's where we use the varous functions

  // we've defined

  let links =

    List.map hyperlinksSync feeds

  // Add the resulting objects to the model-space 

  let len = List.length links

  for index = 0 to len - 1 do

    // This is where you can adjust:

    //  the column spacing (x value)

    //  the vertical offset from origin (y axis)

    let pt =

      new Point3d

        (15.0 * (Int32.to_float index),

        30.0,

        0.0)

    addTextObjects pt (List.nth links index)

  tr.Commit()

Here's a portion of what gets created when you run the "rss" command:

AutoCAD does RSS   

That's it for today - in the next post we'll look at how to use asynchronous workflows to run RSS extraction tasks in parallel.

5 responses to “Turning AutoCAD into an RSS reader with F#”

  1. Fernando Malard Avatar

    Hi Kean,

    I don't think this is useless. One idea is to use a RSS feed to update some dynamic fields inside a DWG such as: project status, client information or any other information that may change during the project development cycle.

    Another use would be a "ToDo" list that the CAD Manager can setup for the week or daily tasks to accomplish for all drafters.

    Sometimes the technical solution appears before the idea! ๐Ÿ™‚

    Regards,

  2. Hi Fernando,

    Thanks - although useless wasn't exactly the word I used. ๐Ÿ˜‰

    AutoCAD as a tool for reading blogs isn't exactly optimal, but you're absolutely right that RSS is much more than just for syndicating blogs: it's also a great transport for project data (for instance). Aside from that the technique shown is applicable to all kinds of data that can be accessed via HTTP.

    Regards,

    Kean

  3. That's very sweet F# code Kean! I'll link across to this from my blog (tomorrow - 'tis late at night here in Cambridge). I can't wait for the asynchronous workflow version of this.

    Don

  4. TheLazyDogsBack Avatar

    Cool stuff!

    Instead of:

    match lst with
    | a::[] -> [a]
    | a::b ->
    if a = List.hd b then
    nub b
    else
    a::nub b

    I think this is more clear:

    match lst with
    | a::[] -> [a]
    | dup0::dup1::_ when dup0=dup1 -> nub dup1
    | a::nub b

  5. TheLazyDogsBack Avatar

    Doh! - last line:

    | _ -> a::nub b

Leave a Reply to Kean Walmsley Cancel reply

Your email address will not be published. Required fields are marked *