Geeks With Blogs

News Welcome to Marco Anastasi and Serena Caruso's technical weblog on Microsoft .NET technologies and related matters!
Juan DoNeblo Just another Dot Net blog by marco anastasi & serena caruso

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

Posted on Tuesday, November 13, 2007 12:15 PM Virtual Earth , ASP.NET , Javascript , C# | Back to top


Comments on this post: Getting started with Virtual Earth Mashups: showing current weather conditions on a map

# re: Getting started with Virtual Earth Mashups: showing current weather conditions on a map
Requesting Gravatar...
Very helpful tutorial, helped me think through the architecture of my VE/eBay mashup at
http://www.ebaymapper.com/
Currently it available only for the US. Have fun browsing.
Left by Vivek on Jul 31, 2008 6:15 AM

# re: Getting started with Virtual Earth Mashups: showing current weather conditions on a map
Requesting Gravatar...
wow, looks great. It seems like great idea for... <cough> Android Apps! Create an apps with this functionality and you'll be getting publicity for sure, in one way or the other...
Left by Motorola Xoom on Jan 24, 2011 4:20 AM

Your comment:
 (will show your gravatar)
 


Copyright © Marco Anastasi & Serena Caruso | Powered by: GeeksWithBlogs.net | Join free