Saturday, March 27, 2010

jQuery zoom addin with image map support

In the past I fiddled around with the moozoom plugin to add support for image maps and on a new project I needed the same thing. But this new project uses a lot of jQuery stuff and jQuery and mootools don’t play together particularly well. Although there are a lot of image zooming plugins available for jQuery, none of them did exactly what I needed. I just wanted image zooming, panning and support for image maps. Anyway, this is what I came up with. This is my first adventure in the world of jQuery plugins, so it may not be a perfect implementation but it works for me…

Usage is pretty simple

$('.selector').zoomable();

where .selector is the selector for your image. View an example here (that page will also be the place I put all new versions of this code). You can now also programmatically zoom in and out using $('.selector').zoomable('zoomIn'); and $('.selector').zoomable('zoomOut'); which you can hook up to buttons for users without a mouse wheel

Here’s the code (requires jQuery and jQuery UI)

(function ($) {
  $.fn.zoomable = function (method) {
 
    return this.each(function (index, value) {
      // restore data, if there is any for this element
      var zoomData;
      if ($(this).data('zoomData') == null) {
        zoomData = {
          busy: false,
          x_fact: 1.2,
          currentZoom: 1,
          originalMap: null,
          currentX: 0,
          currentY: 0
        };
        $(this).data('zoomData', zoomData);
      }
      else
        zoomData = $(this).data('zoomData');
      
      var init = function() {
        if (value.useMap != "") {
          var tempOriginalMap = document.getElementById(value.useMap.substring(1));
          zoomData.originalMap = tempOriginalMap.cloneNode(true);
          // for IE6, we need to manually copy the areas' coords
          for (var i = 0; i < zoomData.originalMap.areas.length; i++)
            zoomData.originalMap.areas[i].coords = tempOriginalMap.areas[i].coords;
        }

        $(value).css('position', 'relative').css('left', '0').css('top', 0).css('margin', '0');

        $(value).draggable();

        // jquery mousewheel not working in FireFox for some reason
        if ($.browser.mozilla) {
          value.addEventListener('DOMMouseScroll', function (e) {
            e.preventDefault();
            zoomMouse(-e.detail);
          }, false);
          if (value.useMap != "") {
            $(value.useMap)[0].addEventListener('DOMMouseScroll', function (e) {
              e.preventDefault();
              zoomMouse(-e.detail);
            }, false);
          }
        }
        else {
          $(value).bind('mousewheel', function (e) {
            e.preventDefault();
            zoomMouse(e.wheelDelta);
          });
          if (value.useMap != "") {
            $(value.useMap).bind('mousewheel', function (e) {
              e.preventDefault();
              zoomMouse(e.wheelDelta);
            });
          }
        }

        $(value).bind('mousemove', function (e) {
          zoomData.currentX = e.pageX;
          zoomData.currentY = e.pageY;
        });
      };

      var left = function() {
        return parseInt($(value).css('left'));
      };
      
      var top = function() {
        return parseInt($(value).css('top'));
      }
      
      var zoomIn = function() {
        // zoom as if mouse is in centre of image
        var parent = $(value).parent()[0];
        zoom(zoomData.x_fact, left()+parent.offsetLeft+(value.width/2), top()+parent.offsetTop+(value.height/2));
      };
      
      var zoomOut = function() {
        // zoom as if mouse is in centre of image
        var yi = parseInt($(value).css('top'));
        var parent = $(value).parent()[0];
        zoom(1 / zoomData.x_fact, left()+parent.offsetLeft+(value.width/2), top()+parent.offsetTop+(value.height/2));
      };
      
      var zoomMouse = function (delta) {

        // zoom out ---------------
        if (delta < 0) {
          zoom(1 / zoomData.x_fact, zoomData.currentX, zoomData.currentY);
        }

        // zoom in -----------
        else if (delta > 0) {
          zoom(zoomData.x_fact, zoomData.currentX, zoomData.currentY);
        }
      };

      var zoomMap = function () {
        // resize image map
        var map = document.getElementById(value.useMap.substring(1));
        if (map != null) {
          for (var i = 0; i < map.areas.length; i++) {
            var area = map.areas[i];
            var originalArea = zoomData.originalMap.areas[i];
            var coords = originalArea.coords.split(',');
            for (var j = 0; j < coords.length; j++) {
              coords[j] = Math.round(coords[j] * zoomData.currentZoom);
            }
            var coordsString = "";
            for (var k = 0; k < coords.length; k++) {
              if (k > 0)
                coordsString += ",";
              coordsString += coords[k];
            }
            area.coords = coordsString;
          }
        }
      };

      var zoom = function (fact, mouseX, mouseY) {
        if (!zoomData.busy) {
          zoomData.busy = true;

          var xi = left();
          var yi = top();

          var new_h = (value.height * fact);
          var new_w = (value.width * fact);
          zoomData.currentZoom = zoomData.currentZoom * fact;

          // calculate new X and y based on mouse position
          var parent = $(value).parent()[0];
          mouseX = mouseX - parent.offsetLeft
          var newImageX = (mouseX - xi) * fact;
          xi = mouseX - newImageX;

          mouseY = mouseY - parent.offsetTop
          var newImageY = (mouseY - yi) * fact;
          yi = mouseY - newImageY;

          $(value).animate({
            left: xi,
            top: yi,
            height: new_h,
            width: new_w
          }, 100, function () {
            zoomData.busy = false;
          });

          zoomMap();
        }
      };
      
      if (method == "zoomIn")
        zoomIn();
      else if (method == "zoomOut")
        zoomOut();
      else
        init();
    });
  };
})(jQuery);

