Ricky's profileRicky's Bing Maps BlogBlogSkyDrive Tools Help

Ricky's Bing Maps Blog

Where you find out how to add bling to your bing.

Ricky Brundritt

Occupation
Location
I am a Consultant, working for Infusion Development (www.infusion.com) in the Location Intelligence Services department. I am also a Microsoft MVP.
10/4/2009

Point in Polygon and Bounding Box search against data in the Customer Service Site

Currently when storing data in the customer service site you have the ability to query your point location data by entity id, property, radius search, and find near route. There are two key search functionalities missing. The first is bounding box searching, and the second is point in polygon searches. A bounding box is simple 4 sided regular polygon and as such if we can perform a point in polygon search we can use the same algorithm to perform a bounding box search. This post is going to outline two methods that can be used to perform such a search against data that is stored in the Customer Service Site.

Method 1

Lets start off with a polygon (blue). If we find the maximum and minimum latitude and longitude coordinates that this polygon has we can create a bounding box (green) that encloses the polygon. We can then calculate the center point and radius from center point to a corner of this bounding box. Once we know this information we can then enclose this bounding box with a circle (red). The information used to create this circle can be used with the radius search tools that are currently available.

image

If you perform a radius search using the calculated information you will end up with a lot of extra data points. You can filter this data points by running them through a point in polygon algorithm like the one outlined here: http://msdn.microsoft.com/en-us/library/cc451895.aspx. You can then return the filtered data to the user. Using this method your polygon must fit inside a bounding box whose sides are no longer than 353.55 miles. The benefit of this approach is that there are only a few calculations required initially. One down side is that there is a lot of extra data that could be potentially returned.

Method 2

The second method is very similar to the first. The maximum and minimum latitude and longitude coordinates of the polygon are used to create a bounding box so that the center of the polygon can be calculated. However, instead of calculating the radius from the center point to a corner of the bounding box, we can loop through and calculate the radii’s from the center point to each point in the polygon. The largest radius can then be used to perform a radius search.

image

Using this approach should reduce the amount of data that is returned by radius search as it is able to enclose the polygon tighter than that previous method. The down side is the number of calculations that need to be performed initially. For complex polygons this can result in slower performance. Using this method  you are limited to a maximum radius of 250 miles.

Conclusion

Using either of these methods will allow you to perform both bounding box and point in polygon searches against your data in the customer service site. Which method should you use. If you are performing bounding box searches or are using complex, predefined polygons then the first method would be ideal. If youare letting the user create the polygon chances are there will only be a limited number of points and as such method 2 would be ideal.

I have put together an example that allows you to draw out polygons using the right mouse button (press the left mouse button when done). After you have drawn a polygon you can then use either method to query the FourthCoffeeShops data source. You can down load the sample code here: http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/CSSFindByPolygon.zip

10/3/2009

Bing Map 3D Altitude Extraction via Mouse Click

When I first started with Virtual Earth a few years ago I always wanted to know how to extract the altitude of buildings from the 3D control but unfortunately there wasn’t any documentation on how to create custom plug-ins. There has been numerous people on the forums over the past couple of years who have also been asking for this. I had a good idea of how to go about this but just never got around to doing it, so here it goes. Lets start off with a screen shot of the finished product:

mouseClickedAltitude 
Assuming you already have the 3D control installed the first step is to create a class library project in Visual Studios and call it “VE3DMouseAltitude”. The next step is to reference the dll’s that will be needed. You will need to add references to the following dll’s:

Microsoft.MapPoint.Geometry
Microsoft.MapPoint.Rendering3D
Microsoft.MapPoint.Rendering3D.Utility
Microsoft.MapPoint.UtilityPartialTrust

