Mapserver and Leaflet

Leaflet

Leaflet is a very simple but incredibly powerful Javascript mapping library that lets you add interactive maps to your website very easily. Try scrolling and zooming around this one:

For example, to add that map to this page, all I did was add the following code (after reading the Quick-Start Guide):

<div id="danMap" style="height: 200px;"></div>

<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
<script type="text/javascript" src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>

<script type="text/javascript">
var map = L.map('danMap').setView([51.4, -1.25], 13);	
L.tileLayer('http://{s}.tiles.mapbox.com/v3/YOUR.MAP.KEY/{z}/{x}/{y}.png', 
{
  attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="http://mapbox.com">Mapbox</a>',
  maxZoom: 18,
}).addTo(map);
</script>

You can add points, polygons and visualise all kinds of live data using a simple web service that returns some GeoJson data. It works like a charm on mobile devices too!

Why combine Leaflet with Mapserver?

I have a couple of use-cases that meant I needed to look at combining Leaflet with Mapserver. This turns out to be easy enough as Leaflet can hook up to any tile provider and Mapserver can be set up to serve images as a Web Map Service (WMS).

The first thing I wanted to do is serve up some map data when not connected to the internet. Imagine I am in the middle of nowhere, connected to the Raspberry Pi in the back of the Land Rover via WiFi to my phone or tablet. I have a GPS signal but I don’t have any connection to a map imagery server as there’s no mobile coverage. I need to use mapserver to render some local map data so I can see where I am. This use case has a boring work-related benefit too – it enables you to serve up maps in a web-based mapping application behind a strict corporate firewall.

The other use case is simple: raster data. Lots of the data we deal with where I work is served up as raster data by Mapserver. Imagine it as heat-maps of some KPI value layered on top of a street map.

Setting up Mapserver

There are a couple of things you need to do to get Mapserver to act as a WMS. The first is to add a projection and web metadata element to the root of the map file (as below). After a large amount of head-scratching, wailing and gnashing of teeth I found that the de-facto standard projection for all “internet” map data is EPSG 3857. Make sure you use that EPSG at the root of your map file.

PROJECTION
  "init=epsg:3857"
END
	
WEB
  METADATA
    "wms_title" "Dans Layers and Stuff"
    "wms_onlineresource" "http://192.168.2.164/cgi-bin/mapserv.exe?"
    "wms_enable_request" "*"
    "wms_srs" "EPSG:3857"
    "wms_feature_info_mime_type" "text/html"
    "wms_format" "image/png"
  END
END

The next thing to do is add some extra stuff to every layer in your map. You need to set the STATUS field to ‘on’; add a METADATA element and set the ‘wms_title’ to something sensible; and finally add a projection, specifying the projection the layer data is stored in. As I am using the OS VectorMap District dataset, which is on the OSGB projection I used EPSG 27700.

LAYER
  NAME         Woodland
  DATA         Woodland
  PROJECTION
    "init=epsg:27700"
  END
  METADATA
    "wms_title" "Woodland"
  END
  STATUS       on
  TYPE         POLYGON
  CLASS	
    STYLE
      COLOR 20 40 20
    END
  END
END 

Connecting it Together

You can then add a new layer to the Leaflet map, connected to your Mapserver. Here I’m using ms4w, the Windows version of Mapserver and hooking it up to a map file in my Dropbox folder. The map file I am using is the one I created for a previous post on mapserver.

L.tileLayer.wms("http://localhost:8001/cgi-bin/mapserv.exe?map=D:\\Dropbox\\Data\\Mapfiles\\leaflet.map", {
			layers: 'Roads,MotorwayJunctions',
			format: 'image/png',
			transparent: true,
			attribution: "Dan's Amazing Roads",
			maxZoom: 18,
			minZoom: 12,
		}).addTo(map);

Sadly I don’t have a mapserver instance on the internet, so all I can show here is a couple of screenshots. You’ll just have to take my word for it – it works brilliantly!

mapserverLeaflet2

mapserverLeaflet1

Shape Files and SQL Server

Over the last couple of weeks I have been doing a lot of work importing polygons into an SQL server database, using them for some data processing tasks and then exporting the results as KML for display. I thought it’d be worth a post to record how I did it.

Inserting polygons (or any other geometry type) from a shape file to the database can be done with the ogr2ogr tool which ships with the gdal libraries (and with Mapserver for Windows). I knocked up a little batch file to do it:

