SQL Server 2008

http://www.microsoft.com/sqlserver/2008/en/us/default.aspx

T-SQL Tuesday 2 Spatial Puzzles

SQLBlog blogger Adam Machanic recently posted an invitation for people to contribute articles on the subject of "puzzling" T-SQL, as part of T-SQL Tuesday.

SQL Bits V

SQLBits logo
Many thanks to everyone who attended my session at SQL Bits V, and also my thanks to the organisers - another great conference and what an amazing venue (and good food... love the afternoon Welsh cakes!)

Here are the links to my slide deck, and an associated sql file for "Creating High Performance Spatial Databases with SQL Server 2008". If you have any questions, please feel free to contact me.

Marking out geography shapes on Virtual Earth

In Chapter 5 of "Beginning Spatial with SQL Server 2008", I show you how to use Virtual Earth as a drawing canvas onto which you can draw Points, LineStrings, and Polygons, and then retrieve the WKT of the shape coordinates for use in SQL Server 2008.
Because Virtual Earth is a flat 2d map, this technique works very well for the geometry datatype, but what about the geography datatype?
Suppose that you had drawn a LineString connecting the points at (34,-118) and (51.5,0) representing a flight from Los Angeles International airport to Heathrow, London.

Plotting Geography LineStrings on Google Maps and Virtual Earth

One problem of passing geography representations from SQL Server to web-mapping services such as Virtual Earth or Google Maps is that they treat the data as lying on a flat plane. This is fine for the geometry datatype, but presents a problem when trying to present geography data.

To demonstrate, consider the route taken by an aeroplane flying from Los Angeles (Latitude 34N, Longitude 118W) to London (Latitude 51.5N, Longitude 0), using the geography datatype.

SELECT
geography::STLineFromText('LINESTRING(-118 34, 0 51.5)', 4326)

Visualising this LineString using the spatial results tab in SQL Server Management Studio gives the correct result that, when projected onto a Mercator projection map, appears as shown below:

The problem is, when creating the equivalent Polyline constructor in Google Maps or Virtual Earth from this WKT, the default behaviour is to create a straight line between the points (34,-118) and (51.5,0) along the flat map plane, in which case you end up with the following route:

var polyline = new VEShape(VEShapeType.Polyline, [new VELatLong(34,-118), new VELatLong(51.5,0)]);
map.AddShape(polyline);

The solution

To get the map to correctly plot a linestring representing the great circle route between two points rather than simply a straight line on the map requires a different approach for Google Maps than for Virtual Earth.

Google Maps

The Google Maps Polyline constructor allows you to specify an option of geodesic: true, which will accurately portray the great circle route, as follows:

var polyline = new google.maps.Polyline(
  [new google.maps.LatLng(34,-118), new google.maps.LatLng(51.5,0)],
  "#ff0000",
  3,
  1,
  {geodesic:true});
map.addOverlay(polyline);

This results in the following map:

Virtual Earth

For Virtual Earth it's a little bit more complicated, because there is no inbuilt ability to draw geodesic shapes - you have to create your own function that calculates a number of waypoints along the length of the LineString, and then plots the segments between each waypoint to approximate the great cirlce route. That technique is demonstrated in the following code:
function AddGeodesicPolyline(start, end) {
  var segments = 32; // The number of line segments used to approximate the true curved route 
  var latLongs = new Array();
  with (Math) {
    // Convert all coordinates to Radians
    var lat1 = start.Latitude * (PI / 180);
    var lon1 = start.Longitude * (PI / 180);
    var lat2 = end.Latitude * (PI / 180);
    var lon2 = end.Longitude * (PI / 180);
    // Calculate the total extent of the route
    var d = 2 * asin(sqrt(pow((sin((lat1 - lat2) / 2)), 2) + cos(lat1) * cos(lat2) * pow((sin((lon1 - lon2) / 2)), 2)));
    // Calculate the position at fixed intervals along the route
    for (var n = 0; n < segments + 1; n++) {
      var f = (1 / segments) * n;
      f = f.toFixed(6);
      var A = sin((1 - f) * d) / sin(d)
      var B = sin(f * d) / sin(d)
      // Calculate 3D Cartesian coordinates of the point
      var x = A * cos(lat1) * cos(lon1) + B * cos(lat2) * cos(lon2)
      var y = A * cos(lat1) * sin(lon1) + B * cos(lat2) * sin(lon2)
      var z = A * sin(lat1) + B * sin(lat2)
      // Convert these to latitude/longitude
      var lat = atan2(z, sqrt(pow(x, 2) + pow(y, 2)))
      var lon = atan2(y, x)
      // Create a VELatLong representing this location (remember to convert back to degrees)
      var p = new VELatLong(lat / (PI / 180), lon / (PI / 180));
      // Add this to the array
      latLongs.push(p);
    }
  }
  // Create a new linestring from the LatLong array and add it to the map
  var linestring = new VEShape(VEShapeType.Polyline, latLongs);
  map.AddShape(linestring);
}

