Showing ESRI Shapefile Layers on a Virtual Earth Map: a simple HowTo

If you've ever tried to find a way to display an ESRI ShapeFile on a Virtual Earth map, you'll have probably noticed how little information there is on the Internet about how to accomplish this apparently arduous task.

As a matter of fact, I needed to do exactly this while adding some collateral features to the software Serena and I are developing for our thesis, but after some Googling, I found that there was (and there still is) almost no useful information on how to achieve integration between Virtual Earth and ESRI Shapefiles.

So I had to figure out what I judge a rather clean solution by myself. After all, I thought I could fill the void on this topic and help someone else avoid to switch "lazy mode" off by writing an article about it ;)

By the way, with the technique I'm describing, you're going to be able to not only display Shapefiles as layers on Virtual Earth, but also interact with them in countless ways: extract data from them, show data associated with a shape when you click on it on the map, even modify or create new Shapefiles (Although only showing and querying the map are going to be covered in this post). All of this care of SharpMap, one of the best (if not the best) open source .NET libraries for GIS.

Actually, SharpMap does much, much more than this... it even has a performing rendering engine for spatial data, so it really is an understatement to think of it as a mere instrument to get ESRI data into our Virtual Earth maps. In fact I strongly recommend to go and give a look at the SharpMap project page on CodePlex.

SharpMap versions

SharpMap 2.0 is currently in beta stage. The currently stable release is 0.9. Version 2.0 beta is not radically different from version 0.9, at least for the parts of the library that we're going to use, so feel free to resort to the stable version if you feel more at ease, but I thought worthy to use version 2.0 beta in my project because of a rather important addition, i.e. the possibility to write on Shapefiles as well as read them, which to my knowledge wasn't present in the 0.9 version. Obviously, if your application doesn't require to modify or create Shapefiles, you can safely fall back to the 0.9 version.

 

A Shapefile containing Great Britain's Administrative Areas

 

The idea.

Let's suppose that we have a Shapefile containing polygons which represent the administrative regions of the United Kingdom, and that we want to overlay that on the map.

 

First of all we need to extract the polygon data from the Shapefile, and convert all the polygons to a format readable by Virtual Earth. Each "part" in the Shapefile will become a "polygon shape" in Virtual Earth. In this phase, we will leverage SharpMap's ability to parse Shapefiles to .NET objects.

 

 

public List<Shape> LoadShapefileLayer(string filePath) 

   
Random random = new Random((int)DateTime.Now.Ticks); 
   
// SharpMap's Shapefile reader / writer
    ShapeFileProvider sf = null
   
