Bing Maps now using Nokia for Geocoding and Traffic Services

The announcement continues by listing a list of countries with noticable improvements including Greece. So -as you would expect- I put this to the test. I have been trying Bing Maps geocoding for various projects in Greece and thus far the results were far from perfect.

For starters, you could never actually type an address in Greek but instead had to use the Latin transliteration. I had to write code, so the user would type the address in Greek and string to be tranliterated back to Latin letters so Bing Maps can understand it! Something that Google maps never had a problem with.

More importantly, apart from the two major cities in Greece, Athens and Thessaloniki, Bing Maps could not find an exact street address but was simply zooming at the begining of the street segment you entered.

But all this has changed now thank god! Users can happily type a Greek address, full with street name and number and Bing maps will zoom to the right place on the map. Still, the actual street labels display in their Latin equivalelents but hey, you can’t have it all – for now at least. So kudos to Microsoft/Nokia for this. I am a happy camper now!

Advertisements

Bing Maps Tile System: A bit of theory and an app to test it in Windows Phone 7

A few weeks back my good friend, colleague and Microsoft trainer par excellence, Elias Markelis asked me to do a joint webinar on Windows Phone and Bing Maps, part of Microsoft’s Live Virtual Academy series. The idea being that since I am the ‘GIS guy’ I will talk a bit more about the spatial / map aspects and not just the code.