This is shown in the following map:

Creating 3D Prism Maps using Google Earth

Prism maps are a type of thematic map that display extruded three-dimensional features to represent the value of an underlying variable. In this post, I'll shown you how to create a prism map using SQL Server 2008 and Google Earth, to take the results of a query like that this:

and make them into something much more exciting, like this:

To view this example you need to get the Google Earth plugin, which is currently available for Firefox, IE6, and IE7 on Windows, and Safari and Firefox on Mac OSX. If you can't see the globe above, click here to see what you're missing.

To recreate the example shown here, follow these steps:

1. Create and Populate the Source Table

This prism map displays data representing the ecological footprint of different countries around the globe. Ecological footprint is a measure of sustainability that represents the area of land required to produce the materials used by each member of the population, and to absorb the waste they produce. It is measured in hectares per capita. You can download a dataset detailing the ecological footprint of various countries from the NationMaster website.

Having acquired the statistical data to plot on the map, we also need to use a set of shape data (i.e. geometry / geography polygons) representing the countries of the world. For thematic mapping purposes, we don't need these shapes to be very accurate, so I started with the excellent simple world borders dataset from thematicmapping.org. I imported the data into SQL Server using Shape2SQL (part of SQL Spatial Tools) and then made a few tweaks to ensure that all of the polygons could be represented using the geography datatype (corrected ring orientation, self-intersections etc.) before joining each of the shapes to their associated footprint data.

You can download the script used to create and populate the final table from the following link: EcoFootPrint.zip

2. Create the Stored Procedure

To create the stored procedure, execute the following code in SQL Server Management Studio:

CREATE PROCEDURE [dbo].[uspEcoFootprint]
AS
BEGIN
SELECT
      COUNTRYNAME AS Title,
      'Footprint: ' + CAST(FOOTPRINT AS VARCHAR(32)) + ' hectares per capita'  AS Description, 
      SHAPE AS Shape,
      100000 + CAST(COALESCE(FOOTPRINT,0)*100000 AS INT) AS Elevation,
      COALESCE(
        sys.fn_varbintohexstr(
          CAST(200 AS BINARY(1)) -- Alpha
          + CAST(100 AS BINARY(1)) -- Blue
          + CAST(200 - CAST(FootPrint / (SELECT MAX(FootPrint) FROM EcoFootprint) * 128 AS INT) AS BINARY(1)) -- Green
          + CAST(127 + CAST(FootPrint / (SELECT MAX(FootPrint) FROM EcoFootprint) * 128 AS INT) AS BINARY(1)) -- Red
        ),
        '0xff808080' -- Grey if no data available
      ) AS Color
  FROM EcoFootPrint
END