Tuesday, March 23, 2010

jqGrid hints and tips

I’ve been spending a lot of time with jqGrid recently. It’s a marvellous piece of work but the documentation doesn’t always match up to the quality of the code. I’m not complaining, I realise it’s free, it’s flipping great and as a developer myself I know documentation is always the last thing I want to tackle. But here are a few things I’ve discovered along the way where it wasn’t immediately apparent what was causing the problem.

Multiple rows being selected – This can be caused by a couple of things. The first thing to check is that the row IDs being used are sensible. My problem was caused by having some rows with the same ID. Another time the problem cropped up when I used email addresses as my row IDs. It seems the grid has problems with certain characters being used in IDs. The @ symbol did it for me but there are others, like spaces, that can cause problems.

Arbitrary XML – The docs say that jqGrid can accept any arbitrary XML document, so long as you provide a mapping to describe how to map it to the standard XML format. This is almost true, but falls over if your XML document contains attributes that you want to pull into the grid. Attributes aren’t supported at all. I managed to get round this in a couple of steps. First I had to hack jqGrid so the loadComplete event was triggered before the grid was actually populated, rather than after. Then I could fiddle with my returned XML before jqGrid tried to read it. Then I wrote some code to convert attributes to elements with he same name (this could probably be improved with some jQuery magic but it did the job for me) and called it for the relevant attributes.

function setNodeText(newElem, text) {
  if (text != null) {
    try {
      newElem.textContent = text;
    }
    catch (e) {
      newElem.text = text;
    }
  }
}

function convertAttributeToElement(xml, node, attributeName) {
  var name = node.getAttribute(attributeName);
  addChildNode(xml, node, attributeName, name);
}

function addChildNode(xml, parent, nodeName, value) {
  var newElem = xml.createElement(nodeName);
  setNodeText(newElem, value);
  parent.appendChild(newElem);
}

Word wrap – I wish this was on by default, since it is now the first thing I do when I’m setting up a grid for the first time. Just modify the ui.jqgrid.css file so the definition for .ui-jqgrid tr.jqgrow td includes the following white-space: normal;

Sunday, March 14, 2010

Forcing a .NET application to run as 32-bit under 64-bit Windows

I was having problems with running a .NET application under 64-bit Windows 7. This was because by default, .NET applications are built to target any platform. This app clearly hadn’t been tested on 64-bit Windows so blew up when it tried to instantiate a 32-bit COM object. The following link explains some of the options on how to fix this problem.  

http://www.lostechies.com/blogs/gabrielschenker/archive/2009/10/21/force-net-application-to-run-in-32bit-process-on-64bit-os.aspx

Obviously setting the platform target wasn’t an option for me since I didn’t have access to the source code. Using corflags.exe also wasn’t possible because the application was strong named so setting that flag would break the strong naming. So I was forced to create a wrapper application that was compiled for 32-bits. This initially also didn’t work but the only thing I needed to do was add the STAThread attribute to my wrapper’s Main method since the application I was calling also had this attribute on its Main method. Then it all worked, hurrah!

Now I just need to figure out how to do the same thing for a .NET service… And for all you .NET developers out there, if you don’t have the time or resources to test on 64-bit Windows, just flip that platform target switch to x86. Even the cheapest new PCs come with 64-bit versions of Windows (my laptop cost £400) and flipping that switch pretty much guarantees your app will work on 64-bit platforms.

