Quantcast
Channel: Maps Blog
Viewing all 102 articles
Browse latest View live

How to Share Maps Using the Search Charm in Windows Store Apps

$
0
0

Depending on the type of application you are creating, you may find it useful to be able to share a map with someone. Whether it’s a map of a single location or a map with a route on it, this can easily be accomplished in a Windows Store app. In this blog post we are going to take a look at two different ways you can share a map in a Windows Store app.

Sharing things is easy in Windows Store apps when using the Share charm. We can easily specify text or HTML that we want to share inside a message. There are two ways we can do share a map. The first is to use the Bing Maps REST Imagery service and generate an image of a map. The second method is to make use of the Bing Maps consumer site (http://bing.com/maps) and build a URL.

Creating a Map Image

The Bing Maps REST Imagery service makes it easy to generate an image of map. There are several different options and parameters that can be used to generate static map images. Here is one of the most basic structures for generating a static map image.

http://dev.virtualearth.net/REST/v1/Imagery/Map/[ImagerySet]/[CenterPoint]/[ZoomLevel]?[ImageOptionParameters]&key=[BingMapsKey]

The ImagerySetparameter can have one of the following values:

clip_image002

clip_image004

clip_image006

Aerial

AerialWithLabels

Road

clip_image008

clip_image010

OrdnanceSurvey (UK only)

CollinsBart (London UK only)

The CenterPoint value of the URL is used to center the map over a specific coordinate. This consists of the latitude and longitude values being separated by a comma. The ZoomLevelis an integer value between 1 and 19 that indicates the level at which the map image should zoom. The following are some of the image option parameters that can be used with the Imagery API.

Parameter

Description

declutterPins

Specifies whether to change the display of overlapping pushpins so that they display separately on a map. (Optional)

1: Declutter pushpin icons.

0: Do not declutter pushpin icons. (default)

format

The image format to use for the static map. By default the best file format for a particular map style is used. (Optional)

gif: GIF image format.

jpeg: JPEG image format. JPEG format is the default for Road, Aerial and AerialWithLabels imagery.

png: PNG image format. PNG is the default format for CollinsBart and OrdnanceSurvey imagery.

mapArea

The geographic area to display on the map. Required when a center point or set of route points are not specified. This is a comma separated set of numbers using the following format:

“South Latitude,West Longitude,North Latitude,East Longitude”

mapLayer

A display layer that renders on top of the imagery set. The only value for this parameter is TrafficFlow. (Optional)

Example: mapLayer=TrafficFlow

mapSize

A string that contains a width and a height separated by a comma. The width must be between 80 and 900 pixels and the height must be between 80 and 834 pixels. The default map size for static maps is 350 pixels by 350 pixels. (Optional)

pushpin

A series of values that include a Point value (latitude and longitude) with options to add a label of up to three (3) characters and to specify an icon style. You can specify up to 18 pushpins within a URL and 100 if you use the HTTP POST method and specify the pushpins in the body of the request. (Optional)

A short form version of this parameter is available: pp

There are many other parameters available that can be used to add routes as well. You can find full documentation on Bing Maps REST Imagery serviceon MSDN.

One nice thing about REST services is that they are very easy to test. Once you have created a REST request URL you can simply put it in the address bar of a browser to see the response.

Using this information we can create the following URL that opens an image of Bing Maps with the map centered on a specific location (Seattle - 47.60356,-122.32943) with a zoom level of 12, and the map view set to the road map view:

http://dev.virtualearth.net/REST/V1/Imagery/Map/Road/47.60356,-122.32943/12?key=YOUR_BING_MAPS_KEY

Here is the generated image:

clip_image011

Creating a URL to Bing Maps

The Bing Maps consumer site exposes a number of URL query parameters which can be used to load a custom map view. These parameters are very similar to the Bing Maps REST Imagery service. You can customize the URL to display specific search results, driving directions, or items in your “My Places” folder.

To create a URL to the Bing Maps consumer site, start with the base URL and then use parameters to specify the location and options such as zoom level, map view, search panels, and more. The base address you should use is http://bing.com/maps/default.aspx. The following is a list of some of the basic parameters you can use:

Parameter

Description

cp

Defines where the center of the map should be. Use the following format for the cp parameter: Latitude~Longitude

lvl

Defines the zoom level of the map. Valid values are 1-19. This parameter is ignored if you don't include the cp parameter.

style

Defines the map view. Valid values for this parameter include:

a: Display an aerial view of the map.

r: Display a road view of the map.

h: Display an aerial view of the map with labels.

o: Use this value to display a bird's eye (oblique) view of the map.

b: Display a bird's eye (oblique) with labels view of the map.

u: Automatic

trfc

Specifies whether traffic information is included on the map. Omitting the trfc parameter produces the same results as trfc=0.

You can find full documentation on creating URLs for the Bing Maps consumer siteon the Bing Help website.

Using this information we can create the following URL that opens Bing Maps with the map centered on a specific location (Seattle - 47.60356,-122.32943) with a zoom level of 12, and the map view set to the road map view: http://bing.com/maps/default.aspx?cp=47.60356~-122.32943&lvl=12&style=r

Opening this URL in a browser will load up the Bing Maps consumer site centered over Seattle.

clip_image013

Sharing from a Windows Store App

Now that we understand the basics of generating a static map image and a link to the Bing Maps consumer site, we can use this to create a Windows Store app that allows us to share this information through the Share charm. For this app we will create a simple application that uses the Bing Maps Windows Store control. When the Share charm is activated it will use the center point and zoom level of the map to generate the map image and URL to the Bing Maps consumer site. In addition to this, we will also add a button to the app that will launch the share charm. You may find this useful in the future if you have a list of results and you want to let the user select a single result and share it all with a single touch.

To get started let’s open up Visual Studios and create a new project in your preferred language: JavaScript, C# or Visual Basic. Select the Blank App template and call the application BingMapsShareCharmand press OK.

clip_image015

Add a reference to the Bing Maps SDK. To do this, right click on the References folder and press Add Reference. Select Windows -> Extensions select Bing Maps for C#, C++ and Visual Basic or Bing Maps for JavaScript. If you do not see this option ensure that you have installed the Bing Maps SDK for Windows Store apps. While you are here, if using C# or Visual Basic, add a reference to the Microsoft Visual C++ Runtime Packageas this is required by the Bing Maps SDK.

clip_image017

If you are using C# or Visual Basic you may notice that there is a little yellow indicator on the references that you just added. The reason for this is that in the C++ runtime package you have to set the Active solution platform in Visual Studio to one of the following options; ARM, x86 or x64. To do this, right click on the Solution folder and select Properties. Then go to Configuration Properties -> Configuration. Find your project and under the Platformcolumn set the target platform. For this blog post I’m going to select x86. Press Ok and the yellow indicator should disappear from our references.

clip_image019

If you are using JavaScript right click on the js folder and select Add -> New Item. Create a new JavaScript file called ShareMap.js. We will put all our JavaScript for this application in there to keep things clean. Now open up the default.htmlfile and update the HTML to the following:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>BingMapsShareCharm</title>

<!-- WinJS references -->
<link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.1.0/js/base.js"></script>
<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

<!-- BingMapsShareCharm references -->
<link href="http://www.bing.com/css/default.css" rel="stylesheet" />
<script src="http://www.bing.com/js/default.js"></script>

<!-- Bing Map Control references -->
<script type="text/javascript" src="ms-appx:///Bing.Maps.JavaScript//js/veapicore.js"></script>

<!-- Our Bing Maps JavaScript Code -->
<script src="http://www.bing.com/js/ShareMap.js"></script>
</head>
<body>
<div id="MyMap"></div>
<button id="ShareBtn" style="position:absolute;top:0;left:400px;background-color:#000;">Share Location</button>
</body>
</html>

If you are using C# or Visual Basic open the MainPage.xamlfile and add update the xaml to the following:

<Page
x:Class="BingMapsShareCharm.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:BingMapsShareCharm"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:Bing.Maps"
mc:Ignorable="d">

<GridBackground="{StaticResource ApplicationPageBackgroundThemeBrush}">
<m:MapName="MyMap"Credentials="YOUR_BING_MAPS_KEY"/>

<StackPanelMargin="10"Height="40"VerticalAlignment="Top"HorizontalAlignment="Center"Background="Black">
<ButtonContent="Share Location"Tapped="ShareLocation_Tapped"/>
</StackPanel>
</Grid>
</Page>

Be sure to add your Bing Maps key into the XAML.

Next we can add the functionality to power the application. Regardless of the language you are using you will have to do the following tasks:

1. Get a reference to the Share charm for the current view using the DataTransferManager.

2. Add an event handler when the user navigates to the app that is fired when data is requests by the Share charm. Remove this event handler when the user leaves the app.

3. Since we are using Bing Maps generate a session key from the map after the map has loaded. A session key is a special Bing Maps key that marks requests to the REST services as non-billable.

4. When a custom share button is tapped show the UI for the Share charm.

5. Create an event handler for when the Share charm is launched. To share an image through the share charm, generate some HTML with the layout you prefer. Add an img tag for where you want the image to go and provide it with a temporary URL. You can then load the image as a stream to the request and specify the temporary URL the image is meant for. In the HTML you can also add an anchor tag that links to the Bing Maps consumer site with the same map, but interactive.

If you are using JavaScript you have one more task and that is to load the Bing Maps control.

If you are using C# open the MainPage.xaml.csfile and update the code to the following:

using System;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage.Streams;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Navigation;

namespace BingMapsShareCharm
{
publicsealedpartialclass MainPage : Page
{
/// <summary>
/// A reference to the DataTransferManager for the Share charm.
/// </summary>
private DataTransferManager dataTransferManager;

privatestring sessionKey;

public MainPage()
{
this.InitializeComponent();

//Get the DataTransferManager object associated with current window
dataTransferManager = DataTransferManager.GetForCurrentView();

MyMap.Loaded += MyMap_Loaded;
}

private async void MyMap_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
sessionKey = await MyMap.GetSessionIdAsync();
}

protectedoverridevoid OnNavigatedTo(NavigationEventArgs e)
{
//Register the share handler event
dataTransferManager.DataRequested += ShareHandler;
}

protectedoverridevoid OnNavigatedFrom(NavigationEventArgs e)
{
//Unregister the share handler event
dataTransferManager.DataRequested -= ShareHandler;
}

privatevoid ShareLocation_Tapped(object sender, TappedRoutedEventArgs e)
{
//Show the charm bar with Share option opened
DataTransferManager.ShowShareUI();
}

privatevoid ShareHandler(DataTransferManager sender, DataRequestedEventArgs e)
{
//A temporary image URL that is used for referencing the created map image.
string localImage = "ms-appx:///images/map.png";

//Use the Bing Maps REST Services to retrieve a static map image of the selected location.
string mapImageUrl = string.Format("http://dev.virtualearth.net/REST/v1/Imagery/Map/Road/{0:N5},{1:N5}/{2}?mapSize=400,300&key={3}",
MyMap.Center.Latitude, MyMap.Center.Longitude, Math.Round(MyMap.ZoomLevel), sessionKey);

//Create URL to the Bing Maps consumer site
string bingMapsUrl = string.Format("http://bing.com/maps/default.aspx?v=2&cp={0:N5}~{1:N5}&lvl={2}",
MyMap.Center.Latitude, MyMap.Center.Longitude, Math.Round(MyMap.ZoomLevel));

//Handle the Share Charm request and insert HTML content.
DataRequest request = e.Request;
request.Data.Properties.Title = "Share Map";
request.Data.Properties.Description = "Share an image of the current map.";

request.Data.SetHtmlFormat(HtmlFormatHelper.CreateHtmlFormat(
string.Format("Map of: {0:N5}, {1:N5}<br/><br/><img src='{2}'/><br/><a href='{3}'>View on Bing Maps</a>",
MyMap.Center.Latitude, MyMap.Center.Longitude, localImage, bingMapsUrl)));

//Load the map image into the email as a stream.
RandomAccessStreamReference streamRef = RandomAccessStreamReference.CreateFromUri(new Uri(mapImageUrl));
request.Data.ResourceMap[localImage] = streamRef;
}
}
}

If you are using Visual Basic open the MainPage.xaml.vbfile and update the code to the following:

Imports Windows.ApplicationModel.DataTransfer
Imports Windows.Storage.Streams
Imports Windows.UI.Xaml.Controls
Imports Windows.UI.Xaml.Input
Imports Windows.UI.Xaml.Navigation

