Posts tagged ‘REST’

July 14, 2011

Searching for POIs with the Bing Phonebook search

The Bing Maps Locations REST API is for geocoding – i.e. finding addresses or places on a map. However, I’ve seen a couple of questions over the last week from people trying to search not for known addresses, but for things – pubs, restaurants, schools, hospitals, and the like. You used to be able to do this kind of search in v6.x of the Bing Maps control using the Find() method, which would search for and then add any matching entities to the map, but this doesn’t exist in the slimmed down v7 control.

Instead, one alternative is to use the Bing API (not the Bing Maps API) to perform a search for the results, and then plot the results on the map. The Bing API exposes a number of REST search services, but the Phonebook search is the one you’re most likely to want to use. To perform a Bing search, you’ll first need a Bing AppId (again, different from your Bing Maps Application Key), which you can sign up for here:
http://www.bing.com/developers

You can find a basic sample of the Bing Phonebook search at
http://msdn.microsoft.com/en-us/library/dd251030.aspx
, which simply outputs the search results into a list. To plot the results on Bing Maps instead, the process is to make a REST request to the Bing phonebook search service and, in the callback function specified, loop through the results and create pushpins at the relevant Latitude and Longitude coordinates and push them on to the map.

For example, in the code below, I search for any pubs that lie within the bounding box of the current map view (refreshes every time the map view changes by listening to the viewchangeend event), and create a pushpin at each location that, when clicked,  displays an Infobox containing the address and phonenumber of the entity.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
  <head>
    <title>Bing Phonebook Search</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
    <script type="text/javascript">
      var map = null;

      function GetMap() {
        // Create a default map
        map = new Microsoft.Maps.Map(
         document.getElementById("mapDiv"),
         {
           credentials: "ENTERYOURBINGMAPSKEY",
           center: new Microsoft.Maps.Location(52.63, 1.3),
           zoom: 17,
           enableSearchLogo: false
         }
        );

         var viewChangeEndHandler = Microsoft.Maps.Events.addHandler(map, 'viewchangeend', ViewChangeEndHandler);   
      }

      function ViewChangeEndHandler(e) {

        var mapCenter = map.getCenter();

        var request = "http://api.bing.net/json.aspx?"
        // Common request fields (required)
            + "AppId=" + "ENTERYOURBINGAPPID"
            + "&Query=Pubs"
            + "&Sources=Phonebook"
        // Common request fields (optional)
            + "&Version=2.0"
            + "&Market=en-us"
            + "&UILanguage=en"
            + "&Latitude=" + mapCenter.latitude
            + "&Longitude=" + mapCenter.longitude
            + "&Radius=100.0"
            + "&Options=EnableHighlighting"
        // Phonebook-specific request fields (optional)
            + "&Phonebook.Count=25"
            + "&Phonebook.Offset=0"
            + "&Phonebook.FileType=YP"
            + "&Phonebook.SortBy=Distance"
        // JSON-specific request fields (optional)
            + "&JsonType=callback"
            + "&JsonCallback=BingPhonebookCallback";

        // Create a new script element
        var script = document.createElement("script");
        script.setAttribute("type", "text/javascript");
        script.setAttribute("src", request);

        // Append the script to the document head
        var dochead = document.getElementsByTagName("head").item(0);
        dochead.appendChild(script);
     }

     function BingPhonebookCallback(response) {
       // If we got a valid, non-null result set
       if (response && response.SearchResponse && response.SearchResponse.Phonebook && response.SearchResponse.Phonebook.Results && response.SearchResponse.Phonebook.Results.length > 0) {

         map.entities.clear();
         
         var results = response.SearchResponse.Phonebook.Results;

         for (var i = 0; i < results.length; i++) {
           var location = new Microsoft.Maps.Location(
             parseFloat(results[i].Latitude),
             parseFloat(results[i].Longitude)
           );

           var pushpin = new Microsoft.Maps.Pushpin(location);
           pushpin.Title = results[i].Title;
           pushpin.Address = results[i].Address;
           pushpin.PhoneNumber = results[i].PhoneNumber;
           pushpin.DisplayUrl = results[i].DisplayUrl;

           Microsoft.Maps.Events.addHandler(pushpin, 'click', function(e) {
           var infoboxOptions = {
             width: 200,
             height: 100,
             showCloseButton: true,
             zIndex: 0,
             offset: new Microsoft.Maps.Point(0, 0),
             showPointer: true,
             title: e.target.Title,
             description: e.target.Address + " " + e.target.PhoneNumber
           };
           var defaultInfobox = new Microsoft.Maps.Infobox(e.target.getLocation(), infoboxOptions);
           map.entities.push(defaultInfobox); 
           });
           
           map.entities.push(pushpin);
         }
       }
     }
    </script>
  </head>
  <body onload="GetMap();">
  <div id='mapDiv' style="position: absolute; top: 0; left: 0; width:800px; height:600px;">
  </div>
  </body>
