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.

image

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.

image

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.

image

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>
    <link href="Cesium/Widgets/widgets.css" rel="stylesheet" />
    "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 modelling and other stuff you can do with the Cesium control in the demo section of Cesiumjs.org.

Happy Coding Smile

Technorati Tags: ,,,
Posted in 3D, Bing Maps, Cesium, SDS | Tagged , , , | Leave a comment

Staying Fit with Bing Maps

I just feel better when I have a certain level of fitness. It has less to do with bringing my body into shape for the speedo season and more with general happiness. For me ‘mens sana’ (a sound mind) lives indeed ‘in corpore sano’ (in a healthy body). Whenever possible I try to get my exercise out in nature, away from distractions, enjoying the fresh air and the occasional wildlife encounter. However, between rubbing the sleep from my face and going to work I find it more convenient to get down to my basement and jump on my bicycle or my rowing machine, especially when it is still dark outside. I also get bored easily and sweating just for the sake of sweating isn’t my cup of tea. Reading eBooks during the workout worked for a while, but it wasn’t very satisfying in the long run so I needed to come up with something that was a bit of a challenge along with some form of entertainment. So, I came up with the idea of the ‘Donkey Bike’.

The idea was to create a virtual trip to visit my parents in Germany and track the progress with every rotation of my wheel and every stroke of the rower. I’d rather choose a scenic route than the direct one, so the first part of the journey is a 6,886 km ride from Redmond, WA to Nova Scotia, Canada.

image

With 1,081 km down, a flat tire (yes, that can happen even in the basement), a few interesting scenes discovered on the imagery and a few pounds lighter than when I started the journey, I thought it’s time to share some details on the somewhat unusual ‘Donkey Bike’.

The Setup

I already had an indoor bicycle trainer with adjustable resistance to put my road bike on. From an old bicycle computer I re-used the reed-switch. I bought an Arduino Uno with a proto shield, dug out my good old soldering station and got to work. The reed switch needs to be connected to ground and digital pin 3 (D3) on the Arduino and then we can connect the board via the USB port to the laptop and start coding.

image

Arduino Firmware

I kept this part very simple and just count the rotations, i.e. the number of times that the reed-switch closed and write that number to the serial port that is being read by the USB connection on the laptop.

The Arduino IDE took a bit getting used to, and for more complex code I would be looking at something like Arduino for Visual Studio.

int reedPin = 3;  //digital pin connected to reed switch
int rotationCount = 0; // will be used in interrrupt

void rotation() {
  byte buff = 0x00;
  for (byte i = 0; i < 8; i++) {
    buff |= !digitalRead(reedPin) << i;
  } 
  if (buff == 0xff) {
    rotationCount += 1;
  }
}

void setup(){
  Serial.begin(9600);  // set pin connected to reed switch to input
  pinMode(reedPin, INPUT);  // set reed pin pull up resistor
  digitalWrite(reedPin, HIGH);  // attach interrupt to reed pin
  attachInterrupt(1, rotation, FALLING);
}


void loop(){
  Serial.println(rotationCount, DEC);
  delay(100);
}

Windows Application

Rather than going through the 200-odd lines of code of the WPF application, I will just point out a few things and put the complete source code on my OneDrive here.

For the mapping I use the Bing Maps WPF Control.

To plot the initial route I use the Bing Maps REST Services, avoid highways and return the route-geometry. I then insert the route as a SqlGeography in a SQL Server table. The create table scripts for the tables I use are also in the download. Note that Bing Maps returns coordinates in the order (latitude, longitude) while the Well Known Text (WKT) in which we describe the SqlGeography expects coordinates in the order (longitude latitude) so we have to flip the order for each coordinate.

Private Sub btnGetRoute_Click(sender As Object, e As RoutedEventArgs)
        myMap.CredentialsProvider.GetCredentials(
            Function(bmKey)
                Dim routeRequest As New Uri( _
            "http://dev.virtualearth.net/REST/V1/Routes/Driving" _
            + "?wp.0=[Your Start]" _
            + "&wp.1=[Your End]" _
            + "&avoid=highways" _
            + "&optimize=distance" _
            + "&distanceUnit=km" _
            + "&travelMode=Driving" _
            + "&routePathOutput=Points" _
            + "&key=" + bmKey.ApplicationId)
                Dim wc As New WebClient
                AddHandler wc.DownloadStringCompleted, AddressOf routeCallback
                wc.DownloadStringAsync(routeRequest)
                Return 0
            End Function)
    End Sub

    Private Sub routeCallback(sender As Object, e As DownloadStringCompletedEventArgs)
        Dim jsonText = e.Result
        Dim jsp = New JsonParser()
        Dim json As Object = jsp.Parse(jsonText)
        Dim routePath As String = "LINESTRING("
        For i = 0 To json.resourceSets(0).resources(0).routePath.line.coordinates.count - 1
            routePath += json.resourceSets(0).resources(0).routePath.line.coordinates(i)(1).ToString + _
                " " + json.resourceSets(0).resources(0).routePath.line.coordinates(i)(0).ToString + ","
        Next
        routePath = routePath.Substring(0, routePath.Length - 2) + ")"
        myGeog = SqlGeography.Parse(routePath)
        myGeog.STSrid = 4326
        myConn.Open()
        Dim myCMD As New SqlCommand
        myCMD.Connection = myConn
        myCMD.CommandText = "INSERT INTO Routes (Geog, StartLat, StartLon) VALUES (@Route, " + json.resourceSets(0).resources(0).routePath.line.coordinates(0)(0).ToString + ", " + json.resourceSets(0).resources(0).routePath.line.coordinates(0)(1).ToString + ")"
        Dim myGeogParam As SqlParameter = myCMD.Parameters.Add("@Route", Sys-tem.Data.SqlDbType.Udt)
        myGeogParam.UdtTypeName = "Geography"
        myGeogParam.Value = myGeog
        myCMD.ExecuteNonQuery()
        myConn.Close()
        MessageBox.Show("Done")
    End Sub