try 
    { 
        sf =
new ShapeFileProvider(filePath); 
        sf.Open(
false); 
        if (sf.ShapeType != ShapeType.Polygon) 
           
return null

       
// Initializing the list of Shapes to be returned 
        List<Shape> layer = new List<Shape>(); 
       
// Retrieving all the geometries in the Shapefile
        BoundingBox ext = sf.GetExtents(); 
       
IEnumerable<Geometry> geometries = sf.ExecuteGeometryIntersectionQuery(ext); 
       
// Iterating through all the geometries in the shapefile, converting them 
        // to Shape objects and adding them to the output list
        foreach (Geometry geometry in geometries) 
        { 
           
List<PointF> points = new List<PointF>(); 
           
Polygon polygon = geometry as SharpMap.Geometries.Polygon
           
/* Verify that the geometry is actually a polygon. If it is not, just skip it. 
             * Multipolygon support is not yet added */ 
            if (polygon != null
            { 
               
foreach (var vertex in polygon.ExteriorRing.Vertices) 
                    points.Add(
new PointF((float)vertex.X, (float)vertex.Y)); 
               
                /* Each shape is assigned a random colour, to create
                 * the effect of a political map */
                Color randomColour = Color.FromArgb(75, 
                    random.Next(255), random.Next(255), random.Next(255)); 
                layer.Add(
new Shape(points, randomColour)); 
            } 
        } 
       
return layer; 
    } 
   
finally 
    { 
       
if (sf != null
            sf.Close(); 
    } 
}

This example only covers the importing of Polygon-based Shapefiles, but it can easily be extended to support Polylines and Points

Next, we need to make this data available to the Virtual Earth Map. We can do this by leveraging ASP.NET AJAX ScriptServices. To make a long story short, in order to use the converted data with the Virtual Earth API, on the client side, we need to create an ASP.NET web service, and decorate it with the [ScriptService] attribute. This will instruct the ASP.NET engine to create a Javascript helper class and make this method callable from client code in the page.

If you want to know more about the details of this process, the underlying communication protocol and different ways to leverage it, I recommend you to read my series of posts on "How to integrate JSON web services in an ASP.NET AJAX web application" here. You could also find my previous post on how to create a simple Virtual Earth Mashup useful. You can find it here

 

[WebService]
[
WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[
ScriptService]
public class MapService : WebService
{
    [
WebMethod]
   
public List<Shape> GetMapData(string fileName)
    {
       
// ShapefileReader is a simple class containing
        // the Shapefile reading and querying methods
        ShapefileReader sr = new ShapefileReader();
       
return sr.LoadShapefileLayer(Server.MapPath("~/Maps/" + fileName));
    }
}

This web method will be callable with extreme simplicity from the client side:

function LoadShapefile(fileName)
{
   
/* The shapefileName global variable holds the filename of the
     * last loaded Shapefile for future reference (i.e. querying the map) */
    shapefileName = fileName;
   
/* Here we call the GetMapData asmx ScriptService, passing fileName as
     * a parameter. GetESRIData_success is a callback that will be executed
     * upon successful completion of the method call, and will be passed the
     * return data as parameter. onFailed, on the other side, will be executed
     * in case of failed request */
    MapService.GetMapData(fileName, GetESRIData_success, onFailed);
   
// MessageBox is a function that displays a nice informative box on the map
    MessageBox(true, "Please wait while retrieving Shapefile data...");
}

Now that the data is available to the client side, we need to go through all the Shape objects and actually convert them to Virtual Earth shapes.

In case of successful completion of the method call, the GetESRIData_success callback will invoke the AddLayer() method, passing it the received object; AddLayer() will perform the conversion before actually drawing the shapes on the map.

function GetESRIData_success(e)
{    
   
if (e!= null)  
        AddLayer(e);
}

function onFailed(e)
{
    MessageBox(
true, "Error while connecting to the remote web service. Please try again later.", 5000);
}

function AddLayer(jsonLayer)
{
   
var polygons = ConvertShapes(jsonLayer);
    
   
for (var i = 0; i < polygons.length; i++)
    {
        DrawPolygon(polygons[i]
);
    }
    MessageBox(
true, String.format("{0} polygons drawn on the map", polygons.length), 2000);
}

The conversion from custom list of deserialized .NET Shape objects to array of actual Virtual Earth VEShape objects will be performed in two steps: first of all, the ConvertShapes() method will convert the list of deserialized Shape objects to an array of JavaScript "polygon" objects, each containing an array of VELatLong objects and a colour for the shape:

function ConvertShapes(layer)
{
   
var polygons = new Array();
   
for (var i = 0; i < layer.length; i++)
    {
       
var polygon = new Object();
       
var llpoints = new Array();
       
for (var j = 0; j < layer[i].Points.length; j++)
        {        
            llpoints.push(
new VELatLong(layer[i].Points[j].Y, layer[i].Points[j].X));
        }
       
var veCol = new VEColor(layer[i].Colour.R, layer[i].Colour.G, layer[i].Colour.B, layer[i].Colour.A/255); 
        polygon.Points = llpoints; 
        polygon.Colour = veCol; 
        polygons.push(polygon);
    }
   
return polygons;
}

Finally, for each of the JavaScript polygon objects, a new VEShape object is created and added to the map, care of the DrawPolygon() method.

function DrawPolygon(polygon)
{
   
var shape = new VEShape(VEShapeType.Polygon, polygon.Points);
    shape.SetFillColor(polygon.Colour);
    shape.SetLineColor(polygon.Colour);
    shape.SetLineWidth(1);
    shape.HideIcon();
        
    map.AddShape(shape);
}

Finally, this is what we get:

Great Britain's Administrative Areas overlaid on a Virtual Earth map

Pretty neat, isn't it? ;)

Obviously, the shapes become more detailed as you zoom in:

Great Britain's Administrative Areas overlaid on a Virtual Earth map Great Britain's Administrative Areas overlaid on a Virtual Earth map (Zoom)

Querying.

Now what if we want to access the info associated with each part (i.e. shape) in the Shapefile?

Luckily, SharpMap comes once again in our help, making this very easy. When provided with a bounding box, the library's ExecuteIntersectionQuery() method returns a DataSet containing the information associated with the shapes that intersect it. All we need to do to get this info to the client is create another ScriptService, and invoke it when the user clicks on the map, providing the Latitude and Longitude of the clicked point.

This is the method that actually performs the query on the Shapefile:

public string QueryPoint(string filePath, double lat, double lon)
{
   
ShapeFileProvider sf = null;
   
try
    {
        sf =
new ShapeFileProvider(filePath);
       
if (!sf.IsOpen)
            sf.Open(
false);

       
/* This is the SharpMap object that will contain the query results */
        FeatureDataSet ds = new FeatureDataSet();
       
/* This is the extents of the map upon which we want to perform the query. 
         * In our case, it will simply be the point where the user clicked. */
        BoundingBox bbox = new SharpMap.Geometries.Point(lon, lat).GetBoundingBox();
       
/* This actually executes the query and puts the results into the FeatureDataSet */
        sf.ExecuteIntersectionQuery(bbox, ds);
       
DataTable table = ds.Tables[0];
       
if (table.Rows.Count < 1)
           
return null;

       
/* Here we format the query results in a simple HTML table to be shown in the
         * MessageBox on the map for demo purposes */
        StringBuilder sb = new StringBuilder();
        sb.AppendLine(
"<table>");
       
foreach (DataRow row in table.Rows)
        {
           
foreach (DataColumn column in table.Columns)
            { 
                sb.AppendFormat(
"<tr><td style='background-color:#113;'>{0}</td>" +
                            "<td>{1}</td></tr>", column.ColumnName, row[column]);
            }
            sb.AppendFormat(
"<tr style='height:10px;'></tr>");
        }
        sb.AppendLine(
"</table>");
       
return sb.ToString();
    }
   
finally
    {
       
if (sf != null)
            sf.Close();
    }
}

And here's the very simple Web Method that exposes the method's functionality to client script:

[WebMethod]
public string GetPlaceInfo(string fileName, double latitude, double longitude)
{
   
ShapefileReader sr = new ShapefileReader();
   
return sr.QueryPoint(Server.MapPath("~/Maps/" + fileName), latitude, longitude);
}

Finally, this is what happens on the client when the user clicks on a point on the map. Notice how we leverage ASP.NET AJAX's page lifecycle "pageLoad" event to initially setup our map and register the onclick event handler. The rest of this snippet should be self explanatory. In case it is not, you can have a look at Virtual Earth SDK to learn more about map events and methods.

function pageLoad()
{
    map =
new VEMap('myMap');
    map.LoadMap(
new VELatLong(50.5, -2.1), 6);
    map.AttachEvent(
"onclick", map_onclick);
}
 
function map_onclick(e)
{
   
if (shapefileName != null)
    { 
       
var ll = map.PixelToLatLong(new VEPixel(e.mapX, e.mapY));
        MapService.GetPlaceInfo(shapefileName, ll.Latitude, ll.Longitude,
                                QueryData_success, onFailed);
    }
}

function QueryData_success(e)
{
   
if (e != null)
        MessageBox(
true, e, 0, true);
   
else
        MessageBox(true, "No data to display", 2000);
}

 

Querying the map

Querying the map Querying the map

That's all folks! I hope you enjoyed this article and found it useful! As always, I urge you to leave a comment if you need any more info or clarification. Finally, if you liked this article and you want to help me spread the word, you might consider clicking on the "kick it" button below! ;)

As usual, you can see a live demo of the application here, and you can download the source code of this project here.

Enjoy!

Marco

 

kick it on DotNetKicks.comDigg!dzone
Getting started with Virtual Earth Mashups: showing current weather conditions on a map

In this article we'll see how easy it is to use Virtual Earth SDK to produce a simple mashup, using web services that provide information in JSON format.

Live Demo - Source Code

If you are not familiar with JSON or how to integrate JSON services in ASP.NET AJAX applications, you can take a look at my 3-part series of articles on JSON and ASP.NET AJAX here.

First of all, we need to create a simple .aspx page, and add a ScriptManager to it. Then, we're going to reference the Virtual Earth API in the ScriptManager's Script section:

<asp:ScriptManager ID="ScriptManager1" runat="server">
  <Scripts>
    <asp:ScriptReference Path="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6" />
  </Scripts>
</asp:ScriptManager>

We also need a Div in the page, to act as a placeholder for the Virtual Earth map, and a button to trigger the web service call.

<div id='myMap' style="position:relative;width:800px;height:600px;"></div>
<input type="button" onclick="getWeather_onclick()" value="Get Weather Info..." />

We've almost succeeded in showing a simple Virtual Earth map in our ASP.NET page. All that's left to do, is call the JavaScript method of the VE API that will actually initialise the map and populate our placeholder div.

<script type="text/javascript">

var map = null;

function pageLoad()
{
    map =
new VEMap('myMap');
    map.LoadMap(
new VELatLong(51.5, -0.1), 10);
}

</script>

You may notice that we put the initialisation logic in the pageLoad() method of our page in order to leverage the ASP.NET AJAX client side lifecycle. Another very common way to initialise the map, is to define an event handler for the body onload event.

If you try to run the project now, you'll see something like this:

ScreenShot002

If you try to run the project now, you'll see that the Div we created is already populated with a road map of Greater London. If you take a moment to play around with it, you'll see that all the standard functionalities of Virtual Earth, including panning, zooming, changing the map style etc. already work out of the box. We could even include the standard "What - When" search with a line of code if we wanted! (see Virtual Earth documentation for more info). It's a great deal of functionalities for such a little effort on our part!

NOTE: As always, inline script and global variables are used for clarity, but in a more complex project, it would be much tidier to use separate .js files for JavaScript code, and enclose methods and variables in client side classes, in order to attain a more OO application model. Using the ASP.NET AJAX Library helps a great deal in this regard.

Now, we need to integrate an external web service that provides us with up-to-date weather information on the area we select on the map. After a short search on the web, you may find that Geonames's free weather services are extremely powerful and useful, and, as a bonus, they come in JSON format, and that will make our job easier ;)

As you may know, for security reasons, cross-domain requests from client script are forbidden, i.e. JavaScript code can only successfully make an HTTP web request to the domain in which the page originated. To circumvent this restriction, we will have to create a .NET proxy service in our server side code, and call it from our JavaScript. I've written a relatively extensive tutorial on this topic, so if you've never created a proxy service, or used JSON web services with ASP.NET AJAX, you may find useful to have a look at it before going on. On a side note, keep in mind that, although I won't talk about them in this article, different solutions exist to call a remote JSON service from client side code. Anyway, in my opinion, the solution presented here is the tidiest and most flexible one.

Geonames provides an entire set of web methods to retrieve weather info provided by observation stations all around the world using different criteria. What we want to do is retrieve the available weather data provided by meteorological stations near a specific point on the map. The most straightforward way to do that is by calling the findNearByWeatherJSON web service, located at http://ws.geonames.org/findNearByWeatherJSON. The service takes at least two parameters, the longitude and latitude of a point, and returns the observation taken at the station closest to the supplied point. When issuing the request, we can also specify the maximum number of results we want returned. By default, only data from the closest station is retrieved, but it can be useful to get data from multiple stations around the selected point.

Let's see the code for the proxy web service:

using System.Web.Services;
using System.Web.Script.Services;

[
WebService]
[
WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[
ScriptService]
public class WeatherService : WebService
{
    [
WebMethod]
   
public string GetWeatherByLocation(double lat, double lng)
    {
       
return GeonamesWeather.GetWeatherByLocation(lat, lng);
    }
}

and for the GeonamesWeather class:

using System;
using System.Net;
using System.Globalization;
using System.IO;

public class GeonamesWeather
{
   
private readonly static string FindNearbyWeatherUrl =
       
"http://ws.geonames.org/findNearByWeatherJSON?lat={0}&lng={1}&maxRows=10";

   
public static string GetWeatherByLocation(double lat, double lng)
    {
       
string formattedUri = String.Format(CultureInfo.InvariantCulture,
                                            FindNearbyWeatherUrl, lat, lng);

       
HttpWebRequest webRequest = GetWebRequest(formattedUri);
       
HttpWebResponse response = (HttpWebResponse)webRequest.GetResponse();
       
string jsonResponse = string.Empty;
       
using (StreamReader sr = new StreamReader(response.GetResponseStream()))
        {
            jsonResponse = sr.ReadToEnd();
        }
       
return jsonResponse;
    }

   
private static HttpWebRequest GetWebRequest(string formattedUri)
    {
       
// Construct the request's URL.  
        Uri serviceUri = new Uri(formattedUri, UriKind.Absolute);

       
// Return a HttpWebRequest object.  
        return (HttpWebRequest)System.Net.WebRequest.Create(serviceUri);
    }
}

As you can see, by specifying maxRows=10 we're retrieving the observations from at most 10 weather stations near the selected location. If you want to know in greater detail what we are doing here, please refer to this post where I concentrate more on how to create a proxy web service. What we absolutely need to point out is that by marking the WeatherService class with the [ScriptService] attribute, we are instructing the compiler to create a proxy JavaScript class to allow the method to be called directly from client code.

For reference, here's an example of a typical response received from the web service call:

{"weatherObservations":
[{"clouds":"scattered clouds",
  "weatherCondition":"n/a",
  "observation":"EGUY 081550Z 27014KT 9999 SCT026 BKN045 10/07 Q1011 BLU",
  "windDirection":270,
  "ICAO":"EGUY",
  "elevation":41,
  "countryCode":"GB",
  "lng":-0.116666666666667,
  "temperature":"10",
  "dewPoint":"7",
  "windSpeed":"14",
  "humidity":81,
  "stationName":
  "Wyton Royal Air Force Base",
  "datetime":"2007-11-08 15:50:00",
  "lat":52.35,
  "hectoPascAltimeter":1011},
{"clouds":"few clouds",
  "weatherCondition":"n/a",
  "observation":"EGXT 081850Z 30018G28KT 9999 FEW038 08/01 Q1013 BLU",
  "windDirection":300,
  "ICAO":"EGXT",
  "elevation":84,
  "countryCode":"GB",
  "lng":-0.466666666666667,
  "temperature":"8",
  "dewPoint":"1",
  "windSpeed":"18",
  "humidity":61,
  "stationName":"Wittering",
  "datetime":"2007-11-08 18:50:00",
  "lat":52.6166666666667,
  "hectoPascAltimeter":1013},
{...},
{...},
{...}]}

NOTE: Carriage returns and indentation have been inserted only to improve readability, but the response stream won't contain any.

So, let's see what we've got here: the received JSON response, once deserialized to a JavaScript object, will have a single property, weatherObservations, which will contain an array of observations, each in turn containing several properties that describe the current weather condition.

Now, one thing we need to make our web method callable from client code, is to add a reference to it in the Services section of the ScriptManager. So here's how our ScriptManager will look like, assuming we named the Web Service WeatherService.asmx and we put it in the site root:

<asp:ScriptManager ID="ScriptManager1" runat="server">
  <Scripts>
    <asp:ScriptReference Path="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6" />
  </Scripts>
  <Services>
    <asp:ServiceReference Path="~/WeatherService.asmx" />
  </Services>
</asp:ScriptManager>

Now we're ready to call the web service, and we'll do this from the button's onclick event handler:

function getWeather_onclick()
{
   
var centre = map.GetCenter();
    WeatherService.GetWeatherByLocation(centre.Latitude, centre.Longitude,
                                        GetWeather_success, onFailed);
    MessageBox(
true, "Please wait while retrieving weather data...");
}

When the button is clicked, the geographical coordinates corresponding to the location currently displayed at the centre of the map are retrieved, using the Virtual Earth map's GetCenter() method. Then the proxy .asmx web service is called in an RPC fashion, passing it the location's latitude and longitude. As the method execution is asynchronous, we need to provide a succeeded callback function, that will be invoked when the request finishes successfully, and will be passed the return value from the Web Service call. We also provide the method with a reference to a callback to be executed in case something goes wrong. Finally, as the web service call can take a while to complete, the MessageBox() function is used to show some visual feedback to the user in order to keep him/her up to date with what's happening. With some appropriate CSS, we can make the MessageBox blend in nicely with the Virtual Earth 6 interface, like this:

ScreenShot005

Let's see how the MessageBox() function is implemented.

function MessageBox(show, message, timeout)
{
   
var balloon = $get('messageBalloon');
   
if (!show)
        balloon.style.display =
'none';
   
else
    {
        balloon.style.display =
'block';
        balloon.innerHTML = message;
       
if (timeout)
            setTimeout(
'MessageBox(false)', timeout);
    }
}

Note that in order for the MessageBox() function to work, we need to add a Div named messageBalloon to the page markup. As you can see, we pass the function three parameters: show determines whether the message is to be shown or hidden, message contains the actual message to be shown, and timeout, if present, instructs the function to show the message only for the specified period in milliseconds. All parameters are optional: in case no parameter is passed, the currently shown message, if any, is hidden.

Finally, let's see how the two callback functions are implemented:

function onFailed(e)
{
    MessageBox(
true, "Error while connecting to the remote web service. Please try again later.", 5000);
}

function GetWeather_success(e)
{
   
var weatherData = Sys.Serialization.JavaScriptSerializer.deserialize(e);
   
var stationsFound = 0;
    
   
if (weatherData.weatherObservations && weatherData.weatherObservations.length > 0)
    {
        Array.forEach(weatherData.weatherObservations, AddWeatherPushpin);
        stationsFound = weatherData.weatherObservations.length;
    }
    
   
if (stationsFound > 0)
    {
       
var message = String.format("Data was retrieved for {0} weather stations.",
                                    stationsFound);
        MessageBox(
true, message, 2000);    
    }
   
else
    {
        MessageBox(
true, "No weather data was found. Please try again.", 5000);
    }
}

The onFailed() function is really self-explanatory: it simply pops up a message to notify the user that something went wrong in the web service call.

On the other hand, the getWeather_success() callback gets a bit more complicated, so let's analyse it in depth. First of all, the returned JSON string is deserialized to a JavaScript object using the AJAX Client Library's JavaScriptSerializer class, and the object is then assigned to the weatherData variable. To learn more about this process, you can refer to this post in my already mentioned series on JSON, or have a look at the msdn.

The weatherData object will contain a weatherObservations property only if one or more observations could be retrieved for the specified location. When everything goes as expected, the weatherObservations property contains an array of observations, one for each weather station. So what we need to do in our function is check for the existence of the weatherObservations property, and if we find it, call the AddWeatherPushpin() function, passing it each of the observations found in the array. The AddWeatherPushpin() function will then prepare and add a specific icon to the map for each of the observations.

Notice that, to improve clarity, rather than looping through the returned array in a for cycle, I preferred to use the Array.forEach() method of the ASP.NET AJAX Client Library, which does that under the hood and processes each item of the array with the function provided as the second argument.

Finally, we notify the user with the query result by popping up a message.

In the AddWeatherPushpin() function, we analyse the clouds property of the weatherData object to determine what is the current cloudiness level above each station. Then, we choose an appropriate icon to be shown on the map based on the cloudiness index. The possible cloudiness levels returned from the web service can be found here. The icon names are chosen following the naming convention you can find here, which to my knowledge happens to be the most followed by the creators of weather icon sets.

This is how the icons will appear on the map:

ScreenShot003

We also need to prepare the content for the InfoBoxes that will pop up when moving the mouse above the icons and show the retrieved weather information details. This is how they will look like:

ScreenShot007

Finally, let's see the implementation of this function:

function AddWeatherPushpin(observation)
{
   
var icon;
   
// The observation time is parsed in order to determine
    // whether the observation was taken during the day or
    // the night and choose an appropriate icon.
    // The format of the returned date is yyyy-MM-dd hh:mm:ss
    var d = observation.datetime;
   
var date = new Date(d.substr(0,4), d.substr(5,2), d.substr(8,2),
                        d.substr(11,2), d.substr(14,2), d.substr(17,2));
    
   
var isDay = (date.getHours() > 6 && date.getHours() < 20);
      
   
switch (observation.clouds)
    {
       
case 'clear sky':
           
if (isDay) icon = '32'; else icon = '31';
           
break;
       
case 'few clouds':
           
if (isDay) icon = '30'; else icon = '29';
           
break;
       
case 'scattered clouds':
           
if (isDay) icon = '28'; else icon = '27';
           
break;
       
case 'broken clouds':
            icon =
'26';
           
break;
       
case 'overcast':
       
case 'vertical visibility':
            icon =
'23';
           
break;
       
default:
            icon =
'NA';
    }
   
// The wind speed is converted from knots to m/s
    var windSpeedMs = Math.round(observation.windSpeed * 0.5144444 * 100)/100;
  
   
var html = "<em>{0}</em><br/><b>Clouds:</b> {1}<br/><b>Weather condition:</b> {2}<br/>" +
           
"<b>Temperature:</b> {3} °C<br/><b>Dew Point:</b> {4} °C<br/>" +
           
"<b>Wind:</b> {5}kt ({6} m/s) from {7}°<br/><b>Altimeter:</b> {8} hPa";
   
var details = String.format(html,
            observation.datetime, observation.clouds, observation.weatherCondition,
            observation.temperature, observation.dewPoint, observation.windSpeed,
            windSpeedMs, observation.windDirection, observation.hectoPascAltimeter);
    
    icon = String.format(
"images/{0}.png", icon);
    icon = GetCorrectIcon(icon, 48, 48, -10, -12);
    AddPushpin(observation.lat, observation.lng, observation.stationName, details, icon);
}

Most of the function above is self-explanatory or already commented in the code. What I want to point out, is what happens in the last three lines. First of all, the icon's name is transformed to a hard-coded virtual path. Then the custom html to use for the pushpin's custom icon is obtained by invoking the GetCorrectIcon() function and passing it the icon's file path, the icon's dimensions and the icon's offset (needed to position the icon correctly on the map). Finally, the call to AddPushpin() actually creates the weather pushpins and adds them to the map.

The kind of elaborations seen in the AddWeatherPushpin() function could obviously have been made on the server and sent to the client together with the remote service's JSON response, but this would have involved serialization and deserialization of data on the server. By choosing this approach, we would have kept the client code tidier, and we would have leveraged the fact that we had to use a proxy Web Service anyway, but this was beyond the scope of this article. If you want to know more of how to elaborate the response from a JSON Web Service on the server, and want to see an example of how this can be done, you can take a look at this article.

Let's see how these functions are implemented:

function GetCorrectIcon(iconPath, width, height, offsetLeft, offsetTop)
{
   
/// <summary>Fixes the IE6 png transparency issue for the passed icon</summary>
    var html;
   
if (Sys.Browser.agent == Sys.Browser.InternetExplorer &&
        Sys.Browser.version == 6)
        html =
"<img src='images/blank.gif' " +
       
"style='position:absolute;left:{0}px;top:{1}px;width:{2}px;height:{3}px;" +
       
"filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\"{4}\", " +
       
"sizingMethod=\"image\" );' />";
   
else
        html = "<img src='{4}' " +
       
"style='position:absolute;left:{0}px;top:{1}px;width:{2}px;height:{3}px;' />";
    
   
if (!offsetLeft) offsetLeft = 0;
   
if (!offsetTop) offsetTop = 0;
   
return String.format(html, offsetLeft, offsetTop, width, height, iconPath);
}    

function AddPushpin(lat, lon, title, description, icon)
{
   
var point = new VELatLong(lat, lon);
   
var shp = new VEShape(VEShapeType.Pushpin, point);
    shp.SetTitle(title);
    shp.SetDescription(description);
   
if (icon)
    {
        shp.SetCustomIcon(icon);
    }
    map.AddShape(shp);
}

Basically, the GetCorrectIcon() function's role is to generate the html needed to position the weather icon correctly on the map. To do this, we need to create an img tag and add some inline CSS properties to it, taking into account the icon's dimensions and the desired offset. In fact, by default, pushpin icons are positioned on the map assuming an icon size of 25x30 pixels; when a different icon size is used, the icon's positioning on the map has to be manually fine-tuned; this can be done by setting the CSS left and top properties for the image.

Another thing to take into account is that we're using transparent .png images, and, as everyone knows, Internet Explorer 6 cannot render them correctly, so we need to use a workaround to make everything work smoothly also on that browser. In particular, we leverage once again the AJAX Client Library's utility classes to check whether the current browser is IE6, and in that case, we use Microsoft's suggested fix for this problem, and add the AlphaImageLoader filter to the img styles collection, while setting the img src attribute to a 1x1 pixel blank image. This will make the experience of those users using IE6 as pleasant as anyone else's.

Finally, let's spend a few words about the AddPushpin() function. It simply creates a Pushpin shape located at the supplied coordinates, sets the content of the associated InfoBox using the supplied title and description, and finally, when the icon parameter is specified, sets a custom icon for the pushpin. Finally, it adds the freshly created icon to the VE map.

And that's all! Finally, our Virtual Earth mashup is ready to be enjoyed by our users!

As usual, you can see a live demo of the application here, and you can download the source code of this project here.

Enjoy!

Marco

 

kick it on DotNetKicks.com Digg!dzone

JSON in ASP.NET Ajax: Part 3. Server side deserialization and elaboration of JSON data returned from a Web Service.

This is the third part of a series of articles on ASP.NET AJAX and JSON. If you haven't read part 1 and part 2 of this series, you are strongly advised to do so before going on to read this article.

Let's take what we've seen so far on JSON and ASP.NET one step further, and discover how we can leverage the ASP.NET AJAX infastructure to make the managing of an AJAX application even sleeker. Now, let's imagine that we want to elaborate the JSON response returned from the web service before sending it back to the client.

This can prove useful for many a reason, for example, to enrich the returned object with properties generated by complex elaborations, which we would rather do on the server than on the client.

In fact, what we will see today, is how to add a Stability Class property, computed used the Pasquill method for categorizing atmospheric turbulence, to the weather observation data returned from the Geonames web service we have come to know in my previous articles. I want to point out that this is merely an example of a complex elaboration that would generally take place on the server rather than on the client, but it could be substituted with any kind of data transformation or elaboration that you would need to apply to the data before sending it back to the client.

Now, in order to do this, we might modify the returned JSON object directly as a string, but you can imagine how cumbersome and error-prone this process would become if the elaborations to be made were less than trivial. We need some more flexibility. And here's where the ASP.NET AJAX infrastructure comes to help us again.

What we want to do first, is convert the JSON response to a .NET object, in order to pave the way for the elaborations that we are going to make. We can do this in a straightforward way, leveraging the .NET JavaScriptSerializer class, which can be found in the System.Web.Script.Serialization namespace. But before starting the actual deserialization process, we need to make some preparation. First of all, we need to create the class we want our JSON object to be shaped into:

 

public class WeatherObservation
{
   
public string clouds { get; set; }
   
public string observation { get; set; }
   
public string weatherCondition { get; set; }
   
public int windDirection { get; set; }
   
public int windSpeed { get; set; }
   
public string ICAO { get; set; }
   
public int elevation { get; set; }
   
public string countryCode { get; set; }
   
public double lat { get; set; }
   
public double lng { get; set; }
   
public int temperature { get; set; }
   
public int dewPoint { get; set; }
   
public int humidity { get; set; }
   
public string stationName { get; set; }
   
public string datetime { get; set; }
   
public int hectoPascAltimeter { get; set; }
   
public string stabilityClass { get; set; }
}

 

As you can see, the properties that we added to the class map directly to those present in the returned JSON object. (See here if you haven't read my previous post and you want to see how the returned JSON string looks like). Also notice that this is the place where we define the additional properties that we want to send to the client along with the original ones; in this case, we added the stabilityClass property.

NOTE: You might notice that we used Automatic Properties, a new language feature introduced with C# 3.0, to make our code more compact and readable (let alone saving a lot of keystrokes ;) ). You can read more on Automatic Properties on Scott Guthrie's blog. If you are not using C# 3.0 yet, the only thing you need to do in order to make this work, is to write basic properties or public fields rather than Automatic Properties.

Actually, if you look at the returned JSON string closely, you'll find out that the weather observation is not returned as a top-level object, but is itself the content of the top-level weatherObservation property. This might seem redundant at first, but this schema is due to fact that more than one weather observation can be returned from the Web Service if desired, and in that case the weatherObservations property would contain an array of observations. Anyway, we can overlook this detail for the moment; all we need to do now to make everything work smoothly, is define a new class, containing a weatherObservation property:

 

public class SingleWeatherObservation
{
   
public WeatherObservation weatherObservation { get; set; }
}

 

Now we're ready to modify our GetWeatherByLocation method.

We'll add a call to the Deserialize<T>() method of the JavaScriptSerializer class. This method will return an object of type T, containing the deserialized data, which we will use for all our subsequent computations. In particular, we'll pass this object to our ComputeStabilityClasses() method, in order to compute the suggested Stability Classes. Later, we'll use this value to set the stabilityClass property of our object. Let's see how the code looks like:

 

public static string GetWeatherByLocation(double lat, double lng)
{
   
string formattedUri = String.Format(CultureInfo.InvariantCulture,
                                        FindNearbyWeatherUrl, lat, lng);

   
HttpWebRequest webRequest = GetWebRequest(formattedUri);
   
HttpWebResponse response = (HttpWebResponse)webRequest.GetResponse();
   
string jsonResponse = string.Empty;
   
using (StreamReader sr = new StreamReader(response.GetResponseStream()))
    {
        jsonResponse = sr.ReadToEnd();
    }

   
JavaScriptSerializer ser = new JavaScriptSerializer();
   
SingleWeatherObservation observation =
                     ser.Deserialize<SingleWeatherObservation>(jsonResponse);
    observation.weatherObservation.stabilityClass =
                      ComputeStabilityClasses(observation.weatherObservation);
   
return ser.Serialize(observation);
}