Introducing Support for Custom Geospatial-Data in Bing SDS

The Bing Spatial Data Services (SDS) have always supported the management and retrieval of your points of interest (POI). You can upload text or XML-files with addresses or GPS-locations and batch-geocode or reverse geocode them, you can store them in the cloud and query your points of interest in a radius around a location, in a bounding box, or along a route. The SDS also provides access to categorized POI in North America and Europe as well as traffic incidents. Back in June, we added a preview of a GeoData API, which allows the retrieval of boundaries for countries, administrative regions, postcodes, cities and neighborhoods.

With the latest release, we have now added additional features that allow you to upload your own geospatial data of type POINT, MULTIPOINT, LINESTRING, MULTILINESTRING, POLYGON, MULTIPOLYGON and GEOMETRYCOLLECTION. We have also extended the Query API and added an additional spatial-filter parameter to retrieve geographies that intersect with another geography or just those parts that represent the intersections. These new features enable you to store and retrieve parcels, flood-plains, trails, power-lines, school-districts, sales-regions or other geospatial data.

In this tutorial we will take a lap around those new features. We start with the preparation of the data, look at the upload and then retrieve them from a Windows Store App.

Preparing the Data

Before we upload the data to the Bing SDS, we must create a text or XML-file that describes the data as OData types. This is no different from other data that you upload to SDS. However, for our geospatial data we have to choose the Edm.Geography type and define the geography as Well Known Text (WKT). In a pipe-delimited text-file this could look like:

Bing Spatial Data Services,1.0,KingCountyTrail
EntityID(Edm.String,primaryKey)|TrailName(Edm.String)|Geom(Edm.Geography)
1|My_Name|LINESTRING (-122.14927 47.40869, … , -122.14378 47.40879)

How do we get to this point? Well, geospatial data exist in a variety of formats and coordinate systems. Often it is required to use spatial ETL (Extract, Transform, Load) tools to transform the data. For the purpose of this tutorial we start with SQL Server 2012 and a table that describes trails as geography data types. I had previously uploaded this data from an Esri Shp-File and converted the coordinates from NAD83 to WGS 84 using OGR2OGR – a tool from the Geospatial Data Abstraction Library (GDAL).

 

Before we create the text or XML file that we want to upload to the Bing SDS, we must understand the limitations:

  • The data must be encoded in UTF-8
  • The uncompressed size of the file must not exceed 300 MB
  • You can have up to 200,000 entities per file
  • For a single data source you can use one initial (loadOperation=complete) and 2 incremental uploads, i.e. the total number of entities per data source can be up to 600,000
  • For geospatial data the maximum number of vertices per geography is limited to 2,000

This particular data source had some records that exceeded the limit of 2,000 points per geography. In order to reduce the number of points, we can leverage the spatial-functions in SQL Server. In this case we apply the Reduce-function and remove all points that are closer than 1 m to another point.

update trail set geog=geog.Reduce(1)

To verify the number of points, we can execute the following SQL-statement:

select geog.STNumPoints() as 'NumPoints' from trail order by NumPoints desc

I mentioned before that the geographies which we want to upload to the Bing SDS need to be described as Well Known Text (WKT) and SQL Server can help with that step as well. We can simply create a view like this:

CREATE VIEW v_trail
AS
SELECT ogr_fid AS EntityID, trail_name, trail_type, geog.STAsText() AS Geom
FROM dbo.trail

Now that we have our data fit for purpose we can export them into a text file. In this case we select the vertical bar as delimiter.

As the final step we replace the first line in the text file – the one that contains the column names - with the following to define the OData types:

Bing Spatial Data Services,1.0,KingCountyTrail
En-tityID(Edm.String,primaryKey)|TrailName(Edm.String)|TrailType(Edm.String)|Geom(Edm.Geography)

When we save the file we need to make sure that we select UTF-8 for the encoding.

Uploading the Data

For the Upload we create a simple Windows Forms application with a button and a text-box.