This is just a normal SELECT statement that returns the various columns of information we will need to build the prism map.

  • The TITLE and DESCRIPTION columns will be used to populate a tooltip window when you click on any of the countries of the map
  • The SHAPE column returns the geography polygon data in raw format, which we will later manpulate into the KML required by Google Earth using a .NET handler
  • The ELEVATION column calculates the amount by which each country should be extruded in the resulting map. The footprint of each country is multiplied by a scale factor of 100,000 and a base level of 100,000 is applied to every country to give a good range of values in the result set. The result is CAST as an integer since Google Earth will expect the elevation of each feature to be measure in a whole number of units.
  • The final expression determines the colour in which each country will be shaded on the globe. The result is a 4-byte hexadecimal value in the format 0xFFFFFFFF where the bytes are in listed in Alpha (Opacity), Blue, Green Red order. Note that this is not RGB, as you may be used to. Countries with a small footprint are shaded light green. As the footprint increases, the colour turns increasingly red (and less green).

3. Create the Visual Studio Website

  1. Click File -> New -> Website
  2. Highlight Empty Website, select Language C#
To use the native SqlGeometry and SqlGeography datatypes from within the .NET handler, you need to add a reference to the SqlServer.Types.dll assembly, as follows:
  1. Click Website -> Add Reference
  2. From the .NET tab, scroll down and highlight Microsoft.SqlServer.Types
  3. Click OK
Add a new HTML page to the website (Website -> Add New Item -> HTML Page) and enter the following code:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script src="http://www.google.com/jsapi?key=ABQIAAAA_ign6iVyyoQkHVSOpMADihRLUAcsATA5ZNXYi8LSZQTvgk_wHxTfH2nhvU3F7BlRO8Je5tPGG2kHCA"></script>
    <script src="JScript.js"></script></head>
</head>
<body onload="init()">
<div id='map3d_container' style='height: 500px; width: 658px;'>
<div id='map3d' style='height: 100%;'></div></div>
</body>
</html>
</body>
</html>
Next, add a new javascript page to the website (Website -> Add New Item -> JScript file). Make sure it is called the same name as referenced in the HTML file above (JScript.js, by default). Enter the following code:
google.load("earth", "1");
 
var ge = null;
 
function init() {
  google.earth.createInstance("map3d", initCallback, failureCallback);
}
 
function initCallback(object) {
  ge = object;
  ge.getWindow().setVisibility(true);
  var networkLink = ge.createNetworkLink("");
  networkLink.setFlyToView(true);
  var link = ge.createLink("");
  link.setHref("http://YOURNETHANDLERHERE.ashx");
  networkLink.setLink(link);
  ge.getFeatures().appendChild(networkLink);
}
 
function failureCallback(object) {
}
Remember to change the href to match the URL of the handler! Finally, create a new generic handler, using the C# language, and enter the following:
<%@ WebHandler Language="C#" Class="Handler" %>
 
using System;
using System.Web;
using System.Text; // Required to build the string
using Microsoft.SqlServer.Types; // Required to access native geometry and geography types
using System.Data.SqlClient; // Required to set up a SQL connection
using System.Data; // Required to set the command type
using System.Xml; // Required to create XML response
 
public class Handler : IHttpHandler
{
 