SET InputShapeFile="D:\Dropbox\Data\SingleView\Brazillian Polygons\BRA_adm3.shp"

SET SqlConnectionString="MSSQL:Server=tcp:yourserver.database.windows.net;Database=danTest;Uid=usernname@yourserver.database.windows.net;Pwd=yourpassword;"

SET TEMPFILE="D:\Dropbox\Data\Temp.shp"
SET OGR2OGR="C:\ms4w\tools\gdal-ogr\ogr2ogr.exe"
SET TABLENAME="TestPolygons"

%OGR2OGR% -overwrite -simplify 0.01 %TEMPFILE% %InputShapeFile% -progress

%OGR2OGR% -lco "SHPT=POLYGON" -f "MSSQLSpatial" %SqlConnectionString% %TEMPFILE% -nln %TABLENAME% -progress

The first ogr2ogr call is used to simplify the polygons. The value 0.01 is the minimum length of an edge (in degrees in this case) to be stored. Results of this command are pushed to a temporary shape file set. The second call to ogr2ogr pushes the polygons from the temp file up to a database in Windows Azure. The same code would work for a local SQL Server, you just need to tweak the connection string.

You can use SQL Server Management Studio to show the spatial results of your query, which is nice! Here I just did a “select * from testPolygons” to see the first 5000 polygons from my file.

PolygonsInSqlServer

Sql Server contains all sorts of interesting data processing options, which I’ll look at another time. Here I’ll just skip to the final step – exporting the polygon data from the database to a local KML file.

polygonsInKml

SET KmlFile="D:\Dropbox\Data\Brazil.kml"

SET SqlConnectionString="MSSQL:Server=tcp:yourserver.database.windows.net;Database=danTest;Uid=usernname@yourserver.database.windows.net;Pwd=yourpassword;"

SET TEMPFILE="D:\Dropbox\Data\Temp.shp"
SET OGR2OGR="C:\ms4w\tools\gdal-ogr\ogr2ogr.exe"
SET SQL="select * from TestPolygons"

%OGR2OGR% -lco "SHPT=POLYGON" -f "KML" %KmlFile% -sql %SQL% %SqlConnectionString%  -progress

Obviously you can make the SQL in that command as complex as you like.

Polygons here are from this site which allows you to download various polygon datasets for various countries.

Combining Shape Files

This is one of those things that’s easy when you know how. Just so I don’t forget, here’s how to combine shape files using ogr2ogr.

I wrote it as a batch file to combine all the OSGB grid squares from the OS VectorMap District dataset into a single large data file for use with MapServer.

echo off

set OGR2OGR="C:\ms4w\tools\gdal-ogr\ogr2ogr"
set inputdir="D:\Dropbox\Data\OS VectorMap"
set outputdir="D:\Dropbox\Data\OS VectorMap Big"

set tiles=(HP HT HU HW HX HY HZ NA NB NC ND NF NG NH NJ NK NL NM NN NO NR NS NT NU NW NX NY NZ OV SC SD SE TA SH SJ SK TF TG SM SN SO SP TL TM SR SS ST TU TQ TR SV SW SX SY SZ TV)

set layers=(Airport AdministrativeBoundary Building ElectricityTransmissionLine Foreshore GlassHouse HeritageSite Land MotorwayJunction NamedPlace PublicAmenity RailwayStation RailwayTrack Road RoadTunnel SpotHeight SurfaceWater_Area SurfaceWater_Line TidalBoundary TidalWater Woodland)

del /Q %outputdir%\*.*

FOR %%L IN %layers% DO (
%OGR2OGR% %outputdir%\%%L.shp %inputdir%\SU_%%L.shp
)

FOR %%T IN %tiles% DO FOR %%L IN %layers% DO (
%OGR2OGR% -update -append %outputdir%\%%L.shp %inputdir%\%%T_%%L.shp -nln %%L
)

After a little map file jiggery-pokery I can now render a huge map of the UK or tiles with smaller maps without the many layer definitions needed to use ~20 shape file sets.

london-zoomed

london-big

uk-big

Mapserver Revisited

Years ago, before they invented Google Earth and Bing Maps and all that, I did some work to show GPS data using Mapserver.  At work yesterday I was forced to revisit it while maintaining an aged and creaking part of our product.  It took a while to get my head back into the right state but once I’d got going I started to enjoy it again.  This time around, because I’m at work, I’m using Mapserver for Windows. A handy tutorial on setting this up can be found here.