In order to track the progress I made along the route I calculate the distance based on the wheel size and the number of rotations that I receive from the Arduino and add it to the total distance I have travelled in previous workouts. Then I determine the corresponding location along the route using the function LocateAlongGeog from the SQL Server Spatial Tools and set the center of the map and the location of the pushpin accordingly.

Private Sub myTimer_Tick(sender As Object, e As EventArgs)
        If stopWatch.IsRunning Then
            Dim ts As TimeSpan = stopWatch.Elapsed
            currentTime = String.Format("{0:00}:{1:00}:{2:00}", ts.Hours, ts.Minutes, ts.Seconds)
            lblTimer.Content = currentTime

            Dim rotations() As String
            rotations = Split(com3.ReadExisting, Environment.NewLine)
            Dim lastRotations = rotations(rotations.Length - 2)
            lblRotations.Content = lastRotations

            Dim thisDistance As Integer = CInt(lastRotations * 700 * 3.14 / 1000)
            Dim totalDistance = thisDistance + myDistSoFar
            lblDistance.Content = thisDistance.ToString

            Dim myCurrentSqlLoc As SqlGeography
            myCurrentSqlLoc = SQLSpatialTools.Functions.LocateAlongGeog(myGeog, totalDis-tance)
            Dim myCurrentBingLoc As Location = New Location(myCurrentSqlLoc.Lat, myCur-rentSqlLoc.Long)
            myPin.Location = myCurrentBingLoc
            myMap.SetView(myCurrentBingLoc, 17)

            myGauge.CurrentValue = thisDistance / ts.TotalSeconds * 3.6
        End If
    End Sub

The gauge measures my average speed and makes me feel bad when I get below 35 km/h. For the gauge I re-used a piece of code written by EvelynT and published on Code Project.

I was contemplating creating a multi-user environment where you can race your friends and maybe that is something I’ll do later, but for now it’s been quite fun to figure out how to make this work and it continues to be fun to cycle virtually through beautiful landscapes. On the downside: I may have to resign as the mayor of the Hippo Pool soon :-)

image

Posted in Arduino, Bing Maps, WPF | Tagged , , , , | Leave a comment

Geo-fencing with Bing Spatial Data Services and Azure Mobile Services

Triggering certain actions such as sending notifications or alerts when a device enters or leaves an area is often referred to as geo-fencing. The geo-fence, the boundary of the area of interest, can be dynamic like a radius around a school or around your own device, it can be pre-defined such as a neighborhood, city or county, or it can be an area defined and digitized for a specific purpose.

For this example we narrow down the many different use cases to location-based notifications that leverage custom boundaries and address scenarios, such as

  • A parent who wants to receive a notification when his or her child arrives at school or is safely back home
  • Local news that want to send notifications or alerts to only the devices that are within a pre-defined area
  • Stores that want to advertise specials and promotions to individuals in the proximity of the store

To enable such scenarios we will leverage the Azure Mobile Services, which provide amongst others authentication, scheduling, and push-notifications, along with SDKs for Windows, Windows Phone, iOS, Android and HTML. No matter what platform, the Azure Mobile Service have you covered. We will also leverage the Bing Spatial Data Services (SDS), which allow you to batch-geocode and reverse-geocode your data, store your geospatial data in the cloud, and query them through a set of APIs. Additionally, the Bing SDS also provide an API to retrieve pre-defined boundaries.

In this end-to-end scenario we will look at the Bing SDS to store and query geo-fences, the tracked device, the Azure Mobile Services as the hub to process the tracked locations and send notifications as well as a Windows Store app to catch and display tile and toast notifications. Extending the notifications to a smart-watch could be an interesting add-on but we will save this for a later post.

image

The Geo-Fences

For this example I have created geo-fences that cover Microsoft offices in Bellevue, WA

image

For the purpose of this blog post we will assume that we have already uploaded the geo-fences to the Bing SDS. Creating the geo-fences and uploading them to the Bing SDS is a topic that we can cover in a separate blog post.

To query the geo-fence that a location is in, we can execute a query such as

http://spatial.virtualearth.net/REST/v1/data/

[Your Data Source ID]/
[Your Data Source Name]/
[Your Entity Name]
?SpatialFilter=intersects(‘POINT%20([longitude]%20[latitude])’)
&$format=json
&key=[Your Bing Maps Key]

The response will contain the geo-fence with its geography described as Well Known Text (WKT), its name, centroid and some other information as described here.

image

The array of results will be empty if the location does not fall into a geo-fence.

Setting Up the Azure Mobile Services

Azure Mobile Services come with a free tier for up 10 services. So we go ahead and create a new Mobile Service with a new database. If you already use your free database you can also add new tables to an existing database. A great tutorial on getting started with Azure Mobile Services is here. Once we have created the new service we can either download a starter app for several platforms or build a new app from scratch. We will do the latter and will require the URL to the service as well as the application key. We can get both from the dashboard in the Azure Portal.

image

While we are in the Azure Portal we also switch over to the Data tab and create a new table ‘Tracker’. For this tutorial we keep it simple and don’t change the default permissions.

image

We don’t need to add any columns either since the database schema is by default dynamic and Azure Mobile Services will automatically create columns based on the data it receives from the client.

We will come back later to the Azure Portal to monitor our apps and add additional functionality but for now we can move on to the mobile app for the devices that we are tracking. In this tutorial we will build a Windows Phone 8 app but as mentioned before Azure Mobile Services support also the Android and iOS platforms – directly through native code as well as through cross-platform solutions such as Xamarin or PhoneGap.

Creating the Windows Phone App

Rather than creating a tracking application from the start we will build upon a sample application for Windows Phone 8 that lets location tracking applications run in the background. For the purpose of this walk through we assume that you have completed the app as described in the tutorial.

