Bing Spatial Data Services Meets 3D

At //build 2014 there were many exciting announcements. It would have been easy to miss one of the implications of the moment of glory for Internet Explorer 11 during the keynote on day 1. The demo of the new FishGL website gave a hint about the enhanced support for WebGL in IE11 and, while I find fish quite yummy, I was more intrigued about what it means for mapping. After installing the Windows 8.1 Update, I quickly checked on Cesium, a free and open source map control that uses by default Bing Maps imagery and the Bing Maps geocoder. Many have experienced the Cesium map control when NORAD tracked Santa last Christmas but developers who wanted to also support IE had to use a special IE11 branch on GitHub. With this release of IE11 you can now take advantage of the latest and greatest in the Cesium control.

In this quick tutorial we will visualize data stored in the Bing Spatial Data Services (SDS) through the Cesium map control in 3D.

The Data

The data for this visualization is a combination of 911 incidents and Seattle Police Department Beats from Seattle’s Open Data Portal. We have to massage the data into a format that we can upload to the SDS. A sample of this specific data source is shown below. It contains a unique identifier, the identifier of the beat district, the number of 911 incidents in 2013 and a polygon describing the beat in Well Known Text (WKT).

Bing Spatial Data Services,1.0,Beat
EntityID(Edm.String,primaryKey)|Beat(Edm.String)|Num911(Edm.Int64)|Geom(Edm.Geography)
1|B3|4604|POLYGON ((-122.370761 47.667885, …, -122.370761 47.667885))

Once uploaded we can query the data with a REST service call such as:

http://spatial.virtualearth.net/REST/v1/data/
  [Data Source ID]/
  [Data Source Name]/
  [Entity Name]
  ?SpatialFilter=intersects
  ('POLYGON%20(([longitude 1]%20[latitude 1],…,[longitude 1]%20[latitude 1]))')
  &$top=250
  &$format=json
  &jsonp=[Your Callback Function]
  &key=[Your Bing Maps Key]

The response will be a JSON object with the polygons described in WKT and the number of 911 incidents that we can leverage to extrude the height of the polygon.

For more details see also Introducing Support for Custom Geospatial-Data in Bing SDS.

The Website

After downloading and extracting the Cesium control (I used b27 for this sample) we can simply drop the sub-folder Cesium into our website project. We also create CSS and a JavaScript as well as our HTML-document that will render the control. Our project tree should look like this in the Solution Explorer.

The HTML-document contains in the head references to the Cesium JavaScript and style-sheet as well as to our own. In the body we create 2 DIV-elements - one for the map control and one for the button that we click to query the data from the Bing SDS.

<!DOCTYPE html>
<html lang="en">
<head>
    <title>SDS - Custom GeoData</title>
    <script src="Cesium/Cesium.js"></script>
 1:  
 2:     <link href="Cesium/Widgets/widgets.css" rel="stylesheet" />
 3:     <script src="JS/MyScript.js">
</script>
    <link href="CSS/MyStyles.css" rel="stylesheet" />
</head>
<body>
    <div id="cesiumContainer"></div>
    <div id="toolBar" style="position:absolute; top:10px; left:10px;">
        <input id="btnGetData" type="button" value="Get Data" style="width:100px;" on-click="getSPD911();" />
    </div>
</body>
</html>

In our JavaScript we declare some global variables for the Bing Maps Key and our data source in the Bing SDS as well as the bounding box that we want to zoom and center the map to. When the HTML document loads we set the Bing Maps tile sources that we want to use and initialize the map control.

The function getSPD911 is triggered when we click on the button btnGetData and will query our data source in SDS. In the callback we render the data on the map as shown below. You will find the complete project here on my OneDrive.

window.onload = OnLoad;

var bmKey = "Your Bing Maps Key";
var baseUrl = "http://spatial.virtualearth.net/REST/v1/data/";
var dsIdSPD911 = "Your Data Source ID";
var dsNameSPD911 = "Your Data Source Name";
var entityNameSPD911 = "Your Entity Name";

var viewer = null;

var west = Cesium.Math.toRadians(-122.45);
var south = Cesium.Math.toRadians(47.55);
var east = Cesium.Math.toRadians(-122.25);
var north = Cesium.Math.toRadians(47.65);