When I last played with Mapserver there was very little decent data.  I had to settle for a very low resolution map of the world (vmap0) and some pretty poor raster data.  Yesterday, after a little googling, I found that OS have released a very nice open source shape file dataset called “OS Vectormap District” which is very easy to use and well documented. There are some reasonable online tutorials too.  I spent a little time getting this to render and the results are shown here in the scaled image above and full-size image below.

I decided to go with a black background as I was thinking of a Raspberry Pi based “GPS” system to show The Duke’s location on a small screen.

Note that the image is a little jagged looking.  Turns out you can fix this by adding the following snippet into your map file (under the MAP element).  Of course, you pay a high price in processing time and image size.

OUTPUTFORMAT
NAME png
DRIVER "AGG/PNG"
MIMETYPE "image/png"
		# Change to IMAGEMODE RGBA for transparent background
IMAGEMODE RGB
EXTENSION "png"
FORMATOPTION "INTERLACE=OFF"
END

The benefit of Mapserver over Google or Bing maps is that it works on local data with no requirement for an internet connection. This means it’s more reliable for a GPS sort of system. Of course, the downside is that there are fewer layers available and you have to do a lot of coding. Was good to play with, though, and I hope to get it working on the raspberry soon.

Here’s my (large) Map file:

MAP
	IMAGETYPE      PNG
	#Whole UK would be...
	#EXTENT         0 0 660000 1230000
	# SU Grid is...
	#EXTENT		400000 100000 500000 200000
	# The top right of SU is...
	EXTENT		450000 150000 500000 200000

	FONTSET "C:\Users\Dan.Taylor\Desktop\Mapfiles\Fonts.txt"

	SIZE           6400 6400
	IMAGECOLOR     0 0 0
	CONFIG "MS_ERRORFILE" "C:\Users\Dan.Taylor\Desktop\Mapfiles\Errors.txt"
	CONFIG "CPL_DEBUG" "ON"
	CONFIG "PROJ_DEBUG" "ON"
	DEBUG 5

	SYMBOL
		NAME "triangle"
		TYPE vector
		POINTS
			0 1
			0.5 0
			1 1
			0 1
		END
		FILLED TRUE
    END

	LAYER
		NAME         Woodland
		DATA         "C:\Users\Dan.Taylor\Desktop\SU\SU_Woodland"
		STATUS       default
		TYPE         POLYGON

		CLASS
			STYLE
				COLOR 20 40 20
			END
		END
	END

	LAYER
		NAME         SurfaceWater
		DATA         "C:\Users\Dan.Taylor\Desktop\SU\SU_SurfaceWater_Area"
		STATUS       default
		TYPE         POLYGON

		CLASS
			STYLE
				COLOR 80 80 128
			END
		END
	END

	LAYER
		NAME         TidalWater
		DATA         "C:\Users\Dan.Taylor\Desktop\SU\SU_TidalWater"
		STATUS       default
		TYPE         POLYGON

		CLASS
			STYLE
				COLOR 60 60 100
			END
		END
	END

	LAYER
		NAME         Buildings
		DATA         "C:\Users\Dan.Taylor\Desktop\SU\SU_Building"
		STATUS       default
		TYPE         POLYGON

		CLASS
			STYLE
				COLOR 80 60 80
			END
		END
	END

	LAYER
		NAME         Foreshore
		DATA         "C:\Users\Dan.Taylor\Desktop\SU\SU_Foreshore"
		STATUS       default
		TYPE         POLYGON

		CLASS
			STYLE
				COLOR 80 80 128
			END
		END
	END

	LAYER
		NAME         Roads
		DATA         "C:\Users\Dan.Taylor\Desktop\SU\SU_Road"
		STATUS       default
		TYPE         LINE

		CLASSITEM	"CLASSIFICA"

		CLASS
			EXPRESSION	 "Motorway"
			STYLE
				COLOR 20 128 128
				WIDTH 8
			END
			STYLE
				COLOR 200 200 255
				WIDTH 2
			END
		END
		CLASS
			EXPRESSION	 "Primary Road"
			STYLE
				COLOR 20 128 20
				WIDTH 6
			END
			STYLE
				COLOR 200 200 0
				WIDTH 2
			END
		END
		CLASS
			EXPRESSION	 "A Road"
			STYLE
				COLOR 20 128 20
				WIDTH 6
			END
		END
		CLASS
			EXPRESSION	 "B Road"
			STYLE
				COLOR 128 20 20
				WIDTH 4
			END
		END
		CLASS
			EXPRESSION	"Private Road, Publicly Accessible"
			STYLE
				COLOR 128 128 128
				WIDTH 2
			END
		END
		CLASS
			EXPRESSION	 "Local Street"
			STYLE
				COLOR 128 128 128
				WIDTH 2
			END
		END
		CLASS
			EXPRESSION	"Pedestrianised Street"
			STYLE
				COLOR 128 128 128
				WIDTH 2
			END
		END
		CLASS
			EXPRESSION	 "Minor Road"
			STYLE
				COLOR 128 128 128
				WIDTH 2
			END
		END
	END

	SYMBOL
		NAME "station"
		TYPE ellipse
		FILLED true
		POINTS
			1 1
		END
	END

	LAYER
		NAME         RailwayStations
		DATA         "C:\Users\Dan.Taylor\Desktop\SU\SU_RailwayStation"
		STATUS       default
		TYPE         POINT
		LABELITEM    "NAME"

		CLASS
			LABEL
				TYPE truetype
				ANTIALIAS true
				FONT arial
				SIZE 10
				POSITION cr
				COLOR 255 50 50
				OUTLINECOLOR 100 0 0
				BUFFER 30
				PARTIALS false
				FORCE true
			END

			STYLE
				SYMBOL station
				COLOR 255 50 50
				OUTLINECOLOR 100 0 0
				SIZE 20
			END
		END
	END

	LAYER
		NAME         Airports
		DATA         "C:\Users\Dan.Taylor\Desktop\SU\SU_Airport"
		STATUS       default
		TYPE         POINT
		LABELITEM    "NAME"

		CLASS
			LABEL
				TYPE truetype
				ANTIALIAS true
				FONT arial
				SIZE 10
				POSITION cr
				COLOR 255 180 50
				OUTLINECOLOR 100 50 0
				BUFFER 30
				PARTIALS false
				FORCE true
			END

			STYLE
				SYMBOL "triangle"
				COLOR 255 180 50
				OUTLINECOLOR 100 50 0
				SIZE 20
			END
		END
	END

	LAYER
		NAME         NamedPlaces
		DATA         "C:\Users\Dan.Taylor\Desktop\SU\SU_NamedPlace"
		STATUS       off
		TYPE         POINT
		LABELITEM    "NAME"

		CLASS
			LABEL
				TYPE truetype
				ANTIALIAS true
				FONT arial
				SIZE 10
				POSITION cc
				COLOR 255 50 50
				OUTLINECOLOR 100 0 0
				BUFFER 30
				PARTIALS false
				FORCE true
			END

			STYLE
				SYMBOL station
				COLOR 255 50 50
				OUTLINECOLOR 100 0 0
			END
		END
	END

	LAYER
		NAME         MotorwayJunctions
		DATA         "C:\Users\Dan.Taylor\Desktop\SU\SU_MotorwayJunction"
		STATUS       default
		TYPE         POINT
		LABELITEM    "JUNCTIONNU"

		CLASS
			LABEL
				TYPE truetype
				ANTIALIAS true
				FONT arial
				SIZE 14
				POSITION cc
				COLOR 200 200 255
				OUTLINECOLOR 200 200 255
				BUFFER 30
				PARTIALS false
				FORCE true
			END

			STYLE
				SYMBOL station
				COLOR 20 128 128
				OUTLINECOLOR 200 200 255
				SIZE 30
			END
		END
	END