  public void ProcessRequest(HttpContext context)
  {
    //Set the MIME type for KML
    context.Response.ContentType = "application/vnd.google-earth.kml+xml";
 
    XmlDocument kmlDoc = new XmlDocument();
    XmlDeclaration xmlDeclaration = kmlDoc.CreateXmlDeclaration("1.0", "utf-8", null);
    // Create the root element
    XmlElement rootNode = kmlDoc.CreateElement("kml");
    rootNode.SetAttribute("xmlns", @"http://www.opengis.net/kml/2.2");
    kmlDoc.InsertBefore(xmlDeclaration, kmlDoc.DocumentElement);
    kmlDoc.AppendChild(rootNode);
    XmlElement documentNode = kmlDoc.CreateElement("Document");
    rootNode.AppendChild(documentNode);
 
 XmlElement styleNode = kmlDoc.CreateElement("Style");
 styleNode.SetAttribute("id", @"examplePolyStyle");
 documentNode.AppendChild(styleNode);
 
      XmlElement polystyleNode = kmlDoc.CreateElement("PolyStyle");
      styleNode.AppendChild(polystyleNode);
 
      XmlElement colorNode = kmlDoc.CreateElement("color");
      polystyleNode.AppendChild(colorNode);
      colorNode.InnerText="ff0000cc";
 
      XmlElement colorModeNode = kmlDoc.CreateElement("colorMode");
      polystyleNode.AppendChild(colorModeNode);
      colorModeNode.InnerText = "random";
 
    SqlConnection myConn = new SqlConnection("server=ENTERYOURSERVERNAMEHERE;Trusted_Connection=yes;database=Spatial");
    //Open the connection
    myConn.Open();
    //Define the stored procedure to execute
    String myQuery = "dbo.uspEcoFootprint";
    SqlCommand myCMD = new SqlCommand(myQuery, myConn);
    myCMD.CommandType = System.Data.CommandType.StoredProcedure;
    //Create a reader for the resultset
    SqlDataReader myReader = myCMD.ExecuteReader();
 
    while (myReader.Read())
    {
      SqlGeography shape = new SqlGeography();
      shape = (SqlGeography)myReader["Shape"];
 
      XmlElement placeMarkNode = kmlDoc.CreateElement("Placemark");
      documentNode.AppendChild(placeMarkNode);
 
      // Create the required nodes
      XmlElement nameNode = kmlDoc.CreateElement("name");
      XmlText nameText = kmlDoc.CreateTextNode(myReader["Title"].ToString());
      placeMarkNode.AppendChild(nameNode);
      nameNode.AppendChild(nameText);
 
      XmlElement descNode = kmlDoc.CreateElement("description");
      XmlText descriptionText = kmlDoc.CreateTextNode(myReader["Description"].ToString());
      placeMarkNode.AppendChild(descNode);
      descNode.AppendChild(descriptionText);
 
      XmlElement styleUrlNode = kmlDoc.CreateElement("styleUrl");
      XmlText styleUrlText = kmlDoc.CreateTextNode("#examplePolyStyle");
      placeMarkNode.AppendChild(styleUrlNode);
      styleUrlNode.AppendChild(styleUrlText);
 
      switch (shape.STGeometryType().ToString())
      {
        case "Point":
          placeMarkNode.AppendChild(createPoint(kmlDoc, shape, myReader["FootPrint"].ToString()));
          break;
        case "LineString":
          placeMarkNode.AppendChild(createLineString(kmlDoc, shape, myReader["FootPrint"].ToString()));
          break;
        case "Polygon":
          placeMarkNode.AppendChild(createPolygon(kmlDoc, shape, myReader["FootPrint"].ToString()));
          break;
        case "MultiPoint":
 
        case "MultiLineString":
        case "MultiPolygon":
        case "GeometryCollection": 
          XmlElement multiGeom = kmlDoc.CreateElement("MultiGeometry");
          placeMarkNode.AppendChild(multiGeom);
          for (int g = 1; g <= shape.STNumGeometries(); g++)
          {
            switch (shape.STGeometryN(g).STGeometryType().ToString())
            {
              case "Point":
                multiGeom.AppendChild(createPoint(kmlDoc, shape.STGeometryN(g), myReader["FootPrint"].ToString()));
                break;
              case "LineString":
                multiGeom.AppendChild(createLineString(kmlDoc, shape.STGeometryN(g), myReader["FootPrint"].ToString()));
                break;
              case "Polygon":
                multiGeom.AppendChild(createPolygon(kmlDoc, shape.STGeometryN(g), myReader["FootPrint"].ToString()));
                break;
            }
          }
          break;
      }
    }
    //Close the reader
    myReader.Close();
    //Close the connection
    myConn.Close();
 
    XmlTextWriter writer = new XmlTextWriter(context.Response.OutputStream, null);
    kmlDoc.WriteTo(writer);
    writer.Flush();
 
  }
 
