Quantcast
Channel: Bing Blogs
Viewing all articles
Browse latest Browse all 815

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


Viewing all articles
Browse latest Browse all 815

Trending Articles