END

The ASCII Speedometer

Once I’d got Mono up and running, the first little project I did with the Raspberry Pi was to hook up an old GPS module and use it to create a text based speedometer for the car.  It was the first step I took towards making The Duke sentient building an on-board computer for my Land Rover.  I was fun to do and raised a smile with the people who I told about it, so I thought I’d bung the details online.

The Hardware Bit

First step is to get the serial port working.  If you have a USB GPS module then it’s just a case of plugging it in, but mine is a 3.3v logic-level serial module that I bought about eight years ago to use with my Gumstix in a linux-powered walk logger for a couple of big charity walks we did back in 2004 and 2006.  It’s safe to say that hooking it up to the Raspberry Pi was a much simpler affair!

There are some great guides out there about soldering up the right pins (here and here).  I hooked up 3.3v power, ground and connected TX on the GPS to RX on the Raspberry Pi.  I used a little bit of veroboard, a 0.1″ header, some ribbon cable and made the GPS pluggable.

Out of the box the Raspberry has a console running on the serial port.  You need to disable this before you can do anything with the port.  Very easy to do: Edit /etc/inittab and remove the line that refers to /dev/ttyAMA0;  Edit /boot/cmdline.txt and remove the chunks of text that refer to /dev/ttyAMA0.  After a reboot the terminal will be gone.