The code behind will read the local file and create a Load Data Source job. It will then generate a URL to monitor the status of the job and write it into the text-box.

Private Sub btnUploadLocal_Click(sender As Object, e As EventArgs) Handles btnUpload-Local.Click
        ' Custom name of spatial data source created during upload.
        Dim dataSourceName As String = "KingCountyTrail"
        ' Path to the spatial data input file to be uploaded.
        Dim dataFilePath As String = "D:\Downloads\Geodata\King Coun-ty\trail_SHP\trail\trail.txt"
        ' The master key used for uploading to Spatial Data Services.
        ' This key should differ from your query key.
        Dim bingMapsMasterKey As String = "YOUR_BING_MAPS_KEY"
        ' Create the spatial data upload URL.
        Dim queryStringBuilder As New StringBuilder()
        queryStringBuilder.Append("dataSourceName=")
        queryStringBuilder.Append(Uri.EscapeUriString(dataSourceName))
        queryStringBuilder.Append("&loadOperation=complete")
        ' Use pipe delimited text-file input and output for the spatial data.
        queryStringBuilder.Append("&input=pipe")
        queryStringBuilder.Append("&output=xml")
        queryStringBuilder.Append("&key=")
        queryStringBuilder.Append(Uri.EscapeUriString(bingMapsMasterKey))
        Dim uriBuilder As New UriBuilder("http://spatial.virtualearth.net")
        uriBuilder.Path = "/REST/v1/Dataflows/LoadDataSource"
        uriBuilder.Query = queryStringBuilder.ToString()
        Dim dataflowJobUrl As String = Nothing
        Using dataStream As FileStream = File.OpenRead(dataFilePath)
            Dim request As HttpWebRequest = DirectCast(WebRequest.Create(uriBuilder.Uri), HttpWebRequest)
            ' The HTTP method must be 'POST'.
            request.Method = "POST"
            request.ContentType = "text/plain"
            Using requestStream As Stream = request.GetRequestStream()
                Dim buffer As Byte() = New Byte(16383) {}
                Dim bytesRead As Integer = dataStream.Read(buffer, 0, buffer.Length)
                While bytesRead > 0
                    requestStream.Write(buffer, 0, bytesRead)
                    bytesRead = dataStream.Read(buffer, 0, buffer.Length)
                End While
            End Using
            ' Submit the HTTP request and check if the job was created successfully.
            Using response As HttpWebResponse = DirectCast(request.GetResponse(), HttpWe-bResponse)
                ' If the job was created successfully, the status code should be
                ' 201 (Created) and the 'Location' header should contain a URL
                ' that defines the location of the new dataflow job. You use this
                ' URL with the Bing Maps Key to query the status of your job.
                dataflowJobUrl = response.GetResponseHeader("Location")
                Dim jobStatusQueryUrl As String = String.Format("{0}?o=xml&key={1}", da-taflowJobUrl, Uri.EscapeUriString(bingMapsMasterKey))

                txtStatusURL.Text = jobStatusQueryUrl

                MessageBox.Show(response.StatusCode)
            End Using
        End Using
    End Sub

We can monitor the job status using the URL that was written into the text-box. Once the job is completed, we will also find the data source id and name which we will need later in our app to query data.

Building the Windows Store App

For the purpose of this tutorial we are going to build a Windows Store App for Windows 8.1 using Visual Studio 2013. However, Bing SDS are accessible via a REST API and can be used equally well in other Bing Maps controls. If you haven’t done so already, you will need to download and install the Bing Maps SDK for Windows 8.1 Store apps and you can do that directly from within Visual Studio by selecting the menu “Tools” and then “Extensions and Updates”. In the dialog go to Visual Studio Gallery and search for Bing Maps.

For this tutorial we are going to build a JavaScript app, so we create a new project and select the template for a “Blank App” in that category.

The app will use the Bing Maps for JavaScript control so we start by adding the reference to this control.