</html>

And here's the results, with an Infobox callout showing location details of the best pub quiz in Norwich (held every tuesday night – our regular team name is “Quizteama Aguilera”, BTW):

image

Tags: ,
June 15, 2011

Creating a Bing Maps v7 control Centred on an Address Rather than a known Latitude/Longitude

This was a question that came up at the cloudhack event last weekend – when you create a new instance of the Bing Maps AJAX control, you specify the centrepoint of the map in latitude and longitude coordinates, using the center property in the viewOptions passed to the constructor. For example:

var map = new Microsoft.Maps.Map( document.getElementById("mapDiv"),
                           { credentials: ENTERYOURBINGMAPSKEY,
                             center: new Microsoft.Maps.Location(54, -4),
                             mapTypeId: Microsoft.Maps.MapTypeId.aerial,
                             zoom: 12
                           });

(You can view more information on the viewOptions parameters here: 
http://msdn.microsoft.com/en-us/library/gg427628.aspx
)

However, what if you want to create a map centred on an address instead of a latitude/longitude coordinate? This seems like a fairly simple, common request, so I was a bit surprised to find that none of the Microsoft method reference guides nor any of the “interactive SDK” sites provide an example showing how to do this.

Fortunately, it’s not too tricky to do so, for the benefit of everyone other than those teams at the cloudhack event who asked me, I thought I’d write it up here ;)

First, you need to geocode the address into latitude and longitude coordinates (using the REST Locations API is the easiest way to do so direct from javascript). Then, in the jsonp callback function that is executed after the geocode service returns its results, create and centre the map on the returned coordinates. Here’s an example (note that you will need to replace ENTERYOURBINGMAPSKEY here with some valid credentials, or else the call to the REST service will fail and the map will not be created):

<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title></title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
  <script type="text/javascript">
    var map = null;
    var credentials = "ENTERYOURBINGMAPSKEY";
    
    function GetMap() {
   
      // Define the address on which to centre the map
      var addressLine = "54 Chiswell Street"; // Street Address
      var locality = "London"; // City or town name
      var adminDistrict = ""; // County
      var country = "UK"; // Country, obviously
      var postalCode = "" //Postcode
       
      // Construct a request to the REST geocode service
      var geocodeRequest = "http://dev.virtualearth.net/REST/v1/Locations" 
                           + "?countryRegion=" + country
                           + "&addressLine=" + addressLine
                           + "&locality=" + locality
                           + "&adminDistrict=" + adminDistrict
                           + "&postalCode=" + postalCode
                           + "&key=" + credentials
                           + "&jsonp=GeocodeCallback"; // This function will be called after the geocode service returns its results
       
      // Call the service
      CallRestService(geocodeRequest);
    }

    function GeocodeCallback(result) {
      // Check that we have a valid response
      if (result && result.resourceSets && result.resourceSets.length > 0 && result.resourceSets[0].resources && result.resourceSets[0].resources.length > 0) {
       
        // Create a Location based on the geocoded coordinates
        var coords = result.resourceSets[0].resources[0].point.coordinates;
        centerPoint = new Microsoft.Maps.Location(coords[0], coords[1]);

        // Create a map centred on the location
        map = new Microsoft.Maps.Map(document.getElementById("mapDiv"),
                           { credentials: credentials,
                             center: centerPoint,
                             mapTypeId: Microsoft.Maps.MapTypeId.aerial,
                             zoom: 18
                           });
         
        // Add a pushpin as well
        var pushpin = new Microsoft.Maps.Pushpin(map.getCenter());
        map.entities.push(pushpin);
      }
    }

    // This is a generic function to call a REST service and insert the JSON
    // results into the head of the document
    function CallRestService(request) {
      var script = document.createElement("script");
      script.setAttribute("type", "text/javascript");
      script.setAttribute("src", request);
      var dochead = document.getElementsByTagName("head").item(0);
      dochead.appendChild(script);
    }      
  </script>