(BTW, you can also find the full webinar at http://www.techdays.gr/videos/4056.html but take note: The slides are in English but seminar was delivered in Greek.)

Elias has now written a 3-part blog post to cover most of the things we talked about in the webinar: You can find the links here:

These are very detailed step-by-step guides on how to develop your first Windows phone map-based app, utilizing the Geocoding and Routing Services, GPS, the emulator and lots of other goodies to get you started.

But this is the code only. In this post, which can be thought as an ‘addendum’ to Elias’ series, I would talk about how Bing Maps work, their tile system, scales and projection systems. The post is mainly geared towards Silverlight and Windows Phone developers who never worked with GIS and mapping systems before. GIS gurus, feel free to skip directly to the code!

Should I care? Just show me some code!

Well, you should care really. Understanding the tile system and projections would make a huge difference in real-world applications where you would invariably need to integrate with other spatial data in different formats. For example you may have to display geometry data from a SQL Server Database, or add a WMS layer. In any of these cases, you would need to understand terms like ‘projection system’, ‘geographic coordinates’ and ‘map scales’. And also understand how these apply in Bing Maps.

But fear not, its not going to be ALL theory. I would also show you how to develop a Bing Maps app which will help you visualize some of the theory and maths behind Bing Maps.

The Basics – Bing Maps tile system

Now, before I start, I should point out that there is an excellent article in MSDN which explains the Bing Maps Tile System in great detail, including all the maths behind it. It even includes all the C# code that implements it. In actual fact, I would be using bits of this code in my own example. But here, I will just give you the basics to get you started in the form of a list of simple, clear statements. So here it goes:

  • Bing Maps uses a single map projection system called Mercator. The projection is necessary since Bing Maps displays the whole earth, the earth, well, is round, so in order to see it a screen (which, you know, is flat) we have to ‘project’ it. The image below depicts this concept. (This is not the Mercator projection BTW, just a peeled orange ).
image
The Earth is round, a computer screen ain
  • To accurately locate a point or any other feature on the map, the WGS84 Spatial Reference System (SRID) is used which uses geographic coordinates (Latitude and Longitude denoted also as φ,λ). This is the same SRID used by the GPS devices (see the link here?)
  • Map scale is another feature of any map. It denotes the ratio between a real-world distance and the map distance at a specified unit. For example a map at a scale of 1:100,000 with map units in meters, means that a meter measured on the map (or you screen) is a 100,000m in the real world. One of the things to point out here is that scale only makes sense and applies on maps on a screen or on paper. Data itself does not have scale.For example, the question of what is the scale of a point at coordinates 40,2345, 22.87361 does not make sense!
  • In very simple terms Bing Maps maps the world using satellite images stitched together. As you can appreciate, these are terrabytes of data we are talking about.To optimize performance these images are divided into tiles of 256×256 pixels each.
  • Bing Maps has the concept of zoom levels. There are 23 zoom levels where 1 displays the whole earth and 23 zooms at street level. When you zoom in or out at a different zoom level, different tiles (and hence satellite images) are displayed.
  • Tiles have their own coordinate system (a “Tile XY coordinate”) which can also be denoted as a single string called a quadkey. As explained in the Bing Maps Tile System article:

To optimize the indexing and storage of tiles, the two-dimensional tile XY coordinates are combined into one-dimensional strings called quadtree keys, or “quadkeys” for short. Each quadkey uniquely identifies a single tile at a particular level of detail…

The example below displays the quadkeys for Levels 1-3 (image from the same article):

image
Quadkeys for zoom levels 1 to 3

Some code (at last)

Just to ‘visualize’ what we talked about, we will create a Windows Phone application with Bing Maps which will accomplish a few simple tasks:

  • Double-tapping the map will display the quadkey value of the current tile in a message box.
  • Panning the map, will display the latitude, longitude coordinates in a textblock.
  • The map will also draw the tiles used in the current map extent and display their quadkey value

To start off with, start a new Windows Phone 7 project in Visual Studio 2010 and add the maps control I won’t go into any details on how to do this, there are a lot of articles abound. You can try Elias’ Windows Phone location awareness– the basics or one of mine Sterling Database for Windows Phone 7, Silverlight and Bing Maps

We will now add 3 event handlers to the map control. One for double-tapping on the map, one for mouse move and one for the ViewChangeEnd which fires when the map has finished zooming or panning. Our XAML file should now look like this:

Code Snippet
  1. <phone:PhoneApplicationPage
  2.     x:Class=”BingMapsBasics.MainPage”
  3.     xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation&#8221;
  4.     xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml&#8221;
  5.     xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
  6.     xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
  7.     xmlns:d=”http://schemas.microsoft.com/expression/blend/2008&#8243;
  8.     xmlns:mc=”http://schemas.openxmlformats.org/markup-compatibility/2006&#8243;
  9.     xmlns:my=”clr-namespace:Microsoft.Phone.Controls.Maps;assembly=Microsoft.Phone.Controls.Maps”
  10.     mc:Ignorable=”d” d:DesignWidth=”480″ d:DesignHeight=”768″
  11.     FontFamily=”{StaticResource PhoneFontFamilyNormal}
  12.     FontSize=”{StaticResource PhoneFontSizeNormal}
  13.     Foreground=”{StaticResource PhoneForegroundBrush}
  14.     SupportedOrientations=”Portrait” Orientation=”Portrait”
  15.     shell:SystemTray.IsVisible=”True”>
  16.     <!–LayoutRoot is the root grid where all page content is placed–>
  17.     <Grid x:Name=”LayoutRoot” Background=”Transparent”>
  18.         <Grid.RowDefinitions>
  19.             <RowDefinition Height=”Auto”/>
  20.             <RowDefinition Height=”*”/>
  21.         </Grid.RowDefinitions>
  22.         <!–TitlePanel contains the name of the application and page title–>
  23.         <StackPanel x:Name=”TitlePanel” Grid.Row=”0″ Margin=”12,17,0,28″>
  24.             <TextBlock x:Name=”ApplicationTitle” Text=”Bing Maps Demo” Style=”{StaticResource PhoneTextNormalStyle}“/>
  25.             <TextBlock x:Name=”PageTitle” Text=”Map Basics” Margin=”9,-7,0,0″ Style=”{StaticResource PhoneTextTitle1Style}“/>
  26.         </StackPanel>
  27.         <Grid x:Name=”ContentPanel” Grid.Row=”1″ Margin=”12,0,12,0″>
  28.             <Grid.RowDefinitions>
  29.                 <RowDefinition Height=”50″/>
  30.                 <RowDefinition Height=”*”/>
  31.             </Grid.RowDefinitions>
  32.             <my:Map Grid.Row=”1″ x:Name=”demoMap” Margin=”0,0,0,0″
  33.                     CredentialsProvider=”YOURBINGMAPSCODEHERE”
  34.                     DoubleTap=”demoMap_DoubleTap”
  35.                     MouseMove=”demoMap_MouseMove” ZoomBarVisibility=”Visible”
  36.                    ViewChangeEnd=”demoMap_ViewChangeEnd”>
  37.                 <my:MapLayer x:Name=”quadkeys”></my:MapLayer>
  38.             </my:Map>
  39.             <TextBlock Grid.Row=”0″ x:Name=”txbCoords” VerticalAlignment=”Top” HorizontalAlignment=”Left” Foreground=”White” FontWeight=”Bold” Text=”Coordinates:” />
  40.         </Grid>
  41.     </Grid>
  42. </phone:PhoneApplicationPage>

The MouseMove event handler is listed below:

Code Snippet
  1. private void demoMap_MouseMove(object sender, MouseEventArgs e)
  2.         {
  3.             // Point in pixels
  4.             Point pnt = e.GetPosition(this.demoMap);
  5.             // Geographic point
  6.             GeoCoordinate geoPnt = new GeoCoordinate();
  7.             // Get current point in Lat/Lon
  8.             geoPnt = demoMap.ViewportPointToLocation(pnt);
  9.             //Update the textblock with the current Lat/Lon
  10.             txbCoords.Text = “Lat: “ + geoPnt.Latitude.ToString() +” Lon: “ + geoPnt.Longitude.ToString();
  11.         }

Our DoubleTap handler follows. It basically picks up the Latitude, Longitude location the user tapped on the map, converts them to pixel coordinates, then to Tile coordinates and then to the quadkey value. All the conversion functions can be found in the (yes, you guessed it) MSDN article mentioned above.

Code Snippet
  1. private void demoMap_DoubleTap(object sender, GestureEventArgs e)
  2.         {
  3.             int pX; int pY;
  4.             int tX; int tY;
  5.             Point p = e.GetPosition(this.demoMap);
  6.             GeoCoordinate geo = new GeoCoordinate();
  7.             // Get current point in Lat/Lon
  8.             geo = demoMap.ViewportPointToLocation(p);
  9.             // Convert to pixel coordinates
  10.             LatLongToPixelXY(geo.Latitude, geo.Longitude, Convert.ToInt32(demoMap.ZoomLevel), out pX, out pY);
  11.             //Convert to tile coordinates
  12.             PixelXYToTileXY(pX, pY, out tX, out tY);
  13.             //Get the Quadkey
  14.             string quadKey = TileXYToQuadKey(tX, tY, Convert.ToInt32(demoMap.ZoomLevel));
  15.             MessageBox.Show(“QuadKey: “ + quadKey + “Lat: “ + geo.Latitude.ToString() +” Lon: “ + geo.Longitude.ToString());
  16.         }

Okimage, for the time being leave our last event handler (ViewChangeEnd) empty and compile and run the project. Zoom and pan the map to an area you are interested in. Notice that while panning the geographic coordinates are getting updated in the text block.

Now double click/tap at different areas of the screen. A message box should appear displaying the quadkey of the current tile

That’s ok. Useful-ish. But you have to \guess’ where the different tiles are. What we will do next is display the actual tiles on the map as rectangles and also display the corresponding quadkeys. And we will wire this functionality in the ViewChangEnd event, so it runs only when the map has finished rendering.

The logic behind the drawing of the tiles is pretty simple: Find the extent of the current map (min & max coordinates) and then ‘loop’ or ‘sweep’ the map, 256pixels at a time to get to the next tile, calculating the rectangle (tile) at each iteration and draw this rectangle on the map.

I implemented this ‘sweeping’ using three loops, one by latitude, one by longitude and one ‘diagonal’ where both latitude and longitude were iterated by 256 pixels. Now, I am pretty sure there is a by far smarter way of achieving the same result but its getting late and this was the easiest way I could come up with.

The relevant code is listed below:

Code Snippet
  1.  private void demoMap_ViewChangeEnd(object sender, MapEventArgs e)
  2.  {
  3.      CalcQuadkeys();
  4.  }
  5.  ///<summary>
  6.  /// Calculates the quadkeys
  7.  ///</summary>
  8.  public void CalcQuadkeys()
  9.  {
  10.      MapLayer l = (MapLayer)demoMap.FindName(“quadkeys”);
  11.      l.Children.Clear();
  12.      LocationRect extents = demoMap.BoundingRectangle;
  13.      double minLat = extents.Southwest.Latitude;
  14.      double minLon = extents.Southwest.Longitude;
  15.      double maxLat = extents.Northeast.Latitude;
  16.      double maxLon = extents.Northeast.Longitude;
  17.      double curLat = minLat;
  18.      double curLon = minLon;
  19.      // ‘Scan’ the map in the Latitude direction increasing the Longitude value in each step
  20.      while (curLat <= maxLat)
  21.      {
  22.          //Starting from the ll
  23.          int nextLatInPixels; int nextLonInPixels;
  24.          int curLatInPixels; int curLonInPixels;
  25.          int curTileX; int curTileY;
  26.          double nextLat; double nextLon;
  27.          LatLongToPixelXY(curLat, curLon, Convert.ToInt32(demoMap.ZoomLevel), out curLatInPixels, out curLonInPixels);
  28.          nextLatInPixels = curLatInPixels + 256;
  29.          nextLonInPixels = curLonInPixels – 256;
  30.          // Now convert them back to WGS84 coords
  31.          PixelXYToLatLong(nextLatInPixels, nextLonInPixels, Convert.ToInt32(demoMap.ZoomLevel), out nextLat, out nextLon);
  32.          PixelXYToTileXY(curLatInPixels, curLonInPixels, out curTileX, out curTileY);
  33.          string quadKey = TileXYToQuadKey(curTileX, curTileY, Convert.ToInt32(demoMap.ZoomLevel));
  34.          DrawQuadkeys(quadKey, curLat, curLon, nextLat, nextLon);
  35.          curLat = nextLat;
  36.      }
  37.      //Reset values
  38.      curLat = minLat;
  39.      curLon = minLon;
  40.      // ‘Scan’ the map in the Longitude direction increasing the Longitude value in each step
  41.      while (curLon <= maxLon)
  42.      {
  43.          int nextLatInPixels; int nextLonInPixels;
  44.          int curLatInPixels; int curLonInPixels;
  45.          int curTileX; int curTileY;
  46.          double nextLat; double nextLon;
  47.          LatLongToPixelXY(curLat, curLon, Convert.ToInt32(demoMap.ZoomLevel), out curLatInPixels, out curLonInPixels);
  48.          nextLatInPixels = curLatInPixels + 256;
  49.          nextLonInPixels = curLonInPixels – 256;
  50.          // Now convert them back to WGS84 coords
  51.          PixelXYToLatLong(nextLatInPixels, nextLonInPixels, Convert.ToInt32(demoMap.ZoomLevel), out nextLat, out nextLon);
  52.          PixelXYToTileXY(curLatInPixels, curLonInPixels, out curTileX, out curTileY);
  53.          string quadKey = TileXYToQuadKey(curTileX, curTileY, Convert.ToInt32(demoMap.ZoomLevel));
  54.          DrawQuadkeys(quadKey, curLat, curLon, nextLat, nextLon);
  55.          curLon = nextLon;
  56.      }
  57.      //Reset values
  58.      curLat = minLat;
  59.      curLon = minLon;
  60.      // ‘Scan’ the map diagonally increasing both Lat & Lon in each step
  61.      while (curLon <= maxLon && curLat <= maxLat)
  62.      {
  63.          int nextLatInPixels; int nextLonInPixels;
  64.          int curLatInPixels; int curLonInPixels;
  65.          int curTileX; int curTileY;
  66.          double nextLat; double nextLon;
  67.          LatLongToPixelXY(curLat, curLon, Convert.ToInt32(demoMap.ZoomLevel), out curLatInPixels, out curLonInPixels);
  68.          nextLatInPixels = curLatInPixels + 256;
  69.          nextLonInPixels = curLonInPixels – 256;
  70.          // Now convert them back to WGS84 coords
  71.          PixelXYToLatLong(nextLatInPixels, nextLonInPixels, Convert.ToInt32(demoMap.ZoomLevel), out nextLat, out nextLon);
  72.          PixelXYToTileXY(curLatInPixels, curLonInPixels, out curTileX, out curTileY);
  73.          string quadKey = TileXYToQuadKey(curTileX, curTileY, Convert.ToInt32(demoMap.ZoomLevel));
  74.          DrawQuadkeys(quadKey, curLat, curLon, nextLat, nextLon);
  75.          curLat = nextLat;
  76.          curLon = nextLon;
  77.      }
  78.  }
  79.  ///<summary>
  80.  /// Draws the Quadkeys as rectangles on the map
  81.  ///</summary>
  82.  ///<param name=”quadkey”></param>
  83.  ///<param name=”minlat”></param>
  84.  ///<param name=”minlon”></param>
  85.  ///<param name=”maxlat”></param>
  86.  ///<param name=”maxlon”></param>
  87.  public void DrawQuadkeys(string quadkey, double minlat, double minlon, double maxlat, double maxlon)
  88.  {
  89.      //Get the Quadkeys layer
  90.      MapLayer l = (MapLayer)demoMap.FindName(“quadkeys”);
  91.      //Create polygon
  92.      LocationCollection locs = new LocationCollection();
  93.      locs.Add(new GeoCoordinate(minlat, minlon));
  94.      locs.Add(new GeoCoordinate(maxlat, minlon));
  95.      locs.Add(new GeoCoordinate(maxlat, maxlon));
  96.      locs.Add(new GeoCoordinate(minlat, maxlon));
  97.      MapPolygon mp = new MapPolygon();
  98.      mp.Locations = locs;
  99.      mp.Stroke = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Red);
  100.      mp.StrokeThickness = 2;
  101.      // Add polygon
  102.      l.Children.Add(mp);
  103.      var avgLat = mp.Locations.Select(q => q.Latitude).Average();
  104.      var avgLon = mp.Locations.Select(q => q.Longitude).Average();
  105.      // Add quadkey label
  106.      TextBox qLabel = new TextBox();
  107.      qLabel.Text = quadkey;
  108.      l.AddChild(qLabel, new GeoCoordinate(avgLat, avgLon));
  109.  }