Next we work on the UI. So we open the default.html and start by adding some references to JavaScript files and style-sheets in the header. We need the references to the Bing Maps control whenever we want to use Bing Maps. In this case we also leverage the module for complex shapes (multi-geometries and polygons with holes); so we need to add the reference to the Bing Maps modules as well.

Finally we need to consider that the Bing SDS will return geospatial data as Well Known Text and to simplify the handling of this format we can leverage the “Well Known Text Reader/Writer” which is available as one of the community contributed modules for Bing Maps on CodePlex. So we download this module, add it to our project and reference it in the header as well.

<!-- Bing Maps -->
<script src="/Bing.Maps.JavaScript/js/veapicore.js"></script>
<script src="/Bing.Maps.JavaScript/js/veapiModules.js"></script>

<!-- JK_CustomGeoData02 references -->
<link href="/css/default.css" rel="stylesheet" />
<script src="/js/default.js"></script>
<script src="js/WKTModule.js"></script>

In the body we add a few HTML elements to host the map and create an animated panel with buttons to call Bing SDS and render the data.

<body>
    <div id="divMap"></div>
    <button id="btnShowPanel">Show Panel</button>
    <div id="divPanel">
        <input id="btnLocateMe" type="button" value="Locate Me" />
        <input id="btnGetTrail" type="button" value="Get Trail" />
        <input id="btnGetSchoolDistrict" type="button" value="Get School District" />
        <input id="btnGetSchool" type="button" value="Get Schools" />
        <input id="btnClearMap" type="button" value="Clear Map" />
        <div id="divLegend">
            <a id="lblLegend">Data provided by permission of King County</a>
        </div>
    </div>
</body>

Styling these elements in a JavaScript Windows Store App is no different than styling HTML-elements in a website. So we open our default.css file and add styles where necessary.

body {
}

#btnShowPanel {
    position: fixed;
    left: 10px;
    top: 10px;
    z-index: 1;
    background-color: black;
}

#divPanel {
    position: fixed;
    right: 0px;
    top: 0px;
    width: 350px;
    height: 100%;
    color: white;
    background-color: #323232;
    opacity: 0;
    z-index: 2;
}


#btnLocateMe, #btnGetParcel, #btnGetTrail, #btnGetSchoolDistrict, #btnGetSchool, #btnClearMap {
    position: relative;
    left: 0px;
    top: 0px;
    margin: 10px;
    background-color: black;
    width:330px;
}

#divLegend {
    position: absolute;
    bottom: 0px;
    margin: 10px;
    font-size: small;
    color: white;
}

#divPanel a {
    color: white !important;
}

Now let’s turn to the JavaScript and make things happen. We open default.js and add some global declarations at the top. We need some variables to handle the animated panel, the map, the Bing SDS data sources that we will want to use and a few more things that I’ll explain when we come to it.

// Global Declaration
var divPanel = null;
var animating = WinJS.Promise.wrap();

//Bing Maps
var map = null;
var cp = null;
var searchResultPage = 0;
var wkt = null;
var currInfobox = null;
var bmKey = "YOUR_BING_MAPS_KEY";
var baseUrl = "http://spatial.virtualearth.net/REST/v1/data/";
var dsIdTrail = "d0859b65e6f74923b0ea3beed96d1083";
var dsNameTrail = "KingCountyTrail";
var entityNameTrail = "KingCountyTrail";
var dsIdSchoolDistrict = "e31c831f5c4945dd81493a7efcc76df9";
var dsNameSchoolDistrict = "KingCountySchDst";
var entityNameSchoolDistrict = "SchDst";
var dsIdSchool = "f3526802415a4913b9d35ff2a67a88e1";
var dsNameSchool = "KingCountySchSite";
var entityNameSchool = "KingCountySchSite";

The template for JavaScript Windows Store Apps provides a frame with a few pre-defined functions. One of those handles an event that fires when the app is activated. In this function we add event-listeners that handle button-tapped or clicked-events. We also load the Bing Maps module and then load the map itself through a function getMap once the module is loaded.