  public XmlElement createPoint(XmlDocument kmlDoc, SqlGeography shape, String Z)
  {
    XmlElement pointNode = kmlDoc.CreateElement("Point");
    XmlElement coordsNode = kmlDoc.CreateElement("coordinates");
    coordsNode.InnerText = string.Format("{0},{1},{2}", shape.Lat.ToString(), shape.Long.ToString(), Z);
    pointNode.AppendChild(coordsNode);
    return pointNode;
  }
 
  public XmlElement createLineString(XmlDocument kmlDoc, SqlGeography shape, String Z)
  {
    XmlElement lineStringNode = kmlDoc.CreateElement("LineString");
    XmlElement coordsNode = kmlDoc.CreateElement("coordinates");
    StringBuilder coordsString = new StringBuilder("");
    for (int i = 1; i <= shape.STNumPoints(); i++)
    {
      coordsString.Append(string.Format("{0},{1},{2}", shape.STPointN(i).Long.ToString(), shape.STPointN(i).Lat.ToString(), Z));
      coordsString.Append(Environment.NewLine);
    }
    coordsNode.InnerText = coordsString.ToString();
    lineStringNode.AppendChild(coordsNode);
    return lineStringNode;
  }
 
  public XmlElement createPolygon(XmlDocument kmlDoc, SqlGeography shape, String Z)
  {
    XmlElement polygonNode = kmlDoc.CreateElement("Polygon");
 
    XmlElement extrudeNode = kmlDoc.CreateElement("extrude");
    XmlText extrudeText = kmlDoc.CreateTextNode("1");
    extrudeNode.AppendChild(extrudeText);
    polygonNode.AppendChild(extrudeNode);
 
    XmlElement altitudeNode = kmlDoc.CreateElement("altitudeMode");
    XmlText altitudeText = kmlDoc.CreateTextNode("relativeToGround");
    altitudeNode.AppendChild(altitudeText);
    polygonNode.AppendChild(altitudeNode);
 
    XmlElement outerboundaryNode = kmlDoc.CreateElement("outerBoundaryIs");
    polygonNode.AppendChild(outerboundaryNode);
 
    XmlElement linearRingNode = kmlDoc.CreateElement("LinearRing");
    outerboundaryNode.AppendChild(linearRingNode);
 
    XmlElement coordsNode = kmlDoc.CreateElement("coordinates");
    StringBuilder coordsString = new StringBuilder("");
    for (int k = 1; k <= shape.RingN(1).STNumPoints(); k++)
    {
      coordsString.Append(string.Format("{0},{1},{2}", shape.RingN(1).STPointN(k).Long.ToString(), shape.RingN(1).STPointN(k).Lat.ToString(), Z));
      coordsString.Append(Environment.NewLine);
    }
    coordsNode.InnerText = coordsString.ToString();
    linearRingNode.AppendChild(coordsNode);
    return polygonNode;
  }
  public bool IsReusable
  {
    get
    {
      return false;
    }
  }
 
}
Remember to change the credentials and connection settings to match your SQL Server! Finally, save all the files, and click 'Preview in Browser'.

Creating Chloropleth Maps with Virtual Earth

This page gives a practical example demonstrating a number of different techniques introduced in the book. The objective is to use SQL Server and Virtual Earth to create a chloropleth map of the United States, where each state is shaded blue or red according to whether the Democratic Party or Republican Party won most votes in that state in the 2008 US presidential election.

The finished product

The key steps involved are as follows:
  1. Obtain and combine two sets of publicly-available data (US State outlines and Election Data)
  2. Reproject US State data into the correct SRID
  3. Import Shapefile data into SQL Server 2008 using Shape2SQL
  4. Create a stored procedure that retrieves the information
  5. Create a .NET handler that executes the stored procedure, retrieves the results, and manipulates them into the equivalent constructor methods for Virtual Earth
  6. Create an HTML page that contains a Virtual Earth map object, and add the necessary JavaScript functions to call the .NET handler
All of these techniques are covered in detail in "Beginning Spatial with SQL Server 2008", so I'll just outline the required code.

