Accessing the Bing Maps REST services from various JavaScript frameworks

On the Bing Maps forums I often come across developers who have difficulty accessing the Bing Maps REST services using different JavaScript frameworks such as jQuery and AngularJS. One common point of confusion is that passing in a REST service request URL into the address bar of a browser works, but passing that same URL into an AJAX method in JavaScript doesn’t. AJAX allows web pages to be updated asynchronously by exchanging small amounts of data with the server behind the scenes. This means that it is possible to update parts of a web page, without reloading the whole page. In this blog post we are going to take a quick look at how to access the Bing Maps services using different JavaScript frameworks.

Using pure JavaScript

When using JavaScript on its own one of the most common methods used to access data using AJAX is to use the XMLHttpRequest object. Unfortunately, until the introduction of Cross-Origin Resource Sharing (CORS) the XMLHttpRequest was limited to accessing resources and services that were hosted on the same domain as the webpage requesting them. CORS is now supported in most modern browsers and became a W3C standard in Janaury 2014 but it also needs to be enabled on the server of the service. Bing Maps REST services have been were released many years ago and currently do not support the use of CORS. Note that it is also worth knowing that there are many older browses that are still in use today that do not support CORS. When the Bing Maps REST services were released most web browsers did not support CORS and a different technique was commonly used to make cross domain calls to services, JSONP. JSONP is a technique which takes advantage of the fact that web browsers do not enforce the same-origin policy on script tags. To use JSONP with the Bing Maps REST services you add a parameter to the request URL like this &jsonp=myCallback. You then append a script tag to the body of the webpage using this URL. This will cause the browser to load call the service and load it like a JavaScript file, except that the JSON response will be wrapped with some code to a function called myCallback. Here is a reusable function you can use to add a script tag to the body of a webpage.

 function CallRestService(request) {
    var script = document.createElement("script");
    script.setAttribute("type", "text/javascript");
    script.setAttribute("src", request);
    document.body.appendChild(script);
}

Now, let’s say you want to be able to call the REST services to geocode a location and call a function called GeocodeCallback. This can be done using the following code:

 var geocodeRequest = "https://dev.virtualearth.net/REST/v1/Locations?query=" + encodeURIComponent("[Location to geocode]") + "&jsonp=GeocodeCallback&key=YOUR_BING_MAPS_KEY";

CallRestService(geocodeRequest);

function GeocodeCallback(result) 
{   
    // Do something with the result
}

Notice that we wrapped the location we wanted to geocode with a function called encodeURIComponent. This is a best practice as web browsers can have difficulty with special characters, especially if you use the ampersand symbol in your location. This significantly increases the success rate of your query. Additional tips on using the REST services can be found here.

A full code sample of using the REST geocoding service in JavaScript using JSONP is documented here.

Using jQuery