We start modifying this existing application by adding the NuGet package for Azure Mobile Services. This will also automatically add a few dependencies.

image

Next we open the WMAppManifest.xml and add permit the app to retrieve the unique device ID in order to identify each tracked device.

image

In the App.xaml.vb we import the Azure Mobile Services namespace

Imports Microsoft.WindowsAzure.MobileServices

And then declare the connection to the Azure Mobile Service with its URL and App ID that we retrieved from the Azure Portal.

' declare the Azure Mobile Service Client
Public Shared MobileService As New MobileServiceClient("https://[your service].azure-mobile.net/", "[your app id]")

In Page2.xaml.vb we import a few namespaces and create a class ‘Tracker’ with properties that we want to submit to the Azure Mobile Services.

Imports Microsoft.WindowsAzure.MobileServices
Imports Microsoft.Phone.Info
Imports System.Threading

Public Class Tracker

    Public Property Id() As String
        Get
            Return m_Id
        End Get
        Set(value As String)
            m_Id = value
        End Set
    End Property
    Private m_Id As String

    <JsonProperty(PropertyName:="DeviceID")> _
    Public Property DeviceID() As String
        Get
            Return m_DeviceID
        End Get
        Set(value As String)
            m_DeviceID = value
        End Set
    End Property
    Private m_DeviceID As String

    <JsonProperty(PropertyName:="Lat")> _
    Public Property Lat() As Double
        Get
            Return m_Lat
        End Get
        Set(value As Double)
            m_Lat = value
        End Set
    End Property
    Private m_Lat As Double

    <JsonProperty(PropertyName:="Lon")> _
    Public Property Lon() As Double
        Get
            Return m_Lon
        End Get
        Set(value As Double)
            m_Lon = value
        End Set
    End Property
    Private m_Lon As Double

    <JsonProperty(PropertyName:="Alt")> _
    Public Property Alt() As Integer
        Get
            Return m_Alt
        End Get
        Set(value As Integer)
            m_Alt = value
        End Set
    End Property
    Private m_Alt As Integer

    <JsonProperty(PropertyName:="Course")> _
    Public Property Course() As Integer
        Get
            Return m_Course
        End Get
        Set(value As Integer)
            m_Course = value
        End Set
    End Property
    Private m_Course As Integer

    <JsonProperty(PropertyName:="Speed")> _
    Public Property Speed() As Integer
        Get
            Return m_Speed
        End Get
        Set(value As Integer)
            m_Speed = value
        End Set
    End Property
    Private m_Speed As Integer

    <JsonProperty(PropertyName:="VAcc")> _
    Public Property VAcc() As Integer
        Get
            Return m_VAcc
        End Get
        Set(value As Integer)
            m_VAcc = value
        End Set
    End Property
    Private m_VAcc As Integer

    <JsonProperty(PropertyName:="HAcc")> _
    Public Property HAcc() As Integer
        Get
            Return m_HAcc
        End Get
        Set(value As Integer)
            m_HAcc = value
        End Set
    End Property
    Private m_HAcc As Integer

    <JsonProperty(PropertyName:="GPSDate")> _
    Public Property GPSDate() As Date
        Get
            Return m_GPSDate
        End Get
        Set(value As Date)
            m_GPSDate = value
        End Set
    End Property
    Private m_GPSDate As Date

End Class

In the class Page2 we retrieve the table ‘Tracker’ from the Azure Mobile Service and declare a string-object that will hold our unique device ID. We will read this unique device ID into the string object when the page is first initialized.

Partial Public Class Page2
    Inherits PhoneApplicationPage

    Private trackTable As IMobileServiceTable(Of Tracker) = App.MobileService.GetTable(Of Tracker)()
    Private myDeviceID As String = ""

    Public Sub New()
        InitializeComponent()

        myDeviceID = Con-vert.ToBase64String(DeviceExtendedProperties.GetValue("DeviceUniqueId"))
    End Sub
Next we add a new dispatcher to the sub geolocator_PositionChanged as shown in the last line of the code-block below.
    Private Sub geolocator_PositionChanged(sender As Geolocator, args As Position-ChangedEventArgs)
        If Not App.RunningInBackground Then
            Dispatcher.BeginInvoke(Function()
                                       LatitudeTextBlock.Text = args.Position.Coordinate.Latitude.ToString("0.00")
                                       LongitudeTextBlock.Text = args.Position.Coordinate.Longitude.ToString("0.00")
                                   End Function)
        Else
            Dim toast As New Microsoft.Phone.Shell.ShellToast()
            toast.Content = args.Position.Coordinate.Latitude.ToString("0.00")
            toast.Title = "Location: "
            toast.NavigationUri = New Uri("/Page2.xaml", UriKind.Relative)

            toast.Show()
        End If

        Deployment.Current.Dispatcher.BeginInvoke(Function() MyPositionChanged(args))    
End Sub

The function that we call from the dispatcher as well as the actual insert into the Azure Mobile Services will also be added to the class.