Friday, March 12, 2010

XML encoding in PHP

I’ve needed it in JavaScript in the past and now I need it in PHP. Escape all those pesky special XML characters like so

  function XmlEncode($content)
  {
    $trans = array("&" => "&amp;", "<" => "&lt;", ">" => "&gt;", "'" => "&apos;",
      "\"" => "&quot;", "’" => "&apos;");
  	return strtr($content, $trans);
  }

Friday, March 05, 2010

Collatz conjecture implementation

I only learnt about the Collatz conjecture today so thought I'd implement it in JavaScript. Type in a starting number and watch as the process converges to 1, probably

Monday, February 15, 2010

Hollands Pies reviewed

So I’m slowly working my way through the pies I purchased the other day and he’s what I think of them so far.

Meat pie – this is a little pie, about the size of a standard pork pie but heated up. It contains pork and beef and tastes pretty yummy. My only complaint is the scalding hot liquid contained within which burnt my mouth first time, although this was easily rectified second time around by slicing the pie in half.

Cheese and onion – many years ago when I was a vegetarian, this was one of the few pies I was able to partake of. That and the butter pie, which seems to have disappeared from the face of the earth, probably due to its unfortunate name. It did contain more than just butter, which is why I didn’t collapse from heart failure. But back to the cheese and onion pie. Unfortunately this was disappointing, there was very little evidence of onion, just some bright yellow gloop that may well have originated at the Sellafield reprocessing plant (I joke of course, before anyone tries to sue me).

Potato and meat – before some government organisation got involved, this was known as a meat and potato pie. Now it’s just a perfectly decent pie with a silly name.

Steak and kidney pudding – this is where things get interesting. This is not like any other pie you’ll ever experience. With most pies the pastry is essentially just some hard packaging to keep the contents together, with little merit of its own. But with the steak and kidney pudding, the suet case is an essential part of the whole experience. Not only are the contents tasty, but so is the pastry. Even the cooking instructions are unique. No oven for this pie, but a pan of boiling water. There is one downside to this. Whereas pies are generally a pretty portable food stuff, the steak and kidney pudding needs a plate and fork.

So in conclusion, I would recommend all of these pies from Hollands, except the cheese and onion. But for ultimate pleasure, order ten steak and kidney puddings…

Saturday, February 13, 2010

FreeFlow now open source

I don’t really have time to maintain it anymore, so I’ve open sourced FreeFlow, my little toolkit and administration tool for Metastorm BPM. Go grab it from CodePlex and make it better!

Wednesday, February 10, 2010

Using PUT and DELETE in WCF web services

IIS7 handler mappingsI’ve been doing a little work on a web page that needs to grab some data from a web service and then update that data via PUT and DELETE requests. I haven’t got access to the web service currently so decided to write my own dummy implementation using WCF.

The first problem I encountered was that by default PUT and DELETE calls won’t be allowed at all by IIS. This can be resolved by fiddling with the Handler Mappings options in IIS7 to allow the required verbs, as shown on the left.

My next problem was that I wanted to use the same URL for both PUT and DELETE requests. The WebInvoke attribute that is applied to your WCF method doesn’t support multiple HTTP verbs, so I was a little stumped at this point. The solution is actually pretty straightforward, have multiple methods pointing to the same URL, like so

[WebInvoke(Method = "DELETE", UriTemplate = "EditFolder")]
public void DeleteFolder(Stream input)
This is actually probably a good restriction by WCF. If you want different verb types on the same URL, the chances are that you have two different operations anyway.

Wednesday, February 03, 2010

Unminifying Javascript files

Am I a web developer? I dunnow, I’ve spent a lot of time building web sites but I don’t really feel like a web developer. The web just plain scares me. Debug any web application and you’re faced with shedloads of anonymous functions, eval code and general weirdness. Look at the poster boys of Web 2.0, JQuery and its children JQuery UI and JQGrid. Yes they are fecking marvellous bits of technology but try stepping through the code and tell me you have the faintest idea what on earth is going on. It feels like the web is held together with a pile of JavaScript sellotape.

But in some kind of sado-masochistic twist, the web is now even more convoluted because the JavaScript is even less readable because it’s all been minified in an attempt to save bandwidth. So now if I find myself in a debugger trying to figure out what some JavaScript is doing, I see one long line of code with useless function names and I go off to bang my head against a wall because it’s less painful. But I have found one tool that improves the situation, JS Beautifier, which will attempt to make your minified code more readable and understandable by a human. Beyond the initial rant, this is primarily a reminder to myself.