Now compile and run the app again. This time you should be able to see the tiles appearing as red rectangles every time you pan and zoom the map with the quadkey displaying inside as in the image below.

image

Hope this example clears up a few things about Bing Maps and how they work. You can download the full application from http://www.box.com/s/ppovoyfbuiqf304qod8b

Enjoy coding! – PV

Storing your last map location in the Bing Maps AJAX control using cookies

This is an example of an easy and clean way of storing the last map location and zoom level for a user. The location is saved in a cookie, so next time the user will open you app the map will zoom to the last used location. And since I much prefer using ASP.NET than plain HTML, this example is implemented in the default ASP.NET Web template using Master Pages.

So here is goes:

The first thing you need to do is create a new ASP.NET Web Site in VS2010 using the Default template. Next open your Default.aspx page.

Add a reference to the Bing Maps control javascript API under the header content:

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
    <script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>

Next, add a reference to the jQuery script (it should be under your Styles folder:

<script type="text/javascript" src="Scripts/jquery-1.4.1.min.js"></script>

Remove (if you wish), all the default template text under the DefaultContent and create a <div> which will hold your map, i.e.

<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
   <div id="map"></div>
</asp:Content>

Now create a few of the helper functions within a <script> tag. The first one to use would be a couple of functions to create and read cookie values as per Peter-Paul Koch’s post

function createCookie(name, value, days) {
     if (days) {
        var date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        var expires = "; expires=" + date.toGMTString();
     }
     else var expires = "";
         document.cookie = name + "=" + value + expires + "; path=/";
}
function readCookie(name) {
    var nameEQ = name + "=";
    var ca = document.cookie.split(';');
    for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') c = c.substring(1, c.length);
            if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
         }
          return null;
     }