Private Async Function MyPositionChanged(ByVal args As PositionChangedEventArgs) As Tasks.Task
        Dim myLat As String = ""
        Try
            myLat = args.Position.Coordinate.Latitude.ToString("0.000000")
        Catch ex As Exception
        End Try
        Dim myLon As String = ""
        Try
            myLon = args.Position.Coordinate.Longitude.ToString("0.000000")
        Catch ex As Exception
        End Try
        Dim myAlt As String = ""
        Try
            myAlt = CInt(args.Position.Coordinate.Altitude).ToString
        Catch ex As Exception
        End Try
        Dim myCourse As String = ""
        Try
            myCourse = CInt(args.Position.Coordinate.Heading).ToString
        Catch ex As Exception
        End Try
        Dim mySpeed As String = ""
        Try
            mySpeed = CInt(args.Position.Coordinate.Speed).ToString
        Catch ex As Exception
        End Try
        Dim myVAcc As String = ""
        Try
            myVAcc = CInt(args.Position.Coordinate.SatelliteData.HorizontalDilutionOfPrecision).ToString
        Catch ex As Exception
            myVAcc = 0
        End Try
        Dim myHAcc As String = ""
        Try
            myHAcc = CInt(args.Position.Coordinate.SatelliteData.VerticalDilutionOfPrecision).ToString
        Catch ex As Exception
            myHAcc = 0
        End Try
        Dim myGPSDate As Date
        Try
            myGPSDate = args.Position.Coordinate.Timestamp.DateTime
        Catch ex As Exception
        End Try

        Dim thisTrackPoint As New Tracker With { _
            .DeviceID = myDeviceID,
            .Lat = myLat,
            .Lon = myLon,
            .Alt = myAlt,
            .Course = myCourse,
            .Speed = mySpeed,
            .VAcc = myVAcc,
            .HAcc = myHAcc,
            .GPSDate = myGPSDate
        }
        Await InsertTrackPoint(thisTrackPoint)

    End Function

    Private Async Function InsertTrackPoint(thisTrackPoint As Tracker) As Tasks.Task
        Await trackTable.InsertAsync(thisTrackPoint)
    End Function

For the purpose of this tutorial we’re done with this app. Let’s run it in the emulator and change the position a few times:

image

Now let’s double check in the Azure Portal. We open the table ‘Tracker’ under the tabulator data and verify that the columns have indeed been created and that we have some records in the table.

image

You will find the complete source code here on my OneDrive. Note: you will need to edit App.xaml.vb to add your own Azure Mobile Service URL as well as your own App ID.

Creating the Windows Store App

For the sake of brevity in this blog post we just download the sample application from the Azure Portal. I chose the JavaScript app.

image

Next we follow the tutorial Get started with push notifications in Mobile Services. Let’s check that everything works well so far by adding a few items in the sample app and verify that we receive notifications.

Bringing it all together with the Scheduler

We will bring now all the different pieces together with our Scheduler in the Azure Mobile Services. Let’s create a new scheduler in the Azure Portal and plan to run it every 5 minutes but not activate it yet. We can always click on ‘Run One’ to test the code.

image

Now that we created our job we can navigate to the scheduler job and the script that we want to create. Again we keep it very simple here. There could be a lot of logic to filter messages by tracked device or type of alert (see Next Steps at the end of the tutorial Get started with push notifications in Mobile Services) but in this case we just verify if the last tracked location was inside a geo-fence, then we build toast- and tile-notifications each with a mini-map for that location created from the Bing Maps REST Imagery Services. You will find the full script below.

image

function DonkeyFence_job() {
    var sql = "select top 1 * from Tracker order by GPSDate desc";
    var trackTable = tables.getTable('Tracker');

    //Bing Maps
    var bmKey = "Your Bing Maps Key";
    var baseUrl = "http://spatial.virtualearth.net/REST/v1/data/";
    var dsIdGeofence = "Your Bing SDS Data Source ID";
    var dsNameGeofence = "Your Bing SDS Data Source Name";
    var entityNameGeofence = "Your Bing SDS Entity Name";

    var httpRequest = require('request');

    mssql.query(sql, {
        success: function (results) {
            if (results.length > 0) {
                //console.log('Lat:' + results[0].Lat.toString() + ' / Lon: ' + re-sults[0].Lon.toString());
                var geoFenceUrl =
                    baseUrl +
                    dsIdGeofence + "/" +
                    dsNameGeofence + "/" +
                    entityNameGeofence +
                    "?SpatialFilter=intersects('POINT (" + results[0].Lon.toString() + " " + results[0].Lat.toString() + ")')&$format=json&key=" + bmKey;
                //console.log(geoFenceUrl);
                httpRequest(geoFenceUrl, function (err, response, body) {
                    if (err) {
                        console.log(statusCodes.INTERNAL_SERVER_ERROR, 'Unable to connect to Bing Maps.');
                    }
                    else {
                        //console.log(JSON.parse(body));
                        var sdsResults = JSON.parse(body).d.results;
                        if (sdsResults.length > 0) {
                            var geoFenceName = sdsResults[0].Name;
                            var channelsTable = tables.getTable('Channels');
                            channelsTable.read({
                                success: function (devices) {
                                    devices.forEach(function (device) {
                                        push.wns.sendTileWidePeekImageAndText02(device.channelUri, {
                                            image1src: 'http://dev.virtualearth.net/REST/v1/Imagery/Map/AerialWithLabels/' +
                                                results[0].Lat.toString() + ',' + re-sults[0].Lon.toString() + '/16' +
                                                '?ms=310,150&key=' + bmKey,
                                            image1alt: 'Geofence',
                                            text1: 'Johannes',
                                            text2: 'arrived at',
                                            text3: geoFenceName
                                        }, {
                                            success: function (pushResponse) {
                                                console.log("Sent push:", pushResponse);
                                            }
                                        });

                                        push.wns.sendToastImageAndText04(device.channelUri, {
                                            image1src: 'http://dev.virtualearth.net/REST/v1/Imagery/Map/AerialWithLabels/' +
                                                results[0].Lat.toString() + ',' + re-sults[0].Lon.toString() + '/16' +
                                                '?ms=150,150&key=' + bmKey,
                                            image1alt: 'Geofence',
                                            text1: 'Johannes',
                                            text2: 'arrived at',
                                            text3: geoFenceName
                                        }, {
                                            success: function (pushResponse) {
                                                console.log("Sent push:", pushResponse);
                                            }
                                        });
                                    });
                                }
                            });
                        }
                        else {
                            console.log('No tracks found.');
                        }
                    }
                });
            }
            else {
                console.log('No tracks found.');
            }
        }
    });
}