Sunday, January 31, 2010

David Cameron defaced

David Cameron defaced When the smug face of David Cameron started popping up on billboards all over the place, I had a very strong urge to deface it. It would appear I wasn’t the only one, although these are generally more amusing than what I had in mind.

http://www.mydavidcameron.com/

Saturday, January 30, 2010

Metastorm release BPM 9 and don’t tell anyone

Here’s an odd thing. When most software companies release a new version of their software, they shout about it until they are hoarse. But Metastorm have released version 9 of their BPM software and as far as I can see they haven’t even produced a press release to announce it to the world. Searching on Google News brings back no results and I can’t see any mention of it on the Metastorm website.

Initially I’d assumed this was because version 9 was released in the quiet period before Christmas and the PR onslaught would start in the new year, but here we are well into 2010 and there is still silence.

Now generally if it’s a choice between cock-up and conspiracy, I’ll plump for cock-up every time but I really can’t believe that any software company can forget to produce a press release to announce their new baby to the world so I have to go for the conspiracy option but what is the conspiracy? Answers on a postcode or in the comments…

Thursday, January 28, 2010

Buying steak and kidney puddings on the web

I was reminded of steak and kidney puddings some time ago when I read Stuart Maconie’s “Pies and Prejudice” and suddenly had a strong desire to once again experience the long forgotten taste of them. But my searches on the web to find somewhere to purchase them in this culinary wasteland called London led me nowhere. Then the other day I received an email from Holland's Pies and ventured back to their website. I then discovered they have recently opened their online shop with a selection of their pies available. So £18 and a couple of days later I am now in possession of ten pies (including several steak and kidney pudds)and am looking forward to sampling one for my lunch tomorrow…

Monday, January 25, 2010

Cost benefit analysis of energy efficient bulbs

We’ve had six GU10 bulbs in our kitchen for a while now and I’ve never really been very happy with them. They swallow a lot of power, don’t seem to last very long and are pretty expensive to replace. I got hold of an LED replacement some time ago and was less than impressed. It didn’t produce enough light, was really really expensive and then died after a couple of months. So when I popped into Maplin at the weekend, I thought I’d try out their low-energy GU10 replacement. Admittedly these are even more expensive than the full powered version, but they claim to last for 8000 hours and use a lot less power. I’m happy with the amount of light they throw out, although they suffer from the usual problem of taking a while to warm up. Anyway, I thought I’d do a quick calculation of how long it would take me to earn back the money I shelled out for them.

Cost per KWH – 0.15

Old 6 bulbs @ 50W = 300W

New 6 bulbs @ 11W = 66W

KWHs saved = 0.3-0.066 = 0.234 KWHs

Cost of bulbs = £7.49*6 = £45

Number of KWHs required to cover cost = 45/0.15 = 300KWHs

Number of hours required to cover cost = 300/0.234 = 1282 hours

Assuming 5 hours use a day = 1282/5 days = 256 days = 8.5 months

So based on this very rough calculation and assuming I’ve calculated it correctly, this would appear to be a good deal. Fingers crossed they actually last for as long as claimed, which I’m reasonably confident about based on my experiences with other energy efficient bulbs.

Monday, January 04, 2010

Getting all the points in a SqlGeometry

It’s quite simple to get hold of the points in a SQL Server geometry polygon using the STNumPoints and STPointN SQL functions, but this requires quite a few queries (or some kind of stored procedure). I tend not to like running lots of queries, even if that does smell like premature optimisation, and for my little project I don’t want to be adding stored procedures but then I realised the SqlGeometry type is implemented as a .NET type, so all its properties and methods are available from .NET code, so here’s a little extension method to get all the points for a polygon

public static class GeometryHelper
{
  public static SqlGeometry[] Points(this SqlGeometry geometry)
  {
    List<SqlGeometry> points = new List<SqlGeometry>();
    for (int i = 1; i <= geometry.STNumPoints(); i++)
    {
      points.Add(geometry.STPointN(i));
    }
    return points.ToArray();
  }
}

Sunday, January 03, 2010

Converting a SqlDouble to a .NET double

Note to self - this is simple, just typecast like so

(double)points[i].STX

Improving performance with Gzip compression in PHP