1. Obtain the required data

You can get a zipfile containing the cartographic boundary files representing all 50 US States from the US government census website, here: http://www.census.gov/geo/cob/bdy/st/st00shp/st99_d00_shp.zip You also need to get the voting data for each state. There are lots of sources of this on the internet. I used the table of results by state on the following wikipedia page: http://en.wikipedia.org/wiki/United_States_presidential_election,_2008#R...

2. Reproject the shapefile data

The US Census Shapefile data is supplied using EPSG:4269 (NAD83). However, we want to display the results of Virtual Earth, for which we need to supply coordinates using EPSG:4326 (WGS 84). Therefore, we first need to reproject the shapefile. Unfortunately SQL Server 2008 doesn't natively support reprojection of spatial data, but there are a number of external tools that you can use to do so. I recommend either Safe FME (commercial) or FWTools .

3. Importing the data

Firstly, you need to import the converted EPSG:4326 data using Shape2SQL. Since we won't be performing any spatial operations on the data other than simply retrieving the WKT representation to pass to Virtual Earth, it doesn't matter whether you store the state outlines using a geometry or a geography column. Seeing as EPSG:4326 is a geographic coordinate system, it's best practice to use geography though. Once you have imported the shapefile, you need to add a new column to contain the result of the electoral vote. For this example, I'll just use a simple CHAR(1) column called ELEC_RESULT that will either contain the value 'D' or 'R' (depending on whether the Democrats or Republicans won that state). You could, of course, maintain the exact number of votes cast for each party and use that to shade the results accordingly. If you want, you can download the following SQL script to create the table for you. US Election Data script.

4. Create the stored procedure

The stored procedure will retrieve the results from the table and pass them to the .NET handler. There's no special magic done by the stored procedure - it just retrieves the election result and the WKT representation of the shape of each state. It is the role of the .NET handler to take the results returned by the stored procedure and turn them into the appropriate constructor functions for Virtual Earth.
CREATE PROCEDURE [dbo].[uspUSElection2008] AS
BEGIN
SELECT
  STATE_SHAPE.STGeometryType() AS GeometryType,
  STATE_SHAPE.STAsText() AS WKT,
  STATE_NAME AS Title,
  ELEC_RESULT
FROM
  Election_08
END
GO

5. Create the .NET handler

The .NET handler creates a connection to SQL Server, executes the stored procedure, and loops through the resultset. It manipulates the WKT representation of each state to the appropriate syntax required to create a Virtual Earth polygon representating that state. It then sets the fill color of each polygon based on whether the value in the election result column was 'D' or 'R'. Website -> Add New Item -> Generic Handler For this example, I'll use Visual Basic .NET and call the handler USElection2008.ashx
<%@ WebHandler Language="VB" Class="USElection2008" %>
 
Imports System
Imports System.Web
Imports System.Data.SqlClient
Imports System.Configuration
Imports System.Text
 