app.onactivated = function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {
        if (args.detail.previousExecutionState !== activa-tion.ApplicationExecutionState.terminated) {
        } else {
        }
        args.setPromise(WinJS.UI.processAll().done(function () {
            var btnShowPanel = document.getElementById("btnShowPanel");
            btnShowPanel.addEventListener("click", togglePanel, false);
            divPanel = document.getElementById("divPanel");

            var btnGetParcel = document.getElementById("btnGetParcel");
            btnGetParcel.addEventListener("click", getParcels, false);

            var btnGetTrail = document.getElementById("btnGetTrail");
            btnGetTrail.addEventListener("click", getTrails, false);

            var btnLocateMe = document.getElementById("btnLocateMe");
            btnLocateMe.addEventListener("click", locateMe, false);

            var btnGetSchoolDistrict = docu-ment.getElementById("btnGetSchoolDistrict");
            btnGetSchoolDistrict.addEventListener("click", getSchoolDistrict, false);

            var btnGetSchool = document.getElementById("btnGetSchool");
            btnGetSchool.addEventListener("click", getSchools, false);

            var btnClearMap = document.getElementById("btnClearMap");
            btnClearMap.addEventListener("click", clearMap, false);

            Microsoft.Maps.loadModule("Microsoft.Maps.Map", { callback: getMap, cul-ture: "en-US", homeRegion: "US" });
        })
       );
    }
};

The function togglePanel handles the button to show or hide the panel and starts the animation. This is not specific to Bing Maps.

function togglePanel() {
    if (btnShowPanel.innerHTML === "Show Panel") {
        btnShowPanel.innerHTML = "Hide Panel";

        // If element is already animating, wait until current animation is complete be-fore starting the show animation.
        animating = animating.
            then(function () {
                // Set desired final opacity on the UI element.
                divPanel.style.opacity = "1";

                // Run show panel animation.
                // Element animates from the specified offset to its actual position.
                // For a panel that is located at the edge of the screen, the offset should be the same size as the panel element.
                // When possible, use the default offset by leaving the offset argument empty to get the best performance.
                return WinJS.UI.Animation.showPanel(divPanel);
            });
    } else {
        btnShowPanel.innerHTML = "Show Panel";

        // If element is still animating in, wait until current animation is complete before starting the hide animation.
        animating = animating
            .then(function () { return WinJS.UI.Animation.hidePanel(divPanel); })
            .then(
                // On animation completion, set final opacity to 0 to hide UI element.
                function () { divPanel.style.opacity = "0"; });
    }
}

In the function getMap we load the map into a div-element and apply a pre-defined theme for the navigation bar, pushpins and infoboxes. Once the map is loaded we call another function that loads the “Advanced Shape Module” which is used to render multi-geometries and polygons with holes correctly.

function getMap() {
    try {
        Microsoft.Maps.loadModule('Microsoft.Maps.Themes.BingTheme', {
            callback: function () {
                var mapOptions =
                  {
                      credentials: bmKey,
                      mapTypeId: "r",
                      enableClickableLogo: false,
                      enableSearchLogo: false,
                      showDashboard: false,
                      center: new Microsoft.Maps.Location(47.603569, -122.329453),
                      zoom: 9,
                      theme: new Microsoft.Maps.Themes.BingTheme()
                  };
                map = new Microsoft.Maps.Map(document.getElementById("divMap"), mapOp-tions);
            }
        });
        loadAdvancedShapeModule();
    }
    catch (e) {
        var md = new Windows.UI.Popups.MessageDialog(e.message);
        md.showAsync();
    }
}

The function that loads the “Advanced Shapes Module” will fire another function through the callback.

function loadAdvancedShapeModule() {
    Microsoft.Maps.loadModule('Microsoft.Maps.AdvancedShapes', {
        callback: loadWKTModule
    });
}

In this callback function we load the module that handles reading and writing of Well Known Text (WKT).

function loadWKTModule() {
    Microsoft.Maps.loadModule('WKTModule');
}

While we are at some general functions, we also add two more: one to determine the user’s locations…