Reading from the Serial Port in Mono

Right, now we’re ready to write some code.  First off I wrote this simple bit of C# to test that I was getting messages. The ReadData method reads text from the serial port one character at a time, detecting end of line characters to return a string for each line. The main method loops forever reading these lines and printing them to the console if they start with the NMEA “Recommended Minimum Content” message $GPRMC.

using System;
using System.IO.Ports;

public class Serial
{
   public static void Main()
   {
      SerialPort serial = new SerialPort("/dev/ttyAMA0", 4800);
      serial.Open();
      serial.ReadTimeout = 1000;

      while(true)
      {
         string data = ReadData(serial);
         if(!string.IsNullOrEmpty(data) && data.StartsWith("$GPRMC"))
         {
             Console.WriteLine(data);
         }
      }
   }

   public static string ReadData(SerialPort serial)
   {
      byte tmpByte;
      string rxString = "";

      do
      {
         tmpByte = (byte) serial.ReadByte();
         rxString += ((char) tmpByte);
         tmpByte = (byte) serial.ReadByte();
      }while (tmpByte != 13 && tmpByte != 10);

      return rxString.Trim();
   }
}

Parsing the NMEA Data

I then did a bit of a Test Driven Development exercise to write a proper parser for NMEA messages. To do this in a test driven way I got my hands on some data files containing raw NMEA data and used that to create a Mock serial port reader. I could then pass these messages through my parser and test that I had managed to extract the right data.

Unit testing and using mocks was a great way to develop this part of the application. I could use recorded routes with real movement to test the parsing of speed data – since coding in a moving car seemed silly. I could also do all the coding work in Visual Studio on my Windows machine. This meant I could make the most of a nice big screen, code completion, resharper’s excellent testing interface and so on, then just push the code onto the Pi when it was done; I didn’t have to worry that “/dev/ttyAMA0” is “COM3” in Windows land, because I wasn’t using a real serial port to do 99% of the development.

A typical test for parsing of individual messages (hand typed!):

[TestCase("$GPRMC,005959,V,4807.038,N,11130.00,E,022.4,084.4,010101,003.1,W*4E", 48.1173, 111.5)]
public void CanParseLatLongFromRmcMessage(string input, double expectedLat, double expectedLong)
{
    NmeaParser parser = new NmeaParser();
    GpsMeasurement measurement = parser.Parse(input);

    Assert.That(measurement.Latitude, Is.EqualTo(expectedLat));
    Assert.That(measurement.Longitude, Is.EqualTo(expectedLong));
}

The mock serial reader class:

public class MockSerialPortReader : IPortReader
{
    private readonly string filename;
    private readonly int sleep;

    public MockSerialPortReader(string filename, int sleep)
    {
        this.filename = filename;
        this.sleep = sleep;
    }

    public IEnumerable Lines
    {
        get
        {
            foreach (string line in File.ReadAllLines(filename))
            {
                Thread.Sleep(sleep);
                yield return line;
            }
        }
    }
}

A typical unit test using the mock reader:

[Test]
public void CanGetMeasurementsFromMockReaderDataSet1()
{
    NmeaParser parser = new NmeaParser();

    IEnumerable measurements = parser.ParseFrom(new MockSerialPortReader(dataSet1, 0));
    Assert.That(measurements, Is.Not.Null);
    CollectionAssert.IsNotEmpty(measurements);
    CollectionAssert.AllItemsAreInstancesOfType(measurements, typeof(GpsMeasurement));
    CollectionAssert.AllItemsAreNotNull(measurements);

    Console.WriteLine("{0}, {1}", measurements.Last().Latitude, measurements.Last().Longitude);
}

Displaying the Speedo Text

The final stage of this little mini-project was to knock up a user interface. I spent a while looking at how to get something working under X Windows, then decided to go back to the Old School and just use ASCII art.

First thing was to find a quick and dirty way to define how each big number would look. Each number is made up of a grid of characters, defined in a class:

public class TextConstants
{
    public static readonly string[] Zero = new[]
        {
            "   000000",
            "  00000000",
            " 000    000",
            " 000    000",
            " 000    000",
            " 000    000",
            "  00000000",
            "   000000"
        };