</head>
<body onload="GetMap();">
  <div id='mapDiv' style="position: relative; width: 400px; height: 600px;">
  </div>
</body>
</html>

Here’s the resulting map:

image

Tags: , ,
January 27, 2011

The bing.com/maps site, the Bing Maps key, and the Elevation REST Service


http://www.bing.com/maps
is Microsoft’s consumer-facing map site. The functionality that it exposes (geocoding, routing, map display etc.) is based on, although not identical to, the features available through the Bing Maps AJAX Control and Silverlight control.

A recent question on the MSDN forum asked whether it would be possible to implement a particular feature from the bing.com/maps site (draggable routes) that wasn’t included “out-of-the-box” in the Bing Maps API. Out of curiosity, this prompted me to have a look at the source code for bing.com/maps, to see how it was achieved there.

Since the AJAX map control used on www.bing.com/maps is just javascript, it’s possible to examine all the source used to create the map by just viewing the page source in a browser (although if has been somewhat obfuscated and minified). But before even getting round to the section of code responsible for draggable routes, I uncovered two interesting, unrelated things:

I’ll Show You My Key if you Show Me Yours

Firstly, the key used to access the Bing Maps services from http://www.bing.com/maps is in plain view in the source code of the page. Bing Maps keys are tied to a particular URL, so there shouldn’t be an issue with letting your key be known publicly. (In fact, since the AJAX map control is executed client-side, there is no way of using the key authentication method without somehow letting your client know the key). However, there had been some discussion on the Bing Maps Developer forum about whether it was wise to try to obfuscate your key from casual snoopers. If we assume that Microsoft lead by example in terms of setting best practice on the bing.com site, since they haven’t hidden their key is it safe to assume that we as developers shouldn’t worry about key obfuscation either?

The Elevation Service

The second interesting thing was a definition of an elevationServiceUrl, as follows:

"elevationServiceUrl","{urischeme}dev.virtualearth.net/REST/v1/Elevation/BoundingRect/{south},{west},{north},{east}/{rows}/{cols}?jsonp=microsoftMapsNetworkCallback&amp;jsonso={jsono}&amp;key={credentials}"

This seems to be an, as yet undocumented, elevation service that can be called via REST – and the URL schema and parameters follow a similar pattern as the existing Geocode, Locations, and Routing REST services.

Experimenting with this elevation service to find out what it did, I tried calling it as follows:


http://dev.virtualearth.net/REST/v1/Elevation/BoundingRect/52,1,53,2/4/4?jsonp=microsoftMapsNetworkCallback&key=AhGSgD1Twhjx9WqxjJZznCBCSzddrrBzkD7k6MjIaLGnp3b3hupQUVbNdv6Wb0qW

The result was a series of decimal “alt” values, rows * cols in length, together with a single “lod” value of 8, in JSON format:

microsoftMapsNetworkCallback({“alt”:[45, -1, 0, 0, 38, -6, -24, -8, 46, 2, -36, -12, 50, -27, -23, 0 ],”lod”:8})

Following the syntax of the other REST services, the result is passed to a callback function specified in the (optional) jsonp parameter – in this case a function called microsoftMapsNetworkCallback. Given that this URL is described as an elevation service, it seems fair to assume that the “alt” values are altitude. “lod” might stand for “level of detail”, but I’m not sure how it relates to the other values. What concerned me more, however, was the alt values themselves. What are they measured relative to, and in what units are they expressed?

The BoundingRect I supplied contains the northeast corner of East Anglia, extending out into the North Sea. I was therefore surprised to see so many negative values. Even assuming that altitude is measured relative to the ellipsoid rather than to sea-level, the altitude coordinates don’t seem to line up with the terrain I know….

To make the comparison simpler, I tried to request the elevation for a single point, by setting both the rows and columns parameters to 1, but this resulted in a “Bad Request” error – it seems that the service is only designed to produce elevation matrix of an area, and you’ll get an error from the service unless both these values are greater than 1. So, the next best thing was setting them both to 2:


