Ricky's profileRicky's Bing Maps BlogBlogSkyDrive Tools Help

Blog


    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>



    8/16/2009

    Determine if a location has Birdseye Imagery

    fRecently I was asked how to determine if a specific area on the map has Birdseye imagery. There are several ways to do this using the AJAX control or the Web Service of Bing Maps. However, one of the clients requirements was to be able to determine this before loading a map and without having to create a web service. With this requirement it is not possible to use the built in imagery metadata functionality of the map. There is not supported method to determine if a location has Birdseye imagery without loading a map and without using the Bing Maps Web Services, however it can be done. In the series of blog posts I made called “Birdseye Imagery Extraction via the Virtual Earth Web Services” I described a method to retrieve Birdseye scene information from a background service that is used by the AJAX control. By extending some of the undocumented functionalities of the we can access this backend service through the AJAX control without loading a map. This backend service takes in an array of VEParameter objects. These parameters mush include the latitude, longitude, zoom level, spin direction, and orientation of a Birdseye request. By hard coding the zoom level to 19, the spin direction to “No Spin” and the orientation to “North” we have the highest chance of getting an accurate response as this is the most common Birdseye imagery setting. Once we have created this array we can pass it along with the URL of the backend service, and a callback function into a method called JSONRequestInvoke. In that callback the response contains a property called Scene. If this property is null then Birdseye imagery is not available for the specified location otherwise it does. Below is a basic HTML page that shows how to do this.

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html>
       <head>
        <title>Birdseye Imagery Query</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <script src="
    http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>
        <script>
        var BEURL = "
    http://dev.virtualearth.net/services/v1/ImageryMetadataService/ImageryMetadataService.asmx/GetBirdsEyeSceneByLocation";
        function CheckBirdseye()
        {
            var lat = document.getElementById("lat").value;
            var lng = document.getElementById("lng").value;
            RequestBirdseyeMetadata(parseFloat(lat),parseFloat(lng));
        }
        function RequestBirdseyeMetadata(lat,lng)
        {   
            var params = [];
            params.push(new VEParameter("latitude",lat));
            params.push(new VEParameter("longitude",lng));
            params.push(new VEParameter("level",19));
            params.push(new VEParameter("spinDirection","\"NoSpin\""));
            params.push(new VEParameter("orientation","\"North\""));
            JSONRequestInvoke(BEURL, params, BirdseyeMetadataCallback);
        }
        function BirdseyeMetadataCallback(response)
        {
            if(response.Scene != null)
            {
                alert("There is Birdseye Imagery at this location");
            }
            else
            {
                alert("No Birdseye Imagery at this location");
            }
        }
        </script>
    </head>
    <body>
        Latitude: <INPUT id="lat" type="text"><br/>
        Longitude: <INPUT id="lng" type="text"><br/>
        <INPUT type="button" value="Check for Birdseye Imagery" onclick="CheckBirdseye();">
    </body>
    </html>


    8/1/2009

    VE Silverlight Control – Pushpins, Infoboxes, and Best Map View

    The Virtual Earth/Bing Silverlight control CTP release was announced at MIX09. Since this control is still in CTP there are a lot of desired functionalities that have not made it in yet. Currently polygon and polyline shapes are built into the control but pushpins are not. This was by design as it is pretty easy to create your own user control on the map to be used as a pushpin. Not all of the functionalities that are in the AJAX control made have been added to the CTP control. In particular the ability to get the best map view for an array of points, or a common infobox class. This article will show how to create a basic pushpin, create an infobox and how to implement the best map view functionality that I put together here: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!943.entry

    image

    Basic Pushpin

    To create a basic pushpin we will add in the more common properties of the pushpin object that’s in the AJAX control such as, title, description, and LatLong. We will also have properties to specify the pushpin image source, a reference to the map, and an offset that will be used to offset the position of the infobox from the center of the pushpin. to get started we will create our pushpin xaml. A MouseLeftButtonDown event will be added to the pushpin. This event will be used to display the infobox.

    <UserControl x:Class="VESilverlightMap.Pushpin"
        xmlns="
    http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="25" Height="25">
        <Grid HorizontalAlignment="Center" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5">
            <Image Width="250" Stretch="Uniform" x:Name="PinImage" MouseLeftButtonDown="PinClicked"></Image>
        </Grid>
    </UserControl>

     

    For the Pushpin class we will have to create the properties that we want the user to be able to set. The following is used to define the properties of the pushpin class.

    private Map _map;
    private Location _latlong;
    private string _title;
    private string _description;
    private int _offset = 0;

    public Map MapInstance
    {
        get { return _map; }
        set { _map = value; }
    }

    public ImageSource ImageSource
    {
        get { return PinImage.Source; }
        set { PinImage.Source = value; }
    }

    public Location LatLong
    {
        get { return _latlong; }
        set { _latlong = value; }
    }

    public string Title
    {
        get {return _title;}
        set { _title = value; }
    }

    public string Description
    {
        get { return _description; }
        set { _description = value; }
    }

    public int Offset
    {
        get { return _offset; }
        set { _offset = value; }
    }

     

    The PinClicked methoded that gets fired when a user clicks on a pushpin is used to populate the infobox information. There are several ways for the infobox to be created. To reduce the number of user control instances that are created a single infobox can be used and it’s contents updated depending on which pushpin you clicked on. This will make a significant performance difference when there are a lot of pushpins on the map. The PinClicked method will set the infobox title and discription properties and will make it visible. The infobox Position, PositionMethod, and PositionOffset properties will also be set. The PinClicked method looks like this:

    private void PinClicked(object sender, MouseEventArgs e)
    {
        //Ensure there is content to be displayed before modifying the infobox control
        if (!String.IsNullOrEmpty(_title) || !String.IsNullOrEmpty(_description))
        {
            Border infobox = (Border)_map.FindName("Infobox");
            TextBlock infoboxTitle = (TextBlock)_map.FindName("InfoboxTitle");
            TextBlock infoboxContent = (TextBlock)_map.FindName("InfoboxDescription");

            infoboxTitle.Text = _title;
            infoboxContent.Text = _description;

            infobox.Visibility = Visibility.Visible;

            PositionMethod position = VESilverlightTools.GetInfoboxPositionMethod(_latlong, _map);
            Point offset = VESilverlightTools.GetInfoboxOffset(_latlong, _map, _offset);

            MapLayer.SetMapPosition(infobox, _latlong);
            MapLayer.SetMapPositionMethod(infobox, position);
            MapLayer.SetMapPositionOffset(infobox, offset);
        }
    }

    Now that we have our Pushpin UserControl made we can now have to create a method to add these pushpins to the map. In the Page.xaml.cs file we can add the following method to add a pushpin to the map:

    public void AddPushpin(Location latlong, string title, string description, MapLayer layer)
    {
        Pushpin pushpin = new Pushpin
        {
            ImageSource = new BitmapImage(new Uri("pin.png", UriKind.Relative)),
            MapInstance = map,
            LatLong = latlong,
            Title = title,
            Description = description,
            Offset = 15
        };

        layer.AddChild(pushpin, latlong, PositionMethod.Center);


    }

    For simplicity one icon is used for all pushpins. This can be easily modified so that you can use a different icon for each pushpin. Also, I’ve hard coded in an offset value of 15. This value is used to offset the position of the infobox a certain number of pixels away from the center of the pin.

    Now that we have a way to add our pushpins to the map the following can be used to add your pushpins:

    MapLayer pinLayer = new MapLayer();
    pinLayer = (MapLayer)map.FindName("PinLayer");
    AddPushpin(new Location(43.647038, -79.3952), “My Title”, “My Description”, pinLayer);

    Note that you will need to add a MapLayer to the Page.xaml file like so:

    <!--Pushpin Layer-->
    <MapControl:MapLayer x:Name="PinLayer">
    </MapControl:MapLayer>

     

    Infobox control

    Adding an infobox to the map is similar to adding a pushpin to the map. However, simply displaying an infobox on the map is not enough, ideally  we will have logic that will know what direction to display the infobox relative to the pushpin and where it is on the map. The infobox should be displayed towards the middle of the map so that there will be less of a chance of the infobox being displayed off the page. Also, as mentioned above having one infobox and updating it’s properties is will lead to better performance than creating a separate infobox user control for each pushpin. Additionally we will want to add the infobox to a map layer that is above the pushpin layer so that the infobox will be displayed above the pushpins. To get start the following xaml will be added to the map:

    <!--Common Infobox-->
    <MapControl:MapLayer>
        <Border x:Name="Infobox" MinHeight="100" MaxHeight="200" Width="300"
                MapControl:MapLayer.MapPosition="0,0"
                Background="Black"
                Opacity="0.9"
                BorderBrush="White"
                BorderThickness="2"
                CornerRadius="5"
                Visibility="Collapsed">
            <StackPanel>
                <Grid>
                    <Button Click="CloseInfobox_Click" Tag="Close" Margin="5" Background="Black" HorizontalAlignment="Right" VerticalAlignment="Top">
                        <TextBlock>X</TextBlock>
                    </Button>
                    <TextBlock x:Name="InfoboxTitle" Foreground="#1cff1c" FontSize="12" Padding="5" Width="280" TextWrapping="Wrap" Grid.Row="0" HorizontalAlignment="Left" />
                </Grid>
                <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" MaxHeight="150">               
                       <TextBlock x:Name="InfoboxDescription" Foreground="#1cff1c" FontSize="10" Padding="5" Width="265" TextWrapping="Wrap" Height="Auto" Grid.Row="1" />                               
                </ScrollViewer>                           
            </StackPanel>
        </Border>
    </MapControl:MapLayer>

     

    This xaml that is used to create the infobox description area is set up so that if the content causes the infobox to grow to it’s max height of 200 pixels a verticle scrollbar appears. The infobox has an close button that calls a method called CloseInfobox when clicked. This method collapses the infobox so that it is no longer displayed. Here is the code for the CloseInfobox method.

    Border infobox = new Border();
    infobox = (Border)map.FindName("Infobox");
    private void CloseInfobox_Click(object sender, RoutedEventArgs e)
    {
        infobox.Visibility = Visibility.Collapsed;
    }

     

    We now need to create the tools needed to retrieve the infobox position method, and position offset properties. These properties are dependant on where the pushpin is on the viewable map when the user clicked it. To determine the position method we first want to figure out which quadrant of the map the pushpin is in so that we can display the infobox in the opposite direction. The following methods can be used to determine the position method that should be used:

    public static PositionMethod GetInfoboxPositionMethod(Location location, Map map)
    {
        Point pinPoint = map.LocationToViewportPoint(location);
        return GetInfoboxPositionMethod(pinPoint, map);
    }

    public static PositionMethod GetInfoboxPositionMethod(Point anchor, Map map)
    {
        int quadKey = 0;
        //Calculate which quadrant the anchor falls in.
        if (anchor.X > map.Width / 2)
        {
            quadKey++;
        }

        if (anchor.Y > map.Height / 2)
        {
            quadKey += 2;
        }

        PositionMethod position = PositionMethod.None;

        switch (quadKey)
        {
            case 0:
                position = PositionMethod.TopLeft;
                break;
            case 1:
                position = PositionMethod.TopRight;
                break;
            case 2:
                position = PositionMethod.BottomLeft;
                break;
            case 3:
                position = PositionMethod.BottomRight;
                break;
        }

        return position;
    }

    To calculate the infobox offset we also need to know which quadrant of the map the pushpin falls in so that the offset will be away from that quadrant. The following method can be used to determine the infobox offset:

    public static Point GetInfoboxOffset(Location location, Map map, int offsetFactor)
    {
        Point pinPoint = map.LocationToViewportPoint(location);
        return GetInfoboxOffset(pinPoint, map, offsetFactor);
    }

    public static Point GetInfoboxOffset(Point anchor, Map map, int offsetFactor)
    {
        Point offset = new Point(0, 0);

        int quadKey = 0;

        if (anchor.X > map.Width / 2)
        {
            quadKey++;
        }

        if (anchor.Y > map.Height / 2)
        {
            quadKey += 2;
        }

        switch (quadKey)
        {
            case 0:
                offset = new Point(offsetFactor, 0);
                break;
            case 1:
                offset = new Point(-1 * offsetFactor, 0);
                break;
            case 2:
                offset = new Point(offsetFactor, -1 * offsetFactor);
                break;
            case 3:
                offset = new Point(-1 * offsetFactor, -1 * offsetFactor);
                break;
        }

        return offset;
    }

    The pushpin user control will now be able to use these methods to position the infobox on the map. Note that these methods require the map to have a width and height property.

    Best Map view

    I have written a couple methods over the years that calculate the best map view for an array of coordinates. The most recent one creates a MapViewSpecification that can be used with the Silverlight control (http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!943.entry). Using this method is pretty straight forward. The following is an example of how to use this method to position the map accordingly:

    IList<Location> locations = new List<Location>();
    locations.Add(new Location(43.647038, -79.3952));
    locations.Add(new Location(43.478527, -80.549013));
    locations.Add(new Location(51.501238, -0.0233687));
    locations.Add(new Location(25.271141, 55.329089));
    locations.Add(new Location(42.362158, -71.083124));
    locations.Add(new Location(40.76083, -73.9797));
    locations.Add(new Location(40.714774, -74.005803));

    MapViewSpecification mapView = GeospatialTools.BestMapView(locations, map.Width, map.Height, 10);

    map.SetView(mapView.Center, mapView.ZoomLevel);

     

    Conclusion

    Using the techniques described in this article you should be able to easily create pushpins with infoboxes and also be able to determine the best MapViewSpecification for those pushpins.

    Source code that uses these methods can be found here: http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/VESilverlightMap%7C_PushpinsInfobox.zip

    This sample code also has a MiniMap that Earthware describes how to make in his blog here: http://www.earthware.co.uk/blog/index.php/2009/03/virtual-earth-silverlight-minimap-tutorial/


    7/21/2009

    Determining Best Map View for an array of locations

     

    In the past it has been useful to be able to determine the best map view to display an array of locations. In the AJAX control there is a method call SetMapView which determines the best map view for an array of coordinates. The VEWS and Silverlight control do not have this functionality. A long time ago someone wanted to know how to determine the best map view before loading the map. This required us to calculate out the best map view ourselves using map scale information. I later implemented the same method when working with the VEWS so that I could geo-reference images from the Imagery service (http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!488.entry). The method worked fairly well but I knew there was a better way to do this. When working with the Silverlight control I came across a situation where I needed this functionality again so I decided to create the updated method for determining the best map view using similar math that is used for the tiling system. This eliminated the need to maintain a list of scales for each zoom level and also reduce the amount of calculations that needed to be performed. Below is the updated algorithm for determining the best map view for a list of locations:

    /// <summary>
    /// Calculates the best map view for a list of locations for a map
    /// </summary>
    /// <param name="locations">List of location objects</param>
    /// <param name="mapWidth">Map width in pixels</param>
    /// <param name="mapHeight">Map height in pixels</param>
    /// <param name="buffer">Width in pixels to use to create a buffer around the map. This is to keep pushpins from being cut off on the edge</param>
    /// <returns>Returns a MapViewSpecification with the best map center point and zoom level for the given set of locations</returns>
    public static MapViewSpecification BestMapView(IList<Location> locations, double mapWidth, double mapHeight, int buffer)
    {
        MapViewSpecification mapView;
        Location center = new Location();
        double zoomLevel = 0;

        double maxLat = -85;
        double minLat = 85;
        double maxLon = -180;
        double minLon = 180;

        //calculate bounding rectangle
        for (int i = 0; i < locations.Count; i++)
        {
            if (locations[i].Latitude > maxLat)
            {
                maxLat = locations[i].Latitude;
            }

            if (locations[i].Latitude < minLat)
            {
                minLat = locations[i].Latitude;
            }

            if (locations[i].Longitude > maxLon)
            {
                maxLon = locations[i].Longitude;
            }

            if (locations[i].Longitude < minLon)
            {
                minLon = locations[i].Longitude;
            }
        }

        center.Latitude = (maxLat + minLat) / 2;
        center.Longitude = (maxLon + minLon) / 2;

        double zoom1=0, zoom2=0;

        //Determine the best zoom level based on the map scale and bounding coordinate information
        if (maxLon != minLon && maxLat != minLat)
        {
            //best zoom level based on map width
            zoom1 = Math.Log(360.0 / 256.0 * (mapWidth - 2*buffer) / (maxLon - minLon)) / Math.Log(2);
            //best zoom level based on map height
            zoom2 = Math.Log(180.0 / 256.0 * (mapHeight - 2*buffer) / (maxLat - minLat)) / Math.Log(2);
        }

        //use the most zoomed out of the two zoom levels
        zoomLevel = (zoom1 < zoom2) ? zoom1 : zoom2;

        mapView = new MapViewSpecification(center, zoomLevel);

        return mapView;
    }


    7/2/2009

    Load 3D map control at a specific location

    In the latest release of the 3D map control some new properties were added that allow you to specify where the map should appear when loading. The benefit of this is that the map will load at a specific location where as before you had to load the map and see the whole globe then call the FlyTo method to fly to your location. The properties are called StartAltitude, StartPitch, StartHeading, StartLongitude, and StartLatitude. To set these properties you should use an event handler on the RenderEngine.Initialized event. For example:

    this.globeControl.Host.RenderEngine.Initialized += new EventHandler(Initialized);

    Inside the method that gets called by the Initialized event the data sources, and the initial location information can be loaded:

    private void Initialized(object sender, EventArgs e)
           {
               // at this point, the control is fully initialized and we can interact with it without worry.

               // set various data sources, here for elevation data, terrain data, and model data.
               this.globeControl.Host.DataSources.Add(new DataSourceLayerData("Elevation", "Elevation", @"http://go.microsoft.com/fwlink/?LinkID=98774", DataSourceUsage.ElevationMap));
               this.globeControl.Host.DataSources.Add(new DataSourceLayerData("Texture", "Texture", @"http://go.microsoft.com/fwlink/?LinkID=98772", DataSourceUsage.TextureMap));
               this.globeControl.Host.DataSources.Add(new DataSourceLayerData("Models", "Models", @"http://go.microsoft.com/fwlink/?LinkID=98775", DataSourceUsage.Model));

               //Set the intial globe position
               this.globeControl.StartAltitude = 0;
               this.globeControl.StartPitch = -14;
               this.globeControl.StartHeading = 89.42;
               this.globeControl.StartLongitude = -115.12571;
               this.globeControl.StartLatitude = 36.07639;

               // Using this event is the proper way to handle loading and activation.
               this.globeControl.Host.CommunicationManager.AttachToEvent(EngineEvents.Group, EngineEvents.OnPlugInLoaded, "Loaded", PlugInLoaded);

               // Plug-ins can also be loaded by path to a dll, but this one is built-in se we reference by type.
               // If loading by path, it is possible to use both filesystem and http paths.
               // Also, if doing that it may be appropriate to execute the LoadPlugIn call on a worker thread,
               // and handle the result in OnPlugInLoaded.
               Guid g = this.loader.LoadPlugIn(typeof(NavigationPlugIn));           
           }

     

    WinForm application that demonstrates how to do this has been uploaded here: http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/VE3DFlyToExample%7C_WinForm.zip



    6/15/2009

    Find Location Nearest to You

    Currently the default behavior of the VEMap.Find function is to center the map over the matching location that is the most popular. This is great most of the time but sometimes it would be nice to be able to search for a location that is closest to where the map currently is. By default the top 10 most popular locations (VEPlace objects) are returned in the find callback. By modifying the properties in the find call up to 20 locations can be return. By setting another property you can prevent the Find method from centering over the most popular match automatically. By calculating the distance from the center of the map to each returned location in the Find callback we can determine the closest (distance) location that matches the users search. The distance from the center of the map to each point can be calculated using the haversine formula: http://rbrundritt.spaces.live.com/default.aspx?_c01_BlogPart=blogentry&_c=BlogPart&handle=cns!E7DBA9A4BFD458C5!317

    The following code is an example of how to go about doing this.

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html>
       <head>
        <title>Find Nearest to Me</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <script src="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>
        <script>
            var map = null;
            var earthRadius = 6367; //radius in km

            function GetMap()
            {
                map = new VEMap('myMap');
                map.LoadMap();
            }  
            function getLatLong(layer, resultsArray, places, hasMore, veErrorMessage)
            {   
                var origin = map.GetCenter();

                if(places.length>0)
                {
                    var location = places[0];
                    var distance = haversineDistance(origin,location.LatLong);
                    for(var i=1;i<places.length;i++)
                    {
                        var d = haversineDistance(origin,places[i].LatLong);
                        if(d < distance)
                        {
                            location = places[i];
                            distance = d;
                        }
                    }
                    map.SetMapView([location.LatLong]);
                }   
            }           

            function FindLoc()
              {
                map.Find(null, document.getElementById('txtWhere').value, null,
                    null, 0, 20, false, false, false, false, getLatLong);
              }
            function haversineDistance(latlong1,latlong2)
            {
                var lat1 = DegtoRad(latlong1.Latitude);
                var lon1 = DegtoRad(latlong1.Longitude);
                var lat2 = DegtoRad(latlong2.Latitude);
                var lon2 = DegtoRad(latlong2.Longitude);

                var dLat = lat2-lat1;
                var dLon = lon2-lon1;
                var cordLength = Math.pow(Math.sin(dLat/2),2)+Math.cos(lat1)*Math.cos(lat2)*Math.pow(Math.sin(dLon/2),2);
                var centralAngle = 2 * Math.atan2(Math.sqrt(cordLength), Math.sqrt(1- cordLength));
                return earthRadius * centralAngle;
            }
            function DegtoRad(x)
            {
                return x*Math.PI/180;
            }

        </script>
    </head>

    <body onload="GetMap();">
        <CENTER>
            <INPUT id="txtWhere" type="text" name="txtWhere" value=>
            <INPUT id="find" type="button" value="Find"  onclick="FindLoc();"><br/>
            <div id='myMap' style="position:relative; width:600px; height:400px;"></div>
        </CENTER>
    </body>
    </html>



    5/31/2009

    Drawing arrow heads on Polylines

    Sometimes adding an arrow head to polyline is desired. There are a couple of ways to do this. One way is to use arrow head images as icons and rotate to point in the desired direction. A second method is to extend the polyline and draw the arrow point. The flowing code shows you how to implement the second method.

    <!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 src="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>
        <script>
        var earthRadius = 6367; //radius in km
        var map;
        var point1 = new VELatLong(40.02,-105.03);
        var point2 = new VELatLong(40.02,-105.02);
        var point3 = new VELatLong(40.015,-105.025);
        function GetMap()
        {
            map = new VEMap('mymap');
            map.LoadMap();
            var points = generatePolylinePointsWithArrow([point1,point2,point3]);
            var polyline = new VEShape(VEShapeType.Polyline,points);
            polyline.HideIcon();
            map.AddShape(polyline);
            map.SetMapView(points);
        }
        function generatePolylinePointsWithArrow(points)
        {
            //last point in polyline array
            var anchorPoint = points[points.length-1];
            //bearing from last point to second last point in pointline array
            var bearing = calculateBearing(anchorPoint,points[points.length-2]);
            //length of arrow head lines in km
            var arrowLength = 0.05;
            //angle of arrow lines relative to polyline in degrees
            var arrowAngle = 15;
            //calculate coordinates of arrow tips
            var arrowPoint1 = calculateCoord(anchorPoint, bearing-arrowAngle, arrowLength);
            var arrowPoint2 = calculateCoord(anchorPoint, bearing+arrowAngle, arrowLength);
            //go from last point in polyline to one arrow tip, then back to the 
            //last point then to the second arrow tip.
            points.push(arrowPoint1);
            points.push(anchorPoint);
            points.push(arrowPoint2);
            return points;
        }
        function DegtoRad(x)
        {
            return x*Math.PI/180;
        }
        function RadtoDeg(x)
        {
            return x*180/Math.PI;
        }
        function calculateCoord(origin, brng, arcLength)
        {
            var lat1 = DegtoRad(origin.Latitude);
            var lon1 = DegtoRad(origin.Longitude);
            var centralAngle = arcLength /earthRadius;
            var lat2 = Math.asin( Math.sin(lat1)*Math.cos(centralAngle) + Math.cos(lat1)*Math.sin(centralAngle)*Math.cos(DegtoRad(brng)));
            var lon2 = lon1+Math.atan2(Math.sin(DegToRad(brng))*Math.sin(centralAngle)*Math.cos(lat1),Math.cos(centralAngle)-Math.sin(lat1)*Math.sin(lat2));
            return new VELatLong(RadtoDeg(lat2),RadtoDeg(lon2));
        }
        function calculateBearing(A,B)
        {
            var lat1 = DegtoRad(A.Latitude);
            var lon1 = A.Longitude;
            var lat2 = DegtoRad(B.Latitude);
            var lon2 = B.Longitude;
            var dLon = DegtoRad(lon2-lon1);
            var y = Math.sin(dLon) * Math.cos(lat2);
            var x = Math.cos(lat1)*Math.sin(lat2) - Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
            var brng = (RadtoDeg(Math.atan2(y, x))+360)%360;
            return brng;
        }
          </script>
          </head>
       <body onload="GetMap();">
            <div id='mymap' style="position:relative; height:400px; width:800px;"></div>
       </body>
    </html>

    5/18/2009

    Drawing Dashed Routes behind Buildings in Birds eyes

    Recently I was asked how to draw route lines in Birds eye so that they become dashed when behind a building. Needless to say this turned into a pretty good brain teaser as the dashed lines on the Birds eye tiles are part of the image. So after some thought the possible methods that I could think of included:

    • Use the 3D API of Virtual earth to determine where buildings are and see if based on the Birds eye orientation if a building is between the camera and the road. This could be done ahead of time and the data stored. It could also be done programmatically if the user is using 3D. This would require a significant amount of development. This method would also only work where 3D models exist.
    • Create regions where tall buildings exist and then if a polyline is drawn in this region the direction in which the line runs can be determined. If the road runs horizontal more horizontal than vertical to the current birdseye orientation then make the line dashed. This method would be easier to develop and could be used dynamically, but would not be perfect. However, looking at some Birds eye imagery I’ve noticed that the dashed lines do not always show up when they should.

    I ended up putting together some sample code on how to do the second method. This is were the code for the article "Dashed Polylines in Virtual Earth" originated from.

    What the code does is defines a set of circular regions where tall buildings are known to exist (user defined) and then calculates the heading of a section of a path (http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!393.entry). If heading is more perpendicular than parallel to the Birds eye orientation the line is drawn as a dashed line.

    As an added bonus, polylines done appear to be rendered when added in Birds eye mode. So the map needs to be flipped to a different map style, the polyline added, and then flipped back to Birds eye. For simplicity I used an array of points for my route. An actual VE route can be used when using client tokens and retrieving the route geometry (http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!782.entry).

    The complete source code for this sample can be found here: http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/dashedRoutes.zip

    Here is a screen shot of this code in action:

    image



    Dashed Polylines in Virtual Earth

    Virtual Earth has a lot of options for for working with shapes, unfortunately there are so many additional options that are not document (thus unsupported) that are available. However, just because it's not documented doesn't mean it can't be used, it just means that it may break at any time and if it does then it's no ones fault but your own. Now that the fair warning is out of the way here is how to draw dashed polylines.

    Before adding a polyline to the map set the VEShapeStyle.prototype.stroke_dashstyle  property to one of the following:

    • Solid
    • DashDot
    • ShortDash
    • ShortDot
    • ShortDashDot
    • ShortDashDotDot
    • Dot
    • Dash
    • LongDash
    • LongDashDot
    • LongDashDotDot

    For an added effect, resize the width of the polyline based on the zoom level.

    I've put together a nice little sample of how to do this which can be downloaded here:

    http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/dashedLines.zip

    Here is a screen shot of what the sample program looks like:

    image


    Multiple Routes in Virtual Earth

    Out of the box Virtual Earth has the ability to draw multipoint routes however it does not have the ability to display more than one route object at a time. Questions on how to do this have been appearing on the Virtual Earth forums for a while now. The general method to do this is to use client tokens with your map so that you can retrieve the route geometry from the Virtual Earth. Once you have the route geometry you can then use polylines to draw any number of routes on your map with out the previous map disappearing.

    To make things easy I create my own Route object. This object has a polyline which is used to draw the route path, an array of icons to mark the turn by turn points, a directions property which contains HTML that displays the turn by turn directions, a route title which is used to label the route in a list next to the map so that we can display the route markers and directions for a different route, a route color in both Hex and RGB format. RGB format is needed for VE, and hex format is needed for HTML. This object has a prototype called ShowRouteInfo, which hides all pushpins on the map, displays the directions for the route, shows the pushpins icons for the route, and sets the best map view for the route. This object looks like s:

    //create a common route object to store only the information we need
    function Route() {
        this.polyline = null;
        this.icons = [];
        this.directions = "";
        this.routeTitle = "";
        this.routeColorHex = "";
        this.routeColorRGB = new VEColor(0,0,255,1);
    }

    Route.prototype.ShowRouteInfo = function() {
        //hide all pins on pin layer
        HideAllPins();

        //add directions to directions panel
        document.getElementById('directions').innerHTML = this.directions;

        //show route icons
        for (var i = 0, j = this.icons.length; i < j; i++) {
            this.icons[i].Show();
        }

        //set the map view to show the route
        map.SetMapView(this.polyline.GetPoints());
    }

     

    One catch with the routing information that is returned by VE is that if you try and get the custom icon for a leg of the route using the GetcustomIcon method on the VERouteLeg.Shape property you will end up with the default red icon. We can however access the url to the correct icon by referencing the _customIcon property of the VERouteLeg.Shape object. We will also want to create a new pushpin object otherwise our pushpin will not exist after we delete the default route. A simple function to create the correct route icon looks like this:

    //copy a route pin so that we can delete the route from the map
    function CopyRoutePin(shape) {
        var newShape = new VEShape(shape.GetType(), shape.GetPoints());
        newShape.Title = shape.GetTitle();
        newShape.Description = shape.GetDescription();

        //using the GetCustomIcon method on a route pin gets the standard red pin
        //The _customIcon property of the route icon is the url to the correct icon
        var customIcon = new VECustomIconSpecification();
        customIcon.TextContent = " ";
        customIcon.Image = shape._customIcon;
        newShape.SetCustomIcon(customIcon);

        return newShape;
    }

     

    The rest of the work done in the code base for this is pretty straight forward. The complete source code can be downloaded here:

    http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/MultipleVERoutes.zip

    Here is a screen shot of this code in action:

    image



    5/3/2009

    Integrating a GPS receiver with Virtual Earth

    Integrating a GPS receiver with Virtual Earth has often been a topic that has come up on the Virtual Earth forums. In the book Practical .NET 2.0 Networking Projects there is a great section on how to create a Virtual Earth WinForm application that integrates with a GPS device. The Virtual Earth related code was written for version 3. Virtual Earth is now in its 6th version and the code has changed a lot since version 3. Many others who have attempted to follow the code in the previously mentioned book have had some issues getting the code to work. After hearing a lot of questions on the forums I ended up buying the book myself and put together an updated version of the this application but never got around to posting it online. So its better late than never.

    I've made a few modifications to the original design. Knowing that I would not always have access to the internet I have set the program up so that even if there is no internet connection it will still be capable of recording the RAW GPS data in a text file. This file can then be read later by the same program when an internet connection is available and display the path on a Virtual Earth map. 

    For the GPS I managed to pick one up used on EBay for $15. It connects to a computer through USB but requires a serial port emulator to work correctly which is available from the GPS manufactures web site.

    This application requires following steps:

    • Connect to GPS receiver
    • Read and process GPS NMEA data
    • Record data to output file (optional)
    • Update current position displayed
    • Display location on a Virtual Earth map
    • Be able to read a saved file and plot path on map

    Connecting to a GPS Receiver

    The first step is to get your computer to recognize your device. Drivers may be needed in order for your device to be made available through one of your serial ports (COM). These drivers usually give you the ability to set which ports your GPS data will be transmitted on and if it will transmit the raw data or NMEA data. You will want to transmit NMEA data. The manufacturer of your GPS Receiver may provide an application to monitor your GPS receiver. This is very useful when debug your application. It also usually allows you to see what other information can be retrieved from the RAW data that the comes from the GPS device. Once your computer is able to connect to your GPS receiver you should be able to connect to it as well.

    To connect to your GPS receiver from your application we will have a drop down with a list of all the available serial ports so that the user can select the port that their GPS device is connected to. You will want to populate this drop down when the application loads. The following code can be used to retrieve a list of serial ports and fill our drop down with the names:

    string[] portNames = System.IO.Ports.SerialPort.GetPortNames();

    for (int i = 0; i < portNames.Length; i++)
    {
        portNumberBox.Items.Add(portNames[i]);
    }

     

    s1

    Our application has a button to connect to the GPS receiver. We will give this button the ability to b oth connect and disconnect to the GPS receiver. If we want to connect to the GPS receiver we will want to close the port if it is already open. Next we can specify the port configuration. Once the port configuration are set the connection can be opened. If we want to disconnect from the GPs receiver we just have to call the Close method of the connected serial port. The following code shows how to do this:

    private void GPSConnect_Click(object sender, EventArgs e)
    {
        if (GPSConnectBtn.Text == "Connect")
        {
            GPSConnectBtn.Text = "Disconnect";
            //close serial port if it is open
            if (serialPort.IsOpen)
            {
                serialPort.Close();
                timer1.Enabled = false;
            }

            try
            {
                //configure the parameters of the serial port
                serialPort.PortName = portNumberBox.Text;
                serialPort.BaudRate = 9600;
                serialPort.Parity = System.IO.Ports.Parity.None;
                serialPort.DataBits = 8;
                serialPort.StopBits = System.IO.Ports.StopBits.One;

                serialPort.Open();
                timer1.Enabled = true;
                statusTxt.Text = "GPS on port " + portNumberBox.Text + " connected.";
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
        else
        {
            //close the serial port
            GPSConnectBtn.Text = "Connect";
            statusTxt.Text = "GPS on port " + portNumberBox.Text + " disconnected.";
            serialPort.Close();
            timer1.Enabled = false;
        }
    }

     

    Reading and Processing GPS Data

    The data that you will be receiving from your GPS will be in NMEA format. NMEA stands for National Marine Electronics Association. The National Marine Electronics Association developed a standard for representing GPS related data, often referred to as NMEA sentences. The following is an example of some NMEA data that I'm able to receive from my GPS:

    $GPRMC,054715,A,4340.254,N,07923.009,W,0.0,0.0,030509,10.4,W*48
    $GPGGA,054715,4340.254,N,07923.009,W,0,12,50.00,37.48,M,4.1,M,,0000*08
    $GPGSA,A,1,00,00,00,00,00,00,00,00,00,00,00,00,0.000,50.00,0.000*35
    $GPGSV,3,1,12,19,07,066,33,11,54,068,00,17,52,267,00,08,43,197,00*7A
    $GPGSV,3,2,12,28,79,335,00,27,20,317,00,32,14,095,00,20,14,123,35*71
    $GPGSV,3,3,12,07,11,173,00,26,07,306,29,120,10,108,27*7B

    Most GPS devices support the NMEA schema. Here are a list of common NMEA data sentences and their meanings:

    Sentence Description
    $GPGGA Global positioning system fixed data
    $GPGLL Geographic position: latitude/longitude
    $GPGSA GNSS DOP and active satellites
    $GPGSV GNSS satellites in view
    $GPRMC Recommended minimum specific GNSS data
    $GPVTG Course over ground and ground speed

     

    More information on NMEA sentences can be found here: http://www.gpsinformation.org/dale/nmea.htm 

    For our application we are only interested in geographical location  of where we are. This information is contained in the $GPGGA sentence. The $GPGGA sentence separates it's data using commas. The data in this field has the following meaning:

    Field Sample Description
    0 $GPGGA Sentence prefix
    1 054715 UTC time (in hhmmss.sss format)
    2 4340.254 Latitude (in ddmm.mmmm format)
    3 N (N)orth or (S)outh
    4 07923.009 Longitude (in ddmm.mmmm format)
    5 W (W)est or (E)ast
    6 0 Position Fix (0 is invalid, 1 is valid, 2 is valid DGPS, 3 is valid PPS)
    7 12 Satellites used
    8 50.00 Horizontal  dilution of precision
    9 37.48 Altitude (unit specified in next field)
    10 M M is meter
    11 4.1 Geoid separation (unit specified in next field)
    12 M M is meter
    13   Age of DGPS data (in seconds)
    14 0000 DGPS station ID
    15 *08 Checksum

    Note that field 14 and 15 are not comma separated.

    For our application we will want to latitude, longitude, altitude, and unit of measure of the altitude. We can retrieve this information from columns 2-5, 9,10.

    To read the GPS data we first need to set up a timer that will consistently query the GPS for data. When the timer fires an event we will call a method called UpdateGPSData.This method will verify that the serial port is open and then will read the current data from the port. It will then verify that data was received. In our application we will output the raw data to a textbox for the user to see. We will then pass the data to method called ProcessNMEAData which will parse the data and update the map if need be. If the user is saving data to a file the data will be written to the file. The UpdateGPSData method looks like this:

    public void UpdateGPSData()
    {
        try
        {
            if (serialPort.IsOpen)
            {
                string data = serialPort.ReadExisting();

                if (!String.IsNullOrEmpty(data))
                {
                    GPSData.Text = data + "\r\n";
                    GPSData.ScrollToCaret();
                    ProcessNMEAData(data);           

                    if (!String.IsNullOrEmpty(outputFile))
                    {
                        sw.WriteLine(data);
                    }
                }
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }

    The ProcessNMEAData method iterates through each NMEA sentence it receives and passes it on to the appropriate method to be process further. Since we only need the $GPGGA data we will send all data in this sentence to a method called ProcessGPGGA. We will setup the frame work from accessing the other sentences for future use. The ProcessNMEAData looks like this:

    private void ProcessNMEAData(string data)
    {
        string[] NMEALine = data.Split('$');
        string[] NMEAType;

        for (int i = 0; i < NMEALine.Length; i++)
        {
            NMEAType = NMEALine[i].Split(',');

            switch (NMEAType[0])
            {
                case "GPGGA":
                    ProcessGPGGA(NMEAType);
                    break;
                case "GPGLL":
                    break;
                case "GPGSA":
                    break;
                case "GPGSV":
                    break;
                case "GPRMC":
                    break;
                case "GPVTG":
                    break;
                default:
                    break;
            }
        }
    }

    The ProcessGPGGA method will parse the raw data in the $GPGGA sentence and will update the appropriate methods. The latitude and longitude values in the NMEA sentence are in degrees, minutes, and decimal minute form. We will want to convert these to decimal degrees. This can be done using the following formula:

    clip_image002

    Note that the vertical bars represent integer division. The direction of the latitude and longitude coordinate can make the respective value negative if the longitude value is in the (W)est direction or if the latitude value is in the (S)outh direction. We will display the current location in a textbox for the user. If the user is connected to the internet then we can send the data to the map to be displayed. If the user is loading in a file of NMEA sentences then this method will call a javascript function called AddPoint. This function creates an array of coordinates. If the user is not loading in a file and has selected the option to follow their position then a pushpin will be used to display the users current location on the map. This is achieved by calling a method called AddPushpin.

    private void ProcessGPGGA(string[] data)
    {
        double lat, lon;
        double rawLatLong;

        rawLatLong = double.Parse(data[2].Replace(":00",""));
        lat = ((int)(rawLatLong / 100)) + ((rawLatLong - (((int)(rawLatLong / 100)) * 100)) / 60);

        if (data[3] == "S")
            lat *= -1;

        rawLatLong = double.Parse(data[4].Replace(":00", ""));
        lon = ((int)(rawLatLong / 100)) + ((rawLatLong - (((int)(rawLatLong / 100)) * 100)) / 60);

        if (data[5] == "W")
            lon *= -1;

        currentLatitudeTbx.Text = lat.ToString();
        currentLongitudeTbx.Text = lon.ToString();

        if(internetConnected)
        {
            if (!loadingFile && FollowCbx.Checked)
            {
                StringBuilder sb = new StringBuilder();
                sb.AppendFormat("<div>Latitude: {0}<br/>Longitude: {1}<br/>Altitude: {2} {3}</div>", lat, lon, data[9], data[10]);
                AddPushpin(lat, lon, sb.ToString());
            }
            else if(loadingFile)
            {
                object[] param = new object[] { lat, lon };
                webBrowser1.Document.InvokeScript("AddPoint", param);
            }
        }
    }

     

    Displaying data on a Virtual Earth map

    s2

    A simple HTML page is used to create an Virtual Earth map. This HTML page is loaded into a Web Browser control in our WinForm application. This map will have two shape layers. One to display paths, and one to display pushpins. A function called AddPushpin will remove all other pushpins and create a new pushpin with the data it is sent. A function called drawPath will use a polyline to draw out a path of locations that either the user has currently traveled or have loaded. Four other simple functions are used to add a point to the points array, clear the points array, center the map and update the data. The updateMapData method will send information back to the WinForm application so that it can display the information in a textbox. This HTML page looks like this:

    <!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://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>
            <script type="text/javascript">
                var map;
                var pinLayer;
                var pathLayer;
                var points = new Array();
                var pin = null;

                function pageLoad()
                {
                    map = new VEMap("myMap");
                    map.LoadMap();
                    map.EnableShapeDisplayThreshold(false);
                    updateMapData("Map Loaded");
                    pinLayer = new VEShapeLayer();
                    map.AddShapeLayer(pinLayer);
                    pathLayer = new VEShapeLayer();
                    map.AddShapeLayer(pathLayer);
                }

                function AddPushpin(lat, lon, description) {
                    var coordinate = new VELatLong(lat, lon);
                    pin = new VEShape(VEShapeType.Pushpin, coordinate);
                    pin.SetDescription(description);

                    if (pinLayer.GetShapeCount() > 0)
                        pinLayer.DeleteAllShapes();
                    pinLayer.AddShape(pin);
                    points.push(coordinate);
                    map.SetCenter(coordinate);
                    updateMapData("Pin added at "+lat+" "+lon);
                }

                function drawPath() {
                    if (points.length > 2) {
                        if (pathLayer.GetShapeCount() > 0)
                            pathLayer.DeleteAllShapes();
                        var path = new VEShape(VEShapeType.Polyline, points);
                        path.HideIcon();
                        pathLayer.AddShape(path);
                        map.SetMapView(points);
                    }
                }

                function centerMap(lat, lon) {
                    map.SetCenter(new VELatLong(lat,lon));
                }

                function updateMapData(msg) {
                    window.external.UpdateMapData(msg);
                }

                function AddPoint(lat, lon) {
                    points.push(new VELatLong(lat,lon));
                }

                function ClearPoints() {
                    points = new Array();
                }
            </script>
        </head>
        <body onload="pageLoad()" style="margin:0px">
            <div id='myMap' style="position:relative; width:740px; height:586px;"></div>
        </body>
    </html>

    Before we can load the map and display locations on it we need to determine if there is an internet connection. A simple way to do this is to try and make a web request to a well known URL and verify that an "OK" response is received. The following method does this:

    private bool HasInternetConnection()
    {
        HttpWebRequest req;
        HttpWebResponse resp;
        try
        {
            req = (HttpWebRequest)WebRequest.Create("http://maps.live.com");
            resp = (HttpWebResponse)req.GetResponse();

            if (resp.StatusCode.ToString().Equals("OK"))
            {
                //its connected.
                return true;
            }
        }
        catch (Exception)
        {
            UpdateMapData("No internet connection");
        }
        return false;
    }

     

    In the ProcessGPGGA method we make a call to an AddPushpin method. This method passes our coordinate information to the javascript. This method looks like this:

    private void AddPushpin(double lat, double lon, string description)
    {
       object[] param = new object[] { lat, lon, description };
       webBrowser1.Document.InvokeScript("AddPushpin", param);
    }

    If the user has decided not to follow their current location on the map but decides they would like to just show the current location on the map this can by centering the map over the current location. The button click event for this functionality looks like this:

    private void MapCurrentLocBtn_Click(object sender, EventArgs e)
    {
        double lat, lon;

        if (internetConnected && Double.TryParse(currentLatitudeTbx.Text, out lat)
            && Double.TryParse(currentLongitudeTbx.Text, out lon))
        {
            object[] param = new object[] { lat, lon };
            webBrowser1.Document.InvokeScript("centerMap", param);
        }
    }

     

    s3

    Many of the other functionality that are in this application are pretty straight forward and can be found in the source code  here: http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/GPSMapper.zip

    An idea for integrating some of the data that is in the other NMEA sentences: It should be possible to determine the position of all the satellites that your GPS sees. With this in mind you should be able to create a Virtual Earth 3D application that displays 3D models of the satellites around the globe in the correct position.

    4/16/2009

    Virtual Earth Image Viewer

    Virtual Earth is a great tool for viewing geo-spatial information on a map. It offers great user experience by being able to pan and zoom the map. Sometimes you may wish you could do this with a static image of your own. In Silverlight you can use DeepZoom to do this but doing this in a regular HTML web page requires a lot of custom JavaScript. So why not use Virtual Earth to do this for you? There are a couple of ways to do this. One is to us MapCruncher and generate a bunch of tile images and then add them to the map as a tile layer. This often results in a large number of tiles. This will work in both 2D and 3D modes which is great if your viewing geo-spatial imagery but if you only need 2D tools there are other options.

    Another option is to add the image to the map like a pushpin and to resize it depending on the zoom level. Recently Chris Pendleton blogged about an interesting way to add pushpins to the map by creating image tags and adding them to a div and then adding the div to the map as a shape layer. You can view the post here: http://blogs.msdn.com/virtualearth/archive/2009/04/09/virtual-earth-api-release-information-april-2009.aspx

    By expanding upon the methods described in this post we can view an image the same way we view a Virtual Earth map. Here is some sample code that demonstrates how to do this:

    <!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://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>
    <script type="text/javascript">

    var map = null;
    var myLayer = null;
    var pinImage = 'http://www.okanagan.com/ski/sun_peaks_map.jpg';

    //image dimensions used form scaling
    var imageHeight = 904;
    var imageWidth = 1025;

    var previousZoomLevel = null;

    //RedrawShapes
    function RedrawShapes()
    {
        var pixel = map.LatLongToPixel(new VELatLong(85,-180));
        var zoom = map.GetZoomLevel();
        var mapWidth = 256 * Math.pow(2,zoom);
        var mapHeight = mapWidth * imageHeight/imageWidth;
        myLayer.innerHTML = "<img src='" + pinImage + "' style='position:absolute; left:"
    + pixel.x + "px; top:" + pixel.y + "px;width:"+mapWidth+"px;height:"+mapHeight+"px;' +/>";
    }

    //View Change Event handler
    function EventViewChange()
    {
        //if zoom level changed, then redraw the shapes, otherwise do nothing
        //-- the map will position the pins on pan properly
        var currentZoomLevel = map.GetZoomLevel();
        if (previousZoomLevel != currentZoomLevel)
        {
            previousZoomLevel = currentZoomLevel;
            ClearShapes();
            RedrawShapes();
        }
    }

    //Event to respond to Birdseye changes
    function EventBirdseyeChanged()
    {
        //When Birdseye rotation changes, redraw the shapes
        ClearShapes();
        RedrawShapes();
    }

    //Clear the shapes
    function ClearShapes()
    {
        myLayer.innerHTML = '';
    }

    //Start Zoom event handler
    function EventStartZoom()
    {
        //When zoom is staretd clear the shapes before zoom since they will need to be redrawn
        ClearShapes();
    }

    //Load Map Event Handler
    function EventMapLoaded()
    {
        //Store previous zoom level as current zoom level
        previousZoomLevel = map.GetZoomLevel();

        CreateLayer();

        //Register for events to update the custom layer
        map.AttachEvent("onchangeview", EventViewChange);
        map.AttachEvent("onstartzoom", EventStartZoom);
        map.AttachEvent("onobliquechange", EventBirdseyeChanged);
        RedrawShapes();
    }
    function CreateLayer()
    {
        //Create the custom layer for the map dynamically on the map surface
        myLayer = document.createElement('div');
        myLayer.style.position = "absolute";
        myLayer.style.top = "0px";
        myLayer.style.left = "0px";
        myLayer.style.width = "500px";
        myLayer.style.height = "400px";
        myLayer.style.zIndex = 1000;
        map.AddCustomLayer(myLayer);
    }

    function CreateMap()
    {
        var mapOptions = new VEMapOptions();
        mapOptions.LoadBaseTiles = false;
        map = new VEMap('myMap');
        map.onLoadMap = EventMapLoaded;
        map.LoadMap(null,1,null,null,null,null,null,mapOptions);
        map.HideDashboard();
        map.HideScalebar();
    }
    </script>
    </head>
    <body onload="CreateMap();">
    <div id='myMap' style="position:relative; width:500px; height:400px;"></div>
    </body>
    </html>

    4/10/2009

    Bird's eye Routes

    It was recently brought to my attention that routes are not supported in the Bird's eye map view. Some how this slipped by me as I swear I've seen it working before. After some thought I suspect that this has to do with possible accuracy issues. A few releases ago a new map method was introduced called VEMap.SetShapeAccuracy (http://msdn.microsoft.com/en-us/library/bb877873.aspx). This method only increases the accuracy of pushpin's. Currently if you draw a route and try an view it in the bird's eye map view you will find that there is no route line drawn only segment markers. One way to correct this issue is to retrieve the route geometry (requires a client token) and draw a polyline in place of the route line. Using this method will not increase the accuracy but will allow you to see a route line. In my tests I haven't noticed any real accuracy issues using this method. Complete source code that demonstrates this method can be found here: http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/BirdseyeRouting.zip

    Here is a screen shot of a bird's map with a route drawn on it using this method:

    image

    4/5/2009

    VE 3D Flight Simulator Version 1.1

    After having such a huge positive response from many people about the Virtual Earth 3D flight simulator I built a couple of weeks ago I decided to make some enhancements to it. The enhancements were mainly focused around improved controls for a XBox controller. I have overridden the default XBox controls that are, by default, similar to the controls for Halo and made them closer to the controls of other common flight simulators. The following controls where overridden:

    Left thumb control - Y direction:  This control used to only move you in a plane parallel to the ground and did not take the pitch of the camera into consideration. This control now allows you to accelerate in the direction the camera is pointing.

    Left thumb control - X direction: This control used to allow you to strafe to the side. This control now allows you to roll the plane.

    Right thumb control - Y direction: This control handles the pitch of the plane.

    Right thumb control - X direction: This control handles turning.

    Here is a video of the new simulator in action:  

     

    Additional enhancement was to hide the default location data that appeared in the bottom right corner of the screen. This was done by adding the following code into the Activate method of the flight simulator plug-in:

    this.Host.WorldEngine.ShowLocation = false;

     

    The new controls were defined by creating xml that could be added to the built in bindings. There are two ways to get this xml into your plug-in. One method is to create an xml file that needs to be merged with the default bindings xml file that is on the users computer. This requires the dll's of the plug-in to be installed onto the users computer and the dll to be placed in the GAC in order to get the required permissions to access the default bindings file. By using this method you can make it so that your controls are available in all instances of Virtual Earth 3D. An example of this method can be found here: http://blogs.msdn.com/virtualearth3d/archive/2008/05/01/installing-plug-ins.aspx

    The second method is to store the xml as a string inside of the plug-in and then add the xml data to the bindings. This method is much simpler and reduces a lot of the overhead.  An example of this method is used in the following article: http://blogs.msdn.com/virtualearth3d/archive/2008/07/08/animation.aspx

    This code uses the second method as it reduces the end users work when it comes to playing with the simulator.

    The new functionality for the left thumb control in the x direction required calculating the LatLong coordinate and altitude of where the plane is suppose to go to. A distance in which to travel is determined based on how much the user pushing the thumb control forward. This distance is then broken into two components, a horizontal and vertical component. These components are determined using the pitch of the camera and some trig. Using the horizontal component and the heading of the camera the destination coordinate of the plane is determined. These calculations are based on the following article: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!400.entry

    The camera is then update by extending the CameraControl class. An example of how to do this can be found here: http://blogs.msdn.com/virtualearth3d/archive/2008/10/22/camera-control.aspx

    Complete source code for this version of the flight simulator can be downloaded here:

     http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/SimpleFlightSimulatorV1.1.zip

    3/22/2009

    Virtual Earth 3D Flight Simulator

    Update: Chck out Version 1.1 of this flight simulator here:http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!773.entry

    The Virtual Earth 3D control is a great tool when you not only want to know where some place is in the world, but also want to know what it looks like. This is particularly useful when traveling to new areas as landmarks can be easily identified ahead of time. Recently Microsoft released documentation on how to develop against this 3D API (http://blogs.msdn.com/virtualearth3d/archive/2009/01/25/documentation.aspx). Infusion development has been working with Virtual Earth 3D for well over a year now developing business applications. Most of these applications have been developed for the Microsoft Surface. Videos of various applications can be found here: http://www.youtube.com/InfusionDevelopment

    One thing I’ve noticed is that there hasn’t been a lot of development against the 3D API yet outside of the business world. This article is going to show you how to create a simple flight simulator plug-in for Virtual Earth. Here is a screen shot of the finished product to get you motivated to read through this article. You can also download the source code for this here: http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/SimpleFlightSimulator.zip

    Note you will need to re-add the references to the Virtual Earth DLL’s.

    clip_image002 

    Here is a video to show it in action. Note I was just using the keyboard for the navigation in this screen capture.

        

    Here is a video of it in action using an XBox 360 controller to navigate.  

    The first step is to install the Virtual Earth 3D control if you haven’t done so already. You can download it here: http://maps.live.com/Help/VE3DInstall/

    The second step is to create an ASP >NET Web Application project. In Visual Studios 2008 you will have to do the following steps:

    1. Open Visual Studio 2005/2008.
    2. From the File menu, choose New | Project.
    3. Choose one of the Project Types. For example, Visual C#.
    4. From the Templates list, choose Class Library.
    5. Call the project SimpleFlightSimulator and verify the project Location.
    6. Click OK to create the solution.

    The next step is to reference the Virtual Earth 3D DLL’s.

    1. From the Solution Explorer, right click the project name and click Add Reference.

    1. In the popup window, click the Browse tab and navigate to the location where you installed the Virtual Earth 3D application. (eg. C:\Program Files\Virtual Earth 3D)
    2. Hold the CTRL key down and select:
      Microsoft.MapPoint.Data
      Microsoft.MapPoint.Geometry

    Microsoft.MapPoint.Graphics3D
    Microsoft.MapPoint.Rendering3D
    Microsoft.MapPoint.Rendering3D.Utility
    Microsoft.MapPoint.UtilityPartialTrust

    4. A reference to System.Drawing, System.IO, System.Runtime.InteropServices, System.Threading and System.Reflection will also be needed.

    1. Click the OK button.

    Now you will have to create the plug-in class. This can be done by doing the following:

    1. Right click the new project's name and select Add | New Item.

    1. Under Visual C# Items | Code, select the Class item and call your class SimpleFlightSimulatorPlugin.cs
    2. Click the Add button and open the newly created file. This file will represent our plug-in for this example.
    3. The first 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.


    Part 2 - http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!752.entry
    Part 3 - http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!751.entry
    Part 4 - http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!750.entry


    Virtual Earth 3D Flight Simulator - part 2

    Your class should look like this:

    using System;
    using System.IO;
    using System.Drawing;
    using System.Threading;
    using System.Reflection;

    using System.Runtime.InteropServices;
    using Microsoft.MapPoint.Rendering3D.GraphicsProxy;
    using Microsoft.MapPoint.PlugIns;
    using Microsoft.MapPoint.Rendering3D;

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

    namespace SimpleFlightSimulator
    {
        [Guid("67D5CFD7-9975-492d-B7AE-1B4DE757B0BD")]
        public class SimpleFlightSimulatorPlugin : 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 "Simple Flight Simulator"; }
            } 


            public SimpleFlightSimulatorPlugin(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();

            }

    You will now create an Airplane class. This class will have three methods, Load, UpdateCockpit, and UpdateGauges. The Load method will create a ScreenImageActor to overlay the airplane cockpit imagery over the map and a ScreenTextActor to create gauge readings. The gauges will display the coordinates of the Airplane (camera), altitude, pitch (horizon) and heading. The UpdateCockpit method will allow you to change the airplane cockpit image. The UpdateGauges method will retrieve all the data required for the gauges and update the ScreenTextActor. The Airplane class should look like this:

    using System;
    using System.IO;
    using System.Drawing;
    using System.Reflection;
    using Microsoft.MapPoint;
    using Microsoft.MapPoint.Rendering3D;
    using Microsoft.MapPoint.Rendering3D.Cameras;
    using Microsoft.MapPoint.Rendering3D.Steps.Actors;
    using Microsoft.MapPoint.Geometry.VectorMath;

    namespace SimpleFlightSimulator
    {
        public class Aiplane
        {
            private ScreenTextActor gauges;
            private ScreenImageActor imageActor;
            private Host _host;

            public Aiplane(Host host)
            {
                this._host = host;
            }

            public void Load(string imageSource, Point imagePosition, Size imageSize, Color gaugeColor, Point gaugePosition)
            {
                gauges = new ScreenTextActor("GaugeReadings", "", new Font(FontFamily.GenericMonospace, 11.0f), gaugeColor, gaugePosition, null);
                this._host.Actors.Add(gauges);

                Stream data = Assembly.GetExecutingAssembly().GetManifestResourceStream(imageSource);
                Bitmap cockpit = (Bitmap)Bitmap.FromStream(data);
                data.Close();

                imageActor = new ScreenImageActor(cockpit, imagePosition, imageSize, null);
                this._host.Actors.Add(imageActor);
            }


            public void UpdateCockpit(Host host, string imageSource, Point imagePosition, Size imageSize, Color gaugeColor, Point gaugePosition)
            {
                gauges.Color = gaugeColor;
                gauges.Position = gaugePosition;

                imageActor.Size = new Size(0, 0);

                Stream data = Assembly.GetExecutingAssembly().GetManifestResourceStream(imageSource);
                Bitmap cockpit = (Bitmap)Bitmap.FromStream(data);
                data.Close();

                ScreenImageActor newImageActor = new ScreenImageActor(cockpit, imagePosition, imageSize, null);
                host.Actors.Add(newImageActor);

                imageActor = newImageActor;
            }


            public void UpdateGauges(object state)
            {
                RollPitchYaw rpy = this._host.Navigation.CameraLocalOrientation;
                GeodeticCameraSnapshot cameraSnapshot = this._host.Navigation.CameraSnapshot;
                double heading = cameraSnapshot.LocalOrientation.RollPitchYaw.Yaw * Constants.DegreesPerRadian;

                if (heading <= 0)
                    heading *= -1;
                else
                    heading = 360 - heading;

                GeodeticPositionSnapshot position = this._host.Navigation.CameraPosition;
                double pitch = rpy.Pitch * Constants.DegreesPerRadian;

                //Altitude in meters
                double altitude = position.Location.Altitude;

                this.gauges.Text = String.Format("Heading: {0:N2}º\r\nPitch: {1:N2}º\r\nAltitude: {2:N2} m\r\nLatitude: {3:N5}º\r\nLongitude: {4:N5}º",
                    heading, pitch, altitude, position.Location.LatitudeDegrees, position.Location.LongitudeDegrees);
            }
        }
    }


    Part 1 - http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!756.entry
    Part 3 - http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!751.entry
    Part 4 - http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!750.entry