function OnLoad() {
    Cesium.BingMapsApi.defaultKey = bmKey;

    var providerViewModels = [];

    providerViewModels.push(new Cesium.ImageryProviderViewModel({
        name: 'Road',
        iconUrl: Ce-sium.buildModuleUrl('../Cesium/Widgets/Images/ImageryProviders/bingRoads.png'),
        tooltip: 'Road Maps',
        creationFunction: function () {
            return new Cesium.BingMapsImageryProvider({
                url: 'http://dev.virtualearth.net',
                mapStyle: Cesium.BingMapsStyle.ROAD
            });
        }
    }));

    providerViewModels.push(new Cesium.ImageryProviderViewModel({
        name : 'Aerial',
        iconUrl : Ce-sium.buildModuleUrl('../Cesium/Widgets/Images/ImageryProviders/bingAerial.png'),
        tooltip : 'Aerial Imagery',
        creationFunction : function() {
            return new Cesium.BingMapsImageryProvider({
                url : 'http://dev.virtualearth.net',
                mapStyle : Cesium.BingMapsStyle.AERIAL
            });
        }
    }));

    providerViewModels.push(new Cesium.ImageryProviderViewModel({
        name: 'Aerial with Labels',
        iconUrl: Ce-sium.buildModuleUrl('../Cesium/Widgets/Images/ImageryProviders/bingAerialLabels.png'),
        tooltip: 'Aerial Imagery with Labels',
        creationFunction: function () {
            return new Cesium.BingMapsImageryProvider({
                url: 'http://dev.virtualearth.net',
                mapStyle: Cesium.BingMapsStyle.AERIAL_WITH_LABELS
            });
        }
    }));

    viewer = new Cesium.Viewer('cesiumContainer',
        {
            timeline: false,
            homeButton: false,
            animation: false,
            imageryProviderViewModels: providerViewModels,
            selectedImageryProviderViewModel: providerViewModels[1],
            terrainProvider: new Cesium.CesiumTerrainProvider({
                url: 'http://cesiumjs.org/stk-terrain/tilesets/world/tiles',
                credit: 'Terrain data courtesy Analytical Graphics, Inc.'
            }),
        }
    );

    var extent = new Cesium.Extent(west, south, east, north);
    viewer.scene.camera.viewExtent(extent);
}

function getSPD911() {
    var sdsRequest =
        baseUrl +
        dsIdSPD911 + "/" +
        dsNameSPD911 + "/" +
        entityNameSPD911 +
        "?SpatialFilter=intersects('POLYGON ((-122.5 47.8,-122.5 47.4,-122.2 47.4,-122.2 47.8,-122.5 47.8))')&$top=250&$format=json&jsonp=SPD911Callback&key=" + bmKey;

    var mapscript = document.createElement('script');
    mapscript.type = 'text/javascript';
    mapscript.src = sdsRequest;
    document.getElementById('cesiumContainer').appendChild(mapscript);
}

function SPD911Callback(result) {
    if (result &&
               result.d &&
               result.d.results &&
               result.d.results.length > 0) {

        var polygonInstances = [];
        var scene = viewer.scene;
        var primitives = scene.primitives;
        var ellipsoid = viewer.centralBody.ellipsoid;

          for (var i = 0; i < result.d.results.length; i++) {
            var entity = result.d.results[i];
            var wkt = entity.Geom;
            console.log(wkt);

            var positions = ellip-soid.cartographicArrayToCartesianArray(WKT2CartographicArray(wkt));

            polygonInstances.push(new Cesium.GeometryInstance({
                geometry: Cesium.PolygonGeometry.fromPositions({
                    positions: positions,
                    extrudedHeight: parseFloat(entity.Num911) / 10,
                    vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT
                }),
                attributes: {
                    color: Cesium.ColorGeometryInstanceAttribute.fromColor(new Ce-sium.Color(1.0, 0.0, 0.0, 0.5))
                }
            }));

            console.log(i);
        }

        primitives.add(new Cesium.Primitive({
            geometryInstances: polygonInstances,
            appearance: new Cesium.PerInstanceColorAppearance({
                closed: true,
                translucent: false
            })
        }));
    }
}

function WKT2CartographicArray(WKT) {
    var newWKT = WKT.replace("((", "");
    newWKT = newWKT.replace("))", "");
    try {
        newWKT = newWKT.replace("POLYGON ", "");
    } catch (e) {
    }
    try {
        newWKT = newWKT.replace("POLYLINE ", "");
    } catch (e) {
    }
    try {
        newWKT = newWKT.replace("POINT ", "");
    } catch (e) {
    }
    var stringArray = (newWKT.split(", "));
    for (var i = 0; i < stringArray.length; i++) {
        stringArray[i] = stringArray[i].replace(",", ",");
        stringArray[i] = stringArray[i].replace(" ", ",");
        var subNumbers = stringArray[i].split(",");
        for (var j = 0; j < subNumbers.length-1; j++) {
            subNumbers[j] = parseFloat(subNumbers[j]);
        }
        stringArray[i] = Cesium.Cartographic.fromDegrees(subNumbers[0], subNumbers[1], 0);
    }
    return stringArray;
}

Check out some of the other cool animations, 3D modeling and other stuff you can do with the Cesium control in the demo section of Cesiumjs.org.

Happy Coding ☺