http://dev.virtualearth.net/REST/v1/Elevation/BoundingRect/52,1,53,2/2/2?key=AhGSgD1Twhjx9WqxjJZznCBCSzddrrBzkD7k6MjIaLGnp3b3hupQUVbNdv6Wb0qW

which results in:

{“alt”:[45,-1,50,-50],”lod”:11}

A different “lod” value, and four elevation values, as expected. Next is to take a guess at how the rows and columns are assigned across the assigned BoundingRect range. If we were to include the range boundaries themselves, we might expect the previous results to correspond to the readings at each corner of the BoundingRect. Testing these results from the Google elevation service instead:


http://maps.googleapis.com/maps/api/elevation/xml?locations=52,1|52,2|53,1|53,2&sensor=false

Gives the following four elevations:

47.3150635, -27.7267170, -5.8248205, -27.2653141

These values look to be interpolated, whereas the Bing values are probably not, but they still don’t seem to line up particularly well.

Perhaps, instead, the Bing results represent the centrepoint of each cell having defined the BoundingRect into cols * rows. In other words, the four results correspond to the locations at:


http://maps.googleapis.com/maps/api/elevation/xml?locations=52.25,1.25|52.25,1.75|52.75,1.25|52.75,1.75&sensor=false

Which, according to Google, are located at elevations of:

60.4444427, -18.9525394, 25.1086426, -24.8328476

These results at least share the same sign as those from Bing and, accounting for the interpolation, could even be using the same dataset. It’s a bit hard to be certain though.

Under the Sea?

The last mystery remained about explaining the negative values – how could a location in the sea apparently have an altitude below sea level…. unless, the elevation service also includes bathymetry data, and is therefore reporting negative values for depth below sea level of locations on the ocean floor. Yes! Or so I thought, until I tried testing the elevation of an area somewhere near the Mariana Trench:


http://dev.virtualearth.net/REST/v1/Elevation/BoundingRect/11,142,12,143/2/2?key=AhGSgD1Twhjx9WqxjJZznCBCSzddrrBzkD7k6MjIaLGnp3b3hupQUVbNdv6Wb0qW

Fully expecting an answer of something like –10,000, instead I got the following:

{“alt”:[46,2,-11,3],”lod”:8}

Oh well. I guess that’s what you get for playing around with undocumented APIs….

January 11, 2011

Draggable Routes in Bing Maps

There have been several recent posts asking how to implement draggable routes in Bing Maps. Following on from my New Year’s resolution, I thought I’d write about how to do this.

To start with, it’s easiest to consider a route between a set number of existing draggable waypoints, as in the following code listing:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "<a href="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;">http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"</a>>
<html>
<head>
 <title>Draggable Route</title>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
 <script type="text/javascript" src="<a href="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0&quot;">http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"</a>></script>
 <script type="text/javascript">
 var map = null;
 var waypointLayer = new Microsoft.Maps.EntityCollection();
 var routeLayer = new Microsoft.Maps.EntityCollection();

function GetMap() {
 // Initialize the map
 map = new Microsoft.Maps.Map(
 document.getElementById("mapDiv"),
 {
 credentials: "BINGMAPSKEY",
 mapTypeId: "r",
 center: new Microsoft.Maps.Location(52.75, -1),
 zoom: 7
 }
 );

// Define initial waypoints for the route
 var waypoints = [
 new Microsoft.Maps.Pushpin(new Microsoft.Maps.Location(52.6, 1.26)),
 new Microsoft.Maps.Pushpin(new Microsoft.Maps.Location(51.75, -1.26)),
 new Microsoft.Maps.Pushpin(new Microsoft.Maps.Location(53, -2)),
 new Microsoft.Maps.Pushpin(new Microsoft.Maps.Location(54, -1))
 ];

// Make the waypoints draggable, attach the drag handler, and add to the layer
 for (var i = 0; i < waypoints.length; i++) {
 waypoints[i].setOptions({ draggable: true, text: i.toString() });
 Microsoft.Maps.Events.addHandler(waypoints[i], 'dragend', UpdateRoute);
 waypointLayer.push(waypoints[i]);
 }

// Add the layers to the map
 map.entities.push(routeLayer);
 map.entities.push(waypointLayer);

// Calculate the initial route on the map
 UpdateRoute();
 }