I was perusing through some of the data provided by Google Webmaster Tools when I came upon the Site Performance section. this told me that doogal.co.uk took longer to load than 96% of sites on the web, which was a little embarrassing. I knew what was causing at least some of this slowdown, the incomplete list of UK postcodes which has been getting gradually slower as the number of postcodes has got longer and longer. I’d already started to address that by paging the data, but the CSV data couldn’t really be paged, without reducing the value of it. So I had a look at the suggestion provided by Google, which was to use Gzip compression on the page. I’ve not really thought much about GZip compression before, assuming if it was so useful it would be on by default but I thought I’d give it a go anyway. So I fired up Fiddler and tried downloading my big CSV page and it took approximately 9.5 seconds to fully download. I arrived at this figure as an average after several reloads.

Next I added this line to my PHP source file, ob_start("ob_gzhandler");, and measured the difference. It now took 3.7 seconds to download, wow! A lot of gain for very little effort.

At this point, I wondered if I could add Gzip compression to all my pages. It turns out this is straightforward, just add the following to the .htaccess file

php_flag zlib.output_compression on

After adding this, the CSV page now arrived in 2.7 seconds. I’m not sure why this is even faster, but I’m not complaining. Now the rest of the site feels much snappier as well. So what am I missing? Is there a reason GZip compression isn’t on by default? Am I going to get bitten by this at some point in the future?

Tuesday, December 22, 2009

Getting boundary data out of OS OpenSpace

County boundaries in SQL Server

I’ve been able to pull out a single county’s boundary data from OS OpenSpace using its JavaScript API, but I wanted to get the data for all counties. Now I could have used my little example webpage to do this manually for each county, but that would be extremely tedious. So based on the fact that all AJAX APIs can be considered as simple HTTP requests, I wrote a little piece of C# code that made the required HTTP request and dumped the data into my SQL Server table. This is very rough and ready but it met my needs. Some time soon I will probably put together a class to handle all the OS Open Space interaction a bit more nicely and with more functionality.

      HttpWebRequest req = (HttpWebRequest)WebRequest.Create(
        "http://openspace.ordnancesurvey.co.uk/osmapapi/boundary?geometry=true&"+
        "key=74B560DED619715AE0405F0AF060615A&f=xml&" +
        "url=http%3A%2F%2Flocalhost%2FOpenSpaceBoundary%2FDefault.aspx&area_code=CTY&" +
        "bbox=0%2C0%2C1400000%2C1400000&resolution=1000&dojo.preventCache=1261317566652&" +
        "callback=dojo.io.script.jsonp_dojoIoScript4._jsonpCallback");
      using (WebResponse resp = req.GetResponse())
      using (Stream respStream = resp.GetResponseStream())
      using (StreamReader reader = new StreamReader(respStream))
      {
        string response = reader.ReadToEnd();
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.LoadXml(response);

        using (SqlConnection conn = new SqlConnection("Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=OGC;Data Source=NEWDOOGAL"))
        {
          conn.Open();
          using (SqlCommand command = conn.CreateCommand())
          {
            command.CommandText = "DELETE FROM CountyBoundaries";
            command.ExecuteNonQuery();
          }

          JavaScriptSerializer serializer = new JavaScriptSerializer();
          XmlNodeList nodes = xmlDoc.SelectNodes("BoundaryResultVO/items/item/geojson");
          foreach (XmlElement element in nodes)
          {
            Dictionary<string, object> data = 
              (Dictionary<string, object>)serializer.DeserializeObject(element.InnerText);
            Dictionary<string, object> properties = (Dictionary<string, object>)data["properties"];
            string countyName = (string)properties["NAME"];

            // get polygon data
            Dictionary<string, object> geometry = (Dictionary<string, object>)data["geometry"];
            object[] coordinates = (object[])geometry["coordinates"];
            
            for (int i = 0; i < coordinates.Length; i++)
            {
              StringBuilder wktBuilder = new StringBuilder();
              object[] polygonParts = (object[])coordinates[i];
              wktBuilder.Append("POLYGON(");
              for (int j = 0; j < polygonParts.Length; j++)
              {
                if (j>0)
                  wktBuilder.Append(",");
                wktBuilder.Append("(");
                object[] points = (object[])polygonParts[j];
                for (int k = 0; k < points.Length; k++)
                {
                  object[] point = (object[])points[k];
                  if (k > 0)
                    wktBuilder.Append(",");
                  wktBuilder.Append(point[0]);
                  wktBuilder.Append(" ");
                  wktBuilder.Append(point[1]);
                }
                wktBuilder.Append(")");
              }
              wktBuilder.Append(")");

              // put into database
              using (SqlCommand command = conn.CreateCommand())
              {
                command.CommandText = string.Format(
                  "INSERT INTO CountyBoundaries (County, Boundary) VALUES ('{0}', " +
                  "geometry::STPolyFromText('{1}', 4277))", countyName, wktBuilder.ToString());
                command.ExecuteNonQuery();
              }
            }
          }
        }
      }