Let’s start the mobile app and move a location into one of the geo-fences.

image

Now we run the scheduler job once and see our tile and toast notifications come in.

image

And that’s it for now. Happy coding and I hope to see you soon back here.

Related blog posts:

 

Posted in 3D, Bing Maps, Cesium, SDS | Tagged , , , , | Leave a comment

Talking Maps

In a previous blog post we had a lap around the new support for Custom Geospatial Data in the Bing Spatial Data Services (SDS). This time around we will build upon that tutorial and extend the app so that we can talk to it and have it talk back.

Check out the video to see and hear what we’re going to build.

In order to achieve this we leverage the Bing Speech Recognition Control for Windows 8.1 as well as the Windows 8.1 SDK for speech synthesis.

The documentation for the Bing Speech Recognition Control contains detailed instructions on how to register and install the control and how to enable a project for speech recognition so we won’t dive too deep into this. Instead we start with our previous project assuming that

  • You signed up for the Bing Speech Recognition Control in the Windows Azure Marketplace
  • You registered an application and created a Client ID and Client Secret in the Azure Marketplace
  • You downloaded and installed the Bing Speech Recognition Control
  • You downloaded and installed the Windows SDK for Windows 8.1

Speech-Enabling our Project

Once we are all set up, we open our project and add references to Bing Speech and the Visual C++ 2013 Runtime.

image

The Visual C++ Runtime requires that we compile the project for individual platforms rather than for all CPUs at the same time. Therefore we open the Configuration Manager and select our first platform (here x64).

image

We also need to modify the package.appxmanifest from the code-view. By adding the capability “microphone” and a section Extensions just underneath the Capabilities.

  <Capabilities>
    <Capability Name="internetClient" />
    <DeviceCapability Name="microphone" />
    <DeviceCapability Name="location" />
  </Capabilities>
  <Extensions>
    <Extension Category="windows.activatableClass.inProcessServer">
      <InProcessServer>
        <Path>Microsoft.Speech.VoiceService.MSSRAudio.dll</Path>
        <ActivatableClass ActivatableClas-sId="Microsoft.Speech.VoiceService.MSSRAudio.Encoder" ThreadingModel="both" />
      </InProcessServer>
    </Extension>
    <Extension Category="windows.activatableClass.proxyStub">
      <ProxyStub ClassId="5807FC3A-A0AB-48B4-BBA1-BA00BE56C3BD">
        <Path>Microsoft.Speech.VoiceService.MSSRAudio.dll</Path>
        <Interface Name="IEncodingSettings" InterfaceId="C97C75EE-A76A-480E-9817-D57D3655231E" />
      </ProxyStub>
    </Extension>
    <Extension Category="windows.activatableClass.proxyStub">
      <ProxyStub ClassId="F1D258E4-9D97-4BA4-AEEA-50A8B74049DF">
        <Path>Microsoft.Speech.VoiceService.Audio.dll</Path>
        <Interface Name="ISpeechVolumeEvent" InterfaceId="946379E8-A397-46B6-B9C4-FBB253EFF6AE" />
        <Interface Name="ISpeechStatusEvent" InterfaceId="FB0767C6-7FAA-4E5E-AC95-A3C0C4D72720" />
      </ProxyStub>
    </Extension>
  </Extensions>

Changing the UX

In the header of default.html we add references to the Bing Speech Control.

    <!-- Bing Speech -->
    <link href="/Bing.Speech/css/voiceuicontrol.css" rel="stylesheet" />
    <script src="/Bing.Speech/js/voiceuicontrol.js"></script>

In the body of default.html we replace all existing buttons with a button to initiate the Bing Speech Recognizer as well as a div-elements one that hosts the Bing SpeechRecognizerUx

    <div id="divPanel" >
        <div id="divSpeechControl" data-win-control="BingWinJS.SpeechRecognizerUx"></div>
        <input id="btnSpeech" type="button" value="Talk to Me" />
    </div>

We also add a style for this new button in the default.css

#btnSpeech {
    position: relative;
    left: 0px;
    top: 0px;
    margin: 10px;
    background-color: black;
    width:330px;
}

Modifying the JavaScript

In the default.js we add a few more global variables for the Bing Speech credentials and the Speech Recognizer.

// Bing Speech 
var speechRecognizer = null;
var bsClientID = "YOUR_BING_SPEECH_ID";
var bsClientSecret = " YOUR_BING_SPEECH_SECRET";

In the app.onactivated event we replace the event-listeners for the removed buttons with one for btnSpeech and we also define the Bing Speech Recognizer. The modified function looks like this:

    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 btnSpeech = document.getElementById("btnSpeech");
                btnSpeech.addEventListener("click", talkToMe, false);
                var credentials = new Bing.Speech.SpeechAuthorizationParameters();
                credentials.clientId = bsClientID;
                credentials.clientSecret = bsClientSecret;
                speechRecognizer = new Bing.Speech.SpeechRecognizer("en-US", creden-tials);

                document.getElementById("divSpeechControl").winControl.tips = new Array(
                    "For more accurate results, try using a headset microphone.",
                    "Speak with a consistent volume.",
                    "Speak in a natural rhythm with clear consonants.",
                    "Speak with a slow to moderate tempo.",
                    "Background noise may interfere with accurate speech recognition."
                );

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

Finally we add a new function talkToMe that handles tapping or clicking the speech-button. When this event fires we initialize the Speech Recognizer and evaluate the result. Depending on key-words that we recognize, we synthesize a response and fire a corresponding function as defined in our previous tutorial.