   ...etc etc...
}

Writing this out then becomes an exercise in text placement:

public void DrawSpeed(int speed)
{
    Console.Clear();

    DrawOutline();

    char[] asciiSpeed = speed.ToString("00").ToCharArray();

    int xOffset = 20;
    foreach (char c in asciiSpeed)
    {
        DrawNumber(TextConstants.For(c), xOffset, 8);
        xOffset += 15;
    }
}

private void DrawNumber(IEnumerable lines, int xOffset, int yOffset)
{
    int lineNo = 0;
    foreach (string line in lines)
    {
        WriteAt(line, xOffset, yOffset + lineNo);
        lineNo++;
    }
}

Did it Work?

Well, yes! The main issue was the update speed. This is because the old GPS module outputs data very very slowly and has quite a slow refresh rate. As a speedometer it wasn’t much good – it generally showed the speed I was doing about 15 seconds ago. As a project it worked brilliantly though.

I extended the code slightly to add some logging. This saved the location data to a set of simple, size-limited, CSV files on the Pi’s flash card. I then knocked up some more code to turn the measurements into a “Trip Report” using the Google Maps API. Top Notch!

A Trip To Work

I don’t drive The Duke that often because it’d cost a fortune and make me deaf. So to test his new GPS powered brain I have been collecting test data in Vinny the Vectra. I can then use this data to write some unit tests and develop cool stuff while stationary.

In a slack moment at work today I knocked up a couple of functions to detect traffic jams and visualise my speed on my way to work. Here’s a typical trip. Click on the pic or here to explore the interactive “report”.

speedmap.png

I quite like these static pages for output. They are very simple to create with some code and can be pushed up onto the web easily. No faffing about with databases, just some HTML and Javascript. On the map above the placemarks show places where I stopped for more than 30 seconds (there was traffic at junction 12 that morning!) and the colour of the line shows my speed. In the end I’d like to detect more events – when we go off road, when I brake suddenly or go round a corner too fast, when we get stuck in a traffic jam and when we visit places we know. I also got the webcam working on the raspberry pi, along with a wireless internet dongle so I can embed photos and videos then upload live and post on twitter.

Here’s some code snippets. I’m just playing, so please don’t think of me as somebody who’d ever return an “Enumerable Of Enumerables” in production code!  First is the function which splits the list of speed measurements based on bands of 10MPH…

        private IEnumerable<IEnumerable<GpsMeasurement>> SplitRouteBySpeed(IEnumerable route)
        {
            var bandedMeasurements = (from measurement in route
                                      select new { Band = (int) (measurement.GroundSpeedMph/10), Measurement = measurement }).ToList();

            int currentBand = int.MaxValue;
            List currentSection = new List();

            foreach (var bm in bandedMeasurements)
            {
                if(bm.Band != currentBand)
                {
                    currentBand = bm.Band;
                    if (currentSection.Count > 0)
                    {
                        currentSection.Add(bm.Measurement);
                        yield return currentSection;
                    }

                    currentSection = new List();
                }

                currentSection.Add(bm.Measurement);
            }
        }

Second is based on somebody else’s hard work really, but I changed it enough to make it worth posting here. I’m basically using speed as a percentage, squishing to a value between -1 and 1 then using four colour “axis” as beautifully described in the comment-linked blog.

        private string SpeedToColour(double groundSpeedMph)
        {
            // Based on this post - which has a very cool image to show what we're doing.
            // http://slged.blogspot.co.uk/2007/03/heat-map-code-snippet.html

            double fraction = (groundSpeedMph             
            double red, green, blue;

            if ( fraction < -0.5 )
            {
                red = 0.0;
                green = 2*(fraction + 1);
                blue = 1.0;
            }
            else if ( fraction < 0 )
            {
                red = 0.0;
                green = 1.0;
                blue = 1.0 - 2.0*(fraction + 0.5);
            }
            else if ( fraction < 0.5 )
            {
                red = 2.0*fraction;
                green = 1.0;
                blue = 0.0;
            }
            else
            {
                red = 1.0;
                green = 1.0 - 2.0*(fraction - 0.5);
                blue = 0.0;
            }

            byte redByte = (byte) (255 * red);
            byte greenByte = (byte) (255 * green);
            byte blueByte = (byte) (255 * blue);

            return string.Format("#{0}{1}{2}", redByte.ToString("x2"), greenByte.ToString("x2"), blueByte.ToString("x2"));
        }