You then need to add a class file to your project called “VE3DMouseAltitudePlugin.cs” and a HTML page called “MouseClickAltitudeFinder.html”. You can now add a strong named key to your project. To create a strongly name key for this plug-in:

  1. Open a Visual Studio Command Prompt.
  2. At the command prompt, type cd C:\Samples\Test (or the directory where you created your application) and press ENTER.
  3. At the command prompt, type sn -k MouseClickAltitudePlugin.snk and press ENTER.
  4. At the command prompt, type exit, and press ENTER.
  5. In Visual Studio, right click your project and select Properties.
  6. Click the Signing tab and check Sign the assembly.
  7. In the Choose a strong name key file drop down list, select <Browse…>
  8. Select the MouseClickAltitudePlugin.snk file and click Open. The strong name file is automatically added to your project.
  9. Save the properties window and then close it.

Your solution explorer should now look like this:

VE3DMouseAltitude

In the VE3DMouseAltitudePlugin.cs file we can add the following references to the top of the file:

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

using Microsoft.MapPoint.PlugIns;
using Microsoft.MapPoint.Rendering3D;
using Microsoft.MapPoint.Rendering3D.Steps;
using Microsoft.MapPoint.Rendering3D.Steps.Actors;
using Microsoft.MapPoint.Binding;


The next thing we need to do with this new plug-in is create a unique identifier. We do this using the GUID tool:

  1. In Visual Studio, click Tools | Create GUID.
  2. Select the Registry Format option.
  3. Click the New GUID button, and then the Copy button.
  4. Paste the GUID as an attribute of your class.
  5. Derive your class from the PlugIn class.

If this is done properly the top of your file should look something like this:

[Guid("83948C0A-F89A-4aa0-97E6-42687ECB6B61")]
public class VE3DMouseAltitudePlugin : PlugIn


There are three methods, Name, Activate and the Deactivate methods, that are a part of the PlugIn class which you will want to override.

public override string Name
{
    get { return "Mouse Click Altitude Finder"; }
}

public VE3DMouseAltitudePlugin(Host host)
    : base(host)
{
    // it is encouraged that most startup logic occur in the Activate function.
}

public override void Activate(object activationObject)
{
    base.Activate(activationObject);
}

public override void Deactivate()
{
    base.Deactivate();
}


The next step is to add a mouse click event to the Activate method that fires when the user clicks the mouse button. We will have this mouse event call a function called “MouseClicked”, we will also call this event a “SimpleMouseClick”. The following line of code will be added to the Activate method:

//Add mouse click event
this.Host.CommunicationManager.AttachToEvent(EngineEvents.Group, EngineEvents.OnClick, "SimpleMouseClick", MouseClicked);


If we attach an event to the control when we activate it, we should detach it when the control deactivates. To do this, add the following line of code to the Deactivate method:

this.Host.BindingsManager.UnregisterAction(this.BindingsSource, "SimpleMouseClick");


While we are at it we will change the units being used to metric (meters). To do this add the following line of code to the Activate method:

//Make the output of the altitude on the 3D metric
this.Host.WorldEngine.UseMetric = true;


We now have to create the MouseClicked method that gets called when the user clicks the mouse. This method will take in two parameters, a string that represents the function name that called it and a CommunicationParameter object. The MouseClicked method will look like this:

private void MouseClicked(string functionName, CommunicationParameter data)
{
}


Inside this method we can extract information about the mouse location through the Host.Navigation.PointerPosition and Host.Navigation.PointerPositionOnObject. We can then take this extracted information and create a CommunicationParameterSet that we can send to the client side. To do this add the following code to the MouseClicked method:

//Verify that the mouse clicked something on the surface of the earth and not a point in the sky.
if(this.Host.Navigation.PointerPosition != null)
{
    //retrieve the altitude of the mouse above relative to sea level
    double altAboveSea = this.Host.Navigation.PointerPositionOnObject.Location.AltitudeAboveSeaLevel;

    //retrieve the altitude of an object the mouse is on.
    double altAboveGround = this.Host.Navigation.PointerPositionOnObject.Location.Altitude - this.Host.Navigation.PointerPosition.Altitude;

    //Create parameters that can be passed back to the client
    CommunicationParameter altSeaParam = new CommunicationParameter("AltitudeAboveSea", altAboveSea);

    CommunicationParameter altGroundParam =
new CommunicationParameter("AltitudeAboveGround", altAboveGround);

    CommunicationParameter ClickedObjectParam =
new CommunicationParameter("ClickedOnObject", this.Host.Navigation.PointerOnObject);

    //Might as well retrieve the coordinates of where the user clicked too.
    CommunicationParameter latitudeParam =
new CommunicationParameter("Latitude", this.Host.Navigation.PointerPosition.Location.LatitudeDegrees);

    CommunicationParameter longitudeParam =
new CommunicationParameter("Longitude", this.Host.Navigation.PointerPosition.Location.LongitudeDegrees);

    //Create a parameter set out of the parameters
    CommunicationParameterSet paramSet = new CommunicationParameterSet(3);
    paramSet.Add(altSeaParam);
    paramSet.Add(altGroundParam);
    paramSet.Add(ClickedObjectParam);
    paramSet.Add(latitudeParam);
    paramSet.Add(longitudeParam);
}

 
Note that we check that the PointerPosition has a value. The reason for this is that if the user clicks the mouse in a random point in the sky the value of the pointer position will be null. We can then throw this information in an event to be later caught on the user side. To do this add the end of following line of code to the above if statement:

//Fire the MouseClickAltitude Event
this.Host.CommunicationManager.FireEvent(this.Guid, "MouseClickAltitude", paramSet);


In the MouseClickAltitudeFinder.html file we will add a map like we would normally would. We will also need to load our plug-in. When the plug-in is loaded we can attach an event that gets fired on the client side when the MouseClickAltitude event gets fired in the control. This can be done with the following line of code:

//Add a custom event listener for a MouseClickAltitude event
control3D.AttachPlugInEvent(objectPlugInGuid, "MouseClickAltitude", "MouseClickAltitude");


The next step is to add a MouseClickAltitude function on the client side that gets fired when the MouseClickAltitude event occurs. This function will take in two properties, an object with our data, and the map GUI ID. We can then process the returned data as we see fit. Here is what the HTML page in the example looks like:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Mouse Click Altitude Finder</title>
<script src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2" type="text/javascript"></script>
<script type="text/javascript">
    var map;
    var objectPlugInGuid;
    var control3D;
    var sampleName = "VE3DMouseAltitude";

    function OnPageLoad() {
        map = new VEMap('myMap');
        //Disable Birdseye mode as it is not needed.
        var mapOptions = new VEMapOptions();
        mapOptions.EnableBirdseye = false;

        //Load the map in 3D mode
        map.LoadMap(null, null, VEMapStyle.Road, false, VEMapMode.Mode3D, false, null, mapOptions);

        //position the map in an area that has buildings well above sea level for testing
        map.SetMapView(new VEMapViewSpecification(new VELatLong(39.76, -104.9917), null, 2500, -45, 0));
        control3D = map.vemapcontrol.Get3DControl();
        control3D.AttachEvent("OnPlugInLoaded", "On3DPlugInLoaded");

        //This will need to be changed to the directory in which your pr0ject is located or the web URL were the dll exists.
        control3D.LoadPlugInDll("C:\\Sample\\Test\\VE3DMouseAltitude\\VE3DMouseAltitude\\bin\\Debug\\VE3DMouseAltitude.dll");
    }

    function On3DPlugInLoaded(data, mapguid) {
        // data returned from events are in JSON format, and should be processed with a JSON parser,
        // but eval will work for demonstration purposes
        var result = eval('(' + data + ')');

        // we want to be sure that we are activating the correct one.
        var reg = new RegExp(sampleName + ".dll$");
        if (result.success && reg.test(result.plugInPath)) {
            objectPlugInGuid = result.guid;
            control3D.ActivatePlugIn(objectPlugInGuid, null);

            //Add a custom event listener for a MouseClickAltitude event
            control3D.AttachPlugInEvent(objectPlugInGuid, "MouseClickAltitude", "MouseClickAltitude");

            document.getElementById('output').innerHTML = "Plugin Loaded!";
        }
    }

    //MouseClickAltitude Event Handler
    function MouseClickAltitude(data, mapguid) {
        //Verify data was returned
        if (data != null) {
            // data returned from events are in JSON format, and should be processed with a JSON parser,
            // but eval will work for demonstration purposes
            var result = eval('(' + data + ')');

            //build a string to display on the browser
            var output = "Altitude above Sea Level: " + result.AltitudeAboveSea + " meters";
            if(result.ClickedOnObject)
            {
                output += "<br/>An Object was clicked.<br/>";
                output += "Altitude above Ground Level: " + result.AltitudeAboveGround + " meters";
            }

            output += "<br/>Latitude: " + result.Latitude + "<br/>";
            output += "Longitude: " + result.Longitude;  

            document.getElementById('output').innerHTML = output;
        }
    }