function talkToMe() {
    document.getElementById("divSpeechControl").winControl.speechRecognizer = speechRec-ognizer;
    speechRecognizer.recognizeSpeechToTextAsync().then(function (result) {
            if (typeof (result.text) == "string") {
                //document.getElementById("divResultText").innerHTML = result.text;

                // The object for controlling and playing audio.
                var audio = new Audio();

                // The object for controlling the speech synthesis engine (voice).
                var synth = new Windows.Media.SpeechSynthesis.SpeechSynthesizer();

                if (result.text.indexOf("locate") > -1) {
                    synth.synthesizeTextToStreamAsync("Locating You.").then(function (markersStream) {
                        var blob = MSApp.createBlobFromRandomAccessStream(markersStream.ContentType, markersStream);
                        audio.src = URL.createObjectURL(blob, { oneTimeOnly: true });
                        audio.play();

                        locateMe();
                    });
                }

                else if (result.text.indexOf("district") > -1) {
                    synth.synthesizeTextToStreamAsync("Searching Bing Spatial Data Ser-vices for school district.").then(function (markersStream) {
                        var blob = MSApp.createBlobFromRandomAccessStream(markersStream.ContentType, markersStream);
                        audio.src = URL.createObjectURL(blob, { oneTimeOnly: true });
                        audio.play();

                        getBoundary();
                    });
                }

                else if (result.text.indexOf("schools") > -1) {
                    synth.synthesizeTextToStreamAsync("Searching Bing Spatial Data Ser-vices for school sites.").then(function (markersStream) {
                        var blob = MSApp.createBlobFromRandomAccessStream(markersStream.ContentType, markersStream);
                        audio.src = URL.createObjectURL(blob, { oneTimeOnly: true });
                        audio.play();

                        getPOI();
                    });
                }

                else if (result.text.indexOf("awesome") > -1) {
                    synth.synthesizeTextToStreamAsync("I know.").then(function (mark-ersStream) {
                        var blob = MSApp.createBlobFromRandomAccessStream(markersStream.ContentType, markersStream);
                        audio.src = URL.createObjectURL(blob, { oneTimeOnly: true });
                        audio.play();
                    });
                }

                else if (result.text.indexOf("built") > -1) {
                    var Ssml = "<speak version='1.0' " +
                        "xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='en-US'>" +
                        "Justin, ,, Yi, Gevorg, Doug, and ." +
                        "<break time='500ms' />" +
                        "These guys rock" +
                        "</speak>";
                    synth.synthesizeSsmlToStreamAsync(Ssml).then(function (markersStream) {
                    var blob = MSApp.createBlobFromRandomAccessStream(markersStream.ContentType, markersStream);
                    audio.src = URL.createObjectURL(blob, { oneTimeOnly: true });
                    audio.play();
                    });
                }
            }
            else {
                // Handle quiet or unclear speech here.
            }
        },
        function (error) {
            // Put error handling here.
        }
    )
}

And that’s already it. Run your app and check it out. You’ll find the complete source code here.

image

Posted in AJAX, Bing Maps, Bing Speech, SDS, Windows Store | Tagged , , , , , , , , , , , , | Leave a comment

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.

image

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).

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).

image

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

image

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.

