Sunday 19 September 2010

Bing Maps Silverlight control weather mash-up with yr.no - Part 3

In this post, I will take a look at creating a web service that will feed weather data to our Silverlight application that we have created in the previous posts
yr.noFirstly a little about where we are going to get the weather data from. yr.no is the joint online weather service from the Norwegian Meteorological Institute (met.no) and the Norwegian Broadcasting Corporation (NRK) and it offers weather forecasts in English (in addition to Norwegian Nynorsk and Norwegian Bokmål) for more than 700,000 places in Norway and 6.3 million places worldwide.

Yr.no supplies its weather data in XML format and you can get this XML by adding forecast.xml to the URL that is generated when you look for a location. For example for my current location in Stavanger, the generated address is http://www.yr.no/place/Norway/Rogaland/Stavanger/Stavanger/ and the XML for this http://www.yr.no/place/Norway/Rogaland/Stavanger/Stavanger/forecast.xml. This will generate most of the information in English.

To get this information in Norwegian you change the place to sted and use varsel.xml instead.

There are additional services provided by eKlima which you can use instead of yr.no if you wish. You can find more information on eKlima here.What I am going to do in this post, is creating the web service. So firstly we will create a web blank web application and a new web service to it.

In my project I have created a common library that contains a business layer, data layer and some classes. I have created a BaseInformation class which contains the basic information that is used by the map to create pushpins. This allows us to create more different objects which can use this class and make a generic method for adding pushpins with different data types, images etc.
1: public class BaseInformation
2:    {
3:        public int ID { get; set; }
4:        public string Title { get; set; }
5:        public double Latitude { get; set; }
6:        public double Longitude { get; set; }
7:        public string ImageLocation { get; set; }
8:        public string Description { get; set; }
9:        public string ToolTipImage { get; set; }
10:    }
The WeatherInfo class contains information specific to weather data and we will use this in our custom tooltip that we will be adding to the pushpin when its created on the map.
1:     public class WeatherInfo : BaseInformation
2:     {
3:         public string TempC { get; set; }
4:         public string FromTime { get; set; }
5:         public string ToTime { get; set; }
6:         public string SymbolName { get; set; }
7:         public string SymbolNumber { get; set; }
8:         public string WindData { get; set; }
9:         public string WindImage { get; set; }
10:     }
We have a simple class with an even simpler method that returns a generic list of strings that contains the XML addresses to download the information from yr.no.
1:    public IEnumerable<string> GetData()
2:         {
3:             return new List<string>
4:                        {
5:                            "http://www.yr.no/place/Norway/Rogaland/Sandnes/Sandnes/varsel.xml",
6:                            "http://www.yr.no/sted/Norge/Rogaland/Stavanger/Stavanger/varsel.xml",
7:                            "http://www.yr.no/place/Norway/Rogaland/Stavanger/Forus/varsel.xml",
8:                        };
9:         }
In the data layer we will create a new method that we will use to download the XML and manipulate it to our needs. The method stub looks like the following
1:       public static WeatherInfo GetWeatherInfo(string feed)
2:         {
3:            
4:         }
So first we need to download the XML file from yr.no. We use the XmlDocument class and use the Load method to load the XML.
1:             var xDoc = new XmlDocument();
2:             xDoc.Load(feed);
Since the XML file is quite large and contains multiple different elements we will create some datasets from the different elements and then use LINQ to join these datasets together and create the objects that we need.