function locateMe() {
    var geoLocationProvider = new Microsoft.Maps.GeoLocationProvider(map);
    geoLocationProvider.getCurrentPosition({ successCallback: function (object) { cp = object.center; } });
}

…and another one to clear the map:

function clearMap() {
    searchResultPage = 0;
    map.entities.clear();
}

At this point we will be able to start the app, see a map centered to the latitude/longitude and zoom-level defined above as well as toggle the panel, determine the user’s location and remove all entities from the map.

Next we move on to the functions that call the Bing SDS and retrieve geospatial data. Let’s start with the one for trails. Here we retrieve the boundaries of the map and prepare the call to the relevant data source in SDS. This data source has 2,885 records, so to limit the number of records, we want to filter by those that are in the currently visible area of the map. To do that we define a spatial-filter and intersect the data source with the polygon that represents this visible area. We also add parameters that define that we want to retrieve the maximum of 250 entities and another one that allows us to page through the results in case we have more than those 250 entities. We will handle this paging in a callback-function. Then we call the SDS asynchronously and define the callback-function that fires when the result comes back.

function getTrails() {
    var bounds = map.getBounds();
    var nw = bounds.getNorthwest();
    var se = bounds.getSoutheast();

    map.getCredentials(function (credentials) {
        var boundaryUrl =
            baseUrl +
            dsIdTrail + "/" +
            dsNameTrail + "/" +
            entityNameTrail +
            "?SpatialFilter=intersects('POLYGON ((" + nw.longitude + " " + nw.latitude + ","
                                                    + nw.longitude + " " + se.latitude + ","
                                                    + se.longitude + " " + se.latitude + ","
                                                    + se.longitude + " " + nw.latitude + ","
                                                    + nw.longitude + " " + nw.latitude
                                                    + "))')&$format=json&$inlinecount=allpages&$top=250&$skip="
                                                    + (searchResultPage * 250).toString() + "&key=" + credentials;
        WinJS.xhr({ url: boundaryUrl }).then(trailCallback);

        searchResultPage = searchResultPage + 1;
    });
}

In the callback function we parse the response into a JSON-object, then we read the Well Known Text from each entity and drop it on the map. If the response indicates that there are more than 250 entities for this query, we call the function getTrails again and skip those that we read already.

function trailCallback(result) {
    result = JSON.parse(result.responseText);

    for (var i = 0; i < result.d.results.length; i++) {
        var entity = result.d.results[i];
        var geomStyles = null;
        geomStyles = { pushpinOptions: {}, polylineOptions: { strokeColor: new Mi-crosoft.Maps.Color(255, 0, 255, 0) }, polygonOptions: { fillColor: new Mi-crosoft.Maps.Color(100, 128, 128, 128), strokeColor: new Microsoft.Maps.Color(255, 128, 128, 128) } };
        var shape = WKTModule.Read(entity.Geom, geomStyles);
        map.entities.push(shape)
    }

    if (result.d.__count > searchResultPage * 250) {
        getTrails();
    }
}

Let’s run the app again and test out the trails:

So far so good. We retrieved some spatial data for the current map view and rendered it in our Windows Store App. Let’s step it up a bit.

In the next step we want to find out which school district I am in and where the schools in this district are. I have uploaded the relevant data following the same procedure as the one for the trails.

In the previous example we have intersected the data source with a polygon that represented the bounding rectangle for the map view. In this case we intersect it with a point that represents the user’s current location and which we can retrieve by using the “Locate Me” button. We also don’t need to worry about paging since we only retrieve a single polygon.

function getSchoolDistrict() {
    map.getCredentials(function (credentials) {
        var boundaryUrl =
            baseUrl +
            dsIdSchoolDistrict + "/" +
            dsNameSchoolDistrict + "/" +
            entityNameSchoolDistrict +
            "?SpatialFilter=intersects('POINT (" + cp.longitude + " " + cp.latitude + ")')&$format=json&key=" + credentials;
        WinJS.xhr({ url: boundaryUrl }).then(schoolDistrictCallback);
    });
}