image

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
EntityID(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.

image

Uploading the Data

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

image

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.

image

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.

image

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.

image

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

image

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, #btn-ClearMap {
    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.

image

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:

image

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.

image

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.

image

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.

Posted in AJAX, Bing Maps, SDS, Windows Store | Tagged , , , , , , | 1 Comment

Retrieving Boundaries from the Bing Spatial Data Services (Preview)

The Bing Spatial Data Services (SDS) allow you to submit large amounts of addresses for batch-geocoding as well as GPS-coordinates for reverse geocoding. You can download the results or keep them in our data centers and retrieve your points of interest (POI) within a distance of a specific location or along a route through the SDS Query API. Aside from your own POI which you can manage through the SDS, you can also query POI from our public data sources which are grouped into categories.

These API and data are useful for a variety of scenarios and are most often used in typical “locator” scenarios where you want to find stores, dealerships, etc.

Today we are beginning to preview a new capability of the SDS which allows you to not only retrieve points but also polygons. Initially this includes boundaries for countries, administrative levels and more. This new capability is exposed through the GeoData API and could be useful to highlight areas of interest…

image

…or create “thematic maps” where you color-code regions based on key performance indicators (KPI) such as the revenue, number of customers, crime statistics, environmental data, etc.

image

You’ll find the preliminary documentation for the preview here; so rather than talking about different parameters let’s go straight into a sample where we retrieve a polygon and visualize it in Windows Store app.

Prerequisites

Since we are developing a Windows Store App, we require access to a Windows 8 machine as well as Visual Studio 2012. A free version of Visual Studio Express 2012 for Windows 8 is available here.

The “Bing Maps SDK or Windows Store Apps” can be installed directly from Visual Studio by selecting “Extensions and Updates” from the menu “Tools” and searching in the online gallery.

We will also require a Bing Maps Key. If you don’t have one, you can follow these instructions to get a free trial or basic key.

Getting Started

We start by creating a new project. For this example we select the template for JavaScript applications in the Windows Store and create a ‘Blank App’.

image

Next we add a reference to the ‘Bing Maps for JavaScript’ control.

image

Let’s code

In the head of default.html we add script-references to load the Bing Maps core as well as additional modules.

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

In the body of default.html we just add a div-element that will host our map.

<div id="divMap"></div>

Moving on to the JavaScript default.js we add a few global variables for the map, the base-URL for the GeoData API as well as a list of safe characters. We will come back to these safe characters later.

var map = null;
var baseUrl = "http://platform.bing.com/geo/spatial/v1/public/geodata?SpatialFilter=";
var safeCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";

Just underneath the line…

app.start();

…we add an event-handler that fires when the entire DOM content is loaded…

document.addEventListener("DOMContentLoaded", initialize, false);

…and executes the function initialize. The following code will be in between those 2 lines.

We start by loading the Bing Maps core and one this is done we execute a function initMap.

function initialize() {
  Microsoft.Maps.loadModule('Microsoft.Maps.Map', { callback: initMap });
}

The function initMap defines center-point and zoom-level of the map as well as a few other options and renders the map in the div-element we reserved for it. Then we move on to load the AdvancedShapes-module.

function initMap() {
  try {
    var mapOptions =
    {
      credentials: "Your Bing Maps Key",
      mapTypeId: "r",
      enableClickableLogo: false,
      enableSearchLogo: false,
      center: new Microsoft.Maps.Location(47.490860, -121.835747),
      zoom:9
    };
    map = new Microsoft.Maps.Map(document.getElementById("divMap"), mapOptions);

    loadAdvancedShapeModule();
  }
  catch (e) {
    var md = new Windows.UI.Popups.MessageDialog(e.message);
    md.showAsync();
  }
}

Loading the AdvancedShape-module may not be necessary, but it will be useful whenever you want to render polygons with holes. If you wanted to render for example the polygon that represents Italy but exclude the enclosed areas representing San Marino and Vatican City you would require the AdvancedShape-module

image

When the module is loaded we execute a function that calls the GeoData API and retrieves the polygon.

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

Notice that we retrieve the credentials from the map rather than just re-using the same key that we specified for the map. By doing so we generate a session-key which will indicate to the transaction counting and reporting system in our backend that this call originated within a map control. Such transactions appear as “non-billable” in your usage reports. For more information on this particular aspect see Viewing Bing Maps Usage Reports.

When we call the GeoData API we can provide either an address string or a latitude and longitude as parameter. In this case we use the string “King County”. We also specify a few more parameters which you find explained in more detail in the documentation before execute the request.

function getBoundary() {
  map.entities.clear();

  map.getCredentials(function (credentials) {
    var boundaryUrl = baseUrl 
      + "GetBoundary('King County',1,'AdminDivision2',0,0,'en','US')&$format=json&key=" 
      + credentials;
      WinJS.xhr({ url: boundaryUrl }).then(boundaryCallback);
    });
  }

To optimize for performance, the response is highly compressed and therefore we call a function ParseEncodedValue for each polygon to decompress it. We also define stroke- and fill-color before we add the polygon to the map.

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

  var entity = result.d.results[0];
  var entityMetadata = entity.EntityMetadata;
  var entityName = entity.Name.EntityName;
  var primitives = entity.Primitives;

  var polygoncolor = null;
  var strokecolor = null;
  var boundaryVertices = null;
  var numOfVertices = 0;

  polygoncolor = new Microsoft.Maps.Color(100, 128, 128, 128);
  strokecolor = new Microsoft.Maps.Color(255, 128, 128, 128);

  var polygonArray = new Array();
  for (var i = 0; i < primitives.length; i++) {
    var ringStr = primitives[i].Shape;
    var ringArray = ringStr.split(",");

    for (var j = 1; j < ringArray.length; j++) {
      var array = ParseEncodedValue(ringArray[j]);

      if (array.length > numOfVertices) {
        numOfVertices = array.length;
        boundaryVertices = array;
      }
      polygonArray.push(array);
    }

    var polygon = new Microsoft.Maps.Polygon(polygonArray, 
      { fillColor: polygoncolor, strokeColor: strokecolor });
    map.entities.push(polygon)
  }
}

The compression algorithm is the same as the one we documented for the Elevations API.

function ParseEncodedValue(value) {
  var list = new Array();
  var index = 0;
  var xsum = 0;
  var ysum = 0;
  var max = 4294967296;

  while (index < value.length) {
    var n = 0;
    var k = 0;

    while (1) {
      if (index >= value.length) 
        {
          return null;
        }
      var b = safeCharacters.indexOf(value.charAt(index++));
      if (b == -1) {
        return null;
      }
      var tmp = ((b & 31) * (Math.pow(2, k)));

      var ht = tmp / max;
      var lt = tmp % max;

      var hn = n / max;
      var ln = n % max;

      var nl = (lt | ln) >>> 0;
      n = (ht | hn) * max + nl;
      k += 5;
      if (b < 32) break;
    }

    var diagonal = parseInt((Math.sqrt(8 * n + 5) - 1) / 2);
    n -= diagonal * (diagonal + 1) / 2;
    var ny = parseInt(n);
    var nx = diagonal - ny;
    nx = (nx >> 1) ^ -(nx & 1);
    ny = (ny >> 1) ^ -(ny & 1);
    xsum += nx;
    ysum += ny;
    var lat = ysum * 0.00001;
    var lon = xsum * 0.00001
    list.push(new Microsoft.Maps.Location(lat, lon));
  }
  return list;
}

And that’s it.

We have a lot of ideas on what we want to add in the future but we always welcome your feedback on what works well and what else you would like to see. Please let us know via the forum.

Posted in Bing Maps, SDS, Windows Store | Tagged , , | Leave a comment

Cross Platform Development with Bing Maps and PhoneGap

Bing Maps provides a variety of APIs and controls. One of which is the Bing Maps AJAX Control that was originally designed to provide interactive maps for the web but meanwhile also powers the WinJS control in the Bing Maps SDK for Windows Store apps. The Bing Maps AJAX control has a slim core to speed up the initial load of a website, is optimized for performance using HTML5 technologies and implements a modular concept that allows the loading of additional modules on-demand. Official modules include the search for locations and business listings, driving directions, traffic overlays, venue maps and more. Our friends in the developer community have picked up this modular concept and developed further modules which extend the AJAX control. An Interactive SDK makes it very simple to become familiar with the control and implement your first maps within minutes.

Another big point for the AJAX control is, that it cannot only be used for websites but also in the context of PhoneGap / Apache Cordova. PhoneGap enables JavaScript developers to build native applications for a number of platforms including Android, iOS and Windows Phone. Getting Started Guides help you over the first hurdles for all supported platforms. No matter if you are used to Visual Studio, Blend, Eclipse or Xcode, if you are familiar with HTML, JavaScript and CSS, you can get your first mobile applications of the ground very quickly.