Create the setInitialView() function. This function sets a cookie with a name of ‘LastLocation’ and returns its value. If no location cookie is set, a default location will be used

function setinitialView() {
     if (readCookie('LastLocation') == null) {
         // Default map center
         createCookie('LastLocation', '40.6318' + ':' + '22.94758' + ':' + '18', 30);
     }
     var lastloc = readCookie('LastLocation');
      return lastloc;
}

Create the getMap() function which will display the Bing Maps control.

function GetMap() {
    // Get cookie
    var s = setinitialView();
    var loc = s.split(":");
    var mapSettings = {
         // MapOptions
         credentials: 'YOURBINGMAPSKEYHERE',
          // ViewOptions
          // default to roughly the center of Thessaloniki.
          center: new Microsoft.Maps.Location(loc[0], loc[1]),
          mapTypeId: Microsoft.Maps.MapTypeId.birdseye,
          padding: 1,
          zoom: parseInt(loc[2])
    };
    map = new Microsoft.Maps.Map(document.getElementById("map"), mapSettings);
    // Store a cookie with the current position
    Microsoft.Maps.Events.addHandler(map, 'viewchangeend', mapViewChangeEnd);
}

Note that we have wired up the viewchangeend event to the mapViewChangeEnd function. This event will fire every time we pan and zoom on the map and will set the LastLocation cookie to the current map center and zoom level:

function mapViewChangeEnd(e) {
    var curLocation = map.getCenter();
    var curLat = curLocation.latitude;
    var curLon = curLocation.longitude;
    createCookie('LastLocation', curLat + ':' + curLon + ':' + map.getZoom(), 30);
}

Finally, use a bit of jQuery to load the map when the DOM is fully loaded:

var map = null;
$(document).ready(function () {
    GetMap();
 });