So first creating the datasets from XML. This is a quick method which takes the name of the tag you want to turn into a dataset and the XMLDocument.
1:        private static DataSet GetDataSet(XmlDocument xDoc, string tagName)
2:         {
3:             var forecast = xDoc.GetElementsByTagName(tagName);
4: 
5:             var ds = new DataSet();
6:             var readerStream = new StringReader(forecast[0].OuterXml);
7:             ds.ReadXml(readerStream);
8:             return ds;
9:         }
We will create datasets from the following XML elements tabular, location, and forecast and we will join these together using LINQ. The following LINQ query is not the best in the world due its level of complexity so I apologise in advance if your eyes burn out of your skull and you decide to run off screaming “Oh the humanity”
1: from time in ds.Tables["time"].AsEnumerable()
2:                     join temp in ds.Tables["temperature"].AsEnumerable()
3:                         on time.Field<int>("time_id") equals
4:                         temp.Field<int>("time_id")
5:                     join symbol in ds.Tables["symbol"].AsEnumerable()
6:                         on time.Field<int>("time_id") equals symbol.Field<int>("time_id")
7:                     join windDirection in ds.Tables["windDirection"].AsEnumerable()
8:                         on time.Field<int>("time_id") equals windDirection.Field<int>("time_id")
9:                     join windSpeed in ds.Tables["windSpeed"].AsEnumerable()
10:                        on time.Field<int>("time_id") equals windSpeed.Field<int>("time_id")
That is the basic LINQ query and we will use that as the basis to generate a WeatherInfo object using the second part of the query.
1:  select new WeatherInfo
2:                     {
3:                         ID = time.Field<int>("time_id"),
4:                         TempC = temp.Field<string>("value") +"C",
5:                         FromTime = time.Field<string>("from"),
6:                         ToTime = time.Field<string>("to"),
7:                         SymbolNumber = symbol.Field<string>("number"),
8:                         SymbolName = symbol.Field<string>("name"),
9:                         WindData = string.Format("{0} {1} {2} m/s", windDirection.Field<string>("name"), windSpeed.Field<string>("name"), windSpeed.Field<string>("mps"))
10:                     }).FirstOrDefault();
The handy thing here is the cast of the WeatherInfo at the start to create an object that we can populate. Also the helper method FirstOrDefault(). If there is no records, it will create a blank object and we will return that. The Full method looks like this
1:     private static WeatherInfo GetWeatherInfoFromDataSet(DataSet ds)
2:         {
3:             return (from time in ds.Tables["time"].AsEnumerable()
4:                     join temp in ds.Tables["temperature"].AsEnumerable()
5:                         on time.Field<int>("time_id") equals
6:                         temp.Field<int>("time_id")
7:                     join symbol in ds.Tables["symbol"].AsEnumerable()
8:                         on time.Field<int>("time_id") equals symbol.Field<int>("time_id")
9:                     join windDirection in ds.Tables["windDirection"].AsEnumerable()
10:                         on time.Field<int>("time_id") equals windDirection.Field<int>("time_id")
11:                     join windSpeed in ds.Tables["windSpeed"].AsEnumerable()
12:                        on time.Field<int>("time_id") equals windSpeed.Field<int>("time_id")
13:                     select new WeatherInfo
14:                     {
15:                         ID = time.Field<int>("time_id"),
16:                         TempC = temp.Field<string>("value") +"C",
17:                         FromTime = time.Field<string>("from"),
18:                         ToTime = time.Field<string>("to"),
19:                         SymbolNumber = symbol.Field<string>("number"),
20:                         SymbolName = symbol.Field<string>("name"),
21:                         WindData = string.Format("{0} {1} {2} m/s", windDirection.Field<string>("name"), windSpeed.Field<string>("name"), windSpeed.Field<string>("mps"))
22:                     }).FirstOrDefault();
23:         }
In the GetWeatherInfo method we will finalise any properties in the object that we haven’t set before this. Such as the latitude and longitude. Now because in Norway the decimal separator is a comma (,) rather than the decimal point (.) this can cause some issues when doing conversions between strings and doubles. So we will use the NumberFormatInfo class to specify how the conversion should take place.
1: weatherInfo.Latitude = double.Parse(dr["latitude"].ToString(), NumberFormatInfo.InvariantInfo);
2: weatherInfo.Longitude = double.Parse(dr["longitude"].ToString(), NumberFormatInfo.InvariantInfo);
The full method for GetWeatherInfo is as follows
1: public static WeatherInfo GetWeatherInfo(string feed)
2:         {
3:             var xDoc = new XmlDocument();
4:             xDoc.Load(feed);
5: 
6:             var ds = GetDataSet(xDoc, "tabular");
7: 
8:             var weatherInfo = GetWeatherInfoFromDataSet(ds);
9: 
10:             var coDs = GetDataSet(xDoc, "location");
11:             var weatherDescDs = GetDataSet(xDoc, "forecast");
12: 
13:             weatherInfo.Description = weatherDescDs.Tables["time"].Rows[0]["body"].ToString();
14:             weatherInfo.Description = weatherInfo.Description.Replace(@"<strong>", "");
15:             weatherInfo.Description = weatherInfo.Description.Replace(@":</strong>", " -");
16: 
17:             weatherInfo.WindImage = "images/windsock.png";
18: 
19:             var dr = coDs.Tables["location"].Rows[1];
20: 
21:             weatherInfo.Latitude = double.Parse(dr["latitude"].ToString(), NumberFormatInfo.InvariantInfo);
22:             weatherInfo.Longitude = double.Parse(dr["longitude"].ToString(), NumberFormatInfo.InvariantInfo);
23: 
24:             weatherInfo.Title = coDs.Tables["location"].Rows[0]["name"].ToString();
25:             weatherInfo.ToolTipImage = "images/temp.png";
26:             weatherInfo.ImageLocation = string.Format("images/{0}.png", weatherInfo.SymbolNumber);
27: 
28:             return weatherInfo;
29:         }
In the Business layer we have a simple method that creates a list of these weatherInfo objects based on the list of feeds that I talked about earlier.
1:  public static List<WeatherInfo> GetAllWeatherInfo()
2:         {
3:             var allFeeds = new FakeWeatherFeeds().GetData();
4: 
5:             var weatherInfos = allFeeds.Select(WeatherInfoDl.GetWeatherInfo).ToList();
6: 
7:             return weatherInfos;
8:         }
Instead of a foreach loop I am using some LINQ to pass in the method and create the list. And finally the web service method looks like the following.
1: [WebMethod]
2:         public List<WeatherInfo> GetAllWeather()
3:         {
4:             return WeatherInfoBl.GetAllWeatherInfo();
5:         }
In the next post we will put this altogether with the Silverlight map application and binding the list of objects to the map.

1 comment:

Elbrinner said...

Que grande!

Los ejemplos están bien claro, espero el proximo.

un saludo