image

Once you have your app running on one platform it is a walk in the park to move it over to other platform. You don’t need to be an expert on Objective C, Java and .NET to support Windows Phone, Android and iOS. You can simply write your application in HTML, JavaScript and CSS and let PhoneGap / Apache Cordova handle the rest.

Getting Started

Let’s put that concept to a test. We start with the projects from the Getting Started Guides for PhoneGap as well as a simple web application that features Bing Maps and allows us to search for locations and business listings. Below you find the HTML-document.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" type="text/css" href="css/index.css" />
    <script 
       type="text/javascript" 
       src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0">
    </script>        
    <script type="text/javascript" src="js/index.js"></script>
    <title>Bing Maps</title>
  </head>
  <body>
  <div id='divSearch' 
       style="position:absolute; top:5px; left:0px; right:0px; height:50px; 
       background-color:White; ">
    <input id="txtSearch" type="text" class="searchBox" />
    <img id="btnSearch" src="./img/search.png" alt="" 
         style="position:absolute; top:0px; right:0px; 
         cursor:pointer" onclick="LoadSearchModule()" />
  </div>

  <div id="divMap" style="width:100%; height:92%; 
       position:absolute; left:0px; top:55px;"></div>
  </body>
</html>

And here is the JavaScript-file that loads the map, applies some themes for controls and pushpins, handles the search for business listings and displays pushpins as well as info-boxes when you click on such a pushpin. You can find all the pieces for this sample in the above mentioned interactive SDK.

window.onload = GetMap;

var map = null;
var searchManager = null;
var currInfobox = null;

function GetMap(){
    Microsoft.Maps.loadModule('Microsoft.Maps.Themes.BingTheme', { callback: function() 
        {
            map = new Microsoft.Maps.Map(document.getElementById('divMap'), 
            { 
                credentials: "Your Bing Maps Key",
                mapTypeId: Microsoft.Maps.MapTypeId.road,
                enableClickableLogo: false,
                enableSearchLogo: false,
                center: new Microsoft.Maps.Location(47.603561, -122.329437),
                zoom: 10,
                theme: new Microsoft.Maps.Themes.BingTheme()
            }); 
        }
     });
}

function createSearchManager() {
    map.addComponent('searchManager', new Microsoft.Maps.Search.SearchManager(map));
    searchManager = map.getComponent('searchManager');
}

function LoadSearchModule() {
    Microsoft.Maps.loadModule('Microsoft.Maps.Search', { callback: searchRequest })
}

function searchRequest() {
    createSearchManager();
    var query = document.getElementById('txtSearch').value;
    var request =
        {
            query: query,
            count: 20,
            startIndex: 0,
            bounds: map.getBounds(),
            callback: search_onSearchSuccess,
            errorCallback: search_onSearchFailure
        };
    searchManager.search(request);
}

function search_onSearchSuccess(result, userData) {
    map.entities.clear();
    var searchResults = result && result.searchResults;
    if (searchResults) {
        for (var i = 0; i < searchResults.length; i++) {
            search_createMapPin(searchResults[i]);
        }
        if (result.searchRegion && result.searchRegion.mapBounds) {
            map.setView({ bounds: result.searchRegion.mapBounds.locationRect });
        }
        else {
            alert('No results');
        }
    }
}

function search_createMapPin(result) {
    if (result) {
        var pin = new Microsoft.Maps.Pushpin(result.location, null);
        Microsoft.Maps.Events.addHandler(pin, 'click', function () { search_showInfoBox(result) });
        map.entities.push(pin);
    }
}

function search_showInfoBox(result) {
    if (currInfobox) {
        currInfobox.setOptions({ visible: true });
        map.entities.remove(currInfobox);
    }
    currInfobox = new Microsoft.Maps.Infobox(
        result.location,
        {
            title: result.name,
            description: [result.address, result.city, result.state, 
              result.country, result.phone].join(' '),
            showPointer: true,
            titleAction: null,
            titleClickHandler: null
        });
    currInfobox.setOptions({ visible: true });
    map.entities.push(currInfobox);
}

function search_onSearchFailure(result, userData) {
    alert('Search failed');
}

Building the first Mobile App

Let’s create a Windows Phone application from this website. If you followed the Getting Started Guides for PhoneGap you have a project structure with a folder “www” under which the HTML-document, the styles, images and JavaScripts are organized.

image

In the head of index.html we need to introduce 2 additional meta-tags to prevent the browser from capturing pinch-to-zoom events and another one that disables format-detection for phone-numbers. Next we need to reference the main PhoneGap / Apache Cordova script.

<head><meta name="format-detection" content="telephone=no" />
  <meta name="viewport" 
        content="user-scalable=no, initial-scale=1, 
                 maximum-scale=1, minimum-scale=1, 
                 width=device-width, height=device-height, 
                 target-densitydpi=device-dpi" /><script type="text/javascript" src="cordova.js"></script></head>

We also need to verify if the path to the images and style sheets is correct.

In the JavaScript we only need to replace a function that fires in the web-browser when the window is loaded with one that PhoneGap has implemented to detect when the app is launched ready to interpret the JavaScript code. We simply search for…

window.onload = GetMap;

…and replace it with

document.addEventListener('deviceready', GetMap, false);

And that is really already it. Run your code in the device emulator or a physical device to check it out.

Porting the app to other platforms

There can certainly be a bit of effort in applying typical styles for the different platforms but if we just look at a simple user interface and the application logic itself the porting from Windows Phone to iOS or Android is literally just copy and paste. PhoneGap will take care of the device-specific impplementations. We can copy the HTML and JavaScript documents from the Windows Phone project to the Android…

image

…or iOS project…

image

Et voila we’re done.

image

Posted in AJAX, Bing Maps, PhoneGap, Windows Phone | Tagged , , , , , | Leave a comment