All set! Your aspx page should now look this:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeFile="Default.aspx.cs" Inherits="_Default" %>
 
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
    <script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
     <script type="text/javascript" src="Scripts/jquery-1.4.1.min.js"></script>
    <script type="text/javascript">
        function createCookie(name, value, days) {
            if (days) {
                var date = new Date();
                date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
                var expires = "; expires=" + date.toGMTString();
            }
            else var expires = "";
            document.cookie = name + "=" + value + expires + "; path=/";
        }
 
        function readCookie(name) {
            var nameEQ = name + "=";
            var ca = document.cookie.split(';');
            for (var i = 0; i < ca.length; i++) {
                var c = ca[i];
                while (c.charAt(0) == ' ') c = c.substring(1, c.length);
                if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
            }
            return null;
        }
        function setinitialView() {
            if (readCookie('LastLocation') == null) {
                // Default map center
                createCookie('LastLocation', '40.6318' + ':' + '22.94758' + ':' + '18', 30);
            }
            var lastloc = readCookie('LastLocation');
            return lastloc;
        }
 
        function mapViewChangeEnd(e) {
            var curLocation = map.getCenter();
            var curLat = curLocation.latitude;
            var curLon = curLocation.longitude;
            createCookie('LastLocation', curLat + ':' + curLon + ':' + map.getZoom(), 30);
        }
        function GetMap() {
            // Get cookie
            var s = setinitialView();
            var loc = s.split(":");
            var mapSettings = {
                // MapOptions
                credentials: 'YOURBINGMAPSKEYHERE',
                // ViewOptions
                // default to roughly the center of Thessaloniki.
                center: new Microsoft.Maps.Location(loc[0], loc[1]),
                mapTypeId: Microsoft.Maps.MapTypeId.birdseye,
                padding: 1,
                zoom: parseInt(loc[2])
            };
            map = new Microsoft.Maps.Map(document.getElementById("map"), mapSettings);
            // Store a cookie with the current position
            Microsoft.Maps.Events.addHandler(map, 'viewchangeend', mapViewChangeEnd);
        }
        var map = null;
        $(document).ready(function () {
            GetMap();
        });
 
    </script>
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
   <div id="map"></div>
</asp:Content>

Greek Weather on Bing Maps

Planning for a Greek holiday or trip this summer? You can now check the weather for the whole of Greece using the Greek National Observatory of Athens’s website http://www.meteo.gr/bingmaps/ which is based on Bing Maps. Click on the play button on the top right of the screen to animate the map and take you through the forecast for the next few days. SOME knowledge of Greek is required! At least as far as knowing what the words are for Temperature (Θερμοκρασία), Pressure (Πίεση), Humidity (Υγρασία) and Rainfall (Βροχόπτωση)

Address Geocoding in .NET using the Bing Maps REST Services API

I came across this excellent post by Jatin Kacha on how to call the Google Map WebService API from asp.net and though to change it slightly to instead call the Bing Maps REST service.

The main changes made on Jatin’s code is the calling URL and the way the returned XML is handled due to the differences between Google’s and Bing’s services plus a small change when retrieving the Lat/Lon values to cater for non-english number formats.

All you need to do is create a class file and copy/paste the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Net;
using System.IO;
using System.Configuration;

namespace Gaiocorp.Geocoding
{
    /// Resolve addresses into latitude/longitude coordinates using Google MAP API webservices
    public static class BingGeocoder
    {
        private static string _BingMapsKey = ConfigurationManager.AppSettings["BingMapsKey"];
        /// Bing Maps Geocoder
        /// Url request to
        ///http://dev.virtualearth.net/REST/v1/Locations?countryRegion=countryRegion&adminDistrict=adminDistrict&locality=locality&postalCode=postalCode&addressLine=addressLine&key=BingMapsKey;
        public static BingGeolocation? ResolveAddress(string query)
        {
            if (string.IsNullOrEmpty(_BingMapsKey))
            {
                _BingMapsKey = System.Configuration.ConfigurationManager.AppSettings["BingMapsKey"];
            }
            string url = "http://dev.virtualearth.net/REST/v1/Locations?o=xml&q={0}&key=" + _BingMapsKey;
            url = String.Format(url, query);
            XmlNode coords = null;
            try
            {
                string xmlString = GetUrl(url);
                XmlDocument xd = new XmlDocument();
                xd.LoadXml(xmlString);
                XmlNamespaceManager xnm = new XmlNamespaceManager(xd.NameTable);
                coords = xd.GetElementsByTagName("Point")[0];
             }

             catch { }
             BingGeolocation? gl = null;
             if (coords != null & coords.HasChildNodes)
             {
                string Lat = coords.ChildNodes[0].InnerText;
                string Lon = coords.ChildNodes[1].InnerText;
                gl = new BingGeolocation(Convert.ToDecimal(Lat, System.Globalization.CultureInfo.InvariantCulture), Convert.ToDecimal(Lon, System.Globalization.CultureInfo.InvariantCulture));
             }
             return gl;
         }

        ///<summary>
        /// Retrieve a Url via WebClient
        /// 
        public static string GetUrl(string url)
        {
            string result = string.Empty;
            System.Net.WebClient Client = new WebClient();
            using (Stream strm = Client.OpenRead(url))
            {
                StreamReader sr = new StreamReader(strm);
                result = sr.ReadToEnd();
            }

            return result;
        }

        /// <summary>
        /// Returns the Lat
        /// </summary>
        /// <param name="address"></param>
        /// <param name="city"></param>
        /// <param name="state"></param>
        /// <param name="postcode"></param>
        /// <param name="country"></param>
        /// <returns></returns>
        public static BingGeolocation? ResolveAddress(string address, string city, string state, string postcode, string country)
        {
            return ResolveAddress("&addressLine=" + address + "&locality=" + city + "&adminDistrict=" + state + "&postalCode=" + postcode + "&countryRegion=" + country);
        }

    }

    public struct BingGeolocation
    {
        public decimal Lat;

        public decimal Lon;

        public BingGeolocation(decimal lat, decimal lon)
        {
            Lat = lat;
            Lon = lon;
        }

        public override string ToString()
        {
            return "Latitude: " + Lat.ToString() + " Longitude: " + Lon.ToString();
        }
        public string ToQueryString()
        {
            return "+to:" + Lat + "%2B" + Lon;
        }
    }
}

And there you have it. You could then call the function in the following manner, say from a button click. The only potential problem may be to parse correctly the input address since the Bing Maps API requires all address components (street, city,zip etc) to be provided separately.

protected void Button4_Click(object sender, EventArgs e) //Get latlon using Bing
        {
            if (txbAddress.Text.Trim() != string.Empty)
            {
               BingGeolocation? bl= BingGeocoder.ResolveAddress(txbAddress.Text.Split(',')[0].Trim(), txbAddress.Text.Split(',')[1].Trim(), string.Empty, string.Empty, string.Empty);
               if (bl != null)
               {
                   txbBing.Text = "Lat: " + bl.Value.Lat.ToString() + " Lon:" + bl.Value.Lon;
               }
            }
        }