Public Class USElection2008 : Implements IHttpHandler
 
    Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
 
    'The response will be javascript
    context.Response.ContentType = "text/javascript"
 
    'Tell the browser not to cache the results
    context.Response.CacheControl = "no-cache"
    context.Response.Expires = 0
 
    'Define the variables
    Dim JS As String = "" 'The javascript we will build and send back
    Dim WKT As String 'The original WKT representation
    Dim VE As String 'The VE equivalent representation
    Dim VEShapeType As String 'The type of VE geometry
    Dim shapeid As Integer = 0 'Shape counter
    Dim FillColour As String = ""
    Dim LineColour As String = ""
 
    'Set up a connection to the server
    Dim myConn = New SqlConnection("server=ecco;" & _
                                   "Trusted_Connection=yes;" & _
                                   "database=Spatial")
 
    'Open the connection
    myConn.Open()
 
    'Define the stored prcedure to execute
    Dim myQuery As String = "dbo.uspUSElection2008"
    Dim myCMD As New SqlCommand(myQuery, myConn)
    myCMD.CommandType = Data.CommandType.StoredProcedure
 
    'Create a reader for the resultset
    Dim myReader As SqlDataReader = myCMD.ExecuteReader()
 
    'Go through the results
    While myReader.Read()
 
      Select Case myReader("ELEC_RESULT").ToString
        Case "D"
          FillColour = "new VEColor(0,0,255,0.7)"
          LineColour = "new VEColor(255,255,255,0.7)"
        Case "R"
          FillColour = "new VEColor(255,0,0,0.7)"
          LineColour = "new VEColor(255,255,255,0.7)"
      End Select
 
      Select Case myReader("GeometryType").ToString
        Case "Polygon"
          'The Virtual Earth equivalent of a POINT is a Pushpin
          VEShapeType = "VEShapeType.Polygon"
          'Get the WKT representation of the object
          WKT = myReader("WKT").ToString
          'Replace the double brackets which surround the co-ordinate point pairs
          WKT = Replace(WKT, "POLYGON ((", "")
          'Remove the last double brackets
          WKT = Replace(WKT, "))", "")
          'Create an array of each point in the polygon   
          Dim PointArray() As String = Split(WKT, ",")
          'Build the VE definition
          VE = ""
          Dim i As Integer = 0
          While i <= PointArray.Length - 1
            Dim Coords() = Split(Trim(PointArray(i)), " ")
            VE = VE + "new VELatLong(" + Coords(1) + "," + Coords(0) + "),"
            i = i + 1
          End While
          'Remove the last trailing comma
          VE = Left(VE, VE.Length - 1)
 
          'Add the constructor for the new shape
                    JS += "var shape" + shapeid.ToString & " = new VEShape(" + VEShapeType + ", [" + VE + "]);"
          'Don't show the icon for polygons
          JS += "shape" + shapeid.ToString + ".HideIcon();"
          'Set the line and fill colour
          JS += "shape" + shapeid.ToString & ".SetLineColor(" + LineColour + ");"
          JS += "shape" + shapeid.ToString & ".SetFillColor(" + FillColour.ToString + ");"
          'Add this shape to the map
          JS += "map.AddShape(shape" + shapeid.ToString + ");"
 
        Case "MultiPolygon"
          'The Virtual Earth equivalent of a POINT is a Pushpin
          VEShapeType = "VEShapeType.Polygon"
          'Get the WKT representation of the object
          WKT = myReader("WKT").ToString
          'Replace the triple brackets which surround the co-ordinate point pairs
          WKT = Replace(WKT, "MULTIPOLYGON (((", "")
          'Remove the last triple brackets
          WKT = Replace(WKT, ")))", "")
          'Create an array of each polygon
          Dim PolyArray() As String = Split(WKT, ")), ((")
          Dim k As Integer = 0
          While k < PolyArray.Length
            'Create an array of each point in the polygon   
            Dim PointArray() As String = Split(PolyArray(k), ",")
            'Build the VE definition
            VE = ""
            Dim i As Integer = 0
            While i <= PointArray.Length - 1
              Dim Coords() = Split(Trim(PointArray(i)), " ")
              VE = VE + "new VELatLong(" + Coords(1) + "," + Coords(0) + "),"
              i = i + 1
            End While
            'Remove the last trailing comma
            VE = Left(VE, VE.Length - 1)
            'Add the constructor for the new shape
            JS += "var shape" + shapeid.ToString & " = new VEShape(" + VEShapeType + ", [" + VE + "]);"
            'Don't show the icon for polygons
            JS += "shape" + shapeid.ToString + ".HideIcon();"
            'Set the line and fill colour
            JS += "shape" + shapeid.ToString & ".SetLineColor(" + LineColour + ");"
            JS += "shape" + shapeid.ToString & ".SetFillColor(" + FillColour.ToString + ");"
            'Add this shape to the map
            JS += "map.AddShape(shape" + shapeid.ToString + ");"
            'Go to the next polygon in the multipolygon collection
            k = k + 1
          End While
 
      End Select
 
      'Increment the shape counter
      shapeid = shapeid + 1
 
    End While
 
    'Close the reader
    myReader.Close()
    'Close the connection
    myConn.Close()
 
    'Output all of the javascript to create all the objects
    context.Response.Write(JS)
 
  End Sub
 
  Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
    Get
      Return False
    End Get
  End Property
 