In the callback function we draw the polygon and set the map view to center and zoom on the school-district, but we do one more thing: when we query an SDS data source spatially we can intersect it with other geometries. In the previous examples we used a point and a polygon that was as simple as a rectangle. We can also use more complex polygons that we pass into the request but for those we are limited to 200 points. Since the polygon coming back from SDS can have more than 200 points we use the Douglas-Peucker algorithm to reduce its complexity. This is another JavaScript which you’ll find in the sample code and which we also need to reference in the default.html. Once we have the simplified polygon we use the Well Known Text module to write the WKT into a variable.

function schoolDistrictCallback(result) {
    result = JSON.parse(result.responseText);

    var entity = result.d.results[0];
    var geomStyles = null;
    geomStyles = { pushpinOptions: {}, polylineOptions: {}, polygonOptions: { fillColor: new Microsoft.Maps.Color(100, 128, 128, 128), strokeColor: new Microsoft.Maps.Color(255, 128, 128, 128) } };
    var shape = WKTModule.Read(entity.Geom, geomStyles);

    var locArray = shape.getLocations();
    map.entities.push(new Microsoft.Maps.Polygon(DouglasPeucker(locArray, 100), { fill-Color: new Microsoft.Maps.Color(100, 128, 128, 128), strokeColor: new Mi-crosoft.Maps.Color(255, 128, 128, 128) }));
    map.setView({ bounds: Microsoft.Maps.LocationRect.fromLocations(locArray) });
    wkt = WKTModule.Write(new Microsoft.Maps.Polygon(DouglasPeucker(locArray, 100)));
}

Let’s run the application again and check the progress so far by locating ourselves and then finding the school-district we’re in.

In the function that determines the school-district we already created the well-known text representation of the polygon and wrote it into the variable wkt. So we can simply pass this variable now into a call to the SDS, query the data source in which we store individual schools and intersect with it.

function getSchools() {
    map.getCredentials(function (credentials) {
        var poiUrl =
            baseUrl +
            dsIdSchool + "/" +
            dsNameSchool + "/" +
            entityNameSchool +
            "?SpatialFilter=intersects('" + wkt + "')&$top=250&$inlinecount=allpages&$format=json&key=" + credentials;
        WinJS.xhr({ url: poiUrl }).then(schoolCallback);
    });
}

In the callback-function we draw the results on the map and create handlers that handle the click on the pushpins.

function schoolCallback(result) {
    result = JSON.parse(result.responseText);

    for (var i = 0; i < result.d.results.length; i++) {
        createMapPin(result.d.results[i]);
    }
}
function createMapPin(result) {
    if (result) {
        var location = new Microsoft.Maps.Location(result.Latitude, result.Longitude);
        var pin = new Microsoft.Maps.Pushpin(location);
        pin.title = result.Name;
        pin.desc = result.AddressLine + "<br>" + result.PostalCode + "<br><br>School-District: " + result.SchoolDistrict;
        if (result.URL.length > 0) {
            pin.desc = pin.desc + "<br><br><a href='" + result.URL + "' tar-get='_blank'>More Info</a><br>";
        }
        Microsoft.Maps.Events.addHandler(pin, 'click', showInfoBox);
        map.entities.push(pin);
    }
}
function showInfoBox(e) {
    if (e.targetType == 'pushpin') {
        if (currInfobox) {
            currInfobox.setOptions({ visible: true });
            map.entities.remove(currInfobox);
        }

        currInfobox = new Microsoft.Maps.Infobox(
            e.target.getLocation(),
            {
                title: e.target.title,
                description: e.target.desc,
                showPointer: true,
                titleAction: null,
                titleClickHandler: null
            });
        currInfobox.setOptions({ visible: true });
        map.entities.push(currInfobox);
    }
}

And there we go. Let’s run our application and test the spatial-query.

We have covered a lot of ground in this post and I hope you saw how powerful those new features can be. The source code is available here.

- Bing Maps Team

CustomGeoSpatialDataMapAppFinal.png