You could also easily extend it to also add reverse address geocoding or routing. For more information you can check the Bing Maps REST Services API Reference.

Enjoy!

Sterling Database for Windows Phone 7, Silverlight and Bing Maps

A bit of a mouthful of a title you may think, but this post should hopefully show you just that. How to create your first Bing Maps enabled, Windows Phone 7 application using Silverlight, and the lovely little database for Windows Phone 7, called Sterling. Best of all, everything you will need for this project is free!

The application will display a Bing Map on your phone, load some POI data in, and then allow you to search for POIs and pan and zoom the map on that location. Think of it as the prototype for a mobile ‘City Guide’ application.

So lets get started. The first bits you will need:

  • Visual Studio 2010 Express
  • Windows Phone Emulator
  • Silverlight 4
  • .NET Framework 4

You can download all this from the Windows Phone Development Site at: http://create.msdn.com/en-us/home/getting_started

Unless you already own a Windows 7 phone, my guess is that you will do most of the work in the emulator. One important note to make is that the emulator does not work (either at all or not well) on Virtual Machines. It took me a while to figure this out so beware!

Windows Phone 7 does not have a native embedded database so I used this little gem of a database called Sterling written by Jeremy Likness. Download the latest Sterling and extract the zip file to a folder of your choice.

Have a play with the sample project by opening the SterlingPhoneExample.sln solution in Visual Studio under the SterlingDB\src\SterlingSln folder (This will also build the Wintellect.Sterling.WindowsPhone.csproj which we will then reference in our project).

In order for the map control to work correctly, you will also need a Bing Maps Key. Click here for more information.

We are now ready to start our project.

  1. Launch Visual Studio 2010 Express for Windows Phone from the Windows Start menu.

  2. Create a new project by clicking the File | New Project menu command.

  3. The New Project window will be displayed. Select the Windows Phone Application template. Name your project PoiFinderSample.image

  4. Click OK. A new project will be created and MainPage.xaml will be opened in the Visual Studio designer window.

    image

5. Add a reference to Wintelect.Sterling.WindowsPhone.dll by navigating to \SterlingDB\src\SterlingSln\Wintellect.Sterling.WindowsPhone\Bin\Debug folder.

6. Add a reference to Microsoft.Phone.Controls.Maps.dll, System.Xml.Linq and System.Device.dll which should be available as .NET assemblies.

7. Replace the XAML code with the following:

<phone:PhoneApplicationPage 
    x:Class="PoiFinderSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True" 
    xmlns:my="clr-namespace:Microsoft.Phone.Controls.Maps;
assembly=Microsoft.Phone.Controls.Maps"> <!--LayoutRoot is the root grid where all page content is placed--> <Grid x:Name="LayoutRoot" Background="Transparent"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28"> <TextBlock x:Name="ApplicationTitle" Text="Bing Map Demo" Style="{StaticResource PhoneTextNormalStyle}" /> <TextBlock x:Name="PageTitle" Text="POI Finder" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}" /> </StackPanel> <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions> <RowDefinition Height="150"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Grid x:Name="ctls" Grid.Row="0"> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <TextBox Name="txbPoi" Width="400" HorizontalAlignment="Left" VerticalAlignment="Center"></TextBox> <Button Name="Test" Content="Find" Width="100" Height="90" Grid.Column="1" /> </Grid> <my:Map Name="map1" Width="Auto" Height="Auto" Grid.Row="1"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
CredentialsProvider="<put your bing maps key here>" /> </Grid> </Grid> </phone:PhoneApplicationPage>

8. Compile and run the application. You should be presented with the following:

image

It doesn’t do much currently but the idea is that you will enter a POI name in the textbox, and then press the [Find] button to pan and zoom to that POI.

9. The next thing we will need to do is get some sample POIs, and in this example I used an XML which contains the POI definition. I took the data from an SQLServer 2008 table by running the following statement. Feel free to skip this step if you don’t have anything similar  the XML output from the query  is defined in the code behind for this sample:

SELECT TOP 10 [PoiID]

     ,[Name]

      ,[Lat]

      ,[Lon]

  FROM [dbo].[Pois]

  WHERE Lat is not null

  and Lon is not null

  FOR XML PATH(‘POIS’), TYPE,  ROOT(‘ALLPOIS’),ELEMENTS

 
10. We then need to create our POI class. To do this create a new class, called POI.cs and copy/paste this code:
 

using System;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Ink;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Animation;

using System.Windows.Shapes;

namespace PoiFinderSample

{

    public class Poi

    {

        public int PoiID { get; set; }   
        public string PoiName { get; set; }

        public double PoiLat { get; set; }

        public double PoiLon { get; set; }

    }

}

11. Next, we need to create the required by Sterling database class. Call this class PoiFinderDB:

using System;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Ink;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Animation;

using System.Windows.Shapes;

using Wintellect.Sterling.Database;

namespace PoiFinderSample

{

    public class PoiFinderDB : BaseDatabaseInstance

    {

        public const string POINAME_INDEX = "POINAME";

        public override string Name

        {

            get { return "PoiFinderDatabase"; }

        }

        protected override System.Collections.Generic.List<ITableDefinition> _RegisterTables()

        {

            return new System.Collections.Generic.List<ITableDefinition>

            {

               CreateTableDefinition<Poi, int>(p => p.PoiID).WithIndex<Poi, string, int>(POINAME_INDEX, p => p.PoiName)

            };

        }

    }

}

Note here that I created the database defining my POI class as a table, where the key is the PoiID column and has an index on the PoiName column. For more information on Sterling’s keys, indexes and tables refer to the comprehensive Sterling Users Guide
 
