Of
course, most people curious about their location prefer to see a map
rather than numeric coordinates. The Silverlight demonstration of the location service displays a map that comes to the program in the form of bitmaps.
In a real phone application, you’d probably be using Bing
Maps, particularly considering the existence of a Bing Maps Silverlight
Control tailored for the phone. Unfortunately, making use of Bing Maps
in a program involves opening a developer account, and getting a maps
key and a credential token. This is all free and straightforward but it
doesn’t work well for a program that will be shared among all the
readers of a book.
For that reason, I’ll be using an alternative that doesn’t require keys or tokens. This alternative is Microsoft Research Maps, which you can learn all about at msrmaps.com.
The aerial images are provided by the United States Geological Survey
(USGS). Microsoft Research Maps makes these images available through a web service called MSR Maps Service, but still sometimes referred to by its old name of TerraService.
The downside is that the images are not quite state-of-the-art and the service doesn’t always seem entirely reliable.
MSR Maps Service is a SOAP (Simple Object Access Protocol) service with the transactions described in a WSDL (Web Services Description Language) file. Behind the scenes, all the transactions between your program and the web service are in the form of XML files. However, to avoid programmer anguish, generally the WSDL file is used to generate a proxy, which is a collection of classes and structures that allow your program to communicate with the web service with method calls and events.
You can generate this proxy
right in Visual Studio. Here’s how I did it: I first created an Windows
Phone 7 project in Visual Studio called SilverlightLocationMapper.
In the Solution Explorer, I right-clicked the project name and selected
Add Service Reference. In the Address field I entered the URL of the
MSR Maps Service WSDL file: http://MSRMaps.com/TerraService2.asmx.
(You might wonder if the URL should be http://msrmaps.com/TerraService2.asmx?WSDL
because that’s how WSDL files are often referenced. That address will
actually seem to work at first, but you’ll get files containing obsolete
URLs.)
After you’ve entered the URL in
the Address field, press Go. Visual Studio will access the site and
report back what it finds. There will be one service, called by the old
name of TerraService.
Next you’ll want to enter
a name in the Namespace field to replace the generic ServiceReference1.
I used MsrMapsService and pressed OK.
You’ll then see MsrMapsService
show up under the project in the Solution Explorer. If you click the
little Show All Files icon at the top of the Solution Explorer, you can
view the generated files. In particular, nested under MsrMapsService and
Reference.svcmap, you’ll see Reference.cs, a big file (over 4000 lines)
with a namespace of XnaLocationMapper.MsrMapsService, which combines
the original project name and the name you selected for the web service.
This Reference.cs file contains
all the classes and structures you need to access the web service, and
which are documented on the msrmaps.com web site. To access these classes in your program, add a using direction:
using SilverlightLocationMapper.MsrMapsService;
You also need a reference to the System.Device assembly and using directives for the System.Device.Location, System.IO, and System.Windows.Media.Imaging namespacess.
In the MainPage.xaml file, I left the SupportedOrientations property at its default setting of Portrait, I removed the page title
to free up more space, and I moved the title panel below the content
grid just in case there was a danger of something spilling out of the
content grid and obscuring the title. Moving the title panel below the
content grid in the XAML file ensures that it will be visually on top.
Here’s the content grid:
Example 1. Silverlight Project: SilverlightLocationMapper File: MainPage.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <TextBlock Name="statusText" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap" />
<Image Source="Images/usgslogoFooter.png" Stretch="None" HorizontalAlignment="Right" VerticalAlignment="Bottom" /> </Grid>
|
The TextBlock is used to display status and (possibly) errors; the Image displays a logo of the United States Geological Survey.
The map bitmaps will be inserted between the TextBlock and Image so they obscure the TextBlock but the Image remains on top.
The code-behind file has just two fields, one for the GeoCoordinateWatcher that supplies the location information, and the other for the proxy class created when the web service was added:
Example 2. Silverlight Project: SilverlightLocationMapper File: MainPage.xaml.cs (excerpt)
public partial class MainPage : PhoneApplicationPage { GeoCoordinateWatcher geoWatcher = new GeoCoordinateWatcher(); TerraServiceSoapClient proxy = new TerraServiceSoapClient(); ... }
|
You use the proxy by calling
its methods, which make network requests. All these methods are
asynchronous. For each method you call, you must also supply a handler
for a completion event that is fired when the information you requested has been transferred to your application.
The completion event is accompanied by event arguments: a Cancelled property of type bool, an Error property that is null if there is no error, and a Result property that depends on the request.
I wanted the process to begin after the program was loaded and displayed, so I set a handler for the Loaded event. That Loaded handler sets the handlers for the two completion events I’ll require of the proxy, and also starts up the GeoCoordinateWatcher:
Example 3. Silverlight Project: SilverlightLocationMapper File: MainPage.xaml.cs (excerpt)
public MainPage() { InitializeComponent(); Loaded += OnMainPageLoaded; }
void OnMainPageLoaded(object sender, RoutedEventArgs args) { // Set event handlers for TerraServiceSoapClient proxy proxy.GetAreaFromPtCompleted += OnProxyGetAreaFromPtCompleted; proxy.GetTileCompleted += OnProxyGetTileCompleted;
// Start GeoCoordinateWatcher going statusText.Text = "Obtaining geographic location..."; geoWatcher.PositionChanged += OnGeoWatcherPositionChanged; geoWatcher.Start(); }
|
When coordinates are obtained, the following OnGeoWatcherPositionChanged method is called. This method begins by turning off the GeoCoordinateWatcher.
The program is not equipped to continuously update the display, so it
can’t do anything with any additional location information. It appends
the longitude and latitude to the TextBlock called ApplicationTitle displayed at the top of the screen.
Example 4. Silverlight Project: SilverlightLocationMapper File: MainPage.xaml.cs (excerpt)
void OnGeoWatcherPositionChanged(object sender, GeoPositionChangedEventArgs<GeoCoordinate> args) { // Turn off GeoWatcher geoWatcher.PositionChanged -= OnGeoWatcherPositionChanged; geoWatcher.Stop();
// Set coordinates to title text GeoCoordinate coord = args.Position.Location; ApplicationTitle.Text += ": " + String.Format("{0:F2}°{1} {2:F2}°{3}", Math.Abs(coord.Latitude), coord.Latitude > 0 ? 'N' : 'S', Math.Abs(coord.Longitude), coord.Longitude > 0 ? 'E' : 'W'); // Query proxy for AreaBoundingBox LonLatPt center = new LonLatPt(); center.Lon = args.Position.Location.Longitude; center.Lat = args.Position.Location.Latitude;
statusText.Text = "Accessing Microsoft Research Maps Service..."; proxy.GetAreaFromPtAsync(center, 1, Scale.Scale16m, (int)ContentPanel.ActualWidth,
(int)ContentPanel.ActualHeight); }
|
The method concludes by making its first call to the proxy. The GetAreaFromPtAsync
call requires a longitude and latitude as a center point, but some
other information as well. The second argument is 1 to get an aerial
view and 2 for a map . The third argument is the desired scale, a member of the Scale enumeration. The member I’ve chosen means that each pixel of the returned bitmaps is equivalent to 16 meters.
Watch out: Some scaling factors—in particular, Scale2m, Scale8m, and Scale32m—result
in GIF files being returned. Remember, remember, remember that
Silverlight doesn’t do GIF! For the other scaling factors, JPEGS are
returned.
The final arguments to GetAreaFromPtAsync are the width and height of the area you wish to cover with the map.
All the bitmaps you get back
from the MSR Maps Service are 200 pixels square. Almost always, you’ll
need multiple bitmaps to tile a complete area. For example, if the last
two arguments to GetAreaFromPtAsync are 400 and 600, you’ll need 6 bitmaps to tile the area.
Well, actually not: An area of 400 pixels by 600 pixels will require 12 bitmaps, 3 horizontally and 4 vertically.
Here’s the catch:
These bitmaps aren’t specially created when a program requests them.
They already exist on the server in all the various scales. The
geographic coordinates where these bitmaps begin and end are fixed. So
if you want to cover a particular area of your display with a tiled map,
and you want the center of this area to be precisely the coordinate you
specify, the existing tiles aren’t going to fit exactly. You want
sufficient tiles to cover your area, but the tiles around the boundary
are going to hang over the edges.
What you get back from the GetAreaFromPtAsync call (in the following OnProxyGetAreaFromPtCompleted method) is an object of type AreaBoundingBox.
This is a rather complex structure that nonetheless has all the
information required to request the individual tiles you need and then
assemble them together in a grid.
Example 5. Silverlight Project: SilverlightLocationMapper File: MainPage.xaml.cs (excerpt)
void OnProxyGetAreaFromPtCompleted(object sender, GetAreaFromPtCompletedEventArgs args) { if (args.Error != null) { statusText.Text = args.Error.Message; return; }
statusText.Text = "Getting map tiles...";
AreaBoundingBox box = args.Result; int xBeg = box.NorthWest.TileMeta.Id.X; int yBeg = box.NorthWest.TileMeta.Id.Y; int xEnd = box.NorthEast.TileMeta.Id.X; int yEnd = box.SouthWest.TileMeta.Id.Y;
// Loop through the tiles for (int x = xBeg; x <= xEnd; x++) for (int y = yBeg; y >= yEnd; y--) { // Create Image object to display tile Image img = new Image(); img.Stretch = Stretch.None; img.HorizontalAlignment = HorizontalAlignment.Left; img.VerticalAlignment = VerticalAlignment.Top; img.Margin = new Thickness((x - xBeg) * 200 - box.NorthWest.Offset.XOffset, (yBeg - y) * 200 - box.NorthWest.Offset.YOffset, 0, 0);
// Insert after TextBlock but before Image with logo ContentPanel.Children.Insert(1, img);
// Define the tile ID TileId tileId = box.NorthWest.TileMeta.Id; tileId.X = x; tileId.Y = y;
// Call proxy to get the tile (Notice that Image is user object) proxy.GetTileAsync(tileId, img); } }
|
I won’t discuss the intricacies of AreaBoundingBox because it’s more or less documented on the msrmaps.com
web site, and I was greatly assisted by some similar logic on the site
written for Windows Forms (which I suppose dates it a bit).
Notice that the loop creates each Image object to display each tile. Each of these Image objects has the same Stretch, HorizontalAlignment, and VerticalAlignmentMargin properties, but a different . This Margin is how the individual tiles are positioned within the content grid. The XOffset and YOffset
values cause the tiles to hang off the top and left edges of the
content grid. The content grid doesn’t clip its contents, so these tiles
possibly extend to the top of the program’s page.
Notice also that each Image object is passed as a second argument to the proxy’s GetTileAsync method. This is called the UserState argument. The proxy doesn’t do anything with this argument except return it as the UserState property of the completion arguments, as shown here:
Example 6. Silverlight Project: SilverlightLocationManager File: MainPage.xaml.cs (excerpt)
void OnProxyGetTileCompleted(object sender, GetTileCompletedEventArgs args) { if (args.Error != null) { return; }
Image img = args.UserState as Image; BitmapImage bmp = new BitmapImage(); bmp.SetSource(new MemoryStream(args.Result)); img.Source = bmp; }
|
That’s how the method links up the particular bitmap tile with the particular Image element already in place in the content grid.
It is my experience
that in most cases, the program doesn’t get all the tiles it requests.
If you’re very lucky—and you happen to be running the program somewhere
in my neighborhood—your display might look like this:
If you change the second argument of the proxy.GetAreaFromPtAsync call from a 1 to a 2, you get back images of an actual map rather than an aerial view:
It has a certain retro
charm—and I love the watercolor look—but I’m afraid that modern users
are accustomed to something just a little more 21st century.