</script>
</head>
<body onload="OnPageLoad();">
    <div id="myMap" style="position:relative;width:800px;height:600px;"></div><br />
    <div id="output">Loading Plugin...</div>
</body>
</html>


We can now click points on the 3D map and find out their altitude. Complete source code can be found here: http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/VE3DMouseAltitude.zip

To get it to work you may need to re-add the references to the Microsoft.MapPoint dll’s. You will also have to change the path to the project dll in the MouseClickAltitudeFinder.html file.

Determining if two bounding boxes overlap

Often when displaying polygons you only need or want to show the polygons that are in the viewable area of the map. By doing this an increase in performance should occur with the map control as there will be less data that will need to be handled by the control. One method to go about this is to store the bounding coordinates of your polygon compare then to the bounding box of the viewable map. This leaves you with the task of determining if two bounding boxes overlap which should be much easier than determining if a polygon with any number of sides is in the map view. What this blog is meant to do is to present a mathematical solution to this problem.

For this solution we need to break a bounding box into some key components. A bounding box has a center point and two important radii’s. A radius from the center point to and edge in the x direction, and a radius in from the center point to and edge in the y direction. The following diagram illustrates this:

 bbox

By looking at this diagram we can derive two key logical statements about how to determine if two bounding boxes overlap. The first statement is that if the radius from the center of bounding box A to bounding box B clip_image002[9] is less than or equal to the sum of the radius of A in the x direction clip_image002[11]and the radius of B in the x direction clip_image002[13]; then bounding boxes A and B are aligned such that they share common coordinates in the x plane. We can write this like so: clip_image002[17]. Using this same train of thought we can make a similar statement for the y plane; clip_image002[15]. If both of these statements are true then bound bounding boxes must overlap or share a common edge.

We can calculate the x coordinate of the center point by adding the top left x value to the radius on the bounding box in the x direction. We can then determine the distance between centers by taking the absolute value of the different between the x coordinates of the center points of bounding box A and B. This gives use the distance clip_image002[9] which can be represented as follows:

clip_image002

The radius clip_image002[11] can be calculated by taking the difference between the top left x coordinate and the bottom right x coordinate and dividing it by 2. By applying this to the above formula we get the following:

clip_image002[6]

this can then be further reduced to the following:

clip_image002[8]

Similarly we can can derive a formula like this for clip_image002[10]:

clip_image002[12]

Now we can look at the other side of the equation where we add the radii's of bounding box A and B. Doing this we get the following for the x direction:

clip_image002[14]

this can then be reduced to the following:

clip_image002[16]

Doing the same for the y direction we get the following:

clip_image002[18]

this can then be reduced to the following:

clip_image002[20]

We can now take the formula clip_image002[17] and put it all together:

clip_image002[32]

this can then be reduced to the following:

clip_image002[34]

Lets call this condition 1.

Doing the same for the y direction we get the following:

clip_image002[36]

Lets call this condition 2.

Now if both condition 1 and condition 2 are true then the bounding boxes A and B must overlap.