11. We now turn to the code behind to bring this all together.
 

using System;

using System.Collections.Generic;

using System.Linq;

using System.Net;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Animation;

using System.Windows.Shapes;

using Microsoft.Phone.Controls;

using Wintellect.Sterling;

using Microsoft.Phone.Controls.Maps;

using System.Xml.Linq;

using System.IO;

using System.Device.Location;

namespace PoiFinderSample

{

    public partial class MainPage : PhoneApplicationPage

    {

        private SterlingEngine _engine;

        private ISterlingDatabaseInstance _databaseInstance;

        private List<Poi> _allPois = new List<Poi>();

        public const string poixml = @"<?xml version=’1.0′ encoding=’UTF-8′ ?>

                                    <ALLPOIS>

                                      <POIS>

                                        <PoiID>8</PoiID>

                                        <Name>Azzuro Bar</Name>

                                        <Lat>40.6135782878</Lat>

                                        <Lon>22.9570902507</Lon>

                                      </POIS>

                                      <POIS>

                                        <PoiID>19</PoiID>

                                        <Name>Goodys</Name>

                                        <Lat>40.6288333338</Lat>

                                        <Lon>22.9539250000</Lon>

                                      </POIS>

                                      <POIS>

                                        <PoiID>20</PoiID>

                                        <Name>The Green Bottle</Name>

                                        <Lat>40.5822834885</Lat>

                                        <Lon>22.9569421118</Lon>

                                      </POIS>

                                      <POIS>

                                        <PoiID>21</PoiID>

                                        <Name>Everest</Name>

                                        <Lat>40.5842518311</Lat>

                                        <Lon>22.9514642883</Lon>

                                      </POIS>

                                      <POIS>

                                        <PoiID>22</PoiID>

                                        <Name>St. Paul</Name>

                                        <Lat>40.5860727577</Lat>

                                        <Lon>22.9555716119</Lon>

                                      </POIS>

                                      <POIS>

                                        <PoiID>23</PoiID>

                                        <Name>ABC</Name>

                                        <Lat>40.5785368598</Lat>

                                        <Lon>22.9520733962</Lon>

                                      </POIS>

                                      <POIS>

                                        <PoiID>24</PoiID>

                                        <Name>Zoop</Name>

                                        <Lat>40.5775640617</Lat>

                                        <Lon>22.9520885767</Lon>

                                      </POIS>

                                      <POIS>

                                        <PoiID>25</PoiID>

                                        <Name>Alcohol</Name>

                                        <Lat>40.5784540771</Lat>

                                        <Lon>22.9508931755</Lon>

                                      </POIS>

                                      <POIS>

                                        <PoiID>26</PoiID>

                                        <Name>The Shop</Name>

                                        <Lat>40.6298566672</Lat>

                                        <Lon>22.9505833333</Lon>

                                      </POIS>

                                      <POIS>

                                        <PoiID>27</PoiID>

                                        <Name>Disc Shop</Name>

                                        <Lat>40.5819934899</Lat>

                                        <Lon>22.9478130433</Lon>

                                      </POIS>

                                    </ALLPOIS>";

        // Constructor

        public MainPage()

        {

            InitializeComponent();

            Loaded += new RoutedEventHandler(MainPage_Loaded);

        }

        private void MainPage_Loaded(object sender, RoutedEventArgs e)

        {

            Test.Click += new System.Windows.RoutedEventHandler(Query_Click);

            StartSterling();

            DisplayPoisFromXml(poixml);

        }

        private void StartSterling()

        {

            _engine = new SterlingEngine();

            _engine.Activate();

            _databaseInstance = _engine.SterlingDatabase.RegisterDatabase<PoiFinderDB>();

        }


        private void DisplayPoisFromXml(string xmlPois)

        {

            var xmlrdr = new StringReader(xmlPois);

            var doc = XDocument.Load(xmlrdr);

            // Populate the POIS list

            _allPois = (from pointrec

                           in doc.Element("ALLPOIS").Elements("POIS")

                        select new Poi

                        {

                            PoiID = Convert.ToInt32(pointrec.Element("PoiID").Value),

                            PoiName = pointrec.Element("Name").Value,

                            PoiLat = Convert.ToDouble(pointrec.Element("Lat").Value),

                            PoiLon = Convert.ToDouble(pointrec.Element("Lon").Value)

                        }).ToList();

            //Loop through the list to save the Pois into Sterling

            // and display them in the map

            for (var idx = 0; idx < _allPois.Count – 1; ++idx)

            {

                Poi thisPoi = _allPois[idx];

                var key = _databaseInstance.Save(thisPoi);

                // Create a pushpin

                Pushpin pin1 = new Pushpin();

                //Create POI location

                GeoCoordinate thisLoc = new GeoCoordinate(thisPoi.PoiLat, thisPoi.PoiLon);

                pin1.Location = thisLoc;

                pin1.Content = thisPoi.PoiName;

                //Add pin to the map

                map1.Children.Add(pin1);

            }

            //Flush to storage

            _databaseInstance.Flush();

            //Zoom the map

            GeoCoordinate mapCenter = new GeoCoordinate(40.6293172201, 22.9462402344);

            map1.SetView(mapCenter, 12);

        }

        private void Query_Click(object sender, RoutedEventArgs e)

        {

            if (txbPoi.Text != string.Empty)

            {

                var q1 = _databaseInstance.Query<Poi, string, int>(PoiFinderDB.POINAME_INDEX).Where(p => p.Index == txbPoi.Text).FirstOrDefault();

                if (q1 != null)

                {

                    //Get POI location and zoom the map

                    GeoCoordinate poiloc = new GeoCoordinate(q1.LazyValue.Value.PoiLat, q1.LazyValue.Value.PoiLon);

                    map1.SetView(poiloc, 18);

                }

            }

        }

    }

}