End Class

6. Create the HTML page and JavaScript

Finally, we need to create the webpage that will contain the Virtual Earth control, and get it to call the .NET handler (via AJAX), and eval() the results to add them to the map. Website -> Add New Item -> HTML Page -> USElection2008.htm
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>US Presidential Election 2008 Chloropleth Map</title>
  <script src="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2" type="text/javascript"></script>
  <script src="USElection2008.js" type="text/javascript"></script>
</head>
<body>
  <div id="divMap" style="position:absolute; width:640px; height:480px;"></div>
</body>
</html>
// Declare the global map object
var map = null;
// Set the Load callback
if (window.addEventListener) { window.addEventListener("load", getMap, false); }
else if (window.attachEvent) { window.attachEvent("onload", getMap); }
// Set the unLoad callback
if (window.addEventListener) { window.addEventListener("unload", disposeMap, false); }
else if (window.attachEvent) { window.attachEvent("onunload", disposeMap); }
 
// This function is called when the page has been loaded
function getMap() {
    // Create a new map object in the divMap container
    map = new VEMap('divMap');
    // Configure the initial map view
    map.LoadMap(new VELatLong(40, -100), 3, VEMapStyle.Road);
    // Call the loadCountryData function when the user clicks the map
    loadElectionData();
}
 
// This function is called when the page is unloaded
function disposeMap() {
    // Release all resources assigned to the map
    map.Dispose();
    // Unset the map variable
    map = null;
}
 
function loadElectionData() {
    //Get the appropriate XMLHTTP object for the browser
    var xmlhttp = GetXmlHttp();
    // If we have a valid XMLHTTP object
    if (xmlhttp) {
        // Open an XmlHTTP request to the .NET handler
        xmlhttp.open("GET", "./USElection2008.ashx", true);
        // Fire this when the readyState of the request changes
        xmlhttp.onreadystatechange = function() {
            // readystate 4 indicates that the request is complete  
            if (xmlhttp.readyState == 4) {
                // Read in the javascript response from the handler
                var result = xmlhttp.responseText;
                // Update the status message
                window.status = 'Loading Data...';
                try {
                    // Execute the dynamically created javascript
                    eval(result);
                    // Update the status message
                    window.status = 'Data Loaded!';
                }
                catch (e) {
                    // If the response cannot be evaluated
                    window.status = 'Data could not be loaded.';
                    alert(e.description);
                }
            }
        }
        // Send the request
        xmlhttp.send(null)
    }
}
 
// This function creates a cross-browser AJAX object
function GetXmlHttp() {
    var x = null;
    try { x = new ActiveXObject("Msxml2.XMLHTTP"); }
    catch (e) {
        try { x = new ActiveXObject("Microsoft.XMLHTTP"); }
        catch (e) { x = null; }
    }
    if (!x && typeof XMLHttpRequest != "undefined") {
        x = new XMLHttpRequest();
    }
    return x;
}

Reprojecting features using OGR

SQL Server supports spatial data defined in a variety of different spatial reference systems - using both projected coordinates (the geometry datatype) and geographic coordinates (the geography datatype). However, it doesn't include the ability to transform, or reproject, data between difference spatial reference systems - to do this, you must use external tools.

Fixing Invalid Geography Data

One of my fellow forumites over at the SQL Server Spatial Forum has been having a few headaches when trying to import shapefile data in to the geography datatype, so this post is designed to explain some of the pitfalls you may come across, and how to resolve them.

Calculating the Bearing from One Point to Another

Sometimes it is helpful to know the bearing (i.e. the direction) of a path between two points.
Syndicate content