We can now create a simple function that takes in two VELatLongRectangle objects and returns boolean depending on if the two bounding boxes overlap.

function DoBoundingBoxesIntersect(bb1, bb2) {

                //First bounding box, top left corner, bottom right corner
                var ATLx = bb1.TopLeftLatLong.Longitude;
                var ATLy = bb1.TopLeftLatLong.Latitude;
                var ABRx = bb1.BottomRightLatLong.Longitude;
                var ABRy = bb1.BottomRightLatLong.Latitude;

                //Second bounding box, top left corner, bottom right corner
                var BTLx = bb2.TopLeftLatLong.Longitude;
                var BTLy = bb2.TopLeftLatLong.Latitude;
                var BBRx = bb2.BottomRightLatLong.Longitude;
                var BBRy = bb2.BottomRightLatLong.Latitude;

                var rabx = Math.abs(ATLx + ABRx - BTLx - BBRx);
                var raby = Math.abs(ATLy + ABRy - BTLy - BBRy);

                //rAx + rBx
                var raxPrbx = ABRx - ATLx + BBRx - BTLx;

                //rAy + rBy
                var rayPrby = ATLy - ABRy + BTLy - BBRy;

                if(rabx <= raxPrbx && raby <= rayPrby)
                {
                                return true;
                }
                return false;
}

9/13/2009

Panning and Zooming with the Bing Map Imagery Web Services

The Bing Maps Imagery service is commonly used for mobile applications. A common issue people have when using the Bing map imagery web services is figure out how to take a map and navigate around it by panning and zooming. Zooming is pretty straight forward as all the user has to do is increase or decrease the zoom level that was used to initially retrieve the map image. Panning on the other hand is much more complicated. Generally when panning you want to move a certain pixel distance from where the user is currently viewing. To calculate the coordinate of a new location knowing our current location, direction (heading) and a distance to travel we can use the method described in this post: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!400.entry 

The distance you will want to pan will depend on the size of your map. Generally you will pick a distance in pixels. To use this pixel distance we will have to convert into a physical distance on the earth. To do this we can calculate the resolution of the ground in pixels for a particular zoom level. To calculate the resolution at a particular zoom level and latitude we can use the following formula.

clip_image002

This formula came from the following article on the Bing Maps tiling system: http://msdn.microsoft.com/en-us/library/bb259689.aspx 

The final piece of information that is needed is the direction (heading). Headings generally are an angle in degrees from 0 to 360 with 0 being North, 90 degrees being East, 180 degrees being south and west being 270 degrees. If you want to pan your map North East you will set your heading to 45 degrees.

I’ve thrown together a simple application that pulls all this together. Complete source code can be found here: http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/BingMapsPanZoom.zip

BingPanZoom


8/26/2009

Fix: Bing Maps Mouse Wheel Bug in Firefox 3.5

Currently Firefox 3.5 is not supported by Bing Maps although there are only minor issues. One issue is that if your page is scrollable and you use the mouse wheel to zoom in or out of the map the page will also scroll. This is not very good for the user experience. After some investigation I have put together the following workaround to correct this issue:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
   <head>
      <title></title>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

      <script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>

      <script type="text/javascript">
         var map = null;
         function GetMap()
         {
            map = new VEMap('myMap');
            map.LoadMap();
            //Detect if browser is Firefox 3.5
            if (navigator.userAgent.indexOf("Firefox/3.5") != -1)
            {
                document.getElementById("myMap").addEventListener('DOMMouseScroll', WindowMouseWheelHandler, false);
            }
        }
        function WindowMouseWheelHandler(e)
        {
            e.stopPropagation();
            e.preventDefault();
            e.cancelBubble = false;
            return false;
        }
      </script>
   </head>
   <body onload="GetMap();" style="font-family:Arial">
      <div id='myMap' style="position:relative; width:600px; height:400px;"></div>
      <!-- Page filling div to make the page scrollable-->
      <div style="height:10000px"></div>
   </body>
</html>