Partial PublicNotInheritableClass MainPage
Inherits Page
''' <summary>
''' A reference to the DataTransferManager for the Share charm.
''' </summary>
Private dataTransferManager As DataTransferManager

Private sessionKey AsString

PublicSubNew()
Me.InitializeComponent()

'Get the DataTransferManager object associated with current window
dataTransferManager = dataTransferManager.GetForCurrentView()

AddHandler MyMap.Loaded, AddressOf MyMap_Loaded
EndSub

Protected Async Sub MyMap_Loaded(sender AsObject, e As Windows.UI.Xaml.RoutedEventArgs)
sessionKey = Await MyMap.GetSessionIdAsync()
EndSub

ProtectedOverridesSub OnNavigatedTo(e As NavigationEventArgs)
'Register the share handler event
AddHandler dataTransferManager.DataRequested, AddressOf ShareHandler
EndSub

ProtectedOverridesSub OnNavigatedFrom(e As NavigationEventArgs)
'Unregister the share handler event
RemoveHandler dataTransferManager.DataRequested, AddressOf ShareHandler
EndSub

PrivateSub ShareLocation_Tapped(sender AsObject, e As TappedRoutedEventArgs)
'Show the charm bar with Share option opened
dataTransferManager.ShowShareUI()
EndSub

PrivateSub ShareHandler(sender As DataTransferManager, e As DataRequestedEventArgs)
'A temporary image URL that is used for referencing the created map image.
Dim localImage AsString = "ms-appx:///images/map.png"

'Use the Bing Maps REST Services to retrieve a static map image of the selected location.
Dim mapImageUrl AsString = String.Format("http://dev.virtualearth.net/REST/v1/Imagery/Map/Road/{0:N5},{1:N5}/{2}?mapSize=400,300&key={3}", MyMap.Center.Latitude, MyMap.Center.Longitude, Math.Round(MyMap.ZoomLevel), sessionKey)

'Create URL to the Bing Maps consumer site
Dim bingMapsUrl AsString = String.Format("http://bing.com/maps/default.aspx?v=2&cp={0:N5}~{1:N5}&lvl={2}",
MyMap.Center.Latitude, MyMap.Center.Longitude, Math.Round(MyMap.ZoomLevel))

'Handle the Share Charm request and insert HTML content.
Dim request As DataRequest = e.Request
request.Data.Properties.Title = "Share Map"
request.Data.Properties.Description = "Share an image of the current map."

request.Data.SetHtmlFormat(HtmlFormatHelper.CreateHtmlFormat(String.Format("Map of: {0:N5}, {1:N5}<br/><br/><img src='{2}'/><br/><a href='{3}'>View on Bing Maps</a>",
MyMap.Center.Latitude, MyMap.Center.Longitude, localImage, bingMapsUrl)))

'Load the map image into the email as a stream.
Dim streamRef As RandomAccessStreamReference = RandomAccessStreamReference.CreateFromUri(New Uri(mapImageUrl))
request.Data.ResourceMap(localImage) = streamRef
EndSub
End Class

If you are using JavaScript open the ShareMap.jsfile and update the code to the following:

(function () {
var app = WinJS.Application;
var map, sessionKey, dataTransferManager;

app.onready = function (args) {
//Get the DataTransferManager object associated with current window
dataTransferManager = Windows.ApplicationModel.DataTransfer.DataTransferManager.getForCurrentView();

//Register the share handler event
dataTransferManager.addEventListener("datarequested", ShareHandler);
};

app.onunload = function (args) {
//Unregister the share handler event
dataTransferManager.removeEventListener("datarequested", ShareHandler);
};

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

document.getElementById("ShareBtn").addEventListener("click", function (e) {
Windows.ApplicationModel.DataTransfer.DataTransferManager.showShareUI();
}, true);
}

function GetMap() {
map = new Microsoft.Maps.Map(document.getElementById("MyMap"), {
credentials: "YOUR_BING_MAPS_KEY"
});

map.getCredentials(function(c){
sessionKey = c;
});
}

function ShareHandler(e){
var mapCenter = map.getCenter();
var lat = Math.round(mapCenter.latitude * 100000) / 100000;
var lon = Math.round(mapCenter.longitude * 100000) / 100000;

//A temporary image URL that is used for referencing the created map image.
var localImage = "ms-appx:///images/map.png";

//Use the Bing Maps REST Services to retrieve a static map image.
var mapImageUrl = 'http://dev.virtualearth.net/REST/v1/Imagery/Map/Road/' + lat + ',' + lon + '/' + map.getZoom() + '?mapSize=400,300&key=' + sessionKey;

//Create URL to the Bing Maps consumer site
var bingMapsUrl = 'http://bing.com/maps/default.aspx?v=2&cp=' + lat + '~' + lon + '&lvl=' + map.getZoom();

//Handle the Share Charm request and insert HTML content.
var request = e.request;
request.data.properties.title = 'Share Map';
request.data.properties.description = 'Share an image of the current map.';

var html = "Map of: " + lat + ", " + lon + "<br/><br/><img src='" + localImage + "'/><br/><a href='" + + "'>View on Bing Maps</a>";
request.data.setHtmlFormat(Windows.ApplicationModel.DataTransfer.HtmlFormatHelper.createHtmlFormat(html));

//Load the map image into the email as a stream.
var streamRef = Windows.Storage.Streams.RandomAccessStreamReference.createFromUri(new Windows.Foundation.Uri(mapImageUrl));
request.data.resourceMap[localImage] = streamRef;
}

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

Be sure to add your Bing Maps key into the GetMapmethod.

Now run your application, and zoom in to a location. Press the custom share button you created or the Share charm button and select the Mail option. You should end up with an email with a map in it. Here is a screenshot of the app:

clip_image021

I have made the full source code for all the languages available on the visual Studio galleries here.

- Ricky Brundritt, EMEA Bing Maps Technology Solution Professional


Cross Platform Development with Bing Maps and PhoneGap

$
0
0

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 off the ground very quickly.

CrossPlatformDevtScreenshot1

 

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 will find the HTML-document.

<!DOCTYPEhtml><html><head><metacharset="utf-8" /><linkrel="stylesheet"type="text/css"href="css/index.css" /> <scripttype="text/javascript"    src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>        <scripttype="text/javascript"src="js/index.js"></script> <title>Bing Maps</title></head><body><divid='divSearch' style="position:absolute; top:5px; left:0px; right:0px; height:50px; background-color:White; "><inputid="txtSearch"type="text"class="searchBox" /> <imgid="btnSearch"src="./img/search.png"alt="" style="position:absolute; top:0px; right:0px;  cursor:pointer"onclick="LoadSearchModule()" /></div>
  <divid="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 along with the infoboxes that appear 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 the web application outlined above. 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.

CrossPlatformDevtSolutionExplorer

In the head of index.html we need to introduce two additional meta-tags: one 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><metaname="format-detection"content="telephone=no" /> <metaname="viewport"         content="user-scalable=no, initial-scale=1,  maximum-scale=1,  minimum-scale=1, 
                  width=device-width,  height=device-height, 
                  target-densitydpi=device-dpi" /><scripttype="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 the following:

window.onload =  GetMap;
…and replace it with
document.addEventListener('deviceready', GetMap, false);

And that’s really 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 required 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 implementations. We can copy the HTML and JavaScript documents from the Windows Phone project to the Android…

CrossPlatformDevtPackageExplorer

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

…or iOS project…

CrossPlatformDevtXcode

Et voila, we’re done.

 

CrossPlatformDevtScreenshot2

Address and Location Capture in Dynamics CRM with Bing Maps

$
0
0

With the Polaris release of Dynamics CRM Online, Bing Maps has been integrated directly into the Process forms for Accounts, Contacts and Leads. The integration is quite basic, however; the existing address details associated with the entity are geocoded when the form is displayed, and the results are used to show a map of the location. No additional data can be overlaid in the map, feedback on the quality of the geocode match are not provided, nor are the geocoded coordinates able to be refined and saved.

In this post, I will demonstrate how we can extend beyond the basic ‘out of the box’ solution using Bing Maps, and also extend it to on-premises CRM deployments as well as online. We will add additional geo-location values to CRM, using the Bing Maps for Enterprise developer APIs to enhance our ability to capture address data quickly and accurately, visualize it on maps, manually adjust latitudes and longitudes with Bing Maps imagery, and use reverse geocoding to find approximate nearby addresses to a given point. This post will provide an additional Bing Maps for Enterprise + Dynamics CRM integration scenario beyond the two previous posts: Heat Maps with Bing Maps and Dynamics CRM and Geocoding Dynamics CRM Data with Bing Maps.

By taking advantage of the address parsing and geocoding capabilities of Bing Maps, we enable CRM users to quickly enter partial or unformatted address data as a single string, and to have Bing Maps parse the address into individual formatted components; and, provide valuable visual and text-based feedback on the location and accuracy of the geocoding match for entities such as Accounts, Contacts and Leads.

For example, if a customer service representative is receiving address information being spoken to them by phone, they can take partial address information such as ‘1 microsoft way, redmond’, and populate their Account entity form with parsed address details:

AddressLocationCaptureAccountInfo

CRM users can also leverage the rich Bing Maps imagery and reverse geocoding capabilities to drag pushpins to specific locations on maps and imagery to capture their latitude and longitude, and provide approximate nearby address-based contextual information for entities such as Cases. For example, a municipality can use Bing Maps imagery to derive latitude and longitude information, and the approximate nearby address information for reports such as graffiti or potholes in 311 scenarios:

AddressLocationCaptureAccountInfo2

We will use Dynamics CRM Online, Bing Maps AJAX v7 control, and the Bing Maps REST Locations API in this post. Our final result will be a web resource that can be embedded in either a ‘Classic’ or ‘Updated’ entity form:

AddressLocationCaptureAccountInfo3

Full code for the web resource, usable in both ‘Classic’ and ‘Updated’ forms, can be found here.

Creating our Address and Location Capture Web Resource

We will present our location capture utility in a Dynamics CRM Online entity form, through the use of an HTML Web Resource.

When creating our HTML Web Resource, we must use the appropriate DOCTYPE declaration, add a META element with the charset attribute set to "utf-8", and include the Bing Maps AJAX v7 map control (to help us work with ‘Updated’ forms, we will also include jQuery).

The HTML for our page will be minimal. The map will occupy all available area, and will include a controls panel which will allow us to enter addresses to geocode, or coordinates to view. When the body has loaded, we will callour GetMap function, to instantiate our map:

<!DOCTYPEhtml PUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html><head><title>Address and Location  Utility</title><metahttp-equiv="Content-Type"content="text/html; charset=utf-8"/><scripttype="text/javascript"src="https://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0&amp;s=1"></script><!-- We include JQuery from the  Microsoft Ajax CDN to assist with our 'Updated' forms attribute values access  --> <scripttype="text/javascript"src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.2.min.js"></script> <bodyonload="GetMap();"><divid='mapDiv'style="position:absolute; width:100%; height:100%;"></div><divid="controls"><divclass="input"><h2>Geocode Tools</h2></div><divclass="input"><b>Geocode Address:</b></div><divclass="input"><inputid="txtWhere"type="text" size="20" /></div><divclass="input"><inputid="GeocodeButton"type="button"value="Go"
onclick="geocodeAddress()"class="button" /></div><divclass="input"><b>Show location:</b></div><divclass="input"><i>Latitude:</i></div><divclass="input"><inputid="txtLat"type="text"size="20" /></div><divclass="input"><i>Longitude:</i></div><divclass="input"><inputid="txtLon"type="text"size="20" /></div> <divclass="input"><inputid="LatLonButton"type="button"value="Go"
onclick="showLatLon()"class="button" /></div></div>   
</body></html>

In our GetMap function, we will instantiate our map, and extend the Microsoft.Maps.Pushpin class to allow us to associate geocoding metadata to pushpins we display. We also determine the form type, so we know whether we are using a Classic or Updated form. If using an Updated form, we will incorporate some alternate logic to update our entity attributes in the form. Note the placeholder for your Bing Maps Key. To obtain a Bing Maps Key, please see here.

// Initiate map:function GetMap() {
    map = new Microsoft.Maps.Map(document.getElementById("mapDiv"), {
        credentials: "INSERT_KEY_HERE",
        zoom: 3, center: new Microsoft.Maps.Location(40.347, -94.218),
        mapTypeId: Microsoft.Maps.MapTypeId.birdseye
    });// extend pushpin class to hold geocoding data:
    Microsoft.Maps.Pushpin.prototype.geoname = null;
    Microsoft.Maps.Pushpin.prototype.geoentitytype = null;
    Microsoft.Maps.Pushpin.prototype.geoaddress = null;
    Microsoft.Maps.Pushpin.prototype.geoconfidence = null;
    Microsoft.Maps.Pushpin.prototype.geomatchcodes = null;
    Microsoft.Maps.Pushpin.prototype.geocalculationmethod = null;
    Microsoft.Maps.Pushpin.prototype.geopindragged = null;
    Microsoft.Maps.Pushpin.prototype.geoaddressrevgeo = null;   // Get CRM Form Type, to choose behavior based on 
    //  Updated or Classic forms:
    crmFormType = parent.Xrm.Page.ui.getFormType();

}

When a user enters a text-based address or location and submits, we will call our geocodeAddress function, which will clear our map, extract the credential from our session, and use them to issue a request to the REST Locations API, Find a Location by Query. We specify geocodeServiceCallback as our callback function:

// clear map and retrieve credentials for REST Locations request:function geocodeAddress() {
    map.entities.clear();
    map.getCredentials(callGeocodeService);
}// call geocoding service with user-entered location details:function callGeocodeService(credentials) {var searchRequest = 'https://dev.virtualearth.net/REST/v1/Locations/' +
        document.getElementById("txtWhere").value + '?output=json&jsonp=geocodeServiceCallback&key=' + credentials;var mapscript = document.createElement('script');
    mapscript.type = 'text/javascript';
    mapscript.src = searchRequest;
    document.getElementById('mapDiv').appendChild(mapscript);
}

In our callback function, we confirm that we have received geocoding results, and create a pushpin object for each result. We will also add the relevant geocoding metadata and formatted address details to each pushpin, so we will have it available for viewing and populating our form as needed. If we have a unique result, we will set the map view based on the bounding box returned with that geocoding result. If we have multiple results, we will auto-scale the map to show all results. Finally, we open an infobox showing the geocoding metadata for our top result:

// Callback for REST Locations request:function geocodeServiceCallback(result) {if (result &&
    result.resourceSets &&
    result.resourceSets.length > 0 &&
    result.resourceSets[0].resources &&
    result.resourceSets[0].resources.length > 0) {var results = result.resourceSets[0].resources;var locationArray = new Array(); for (var j = 0; j < results.length; j++) {var location = new Microsoft.Maps.Location(results[j].point.coordinates[0],
                results[j].point.coordinates[1]);var pushpin = createPushpin(location, (j + 1).toString()); // get calculation method: var calculationMethod = getCalcMethod(results[j]);// Add geocoding metadata to pin:
            pushpin.geoname = results[j].name;
            pushpin.geoentitytype = results[j].entityType;
            pushpin.geoaddress = results[j].address;
            pushpin.geoconfidence = results[j].confidence;
            pushpin.geomatchcodes = results[j].matchCodes;
            pushpin.geocalculationmethod = calculationMethod;
            pushpin.geopindragged = false;
            pushpin.geoaddressrevgeo = false;// Add pin to map:
            map.entities.push(pushpin);  // Add location to array for map auto-scaling:
            locationArray.push(location);
        }// Set view depending on whether result is unique or not: if (results.length == 1) {var bbox = results[0].bbox;var viewBoundaries = Microsoft.Maps.LocationRect.fromCorners(new Microsoft.Maps.Location(bbox[0], bbox[1]),new Microsoft.Maps.Location(bbox[2], bbox[3]));
            map.setView({ bounds: viewBoundaries });
        } else {// Show a best view for all locationsvar viewBoundaries = Microsoft.Maps.LocationRect.fromLocations(locationArray);
            map.setView({ bounds: viewBoundaries, padding: 75 });
        }// Open infobox for top result:
        showInfoBox(map.entities.get(0));
    } else { if (result && result.errorDetails) {
            alert("Message :" + response.errorDetails[0]);
        }
        alert("No results for the query");
    }
}

This allows users to manually enter a latitude and longitude to display a pushpin at that location. Ensure the latitude and longitude entered are valid, and then create a pushpin and display its infobox:

// Take user-entered coordinates, and add pushpin in that location:function showLatLon() {// Clear existing entities:
    map.entities.clear();// get user input coordinatesvar lat = document.getElementById("txtLat").value;var lon = document.getElementById("txtLon").value;// Validate coordinatesif (isLatLonValid(lat, lon) === false) {
        alert('Please enter valid WGS84 decimal values for Latitude and Longitude.');return false;
    }// Display location with pushpin:var latlonLocation = new Microsoft.Maps.Location(lat, lon);
    map.setView({ zoom: 12, center: latlonLocation });var pushpin = createPushpin(latlonLocation, "1");
    pushpin.geoname = "Manually Entered Location";// Add pushpin to map, and open infobox:
    map.entities.push(pushpin);
    showInfoBox(pushpin);

}

When we create any of our pushpins, we need to ensure they are draggable so the user can move the pushpin to refine the location manually. We also add event handlers which enable us to remind the user that the pin has been dragged, and to open the infobox when the pushpin is clicked:

// Create pushpin with appropriate  options and event handlers: function createPushpin(location, label) {var pushpin = new  Microsoft.Maps.Pushpin(location, { draggable: true, text:  label 
});var pushpinclick =  Microsoft.Maps.Events.addHandler(pushpin, 'click',  
pinClickHandler);var pushpindragend =  Microsoft.Maps.Events.addHandler(pushpin, 'drag',  
dragHandler);return pushpin;
} 

If users add a pushpin to the map based on a latitude and longitude, or drag any pushpin to a new location, we will give them the ability to capture approximate nearby address information for that location. Our callReverseGeocodeService function will receive the session credentials, and extract the current coordinates from the associated pushpin. These coordinates are used to issue a request to the REST Locations API, Find a Location by Point. We specify revgeoServiceCallback as our callback function:

// call reverse-geocoding service with current pushpin location:function callReverseGeocodeService(credentials) {// Get pushpin location:var pinLocation = revgeoPushpin.getLocation();var searchRequest = 'https://dev.virtualearth.net/REST/v1/Locations/' + pinLocation.latitude + "," + pinLocation.longitude + '?output=json&jsonp=revgeoServiceCallback&key=' + credentials;var mapscript = document.createElement('script');
    mapscript.type = 'text/javascript';
    mapscript.src = searchRequest;
    document.getElementById('mapDiv').appendChild(mapscript);
}

In our callback function, we confirm that we have received reverse geocoding results, and add the resulting reverse-geocode metadata from the top result to the pushpin, so we will have it available for viewing and saving as needed. Finally, we open an infobox showing the new metadata for our location:

// Callback for REST Locations request:function revgeoServiceCallback(result) {if (result &&
    result.resourceSets &&
    result.resourceSets.length > 0 &&
    result.resourceSets[0].resources &&
    result.resourceSets[0].resources.length > 0) { // Take only first result:var revgeoResult = result.resourceSets[0].resources[0];var location = new Microsoft.Maps.Location(revgeoResult.point.coordinates[0], revgeoResult.point.coordinates[1]);// get calculation method:var calculationMethod = getCalcMethod(revgeoResult);// Add geocoding metadata to appropriate pin:
        revgeoPushpin.geoname = revgeoResult.name;
        revgeoPushpin.geoentitytype = revgeoResult.entityType;
        revgeoPushpin.geoaddress = revgeoResult.address;
        revgeoPushpin.geoconfidence = revgeoResult.confidence;
        revgeoPushpin.geomatchcodes = revgeoResult.matchCodes;
        revgeoPushpin.geocalculationmethod = calculationMethod;
        revgeoPushpin.geoaddressrevgeo = true;// Open infobox for revgeo pin:
        showInfoBox(revgeoPushpin);
    }else {if (result && result.errorDetails) {
            alert("Message :" + response.errorDetails[0]);
        }
        alert("No results for the query");
    }
}

When we show our infoboxes, we retrieve the latitude, longitude, and geocoding metadata from the pushpin, and display this as our infobox content. The metadata we display includes:

  • The Display Name of the found location; e.g. ‘1 Microsoft Way, Redmond, WA 98052’
  • The Entity Type or classification of the found location; e.g. Address, Populated Place, etc.
  • The level of Confidence of our geocoding match; e.g. High, Medium or Low
  • The Match Codes indicating the geocoding level for the match; e.g. Good, Ambiguous, UpHierarchy

We use some logic to size our infobox based on data to be displayed, to optimize the available map area. And finally, we add some Actions to our infobox. Our Actions include:

  • Zoom: Zoom and center the map on this location
  • Reverse Geocode: If the pushpin has been added based on a lat/long, or if a pin has been dragged, find contextual address and location details
  • Populate Form: Add the address details and latitude and longitude of this location to our CRM entity

The Populate Form action is key, as it allows us to take our Bing Maps location data, and quickly add it to our CRM entity, without manually entering data in each form field. Note that within this action, we use alternate methodologies depending on the type of form being used: Classic or Updated. If using an Updated form, we use a workaround to populate the form values. We can revisit the need to do this after the Orion CRM release.

// Show infobox for a specific pushpin:
function showInfoBox(pushpin) {//Hide other infoboxes
    hideInfoBox();// Get pushpin location:var pinLocation = pushpin.getLocation();// Create the info box content for the pushpinvar description = "<div class='pinContent'  style='border=1'>";
if (pushpin.geoentitytype != null) { description += "<div class='pinDetail'><b>Entity type:</b> " + pushpin.geoentitytype + "</div>" };
if (pushpin.geoconfidence != null) { description += "<divclass='pinDetail'><b>Confidence:</b> " + pushpin.geoconfidence + "</div>" };
if (pushpin.geomatchcodes != null) { description += "<div class='pinDetail'><b>Match Codes:</b> " + pushpin.geomatchcodes + "</div>" };if (pushpin.geocalculationmethod != null) { description += "<div class='pinDetail'><b>Calculation Method:</b> " + pushpin.geocalculationmethod + "</div>" }; description += "<div class='pinDetail'><b>Lat:</b> " + pinLocation.latitude + "</div>";
description += "<div class='pinDetail'><b>Lon:</b> " + pinLocation.longitude + "</div>";
if (pushpin.geoaddressrevgeo == true) { description += "<div class='pinDetail'><bstyle='color:red'>Alert: Address from RevGeo</b></div>" };
if (pushpin.geopindragged == true) { description += "<div class='pinDetail'><bstyle='color:red'>Alert: Pin Dragged</b></div>" };
description += "</div>";// Determine infobox height based on specific pieces of content:var infoboxHeight = 180; infoboxHeight += (Math.max(0, (Math.ceil(pushpin.geoname.length / 25) - 1))) * 18;if (pushpin.geopindragged) infoboxHeight += 15; if (pushpin.geoaddressrevgeo) infoboxHeight += 15; infobox = new Microsoft.Maps.Infobox(pinLocation, { title: pushpin.geoname,
description: description, offset: new Microsoft.Maps.Point(7, 25), visible: true, zIndex:
1000, height: infoboxHeight }); infobox.setOptions({ actions: [ { label: "Zoom", eventHandler: function (mouseEvent) {// Zoom to pin location: map.setView({ zoom: 17, center: pinLocation }); } }, { label: "Reverse Geocode", eventHandler: function (mouseEvent) {// initiate reverse geocode: revgeoPushpin = pushpin; map.getCredentials(callReverseGeocodeService); } }, { label: "Populate Form", eventHandler: function (mouseEvent) { // Geocoding metadata will be available as properties of pushpin object // If using 'Classic Forms' (not the Read-Only forms of type 11):if (crmFormType != 11) {var controls = parent.Xrm.Page.ui.controls; controls.get("address1_latitude").getAttribute().setValue(pinLocation.latitude ?
pinLocation.latitude : "");
controls.get("address1_longitude").getAttribute().setValue(pinLocation.longitude ?
pinLocation.longitude : "");
controls.get("address1_line1").getAttribute().setValue(pushpin.geoaddress.addressLine ?
pushpin.geoaddress.addressLine : "");
controls.get("address1_city").getAttribute().setValue(pushpin.geoaddress.locality ?
pushpin.geoaddress.locality : "");
controls.get("address1_stateorprovince").getAttribute().setValue(pushpin.geoaddress.adminDistrict ?
pushpin.geoaddress.adminDistrict : "");
controls.get("address1_postalcode").getAttribute().setValue(pushpin.geoaddress.postalCode ?
pushpin.geoaddress.postalCode : "");
controls.get("address1_country").getAttribute().setValue(pushpin.geoaddress.countryRegion ?
pushpin.geoaddress.countryRegion : ""); } else {// Use workaround to populate address properties in Updated Process forms: // TO-DO: revisit after Orion release: prepAttribute('address1_line1'); prepAttribute('address1_city'); prepAttribute('address1_stateorprovince'); prepAttribute('address1_postalcode'); prepAttribute('address1_country'); prepAttribute('address1_latitude'); prepAttribute('address1_longitude'); setFormValue('address1_line1', (pushpin.geoaddress.addressLine ?
pushpin.geoaddress.addressLine : ""));
                    setFormValue('address1_city', (pushpin.geoaddress.locality ? 
pushpin.geoaddress.locality : ""));
                    setFormValue('address1_stateorprovince', (pushpin.geoaddress.adminDistrict ? 
pushpin.geoaddress.adminDistrict : ""));
                    setFormValue('address1_postalcode', (pushpin.geoaddress.postalCode ? 
pushpin.geoaddress.postalCode : ""));
                    setFormValue('address1_country', (pushpin.geoaddress.countryRegion ? 
pushpin.geoaddress.countryRegion : ""));
                    setFormValue('address1_latitude', (pinLocation.latitude ? 
pinLocation.latitude : ""));
                    setFormValue('address1_longitude', (pinLocation.longitude ? 
pinLocation.longitude : ""));

                }
            }
        }
        ]
    });

    map.entities.push(infobox);
}

*Important Notes on REST Locations API address data:

  • The primary purpose of the forward geocoder is to provide accurate latitude and longitude data for the given input information, and as a result the parsed address data returned in the response is subject to change in terms of exact format and content. Thus, it is important to ensure that you are not relying on specific or consistent formatting of address elements in responses
  • The reverse geocoder will provide an approximation of a nearby address to the given latitude and longitude provided as input, and as a result, your business processes assume that the address provided is precise, or definitively correct

You can download the complete Web Resource code using the link above.

Adding Web Resource to Form

We are now ready to add our Web Resource to the desired form in CRM. The first step is to upload the Web Resource as an HTML Web Resource using CRM’s Settings…Customizations…Web Resources tools. Next, use the Form Editor in the CRM Web Client to edit the form you want to add the address capture capability to. Make sure that the following attributes are visible on the form:

  • address1_line1
  • address1_city
  • address1_stateorprovince
  • address1_postalcode
  • address1_country
  • address1_latitude
  • address1_longitude

Now add your web resource to your form, and if you are planning to use an Updated form, ensure you tick the ‘Show this Web Resource in Read Optimized Form’:

AddressLocationCaptureWebResourceProperties

You are now ready to start capturing address and location details quickly and easily!

Conclusion

Being able to quickly and accurately capture address or location information in this manner has applicability in a number of CRM scenarios, such as:

  • Capturing new customer addresses provided by phone, as quickly and accurately as possible, without manually entering each address field. Even data that the customer may not provide, such as county information or zip code, can potentially be captured
  • Capturing accurate location information for Cases such as reports of potholes, graffiti, and more. CRM users can quickly orient the map to the general location by entering cross-streets, landmarks, or other location information, then drag the pin to capture latitude and longitude information using Bing Maps imagery, and additional information from the user

Bing Maps and Dynamics CRM can help your organization:

  • Spend less time capturing customer data
  • Ensure your addresses are captured correctly
  • Ensure you capture accurate coordinates for your entity data

Now that you have accurate location information for your CRM data, you can start using location-based trends to make smarter business decisions by adding heat maps to Dynamics CRM as well!

 

Geoff Innis
Bing Maps Technical Specialist

Heat Maps in Windows Store Apps (JavaScript)

$
0
0

Heat maps are an effective means of visualizing geography-based trends by showing the relative density of location-based data. We can see where we have ‘hot spots’ in our data, and use this insight to drive better decisions for application users. In this blog post, we will show how you can visualize location data in the form of a heat map within Windows Store apps, pulling in both public points-of-interest data sources available within Bing Maps, as well as custom data sources containing your own data.

We will use the Bing Maps Windows Store JavaScript API and a Heat Map Module to create our visualization, and we will pull in our data via the Bing Maps Spatial Data Query API. The full source code for the application is available in the Visual Studio Samples Gallery.

To run this application, you must install the Bing Maps SDK for Windows Store apps and get a Bing Maps key for Windows Store apps. You must also have Windows 8 and Visual Studio 2012.

Instantiating Our Map

We will create a JavaScript file for our project in which all of our custom JavaScript code for the app will reside. Within this file, we will include initialization logic to populate a dropdown with available data sources, load the Map module and use the loadMap callback to instantiate our map:

//Initialization logic for populating  data source options and loading the map control 
   (function () {function initialize() {
   loadDataSourceOptions(publicdatasources);
   Microsoft.Maps.loadModule('Microsoft.Maps.Map', { callback: loadMap });
   }
    document.addEventListener("DOMContentLoaded", initialize, false);
})(); 

The loadMap callback will include the code to instantiate our map, and to add event handlers to manage the retrieval of the appropriate data from our data source as the user navigates the map. We will also load the heat map module which provides the heat map rendering capabilities:

// Called once app is loaded or map is  reloaded, instantiating the map and initiating the loading of data: function loadMap() {…
   // Load the map: 
   map = new MM.Map(document.getElementById("mapDiv"),
   {
   credentials:  document.getElementById("txtQueryKey").value,
   mapTypeId:  startMaptype,
   zoom: startZoom,
   center: startCenter
 });
…   // Load the Client Side  HeatMap Module
   Microsoft.Maps.loadModule("HeatMapModule", {
   callback: function () {
   }
   });
} 

The heat map module was created by Alastair Aitchison, Microsoft MVP, and is shared on CodePlex as part of the Bing Maps V7 Modules project.

Accessing the Data

We will be drawing on data hosted in data sources within Bing Maps Spatial Data Services. Bing Maps includes Public Data Sources, including:

  • FourthCoffeeSample – Sample data for a fictional coffee chain
  • NavteqNA – Points of Interest for North America, categorized by SIC
  • NavteqEU – Points of Interest for Europe, categorized by SIC
  • Traffic Incident Data Source – Traffic incidents

We will also enable access to private data sources that you can create and populate with the Spatial Data Services Data Source Management API.

As the user navigates the map to a new view, we will dynamically load only the data from the data source that is located within the current map view, for performance reasons. This allows us to work with data sources that contain millions of entities, while still ensuring reasonable performance. We will add a throttled event handler for the viewchangeend event to retrieve the data, and an event handler for the viewchangestart event to cancel any outstanding data retrieval processes if the map is navigated again before they complete:

// Add Event to search for entities when  map moved:
   Microsoft.Maps.Events.addThrottledHandler(map, 'viewchangeend', NavEndHandler, 2000);
    // Add Event to check whether  to cancel the retrieval of entity data if the map is 
moved before data is  returned: 
   Microsoft.Maps.Events.addHandler(map, 'viewchangestart', CheckSearch);

The user is given the option to opt-out of reloading data as the map is navigated, so the NavEventHandler function will confirm that data should be reloaded, and then initiate the search. The InitiateSearch function will remove any existing heat map layer, reset various values, and elements of the UI, and then retrieve the credentials for the map session, to use in a request to the SDS Query API in the ExecuteSearch function:

function NavEndHandler() {// Reload data, unless user  has checked otherwise:if (document.getElementById("chkUpdate").checked) { InitiateSearch(); }
   }
function InitiateSearch() {
    // remove heatmap, if  present:if (heatmapLayer) { removeHeatmapLayer() };
    // reset search results  array, and pagination global variables: 
   searchResultsArray = new Array();
   searchResultsPage = 0;
   document.getElementById("spanZoomWarning").style.display = "none";
   document.getElementById("spanNumEntities").innerHTML = "...";
    // update progress bar:var progressBar = document.getElementById("determinateProgressBar");
   progressBar.value = 0;
    // retrieve map credentials,  and use them to execute a search against the data 
source 
   map.getCredentials(ExecuteSearch);
}

The ExecuteSearch function will obtain the current map view, and determine the bounds of the map for use in a Query by Area (bounding-box) with the Query API. Note that we also include additional query options and parameters, including:

  • $top=250 – this is the maximum number of records we can retrieve in any single request
  • $skip – we use this parameter to retrieve successive batches of results, if there are more than 250
  • $select=Latitude,Longitude – we want to minimize the amount of data transferred, and these two attributes are the minimum required to create our heat map
  • $filter – the UI includes a freeform text input to allow filter text to be specified. This would not typically be exposed in a production-ready app, but it gives us flexibility to easily work with any supported filters
  • $inlinecount=allpages – this will cause the total number of entities that sit within the current map view and that meet our filter criteria to be included in the response. This will allow us to make a decision as to whether to continue requesting all of the data and rendering it on our map, depending on a threshold that the user will set in the UI

The request is executed using the WinJS.xhr function, with completed and error callbacks specified:

function ExecuteSearch(credentials) {
   // get map bounds for  filtering:var bounds = map.getBounds();var south = bounds.getSouth();var north = bounds.getNorth();var east = bounds.getEast();var west = bounds.getWest();
    // Construct query URL: var searchRequest = "http://spatial.virtualearth.net/REST/v1/data/" + 
document.getElementById("txtDSID").value + "/" + 
document.getElementById("txtDSName").value + "/" + document.getElementById("txtEntityName").value;
     // Add filter clauses, if  appropriate: var searchFilter = (document.getElementById("txtFilter").value != "") ? "$filter="
+ document.getElementById("txtFilter").value + "&" : "";
   searchRequest += "?spatialFilter=bbox(" + south + "," + west + "," + north + "," + 
east + ")&" + searchFilter + "$inlinecount=allpages&$select=Latitude,Longitude&$format=JSON&key=" + credentials;
   searchRequest += "&$top=250&$skip=" + (searchResultsPage * 250).toString();
    // Use WinJS to execute  request:
   xhrPromise = WinJS.xhr({ url: searchRequest }).then(SearchCallback,  ErrorCallback);
   searchResultsPage++;
} 

Our SearchCallback function is called when the request completes successfully. This function will make sure that the total number of entities that match our search criteria does not exceed the maximum number the user specifies in the UI. If not, it will add the results to an array, update a progress bar, and then check to see if we still need to retrieve more data. If so, an additional request is issued. If not, the results are turned into an array of Microsoft.Map.Location objects, and the drawHeatmap function is called:

function SearchCallback(result) {
   xhrPromise = null;// Parse results:
   result = JSON.parse(result.responseText);if (result &&
   		result.d &&
   		result.d.results &&
   		result.d.results.length > 0)  {
        if (result.d.__count >  document.getElementById("txtLimit").value) {
            // Show warning:
   	     document.getElementById("spanZoomWarning").style.display = "block";
            // Update span to show total entities:
   	     document.getElementById("spanNumEntities").innerHTML =
result.d.__count.toString();return;
   	}
        // grab search results from  response 
   	  searchResultsArray = searchResultsArray.concat(result.d.results);
        // Update span to show total  entities: 
   	  document.getElementById("spanNumEntities").innerHTML =
 result.d.__count.toString();
         // Update progress bar:var progressBar = document.getElementById("determinateProgressBar");
   	  progressBar.max = result.d.__count;
   	  progressBar.value = searchResultsArray.length;
        // check to see if we need to  retrieve more results: 	  if (result.d.__count >  (searchResultsPage * 250)) {
   	  map.getCredentials(ExecuteSearch);
	 } else {// Process results: 
   	   // grab search results from responsevar locations = result.d.results;
       // Array to use for heatmap:
   	  heatmapLocations = new Array();
       for (var j = 0; j < searchResultsArray.length; j++) {var  location = new  MM.Location(searchResultsArray[j].Latitude, 
searchResultsArray[j].Longitude);
   		heatmapLocations.push(location);
  	    }
       drawHeatmap();
       }
  } else {	// update entities count: 
   	document.getElementById("spanNumEntities").innerHTML = "0";
  }
}

Note that other methodologies of retrieving data for heat mapping would be possible in the cases of custom Bing Maps data sources, or other sources of location data, including batch retrieval of data for local storage and rendering.

In the drawHeatmap function, we remove any existing heat map, and then create a new heat map layer using our array of locations, and using rendering options the user has specified in the UI, which will affect the radius and intensity of the hotspots for each location:

// redraw the heatmap:function drawHeatmap() {// remove heatmap, if  present: if (heatmapLayer) { removeHeatmapLayer() };
   // Construct heatmap layer, using heatmapping JS module: 
   heatmapLayer = new HeatMapLayer(
       map,
       [],
       {
   		intensity: document.getElementById("txtIntensity").value,
   		radius: document.getElementById("txtRadius").value,
   		colourgradient: {
   			0.0: 'blue',
   			0.25: 'green',
   			0.5: 'yellow',
   			0.75: 'orange',
   			1.0: 'red'
   		}
   	});
   	heatmapLayer.SetPoints(heatmapLocations);
}  

Running the Application

If you have downloaded the source code for the application, you can simply open the sample in Visual Studio and insert your Bing Maps key in the default.html file where it says “INSERT_BING_MAPS_KEY_HERE” in the source code. Build the sample by pressing F5. 

Here is a screenshot of the application, showing hotspots of restaurants in Seattle:

DataSourceHeatmap 

Some tips on using the app:

  • You can manually choose from four publicly available data sources (FourthCoffee, NavteqNA, NavteqEU, or TrafficIncidents) or enter your own data source details and Bing Maps key, to reload the map and view your own custom data. You will need to enter a key that has permissions to query private data sources
  • The app uses the current map view to retrieve only those entities in the current view
    • As the map is navigated, the entities in the current map view will be reloaded, unless the ‘Update entities on pan/zoom?’ checkbox is de-selected. De-selecting the checkbox can be useful for analyzing a particular heat map view without the performance hit of reloading all of the data
  • You can add in your own filter text, to apply filters on-the-fly, though you must know the data source schema to use this effectively
  • You can select your own Maximum Entities value, to choose your own cut-off point in the tradeoff between performance and data volume
  • As you navigate the map, you can update the heat map radius and intensity, to best show the current data, based on zoom factor and data concentration
    • If you click the ‘Update Heat’ button, it will update these attributes without reloading all of the data
  • If you change the Maximum Entities or Filter text, and wish to reload the data, or if you have deselected the ‘Update entities on pan/zoom?’ checkbox, you can reload the data on demand with the ‘Reload data’ button

Scenarios

Both consumer- and enterprise-oriented apps can benefit from the visualization of data in this way. Some possible scenarios include the following:

  • Heat maps of restaurants/bars in a city to show where nightlife is concentrated
  • Heat maps showing where specific industry is concentrated, for civic planning, event planning, or site selection
  • Heat maps showing where competing retail chains are concentrated, for business intelligence or site selection
  • Heat maps of your customers, opportunities, or service requests from enterprise data in Dynamics CRM, to drive smarter planning and operational optimization business decisions

While this example application draws on data from Bing Maps public and private data sources, the same concepts can be leveraged to present compelling visualizations of data from SQL Server, SQL Azure, Dynamics, and other sources of location-based data.

Geoff Innis
Bing Maps Technical Specialist

How to Create a Spatial Web Service That Connects a Database to Bing Maps Using EF5

$
0
0

Entity Framework is a quick and easy way to connect a database to your application. It provides a set of tools that allow you to auto generate your classes for your database tables. Also, it provides you with easy-to-use functionality for connecting and querying your data using LINQ. When Entity Framework was originally released, it simplified my life when it came to connecting a database to Bing Maps via a web service. Unfortunately, it didn’t support the spatial functionalities in SQL 2008, so my new found love for Entity Framework quickly came to an end. In May of 2012 a pre-release version of Entity Framework 5 (EF5) was made available which added support for spatial types when using it in .NET 4.5. I quickly tested it out and wrote a blog post titled Entity Framework 5 & Bing Maps WPF. Since writing that blog post, EF5 has been officially released. I haven’t had much time to play with it, but have since noticed that I get regular questions from people on how to connect a database to Bing Maps. So with that in mind, this blog post will show how to create a spatial web service that connects a database to Bing Maps using Entity Framework 5.

Creating a Spatial Database

The first thing we will want to do is create a spatial database using SQL Server 2012, or SQL Azure. To keep things easy, I’m going to use an existing database I have called SaptialSample. You can download the sample database as a SQL script. You could use SQL 2008, but you may run into issues generating the database from the script. This database consists of two tables: Cities and Countries. The cities table has point based data for the locations of over 23,000 cities which have a population of 15,000 or more. The countries table has country boundary information. This database gives us a good mix of simple and complex spatial data types to work with. If you have SQL Server Management studio and you view all the data in the Countries table, you should see something like this:

CreatingaSpatialWebServiceMap1

Note you can change the projection to Mercator so that you can see what the data will look like when overlaid in the same projection as Bing Maps.

To keep things simple, I’m using a local SQL 2012 database. If you don’t have SQL 2012 installed, you can get an express version or get a 3-month trial for Windows Azure.

Setting Up the Visual Studio’s Project

To start off we will create an ASP.NET Empty Web Application project in Visual Studios called BM_EF5_WebService. Be sure to target .NET 4.5.

AddNewProject

Once this is loaded, we will use NuGet to load in EF5 into our project. To do this, you will need to go to Tools –> Library Package Manager –> Package Manager Console.

PackageManagerConsole

This will open up a console panel. You will need to run this command: Install-Package EntityFramework. Additional information can be found here. Doing so will result in the Entity Framework being installed into your application as shown below:

PackageManagerConsole2.png

Create the Spatial Data Model

By now you should have created the database and can move on to creating the entity model. To do this, add a new ADO.NET Entity Data model to the project called SpatialDataModel.edmx.

AddNewItem

On the next screen, we will select Generate from database then press next. Then connect to the sample database. Name the entities SpatialSampleEntities and then press next.

EntityDataModelWizard

On the next screen you will need to select the tables from the database that you want to add to the model. Select both the Cities and the Countries tables. Select the model namespace to be SpatialSampleModel and then press Finish.

EntityDataModelWizard2

Once the model is generated, you should see the designer that shows the table layout. The database consists of two simple tables. The City table has a SQLGeography column called Location which contains the coordinates for a city. The Country table has a SQLGeography column called Boundary which contains the polygon data for the country boundaries.

CountryTable

Creating the Service Data Contracts

Before we jump right into creating the service, we will first create a set of classes which the service will return (data contracts). Since we may at some point want to access the service from .NET code, we will create these classes in a new project. To do this, right click on the Solution and select Add –> New Project. Then create a Windows class library called BM_EF5_WebService.Common.

AddNewProject

Add a reference to the System.Runtime.Serialization to this project. In the project will be a file called class.cs, rename this to ServiceModels.cs. Open this file and add the following classes:

using  System.Collections.Generic;usingSystem.Runtime.Serialization;
namespace  BM_EF5_WebService.Common
{
   [DataContract]
   [KnownType(typeof(City))]
   [KnownType(typeof(Country))]public classBaseEntity
   {

   	[DataMember]public string WKT { get; set; }
       [DataMember]public string Name { get; set; }
       [DataMember(EmitDefaultValue = false)]public double Distance { get; set; }
   }
   [DataContract]public class City : BaseEntity 
   {
   	[DataMember]public string CountryISO { get; set; }
    [DataMember]public int Population { get; set; }
   }
   [DataContract]public class Country : BaseEntity
   {
   	[DataMember]public string ISO { get; set; }
    [DataMember]public int Population { get; set; }
   }
   [DataContract]public class Response 
   {
   	[DataMember]public List<BaseEntity> Results { get; set; }
        [DataMember(EmitDefaultValue = false)] public string Error { get; set; }
   }
} 

This data contract consists of a Response class which contains a list of results and a string property for errors. The results will be either a Country or City class which both derive from a common base class. This base class will have three common properties; Name, WKT and Distance. The WKT property stands for Well Known Text which is a standard way of representing spatial data as a string. The Distance property will only be returned by the service if it has a value. The reason for doing this is that not all spatial searches will have a distance value. By creating this separate class library, we have made it easier for us to access the spatial data from any programming language. It’s also a best practice to not simply return the data objects that are auto generated by the Entity Framework.

Creating the Spatial REST Service

We can now create the spatial REST service. First go back to the main project and add a reference to this newly created common class library. Next right click on the main project and add a new item. Create a WCF Service called SpatialService.svc.

AddNewItem2

This will create three files: SpatialService.svc, SpatialService.svc.cs, and ISpatialService.cs. To start, open the ISpatialService.cs interface class file and add the following to it:

using BM_EF5_WebService.Common;using System.ServiceModel;using System.ServiceModel.Web;namespace BM_EF5_WebService
{
    [ServiceContract(Namespace = "SpatialService")]
    [ServiceKnownType(typeof(Response))]public interfaceISpatialService
    {/// <summary>
        /// Finds all locations that are within a specified distance of a central coordinate
for a specified layer
./// </summary>///<param name="latitude">Center latitude value.</param>///<param name="longitude">Center longitude value.</param>///<param name="radius">Search radius in Meters</param>///<param name="layerName">Name of the layer (SQL table) to search against.</param> /// <returns></returns> [OperationContract] [WebGet(ResponseFormat = WebMessageFormat.Json)]Response FindNearBy(double latitude, double longitude, double radius, string layerName); } }

This interface defines our service and has one method called FindNearBy which returns a Response object. This method takes in latitude and longitude properties for the center of the search along with a radius in meters and layer name which is in regards to the database table to search against (Cities or Countries). We will look at adding more advance spatial queries in another blog post. Next open the SpatialService.svc.cs file and implement the service reference by right clicking on the ISpatialService name in the class and selecting Implement Interface. You should end up with something like this:

using BM_EF5_WebService.Common;using System;using  System.ServiceModel.Activation;
namespace BM_EF5_WebService
{
   [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]public class SpatialService : ISpatialService 
   {public Response FindNearBy(double latitude, double longitude, double radius, string
layerName)
   {throw newNotImplementedException();
   }
 }
} 

Finally we need to define our service in the Web.config file. Open the Web.config file and add the following:

<system.serviceModel><serviceHostingEnvironmentaspNetCompatibilityEnabled="true"multipleSiteBindingsEnabled="true" /><standardEndpoints> <webHttpEndpoint><standardEndpointhelpEnabled="true"automaticFormatSelectionEnabled="false"crossDomainScriptAccessEnabled="true"><securitymode="None"/></standardEndpoint></webHttpEndpoint></standardEndpoints><behaviors><endpointBehaviors><behavior name="webHttpBehavior"><webHttp /> </behavior></endpointBehaviors><serviceBehaviors><behavior name=""><serviceMetadata httpGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="false" /></behavior></serviceBehaviors></behaviors><services><service name="BM_EF5_WebService.SpatialService"><endpoint address="" binding="webHttpBinding" behaviorConfiguration="webHttpBehavior" contract="BM_EF5_WebService.ISpatialService"/></service></services>
</system.serviceModel>

At this point, if we run the application it should build without any issues. However, if you try and call the service, a not implemented exception will occur as we haven’t yet added the query functionality to our service.

Adding the Spatial Query Functionality

Now that our service is created, we can add some functionality to it. To do a nearby search against our data we will first need to take our center coordinate information and turn it into a form that we can use with LINQ to SQL. Entity Framework provides us with a set of spatial classes which can be used with LINQ to SQL. These are similar to the built spatial classes in SQL Server, but modified to work with LINQ. We can create a center object from the user provided data like so:

DbGeography center = DbGeography.PointFromText("POINT(" + longitude + " " + latitude + ")", 
4326);  

What this does is creates a Well Know Text representation of the center coordinate and then converts this into a DbGeography object which LINQ to SQL will be able to use. Note the 4326 is the spatial reference identifier (SRID) that defines the type projection of the data. The 4326 refers to the WGS84 projection which is the standard projection used by most GPS devices and is what we normally use with Bing Maps. All the spatial data in our database has the same spatial reference identifier.

The next step is to create the LINQ query that will find all locations in our database that are within the specified radius. While we are at it, we will also order the results by the distance and return each object as a common City object. We will also return the distance for each location. Putting this all together you will end up with a LINQ statement that looks something like the following:

from c in e.Citieslet distance =  center.Distance(c.Location)where distance <=  radiusorderby distanceselect new Common.City()
{
   Name = c.Name,
   CountryISO = c.Country_ISO,
   Population = c.Population.HasValue ? (int)c.Population.Value  : 0,
   WKT = c.Location.AsText(),
   Distance = (distance.HasValue) ? Math.Round(distance.Value) : 0
} 

Now this query will only work with the Cities table in our database. We would have to create a slightly different query for the Countries table. We can specify which query our application can use by checking the value of the layer name property.

Next we will add in a bit of optimization for the data. The coordinates returned by the database will have a large number of decimal places often being 12 or more. At 5 decimal places we have an accuracy of about 1 meter. Any more than 5 decimal places just adds extra size in our response without providing any more information. To reduce the number of decimal places, we could loop through each number in spatial object and round it of However, this would be very slow, especially when used with the country boarders. A faster and easier method is to take the Well Known Text version of the spatial object and run a regular expression against it that drops the extra decimal places.

Finally we will catch any exceptions and note them in the Error property of the response. Putting this all together, we end up with the following code for the SpatialService.svc.cs file:

using BM_EF5_WebService.Common;using System;using System.Data.Spatial;using System.Linq;using System.ServiceModel.Activation;using System.Text.RegularExpressions;
namespace BM_EF5_WebService
{
   [AspNetCompatibilityRequirements(RequirementsMode = 
AspNetCompatibilityRequirementsMode.Allowed)]public classSpatialService : ISpatialService {privateRegex shortenCoordinate = newRegex("([0-9].[0-9]{5})[0-9]*");
   publicResponse  FindNearBy(double latitude, double  longitude, double radius, string
layerName) {Response r = new Response();
       try
   	{DbGeography  center = DbGeography.PointFromText("POINT(" + longitude + " " + 
latitude + ")", 4326);
           using (SpatialSampleEntities e = newSpatialSampleEntities())
   		{switch (layerName.ToLowerInvariant())
   			{case"cities":
   				    r.Results = (from c in e.Citieslet  distance = center.Distance(c.Location)where distance <= radiusorderby distanceselect new Common.City()
   						   {
   						     Name = c.Name,
   						     CountryISO = c.Country_ISO,
   						     Population = c.Population.HasValue ? 
(int)c.Population.Value : 0, WKT = c.Location.AsText(), Distance = (distance.HasValue) ?
Math.Round(distance.Value) : 0 }).ToList<BaseEntity>();break;case"countries": r.Results = (from c in e.Countrieslet distance = center.Distance(c.Boundary)where distance <= radiusorderby distanceselect new Common.Country() { ISO = c.ISO, Population = c.Population.HasValue ?
(int)c.Population.Value : 0, Name = c.Name, WKT = c.Boundary.AsText(), Distance = (distance.HasValue) ? Math.Round(distance.Value) : 0 }).ToList<BaseEntity>();break;default: r.Error = "Invalid Layer Name.";break; }
               if (r.Results != null)
   		{
   		     r.Results.ForEach(c  => c.WKT = shortenCoordinate.Replace(c.WKT, 
"$1")); } } }catch(Exception ex){ r.Error = ex.Message; }
       return r;       
} }
} 

At this point we have all we need to try out the service. Build the project and then right click on the SpatialService.svc file and select View in Browser. A browser should open and you should see have a base URL for your service that looks like this:

http://localhost:65521/SpatialService.svc 

Note that you will likely be using a different port for your application. Now run the solution (F5). At this point it will likely open a WCF Test client which we won’t use, but by running the application we can debug our code and hit break points. To test the service you need to put together a query URL and then simply open it in a browser. The following query looks for cities that are within 20km of the coordinate (52, 0) which is a point in Great Britain.

http://localhost:65521/SpatialService.svc/FindNearBy?latitude=52&longitude=0&radius=20000  &layerName=Cities

Running this query we end up with the following response:

{"Results":
    [
        { "__type":"City:#BM_EF5_WebService.Common","Distance":15737,"Name":"Letchworth Garden City","WKT":"POINT (-0.22664 51.97938)","CountryISO":"GB","Population":33600
        },{"__type":"City:#BM_EF5_WebService.Common","Distance":15856,"Name":"Letchworth","WKT":"POINT (-0.2284 51.97944)","CountryISO":"GB","Population":33955
        },{"__type":"City:#BM_EF5_WebService.Common", "Distance":17671,"Name":"Stevenage","WKT":"POINT (-0.20256 51.90224)","CountryISO":"GB","Population":84651
        },{"__type":"City:#BM_EF5_WebService.Common", "Distance":18020,"Name":"Bishops Stortford","WKT":"POINT (0.15868 51.87113)","CountryISO":"GB","Population":45001
        },{"__type":"City:#BM_EF5_WebService.Common","Distance":18953,"Name":"Hitchin","WKT":"POINT (-0.26519 51.95314)","CountryISO":"GB","Population":33830
        }
    ]
}

Connecting the Service to Bing Maps

At this point we have a working service that can find cities and countries that are within a specified distance of a coordinate. This is a good start, but it would be great if we could see this on a map. Looking at our service, we see that our spatial data is being returned as a Well Known Text string. Bing Maps doesn’t understand this out of the box, but there is a module for Bing Maps that was created through the Bing Maps V7 Modules CodePlex project called Well Known Text Reader/Writer. This module will be able to take the Well Known Text returned by our service and turn it into a Bing Maps shape that can be added to the map.

To add this functionality, add a folder to the main project called scripts and then copy in the WKTModule.js file from the Well Known Text Reader/Writer module project. Next add an html file to the project called index.html. At this point your solution should look like this:

SolutionBM_EF5_WebService

For the Bing Maps application we are going to create, we will keep it simple and have two buttons that when pressed will search for Cities or Countries that are within 100km of the center of the map. In addition, there will be functionality that opens an infobox with the name of the location that was clicked. The infobox functionality we will implement will be based on a previous blog post I wrote called Multiple Pushpins and Infoboxes in Bing Maps v7. Putting this all together, we end up with the following code which you will want to add to the index.html file:

<!DOCTYPEhtml><htmlxmlns="http://www.w3.org/1999/xhtml"><head><title></title><metahttp-equiv="Content-Type"content="text/html;  charset=utf-8" />
    <scripttype="text/javascript"
src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
    <scripttype="text/javascript">var map, infobox, dataLayer;
        function GetMap() {
           map = new Microsoft.Maps.Map(document.getElementById("myMap"),
           {
               credentials: "YOUR_BING_MAPS_KEY"
           });
           dataLayer = new Microsoft.Maps.EntityCollection();
           map.entities.push(dataLayer);
            var infoboxLayer = new Microsoft.Maps.EntityCollection();
            map.entities.push(infoboxLayer);
            infobox = new Microsoft.Maps.Infobox(new Microsoft.Maps.Location(0, 0), {  
visible: false, offset: new Microsoft.Maps.Point(0, 20) }); infoboxLayer.push(infobox);
   //Register and load the WKT Module
   Microsoft.Maps.registerModule("WKTModule", "scripts/WKTModule.js");
   Microsoft.Maps.loadModule("WKTModule");
   }
   function GetNearbyLocations(layer) {var center  = map.getCenter();var request = "http://localhost:65521/SpatialService.svc/FindNearBy?latitude=" +
                   center.latitude + "&longitude=" + center.longitude +"&radius=100000&layerName=" + layer + "&callback=?";
       CallRESTService(request,  DisplayData);
   }
   function DisplayData(data) {
       dataLayer.clear();
       infobox.setOptions({ visible: false });
       if (data &&  data.Results != null) {for (var i = 0; i < data.Results.length; i++) {var shape = WKTModule.Read(data.Results[i].WKT);
              //Complex shapes are returned as  EntityCollections 
             //Loop through each shape in the  collection and add click event              if (shape.getLength) {for (var j = 0; j < shape.getLength(); j++) {
                 shape.get(j).Title  = data.Results[i].Name;
                 Microsoft.Maps.Events.addHandler(shape.get(j),  'click', 
DisplayInfobox); } }else{ shape.Title = data.Results[i].Name; Microsoft.Maps.Events.addHandler(shape, 'click', DisplayInfobox); }
             dataLayer.push(shape);
           }
   }else if (data && data.Error != null) {
   alert("Error: " + data.Error);
   }
 }
 function DisplayInfobox(e) {var offset;
   if (e.targetType == 'pushpin') {
       infobox.setLocation(e.target.getLocation());
       offset = new Microsoft.Maps.Point(0, 15);
   }else{//Handle polygons and polylines    var bounds  = 
Microsoft.Maps.LocationRect.fromLocations(e.target.getLocations()); infobox.setLocation(bounds.center);
   offset = new Microsoft.Maps.Point(0, 0);
 }
        infobox.setOptions({ visible: true, title: e.target.Title, offset : offset, 
height : 40 }); }
       function CallRESTService(request, callback) {var xmlHttp;if (window.XMLHttpRequest) {
             xmlHttp = new XMLHttpRequest();
   } else if (window.ActiveXObject) {try {
           xmlHttp = new ActiveXObject("Msxml2.XMLHTTP");
       } catch (e) {try {
               xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
           } catch (e) {throw (e);
           }
        }
   }
   xmlHttp.open("GET", request, false);
   xmlHttp.onreadystatechange = function (r) {if (xmlHttp.readyState == 4) {
           callback(eval('(' + xmlHttp.responseText + ')'));
       }
   };
   xmlHttp.send();
 }</script>
</head><bodyonload="GetMap();"><divid='myMap'style="position:relative;width:800px;height:600px;"></div><br/><inputtype="button"value="Get Nearby  Cities"onclick="GetNearbyLocations('Cities')" /><inputtype="button"value="Get Nearby  Countries"
onclick="GetNearbyLocations('Countries')" /></body>
</html>

Ensure that the proper port number is specified in the REST request in the GetNearbyLocations method. If you now run the application and zoom in to some location and press either button, all the Cities or Countries that are within 100km of the center of the map will appear. Here is a screenshot of the cities returned when zoomed into Paris:

CreatingSpatialWebServiceParis

Here is a screenshot of countries in the middle of Europe that are within 100km of each other:

CreatingSpatialWebServiceCzechRepublic

Additional Tips & Tricks

If you would like to open this service up to client side applications such as a Windows Store or Silverlight application, you will need to add a clientaccesspolicy.xml and crossdomain.xml file to your project. Additional information on this can be found here.

Overall you should be able to reuse the majority of the code in index.html file to create a Windows Store application. You will just need to update the references to the Bing Maps API to point to the local copy of Bing Maps and also modify the code that loads the module as described here.

There are a lot of great free data sources on the web that contain all kinds of information. You may find that a lot of this information is in the form of an ESRI Shapefile. These can be easily imported into SQL using the Shape2SQL tool which was created by one of our Microsoft MVPs.

Recap

In this blog post we have seen a complete end to end solution for connecting a database to Bing Maps using Entity Framework 5. We have also learnt the basics of using the spatial functionalities in EF5 to perform a nearby search. In a future blog post, we will dive in deeper into more complex spatial queries such as find in polygon and find along a route.

- Ricky Brundritt, EMEA Bing Maps Technology Solution Professional

Advance Spatial Queries using Entity Framework 5

$
0
0

Recently we published a blog post titled “How to Create a Spatial Web Service That Connects a Database to Bing Maps Using EF5.” This post showed how to create a web service that connected Bing Maps to a database using the spatial functionality in Entity Framework 5 (EF5). In that example we only implemented a basic nearby search query. In this post, we are going to expand upon that application and add in more advance spatial search queries such as find in polygon, find in bounding box and find along a route. You can download the full source code and sample database here.

Some of these spatial queries are going to require more advanced spatial functionalities than what is available through EF5. The Entity Framework exposes only a subset of the spatial functionalities available in SQL Server. Fortunately all the spatial functionality in SQL Server is available as a .NET library that we can use in our application. If you don’t already have SQL 2008 or above installed locally, get an express version of SQL 2012. Once installed you should be able to add a reference to the Microsoft.SqlServer.Types.dll which is located in the Assemblies -> Extensions.

ReferenceManager-VM_EF5_WebService

Add Queries To The Service

The first thing we will need to do is add these queries to our service interface. To do this open the ISpatialService.cs file and update to the following:

using BM_EF5_WebService.Common;using System.ServiceModel;using System.ServiceModel.Web;
namespace BM_EF5_WebService
{
   [ServiceContract(Namespace = "SpatialService")]
   [ServiceKnownType(typeof(Response))]public interfaceISpatialService
   {/// <summary>///Finds all locations that are within a  specified distance of a central coordinate
for a specified layer.
/// </summary>/// <param name="latitude">Center latitude value.</param>/// <param name="longitude">Center longitude value.</param>/// <param name="radius">Search radius in Meters</param> /// <param name="layerName">Name of the layer (SQL table) to search against.</param>/// <returns>A list of results</returns> [OperationContract] [WebGet(ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.Bare)]Response FindNearBy(double latitude, double longitude, double radius, string layerName);
   /// <summary>/// Finds all locations that are within a  polygon for a specified layer.  /// </summary> /// <param name="polygonWKT">Well Known Text for a Polygon.</param>/// <param name="layerName">Name of the layer (SQL table) to search against.</param>/// <returns>A list  of results</returns> 
   [OperationContract]
   [WebGet(ResponseFormat = WebMessageFormat.Json, BodyStyle = 
WebMessageBodyStyle.Bare)]Response FindInPolygon(string polygonWKT, string layerName);
  /// <summary>///Find all locations that are within a  bounding box for a specified layer. /// </summary> /// <param name="north">North Latitude</param>/// <param name="east">East Longitude</param>/// <param name="south">South Latitude</param>/// <param name="west">West Longitude</param> /// <param name="layerName">Name of the layer (SQL table) to search against.</param>/// <returns>A list  of results</returns> 
   [OperationContract]
   [WebGet(ResponseFormat  = WebMessageFormat.Json, BodyStyle = 
WebMessageBodyStyle.Bare)]Response FindByBoundingBox(double north, double east, double south, double west,
string layerName);
 /// <summary>/// Finds all locations that are within a  specified distance (radius) of a route
path for a specified layer.
/// </summary> /// <param name="waypoints">A pipe (|) delimited list of waypoints a route goes through.</param>/// <param name="radius">Search radius aourd the route in meters.</param>/// <param name="bingMapsKey">Bing Maps key for requesting route.</param>/// <param name="layerName">Name of the layer (SQL table) to search against.</param> /// <returns>A list of results</returns>
[OperationContract] [WebGet(ResponseFormat = WebMessageFormat.Json, BodyStyle =
WebMessageBodyStyle.Bare)]Response FindNearRoute(string waypoints, double radius, string bingMapsKey,
string layerName); }
} 

Next we will need to implement these new methods in our service. Open up the SpatialService.cs file and right click on the ISpatialService value which the service inherits from and select Implement Interface -> Implement Interface. This will add the method blocks for these new search queries and should look like this:

publicResponse FindInPolygon(string polygonWKT, string layerName)
   {throw newNotImplementedException();
   }
publicResponse FindByBoundingBox(double north, double east, double south, double west, 
string layerName) {throw newNotImplementedException(); }
publicResponse FindNearRoute(string waypoints, double radius, string bingMapsKey, bool
returnBoundary, string layerName) {throw newNotImplementedException();
}  

At this point you should be able to build the application without any issues. If you try and call any of these new queries, an exception will be thrown. We will replace the exception code with the query functionality as we go along.

You might be wondering why we are passing a Bing Maps key into the FindNearRoute method. The reason is that we will need to use the Bing Maps REST services to generate a route line for us. We could just store this key in the code for our service. However, if you are using one of the interactive Bing Maps controls you can generate a special Bing Maps key from it called a session key. This session key will make the route request a part of the map session and mark it as a non-billable transaction in your account.

Find By Polygon Query

For this query we will take in a Well Known Text value that represents a polygon and then use this to perform an intersection test against the appropriate data table. The first thing we will need to do in this method is turn the Well Known Text into a DbGeography object like so:

DbGeography polygon = DbGeography.FromText(polygonWKT, 4326); 

The next step is to create the LINQ query that will find all locations in our database that intersect with the polygon. This type of LINQ Query looks like this:

from c in e.Citieswhere c.Location.Intersects(polygon)select new Common.City()
   {
   Name = c.Name,
   CountryISO = c.Country_ISO,
   Population = c.Population.HasValue ? (int)c.Population.Value  : 0,
   WKT = c.Location.AsText()
} 

Now this query will only work with the Cities table in our database. We would have to create a slightly different query for the Countries table. We can specify which query our application can use by checking the value of the layer name property. Putting this all together our FindInPolygon method looks like this:

publicResponse FindInPolygon(string polygonWKT, string layerName)
{Response r = newResponse();
   try 
   {DbGeography polygon = DbGeography.FromText(polygonWKT,  4326);
      if (polygon.Area.HasValue &&  polygon.Area.Value > (580000000000000 / 2)){SqlGeography sqlPolygon = SqlGeography.STGeomFromWKB(new
System.Data.SqlTypes.SqlBytes(polygon.AsBinary()), 4326); sqlPolygon = sqlPolygon.ReorientObject(); polygon = DbGeography.FromBinary(sqlPolygon.STAsBinary().Value); }
       using (SpatialSampleEntities e = newSpatialSampleEntities())
       {switch  (layerName.ToLowerInvariant())
           {case"cities":
                   r.Results = (from c in e.Citieswhere c.Location.Intersects(polygon)select new Common.City()
                                {
                                   Name =  c.Name,
                                   CountryISO  = c.Country_ISO,
                                   Population  = c.Population.HasValue ? 
(int)c.Population.Value : 0, WKT = c.Location.AsText() }).ToList<BaseEntity>(); break;case "countries": r.Results = (from c in e.Countrieswhere c.Boundary.Intersects(polygon)select new Common.Country() { ISO = c.ISO, Population = c.Population.HasValue ?
(int)c.Population.Value : 0, Name = c.Name, WKT = c.Boundary.AsText() }).ToList<BaseEntity>();break;default: r.Error = "Invalid Layer Name.";break; }
           if (r.Results != null)
           {
               r.Results.ForEach(c => c.WKT  = shortenCoordinate.Replace(c.WKT, "$1"));
           }
       }
   }catch (Exception  ex)
   {
      r.Error = ex.Message;
   }
    return r;
}  

We can now test this new search query. To test the service you need to put together a query URL and then simply open it in a browser. The following query looks for cities that are within triangle over part of the USA.

http://localhost:65521/SpatialService.svc/FindInPolygon?polygonWKT=POLYGON((-100 40,-102.5 45,-105 40,-100 40))&layerName=Cities

Running this query we end up with the following response:

{"Results":
   [
     {"__type":"City:#BM_EF5_WebService.Common",
   	"Name":"Greeley",
   	"WKT":"POINT (-104.70913  40.42331)",
   	"CountryISO":"US",
   	"Population":92889
   },{"__type":"City:#BM_EF5_WebService.Common",
   	"Name":"Scottsbluff",
   	"WKT":"POINT (-103.66717  41.86663)",
   	"CountryISO":"US",
   	"Population":15039
   },{"__type":"City:#BM_EF5_WebService.Common",
   	"Name":"Evans",
   	"WKT":"POINT (-104.69219  40.37637)",
   	"CountryISO":"US",
   	"Population":18537
   },{"__type":"City:#BM_EF5_WebService.Common",
   	"Name":"North Platte",
   	"WKT":"POINT (-100.76542  41.12389)",
   	"CountryISO":"US",
   	"Population":24733
   }
 ]
}  

Now we called this FindInPolygon, but in reality we can pass in any shape into this method and do an intersection search against any shape. For example, you could pass in a line and find all the countries that the line goes through. This could be useful if you wanted to know all the countries a route went through.

It is also worth noting that really complex polygons will result in much longer Well Known Text strings being generated. This can be an issue as browsers have a limit on how long URL’s can be. If you find that this becomes a problem for you, then modifying this part of the service to use a POST request rather than a GET request.

Tip: Use the Shape Toolbox and the Well Known Text Reader/Writer modules for Bing Maps together to allow your users to draw out a search area on the map and generate a polygon Well Known Text to pass to the service.

Caution: One thing we need to be aware of is that when it comes to spatial polygons the order of the coordinates are very important. Changing the order can cause them appear to rotate a different direction which would result in the inverse area being searched. If you were using SQL 2008, you would likely get an error, as this would generate a huge polygon that spans more than a hemisphere in size. However, in SQL 2012 and SQL Azure there is better support for things like this and it will actually work and return all the locations but those we expected. For example, changing the Well Known Text of our polygon to this: POLYGON((-100 40,-105 40,-102.5 45,-100 40)) results in a shape that looks like this:

SpatialResults

If you find this becomes an issue in your application, you can get around this by checking what the area is of the polygon and if it is too large then reverse the orientation of the polygon. To do this we require using the SQL Spatial library we added earlier .NET so that you can make use of the ReorientObject method that reverses the order of the points. Insert this after creating the initial DbGeography polygon the following code will change the orientation of any polygon that is larger than half the surface area of the globe:

if (polygon.Area.HasValue && polygon.Area.Value >  (580000000000000 / 2)){SqlGeography sqlPolygon = SqlGeography.STGeomFromWKB(new
System.Data.SqlTypes.SqlBytes(polygon.AsBinary()), 4326); sqlPolygon = sqlPolygon.ReorientObject(); polygon = DbGeography.FromBinary(sqlPolygon.STAsBinary().Value);
}  

Find By Bounding Box

Now you are probably wondering why I choose to start off with the find by polygon query rather than this one. Well, that’s easy. A bounding box is nothing more than a square polygon. All we need to do is create the Well Known Text for the bounding box using the north, south, east and west coordinates and then pass this to the FindInPolygon method we just created. Putting this together, we end up with the following code:

public Response FindByBoundingBox(double north, double east, double south, double west, 
string layerName) {//Create poylgon of bounding boxstring bboxWKT = string.Format("POLYGON(({0} {1},{2} {1},{2} {3},{0} {3},{0} {1}))",
east, north, west, south);return FindInPolygon(bboxWKT, layerName);
}

We can now test this new search query. To test the service you need to put together a query URL and then simply open it in a browser. The following query looks for cities that are within small bounding box over part of the UK.

http://localhost:65521/SpatialService.svc/FindByBoundingBox?north=51.2&east=0&south=51&west=0.2&layerName=Cities

Running this query, we end up with the following response:

{"Results":
   [
   	{"__type":"City:#BM_EF5_WebService.Common","Name":"Crowborough","WKT":"POINT (0.16171  51.06044)","CountryISO":"GB","Population":20733
   	}
   ]
}  

Note that if you implemented the functionality that reorients the polygon if its area is too big in the FindInPolygon method then this will not return any results if you pass in a full globe bounding box. A possible solution is to add a Boolean value to that method indicating if that logic should be used or not.

Tip: Dynamically update the map with the data that is in view by attaching to the viewchangeend event of the map, and use the bounding box of the map to call this service. You may find that using a throttled event handler is a good choice as that will help reduce the number of calls to the service if the user pauses momentarily when panning and zooming.

Find Near Route

The find along a route query is a bit more complex than what we have covered so far. For this query we will have to generate a route line between the specified waypoints. We can do this using the Bing Maps REST Routing Service. To make use of the Bing Maps REST services we will follow the documentation on Using the REST services with .NET. To implement this add a new file to the project called BingMapsRESTServiceModels.cs. Open this file up and delete its contents, then copy and paste the JSON Data Contracts from the Bing Maps REST services which are available here. In the documentation asynchronous calls are being made to the Bing Maps Service. Since we are going to be making these calls from inside our service, we need the response inline, so we will make a synchronous call instead. To make things nice we will create a private method in the SpatialService.cs file call GetResponse that will make this synchronous call for us and will deserialize the response for us. This method looks like this:

private  BingMapsRESTService.Common.JSON.Response GetResponse(Uri uri)
{WebClient client = newWebClient();using (var stream = client.OpenRead(uri))
   {DataContractJsonSerializer ser = new
DataContractJsonSerializer(typeof(BingMapsRESTService.Common.JSON.Response));return ser.ReadObject(stream) as BingMapsRESTService.Common.JSON.Response; }
}  

Going back to the FindNearRoute method, one of the first things we will need to do is split the waypoint parameter on the pipe delimiter character and then use the waypoints to create a route request URL for the Bing Maps REST services. Once a URL is created we can then request the route from Bing Maps and use the returned route path information to create a SqlGeography object. The route line has a lot of points in it, and we will want to reduce the resolution to speed up the next step of generating a buffer around the line that is equal to our search radius. I’ve found a tolerance of 100 works well for the Reduce and BufferWithTolerance methods when working with route lines. Once we have generated our buffer, we can convert the SqlGeography object into a DbGeography object and use it to do an intersection test against our database. Putting this all together, we end up with the following code for the FindNearRoute method.

public Response FindNearRoute(string waypoints, double radius, string  bingMapsKey, string
layerName) {Response response = newResponse();
   try
   {//Create the request URL for  the route service	string[] wp = waypoints.Split(new char[]{'|'});
       if (wp.Length < 2)
   	{throw new Exception("Invalid number of waypoints.");
   	}
       StringBuilder request = new
StringBuilder("http://dev.virtualearth.net/REST/V1/Routes/Driving?rpo=Points");
       for (int i = 0; i <  wp.Length; i++)
   {
       request.AppendFormat("&wp.{0}={1}", i, wp[i]);
   }
   request.AppendFormat("&key={0}", bingMapsKey);
   Uri routeRequest = newUri(request.ToString());
   BingMapsRESTService.Common.JSON.Response r  = GetResponse(routeRequest);
   if (r == null ||
   r.ResourceSets == null ||
   r.ResourceSets.Length == 0 ||
   r.ResourceSets[0].Resources == null ||
   r.ResourceSets[0].Resources.Length == 0)
   {	throw newException("Unable to calculate route between waypoints.");
   }
   BingMapsRESTService.Common.JSON.Route route =  
(BingMapsRESTService.Common.JSON.Route)r.ResourceSets[0].Resources[0];
   if(route.RoutePath == null ||
   route.RoutePath.Line == null ||
   route.RoutePath.Line.Coordinates == null ||
   route.RoutePath.Line.Coordinates.Length == 0)
   {throw newException("Unable to calculate route between waypoints.");
   }
   var coords =  route.RoutePath.Line.Coordinates;
   //Turn the coordinate array  into an SQLGeography object   SqlGeographyBuilder builder = newSqlGeographyBuilder();
   builder.SetSrid(4326);
   builder.BeginGeography(OpenGisGeographyType.LineString);
   builder.BeginFigure(coords[0][0], coords[0][1]);for (var i = 1; i <  coords.Length; i++)
   {if (coords[i].Length >= 2)
   	{
   		builder.AddLine(coords[i][0],  coords[i][1]);
   	}
   }
   builder.EndFigure();
   builder.EndGeography();
   //Reduce the resolution of  the line and give is a buffer equal to our radius    var buffer = builder.ConstructedGeography.Reduce(100);
   buffer = buffer.BufferWithTolerance(radius, 100, false);
   //Turn the SqlGeography  object into a DbGeography object    DbGeography routeBuffer = DbGeography.FromBinary(buffer.STAsBinary().Value, 4326);
   using (SpatialSampleEntities e = newSpatialSampleEntities())
   {switch  (layerName.ToLowerInvariant())
       {case"cities":
   		response.Results = (from c in e.Citieswhere c.Location.Intersects(routeBuffer)select new Common.City()
              {
   		    Name =  c.Name,
   		    CountryISO = c.Country_ISO,
   		    Population = c.Population.HasValue ? 
(int)c.Population.Value : 0, WKT = c.Location.AsText() }).ToList<BaseEntity>();break;case"countries": response.Results = (from c in e.Countrieswhere c.Boundary.Intersects(routeBuffer)select new Common.Country() { ISO = c.ISO, Population = c.Population.HasValue ?
(int)c.Population.Value : 0, Name = c.Name, WKT = c.Boundary.AsText() }).ToList<BaseEntity>();break;default: response.Error = "Invalid Layer Name.";break; }
   if (response.Results != null)
   {
       response.Results.ForEach(c  => c.WKT = shortenCoordinate.Replace(c.WKT, 
"$1")); } } }catch(Exception ex){ response.Error = ex.Message; }
 return response;   
}  

We can now test this new search query. To test the service you need to put together a query URL and then simply open it in a browser. You can pass in string addresses or coordinates as waypoints. If passing in coordinates simple use the format “latitude, longitude”. The following query looks for cities that are within 10km of a route between London and Paris.

http://localhost:65521/SpatialService.svc/FindNearRoute?waypoints=London|Paris&radius=10000&layerName=Cities&bingMapsKey=Your_Bing_Maps_Key

Running this query returns a large number of results.

Tip: If you are curious about what the route buffer looks like, add the following line of code after the buffer is generated and put a break point in your code after it.

string wkt = new string(buffer.STAsText().Value);

When you run your code you can then grab the Well Known Text for the route buffer. You can then use the test application that comes with the Well Known Text Reader/Writer module to render this buffer on Bing Maps. By doing this for the test query we did earlier, we end up with this:

AdvanceSpatialQueriesMap

Adding These Queries To the Map

At this point we have proven that our advance spatial queries work well, but looking at a bunch of JSON results isn’t all that interesting. Open up the index.html file and add the following JavaScript functions.

function FindByPolygon(layer) {var request = "http://localhost:65521/SpatialService.svc/FindInPolygon?" +"polygonWKT=POLYGON((-100  40,-102.5 45,-105 40,-100 40))&layerName=" +
       layer  + "&callback=?";
    CallRESTService(request, DisplayData);
}
function FindByBoundingBox(layer) {var bbox =  map.getBounds();
   var request = "http://localhost:65521/SpatialService.svc/FindByBoundingBox?" +"north=" + bbox.getNorth() + "&east=" + bbox.getWest() +"&south=" + bbox.getSouth() + "&west=" + bbox.getEast() +    "&layerName=" + layer + "&callback=?";
    CallRESTService(request, DisplayData);
}
function FindNearRoute(layer) { //Generate Session key
   map.getCredentials(function (c) {var request = "http://localhost:65521/SpatialService.svc/FindNearRoute?" +"waypoints=London|Paris&radius=10000&bingMapsKey=" + c +"&layerName=" + layer + "&callback=?";
        CallRESTService(request, DisplayData);
   });
}

Next add the following HTML into the body of the page.

<br/><inputtype="button"value="Get Cities in Polygon"
onclick="FindByPolygon('Cities')" /><inputtype="button"value="Get Nearby in Polygon"
onclick="FindByPolygon('Countries')"/><br/><inputtype="button"value="Get Cities in View"
onclick="FindByBoundingBox('Cities')"/><inputtype="button"value="Get Countries in View"
onclick="FindByBoundingBox('Countries')" /><br/><inputtype="button"value="Find Cities Near Route"
onclick="FindNearRoute('Cities')"/>
<inputtype="button"value="Find Nearby Near Route"
onclick="FindNearRoute('Countries')"/>

This JavaScript consists of three functions; FindByPolygon, FindByBoundingbox, and FindNearRoute. The FindByPolygon function searches for locations inside a triangle over the US. The FindByBoundingBox function finds all locations that are within the current map view. Be careful, if you do this when zoomed all the way out you will be loaded a lot of data onto the map. Note this method will throw an error if you have implemented the logic to reorient the polygon when used with the full globe. The FindNearRoute function finds all locations that are within 10km of the route from London to Paris.

Before testing this code out ensure that the proper port number is specified in the REST requests inside the search functions. If you now run the application and press Find Cities Near Route button it will find all cities that are within 10km of the route from London to Paris. Here is a screenshot of the cities returned when zoomed into Paris:

AdvanceSpatialQueriesParisMap

Here is a screenshot of countries that are in the current map view using a bounding box search while zoomed in over South Africa:

AdvanceSpatialQueriesSouthAfricaMap

Recap

We have now seen how we can create some advance spatial queries using Entity Framework 5. This time around we also made use of the SQL Spatial library so that we could do some spatial calculations right inside the service. Taking things a bit further you could use this service as a way to offload complex spatial calculates to the server side.

- Ricky Brundritt, EMEA Bing Maps Technology Solution Professional

Bing Maps Publishes Equivalent of 100,000 DVD’s of Bird’s Eye Imagery

$
0
0

Today, we are excited to announce a number of updates to Bing Maps, including the largest shipment of Bird’s Eye imagery yet, nearly 270 terabytes of data or the equivalent of 100,000 DVD’s, along with expanded venue maps and our new “Report a problem” feature.

BirdsEye_divider_550x3

birdseye

Bird's Eye
Imagery captured at a 45 degree angle, giving depth and three-dimensionality to ortho photography

BirdsEye_divider_550x3

To date Bing has published a total of 1,452,958 sq km, or half a petabyte of data, of Bird's Eye scenes from around the world. Look for yellow in the below World map to see our new Bird’s Eye coverage from this release:

BE_CoverageMap

Click the links below, and view more imagery on Bing Maps!

Tampere, Finland

W8 Link | Bing Maps Link

BE_Tampere_Large

Rome, Italy

W8 Link | Bing Maps Link

BE_Rome_Large

Kaanapali, Hawaii

W8 Link | Bing Maps Link

Stavanger, Norway

W8 Link | Bing Maps Link

BE_Hawaii_MedBE_StavengerNorway

Milan, Italy

W8 Link | Bing Maps Link

Ringerike, Norway

W8 Link | Bing Maps Link

BE_MilanItaly_SmallBE_Ringerike_Small

Toledo, Ohio

W8 Link | Bing Maps Link

Kuopio, Finland

W8 Link | Bing Maps Link

BE_ToledoOHBirdsEye_Finland_Small

Additional Highlights

Melbourne, Australia
W8 Link | Bing Maps Link
Eugene, Oregon
W8 Link | Bing Maps Link
Pescara, Italy
W8 Link | Bing Maps Link
Saratoga Springs, NY
W8 Link | Bing Maps Link
Tokyo, Japan
W8 Link | Bing Maps Link

VenueMap_divider_550x3

VM_Icon_Small

Venue Maps
Provides detailed maps illustrating a point of interest

VenueMap_divider_550x3

Ever need to find your seats at a football game? Lost in the Mall of America? With more than 4,700 Venue Maps in more than 59 countries on Bing Maps, you can navigate your way around malls, airports, amusement parks and more using your Windows phone, tablet, or PC!

Mall of America, St. Paul, Minnesota

W8 Link | Bing Maps Link

VENUE_MofA_large

While on Bing Maps and within a zoom level of 1000 feet, purple and green polygons will appear to outline particular points of interest for a given venue. Green shading indicates restaurants, while stores appear purple.

When you click on any outlined polygon, a directory will appear with a full list of points of interest and offer options to visually explore additional floors of the venue. Try it with the additional links below:

Boston, Massachusetts

W8 Link | Bing Maps Link

Singapore Zoo, Singapore

W8 Link | Bing Maps Link

VENUE_BostonVENUE_Singapore

Las Vegas, Nevada

W8 Link | Bing Maps Link

King's College, London

W8 Link | Bing Maps Link

VENUE_VegasVENUE_London

VenueMap_divider_550x3

If you see an outlined feature that is not correct, please tell us about it. All you have to do is click on the point of interest in question and click “Report a problem”.

Follow the steps below for a walk-through at Hartsfield-Jackson International Airport, Atlanta, Georgia.

Step 1: Click on US Post Office, then click "Report a problem"
W8 Link |
Bing Maps Link

VM_Report_Main
Step 2: Select an option and click "Continue"
VM_Report_Image2
Step 3: Select option or type in comment and hit "Submit"
VM_Report_Image3

- The Bing Maps Team

Infoboxes for Native Windows Store Apps

$
0
0

Recently there has been a number of requests for information on how to create infoboxes using the Bing Maps Native control. Many developers who have used our JavaScript controls are used to an infobox control being available out of the box and are a bit surprised that there isn’t one in the Native control. This wasn’t an oversight, but really not needed. With the Native control, we have the ability to overlay user controls directly on top of the map and tie them to a location on the map. This means that rather than being restricted to having to use an infobox control that looks and feels the way we think it should, you have the ability to create an infobox that looks and feels how you want.

In this blog post we will take a look at how to create a simple infobox control. To optimize this application, we will use a common method of having one Infobox control which we will reuse and update rather than creating an infobox for each pushpin we create. This will drastically reduce the number of objects your application will need to render and keep track of. This same approach is recommended when using the JavaScript control as well. If using JavaScript, take a look at this blog post.

Creating the View

To start off, create a Blank Windows Store project in Visual Studio called BingMaps_Native_Infobox and add a reference to the Bing Maps SDK. If you are not familiar with how to do this, take a look at the Getting started with Bing Maps Windows Store Apps (Native) blog post.

Once your project is created, open up the MainPage.xaml file and add a reference to the Bing Maps SDK and add a map to the main Grid. Inside of the map, we will add two MapLayers and give the first layer a name of DataLayer. We will use this layer to add our pushpins to. We will put the infobox in the second layer and this will ensure that the infobox always appears above the data layer. For the infobox itself, we will use a Grid control and add some Textboxes for Title and Description information. We will also add a close button to the infobox. Putting this together, your XAML should look like this:

<Page
x:Class="BingMaps_Infoboxes_Native.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:BingMaps_Infoboxes_Native"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:Bing.Maps"
mc:Ignorable="d">

<GridBackground="{StaticResource ApplicationPageBackgroundThemeBrush}">
<m:MapCredentials="YOUR_BING_MAPS_KEY">
<m:Map.Children>
<!-- Data Layer-->
<m:MapLayerName="DataLayer"/>

<!--Common Infobox-->
<m:MapLayer>
<Gridx:Name="Infobox"Visibility="Collapsed"Margin="0,-115,-15,0">
<BorderWidth="300"Height="110"Background="Black"Opacity="0.8"BorderBrush="White"BorderThickness="2"CornerRadius="5"/>

<StackPanelHeight="100"Margin="5">
<GridHeight="40">
<TextBlockText="{Binding Title}"FontSize="20"Width="250"TextWrapping="Wrap"HorizontalAlignment="Left"/>
<ButtonContent="X"Tapped="CloseInfobox_Tapped"HorizontalAlignment="Right"VerticalAlignment="Top"/>
</Grid>
<ScrollViewerHorizontalScrollBarVisibility="Auto"VerticalScrollBarVisibility="Auto"MaxHeight="60">
<TextBlockText="{Binding Description}"FontSize="16"Width="290"TextWrapping="Wrap"Height="Auto"/>
</ScrollViewer>
</StackPanel>
</Grid>
</m:MapLayer>
</m:Map.Children>
</m:Map>
</Grid>
</Page>

Adding the Application Logic

If you try running the application now, an error will be thrown as we haven’t defined the CloseInfobox_Tapped event handler. To fix this, open the MainPage.xaml.cs file and add the following event handler:

 
privatevoid CloseInfobox_Tapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
{
Infobox.Visibility = Visibility.Collapsed;
}

Next, we need to add some pushpins to the map. To do this, we will create a reusable function that takes in a Location, title, description and a reference to a MapLayer to add the pushpin to. When we create the pushpin we will store the title and description inside the Tag property of the pushpin. We will then add a Tapped event which we will use to open our infobox. Finally, we will add the pushpin to the layer. Add the following code to your application:
publicvoid AddPushpin(Location latlong, string title, string description, MapLayer layer)
{
Pushpin p = new Pushpin()
{
Tag = new Metadata()
{
Title = title,
Description = description
}
};

MapLayer.SetPosition(p, latlong);

p.Tapped += PinTapped;

layer.Children.Add(p);
}

publicclass Metadata
{
publicstring Title { get; set; }
publicstring Description { get; set; }
}

Now we will create the PinTapped event handler. When the user taps the pushpin, we will take the metadata we stored in the pushpin and bind it to the Infobox. We will then make the infobox visibility and then use the MapLayer to set the position of the infobox.

privatevoid PinTapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
{
Pushpin p = sender as Pushpin;
Metadata m = (Metadata)p.Tag;

//Ensure there is content to be displayed before modifying the infobox control
if (!String.IsNullOrEmpty(m.Title) || !String.IsNullOrEmpty(m.Description))
{
Infobox.DataContext = m;

Infobox.Visibility = Visibility.Visible;

MapLayer.SetPosition(Infobox, MapLayer.GetPosition(p));
}
else
{
Infobox.Visibility = Visibility.Collapsed;
}
}

We now have all the code we need to create pushpins that open up an Infobox when tapped. All we need to do now is create some pushpins. To do this, update the constructor to look like this:

public MainPage()
{
this.InitializeComponent();

AddPushpin(new Location(47.6035, -122.3294), "Seattle", "Seattle is in the state of Washington. ", DataLayer);

AddPushpin(new Location(51.5063, -0.1271), "London", "London is the capital city of England and the United Kingdom, and the largest city in the United Kingdom.", DataLayer);
}

If you run the application, you will see two pushpins on the map. If you tap on one, the Infobox will appear with the relevant information for that pushpin and look something like this:

image

You can take this a step further and create a custom UserControl that has a lot more advanced features. In the end you simply just need to add it to a MapLayer and set its position.


How To Load Spatial Data From SQLite In A Windows Store App

$
0
0

Sometimes it can be helpful to hold geospatial data locally and to distribute it with the application. In this example, we will use SQLite to store geospatial information and visualize it as a layer in the Bing Maps Control for Windows Store Apps.

SQLite is a software library that implements a self-contained, server-less, zero-configuration, transactional SQL database engine.

Preparing the Data

For this example we will be using trails which we retrieved from the King County GIS Data Portal. This data is available in Esri Shapefile format with coordinates in the North American Datum 1983 (NAD83) system. I converted them into a SQLite database with coordinates described as latitudes and longitudes with decimal degrees in the World Geodetic System 1984 (GS84) using the ogr2ogr command-line tool from Geospatial Data Abstraction Library (GDAL). The command for this conversion is:

ogr2ogr.exe -f sqlite -lco FORMAT=WKT "D:\Downloads\GeoData\King County\trail_SHP\trail.db" 
"D:\Downloads\GeoData\King County\trail_SHP\trail.shp" -t_srs EPSG:4326

Prerequisites

Since we are developing a Windows Store App, we need 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.

For this project we require the “Bing Maps SDK for Windows Store Apps” as well as “SQLite for Windows Runtime” you can install both from Visual Studio 2012 by selecting “Extensions and Updates” from the menu “Tools” and searching for the respective SDKs in the online gallery.

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

Preparing the Project

Let’s start by creating a new project using the blank Visual C# template for Windows Store Apps.

BlankVisualCSharpTemplateWindowsStoreApps

Next we add references to the Bing Maps SDK, the Visual C++ Runtime and SQLite.

ReferenceManager-SQLite_Blog

The Bing Maps SDK requires that we compile separately for each processor architecture. So we need to open the “Configuration Manager” and change the platform from “Any CPU” to a specific one – here “x64”.

ConfigurationManager

We also require the “sqlite-net” library and we can add this from NuGet by opening the menu “Tools” “Library Package Manager” “Manage NuGet Packages for Solution” and searching for “sqlite-net”.

ManageNuGetPackages

Finally we add the SQLite database trail.db to the folder “Assets” of the project, set the property “Build Action” to “Content” and “Copy to Output Directory” to “Copy if Newer”.

SolutionExplorer

Adding the Application Markup

Now that our project is prepared, we open the MainPage.xaml add the namespace for the Bing Maps control and define the user interface. In the user interface, we load Bing Maps centered to a location in King County at zoom-level 13 and specify that we want to display the aerial imagery.

<Pagex:Class="SQLite_Blog.MainPage"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="using:SQLite_Blog"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:bm="using:Bing.Maps"mc:Ignorable="d">
<GridBackground="{StaticResourceApplicationPageBackgroundThemeBrush}"><Grid.RowDefinitions><RowDefinitionHeight="140"/><RowDefinitionHeight="*"/> <RowDefinitionHeight="Auto"/></Grid.RowDefinitions>
   <TextBlockx:Name="pageTitle"Text="WinRT and Spatial Data  from SQLite"IsHitTestVisible="false"Style="{StaticResourcePageHeaderTextStyle}"VerticalAlignment="Center"Margin="30,0,30,40"/>
   <bm:Mapx:Name="myMap"Grid.Row="1"MapType="Aerial"ZoomLevel="13"Credentials="Your_Bing_Maps_Key"><bm:Map.Center><bm:LocationLatitude="47.702894"Longitude="-122.054860" /></bm:Map.Center></bm:Map>
   <TextBlockx:Name="myAttribution"Text="Data provided by  permission of King County"Grid.Row="2"/></Grid></Page>

Adding the Code-Behind

In the code-file MainPage.xaml.cs we define a class that describes the SQLite table.

public class trail
{    public string OGC_FID { get; set; } public string WKT_GEOMETRY { get; set; }  public string kc_fac_fid {  get;  set; }public string trail_name {  get;  set; }public string trail_type {  get;  set; }public string surf_type {  get;  set; }public string sitefacfid {  get;  set; }public string sitename {  get;  set; }public string sitetype {  get;  set; }public string owner {  get;  set; }public string ownertype {  get;  set; }public string manager {  get;  set; }public string managertype {  get;  set; }public string maintd_by {  get;  set; }public string mainttype {  get;  set; } public string shape_len {  get;  set; }
 }

For the class that will actually read the data and display it on the map, we import three libraries:

using Bing.Maps;using Windows.Storage;using Windows.UI.Popups; 

The class that reads the trail-database copies it first in the local application directory. It creates a MapShapeLayer, reads through the table-records and adds the records for the trails to the layer before it adds the entire layer to the map.

One point to call out here is that the data is stored as Well Known Text (WKT) in SQLite. The WKT has the coordinates in the order Longitude and then Latitude while Bing Maps expects them in the order Latitude and then Longitude. So we have to swap the order of the coordinates.

public async void GetTrails()
{var uri = newUri("ms-appx:///Assets/trail.db"); var file = awaitStorageFile.GetFileFromApplicationUriAsync(uri);
   var destinationFolder = ApplicationData.Current.LocalFolder;//local appdata dir
   try 
   {await  file.CopyAsync(destinationFolder); //copied application local folder}}
   }catch { }var dbpath = Path.Combine(Windows.Storage.ApplicationData.Current.LocalFolder.Path, "trail.db");var db = new SQLite.SQLiteConnection(dbpath);var trails = db.Table<trail>();
   MapShapeLayer  shapeLayer = newMapShapeLayer();int numLocs = 0;int numTrails = 0;
   foreach (trail thisTrail in trails)
   {var wkt =  thisTrail.WKT_GEOMETRY.Replace("LINESTRING  (" , "").Replace(")", "");string[] wktArray = wkt.Split(',');LocationCollection bmPolylineLocs = newLocationCollection();for (var i = 0; i <  wktArray.Length; i++)
   { var loc = wktArray[i];var locArray = loc.Split(' ');
   bmPolylineLocs.Add(new Location(Convert.ToDouble(locArray[1]), 
   Convert.ToDouble(locArray[0])));
   numLocs  = numLocs + 1;
   }
   MapPolyline bmPolyline = newMapPolyline();
   bmPolyline.Locations = bmPolylineLocs;
   bmPolyline.Color = Windows.UI.Colors.Red;
   bmPolyline.Width = 5;
   shapeLayer.Shapes.Add(bmPolyline);
   numTrails  = numTrails + 1;
  }
  myMap.ShapeLayers.Add(shapeLayer);
  var myMsg = newMessageDialog("Loaded " + numTrails.ToString() + " with " + numLocs.ToString() + " locations" );await myMsg.ShowAsync(); 
}

Finally we add a call to this new class right after we initialize the app.

public MainPage()
{this.InitializeComponent();
  GetTrails();
} 

And that’s it. Below you see a screenshot of the trails in King County on top of Bing Maps.

Happy Coding!

WinRTSpatialDataSQLite

Automating the Windows Map App

$
0
0

Windows allows a Windows Store application to register as the default handler for a certain URI schema. This process is called Protocol Activation and it can be leveraged in the WinRT as well as the WinJS framework.

The Windows Map app is registered for protocol activation and the URI schema is documented here. Through protocol activation you can control the map view, find places or business listings, calculate routes or even map entire collections of points of interest.

 ProtocolActivation

In this quick example, we use this capability to display the location where we captured a photo. Rather than starting from the scratch, we build upon the Simple Image Sample from the Windows Dev Center. The first scenario in this application reads and writes properties of an image – including the coordinates (latitude and longitude) where we captured the photo.

SimpleImagingSample

There are a few changes that we need to apply to this app in order to launch the Map app and center on this location.

In the application markup language of scenario 1 we do the following: 

  1. Replace the boxes for degrees, minutes and seconds as well as the one for the compass orientation with a single text-box in which we will display the latitude and longitude in decimal degrees.
  2. Add a button that will launch the Map app with parameters.

The new XAML will look like this:

<TextBlockGrid.Column="0"Grid.Row="6"TextWrapping="Wrap"	 Style="{StaticResourceBasicTextStyle}"	 HorizontalAlignment="Left"VerticalAlignment="Center">Latitude</TextBlock><TextBoxGrid.Column="1"Grid.Row="6"Margin="0,0,10,10"x:Name="LatTextbox"         HorizontalAlignment="Left"  Text=""Width="300" />

<
TextBlockGrid.Column="0"Grid.Row="7"TextWrapping="Wrap" Style="{StaticResourceBasicTextStyle}" HorizontalAlignment="Left"VerticalAlignment="Center">Longitude</TextBlock><TextBoxGrid.Column="1"Grid.Row="7"Margin="0,0,10,10"x:Name="LonTextbox" HorizontalAlignment="Left"Text=""Width="300" />
<Buttonx:Name="myPhotoLoc"Grid.Column="1"Grid.Row="8"Click="myPhotoLoc_Click"Width="300"Margin="0,0,10,0">Open Map</Button> 

Next we look at the code behind and in the function GetImagePropertiesForDisplay. We replace the code that splits the coordinates in degrees, minutes and seconds with the snippet below.

' Do a simple check if GPS data exists.If  (m_imageProperties.Latitude.HasValue) AndAlso  (m_imageProperties.Longitude.HasValue) Then
     LatText = Math.Round(m_imageProperties.Latitude.Value,  6).ToString
     LatTextbox.Text = LatText
  LonText = Math.Round(m_imageProperties.Longitude.Value,  6).ToString
  LonTextbox.Text = LonTextEnd If

We also introduce a new function to handle the click-event on our button and launch the Map app:

Private Async Sub myPhotoLoc_Click(sender As Object, e AsRoutedEventArgs)' Create the URI to launch  from a string.Dim uri = NewUri("bingmaps:?collection=name.Photo%20Locations~point." + _
     LatText + "_" + LonText + "_My%20Photo&sty=a")
  ' Launch the URI.Dim success As Boolean = Await Windows.System.Launcher.LaunchUriAsync(uri)End Sub

And that’s it. You’ll find the complete source code here.

Happy Mapping

PhotoViewer

How the New Japan Geocoder Can Help You

$
0
0

We are pleased to announce a new Japan geocoder that improves how you search for a Japanese address on http://www.bing.com/maps and how you geocode a Japanese address using the Bing Maps APIs. In this post, we will introduce you to the new features of this geocoder and show you how to use these new features with the Bing Maps REST Services API. We’ve received more than a few inquiries about Japanese address geocoding, so we decided to write a detailed post.

You can also find details about Japanese geocoding using the Bing Maps APIs on MSDN.

Let’s start with some background.

The Bing Maps geocoder is a global geocoding service, but there are many system components that are responsible for geocoding individual markets. One such component is the Japan geocoder, which resolves Japan in-market queries. The Japan geocoder is rooftop based, and relies on Zenrin data provider address points. Zenrin has phenomenal coverage over Japan (99.6%), with the exception of a few offshore islands. Statistically, there’s about a total of 36.1 million rooftop address points that are geocodable. Also, we use more than 250,000 geocodable places of interests.

Japanese address geocoding is complicated because there are three different types of native character sets (Kanji, Hiragana, and Katakana). Also, there is a fourth non-native character set that is the romanization of Japanese using Latin script (Romaji). Let’s take a look at how the new geocoder features support these character sets and more.

New Japan Geocoder Features

Below we will introduce major features of the new geocoder and show how they solve problems users have faced. These features include support for:

  • Latin characters
  • Western and Japanese address systems
  • Mixed Latin and Kana characters
  • Kana variants

Western and Japanese Address System Support

There are times when you want to geocode a Japanese address that does not use the convention for addresses in Japan. In the past, there was a geocoding issue when a user would enter a Japanese address based on the western address convention (see below for explanation). This was often the case when the address was written in Latin characters. Also, the order of items in the address could be chaotically jumbled because of lack of understanding or some data store issue.

JapanGeocoderImage

To illustrate, if you have ever sent a letter to Japan using the western addressing system, what you write on the front of the envelope is typically in Latin characters (Romaji):

“1-12-1 Nihonbashi Chuo, Tokyo, 103-8260, Japan”

If you translate this into Japanese characters, it looks like this:

“1-12-1,日本橋,中央区,東京103-8260,日本”

However, the Japanese convention is to write this address in reverse order:

“〒103-8260東京都日本橋中央区1-12-1”

Note:In the Japanese convention, the country Japan (日本) is not required and symbols for postcode (〒) and prefecture (都) are required.

A typical Japanese address written using the Japanese addressing system uses the opposite convention of addresses in western countries such as the United States and Europe. Below is a comparison of the Japanese and western address conventions:

Japanese Addressing System

(postcode) (prefecture) (city/municipality/ward) (location in city/municipality/ward) (detailed sections)

Example: {108-0075東京都港区港南2-16-3} which reads as {108-0075Tokyo-to Minato-ku Konan2-16-3

Western Addressing System

(detailed sections)(location in city/municipality/ward) (city/municipal/ward)(prefecture)(postcode){country}

Example: “2-16-3KounanMinato-ku, Tokyo108-0075 Japan”

The new Japanese geocoder is far more robust than its predecessor and can now geocode addresses that are not in the correct order. We are no longer requiring that the address use the Japanese addressing system.

Latin Character Support

Latin-based Japanese (Romaji) is quite complex. Often users outside of Japan don’t know how to write Japanese using Latin characters. If you want to learn more, James Curtis Hepburninvented the Hepburn Romanization system for writing the Japanese language using the Latin alphabet.

In many cases, users and applications expect Hepburn (also known as Hebon) spelling and its variants to geocode successfully when they enter a Japanese address such as the following example: 

“2-16-3 Kounan Minato-ku, Tokyo-to 108-0075 Japan”

In the example above, there are suffixes like “-ku”, “-to”. These are suffixes that come from the phonetic sounds that mean ward and prefecture respectively. In the past, the geocoder would have difficulty geocoding this Hebon version of the address. However, the new Japan geocoder can handle this type of Latin address.

The new geocoder can also handle multiple forms of Japanese Hebon spelling (e.g., “Konan” instead of the Japanese phonetic spelling “Kounan”).

“2-16-3 Konan, Minato Ward, Tokyo-to, Japan”

The new geocoder can also handle a special unicode characters that are part of Hebon such as a Latin character “o” with macron ō:

“2-16-3 Kōnan, Minato-ku, Tōkyō, Japan”

The geocoder can also handle a complex query containing both Latin and Japanese characters:

“東京都Minato-ku Kounan 2-16-3”

Note that a Japanese address written in Latin form (Romaji) typically uses the western address convention that is reversed from the Japanese addressing system. The new geocoder will handle either system.

In the past, there was also confusion about whether you needed to specify “Japan” as the country in your geocode request. Before, you needed to specify “Japan” unless the address was written in Japanese. The new geocoder clears up this confusion by not requiring “Japan” for any form of Japanese addresses. If the geocoder identifies the address as a Japanese address, it will handle it.

Note that when you make a geocode request using the Bing Maps REST Services or Bing Spatial Data Services, you do not need to specify “Japan” in your address, but you will need to set the culture parameter/value to “ja”.

All in all, the new Japan geocoder is able to handle Latin and is less susceptible to variants arising from using different spellings or notations.

Additional Japanese Kana Variants Support

You may wish to create queries that contain phonetic Hiragana or Katakana representations instead of the logographic systemof Kanji as shown in the following examples:

“ながのけんすわし" (Hiragana)

“ナガノケンスワシ" (Katakana)

The English translation of this query is “Nagano Prefecture, Suwa city.”

Previous versions of the Japan geocoder have not been able to handle these type of queries. However, the new geocoder can recognize these representations as the Japanese Kanji official address:

“長野県諏訪市"

The new geocoder can also interpret partial variants that contain a mixture of Kanji, Hiragana, and Katakana:

“長野県オカヤ市"

Typically, this ability to resolve Japanese Kana instead of Kanji is extremely useful when the user remembers the sound of the address, but not the Kanji representation. While we do not guarantee that we can resolve the address for every case, the new geocoder is designed to be better at handling these cases.

Custom / Additional Address Support

The new Japan geocoder has the additional ability to handle custom addresses or unofficial addresses. For example, if an address contains additional information that is not part of the official address, you may still be able to geocode the address.

Address alone: 長野県長野市南長野

Address with extra value: 長野県長野市大字南長野

While we do not guarantee that we can geocode every possible unofficial address that arises, the new geocoder does have the framework to handle these situations.

Examples Using the Locations (Geocoding) API [Bing Maps REST Services]

Below are some examples of geocoding locations in Japan using the Locations (geocoding) APIthat is part of the suite of Bing Maps REST Services.

Note: In the examples below, you can replace the Japanese Kana with Latin characters.

Geocoding Using the Locations API Address Parameters

Geocoding using the URL address parameters should only be used when the user knows the address hierarchy mapping. Typical address mappings for Japan to the Locations API Find by Query parameters are as follows:

Used Fields

Examples

CountryRegion

{JP} :

PostalCode

{〒100-0000} : the symbol {〒} is optional.

AdminDistrict

{東京都} : in the case of Tokyo prefecture

{北海道} : in the case of Hokkaido prefecture

{大阪府} : in the case of Osaka prefecture

{福岡県} : in the case of all other prefectures

{日本} : if you want to include the country name, Japan, append in front, such as {日本、東京都}.

Locality

{港区} : in Tokyo or any other special designated cities

{西多摩郡} : in prefectures with gun

{那覇市} : similar in case of all other municipals

AddressLine

{港区2} : all address subsections below municipals

Geocoding Using the Locations API and a Single Query Address String

You can geocode by specifying a string that contains address data. Unlike using URL address parameters, you do not need to identify the different address values. For more information about what an address string would look like, refer to the address conventions described in the previous section on western and Japanese Address Systems and the mapping table above. For queries using a single address string, it is best to specify the address in the western or Japanese order as described in the western and Japanese Address Systems Support section of this post.

REST URL Examples

Note please replace the placeholder “xxx” with your Bing Maps Key in the queries below. The Latin character and Japanese Kana query examples will also work if you reverse the order of the address values. Note that you must set the culture (c) parameter to “ja” for these URLs for the geocoding to work correctly.

Category

Full Address OR corresponding address sections, coordinates

URL

full address using URL parameters

AdminDistrict = 東京都

Locality = 港区

AddressLine =
港南2-16-3

http://dev.virtualearth.net/REST/v1/Locations?
countryRegion=JP&adminDistrict
=%E6%9D%B1%E4%BA%AC%E9%
83%BD&locality=%e6%b8%af%
e5%8c%ba&addressLine=%e6%
b8%af%e5%8d%97%ef%bc%92%
e2%88%92%ef%bc%91%ef%bc%
96%e2%88%92%ef%bc%93&o=xml&
key=Xxx

Full address using query string

Query = 〒108-0075東京都港区港南2-16-3

http://dev.virtualearth.net/REST/v1/
Locations?query=%e3%80%92108%2
d0075%e6%9d%b1%e4%ba%ac%e9
%83%bd%e6%b8%af%e5%8c%ba%e
6%b8%af%e5%8d%97%ef%bc%92%e
2%88%92%ef%bc%91%ef%bc%96%e
2%88%92%ef%bc%93&o=xml&c=ja&
key=Xxx

postcode geocode using postal code parameter

PostalCode = 108-0075

http://dev.virtualearth.net/REST/v1/
Locations?countryRegion=JP&postal
Code=108-
0075&o=xml&key=Xxx&c=ja

postcode geocode using query string

Query = 〒168-0063

http://dev.virtualearth.net/REST/v1/
Locations?countryRegion=JP&postal
Code=%e3%80%92108%2d0075&o=
xml&key=Xxx&c=ja

Reverse Geocoding

Latitude=35

Longitude=139

http://dev.virtualearth.net/REST/v1/L
ocations/35,139?o=xml&key=xxx

Important Note:

For URLs containing Japanese Kana, please base64 encode the utf8 strings.

For reverse geocoding, responses are returned in Japanese native character sets only (Kanji, Katakana, or Hiragana).

Final Words

Japan geocoding is complex because, linguistically, it must handle four types of characters (Kanji, Katakana, Hiragana, and Romaji). Three of these types of characters are native to the language (Kanji, Katakana, and Hiragana) while the fourth uses Latin (Romaji) ) characters. Romaji is not commonly used by Japanese native users for addresses. However, its usage has become more common, especially for clients outside of Japan who are interested in geocoding addresses in Japan, but cannot read Japanese.

Also, Japanese addresses do not contain street names. (There are exceptions like Kyoto Toorina addresses). Japanese addresses are generally based on blocks with representative centroids.

Japanese geocoding is not difficult if you understand the Japanese addressing system and how to map Japanese address values to the Locations API URL address parameters. If you are not comfortable with the address parameters, you can stick to the query string method. The new features of the Japanese geocoder that allow for addresses to be specified out-of-order and that accept Latin and other Japanese character sets should help you with your Japanese geocoding needs.

- Tetsuaki Otsuki, Program Manager, Bing Maps Japan

Localizing Custom Mapping Data With Bing Translator

$
0
0

The Bing Maps for Enterprise platform offers extensive localization capabilities for map navigation, map labels, directions, and geocoding results in the AJAX v7 map control, Windows Store map control, and REST Services. Typically, you will also have your own custom data that you will be displaying within your mapping application. In this post, we will show how you can use another of the Bing Developer Services – the Bing Translator Control and Microsoft Translator API– to localize your own custom content within your Bing Maps applications. We will use the Microsoft Translator API to demonstrate how we can localize custom content in a web application using our AJAX v7 map control, and we will also use the Bing Translator Control to show how we can localize custom content in native Windows Store apps.

Prerequisites for building our applications include:

Signing Up for Microsoft Translator on Windows Azure Marketplace

As outlined in the Bing Developer Center, the Translator Service offers easy access to robust, cloud-based, automatic translation between more than 40 languages. But before we can get started with building our application, we must subscribe to the Translator Service through the Windows Azure Marketplace, Microsoft’s one-stop shop for premium data and applications.

A detailed walkthrough of the process of signing up for the Translator service and obtaining credentials is provided here. To briefly summarize the process, you must:

  1. Sign in and register for the Windows Azure Marketplace
  2. Subscribe to the Microsoft Translator API
  3. Register an application on the Windows Azure Marketplace
  4. Obtain your authentication credentials (client ID and client secret)

Creating Our Sample Custom Data Source

In both of our sample applications, we will be pulling in data from a very simple custom data source that is hosted in Bing Spatial Data Services. The data source will contain only five fields: a unique EntityId field; Latitude and Longitude for location; and Name and Description fields, which will be translated in our apps. You can download the csv file which can be uploaded via the Data Source Management API to your own custom data source. Alternately, the code samples are already configured to access the data from an existing publicly accessible data source containing this data.

Creating a Web Application with AJAX v7 and Translator API

The first sample application we will build will be a web application that accesses the sample data via the Query API, and displays the locations as pushpins on the AJAX v7 map control. When the pushpins are clicked, the Name and Description content for the location are translated via the Translator API.

We will now create a simple project using the ASP.NET Empty Web Application template in Visual Studio 2012.

The first thing we will add to our application is a Token.cs class which helps us obtain an access token for the Translator API. The access token is obtained by making an HTTP POST request to a token service, passing the client ID and client secret that we previously obtained when signing up for the Translator service. We will populate this class with the sample code provided in the Translator SDK, here.

The Token.cs class also requires us to add the following references to our project:

  • System.Runtime.Serialization
  • System.ServiceModel

To our application, we will add a C# web form called translate.aspx. To the code-behind, we will add the following code, which will use our client ID and client secret to obtain an access token, which is added as a property that will be accessible in our JavaScript code. Populate the placeholders with your own Translator credentials:

using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.UI;using System.Web.UI.WebControls;
namespace BingMapsTranslator
{public partial classtranslate : System.Web.UI.Page 
   {protected void Page_Load(object sender, EventArgs e)
        {
            AdmAuthentication admAuth = new AdmAuthentication("client  id", client 
secret"
); AdmAccessToken token = admAuth.GetAccessToken();
            Response.Write(string.Format(@" <script  type=""text/javascript""> 
                window.accessToken =  ""{0}""; </script>", token.access_token));
} }
} 

Now we will begin adding code to our translate.aspx web form. Aside from our JavaScript code, the page layout will be very basic. We have a map element which occupies the entire page, and we link to the Bing Maps AJAX v7 map control. Note how we are using a mkt parameter to localize the map navigation and labels in French. Also note how we will call the GetMap function when the page loads:

<%@PageLanguage="C#"AutoEventWireup="true"CodeBehind="translate.aspx.cs"
Inherits="BingMapsTranslator.translate"%>
<!DOCTYPEhtml PUBLIC"-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html><head><title>Translate Custom SDS Data Demo</title><metahttp-equiv="Content-Type"content="text/html;charset=utf-8"/> <styletype="text/css">html {overflow:hidden;font-family: Verdana;font-size: 12px;width:100%;height:100%; }body {width:100%; height:100%; margin:0 0 0 0; }</style><scripttype="text/javascript"
src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0&mkt=fr-
FR"></
script>
</head><bodyonload="GetMap();"> <divid='myMap'style="position:absolute; width:100%; height:100%;"></div></body>
</html>

To our web form, we will add some JavaScript to declare some global variables, and to define our GetMap function which will instantiate our map. Note that we also add an event handler to allow us to query our data source when the map view changes. We also extend the Pushpin class with title and description properties, which will be used to hold data to be displayed later. You will need to substitute your own Bing Maps key in the placeholder for the bmKey value:

// Configure global variables:var bmKey = "insert  Bing Maps Key here";var map = null;var MM = Microsoft.Maps;var datasourceLayer = null;var infoBoxLayer = null;
// Instantiate the map GetMap() {
   map = new MM.Map(document.getElementById('myMap'), {
       credentials: bmKey,
       mapTypeId: MM.MapTypeId.road,
       center: new MM.Location(43.4702518, -80.7727581),
       zoom: 6
   });
   // Add Event to search for  POI when map moved: 
   Microsoft.Maps.Events.addThrottledHandler(map, 'viewchangeend', QueryDatasource, 
2000);
   // Extend Pushpin class with  title and description properties:
   MM.Pushpin.prototype.title = null;
   MM.Pushpin.prototype.description = null;
}

In our QueryDatasource event handler function, which is called when the map view changes, we obtain the credentials from our map session, specifying QueryDatasourceRequest as our callback. QueryDatasourceRequest receives the credentials, and uses them in a request to the Spatial Data Services Query API. The current bounds of the map are obtained, and used to query our sample data using a bounding box spatial filter, and specifying json as the output format. The request is issued with the CallRestService function, which adds the request URL as the source for a JavaScript resource. We specify callbackQueryDatasource as our callback function in our request:

// retrieve map credentials and specify  callback function: function QueryDatasource() {
   map.getCredentials(QueryDatasourceRequest);
}
// retrieve custom data in the current  map view: function QueryDatasourceRequest(credentials) {  // Get Map bounds for spatial  query by bounding box:var bounds = map.getBounds();var south = bounds.getSouth();var north = bounds.getNorth(); var east = bounds.getEast();var west = bounds.getWest();
   // SDS data source details: var sdsAccessId = "8687519045ca4eeabafb179cfe28e545";var sdsDataSourceName = "SamplePropertyData";var sdsEntityTypeName = "SampleProperties";
   // format request to Query  API, using bounding box spatial filter, and specifying 
callback
var request = "http://spatial.virtualearth.net/REST/v1/data/" + sdsAccessId + "/" + sdsDataSourceName + "/" + sdsEntityTypeName + "?" +"key=" + credentials +"&$format=json" +"&jsonp=callbackQueryDatasource" +"&spatialFilter=bbox(" + south + "," + west + "," + north + "," + east +
")&$select=*&$top=250";
   // Call rest service 
   CallRestService(request);
} 
// function for making requests to REST  APIs:function CallRestService(request) {var script = document.createElement("script");
   script.setAttribute("type", "text/javascript");
   script.setAttribute("src", request);
   document.body.appendChild(script);
}  

In our callbackQueryDatasource function, we check to make sure we have received results, and then loop through each of the individual entities, adding a pushpin for each, using the coordinates to place the pushpin, and adding the Name and Description to our pushpin title and description properties.

We also add a pushpin click event handler, translateInfoBox, to each pushpin:

// display the custom data as pushpins on  the map: function callbackQueryDatasource(result) {// Ensure we have results: if (result &&
      result.d &&
      result.d.results) {try {   // configure our entity collections:           try {
              datasourceLayer.clear();
              infoBoxLayer.clear();
      }catch (err) {
          datasourceLayer = new MM.EntityCollection;
          infoBoxLayer = new MM.EntityCollection;
      }// for each result, create a pushpin and add it to the map:       for (i = 0; i <  result.d.results.length; i++) {   var  pushpinOptions = {};var pushpin  = new MM.Pushpin(new
MM.Location(result.d.results[i].Latitude, result.d.results[i].Longitude), pushpinOptions);
    // Add title and description to pin:
          pushpin.title =  result.d.results[i].Name;
          pushpin.description =  result.d.results[i].Description;
     // Add event handler to translate content when pushpin clicked
         pushpinClick =  MM.Events.addHandler(pushpin, 'click', 
translateInfoBox);// Add pin to entity collection
        datasourceLayer.push(pushpin);
      }// Add entity collections to map:
      map.entities.push(datasourceLayer);
      map.entities.push(infoBoxLayer);
  }catch (err) {
  }
 }else {
    alert('An error  occurred.');
 }
}  

Our translateInfoBox functionis called when a pushpin is clicked, and we construct a request to the Translator API AJAX interface, using theTranslateArray method, which allows us to translate an array of individual text blocks. The parameters that we pass in the request include:

  • appId– our access token, retrieved from window.accessToken, which we previously populated after using the token service with our credentials
  • from – the language we are translating from; we hardcode this as ‘en’, as our data source contains content in English
  • to– the language we are translating to; in this instance, we hardcode this as ‘fr’ to translate into French; note that we can use the GetLanguagesForTranslate method of the Translator API to dynamically retrieve all possible languages if desired
  • texts– an array of the text blocks we wish to translate, including the pushpin title and description properties
  • options – we can specify a number of optional options, including State; we use the State option to correlate the response to our request with the appropriate pushpin
  • oncomplete – we specify our callback function, showInfoBox

Note that the supported cultures for the Bing Maps APIs and map controls differ from the languages supported by the Translator APIs. In a production application, some consideration should be given to the overall application localization you wish to support, and associating the localization and translation parameters as appropriate.

We use our previously shown CallRestService function to issue the request:

function translateInfoBox(e) {if (e.targetType = "pushpin") {// Specify languages to  translate to and from//  Note that you can use the  GetLanguagesForTranslate method to //  dynamically retrieve supported languagesvar to = "en";var from = "fr";
   // obtain the pushpin index  in the entity collection, and send it in the 
   //  state parameter, enabling us to show the  infobox next to the correct 
pushpin:   var options = "{\"State\":\"" + datasourceLayer.indexOf(e.target) + "\"}";
   // create an array of text  blocks to translate:    var texts = "[\"" + e.target.title + "\",\"" + e.target.description + "\"]";
   // format a request to the  AJAX API, using the TranslateArray method:   var translateRequest = 
"http://api.microsofttranslator.com/V2/Ajax.svc/TranslateArray" +"?appId=Bearer " + encodeURIComponent(window.accessToken) +"&from=" + encodeURIComponent(to) +"&to=" + encodeURIComponent(from) +"&texts=" + encodeURIComponent(texts) +"&options=" + encodeURIComponent(options) +"&oncomplete=showInfoBox";
   // Issue the request
   CallRestService(translateRequest);
  }
}  

In our showInfoBox callback function, we receive the response from the TranslateArray request. We use the State attribute in the response to correlate the response to the correct pushpin. We then instantiate an Infobox, and use the translated string elements in the response array to display the translated name and description for the location:

// callback from translator request, to  display infobox with translated text: function showInfoBox(response) {   // retrieve the pushpin's  index in the datasource layer from the 
   //  State attribute in the response:var pushpin =  datasourceLayer.get(response[0].State);//Add the infobox, displaying  the translated Name as the title, and the 
   //  translated Description as the description: 
   infoBoxLayer.clear();
   infobox = new MM.Infobox(pushpin.getLocation(), {  title: 
response[0].TranslatedText, description: response[1].TranslatedText });
   infoBoxLayer.push(infobox);
}  

When we hit F5 in Visual Studio to run our application, we now see not only French-labeled navigation in our map control, but French translations for our infobox content as well:

LocalizingCustomMappingDataImg1

Creating a Windows Store Application with the Native Bing Maps Control and the Translator Control

The second sample application we will build will be a native Windows Store application that will access the same sample data via the Query API, and displays the locations as pushpins on the native Bing Maps for Windows Store Apps control. When the pushpins are clicked, the Name and Description content for the location are translated via the Translator Control.

Before we start, we must download and install the Translator Control for Visual Studio.

In Visual Studio 2012, we will first create a new project using the Visual C# Windows Store Blank App (XAML) template, and will name our project TranslateControl. We will also choose our platform target in the project Build properties as desired.

We now add the following references to our project:

  • Bing Maps for C#, C+ + or Visual Basic
  • Microsoft Visual C+ + Runtime Package
  • Bing Translator Control

Our UI will be very basic, and will leverage the infobox capabilities previously presented in the Infoboxes for Native Windows Store Apps post. We will copy the same XAML code from the previous post, and to it we will add an XML namespace declaration for Bing.Translator, a TranslatorControl with our credentials included, and will set our Culture property to fr-FR, to display our navigation controls and some map labels in French. Our final XAML code in MainPage.xaml will be as shown below. Substitute your own credentials for the Translator Control client id and client secret, as well as for the Bing Maps key:

<Pagex:Class="TranslatorControl.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:TranslatorControl"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:bt="using:Bing.Translator"xmlns:m="using:Bing.Maps"mc:Ignorable="d">
    <GridBackground="{StaticResourceApplicationPageBackgroundThemeBrush}"><bt:TranslatorControlClientId="client id"ClientSecret="client secret"x:Name="myTranslator"/> <m:Mapx:Name="myMap"Credentials="insert Bing Maps Key"Culture="fr-FR"> 
            <m:Map.Children><!-- See  http://www.bing.com/blogs/site_blogs/b/maps/archive/2013/06/17/infoboxes-for-native-
windows-store-apps.aspx  for infobox post--><!-- Data Layer--> <m:MapLayer Name="DataLayer"/> 
                <!--Common Infobox--><m:MapLayer><Gridx:Name="Infobox"Visibility="Collapsed"Margin="0,-115,-
15,0">
<BorderWidth="300"Height="110"Background="Black"Opacity="0.8"BorderBrush="White"BorderThickness="2"CornerRadius="5"/>
                        <StackPanelHeight="100"Margin="5"> <GridHeight="40"><TextBlockText="{BindingTitle}"FontSize="20"Width="250"TextWrapping="Wrap"HorizontalAlignment="Left" /><ButtonContent="X"Tapped="CloseInfobox_Tapped"HorizontalAlignment="Right"VerticalAlignment="Top"/> </Grid><ScrollViewerHorizontalScrollBarVisibility="Auto"VerticalScrollBarVisibility="Auto"MaxHeight="60"><TextBlockText="{BindingDescription}"FontSize="16"Width="290"TextWrapping="Wrap"Height="Auto"/></ScrollViewer></StackPanel></Grid></m:MapLayer></m:Map.Children></m:Map></Grid></Page>

In our MainPage.xaml.cs code-behind, we add the following using statements:

using Bing.Maps;using Bing.Translator;using System.Net;using Windows.Data.Json;
using System.Threading.Tasks;

We now update our constructor to set the map view, and to call the LoadSDSData method to retrieve our data:

public MainPage()
{this.InitializeComponent();// Set map view:
   myMap.SetView(newLocation(41.4702518,-80.7727581),  6, MapAnimationDuration.None);// Load data from Spatial  Data Services:
   LoadSDSData();
}  

In our LoadSDSData method, we mark the method with the async modifier, to allow us to request data from the Spatial Data Service asynchronously with the await operator. The method retrieves the current map bounds, and constructs a request URI for the Query API, using our sample data source, and using a bounding box spatial filter. We also specify the response format as json. The request is issued in the GetResponse method.

For simplicity, a StreamReader object is used to read the response stream, with the response parsed into a JsonObject. We use the tools provided in the System.Json namespace to retrieve the coordinates, name and description for each result. We then add a pushpin for each result with the AddPushpin method, as taken from the previously referenced infobox post:

private async void LoadSDSData()
{// get map bounds:   LocationRect lr = myMap.Bounds;string north = lr.North.ToString();string east = lr.East.ToString();string south = lr.South.ToString();string west = lr.West.ToString();
   // SDS data source details:   string sdsAccessId = "8687519045ca4eeabafb179cfe28e545";string sdsDataSourceName = "SamplePropertyData";string sdsEntityTypeName = "SampleProperties";
   // retrieve credentials for  use in SDS request:   string credentials = await myMap.GetSessionIdAsync();
   // build request to SDS Query  API, filtering by bounding box:   string uriString = 
string.Format("http://spatial.virtualearth.net/REST/v1/data/{0}/{1}/{2}?" +"key={3}&$format=json&spatialFilter=bbox({4},{5},{6},{7})&$select=*&$top=250", sdsAccessId, sdsDataSourceName, sdsEntityTypeName, credentials, south, west,
north, east);
    Uri queryRequest = new Uri(uriString);
    try
     {// Obtain response as  JsonObject: 
   JsonObject j = await  GetResponse(queryRequest);// Retrieve Array of results:JsonObject d = (JsonObject)j.GetNamedObject("d");JsonArray ja = (JsonArray)d.GetNamedArray("results"); // for each result, create a  pushpin using the AddPushpin methodint arraySize = ja.Count;for (int i = 0; i <  arraySize; i++)
   {
        AddPushpin(new Location(ja[i].GetObject().GetNamedNumber("Latitude"), 
ja[i].GetObject().GetNamedNumber("Longitude")),
        ja[i].GetObject().GetNamedString("Name"), 
ja[i].GetObject().GetNamedString("Description"), DataLayer);
        }
   }catch (Exception e)
   {// TO-DO: implement error  handling:  return;
   }           
} private asyncTask<JsonObject> GetResponse(Uri uri)
{// issue Query API request  with HttpClient: 
   System.Net.Http.HttpClient client = new  System.Net.Http.HttpClient();var response = await client.GetAsync(uri);
   // read response with  StreamReader, and parse string into JsonObject:using (var stream = await response.Content.ReadAsStreamAsync())
   {StreamReader streamRead = newStreamReader(stream);string responseString = streamRead.ReadToEnd();return JsonObject.Parse(responseString)  asJsonObject;
   }
}  

We re-use the pushpin and infobox code from the previous infobox post, with one modification: we use the Translator Control to asynchronously translate our entity name and description, using the TranslateAsync method of the Translator Library. To this method, we pass the following parameters:

  • From language - our source data is in English, so we pass in ‘en’
  • To language - we will translate our data into French with a value of ‘fr’
  • Category - domain of the translation; we use the default category of ‘general’
  • Text to translate - our entity name or description

// configure to and from languages for  translation: string fromLang = "en";string toLang = "fr";
// translate title and description  asynchronously:TranslationResult transTitle = await
myTranslator.TranslatorApi.TranslateAsync(fromLang, toLang, "general", m.Title);TranslationResult transDesc = await myTranslator.TranslatorApi.TranslateAsync(fromLang, toLang, "general", m.Description);
m.Title = transTitle.TextTranslated;
m.Description = transDesc.TextTranslated;

Note that we could also retrieve a list of available languages for translation through the GetLanguagesAsync method of the Translator Library, if desired. The full pushpin and infobox code, including our translation updates, is shown below:

#region pushpinpublic void AddPushpin(Location latlong, string title, string description, MapLayer
layer)
{
    Pushpin p = newPushpin()
     {
         Tag = newMetadata()
         {
             Title = title,
             Description = description
         }
     };
    MapLayer.SetPosition(p, latlong);
    p.Tapped += PinTapped;
    layer.Children.Add(p);
}
public classMetadata
{public string Title { get; set; }
   public string Description { get; set; }
   }
private asyncvoid PinTapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
   {Pushpin p = sender as Pushpin;Metadata m = (Metadata)p.Tag;
   // configure to and from  languages for translation:   string fromLang = "en";string toLang = "fr";
   // translate title and description  asynchronously:   TranslationResult transTitle = await
myTranslator.TranslatorApi.TranslateAsync(fromLang, toLang, "general", m.Title);TranslationResult transDesc = await
myTranslator.TranslatorApi.TranslateAsync(fromLang, toLang, "general", m.Description);
    m.Title = transTitle.TextTranslated;
    m.Description = transDesc.TextTranslated;
    //Ensure there is content to  be displayed before modifying the infobox control    if (!String.IsNullOrEmpty(m.Title)  || !String.IsNullOrEmpty(m.Description))
    {
        Infobox.DataContext = m;
        Infobox.Visibility = Visibility.Visible;
        MapLayer.SetPosition(Infobox, MapLayer.GetPosition(p));
   }else 
   {
      Infobox.Visibility = Visibility.Collapsed;
   }
}
private void CloseInfobox_Tapped(object sender,
Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
{
   Infobox.Visibility = Visibility.Collapsed;
}
#endregion  

As with our previous application, when we hit F5 in Visual Studio, we now see not only French navigation in our map control, but French translations for our infobox content as well:

LocalizingCustomMappingDataImg2

We have shown how we can use the power of Bing as a developer platform to not only map our custom location data on the web and in Windows Store apps, but to also localize our custom mapping data and other application content for global audiences, using a world-class machine translation system built on over a decade of natural language research from Microsoft Research.

The samples in this application use the Translator Service to translate text at application runtime. When developing your application, you will want to consider whether your consumption of the service can be optimized by translating text as part of your content editorial workflow instead. Considerations to take into account include:

  • The frequency with which your custom content changes
  • The number of languages you wish to support with translations
  • The degree to which your custom data source infrastructure supports storing content for multiple languages
  • The frequency with which translations will be requested by end users

The complete code for both projects, as well as the sample SDS data source can be found here.

- Geoff Innis, Bing Maps Technical Specialist

Bing Maps Publishes 13 Million Square Kilometers of Imagery

$
0
0

This month, we are excited to announce 13 million sq km, or 315.92 terabytes, of new aerial imagery from around the world. Click on the links below to explore the new and exciting imagery from Bing Maps.

Satellite_divider_550x3_thumb_7817EE23

Satellite

Satellite

High-resolution nadir or "straight down" orthophotographs taken by aircraft or satellite

Satellite_divider_550x3_thumb_7817EE23

We routinely refresh our imagery and are happy to announce this latest update. The map below shows the coverage and location of the aerial imagery recently published:

SAT_CoverageMap

 
SAT_Iceland
Ísafjarðarbær, Iceland
Bing Maps Link
SAT_France
Maritimes Alps, France
Bing Maps Link

Satellite_divider_550x3_thumb_7817EE23

SAT_Rio
Rio De Janeiro, Brazil
Bing Maps Link
SAT_Scilly
Hugh Town, Scilly Isles, England
Bing Maps Link

Satellite_divider_550x3_thumb_7817EE23

Additional Highlights

View additional imagery from around the world below:

- Bing Maps Team

Draggable Pushpins in Bing Maps (.NET)

$
0
0

When building a Bing Maps application, you may want to give the user the ability to drag a pushpin. In the JavaScript version of Bing Maps this can be done by setting the draggable property of a pushpin to true, but the Pushpin class in the .NET version does not have this property, so what can you do?

In this blog post, I’ll show you how to create a reusable user control that gives you draggable pushpins, plus a lot more flexibility in terms of customization.

Some common uses for draggable pushpins include:

  • Allowing the user to edit locations on the map by moving pushpins.
  • Using draggable pushpins as handles for data points in drawing tools.

Creating the user control

To get started, open Visual Studio and create a new project in C# or Visual Basic. Select the Blank App (XAML) template, name the application and then press OK.

AddNewItem-DraggablePushpins

Next, add a reference to the Bing Maps SDK. Right click on the References folder and press Add Reference. Select Windows → Extensions, and then select Bing Maps for C#, C++ and Visual Basic. If you do not see this option, be sure to verify that you have installed the Bing Maps SDK for Windows Store apps. While you are here, also add a reference to the Microsoft Visual C++ Runtime Package, as this is required by the Bing Maps SDK when developing using C# or Visual Basic. Press OK.

ReferenceManager-DraggablePushpins

In Solution Explorer, set the Active solution platform in Visual Studio by right clicking on the Solution folder and selecting Properties. Select Configuration Properties → Configuration. Find your project and under the Platform column, set the target platform to x86, and press OK.

ConfigurationManager-DraggablePushpins

  Right click on the solution folder and select Add → New Item. Select the User Control template and give it a name, such as “DraggablePin.” Open the DraggablePin.xaml file and update the XAML to the following:

<UserControl x:Class="DraggablePushpin.DraggablePin" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="using:DraggablePushpin"xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"> <EllipseHeight="30"Width="30"Fill="Blue"             Stroke="White"StrokeThickness="5"Margin="-15,-15,0,0"/>
</UserControl>

Now open DraggablePin.xaml.cs or DraggablePin.xaml.vb.

The code-behind for this user control will need a reference to the map control in its constructor and a local reference to it stored in a private variable. This control will use a number of events on the map to enable the dragging functionality.

Next, override the OnPointerPressed event handler to add the required map events for dragging the user control. When the user control is initially pressed, the coordinate of the center of the map will be stored. Next, fill in the ViewChange event of the map to set the center of the map to the stored center value, which will keep the map from panning when dragging the user control.

Set the PointerMovedOverride event of the map to update the position of the user control as it is dragged. Then set the OnPointerReleasedOverride event of the map to release all the events from the map that were used for dragging the user control.

Now add a Boolean property called Draggable, which can be used to disable the dragging functionality of the pushpin. Lastly, expose three Action events: DragStart, Drag, and DragEnd, for use in future applications.

The following example shows the complete code for the user control.

C#

using  Bing.Maps;using  System;using  Windows.UI.Xaml.Controls;using  Windows.UI.Xaml.Input;
namespace  DraggablePushpin
   {public sealed partial classDraggablePin : UserControl 
   { privateMap  _map;private bool  isDragging = false;Location  _center;
        public  DraggablePin(Map map)
   {this.InitializeComponent();
            _map = map;
   }
public sealed partial classDraggablePin : UserControl { this.InitializeComponent();
     _map = map;
   }
   /// <summary>/// A  boolean indicating whether the pushpin can be dragged. /// </summary>    public bool  Draggable { get; set; }
   /// <summary> /// Occurs when the pushpin is being dragged. /// </summary>    publicAction<Location>  Drag;
   /// <summary> ///Occurs when the pushpin starts being dragged./// </summary>   publicAction<Location>  DragStart;
   /// <summary> ///Occurs when the pushpin stops being dragged. /// </summary>    publicAction<Location>  DragEnd;
   protected override void  OnPointerPressed(PointerRoutedEventArgs e)
   {base.OnPointerPressed(e);
       if  (Draggable)
       {if  (_map != null)
         {            //Store the  center of the map
              _center = _map.Center;
             //Attach events  to the map to track touch and movement events 
             _map.ViewChanged +=  Map_ViewChanged;
             _map.PointerReleasedOverride += Map_PointerReleased;
             _map.PointerMovedOverride  += Map_PointerMoved;
         }
                var  pointerPosition = e.GetCurrentPoint(_map);
                Location  location = null;
                //Convert the  point pixel to a Location coordinate               if  (_map.TryPixelToLocation(pointerPosition.Position, out  location))
                                            {
                    MapLayer.SetPosition(this,  location);
               }
                if  (DragStart != null)
               {
                     DragStart(location);
              }
              //Enable Dragging               this.isDragging  = true;
           }
        }
        private void  Map_PointerMoved(object sender, PointerRoutedEventArgs e)
        {//Check if the  user is currently dragging the Pushpin          if (this.isDragging)
        {             //If so, move the  Pushpin to where the pointer is.             var  pointerPosition = e.GetCurrentPoint(_map);
           Location  location = null;
             //Convert the  point pixel to a Location coordinate              if  (_map.TryPixelToLocation(pointerPosition.Position, out  location))
             {MapLayer.SetPosition(this,  location);
             }
             if  (Drag != null)
             {
               Drag(location);
             }
         }
     }
     private void  Map_PointerReleased(object sender, PointerRoutedEventArgs e)
    {//Pushpin  released, remove dragging events           if  (_map != null)
          {
               _map.ViewChanged -=  Map_ViewChanged;
               _map.PointerReleasedOverride -=  Map_PointerReleased;
               _map.PointerMovedOverride -=  Map_PointerMoved;
          }
          var  pointerPosition = e.GetCurrentPoint(_map);
          Location  location = null;
          //Convert the  point pixel to a Location coordinate 
          if  (_map.TryPixelToLocation(pointerPosition.Position, out  location))
          {MapLayer.SetPosition(this,  location);
          }
          if  (DragEnd != null)
          {
               DragEnd(location);
          }
            this.isDragging  = false;
       }
       private void  Map_ViewChanged(object sender, ViewChangedEventArgs e)
       {
       if  (isDragging)
       {         //Reset the map  center to the stored center value. 
           //This prevents  the map from panning when we drag across it. 
           _map.Center = _center;
       }
     }
   }
}  

Visual Basic

Imports  Bing.Maps
Public NotInheritable Class DraggablePin  Inherits UserControl 
   Private _map As Map  Private isDragging As Boolean = False Private _center As Location 
   Public Sub New(map As Map)
   Me.InitializeComponent()
        _map = mapEnd Sub
   '''<summary>   ''' A boolean indicating whether  the pushpin can be dragged. '''</summary>Public Property  Draggable() As Boolean  Get Return  m_DraggableEnd Get Set(value  As Boolean)
       m_Draggable = ValueEnd Set  End Property Private m_Draggable As Boolean
   ''' <summary>   ''' Occurs when the pushpin is  being dragged. ''' </summary>Public Drag As Action(OfLocation)
   ''' <summary>   ''' Occurs when the pushpin starts  being dragged.''' </summary>Public DragStart As Action(OfLocation)
   ''' <summary>   ''' Occurs when the pushpin stops  being dragged.''' </summary>Public DragEnd As Action(OfLocation)
Protected OverridesSub  OnPointerPressed(e AsPointerRoutedEventArgs)MyBase.OnPointerPressed(e)
   If  Draggable ThenIf _map  IsNot Nothing Then ''Store the  center of the map
          _center = _map.Center
          ''Attach events  to the map to track touch and movement events          AddHandler  _map.ViewChanged, AddressOf Map_ViewChangedAddHandler  _map.PointerReleasedOverride, AddressOf  Map_PointerReleased        AddHandler  _map.PointerMovedOverride, AddressOf Map_PointerMovedEnd If 
       Dim  pointerPosition = e.GetCurrentPoint(_map)
       Dim  location AsLocation = Nothing
       ''Convert the  point pixel to a Location coordinate       If  _map.TryPixelToLocation(pointerPosition.Position, location) Then MapLayer.SetPosition(Me,  location)End If 
       If  DragStart IsNot Nothing Then 
           DragStart(location)End If
       ''Enable Dragging       Me.isDragging  = TrueEnd If
End Sub 
Private Sub  Map_PointerMoved(sender As Object, e As PointerRoutedEventArgs)''Check if the user  is currently dragging the Pushpin   If Me.isDragging  Then''If so, move the  Pushpin to where the pointer is.        Dim  pointerPosition = e.GetCurrentPoint(_map)
        Dim  location As Location = Nothing 
        ''Convert the  point pixel to a Location coordinate        If  _map.TryPixelToLocation(pointerPosition.Position, location) Then MapLayer.SetPosition(Me,  location)End If 
        If Drag  IsNot Nothing Then 
           Drag(location)     End If End If End Sub 
 Private Sub  Map_PointerReleased(sender As Object, e As PointerRoutedEventArgs)''Pushpin  released, remove dragging events If _map  IsNot Nothing Then RemoveHandler  _map.ViewChanged, AddressOf Map_ViewChangedRemoveHandler  _map.PointerReleasedOverride, AddressOf  Map_PointerReleasedRemoveHandler  _map.PointerMovedOverride, AddressOf Map_PointerMoved End If 
        Dim  pointerPosition = e.GetCurrentPoint(_map)
        Dim  location AsLocation = Nothing
        ''Convert the  point pixel to a Location coordinate If  _map.TryPixelToLocation(pointerPosition.Position, location) Then MapLayer.SetPosition(Me,  location)End If 
   If  DragEnd IsNot Nothing Then
       DragEnd(location)End If
   Me.isDragging  = False End Sub 
Private Sub  Map_ViewChanged(sender As Object, e As ViewChangedEventArgs) If  isDragging Then''Reset the map  center to the stored center value. 
       ''This prevents  the map from panning when we drag across it.
   _map.Center = _centerEnd If  End Sub 
End Class

Implementing the user control in an application

Now that you have a DraggablePin user control, you can now try using it in an app. Open MainPage.xaml and update the XAML to the following:

<Pagex:Class="DraggablePushpin.MainPage"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:DraggablePushpin" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"    xmlns:m="using:Bing.Maps"> 
    <GridBackground="{StaticResourceApplicationPageBackgroundThemeBrush}">        <m:MapName="MyMap"Credentials="YOUR_BING_MAPS_KEY"/>
        <BorderBackground="Black"HorizontalAlignment="Center"VerticalAlignment="Bottom">            <TextBlockName="CoordinatesTbx"FontSize="24"Margin="10"/>        </Border></Grid>
</Page>

Open MainPage.xaml.cs or MainPage.xaml.vb.

In the constructor, add a handler to the MapLoaded event of the map. Set the handler to create a DraggablePin and add it to the center of the map. Then attach to the Drag event of the pin and have it update the coordinates displayed in a TextBlock at the bottom of the map, as shown in the following example.

C#

using  Windows.UI.Xaml.Controls;
namespace  DraggablePushpin
{public sealed partial classMainPage : Page 
 {public  MainPage()
   {this.InitializeComponent();
   MyMap.Loaded += MyMap_Loaded;
 }
 private void  MyMap_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
 {DraggablePin pin  = new DraggablePin(MyMap);
   //Set the  location of the pin to the center of the map. 
   Bing.Maps.MapLayer.SetPosition(pin,  MyMap.Center);
   //Set the pin as  draggable.
   pin.Draggable = true;
   //Attach to the  drag action of the pin.
   pin.Drag += Pin_Dragged;
   //Add the pin to the map.
   MyMap.Children.Add(pin);
 }
 private void  Pin_Dragged(Bing.Maps.Location location)
 {
   CoordinatesTbx.Text = string.Format("{0:N5},{1:N5}",  location.Latitude, 
location.Longitude); } }
}  

Visual Basic

Imports  Windows.UI.Xaml.Controls
Public NotInheritable ClassMainPage  InheritsPage
  Public Sub New()
       InitializeComponent()
    AddHandler  MyMap.Loaded, AddressOf MyMap_LoadedEnd Sub
  Private Sub  MyMap_Loaded(sender As Object, e As  Windows.UI.Xaml.RoutedEventArgs)Dim pin As NewDraggablePin(MyMap)
    ''Set the  location of the pin to the center of the map.
     Bing.Maps.MapLayer.SetPosition(pin,  MyMap.Center)
    ''Set the pin as  draggable.
     pin.Draggable = True
    ''Attach to the  drag action of the pin.
     pin.Drag = AddressOf  Pin_Dragged
    ''Add the pin to  the map.
     MyMap.Children.Add(pin)End Sub 
  Private Sub  Pin_Dragged(location As Bing.Maps.Location)
   CoordinatesTbx.Text = String.Format("{0:N5},{1:N5}",  location.Latitude, 
location.Longitude)End Sub
End Class

Now if you run the application you will see a blue circle pushpin in the center of the map. Simply press down on it and drag it around on the screen using a mouse or your finger if you have a touch screen device.

DraggablePushpins

You can find the complete source code for this blog post in the MSDN Code Sample Gallery. If you are working with the WPF version of the control, I have a similar code sample available here.

- Ricky Brundritt, EMEA Bing Maps TSP

Updated Bing Maps SDKs for Windows Store Apps, for Windows 8.0 and 8.1

$
0
0

Today we are announcing the availability of updated Bing Maps SDKs for Windows Store apps for both Microsoft Windows 8.0 and 8.1 in the Visual Studio Gallery.

The Bing Maps SDK for Windows Store Apps update for Microsoft Windows 8.1, which consists of the Bing Maps SDK for JavaScript (for JavaScript Developers) and Bing Maps SDK for Windows Store apps (for Visual Basic, C# and C++ Developers), has been updated for the Microsoft Windows 8.1 release. The SDKs also contain bug fixes and the new Bing logo that was announced on September 16, 2013.

Note that the Windows 8.1 release of the Bing Maps SDK is only compatible with Microsoft Windows 8.1 RTM or later, and Microsoft Visual Studio 2013 RC or later. Windows 8.1 RTM and Visual Studio 2013 RC are only available to MSDN Subscribers until Windows 8.1 GA later in October. In order to submit apps developed using this release to the Windows 8.1 Store, the apps will need to be rebuilt using Microsoft Windows 8.1 GA and Microsoft Visual Studio 2013 RTM or later, when released.

Developers using the Preview release of Microsoft Windows 8.1 should continue to use the Bing Maps SDK for Windows Store apps (Beta for Windows 8.1 Preview) which is available for download from the Visual Studio Gallery.

The Bing Maps SDK for Windows Store Apps update for Microsoft Windows 8.0 contains bug fixes and the new Bing logo. Incorporating the new logo into an existing application requires the application to be rebuilt using the updated SDK.

Resource:

Create a Windows Store app (for Windows 8.1)

- Bing Maps Team


Clustering Pushpins in Windows Store Apps

$
0
0

Clustering of pushpins in Bing Maps consists of grouping together nearby locations into clusters. As the user zooms in, the clusters break apart to reveal the individual locations. The goal of this process is to reduce the number of pushpins that are displayed on the map at any given time. This results in better performance of the map control and also a better experience for the user, as they will be able to see the map and not have pins hiding behind other pins. 

I wrote my first clustering algorithm in the fall of 2007 for version 5 of the Bing Maps AJAX control. This was later turned into an MSDN article which is still available. Over the years this algorithm has evolved. It was added to v6.3, and later turned into a module for v7. The original algorithm used a grid-based system that updated every time you moved the map. This was fast, but also had a small side effect in that even the slightest pan of the map caused the data to re-cluster. Since the grid was based on the current map view, it would sometimes cause data points to move from one grid cell to another, which resulted in pins jumping around.

A couple years ago I created a new point-based clustering algorithm. This algorithm adds the first location in the data set as a cluster point. It then takes the next location in the data set and checks to see if it is nearby any existing cluster point. If it is, then it is added to it. Otherwise a new cluster point is created. This continues until the whole data set is assigned to a cluster. This algorithm is a bit slower than the grid-based algorithm, but results in a much better user experience as the pins don’t jump around when panning the map. Both the grid and point-based clustering modules work with the Bing Maps JavaScript SDK for Windows Store apps.

Recently I had someone ask how to implement clustering in the Bing Maps WPF control. I migrated over the JavaScript to C# and created the code sample you can find here. Since then I ported both of these clustering algorithms into a reusable C# library for use with the Bing Maps Windows Store SDK. Rather than going through how to write the code for the algorithms, this blog post is going to cover how to make use of this library in your Windows Store app.

Before we get started, you can download the full source code for this blog here. If you run the sample application, you will first have to press the Generate Mock Data button so that the application has a data set to work with. Once you have done this and the data set is created, you will be able to view the data on the map either as individual pushpins or clustered using the grid or point-based clustering algorithm. Below is a screenshot of 5,000 pushpins drawn on the map:

 

ClusteringPushpins-5000Pushpins

 

As you can see, visualizing 5,000 pushpins makes the map pretty crowded and hard to see. You may notice it is harder to pan and zoom the map. Now if you then select the point or grid-based clustering buttons, the pins on the map will update and look something like this.

 

ClusteringPushpins-Buttons

 

To make the clusters easier to see, I have made them red. As you zoom in you will find them break apart into individual clusters.

Creating the Base Project

To implement clustering, first create a new Windows Store App in Visual Studio. Open Visual Studio and create a new project in C# or Visual Basic. Select the Blank App (XAML) template, name the application and then press OK.

 

ClusteringPushpins-NewProject

 

Next, download the clustering code sample and unzip it. Inside you will find a folder called BingMapsClusteringEngine. Copy this folder and navigate to the folder your project is stored in and paste it there. Next, in Visual Studios, right click on the Solution folder and select Add → Existing Project. In the window that opens, navigate to the folder where your project is stored, locate the BingMapsClusteringEngine.csproj file, and select it. This will add the reusable clustering library to your project.

 

ClusteringPushpins-AddExistingProject

 

Next, add a references to the clustering library and the Bing Maps SDK. Right click on the References folder and press Add Reference. Select Solution → Projects and select BingMapsClusteringEngine. Next, select Windows → Extensions and then select Bing Maps for C#, C++ and Visual Basic. If you do not see this option, be sure to verify that you have installed the Bing Maps SDK for Windows Store apps. While you are here, also add a reference to the Microsoft Visual C++ Runtime Package, as this is required by the Bing Maps SDK when developing using C# or Visual Basic. Press OK.

 

ClusteringPushpins-ReferenceManager

 

In Solution Explorer, set the Active solution platform in Visual Studio by right clicking on the Solution folder and selecting Properties. Select Configuration Properties → Configuration. Find your project and the BingMapsClusteringEngine project and under the Platform column, set the target platform to x86 and press OK.

 

ClusteringPushpins-ConfigurationManager

 

Now open the MainPage.xaml file. Update the XAML to the following.

<Page

    x:Class="BingMapsClusteringExample.MainPage"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="using:BingMapsClusteringExample"

    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

    xmlns:m="using:Bing.Maps"

  

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">

        <m:Map Name="MyMap" Credentials="YOUR_BING_MAPS_KEY"/>

 

        <Border Width="260" Height="270" CornerRadius="15" Background="Black"

                Margin="20"VerticalAlignment="Center" HorizontalAlignment="Right">

            <StackPanel Margin="10">

 

                <TextBlock Text="Mock Data Size:" FontSize="18"/>

 

                <TextBox Name="EntitySize" Text="5000"/>

 

                <Button Name="GenerateBtn" Content="Generate Mock Data"

                        Click="GenerateData_Clicked" Margin="0,10"/>

 

                <Button Name="ViewAllBtn" Content="View all locations"

                        IsEnabled="False" Click="ViewAllData_Clicked"/>

 

                <Button Name="PointBtn" Content="Use Point Based Clustering"

                IsEnabled="False" Margin="0,10" Click="PointClusterData_Clicked"/>

 

                <Button Name="GridBtn" Content="Use Grid Based Clustering"

                        IsEnabled="False" Click="GridClusterData_Clicked"/>

            </StackPanel>

        </Border>

    </Grid>

</Page>


This will add a map and a bunch of buttons to the app for testing out the clustering functionality. Next, open the MainPage.xaml.cs or MainPage.xaml.vb file and update it with the following code. This code contains the event handlers for the buttons without any logic in them yet along with a useful method for generating mock data to test with. When mock data is being generated, all the buttons are disabled until the process is completed. The mock data is stored as a global private property in the document so that we can use it from the button handlers and do all the testing using the same data set.

C#

using Bing.Maps;

using BingMapsClusteringEngine;

using System;

using Windows.UI;

using Windows.UI.Popups;

using Windows.UI.Xaml;

using Windows.UI.Xaml.Controls;

using Windows.UI.Xaml.Media;

 

namespace BingMapsClusteringExample

{

    publicsealedpartialclassMainPage : Page

    {

        privateItemLocationCollection _mockData;

 

        public MainPage()

        {

            this.InitializeComponent();

        }

 

        privateasyncvoid GenerateData_Clicked(object sender, RoutedEventArgs e)

        {

        }

 

        privatevoid ViewAllData_Clicked(object sender, RoutedEventArgs e)

        {

        }

 

        privatevoid PointClusterData_Clicked(object sender, RoutedEventArgs e)

        {

        }

 

        privatevoid GridClusterData_Clicked(object sender, RoutedEventArgs e)

        {

        }

 

        privatevoid GenerateMockData(int numEntities)

        {

            GenerateBtn.IsEnabled = false;

            ViewAllBtn.IsEnabled = false;

            PointBtn.IsEnabled = false;

            GridBtn.IsEnabled = false;

 

            _mockData = newItemLocationCollection();

 

            Random rand = newRandom();

 

            object item;

            Location loc;

 

            for (int i = 0; i < numEntities; i++)

            {

                item = "Location number: " + i;

 

                loc = newLocation()

                {

                    Latitude = rand.NextDouble() * 180 - 90,

                    Longitude = rand.NextDouble() * 360 - 180

                };

 

                _mockData.Add(item, loc);

            }

 

            GenerateBtn.IsEnabled = true;

            ViewAllBtn.IsEnabled = true;

            PointBtn.IsEnabled = true;

            GridBtn.IsEnabled = true;

        }

    }

}

 

VB

Imports BingMapsClusteringEngine

Imports Bing.Maps

Imports Windows.UI.Popups

Imports Windows.UI

 

PublicNotInheritableClassMainPage

    InheritsPage

 

    Private _mockData AsItemLocationCollection

 

    PublicSubNew()

        Me.InitializeComponent()

    EndSub

 

    PrivateAsyncSub GenerateData_Clicked(sender AsObject, e AsRoutedEventArgs)

    EndSub

 

    PrivateSub ViewAllData_Clicked(sender AsObject, e AsRoutedEventArgs)

    EndSub

 

    PrivateSub PointClusterData_Clicked(sender AsObject, e AsRoutedEventArgs)

    EndSub

 

    PrivateSub GridClusterData_Clicked(sender AsObject, e AsRoutedEventArgs)

    EndSub

 

    PrivateSub GenerateMockData(numEntities AsInteger)

        GenerateBtn.IsEnabled = False

        ViewAllBtn.IsEnabled = False

        PointBtn.IsEnabled = False

        GridBtn.IsEnabled = False

 

        _mockData = NewItemLocationCollection()

 

        Dim rand AsRandom = NewRandom()

 

        Dim item AsObject

        Dim loc AsLocation

 

        For i AsInteger = 0 To numEntities

            'Create some mock metadata to store

            item = "Location number: " + CStr(i)

 

            loc = NewLocation()

            loc.Latitude = rand.NextDouble() * 180 - 90

            loc.Longitude = rand.NextDouble() * 360 - 180

 

            _mockData.Add(item, loc)

        Next

 

        'Enable all the buttons

        GenerateBtn.IsEnabled = True

        ViewAllBtn.IsEnabled = True

        PointBtn.IsEnabled = True

        GridBtn.IsEnabled = True

    EndSub

EndClass

 

At this point you should be able to build the application without any error occurring. However, since there is no logic in button handlers yet, the app won’t do much. The mock data is being stored in an ItemLocationCollection which allows each record to store an object and related location as a tuple. The object can be anything you want to have associated with the location, such as an ID value for requesting additional information from a service or a view model for populating an infobox.

To generate the mock data for testing, add the following code to the GenerateData_Clicked event handler. This code will clear all data from the map and get the mock data size from a textbox on the page. A check is done to ensure the number is valid and a message shown if it isn’t. If it is valid, then the GenerateMockData method is called to generate the desired number of mock data points.

C#

privateasyncvoid GenerateData_Clicked(object sender, RoutedEventArgs e)

{

    MyMap.Children.Clear();

 

    int size;

 

    if (string.IsNullOrWhiteSpace(EntitySize.Text) ||

        !int.TryParse(EntitySize.Text, out size))

    {

        var dialog = newMessageDialog("Invalid size.");

        await dialog.ShowAsync();

        return;

    }

 

    GenerateMockData(size);

}

 

VB

PrivateAsyncSub GenerateData_Clicked(sender AsObject, e AsRoutedEventArgs)

    MyMap.Children.Clear()

 

    Dim size AsInteger

 

    IfString.IsNullOrWhiteSpace(EntitySize.Text) Or

            NotInteger.TryParse(EntitySize.Text, size) Then

 

        Dim dialog = NewMessageDialog("Invalid size.")

        Await dialog.ShowAsync()

        Return

    EndIf

 

    GenerateMockData(size)

EndSub


Next we will add the logic for viewing all the mock data on the map. To do this, loop through all the mock data items and create a pushpin for each one and add it to the map. Update the ViewAllData_Clicked event handler with the following code:

C#

privatevoid ViewAllData_Clicked(object sender, RoutedEventArgs e)

{

    MyMap.Children.Clear();

 

    for (int i = 0; i < _mockData.Count; i++)

    {

        var pin = newPushpin();

        pin.Tag = _mockData[i].Item;

        MapLayer.SetPosition(pin, _mockData[i].Location);

        MyMap.Children.Add(pin);

    }

}

 

VB

PrivateSub ViewAllData_Clicked(sender AsObject, e AsRoutedEventArgs)

    MyMap.Children.Clear()

 

    For i AsInteger = 0 To _mockData.Count

        Dim pin = NewPushpin()

        pin.Tag = _mockData(i).Item

        MapLayer.SetPosition(pin, _mockData(i).Location)

        MyMap.Children.Add(pin)

    Next

EndSub


If you run the application and try first pressing the button to create the mock data and then clicking the button to view all the data, you should see the map fill with pushpins. This may be slow, and if you entered a really large number, the application may even throw an error. If you try and pan or zoom you will likely notice significant lag by the map.

Implementing Clustering

In the sample, there are two classes with clustering logic in them; GridBasedClusteredLayer and PointBasedClusterLayer. Both of these classes inherit from an abstract class called BaseClusteredLayer. As such, these two classes share the same public properties and events.

 

There are two properties available: ClusterRadius and Items. The ClusterRadius is used by the algorithms for specifying how many pixels two pushpins can be separated before being grouped together. The smaller the radius, the more clusters (and more pushpins) will be displayed on the map. A smaller radius may also reduce the number of items you can have in the dataset before running into performance issues. The Items property is an ItemLocationCollection class which allows you to add your item and location information to be clustered. Every time this collection changes, the layer re-clusters. For best performance when initially adding a lot of locations, it is best to first add all your data to a separate ItemLocationCollection and then add it to the Items property using the AddRange method.

 

There are two event handlers on the cluster layers, CreateItemPushpin and CreateClusteredItemPushpin. These two event handlers are fired when the cluster layer tries to create the pushpins for representing the individual and clustered items. The CreateItemPushpin event handler accepts an object, which is the item that is linked with the location. The CreateClusteredItemPushpin event handler takes in a ClusteredPoint object. The ClusteredPoint class contains information about a cluster such as the location and a list of indices of all the items in the collection, and the length of this list is the number of items in the cluster. You can also get all the items by indices by using the GetItemByIndex or GetItemsByIndex methods from the Items property on the clustering layer. Both of these events can return a standard Pushpin or create a completely custom UIElement to represent the location on the map.

 

The following are two simple event handlers that return pushpins to represent the locations on the map. Individual locations use a standard pushpin, clusters are represented using a red pushpin with a plus sign. I used red, as the plus sign was hard to see in the screenshots, but you may prefer to use a different color. Add these event handlers to the MainPage.xaml.cs or MainPage.xaml.vb file.

C#

privateUIElement CreateItemPushpin(object item)

{

    var pin = newPushpin()

    {

        Tag = item

    };

 

    return pin;

}

 

privateUIElement CreateClusteredItemPushpin(ClusteredPoint clusterInfo)

{

    var pin = newPushpin()

    {

        Background = newSolidColorBrush(Colors.Red),

        Text = "+",

        Tag = clusterInfo               

    };

 

    return pin;

}

 

VB

PrivateFunction CreateItemPushpin(item AsObject) AsUIElement

    Dim pin = NewPushpin()

    pin.Tag = item

    Return pin

EndFunction

 

PrivateFunction CreateClusteredItemPushpin(clusterInfo AsClusteredPoint) AsUIElement

    Dim pin = NewPushpin()

    pin.Background = NewSolidColorBrush(Colors.Red)

    pin.Text = "+"

    pin.Tag = clusterInfo

    Return pin

EndFunction


Adding a clustering layer to the map is as simple as adding a MapLayer to the map. Use the following code to update the PointClusterData_Clicked button handler. This code clears the map, creates an instance of the PointBasedClusteredLayer class, and adds the event handlers for creating the pushpins. It then adds the layer as a child of the map and populates the Items property of the layer with the mock data.

C#

privatevoid PointClusterData_Clicked(object sender, RoutedEventArgs e)

{

    MyMap.Children.Clear();

 

    //Create an instance of the Point Based clustering layer

    var layer = newPointBasedClusteredLayer();

 

    //Add event handlers to create the pushpins

    layer.CreateItemPushpin += CreateItemPushpin;

    layer.CreateClusteredItemPushpin += CreateClusteredItemPushpin;

 

    MyMap.Children.Add(layer);

 

    //Add mock data to layer

    layer.Items.AddRange(_mockData);

}

 

VB

PrivateSub PointClusterData_Clicked(sender AsObject, e AsRoutedEventArgs)

    MyMap.Children.Clear()

 

    'Create an instance of the Point Based clustering layer

    Dim layer = NewPointBasedClusteredLayer()

 

    'Add event handlers to create the pushpins

    AddHandler layer.CreateItemPushpin, AddressOf CreateItemPushpin

    AddHandler layer.CreateClusteredItemPushpin, AddressOf CreateClusteredItemPushpin

 

    MyMap.Children.Add(layer)

 

    'Add mock data to layer

    layer.Items.AddRange(_mockData)

EndSub


The logic for adding a GridBasedClusteredLayer is exactly the same. Update the GridClusterData_Clicked button handler with the following code.

C#

privatevoid GridClusterData_Clicked(object sender, RoutedEventArgs e)

{

    MyMap.Children.Clear();

 

    //Create an instance of the Grid Based clustering layer

    var layer = newGridBasedClusteredLayer();

 

    //Add event handlers to create the pushpins

    layer.CreateItemPushpin += CreateItemPushpin;

    layer.CreateClusteredItemPushpin += CreateClusteredItemPushpin;

 

    //Add mock data to layer

    layer.Items.AddRange(_mockData);

    MyMap.Children.Add(layer);    

}

 

VB

PrivateSub GridClusterData_Clicked(sender AsObject, e AsRoutedEventArgs)

    MyMap.Children.Clear()

 

    'Create an instance of the Grid Based clustering layer

    Dim layer = NewGridBasedClusteredLayer()

 

    'Add event handlers to create the pushpins

    AddHandler layer.CreateItemPushpin, AddressOf CreateItemPushpin

    AddHandler layer.CreateClusteredItemPushpin, AddressOf CreateClusteredItemPushpin

 

    MyMap.Children.Add(layer)

 

    'Add mock data to layer

    layer.Items.AddRange(_mockData)

EndSub


The application is now complete. Run it and press the button to generate mock data. Select a clustering method to implement and then test it out by panning and zooming the map. Doing some testing I have found these work well for upwards of 50,000 locations on an x86 machine. I would expect this to be lower on an ARM-based device. That said, 50,000 locations is a lot of data points. You likely won’t want to have the user download that much data all at once. Again, you can download the full source code for this blog from the MSDN samples here.

 

If you are looking for some other great resources on Bing Maps for Windows Store apps, look through this blog, or check out all the Bing Maps MSDN code samples.

 

If you are looking to implement clustering on Windows Phone 8, this code should be easy enough port over. Also, you can find an interesting blog post that uses a hexagon based clustering algorithm for Windows Phone 8 here.

 

- Ricky Brundritt, EMEA Bing Maps TSP

Complex Polygons in Bing Maps

$
0
0

In Bing Maps we can easily create simple polygons. Simple polygons consist of a single exterior ring of coordinates. However, in more advance applications it is useful to be able to draw more complex polygons. Take for instance the borders of Lesotho, which is a land locked country within the main exterior borders of South Africa. In this case to properly represent this country’s borders we would need to have a hole in the polygon. In this blog post we are going to see how you can create complex polygons such as MultiPolygons and polygons with holes using both JavaScript and .NET code in Windows Store Apps.

Complex Polygons in the JavaScript API

The Bing Maps JavaScript controls, both for the web and for Windows Store apps support complex polygons. To enable this support you first need to load the AdvancedShapes module into your application. This module updates the definition of the Polygon and EntityCollection classes. The new Polygon class is initialized with an array of rings, where each ring is an array of locations. The changes to the EntityCollectionare simply to allow it to support the updated complex polygon class.

To implement complex polygons, first create a new Windows Store App in Visual Studio.

Open Visual Studio and create a new project in JavaScript. Select the Blank App template, name the application, and then press OK.

image

Next, add a reference to the Bing Maps SDK.

Right click on the References folder and press Add Reference. Select Windows → Extensions, and then select Bing Maps for JavaScript. If you do not see this option, be sure to verify that you have installed the Bing Maps SDK for Windows Store apps. Press OK.

image

Add a new JavaScript file in the js folder called ComplexShapes.js by right clicking on the js folder and selecting Add → New Item. We will add our application specific logic to this file to keep things clean.

Now open the default.html file and add a reference to the Bing Maps SDK along with the modules API. Add a reference to the ComplexShapes.js file. In the body of the document add a div for the map and a button for adding a complex polygon to the map. To accomplish this, update the HTML in the default.html file to the following:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ComplexPolygons</title>

<!-- WinJS references -->
<link href="//Microsoft.WinJS.2.0/css/ui-dark.css" rel="stylesheet" />
<script src="//Microsoft.WinJS.2.0/js/base.js"></script>
<script src="//Microsoft.WinJS.2.0/js/ui.js"></script>

<!-- ComplexPolygons references -->
<link href="http://www.bing.com/css/default.css" rel="stylesheet" />
<script src="http://www.bing.com/js/default.js"></script>

<!-- Bing Map Control references -->
<script type="text/javascript" src="ms-appx:///Bing.Maps.JavaScript//js/veapicore.js"></script>
<script type="text/javascript" src="ms-appx:///Bing.Maps.JavaScript//js/veapiModules.js"></script>

<!-- Our Bing Maps JavaScript Code -->
<script src="http://www.bing.com/js/ComplexPolygons.js"></script>
</head>
<body>
<div id='MyMap'></div>

<divstyle="position:absolute;bottom:0px;left:50%;margin-left:-110px;background-color:black;padding:10px;">
<button id="AddComplexPolygonBtn">Add Complex Polygon</button>
</div>
</body>
</html>

Next open the ComplexShapes.js file. In this file we will need to add the logic to load the map and the AdvancedShapes module. We will also need to add the logic to add a complex polygon to the map when the button is pressed. In the button handler, clear the map, create some mock data and create a complex polygon with it, then add the polygon to the map. Add the following code to the ComplexShapes.js file to accomplish this.

(function () {
var map = null;

function GetMap() {
map = new Microsoft.Maps.Map(document.getElementById("MyMap"), { credentials: "YOUR_BING_MAPS_KEY" });

Microsoft.Maps.loadModule('Microsoft.Maps.AdvancedShapes');
}


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

document.getElementById("AddComplexPolygonBtn").addEventListener("click", AddComplexPolygon_Tapped, true);
}

function AddComplexPolygon_Tapped() {
//Clear the map
map.entities.clear();

//Create mock data
var rings = [
[new Microsoft.Maps.Location(40, -100), new Microsoft.Maps.Location(45, -100), new Microsoft.Maps.Location(45, -90), new Microsoft.Maps.Location(40, -90)],
[new Microsoft.Maps.Location(41, -99), new Microsoft.Maps.Location(43, -97), new Microsoft.Maps.Location(41, -95)],
[new Microsoft.Maps.Location(44, -91), new Microsoft.Maps.Location(43, -93), new Microsoft.Maps.Location(44, -95)]
];

//Create a complex polygon
var polygon = new Microsoft.Maps.Polygon(rings, {
fillColor: new Microsoft.Maps.Color(150, 0, 255, 0),
strokeColor: new Microsoft.Maps.Color(150, 0, 0, 255)
});

//Add the polyon to the map.
map.entities.push(polygon);
}

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

If you run this application and press the button, you should see a square with two triangular holes cut out of it displayed on the map.

image

Complex Polygons in the Native Windows Store Apps

The Native Bing Maps Windows Store SDK currently does not have built in support for complex polygons. However, we can easily use the techniques used in the past to add complex polygons to previous versions of Bing Maps and add this support to the Window Store SDK. Below are some blog posts I’ve written in the past on this topic:

A complex polygon consists of closed rings that represent each part of the polygon, such as the outer boundary or a hole. We can use the standard polygon class in Bing Maps to create complex polygons by concatenating these rings together and tying them back to a single common location. By tying the rings back to a single common location the polygon ends up back tracking on itself and cancels out that part of the polygon. Here is an example of how to order locations so as to draw a triangle with a hole in it. #

image

To implement complex polygons in .NET, first create a new Windows Store App in Visual Studio.

Open Visual Studio and create a new project in C# or Visual Basic. Select the Blank App (XAML) template, name the application and then press OK.

image

Next, add a reference to Bing Maps SDK. Right click on the References folder and press Add Reference. Select Windows → Extensions, and then select Bing Maps for C#, C++ and Visual Basic. If you do not see this option, be sure to verify that you have installed the Bing Maps SDK for Windows Store apps. While you are here, also add a reference to the Microsoft Visual C++ Runtime Package, as this is required by the Bing Maps SDK when developing using C# or Visual Basic. Press OK.

image

In Solution Explorer, set the Active solution platform in Visual Studio by right clicking on the Solution folder and selecting Properties. Select Configuration Properties → Configuration. Find your project and under the Platform column, set the target platform to x86, and press OK.

image

Now open the MainPage.xaml file and update the XAML to the following:

<Page
x:Class="ComplexPolygons.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ComplexPolygons"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:Bing.Maps"
mc:Ignorable="d">

<GridBackground="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<m:MapName="MyMap"Credentials="YOUR_BING_MAPS_KEY"/>

<BorderBackground="Black"Width="220"Height="65"Padding="10"VerticalAlignment="Bottom">
<ButtonContent="Add Complex Polygon"Tapped="AddComplexPolygon_Tapped"Width="200"Height="45"/>
</Border>
</Grid>
</Page>

This will add a map and a button to the app for adding a complex polygon. Next open the MainPage.xaml.cs or MainPage.xaml.vb file and update it with the following code. This code adds a MapShapeLayerto the map and the event handler for the button without any logic in it yet.

C#

using Bing.Maps;
using System.Collections.Generic;
using Windows.UI;
using Windows.UI.Xaml.Controls;

namespace ComplexPolygons
{
publicsealedpartialclass MainPage : Page
{
private MapShapeLayer shapeLayer;

public MainPage()
{
this.InitializeComponent();

//Add a MapShapeLayer to the map to display the polygon in
shapeLayer = new MapShapeLayer();
MyMap.ShapeLayers.Add(shapeLayer);
}

privatevoid AddComplexPolygon_Tapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
{
}
}
}

VB

Imports Bing.Maps
Imports Windows.UI

PublicNotInheritableClass MainPage
Inherits Page

Private shapeLayer As MapShapeLayer

SubNew()
InitializeComponent()

'Add a MapShapeLayer to the map to display the polygon in
shapeLayer = New MapShapeLayer()
MyMap.ShapeLayers.Add(shapeLayer)
EndSub

PrivateSub AddComplexPolygon_Tapped(sender AsObject, e As TappedRoutedEventArgs)
EndSub
End Class

Before adding logic to the button handler, we will first create a method for generating a complex polygon. This method will take in a list of rings which are LocationCollections, a fill color, a stroke color and width. This method will also return a MapPolygon and a list of MapPolyline’s which can be used to outline the polygon. This method will ensure all the rings are closed and concatenate them together and tie them to a base location as described earlier to create the complex polygon. Add the following code to the MainPage.xaml.cs or MainPage.xaml.vbfile:

C#

publicvoid CreatePolygon(List<LocationCollection> rings, Color? fillColor, Color? strokeColor, double width, out MapPolygon polygon, out List<MapPolyline> outlines)
{
outlines = new List<MapPolyline>();

if (rings != null&& rings.Count >= 0 && rings[0].Count >= 3)
{
//Get the first location in the first ring. This will be used as a base point for all rings.
var baseLocation = rings[0][0];

var exteriorRing = new LocationCollection();

foreach (var r in rings)
{
//Ensure ring is closed.
r.Add(r[0]);

if (r.Count >= 3)
{
//Add ring to main list of locations
foreach (var l in r)
{
exteriorRing.Add(l);
}

//Loop back to starting location
exteriorRing.Add(baseLocation);

if (strokeColor.HasValue && width > 0)
{
//Create Polyline to outline ring
outlines.Add(new MapPolyline()
{
Color = strokeColor.Value,
Width = width,
Locations = r
});
}
}
}

if (fillColor.HasValue && exteriorRing.Count > 3)
{
polygon = new MapPolygon()
{
Locations = exteriorRing,
FillColor = fillColor.Value
};

return;
}
}

polygon = null;
}

VB

PrivateSub CreatePolygon(rings As List(Of LocationCollection), fillColor As Color?, strokeColor As Color?, width AsDouble, ByRef polygon As MapPolygon, ByRef outlines As List(Of MapPolyline))
outlines = New List(Of MapPolyline)

If (rings IsNot NothingAnd rings.Count >= 0 And rings(0).Count >= 3) Then
'Get the first location in the first ring. This will be used as a base point for all rings.
Dim baseLocation = rings(0)(0)

Dim exteriorRing = New LocationCollection()

ForEach r In rings
'Ensure ring is closed.
r.Add(r(0))

If (r.Count >= 3) Then
'Add ring to main list of locations
ForEach l In r
exteriorRing.Add(l)
Next

'Loop back to starting location
exteriorRing.Add(baseLocation)

If (strokeColor.HasValue And width > 0) Then
'Create Polyline to outline ring
Dim p = New MapPolyline()
p.Color = strokeColor.Value
p.Width = width
p.Locations = r

outlines.Add(p)
EndIf
EndIf
Next

If (fillColor.HasValue And exteriorRing.Count > 3) Then
polygon = New MapPolygon()
polygon.Locations = exteriorRing
polygon.FillColor = fillColor.Value
Return
EndIf
EndIf

polygon = Nothing
End Sub

We can now add logic to the button event handler. In this event handler we will want to clear the shape layer of any data, create mock data and use it to generate a complex polygon, and then finally add the polygon and/or the outlines of the polygon to the map. This last step is a great place to add any additional logic you wish to add to the polygon, such as a tap event. Update the AddComplexPolygon_Tappedevent handler with the following code to do this:

C#

privatevoid AddComplexPolygon_Tapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e)
{
//Clear any shapes in the map shape layer.
shapeLayer.Shapes.Clear();

//Create mock data
var rings = new List<LocationCollection>(){
new LocationCollection(){ new Location(40, -100), new Location(45, -100), new Location(45, -90), new Location(40, -90) },
new LocationCollection(){ new Location(41, -99), new Location(43, -97), new Location(41, -95) },
new LocationCollection(){ new Location(44, -91), new Location(43, -93), new Location(44, -95) }
};

//Create the colors for styling the complex polygon
var fillColor = Windows.UI.Color.FromArgb(150, 0, 255, 0);
var strokeColor = Windows.UI.Color.FromArgb(150, 0, 0, 255);

MapPolygon polygon;
List<MapPolyline> outlines;

//Create the complex polygon.
CreatePolygon(rings, fillColor, strokeColor, 5, out polygon, out outlines);

//If the polygon is not null add it to the shape layer
if (polygon != null)
{
shapeLayer.Shapes.Add(polygon);
}

//If the outlines for the complex polygon are not null add them to the shape layer.
if (outlines != null)
{
foreach (var l in outlines)
{
shapeLayer.Shapes.Add(l);
}
}
}

VB

PrivateSub AddComplexPolygon_Tapped(sender AsObject, e As TappedRoutedEventArgs)
'Clear any shapes in the map shape layer.
shapeLayer.Shapes.Clear()

'Create mock data
Dim rings = New List(Of LocationCollection)

rings.Add(New LocationCollection())
rings(0).Add(New Location(40, -100))
rings(0).Add(New Location(45, -100))
rings(0).Add(New Location(45, -90))
rings(0).Add(New Location(40, -90))

rings.Add(New LocationCollection())
rings(1).Add(New Location(41, -99))
rings(1).Add(New Location(43, -97))
rings(1).Add(New Location(41, -95))

rings.Add(New LocationCollection())
rings(2).Add(New Location(44, -91))
rings(2).Add(New Location(43, -93))
rings(2).Add(New Location(44, -95))

'Create the colors for styling the complex polygon
Dim fillColor = Windows.UI.Color.FromArgb(150, 0, 255, 0)
Dim strokeColor = Windows.UI.Color.FromArgb(150, 0, 0, 255)

Dim polygon As MapPolygon
Dim outlines As List(Of MapPolyline)

'Create the complex polygon.
CreatePolygon(rings, fillColor, strokeColor, 5, polygon, outlines)

'If the polygon is not null add it to the shape layer
If polygon IsNot NothingThen
shapeLayer.Shapes.Add(polygon)
EndIf

'If the outlines for the complex polygon are not null add them to the shape layer.
If outlines IsNot NothingThen
ForEach l In outlines
shapeLayer.Shapes.Add(l)
Next
EndIf
End Sub

The application is now complete. Run it and press the button to generate a complex polygon on the map.

image

You can download the full source code for this blog from the MSDN samples here. You can also find a WPF version of this code sample here.

If you are looking for some other great resources on Bing Maps for Windows Store apps, look through this blog, or check out all the Bing Maps MSDN code samples.

For more Bing Maps posts for developers, check out the Bing Developer Center blog.

- Ricky Brundritt, EMEA Bing Maps TSP

Introducing Support for Custom Geospatial-Data in Bing SDS

$
0
0

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.

CustomGeoSpatialDataMaps

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

 

CustomGeoSpatialDataSQLQuery

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 orderby 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:

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

CustomGeoSpatialDataSQLQuery2

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.

CustomGeoSpatialDataSQLServerImportWizard

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

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

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

CustomGeoSpatialDataSaveAs

Uploading the Data

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

CustomGeoSpatialDataUploadGeoData

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.

PrivateSub btnUploadLocal_Click(sender AsObject, e As EventArgs) Handles btnUpload-Local.Click
' Custom name of spatial data source created during upload.
Dim dataSourceName AsString = "KingCountyTrail"
' Path to the spatial data input file to be uploaded.
Dim dataFilePath AsString = "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 AsString = "YOUR_BING_MAPS_KEY"
' Create the spatial data upload URL.
Dim queryStringBuilder AsNew 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 AsNew UriBuilder("http://spatial.virtualearth.net")
uriBuilder.Path = "/REST/v1/Dataflows/LoadDataSource"
uriBuilder.Query = queryStringBuilder.ToString()
Dim dataflowJobUrl AsString = 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 AsByte() = NewByte(16383) {}
Dim bytesRead AsInteger = dataStream.Read(buffer, 0, buffer.Length)
While bytesRead > 0
requestStream.Write(buffer, 0, bytesRead)
bytesRead = dataStream.Read(buffer, 0, buffer.Length)
EndWhile
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 AsString = String.Format("{0}?o=xml&key={1}", da-taflowJobUrl, Uri.EscapeUriString(bingMapsMasterKey))

txtStatusURL.Text = jobStatusQueryUrl

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

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.

CustomGeoSpatialDataQueryData

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.

CustomGeoSpatialDataBingMapsforWindows81SDKDownload

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.

CustomGeoSpatialDataNewProject

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

CustomGeoSpatialDataReferenceManager

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>
<divid="divMap"></div>
<buttonid="btnShowPanel">Show Panel</button>
<divid="divPanel">
<inputid="btnLocateMe"type="button"value="Locate Me"/>
<inputid="btnGetTrail"type="button"value="Get Trail"/>
<inputid="btnGetSchoolDistrict"type="button"value="Get School District"/>
<inputid="btnGetSchool"type="button"value="Get Schools"/>
<inputid="btnClearMap"type="button"value="Clear Map"/>
<divid="divLegend">
<aid="lblLegend">Data provided by permission of King County</a>
</div>
</div>
</body>

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

body {
}

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

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


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

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

#divPanel a {
color: white !important;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

…and another one to clear the map:

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

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

CustomGeoSpatialDataMapApp.jpg

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:

CustomGeoSpatialDataMapApp2

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.

CustomGeoSpatialDataMapApp3

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.

CustomGeoSpatialDataMapAppFinal

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.

For more Bing Maps content for developers, check out the Bing Developer Center.

- Bing Maps Team

How to Extend Your App with Talking Maps

$
0
0

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 the project that we created for the previous blog post and add references to Bing Speech and the Visual C++ 2013 Runtime.

TalkingMapsReferenceManager

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

TalkingMapsConfigurationManager

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

<Capabilities>
<CapabilityName="internetClient"/>
<DeviceCapabilityName="microphone"/>
<DeviceCapabilityName="location"/>
</Capabilities>
<Extensions>
<ExtensionCategory="windows.activatableClass.inProcessServer">
<InProcessServer>
<Path>Microsoft.Speech.VoiceService.MSSRAudio.dll</Path>
<ActivatableClassActivatableClas-sId="Microsoft.Speech.VoiceService.MSSRAudio.Encoder"ThreadingModel="both"/>
</InProcessServer>
</Extension>
<ExtensionCategory="windows.activatableClass.proxyStub">
<ProxyStubClassId="5807FC3A-A0AB-48B4-BBA1-BA00BE56C3BD">
<Path>Microsoft.Speech.VoiceService.MSSRAudio.dll</Path>
<InterfaceName="IEncodingSettings"InterfaceId="C97C75EE-A76A-480E-9817-D57D3655231E"/>
</ProxyStub>
</Extension>
<ExtensionCategory="windows.activatableClass.proxyStub">
<ProxyStubClassId="F1D258E4-9D97-4BA4-AEEA-50A8B74049DF">
<Path>Microsoft.Speech.VoiceService.Audio.dll</Path>
<InterfaceName="ISpeechVolumeEvent"InterfaceId="946379E8-A397-46B6-B9C4-FBB253EFF6AE"/>
<InterfaceName="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 -->
<linkhref="/Bing.Speech/css/voiceuicontrol.css"rel="stylesheet"/>
<scriptsrc="/Bing.Speech/js/voiceuicontrol.js"></script

In the body of default.html, we replace the existing buttons with a button to initiate the Bing Speech Recognizer and a div element to host the Bing SpeechRecognizerUx

<divid="divPanel">
<divid="divSpeechControl"data-win-control="BingWinJS.SpeechRecognizerUx"></div>
<inputid="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.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 keywords 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();
});
}

elseif (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();
});
}

elseif (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();
});
}

elseif (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();
});
}

elseif (result.text.indexOf("built") > -1) {
var Ssml = "<speak version='1.0' " +
"xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='en-US'>" +
"Justin, ,<phoneme alphabet='x-microsoft-ups' ph='S1 P R AH . M I L'></phoneme>, Yi, Gevorg, Doug, and <phoneme alphabet='x-microsoft-ups' ph='S1 P R AH S I . D AH'></phoneme>." +
"<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.

TalkingMapsCompletedApp

- Bing Maps Team

Introducing the Bing Maps Preview App for Windows 8.1

$
0
0

Today we announced the release of the Bing Maps Preview app built for Windows 8.1. As a first look at the next generation of the Bing Maps app, it offers a beautiful 3D experience designed for touch that brings the best of Bing and Windows together. Get all the details in the official announcement on the Bing Blog and let us know what you think.

BingMapsPreviewValencia_Small

Viewing all 102 articles
Browse latest View live