function UpdateRoute(credentials) {
 map.getCredentials(MakeRouteRequest);
 }

function MakeRouteRequest(credentials) {

// Build up the string containing waypoint arguments
 var wpargs = "";
 for (var i = 0; i < waypointLayer.getLength(); i++) {
 var wp = waypointLayer.get(i).getLocation();
 wpargs = wpargs + "wp." + i + "=" + wp.latitude + "," + wp.longitude + "&";
 }
 // Append to the URL for the REST roue service
 var routeRequest = "<a href="http://dev.virtualearth.net/REST/v1/Routes?&quot;">http://dev.virtualearth.net/REST/v1/Routes?"</a> + wpargs + "routePathOutput=Points&output=json&jsonp=RouteCallback&key=" + credentials;

CallRestService(routeRequest);
 }

function RouteCallback(result) {
 // Check that we have a valid response
 if (result && result.resourceSets && result.resourceSets.length > 0 && result.resourceSets[0].resources && result.resourceSets[0].resources.length > 0)
 {
 // Construct a polyline of the route
 var routeline = result.resourceSets[0].resources[0].routePath.line;
 var routepoints = new Array();
 for (var i = 0; i < routeline.coordinates.length; i++) {
 routepoints[i] = new Microsoft.Maps.Location(routeline.coordinates[i][0], routeline.coordinates[i][1]);
 }
 var routeshape = new Microsoft.Maps.Polyline(routepoints, { strokeColor: new Microsoft.Maps.Color(200, 255, 140, 0) });
 // Add the route polyline to the map
 routeLayer.clear();
 routeLayer.push(routeshape);
 }
 }

function CallRestService(request) {
 var script = document.createElement("script");
 script.setAttribute("type", "text/javascript");
 script.setAttribute("src", request);
 document.body.appendChild(script);
 }
 </script>
</head>
<body onload="GetMap();">
 <div id="mapDiv" style="position: relative; width: 800px; height: 600px;"></div>
</body>
</html>

WordPress won’t let me embed an interactive map into a post, but you can see a static image of the map created below. Each waypoint is draggable, and the route is automatically recalculated whenever any point is moved.

image

Now that you’ve seen how to make a route between existing waypoints draggable, it’s relatively simple to extend this example to add additional waypoints to the route. The steps required are as follows:

  1. Firstly, you need to choose what action will be used to add the additional waypoint – for example: a click on the map, or a drag on the existing polyline route, etc., and define an appropriate handler to listen for this event using the Microsoft.Maps.Events.addHandler() method.
  2. When the event fires, create a Location object representing the coordinates of the new waypoint. To do so, convert the pixel reference passed by the event handler to a Location, using the Microsoft.Maps.Map.tryPixelToLocation() method.
  3. Now, you need to insert the new Location object at the appropriate position in the array of waypoints. Note that the “appropriate” position is subjective and depends on your application – you might, for example, want to allow users to sequentially define the order in which waypoints are visited (in which case, just push each new waypoint onto the end of the array, in the order they are defined), or you might allow users to choose only the location of the waypoints and use application logic to optimise the order in which they are visited (by, e.g. using the Haversine formula to determine the two closest existing waypoints in the route and insert the new Location between them). Perhaps you want different behaviours depending on whether the user clicked (and released), or if they clicked and dragged on the map. For example, consider the following existing route, and suppose the user clicked, dragged and released the mouse as shown:

image

The new point could be inserted into the array of waypoints between waypoints 1 and 2, since that was the position of the point on the route that was originally clicked. Or, the new waypoint could be inserted between waypoints 0 and 1, since that is the location that would optimise the generated route. Or the new waypoint could be inserted at the end of the array, after waypoint 3, since it is the last waypoint to be added to the route. It’s up to you to decide what rules to use when adding waypoints to an existing route, and where to place the new Location in the existing array of waypoints.

4.  Once you’ve added the new Location(s) into the appropriate position of the waypoints array, simple call the UpdateRoute() method to request the new route and plot it on the map.

Follow

Get every new post delivered to your Inbox.

Join 53 other followers