Thursday, December 10, 2009

Getting all available geographic reference systems out of SQL Server 2008

As a reminder to myself, the query required to get a list of all available geographic reference systems out of SQL Server 2008 is

SELECT * FROM sys.spatial_reference_systems

Wednesday, December 09, 2009

uDig – free GIS

To be frank I don’t much like anything Java based but uDig is the exception that probably proves the rule. It’s a free GIS based on Eclipse and it is pretty user friendly. Unfortunately it doesn’t handle data stored in SQL Server 2008, which marks it down a bit in my eyes, but it does support most other data sources.

Saturday, November 28, 2009

Using OGC functions in SQL Server 2008 part 5 – Google Maps and polygons

County boundary In this series of posts I’ve so far managed to pull some spatial data into SQL Server, query it in a number of ways and finally been able to display some of that data in Google Maps. The next thing I wanted to do was to display the county boundary for Lancashire, since it was the postcodes for that great county I was displaying. I’ve already got the data for the county boundary from OS OpenSpace but the problem is this data is in UK OS coordinates rather than latitude and longitude as used by Google Maps. There are two ways to deal with this. Convert the data to latitude/longitude before displaying it or implement the GProjection interface to handle the conversion in the web page. The latter is something I want to look at at some point but for this I decided to do the conversion server-side.

So the first thing I did was to write a simple generic handler to generate an XML document containing the points for the county boundary. This code uses the .NET Coordinates library I have converted from the Java original. This is what the handler looks like

  public void ProcessRequest (HttpContext context) 
  {
    context.Response.ContentType = "text/xml";
    StringWriter stringWriter = new StringWriter();
    XmlTextWriter writer = new XmlTextWriter(stringWriter);
    writer.WriteStartElement("points");
    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["postcodesDatabase"].ConnectionString))
    {
      conn.Open();
      using (SqlCommand comm = conn.CreateCommand())
      {
        comm.CommandText = "SELECT Boundary.STNumPoints() FROM CountyBoundaries WHERE County='Lancashire'";
        int numPoints = (int)comm.ExecuteScalar();
        for (int i = 1; i <= numPoints; i++)
        {
          writer.WriteStartElement("point");
          // get X
          comm.CommandText = "SELECT Boundary.STPointN(" + i + ").STX FROM CountyBoundaries WHERE County='Lancashire'";
          double x = (double)comm.ExecuteScalar();

          // get Y
          comm.CommandText = "SELECT Boundary.STPointN(" + i + ").STY FROM CountyBoundaries WHERE County='Lancashire'";
          double y = (double)comm.ExecuteScalar();

          // convert to lat/long
          OSRef osRef = new OSRef(x, y);
          LatLng latLong = osRef.ToLatLng();

          writer.WriteAttributeString("x", latLong.Longitude.ToString());
          writer.WriteAttributeString("y", latLong.Latitude.ToString());
          
          writer.WriteEndElement();
        } 
      }
    }
    writer.WriteEndElement(); // points
    context.Response.Write(stringWriter.ToString());
  }

Next up was the JavaScript required in the web page to display the data. This is pretty simple

        // load boundary
        var boundReq = new XMLHttpRequest();
        boundReq.open("GET", "GetBoundary.ashx", true);
        boundReq.onreadystatechange = function() {
          if (boundReq.readyState == 4) {
            if (boundReq.status == 200) {
              var nodes = boundReq.responseXML.selectNodes("points/point");
              var latLongs = new Array();
              for (var i = 0; i < nodes.length; i++) {
                var lng = nodes[i].getAttribute("x");
                var lat = nodes[i].getAttribute("y");
                var latLong = new GLatLng(lat, lng);
                latLongs.push(latLong);
              }
              var polygon = new GPolygon(latLongs, "#FF0000", 3);
              map.addOverlay(polygon);
            }
          }
        };
        boundReq.send(); 

If you look at the image above you may think the boundary displayed is not correct since it doesn’t include some places that are quite obviously in Lancashire (Blackburn and Darwen for instance) and this confused me for a while. But the boundary displayed is that of Lancashire County council, rather than the full county, which doesn’t include some areas which are in unitary authorities but still within the county.