12. You should be all done. Compile and run the project. The map should zoom somewhere around downtown Thessaloniki, Greece displaying all 10 POIs. Now enter the name of any Poi in the text box and press [Find]. The map should zoom to that point (e.g. try ‘Everest’) as shown below. A final note to remember:  queries are case-sensitive so for example ‘everest’ will not return any results.

image

 

You can download the full source code from the Box.Net  widget on the left (PoiFinderSample.rar)

Displaying pushpins in Bing Maps Silverlight control using GeoRSS feeds

In this post I will briefly explain how to add pushpins in the Bing Maps control using a GeoRSS feed. Although there are many ways of adding pushpins (or any geographic shape for that matter) on a Bing Maps backdrop, I have found this by far the easiest and most flexible.

So assuming you have a spatial table (in this example the table is stored in a SQL Server 2008 database, but any database could be used) containing the points you want to plot. A sample of the table is shown below:

I wont go into any details on how to create a Silverlight project and implement Bing Maps, there are plenty of good examples around. You can start here or here.  Instead, we will first focus on how to create the GeoRSS handler to retrieve the shape data from the table. To do this, right-click on your Web project and select Add->New Item->Generic Handler and name it GeoRSSHandler.ashx.

Replace the default contents in the GeoRssHandler.ashx file with the following:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Configuration;
using System.Data.SqlClient;
namespace GeoRSSAndBing.Web
{
///
/// Summary description for GeoRSSHandler
///
public class GeoRSSHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string connStr = ConfigurationManager.ConnectionStrings["spatialDB"].ToString();
context.Response.ContentType = "text/xml";
string type = context.Request["type"];
//Build the GeoRSS feed
System.Text.StringBuilder rssOutput = new System.Text.StringBuilder("");
rssOutput.AppendLine("");
rssOutput.AppendLine("");
rssOutput.AppendLine("");
rssOutput.AppendLine("POIS");
rssOutput.AppendLine("");
rssOutput.AppendLine("" + System.DateTime.Now + "");
SqlConnection conn = new SqlConnection(connStr);
conn.Open();
try
{
SqlCommand cmd = new SqlCommand();
cmd.Connection = conn;
cmd.CommandType = System.Data.CommandType.Text;
if (type == "POI")
{
//Change this statement depending on your settings
cmd.CommandText = "select Eva_Event_ID,EVA_EVENT_TYPE, EVA_EVENT_DESCRIPTION, shape.AsGml() as GeomGml from pois_wgs84";
}
//TODO: Add logic to handle lines and polygons
SqlDataReader sqlGeomRdr= cmd.ExecuteReader();
while (sqlGeomRdr.Read())
{
//Create an element for this row
rssOutput.AppendLine("");
//Set title and description
if (type == "POI")
{
rssOutput.AppendLine("" + sqlGeomRdr["EVA_EVENT_ID"].ToString() + "");
rssOutput.AppendLine("");
rssOutput.AppendLine("");
}
//TODO: Add logic to handle lines and polygons
rssOutput.AppendLine("");
//Get the geography instance GML from column 2
string gml= sqlGeomRdr["GeomGml"].ToString();
//Append the gml: prefix to all the elements due to VE parsing behavior
if (type == "POI")
{
gml = gml.Replace("<Point", "<gml:Point");
gml = gml.Replace("pos", "gml:pos");
gml = gml.Replace("</Point", "</gml:Point");
}
//TODO: Add logic to handle lines and polygons
Add the elements to the output XML
rssOutput.AppendLine(gml);
//Close and elements
rssOutput.AppendLine("");
rssOutput.AppendLine("");
}
rssOutput.Append("");
context.Response.Write(rssOutput.ToString());
}
catch (Exception ex)
{
throw ex;
}
finally
{
conn.Close();
}
}

Next, add a [Show Locations] button in your XAML such as:

<Button Cursor=”Hand” Width=”195″ Height=”25″ HorizontalAlignment=”Left” Content=”Show Locations” x:Name=”b1″ Margin=”2,10,0,1″ Click=”b1_Click”></Button> 
 
 The implementation of the Onclick event in the code behind will call the handler we just created asynchronously i.e.:
 
 private void b1_Click(object sender, RoutedEventArgs e)
 {
   Uri url = new Uri(“../GeoRSSHandler.ashx?type=POI”, UriKind.Relative);  WebClient client = new WebClient();  
    client.DownloadStringCompleted +=new DownloadStringCompletedEventHandler(client_DownloadStringCompleted); 
   client.DownloadStringAsync(url);
 } 

Finally, the downloadString Event handler code is the bit that draws the pushpins on the map:

 void client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)

{ if (e.Error == null)

{ StringReader stream = new StringReader(e.Result); 

 XmlReader reader = XmlReader.Create(stream); 

 string gmlURI = http://www.opengis.net/gml&#8221;;map.Children.Clear();

 while (reader.Read()){

   if (reader.NodeType == XmlNodeType.Element)  {

    if (reader.NamespaceURI == gmlURI && reader.Name == reader.Prefix + “:pos”)

   {

      string[] loc = reader.ReadInnerXml().Split(” “.ToCharArray()); 

      double lat = Double.Parse(loc[1]); 

      double lon = double.Parse(loc[0]); 

      Pushpin p = new Pushpin();p.Location =new Location(lat, lon);map.Children.Add(p);
  }}}}} Next, compile and run your application. Assuming you set the initial Bing Map extent correctly, (i.e. around the area where your points are), clicking on the [Show Locations] button should display your points on Bing Maps using the Default pushpin.

The great thing with this approach, is that you can easily add logic to either add other shape types such as polygons or lines, or retrieve data from other databases by simply amending the code in the GeoRSS handler.