JQuery (https://jquery.com) is a very popular JavaScript framework that makes it easier to developer JavaScript that works across different browsers. jQuery provides a three of different functions to make HTTP GET requests to services; jQuery.ajax ( $.ajax), jQuery.get ( $.get) and jQuery.getJSON ( $.getJSON). The jQuery.get and jQuery.getJSON function is meant to be a simplified version of the jQuery.ajax function but have less functionality. The jQuery.get and jQuery.getJSON functions do not support cross-domain requests or JSONP whereas the jQuery.ajax function does. In order to make a cross-domain request using the jQuery.ajax function you have to specify that it uses JSONP and set the dataType property to JSONP. For example, here is a reusable function that uses the jQuery.ajax function to download the results from the specified REST URL request and send them to a callback function.

 function CallRestService(request, callback) {
    $.ajax({
        url: request,
        dataType: "jsonp",
        jsonp: "jsonp",
        success: function (r) {
            callback(r);
        },
        error: function (e) {
            alert(e.statusText);
        }
    });
}

Here is an example of how you can use this function to geocode a location and have the results sent to a callback function called GeocodeCallback.

 var geocodeRequest = "https://dev.virtualearth.net/REST/v1/Locations?query=" + encodeURIComponent("[Location to geocode]") + "&key=YOUR_BING_MAPS_KEY";

CallRestService(geocodeRequest, GeocodeCallback);

function GeocodeCallback(result) {
    // Do something with the result
}

Using AngularJS

AngularJS is an open source JavaScript framework that lets you build well structured, easily testable and maintainable front-end applications by using the Model-View-Controller (MVC) pattern. When making HTTP requests using AngularJS the $http.get function is often used. This function wraps the XMLHttpRequest object, and as mentioned earlier, this doesn’t work when making cross-domain requests unless CORS is enabled on the server. AngularJS has another function $http.jsonp which allows you to easily make JSONP requests. Using this instead of the $http.get function works well. For example, here is a reusable function that uses the $http.jsonp function to download the results from the specified REST URL request and send them to a callback function.

 function CallRestService(request, callback) {
    $http.jsonp(request)
        .success(function (r) {
            callback(r);
        })
    .error(function (data, status, error, thing) {
        alert(error);
    });
}

Here is an example of how you can use this function to geocode a location and have the results sent to a callback function called GeocodeCallback.

 var geocodeRequest = "https://dev.virtualearth.net/REST/v1/Locations?query=" + encodeURIComponent("[Location to geocode]") + "&jsonp=JSON_CALLBACK&key=YOUR_BING_MAPS_KEY";

CallRestService(geocodeRequest, GeocodeCallback);

function GeocodeCallback(result) {
    // Do something with the result
}

Notice how we have to specify the JSONP parameter of the REST request URL to JSON_CALLBACK.

What about HTTP POST requests?

So far we have mainly focused on making HTTP GET requests as majority of the features in the Bing Maps REST services are accessed in that way. GET requests are simple and fast but are limited by the length of the URL which is limited to 2083 characters. There is however a few features in the REST services where you can make HTTP POST requests so that you can pass in more data than what is supported by GET requests.

- The Elevation service allows you to POST coordinate data. This is useful as this service allows you to pass up to 1024 coordinates in a single request.

- The Imagery service allows you to POST data for up to 100 pushpins when generating a static image of a map.

JSONP does not support HTTP POST requests which limits us quite a bit. There is however a way around this which consists of creating a proxy service and hosting it on the same server as your webpage and then have it make the POST request to Bing Maps. If using Visual Studio, a Generic handler can be easily turned into a proxy service.

To do this, open Visual Studio and create a new ASP.NET Web Application called BingMapsPostRequests.

clip_image002

Screenshot: Creating ASP.NET Web Application Project

Next add a Generic Handler by right clicking on the project and selecting Add -> New Item and selecting the Generic Handler option from the web template section. Call it ProxyService.ashx.

clip_image004

Screenshot: Adding Generic Handler

Open the ProxyService.ashx.cs file and update it with the following code. This code will allow the proxy service to take in three parameters; a url to make the proxy request for, a type value which indicates if a HTTP GET or POST request should be made by the proxy service, and a responseType value which indicates the content type of the response data. If the response type is an image, the response will be a 64bit encode string which will make it easier to work within JavaScript.

 using System;
using System.IO;
using System.Net;
using System.Web;

namespace BingMapsPostRequests
{
    public class ProxyService : IHttpHandler
    {
        public void ProcessRequest(HttpContext context)
        {
            //Get request URL from service.
            string source = context.Request.QueryString["url"];

            //Get the type of request to make.
            string type = context.Request.QueryString["type"];

            //Get the response type of the response data.
            string responseType = context.Request.QueryString["responseType"];

            //Default the response type of JSON if it is not specified.
            if (string.IsNullOrEmpty(responseType))
            {
                responseType = "application/json";
            }

            //Do not cache response
            context.Response.Cache.SetCacheability(HttpCacheability.NoCache);

            context.Response.ContentType = responseType;
            context.Response.ContentEncoding = System.Text.Encoding.UTF8;

            //Make required cross-domain POST request.
            var request = (HttpWebRequest)HttpWebRequest.Create(source);

            if (string.Compare(type, "POST", true) == 0)
            {
                request.Method = "POST";

                request.ContentType = "text/plain;charset=utf-8";

                var reader = new StreamReader(context.Request.InputStream);
                var encoding = new System.Text.UTF8Encoding();
                byte[] bytes = encoding.GetBytes(reader.ReadToEnd());

                request.ContentLength = bytes.Length;

                using (var requestStream = request.GetRequestStream())
                {
                    // Send the data.
                    requestStream.Write(bytes, 0, bytes.Length);
                }
            }

            using (var response = (HttpWebResponse)request.GetResponse())
            {
                //Format the response as needed and return to proxy service response stream.
                if (responseType.StartsWith("image"))
                {
                    using (var ms = new MemoryStream())
                    {
                        response.GetResponseStream().CopyTo(ms);
                        byte[] imageBytes = ms.ToArray();

                        // Convert byte[] to Base64 String
                        string base64String = Convert.ToBase64String(imageBytes);
                        context.Response.Write("data:" + responseType + ";base64," + base64String);
                    }
                }
                else
                {
                    using (var stream = new StreamReader(response.GetResponseStream(), System.Text.Encoding.ASCII))
                    {
                        context.Response.Write(stream.ReadToEnd());
                    }
                }
            }
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}

Next add an HTML file by right clicking on the project and selecting Add -> New Item and selecting the HTML Page template. Call it index.html.

clip_image006

Screenshot: Creating and HTML page

Open the index.html page and update it with the following code. This code will add two buttons to the webpage which show how to use the proxy service to make requests the Bing Maps REST elevation and imagery services.

 <!DOCTYPE html>
<html xmlns="https://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.1.min.js"></script>

    <script type="text/javascript">
        var bingMapsKey = 'YOUR_BING_MAPS_KEY;
        var proxyService = "ProxyService.ashx?type=POST&url=";

        function GetElevationData() {
            var request = 'https://dev.virtualearth.net/REST/v1/Elevation/List?&key=' + bingMapsKey;

            //Generate mock data
            var coords = [];

            for (var i = 0; i < 100; i++) {
                coords.push(GetRandomCoordinate());
            }

            var postData = 'points=' + coords.join(',');

            //Call service
            CallPostService(request, postData, null, function (r) {
                document.getElementById('outputDiv').innerHTML = r;
            });
        }

        function GetStaticImage() {
            var request = 'https://dev.virtualearth.net/REST/v1/Imagery/Map/Road/?key=' + bingMapsKey;

            //Generate mock data
            var pushpins = [];

            for (var i = 0; i < 100; i++) {
                pushpins.push('pp=' + GetRandomCoordinate() + ';23;');
            }

            var postData = pushpins.join('&');

            //Call service
            CallPostService(request, postData, 'image/png', function (r) {
                document.getElementById('outputDiv').innerHTML = '<img src="' + r + '"/>';
            });
        }

        function CallPostService(requestUrl, postData, responseType, callback) {
            responseType = (responseType) ? responseType : '';

            $.ajax({
                type: 'POST',
                url: proxyService + encodeURIComponent(requestUrl) + '&responseType=' + responseType,
                dataType: "text",
                contentType: 'text/plain;charset=utf-8',
                data: postData,
                success: function (r) {
                    callback(r);
                },
                error: function (e) {
                    document.getElementById('outputDiv').innerHTML = e.responseText;
                }
            });
        }

        function GetRandomCoordinate() {
            //Limit coordinates to a small region in the US.
            var lat = Math.random() * 10 + 30; //Latitude range 30 - 40 degrees
            var lon = Math.random() * 10 - 100; //Longitude range -90 - -100 degrees
            return lat + ',' + lon;
        }
    </script>
</head>
<body>
    <input type="button" value="Get Elevation Data" onclick="GetElevationData();"/>
    <input type="button" value="Get Static Image" onclick="GetStaticImage();" />
    <br/><br />
    <div id="outputDiv" style="width:500px;"></div>
</body>
</html>

 

When the first button is pressed it will make a request to get the elevations of 100 random coordinates in the USA. This will result in the page looking something like this:

clip_image008

Screenshot: Response from POST Elevation request

When the second button is pressed it will make a request to get a static map image with 100 random pushpins. The response from the proxy service is a 64 bit encode string which we can pass as the source for an image tag to render the image on the page like this.

clip_image010

Screenshot: Response from POST Imagery request

Full source code for the proxy service example can be downloaded here.

- Ricky Brundritt, Bing Maps Program Manager