Update: Live demo and source code now available!
The recent wave of earthquakes (no pun intended) being reported in the news got me wondering about the frequency and severity of earthquakes around the world. Since I’ve been doing a lot of Silverlight development lately, I decided to scratch my curiosity with a nice little Bing Maps application that will show the location and relative strength of recent seismic activity.
Here is a list of technologies this application will utilize, so be sure to have everything downloaded and installed if you plan on following along.
Silverlight 3
WCF RIA Services
Bing Maps Silverlight Control *
Managed Extensibility Framework (optional)
MVVM Light Toolkit (optional)
log4net (optional)
* If you are new to Bing Maps or have not signed up for a Developer Account, you will need to visit www.bingmapsportal.com to request a Bing Maps key for your application.
Getting Started
We start out by creating a new Silverlight Application called EarthquakeLocator and specify that we want to automatically create the Web Application Project with RIA Services enabled.
I cleaned up the web app by removing the Default.aspx and EarthquakeLocatorTestPage.html. Then I renamed the EarthquakeLocatorTestPage.aspx to Default.aspx and set it as my start page. I also set the development server to use a specific port, as shown below.
RIA Services
Next, I created a Services folder in the EarthquakeLocator.Web project and added a new Domain Service Class called EarthquakeService.cs. This is the RIA Services Domain Service that will provide earthquake data for our client application. I am not using LINQ to SQL or Entity Framework, so I will use the <empty domain service class> option. We will be pulling data from an external Atom feed, but this example could just as easily pull data from a database or another web service. This is an important distinction to point out because each scenario I just mentioned could potentially use a different Domain Service base class (i.e. LinqToSqlDomainService<TDataContext>).
Now we can start adding Query methods to our EarthquakeService that pull data from the USGS web site. Here is the complete code for our service class:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.ServiceModel.Syndication;
using System.Web.DomainServices;
using System.Web.Ria;
using System.Xml;
using log4net;
using EarthquakeLocator.Web.Model;
namespace EarthquakeLocator.Web.Services
{
/// <summary>
/// Provides earthquake data to client applications.
/// </summary>
[EnableClientAccess()]
public class EarthquakeService : DomainService
{
private static readonly ILog log = LogManager.GetLogger(typeof(EarthquakeService));
// USGS Data Feeds: http://earthquake.usgs.gov/earthquakes/catalogs/
private const string FeedForPreviousDay =
"http://earthquake.usgs.gov/earthquakes/catalogs/1day-M2.5.xml";
private const string FeedForPreviousWeek =
"http://earthquake.usgs.gov/earthquakes/catalogs/7day-M2.5.xml";
/// <summary>
/// Gets the earthquake data for the previous week.
/// </summary>
/// <returns>A queryable collection of <see cref="Earthquake"/> objects.</returns>
public IQueryable<Earthquake> GetEarthquakes()
{
var feed = GetFeed(FeedForPreviousWeek);
var list = new List<Earthquake>();
if ( feed != null )
{
foreach ( var entry in feed.Items )
{
var quake = CreateEarthquake(entry);
if ( quake != null )
{
list.Add(quake);
}
}
}
return list.AsQueryable();
}
/// <summary>
/// Creates an <see cref="Earthquake"/> object for each entry in the Atom feed.
/// </summary>
/// <param name="entry">The Atom entry.</param>
/// <returns></returns>
private Earthquake CreateEarthquake(SyndicationItem entry)
{
Earthquake quake = null;
string title = entry.Title.Text;
string summary = entry.Summary.Text;
string point = GetElementValue<String>(entry, "point");
string depth = GetElementValue<String>(entry, "elev");
string utcTime = null;
string localTime = null;
string depthDesc = null;
double? magnitude = null;
double? latitude = null;
double? longitude = null;
double? depthKm = null;
if ( !String.IsNullOrEmpty(title) && title.StartsWith("M") )
{
title = title.Substring(2, title.IndexOf(',')-3).Trim();
magnitude = TryParse(title);
}
if ( !String.IsNullOrEmpty(point) )
{
var values = point.Split(' ');
if ( values.Length == 2 )
{
latitude = TryParse(values[0]);
longitude = TryParse(values[1]);
}
}
if ( !String.IsNullOrEmpty(depth) )
{
depthKm = TryParse(depth);
if ( depthKm != null )
{
depthKm = Math.Round((-1 * depthKm.Value) / 100, 2);
}
}
if ( !String.IsNullOrEmpty(summary) )
{
summary = summary.Replace("</p>", "");
var values = summary.Split(
new string[] { "<p>" },
StringSplitOptions.RemoveEmptyEntries);
if ( values.Length == 3 )
{
var times = values[1].Split(
new string[] { "<br>" },
StringSplitOptions.RemoveEmptyEntries);
if ( times.Length > 0 )
{
utcTime = times[0];
}
if ( times.Length > 1 )
{
localTime = times[1];
}
depthDesc = values[2];
depthDesc = "Depth: " + depthDesc.Substring(depthDesc.IndexOf(":") + 2);
}
}
if ( latitude != null && longitude != null )
{
quake = new Earthquake()
{
Id = entry.Id,
Title = entry.Title.Text,
Summary = entry.Summary.Text,
Date = entry.LastUpdatedTime.DateTime,
Url = entry.Links.Select(l => Path.Combine(l.BaseUri.OriginalString,
l.Uri.OriginalString)).FirstOrDefault(),
Age = entry.Categories.Where(c => c.Label == "Age")
.Select(c => c.Name).FirstOrDefault(),
Magnitude = magnitude.GetValueOrDefault(),
Latitude = latitude.GetValueOrDefault(),
Longitude = longitude.GetValueOrDefault(),
DepthInKm = depthKm.GetValueOrDefault(),
DepthDesc = depthDesc,
UtcTime = utcTime,
LocalTime = localTime
};
}
return quake;
}
private T GetElementValue<T>(SyndicationItem entry, String name)
{
var el = entry.ElementExtensions.Where(e => e.OuterName == name).FirstOrDefault();
T value = default(T);
if ( el != null )
{
value = el.GetObject<T>();
}
return value;
}
private double? TryParse(String value)
{
double d;
if ( Double.TryParse(value, out d) )
{
return d;
}
return null;
}
/// <summary>
/// Gets the feed at the specified URL.
/// </summary>
/// <param name="url">The URL.</param>
/// <returns>A <see cref="SyndicationFeed"/> object.</returns>
public static SyndicationFeed GetFeed(String url)
{
SyndicationFeed feed = null;
try
{
log.Debug("Loading RSS feed: " + url);
using ( var reader = XmlReader.Create(url) )
{
feed = SyndicationFeed.Load(reader);
}
}
catch ( Exception ex )
{
log.Error("Error occurred while loading RSS feed: " + url, ex);
}
return feed;
}
}
}
The only method that will be generated in the client side proxy class, EarthquakeContext, will be the GetEarthquakes() method. The reason being that it is the only public instance method and it returns an IQueryable<Earthquake> collection that can be consumed by the client application. GetEarthquakes() calls the static GetFeed(String) method, which utilizes the built in SyndicationFeed API to load the external data feed. You will need to add a reference to the System.ServiceModel.Web library in order to take advantage of the RSS/Atom reader. The API will also allow you to create your own feeds to serve up in your applications.
Model
I have also created a Model folder and added a new class, Earthquake.cs. The Earthquake object will hold the various properties returned from the Atom feed. Here is a sample of the code for that class. Notice the [Key] attribute on the Id property, which is required by RIA Services to uniquely identify the entity.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ComponentModel.DataAnnotations;
namespace EarthquakeLocator.Web.Model
{
/// <summary>
/// Represents an earthquake occurrence and related information.
/// </summary>
[DataContract]
public class Earthquake
{
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
[Key]
[DataMember]
public string Id { get; set; }
/// <summary>
/// Gets or sets the title.
/// </summary>
/// <value>The title.</value>
[DataMember]
public string Title { get; set; }
/// <summary>
/// Gets or sets the summary.
/// </summary>
/// <value>The summary.</value>
[DataMember]
public string Summary { get; set; }
// additional properties omitted
}
}
View Model
The recent trend to use the MVVM pattern for WPF and Silverlight provides a great way to separate the data and behavior logic out of the user interface layer of your client applications. I have chosen to use the MVVM Light Toolkit for the Earthquake Locator, but there are other options out there if you prefer another library. That said, I went ahead and created a ViewModel folder in the Silverlight project and added a EarthquakeViewModel class that derives from ViewModelBase. Here is the code:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using Microsoft.Maps.MapControl;
using GalaSoft.MvvmLight;
using EarthquakeLocator.Web.Model;
using EarthquakeLocator.Web.Services;
namespace EarthquakeLocator.ViewModel
{
/// <summary>
/// Provides data for views displaying earthquake information.
/// </summary>
public class EarthquakeViewModel : ViewModelBase
{
[Import]
public EarthquakeContext Context;
/// <summary>
/// Initializes a new instance of the <see cref="EarthquakeViewModel"/> class.
/// </summary>
public EarthquakeViewModel()
{
var catalog = new AssemblyCatalog(GetType().Assembly);
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
Initialize();
}
/// <summary>
/// Initializes a new instance of the <see cref="EarthquakeViewModel"/> class.
/// </summary>
/// <param name="context">The context.</param>
public EarthquakeViewModel(EarthquakeContext context)
{
Context = context;
Initialize();
}
private void Initialize()
{
MapCenter = new Location(20, -170);
ZoomLevel = 2;
}
#region Private Methods
private void OnAutoLoadDataChanged()
{
LoadEarthquakes();
}
private void LoadEarthquakes()
{
var query = Context.GetEarthquakesQuery();
Context.Earthquakes.Clear();
Context.Load(query, (op) =>
{
if ( !op.HasError )
{
foreach ( var item in op.Entities )
{
Earthquakes.Add(item);
}
}
}, null);
}
#endregion Private Methods
#region Properties
private bool autoLoadData;
/// <summary>
/// Gets or sets a value indicating whether to auto load data.
/// </summary>
/// <value><c>true</c> if auto loading data; otherwise, <c>false</c>.</value>
public bool AutoLoadData
{
get { return autoLoadData; }
set
{
if ( autoLoadData != value )
{
autoLoadData = value;
RaisePropertyChanged("AutoLoadData");
OnAutoLoadDataChanged();
}
}
}
private ObservableCollection<Earthquake> earthquakes;
/// <summary>
/// Gets the collection of earthquakes to display.
/// </summary>
/// <value>The collection of earthquakes.</value>
public ObservableCollection<Earthquake> Earthquakes
{
get
{
if ( earthquakes == null )
{
earthquakes = new ObservableCollection<Earthquake>();
}
return earthquakes;
}
}
private Location mapCenter;
/// <summary>
/// Gets or sets the map center.
/// </summary>
/// <value>The map center.</value>
public Location MapCenter
{
get { return mapCenter; }
set
{
if ( mapCenter != value )
{
mapCenter = value;
RaisePropertyChanged("MapCenter");
}
}
}
private double zoomLevel;
/// <summary>
/// Gets or sets the zoom level.
/// </summary>
/// <value>The zoom level.</value>
public double ZoomLevel
{
get { return zoomLevel; }
set
{
if ( zoomLevel != value )
{
zoomLevel = value;
RaisePropertyChanged("ZoomLevel");
}
}
}
#endregion Properties
}
}
The EarthquakeViewModel class contains all of the properties that will be bound to by the various controls in our views. Be sure to read through the LoadEarthquakes() method, which handles calling the GetEarthquakes() method in our EarthquakeService via the EarthquakeContext proxy, and also transfers the loaded entities into the view model’s Earthquakes collection.
Another thing to notice is what’s going on in the default constructor. I chose to use the Managed Extensibility Framework (MEF) for my composition needs, but you can use any dependency injection library or none at all. To allow the EarthquakeContext class to be discoverable by MEF, I added the following partial class so that I could supply the appropriate [Export] attribute:
using System;
using System.ComponentModel.Composition;
namespace EarthquakeLocator.Web.Services
{
/// <summary>
/// The client side proxy for the EarthquakeService class.
/// </summary>
[Export]
public partial class EarthquakeContext
{
}
}
One last piece I wanted to point out before moving on to the user interface, I added a client side partial class for the Earthquake entity that contains helper properties that we will bind to later:
using System;
namespace EarthquakeLocator.Web.Model
{
/// <summary>
/// Represents an earthquake occurrence and related information.
/// </summary>
public partial class Earthquake
{
/// <summary>
/// Gets the location based on the current Latitude/Longitude.
/// </summary>
/// <value>The location.</value>
public string Location
{
get { return String.Format("{0},{1}", Latitude, Longitude); }
}
/// <summary>
/// Gets the size based on the Magnitude.
/// </summary>
/// <value>The size.</value>
public double Size
{
get { return (Magnitude * 3); }
}
}
}
View
Now the fun part! Usually, I would create a Views folder to place all of my View controls in, but I took the easy way out and added the following XAML code to the default MainPage.xaml file. Be sure to add the bing prefix associating the Microsoft.Maps.MapControl namespace after adding the assembly reference to your project.
The MVVM Light Toolkit project templates come with a ViewModelLocator class that you can use via a static resource, but I am instantiating the EarthquakeViewModel directly in my user control. I am setting the AutoLoadData property to true as a way to trigger the LoadEarthquakes() method call. The MapItemsControl found within the <bing:Map> control binds its ItemsSource property to the Earthquakes collection of the view model, and since it is an ObservableCollection<T>, we get the automatic two way data binding via the INotifyCollectionChanged interface.
<UserControl x:Class="EarthquakeLocator.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:bing="clr-namespace:Microsoft.Maps.MapControl;assembly=Microsoft.Maps.MapControl"
xmlns:vm="clr-namespace:EarthquakeLocator.ViewModel"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"
>
<UserControl.Resources>
<DataTemplate x:Key="EarthquakeTemplate">
<Ellipse Fill="Red" Stroke="Black" StrokeThickness="1"
Width="{Binding Size}" Height="{Binding Size}"
bing:MapLayer.Position="{Binding Location}"
bing:MapLayer.PositionOrigin="Center">
<ToolTipService.ToolTip>
<StackPanel>
<TextBlock Text="{Binding Title}" FontSize="14" FontWeight="Bold" />
<TextBlock Text="{Binding UtcTime}" />
<TextBlock Text="{Binding LocalTime}" />
<TextBlock Text="{Binding DepthDesc}" />
</StackPanel>
</ToolTipService.ToolTip>
</Ellipse>
</DataTemplate>
</UserControl.Resources>
<UserControl.DataContext>
<vm:EarthquakeViewModel AutoLoadData="True" />
</UserControl.DataContext>
<Grid x:Name="LayoutRoot">
<bing:Map x:Name="map" CredentialsProvider="--Your-Bing-Maps-Key--"
Center="{Binding MapCenter, Mode=TwoWay}"
ZoomLevel="{Binding ZoomLevel, Mode=TwoWay}">
<bing:MapItemsControl ItemsSource="{Binding Earthquakes}"
ItemTemplate="{StaticResource EarthquakeTemplate}" />
</bing:Map>
</Grid>
</UserControl>
The EarthquakeTemplate defines the Ellipse that will represent each earthquake, the Width and Height that are determined by the Magnitude, the Position on the map, and also the tooltip that will appear when we mouse over each data point. Running the application will give us the following result (shown with a tooltip example):
That concludes this portion of our show but I plan on implementing additional functionality in later blog posts. Be sure to come back soon to see the next installments in this series.
Enjoy!
Additional Resources
USGS Earthquake Data Feeds
Brad Abrams shows how RIA Services and MVVM can work together