Ricky's profileRicky's Bing Maps BlogBlogSkyDrive Tools Help

Blog


    3/22/2009

    Virtual Earth 3D Flight Simulator - part 3

    Another class called Airport can be made to specify an Airport location. This class will have a method called FlyToHere that will cause the plane to fly to runway of the selected airport. This class should look like this:

    using System;
    using Microsoft.MapPoint;
    using Microsoft.MapPoint.Rendering3D;
    using Microsoft.MapPoint.Rendering3D.Cameras;
    using Microsoft.MapPoint.Geometry.VectorMath;

    namespace SimpleFlightSimulator
    {
        public class Airport
        {
            private LatLonAlt _latlong;
            private RollPitchYaw _rpy;

            public Airport(double latitude, double longitude, double altitude, double pitch, double heading)
            {
                this._latlong = LatLonAlt.CreateUsingDegrees(latitude, longitude, altitude);
                this._rpy = new RollPitchYaw(0, pitch, heading);
            }


            public void FlyToHere(Host host)
            {
                host.Navigation.FlyTo(this._latlong, this._rpy.Pitch, this._rpy.Yaw);
            }
        }
    }

    The next step is to create an HTML page to load Virtual Earth into. A reference to the virtual Earth map control will need to be made. In a script tag you will need to create four functions; OnPageLoad, On3DPlugInLoaded, FlyToAirport, and ChangeCockpit. The OnPageLoad function will create and load a Virtual Earth map. It will then load in the new plug-in you created. The On3DPlugInLoaded function will activate the plug-in that you created. The FlyToAirport function will allow you to specify an airport to fly to from the client side. The ChangeCockpit method will allow you to change the cockpit overlay from the client side. The script section of your HTML page should look like this:

    <title>Simple Flight Simulator</title>
    <script src="http://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 = "SimpleFlightSimulator";
    var isLoaded = false;

    function OnPageLoad() {
        map = new VEMap('myMap');
        map.LoadMap(null, 2, VEMapStyle.Hybrid, false, VEMapMode.Mode3D);

        map.HideDashboard();
        map.HideScalebar();
        control3D = map.vemapcontrol.Get3DControl();
        control3D.AttachEvent("OnPlugInLoaded", "On3DPlugInLoaded");
        control3D.LoadPlugInDll("C:\\Users\\rbrundritt\\Documents\\VE3D\\SimpleFlightSimulator\\SimpleFlightSimulator\\bin\\Debug\\SimpleFlightSimulator.dll"); // local path to where your dll lives
    }

    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);
            isLoaded = true;
        }
    }

    function FlyToAirport() {
        if (isLoaded) {
            var airport = document.getElementById('airports').options[document.getElementById('airports').selectedIndex].value;
            control3D.RaiseEvent(objectPlugInGuid, "FlyToAirport", airport);
        }
        else
            alert("Please wait for plugin to load");
    }

    function ChangeCockpit() {
        if (isLoaded) {
            var cockpit = document.getElementById('cockpits').options[document.getElementById('cockpits').selectedIndex].value;
            control3D.RaiseEvent(objectPlugInGuid, "ChangeCockpit", cockpit);
        }
        else
            alert("Please wait for plugin to load");
    }       
    </script>

    The body of your page should look like this:

    <body onload="OnPageLoad();" style="background-color:Black;">
    <table>
        <tr><td colspan="2" align="center" style="color:White;"><h2>VE 3D Simple Flight Simulator</h2></td></tr>
        <tr>
            <td align="center">
                <select id="airports">
                    <option value="LasVegas">Las Vegas International</option>
                    <option value="newyorkNewark">Newark Liberty International (New York)</option>
                    <option value="seattleTacoma">Seattle-Tacoma International</option>
                    <option value="TorontoPearson">Pearson Internalional (Toronto)</option>
                </select>
                <input type="button" value="Go To Airport" onclick="FlyToAirport();" />
            </td>
            <td align="center">
                <select id="cockpits">
                    <option value="cockpit1">Cockpit 1</option>
                    <option value="cockpit2">Cockpit 2</option>
                    <option value="cockpit3">Cockpit 3</option>
                </select>
                <input type="button" value="Change Cockpit" onclick="ChangeCockpit();" />
            </td>
        </tr>
        <tr>
            <td colspan="2" align="center">
                <div id="myMap" style="position:relative;width:1024;height:768;"></div>
            </td>
        </tr>
    </table>
    </body>


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

    Virtual Earth 3D Flight Simulator - part 4

    You now have to create the main functionality of the plug-in. You will want to create some global Airport objects like so:

    private Airport lasVegas = new Airport(36.07639, -115.12571, 0, -14, 89.42);
    private Airport torontoPearson = new Airport(43.6738, -79.66459, 0, -14, -47.13);
    private Airport newyorkNewark = new Airport(40.67988, -74.17279, 0, -14, -24.56);
    private Airport seattleTacoma = new Airport(47.43102, -122.30807, 0, -14, -2);

    You will want to create a method to hook into the JavaScript from the client side. This can be done like so:

    private void RegisterInterfaces()
    {
        this.UnregisterInterfaces();
        if (!this.interfacesRegistered)
        {
            this.Host.CommunicationManager.AttachToEvent(this.Guid, "FlyToAirport", "FlyToAirportHandler", FlyToAirport);
            this.Host.CommunicationManager.AttachToEvent(this.Guid, "ChangeCockpit", "ChangeCockpitHandler", ChangeCockpit);
            this.interfacesRegistered = true;
        }
    }

    For good practice you should also create a method to unreferenced these hooks. This can be done like so:

    private void UnregisterInterfaces()
    {
        if (this.interfacesRegistered)
        {
            this.Host.CommunicationManager.DetachFromEvent(this.Guid, "FlyToAirport", "FlyToAirportHandler");
            this.Host.CommunicationManager.DetachFromEvent(this.Guid, "ChangeCockpit", "ChangeCockpitHandler");
            this.interfacesRegistered = false;
        }
    }

    In order to fly between airports you will need to create a method called FlyToAirport which will determine which airport you want to fly to and will navigate the camera to the runway at the selected airport. This method should like this:

    private void FlyToAirport(string functionName, object airport)
    {
        switch (airport.ToString())
        {
            case "LasVegas":
                lasVegas.FlyToHere(this.Host);
                break;
            case "TorontoPearson":
                torontoPearson.FlyToHere(this.Host);
                break;
            case "newyorkNewark":
                newyorkNewark.FlyToHere(this.Host);
                break;
            case "seattleTacoma":
                seattleTacoma.FlyToHere(this.Host);
                break;
            default:
                break;
        }
    }

    In order to change the cockpit overlay you will need to create a method called ChangeCockpit. This method will determine which cockpit the user has selected and will call the UpdateCockpit method of the airplane. This method should like this:

    private void ChangeCockpit(string functionName, object cockpit)
    {
        switch (cockpit.ToString())
        {
            case "cockpit1":
                airplane.UpdateCockpit(this.Host, "SimpleFlightSimulator.cockpit.png", new Point(0, 0), new Size(1024, 768), Color.White, new Point(755, 375));
                break;
            case "cockpit2":
                airplane.UpdateCockpit(this.Host, "SimpleFlightSimulator.cockpit2.png", new Point(0, 268), new Size(1024, 500), Color.GreenYellow, new Point(430, 375));
                break;
            case "cockpit3":
                airplane.UpdateCockpit(this.Host, "SimpleFlightSimulator.cockpit3.png", new Point(0, 0), new Size(1024, 768), Color.DarkBlue, new Point(560, 235));
                break;
            default:
                break;
        }
    }

    In the Activate method you will want to create and Airplane object and load it. You can also want to create a timer object that will call a method calls the UpdateGaage method of your airplane object. You will also want to call the RegisterInerfaces method. This method should now look like this:

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

        //create the airplane cockpit overlay
        airplane = new Aiplane(this.Host);
        airplane.Load("SimpleFlightSimulator.cockpit.png", new Point(0, 0), new Size(1024, 768), Color.White, new Point(775, 375));

        //registers a javascript interface
        RegisterInterfaces();

        //use a timer to update the gauges every 250ms
        timer = new Timer(new TimerCallback(airplane.UpdateGauges), null, 0, 250);

        //start off at the Toronto Pearson airport
        FlyToAirport(null,"TorontoPearson");
    }

    The Deactivate method should also be updated to call the UnregisterInterfaces method and setting the timer timeout to infinite. Setting a timers timeout to infinite essentially disables it. This method should now look like this:

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

        //stop the timer
        timer.Change(Timeout.Infinite, 25);

        //unregister the javascript interface
        UnregisterInterfaces();
    }

     

    In order to load the cockpit images into the plug-in you will have to make them embedded resources. This can be done by right clicking on the images and going to the properties. Set the Build Action to Embedded Resource.

    The final step is to strongly name the plug-in. This is required to give the plug-in partial trust to your system. 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 TestPlugin.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 SimpleFlightSimulatorPlugin.snk file and click Open. The strong name file is automatically added to your project.
    9. Save the properties window and then close it.

    You can now build your project to compile the plug-in DLL. You can then open the HTML in a browser to view your flight simulator. To increase the sensation of flying around Virtual Earth with your own flight simulator try using an Xbox controller. The built in navigation controls of Virtual Earth recognizes the Xbox controller. The controls are similar to Halo.

    The complete source code for this application can be downloaded here:

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


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

    3/12/2009

    Recursive Find Calls

    When using the AJAX version of the Virtual Earth control people have had issues making multiple find calls, one after the other. The resulting information does not always return in the order expected. The way around this issue is to use recursion. This will essentially make one call at a time and wait for one find call to finish before making another. The following code demonstrates how to do this:

     

    <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 map = null;
             var count = 0;
             var locations = ["Toronto, Ontario","New York, New York","Seattle, WA"];
             function GetMap()
             {
                map = new VEMap('myMap');
                map.LoadMap();
             }  

             function FindLocation(searchstr)
             {
                if (searchstr != '')
                {
                    map.Find(null, searchstr, null, null, null, null, false, null, null, false, AddPin);
                }
             }
             function AddPin(layer, resultsArray, places, hasMore, veErrorMessage)
             { 
                if(places.length > 0)
                {
                    var shape = new VEShape( VEShapeType.Pushpin,places[0].LatLong);
                    shape.SetTitle(places[0].Name);
                    map.AddShape(shape);
                }
                count++;
                if(count<locations.length)
                    FindLocation(locations[count]);
             } 
          </script>
       </head>
    <body onload="GetMap();">
    <div id='myMap' style="position:relative; width:800px; height:600px;"></div>
    <INPUT type="button" value="Find Locations" onclick="FindLocation(locations[0]);">
    </body>
    </html>


    2/22/2009

    Drawing Routes with the VE Web Service

    The Virtual Earth web services are fairly new and as such it does not currently have all the capabilities of it's JavaScript counter part. One key functionality that is not currently available is the ability to draw routes on a map image. This can be done by extending the methods described in the article "VE Imagery Service Polygons and Polylines" found here: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!497.entry 

    In order to display a route on a map image the following steps will have to be taken:

    1) Geocode the start and end points of the route using the Geocoding service.

    2) Calculate a Route and have the route geometry returned using the Routing Service.

    3) Calculate the best map view for the route path.

    4) Request map image from the imagery service that matches the best map view calculated in step 3.

    5) Draw the route line on the map image using .NET drawing tools.

    6) Draw the route turn points on the map image using .NET drawing tools.

    Below is an example of a route that was created using the above method. Complete sample code can be found here:

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

    image

    2/17/2009

    Advance Polygon shapes in Virtual Earth

    Virtual Earth currently supports polygons that consist of a single array of points. Many geospatial systems have complex polygons which may consist of several arrays of points which are a combination of outer and inner arrays of points. These are often referred to as Multipolygons. Multipolygons are often used to represent complex boundary areas such as "donut" shaped polygons. By properly concatenating inner and outer polygon array segments in the proper order a single array can be created to represent a complex polygon. An array of polylines can then be created to represent the edges of the polygon. The following method takes in an array of inner and outer polygon array segments and returns and an array of shapes. This array consists of a polygon and and several polylines. This array of shapes can then be added to the map either by looping through each shape and adding it to the map, or by passing the array through the MultiShape class described here:

    http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!605.entry

    The MultiShape class will give you greater control of the complex polygon. 

    function CreateAdvancePolygon(polyPoints)
    {
        if(polyPoints.length > 0)
        {
            var anchor = polyPoints[0][0];
            var points = polyPoints[0].concat(anchor);
            var lines = new Array();
            var line = new VEShape(VEShapeType.Polyline,points);
            line.HideIcon();
            lines.push(line);
            for(var i=1;i<polyPoints.length;i++)
            {
                points = points.concat(polyPoints[i],polyPoints[i][0],anchor);
                var line = new VEShape(VEShapeType.Polyline,polyPoints[i].concat(polyPoints[i][0]));
                line.HideIcon();
                lines.push(line);
            }
            var polygon = new VEShape(VEShapeType.Polygon, points);
            polygon.SetLineColor(new VEColor(0,0,0,0));
            polygon.HideIcon();
            return lines.concat(polygon);
        }
        return null;
    }

     

    Below is an example of a complex polygon that was created using the above method. Complete sample code for this polygon can be found here: http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/AdvancePolygon.zip

    image

    2/14/2009

    VEImagery Service Pixel to LatLong calculations

     

    The VEImagery service gives us the ability to render static versions of Virtual Earth Maps. As handy as this is it would also be handy to be able to georeference the image that is returned. The article found here:

    http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!488.entry

    explains how to convert from LatLong to Pixel so that custom icons could be added to the map, however the conversion from Pixel to LatLong was not needed at that time. Building on top of the code that was provided in that article the following method can be used to calculate a LatLong value based on the pixel coordinate of a static image. The following formula can be used to perform this calculation.

            /// <summary>
            /// Convert Pixel coordinates to LatLong coordinates
            /// Based on code found here: http://msdn.microsoft.com/en-us/library/cc161076.aspx
            /// </summary>
            /// <param name="offset"></param>
            /// <returns></returns>
            public LatLong PixelToLatLong(Point offset)
            {
                int x = TopLeftCorner.X + offset.X;
                int y = TopLeftCorner.Y + offset.Y;

                double Longitude = (((double)x * 360) / (256 * Math.Pow(2, view.zoom))) - 180;

                double efactor = Math.Exp((0.5 - (double)y / 256 / Math.Pow(2, view.zoom)) * 4 * Math.PI);

                double Latitude = Math.Asin((efactor - 1) / (efactor + 1)) * 180 / Math.PI;

                LatLong latlong = new LatLong();
                latlong.Latitude = Latitude;
                latlong.Longitude = Longitude;

                return latlong;
            }

     

    The complete source code can be downloaded here:

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

    Here is a screen shot of this application in action.

    image

    2/5/2009

    MultiShapes and Virtual Earth

     

    Current Virtual Earth supports pushpins, polylines, and polygons. Another commonly used shape type that is used in other mapping applications is MultiShape or MultiGeometry. Not having this in Virtual Earth has been an issue for some users. I have put together a javascript file that can be loaded after the Virtual Earth map control that creates a MultiShape class that can be used with Virtual Earth. This new shape object takes in an array of VEShape objects and has methods to add and remove shapes from the object. Many of the functionalities that exist in the VEShape class exist in the MultiShape class. The VEMap and VEShapelayer classes have been extended in order to support this adding and removing of the MultiShape objects from the map using the existing methods in Virtual Earth. This javascript file and a simple sample html file can be downloaded here:

    http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Tools/MultiShape.zip

    Note: Since this extends the VEMap and VEShapeLayer classes by accessing undocumented methods in the map control it is possible that those methods may change without notice. This may result in unexpected errors occurring in the future.

    Browsers tested: IE7, FF2

    Virtual Earth versions tested: 5, 6, 6.1, 6.2

    MultiShape Constructor

    var x = new MultiShape(shapes);

    Parameter Descrption
    shapes An array of VEShape objects to be added. Optional.

     

    Public Methods

    These methods can be used in the same way as the equivalent methods in the VEShape object.

    Method Description
    SetDescription Sets the description of the MultiShape object.
    GetDescription Gets the description of the MultiShape object.
    SetTitle Sets the title of the MultiShape object.
    GetTitle Gets the title of the MultiShape object.
    GetAltitudeMode Gets the mode in which the shape's altitude is represented.
    SetAltitudeMode Specifies the mode in which a shape's altitude is represented.
    GetAltitude Returns the altitude for the shape.
    SetAltitude Specifies the altitude for the shape.
    GetCustomIcon Gets the MultiShape object's custom icon.
    SetCustomIcon Sets the MultiShape object's custom icon.
    GetFillColor Gets the fill color and transparency used for all polygons. The default is null, unless a fill color is set for the MultiShape object.
    SetFillColor Sets the fill color and transparency for all polygons in the MultiShape object.
    GetIconAnchor Gets a VELatLong Class object representing the MultiShape's custom icon anchor point.
    SetIconAnchor Sets the info box anchor of the MultiShape object.
    GetLineColor Gets the line color or transparency for all polylines or polygons. The default is null, unless a line color is set for the MultiShape object.
    SetLineColor Sets the line color or transparency for all polylines or polygons in the MultiShape object.
    GetLineWidth Gets the line width of all polylines or polygons. The default is null, unless a line width is set for the MultiShape object.
    SetLineWidth Sets the line width for all polylines or polygons in the MultiShape object.
    Hide Hides the specified MultiShape object from view
    Show Makes the specified MultiShape object visible.
    HideIcon Hides the icon associated with the MultiShape object.
    ShowIcon Shows the icon associated with the MultiShape object.
    SetMinZoomLevel Sets the minimum zoom level at which the shape is visible
    GetMinZoomLevel Gets the minimum zoom level at which the shape is visible
    SetMaxZoomLevel Sets the maximum zoom level at which the shape is visible
    GetMaxZoomLevel Gets the maximum zoom level at which the shape is visible.
    SetPhotoURL Sets the shape's "photo" URL.
    GetPhotoURL Gets the shape's "photo" URL.
    GetMoreInfoURL Gets the shape's "more info" URL.
    SetMoreInfoURL Sets the shape's "more info" URL.
    SetZIndex Sets the z-index value for all shapes in the MultiShape object.
    GetZIndex Gets the z-index of a pushpin shape or pushpin attached to a polyline or polygon. The default is null, unless a z-index is set for the MultiShape object.
    GetID Gets the internal identifier of the MultiShape object.
    GetShapeLayer Gets the reference to the layer containing the specified MultiShape object.
    GetShapeByIndex Retrieves a reference to a VEShape object contained in this MultiShape object based on the specified index.

     

    These methods can be used in the same way as the equivalent methods in the VEShapeLayer class.

    Method Description
    AddShape Add's a VEShape object to the MultiShape object.
    DeleteShape Remove's a VEShape object to the MultiShape object.
    GetShapeByID Retrieves a VEShape object from a MultiShape object by it's id.
    GetBoundingRectangle Retrieves a VELatLongRectangle which encloses all VEShapes that are in the MultiShape object
    GetShapeCount Returns the number of shapes are in the MultiShape object.

     

    These are new methods which can be used with a MultiShape object.

    Method Description
    GetIconLocation Gets a VELatLong Class object of the pushpin associated with the MultiShape object.
    SetIconlocation Sets the location of the pushpin associated with the MultiShape object.
    IndexOfShape Retrieves the index of a VEShape that is in a MultiShape object.

     

    Constructors

    var x = MultShape.GetIconLocation();

    var x = MultShape.SetIconlocation(latlong); where latlong is a VELatLong object.

    var x = MultShape.IndexOfShape(shape); where shape is a VEShape object.

    MultiShape properties

    Property Description
    id The id used to identify the MultiShape object.
    shapes The array of VEShape objects which are contained in the MultiShape object.
    pin The pushpin used associated with the MultiShape object.

     

    Extended VEMap and VEShapeLayer method's

    The following methods have been extended to work when used with MultiShape objects.

    Method Description
    AddShape Adds a MultiShape object to the map/shape layer. All VEShapes in the MultiShape object will have a parentId parameter which is the id of the MultiShape object.
    DeleteShape Deletes a MultiShape object from any layer, including the base map layer.
    DeleteAllShapes Deletes all shapes.
    GetShapeByID Retrieves a reference to a MultiShape object contained in this layer based on the specified ID.
    ShowInfoBox Shows an information box for the shape.

    1/18/2009

    Custom Client Side Clustering Algorithm

    In the latest release of Virtual Earth, version 6.2, client side clustering was included as a built in function for shape layers. You can assign a client side clustering algorithm to a shape layer so that it will automatically cluster the shapes when the map is zoomed. This is done by using the VEShapeLayer.SetClusteringConfiguration method. This method can take in two parameters. A clustering type and a VEClusteringOptions object. Currently there is only one built in clustering algorithm type available, grid clustering. Unfortunately there is limited flexibility in this algorithm in that the grid size can not be modified. This can be resolved by making a custom clustering algorithms. This post will show how to create a custom clustering algorithm that can be used with the VEShapeLayer.SetClusteringConfiguration method.

    According to the documentation (http://msdn.microsoft.com/en-us/library/cc966716.aspx) a custom clustering algorithm must take in a VEShapeLayer and return an array of VEClusterSpecification objects.

    A VEClusterSpecification objects has two properties. A Shapes property and a LatLong property. The Shapes object consists of an array shapes that are being clustered. The LatLong property is the coordinate used to place the clustered pushpin. (http://msdn.microsoft.com/en-us/library/cc966730.aspx)

    The custom clustering algorithm that will be created in this blog post will implement a grid clustering algorithm and place the clustered pushpins in the mean average coordinate of the clustered pins. This algorithm is an improvement upon the clustering algorithm described here: http://msdn.microsoft.com/en-us/library/cc161072.aspx

    Here is the custom clustering algorithm which grid clusters and places the clusters in the mean average coordinate of the clustered pins. You can modify the size of the grid used in the algorithm by changing the value of the gridSize variable.

    function meanAverageGridCluster(baseLayer)
    {
        var cluster = new Array();
        //the size of the grid in pixels   
        var gridSize = 30;                       

        //Calculate the size of the map in pixels
        var mapView = map.GetMapView();
        var bottomRight = map.LatLongToPixel(mapView.BottomRightLatLong);
        var mapWidth = parseInt(Math.ceil(bottomRight.x));
        var mapHeight = parseInt(Math.ceil(bottomRight.y));
        //break the map into a grid
        var numXCells = parseInt(Math.ceil(mapWidth / gridSize));
        var numYCells = parseInt(Math.ceil(mapHeight / gridSize));

        //create an array to store all the grid data.
        var gridCells = new Array(numXCells*numYCells);
        //Itirate through the shapes in the base layer
        for(var cnt = 0; cnt < baseLayer.GetShapeCount(); cnt++)
        {
            //convert the shapes latlong to a pixel location
            var shape = baseLayer.GetShapeByIndex(cnt);
            var latLong = (shape.GetPoints())[0];
            var pixel = map.LatLongToPixel(latLong);
            var xPixel = pixel.x;
            var yPixel = pixel.y;

            //check to see if the shape is within the bounds of the viewable map
            if(mapWidth >= xPixel && mapHeight >= yPixel && xPixel >= 0 && yPixel >= 0)
            {
                //calculate the grid position on the map of where the shape is located
                var i = Math.floor(xPixel/gridSize);
                var j = Math.floor(yPixel/gridSize);
                //calculates the grid location in the array
                var key = i+j*numXCells;

                if(gridCells[key]==null)
                {
                    gridCells[key] = new VEClusterSpecification();
                    gridCells[key].Shapes = new Array();
                    gridCells[key].Shapes.push(shape);
                }
                else
                {
                    gridCells[key].Shapes.push(shape);
                }
            }
        }
        //Iterate through the clustered data in the grid array
        for(var key = 0; key < gridCells.length; key++)
        {
            //calculate mean clustered coordinate
            if(gridCells[key] != null)
            {
                var size = gridCells[key].Shapes.length;
                var latSum = 0;
                var lonSum = 0;
                for(var i=0;i<size;i++)
                {
                    var point = gridCells[key].Shapes[i].GetPoints()[0];
                    latSum += point.Latitude;
                    lonSum += point.Longitude;
                }
                gridCells[key].LatLong = new VELatLong(latSum/size,lonSum/size);
                cluster.push(gridCells[key]);
            }
        }
        return cluster;   
    }

    The following code can be used to implement this customer clustering algorithm.

    var map = null;

    function GetMap()
    {
        map = new VEMap('myMap');
        map.LoadMap();

        var clusterLayer = new VEShapeLayer();
        clusterLayer.SetClusteringConfiguration(meanAverageGridCluster);
        map.AddShapeLayer(clusterLayer);


        //Import data from source. In this case a GeoRSS is used. 
        var veLayerSpec = new VEShapeSourceSpecification(VEDataType.GeoRSS, "georsstest.xml", clusterLayer);
        map.ImportShapeLayerData(veLayerSpec);
    }

     


    1/7/2009

    Birds Eye Imagery Extraction Via the Virtual Earth Web Services - Part 1

    Using the Imagery service available in the Virtual Earth Web Services, it is possible to retrieve a map image for a specific location (as specified by specific latitude and longitude values). This method however, is not available to the Birds Eye imagery. Rather further functionality must be applied within the Web Services in order to retrieve accurate Birds Eye Imagery Metadata. The information on the Birds Eye scene for the desired location is present in the URI that is used to retrieve the map tiles which make up the Birds Eye scene. To return the tile in which the specified coordinate exits currently a tile id for the map image must first be provided. The extraction of the correct tile id for alternate map styles, using the Quad Key notation is described in the article, Virtual Earth Tile System (http://msdn.microsoft.com/en-us/library/bb259689.aspx).

    The methodology described in the Virtual Earth Tile System article is unfortunately not applicable to Birds Eye imagery as a standard Birds Eye scene does not fit perfectly into a quad key frame (for example, a Birds Eye scene at zoom level 19 consists of 4 tiles in the x-axis and 6 tiles in the y-axis and at zoom level 20 there are 8 tiles in the x-axis and 12 tiles in the y-axis).  Furthermore the Birds Eye scenes do not populate the quad key format properly, rather tiles that are on the edge of a scene, only fill a portion of a standard map tile. This behavior is caused because the Birds Eye images are geo-referenced, high resolution images which are then overlayed onto the Virtual Earth map differently than a standard tile.

    The following (figure 1), is a Birds Eye scene shown over a standard road view in Virtual Earth at zoom level 19. The red boxes represent the Birds Eye tiles, and the black boxes represent the VE road tiles over the same area. As is evident, the Birds Eye tiles do not properly line up with the edges of the Virtual Earth tiles, which are placed on the map according to the quad key notation. The Birds Eye imagery therefore must be retrieved using a difference methodology than would be used for a standard VE tile.

    image001

    Figure 1 - Birds Eye over standard map tile at zoom level 19

    The Birds Eye geo-referencing information is stored in a database that can only be extracted using the backend web services the Virtual Earth Map Control implements. The data is returned as JavaScript, and can be extracted from the feed using another step, such as regular expressions. With the information retrieved, it is possible to then geo-reference the Birds Eye tiles, or calculate the specific tile id that a coordinate falls into. Note that this is a hack and is completely unsupported as it's possible that a change on VE's side will cause this URL or method to break.

    The following are the necessary steps to calculate the tile id based on an input coordinate:

    1)      Retrieve the Birdseye Imagery Metadata for a coordinate using the Virtual Earth Web Services.

    2)      Retrieve geo-referencing data for the Birdseye scene in which the coordinate resides using backend Virtual Earth web service.

    3)      Extract geo-reference data from web service response using regular expressions.

    4)      Convert LatLong to pixel coordinate, relative to top left corner of Birdseye scene.

    5)      Calculate the x and y tile positions.

    6)      Calculate tile id from x and y tile positions.

    7)       Retrieve Birds Eye tile in which the coordinate resides.

    Retrieve Birds Eye Imagery from Virtual Earth Web Services

     

    Retrieving the Imagery Metadata for a Birds Eye Scene in which a coordinate exists can be accomplished using the following simple example on how to build the request;

    Code Sample

    double latitude = 40.68;

    double longitude = -73.93;

    int zoom = 19;

    int heading = 0;

     

    ImageryMetadataRequest metadataRequest = new ImageryMetadataRequest();

     

    // Set credentials using a valid Virtual Earth Token

    metadataRequest.Credentials = new VEImageryService.Credentials();

    metadataRequest.Credentials.Token = clientToken;

     

    // Set the imagery metadata request options

    VEImageryService.Location centerLocation = new VEImageryService.Location();

    centerLocation.Latitude = latitude;

    centerLocation.Longitude = longitude;

     

    metadataRequest.Options = new ImageryMetadataOptions();

    metadataRequest.Options.Location = centerLocation;

     

    metadataRequest.Options.ZoomLevel = zoomLevel;

     

    metadataRequest.Options.Heading = new Heading();

    metadataRequest.Options.Heading.Orientation = heading;

     

    metadataRequest.Style = MapStyle.BirdseyeWithLabels;

     

    // Make the imagery metadata request

    ImageryServiceClient imageryService = new ImageryServiceClient();

    ImageryMetadataResponse metadataResponse = imageryService.GetImageryMetadata(metadataRequest);

     

    The ImageryMetadataResult object contains several properties; the following variables will be needed in either the calculations that follow or in creating the URI to the map tile:

    Variable

    Value

    ImageSize.Height

    Height of a single tile

    ImageSize.Width

    Width of a single tile

    ImageUriSubdomains

    An array of sub-domains that can be used to construct the URI

    ImageUri

    A string specifying the URI for the image

     

    Additional information on how to use the Virtual earth web services can be found in the article, Developing a .NET Application Using Virtual Earth Web Services (http://msdn.microsoft.com/en-us/library/dd221354.aspx).

    Retrieve Geo-Referencing Data for a Birds Eye scene

     

    Geo-Referencing data for Birds Eye scenes is not available through the Virtual Earth Web Services rather the information needs to be retrieved from a backend web service. The following is an example web service request that can be used to access the service:

    http://dev.virtualearth.net/services/v1/ImageryMetadataService/ImageryMetadataService.asmx/GetBirdsEyeSceneByLocation?latitude=40.77638178482896&longitude=-73.61663818359376&level=20&spinDirection=%22NoSpin%22&orientation=%22North%22&culture=%22en-us%22&format=json&rid=1227643110358&

    This request contains multiple variables:

    Variable

    Value

    latitude

    Latitude value

    longitude

    Longitude value

    level

    Zoom level of Birds Eye image

    orientation

    Direction of Orientation (North, South, East, West)

    culture

    The culture in which the labels should be in

    format

    This is the return format of the data. Currently “json” is the only available format.

    rid

    This is an id used for the request. This needs to be in the request however using the same rid for each request is acceptable.

     

    The following is an example of a response from the web service:

    Example Response

    function _f1227643110358(){return {"__type":"Microsoft.VirtualEarth.Engines.Core.ImageryMetadata.PublicTypes.BirdsEyeSearchResponse","Scene":{"S":8110562,"O":0,"Q":"03201011103","RI":4354,"L":20,"Fcx":16,"Fcy":12,"Hcx":8,"Hcy":6,"QA":-0.40797472410705138,"QB":20.928199325031251,"QC":165807.88732506905,"QD":0.22288172553678412,"QE":-11.587628391710947,"QF":-91840.5941131519,"QG":0.0054790346170970417,"QH":-0.28430088283524946,"QI":-2252.1714401186127,"XA":-207.10129411915017,"XB":-86.358261661444089,"XC":-11725.499006073529,"XD":-19.539468822757811,"XE":164.61435354728968,"XF":-8151.2791376682235,"XG":0.0019627160694678978,"XH":-0.020990038814928769,"XI":1},"ResponseSummary":{"Copyright":"Copyright © 2008 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.","StatusCode":0,"AuthResultCode":0,"ErrorMessage":null,"TraceId":"2cb7463b-dc46-4a21-8ba5-25bd825845fb"}};} if(typeof closeDependency !== 'undefined'){closeDependency('1227643110358');}

    Extract Geo-Reference Data from Web Service Response

     

    The web service response contains more information than necessary; the following is the only portion that is needed:

    {"S":8110562,"O":0,"Q":"03201011103","RI":4354,"L":20,"Fcx":16,"Fcy":12,"Hcx":8,"Hcy":6,"QA":-0.40797472410705138,"QB":20.928199325031251,"QC":165807.88732506905,"QD":0.22288172553678412,"QE":-11.587628391710947,"QF":-91840.5941131519,"QG":0.0054790346170970417,"QH":-0.28430088283524946,"QI":-2252.1714401186127,"XA":-207.10129411915017,"XB":-86.358261661444089,"XC":-11725.499006073529,"XD":-19.539468822757811,"XE":164.61435354728968,"XF":-8151.2791376682235,"XG":0.0019627160694678978,"XH":-0.020990038814928769,"XI":1}

     

    Each variable is defined with a variable name and value in the format:  “name”: value. An explanation as to what some variable names means is as follows:

    Variable

    Value

    S

    Scene ID

    O

    Direction in degrees (North, East, South, West)

    Q

    Quad key Birds Eye scene is contained in at zoom level 11

    L

    Maximum zoom level available for this scene

    Hcx

    Number of tiles used in the x-axis

    Fcy

    Number of tiles used in the y-axis

    QA - QI

    Geo-referencing constants needed to convert Pixel to LatLong

    XA - XI

    Geo-referencing constants needed to convert LatLong to Pixel

     

    These variables can be extracted using regular expressions similar to the following:

    C# Example

    Regex QArx = new Regex("\"QA\":(-?\\d+\\.?\\d*)");

    Match match = QArx.Match(response);

    double QA = double.Parse(match.Groups[1].Value.ToString());

     

    Java Example

    Pattern QArx = Pattern.compile("\"QA\":(-?\\d+\\.?\\d*)");

    Matcher match = QArx.matcher(response);

    double QA = Double.parseDouble(match.group(1));



    Birds Eye Imagery Extraction Via the Virtual Earth Web Services - Part 2

     

    Convert Latitude & Longitude to Pixel Coordinate

     

    The calculated pixel will be the pixel coordinates relative to the top left corner of the scene.  The first step in this calculation is to create a 1x3 matrix of the LatLong coordinate.

    image002

    Once the LatLong matrix is created a second matrix is needed to store the geo-referencing data XA-XI. This will require a 3x3 matrix.

     image003

    These matrices need to be multiplied together in the following fashion:

    image004

    Before the pixel coordinates can be calculated a zoom factor constant is needed.

    image005

    The following formula’s can then be used to calculate the pixel coordinates:

    image006

    image007

    By expanding out the calculations the pixel coordinates can be calculated as follows:


    image008

    image009

    Calculate X and Y Tile Positions

     

    The X tile position can be calculated by dividing the x pixel location by the width of a tile and rounding this number down to an integer value.

    image010

    The Y tile position can be calculated by dividing the y pixel location by the height of a tile and rounding this number down to an integer value.

    image011

    Calculate tile id from x and y tile positions

     

    The X and Y tile positions along with the tileId are calculated for zoom level 19 using the following grid pattern:

    X = 0

    Y = 0

    tileID = 0

    X = 1

    Y = 0

    tileID = 1

    X = 2

    Y = 0

    tileID = 2

    X = 3

    Y = 0

    tileID = 3

    X = 0

    Y = 1

    tileID = 4

    X = 1

    Y = 1

    tileID = 5

    X = 2

    Y = 1

    tileID = 6

    X = 3

    Y = 1

    tileID = 7

    X = 0

    Y = 2

    tileID =8

    X = 1

    Y = 2

    tileID = 9

    X = 2

    Y = 2

    tileID = 10

    X = 3

    Y = 2

    tileID = 11

    X = 0

    Y = 3

    tileID = 12

    X = 1

    Y = 3

    tileID = 13

    X = 2

    Y = 3

    tileID = 14

    X = 3

    Y = 3

    tileID = 15

    X = 0

    Y = 4

    tileID = 16

    X = 1

    Y = 4

    tileID = 17

    X = 2

    Y = 4

    tileID = 18

    X = 3

    Y = 4

    tileID = 19

    X = 0

    Y = 5

    tileID = 20

    X = 1

    Y = 5

    tileID = 21

    X = 2

    Y = 5

    tileID = 22

    X = 3

    Y = 5

    tileID = 23

     

    Zoom level 20 uses a similar method, however the grid is 8 tiles in the x-axis and 12 tiles in the y-axis. The number of tiles in the x-axis for a particular Birds Eye Scene is stored in the Hcx value and the number of tiles in the y-axis is stored in the Fcy value of the web response to the back end web service. The following formula calculates a tile id from the X and Y tile positions:

    image012

    Retrieve Birds Eye tile

     

    Here is an example of a URI to a hybrid Birds Eye tile that is provided by the ImageryMetadataResult.ImageUri property:

    http://{subdomain}.staging.tiles.virtualearth.net/tiles/cmd/ObliqueHybrid?a=03201011031-7384-19-{tileId}&g=213&token={token}

    There are three properties which have to be set; subdomain, tileId, and token.  The subdomain can be assigned any value that is in the ImageUriSubdomains array. The token value should be assigned as a valid client token. The tileId is the id of a specific tile in a Birds Eye scene.

    Birds Eye Imagery Extraction Via the Virtual Earth Web Services - Part 3

     

    Pixel to Latitude & Longitude

     

    The following calculation show how to convert a Pixel coordinate which is relative to the top left corner of the Birds Eye scene into a LatLong coordinate. The first step in this calculation is to calculate the zoom factor:

    image005

    Next a 1x3 matrix of the pixel coordinate is created.

     image013

    Once the pixel matrix is created a second matrix is needed to store the geo-referencing data QA-QI. This will require a 3x3 matrix.

    image014

    These matrices need to be multiplied together in the following fashion:

    image015

    The following formula’s can then be used to calculate the pixel coordinates:

    image016 

    image017

    By expanding out the calculations the pixel coordinates can be calculated as follows:

    image018 

    image019

    Application

     

    A sample application can be downloaded here: http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/VEImagery%5E_Birdseye.zip

    The following is an example of this application in use. The first image is the test coordinate. The second image is the output of the application.

    image020

    Figure 2 – Birdseye image of a coordinate in Virtual Earth

     

    image021

    Figure 3 - Birdseye tile of a coordinate generated using the VE Web Services




    11/2/2008

    VE Imagery Service, Polygons and Polylines

    Currently the VE Imagery service does not have the ability to render polygons or polylines on a map image. The high level steps involved to accomplish this are as follows:

    1) Calculate the best map view for an array of Location objects.

    2) Request map image from the imagery service that matches the best map view calculated in step 1.

    3) Calculate the pixel coordinate of each location.

    4) Use .NET drawing tools to draw polygons and polylines.

    Many of the steps in this article are explained in the article title VE Imagery Service and Custom Icons

    The following code block shows how to draw a polygon on a map image from an array of points that form a polygon.

    private void AddPolygon(BestView view, Image map, Location[] polygon)
    {
    //define the map as a gaphics object Graphics graphics = Graphics.FromImage(map); Point[] points = new Point[polygon.Length];

    //calculate pixel points for (int i = 0; i < polygon.Length; i++)
    {
    points[i] = LatLongToPixel(polygon[i], view);
    }

    // Create blue pen for outline of polygon. Pen bluePen = new Pen(Color.Blue, 1);

    //Create brush to fill polygon. Color transparentGreen = Color.FromArgb(50, 0, 255, 0); Brush greenBrush = new SolidBrush(transparentGreen);

    graphics.DrawPolygon(bluePen, points);
    graphics.FillPolygon(greenBrush, points);
    }

    The following code block shows how to draw a polyline on a map image from an array of points that form a polyline.

    private void AddPolyline(BestView view, Image map, Location[] polyline)
    {
    //define the map as a gaphics object Graphics graphics = Graphics.FromImage(map); Point[] points = new Point[polyline.Length];

    //calculate pixel points for (int i = 0; i < polyline.Length; i++)
    {
    points[i] = LatLongToPixel(polyline[i], view);
    }

    // Create pen. Pen bluePen = new Pen(Color.Blue, 1);

    graphics.DrawLines(bluePen, points);
    }

    By combining these algorithms with the code in the article; VE Imagery Service and Custom Icons you can add your polygons and polylines to a VE map image. This can be useful to create tile layers when wanting to display a large number of polygons in the JavaScript version of Virtual Earth without having performance issues.

    Complete source code can be downloaded here: http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/VEPolygonsImageryService.zip

    image




    10/25/2008

    VE Imagery Service and Custom Icons

    Currently the VE Imagery service has the ability to display 26 different icons on the map. Oddly enough the default pushpin icon from the JavaScript version of Virtual Earth is not one of these icons. Unfortunately there is currently no built in method to display custom icons on the map image. There is also currently a limitation of 10 pushpins being passed to the imagery service. This article shows how to add any number of custom icons to the map image in the correct location. The default pushpin icon from the JavaScript version of Virtual Earth will be used as the custom icon. The high level steps involved to accomplish this are as follows:

    1. Calculate the best map view for an array of Location objects.
    2. Request map image from the imagery service that matches the best map view calculated in step 1.
    3. Calculate the pixel coordinate of each location and display custom icon on map image.
    Calculate the Best Map View

    The best map view is the zoom level and center point of a map image that contains all locations with the closest zoom level. The best map view will be stored using a structure.

    /// <summary>
    /// Structure to define a BestView object which defines a map view (centerpoint and zoom level)
    /// </summary>
    struct BestView
    {
    public Location center;
    public int zoom;

    //Constructor public BestView(Location _center, int _zoom)
    {
    center = _center;
    zoom = _zoom;
    }
    }

    Listing 1Structure to define a BestView object

    The following method calculates the best map view for an array of locations. This results in a map image similar to what is displayed if the VEMap.SetMapView function is used in the JavaScript version of Virtual Earth.

    /// <summary>
    /// Calculates best map view for an array of points. Similar to VEMap.SetMapView method
    /// </summary>
    /// <param name="points">Array of Location objects</param>
    /// <returns></returns>
    private BestView CalculateMapView(Location[] points)
    {
    //Calculate bounding rectangle double maxLat = -90, minLat = 90, maxLon = -180, minLon = 180;

    //default zoom scales in km/pixel from http://msdn2.microsoft.com/en-us/library/aa940990.aspx double[] defaultScales = { 78.27152, 39.13576, 19.56788, 9.78394, 4.89197, 2.44598, 1.22299,
    0.61150, 0.30575, 0.15287, .07644, 0.03822, 0.01911, 0.00955, 0.00478, 0.00239, 0.00119, 0.0006, 0.0003 };

    //Calculate bounding box for array of locations for (int i = 0; i < points.Length; i++)
    {
    if (points[i].Latitude > maxLat)
    maxLat = points[i].Latitude;

    if (points[i].Latitude < minLat)
    minLat = points[i].Latitude;

    if (points[i].Longitude > maxLon)
    maxLon = points[i].Longitude;

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

    //calculate center coordinate of bounding box double centerLat = (maxLat + minLat) / 2;
    double centerLong = (maxLon + minLon) / 2;

    //create a Location object for the center point Location centerPoint = new Location();
    centerPoint.Latitude = centerLat;
    centerPoint.Longitude = centerLong;

    //want to calculate the distance in km along the center latitude between the two longitudes double meanDistanceX = HaversineDistance(centerLat, minLon, centerLat, maxLon);

    //want to calculate the distance in km along the center longitude between the two latitudes double meanDistanceY = HaversineDistance(maxLat, centerLong, minLat, centerLong) * 2;

    //calculates the x and y scales var meanScaleValueX = meanDistanceX / mapWidth; var meanScaleValueY = meanDistanceY / mapHeight; double meanScale;

    //gets the largest scale value to work with if (meanScaleValueX > meanScaleValueY)
    meanScale = meanScaleValueX;
    else meanScale = meanScaleValueY; //intialize zoom level variable int zoom = 1;

    //calculate zoom level for (var i = 1; i < 19; i++)
    {
    if (meanScale >= defaultScales[i])
    {
    zoom = i;
    break;
    }
    }

    //return a BestView object with the center point and zoom level to use return new BestView(centerPoint, zoom);
    }

    Listing 2 Method to calculate the best map view for an array of Locations

    The method that calculates the best map view uses a method called HaversineDistance. This method calculates the distance in kilometers between two coordinates using the Haversine formula. The code for this method is as follows:

    /// <summary>
    /// Calculate the distance in kilometers between two coordinates
    /// </summary>
    /// <param name="lat1"></param>
    /// <param name="lon1"></param>
    /// <param name="lat2"></param>
    /// <param name="lon2"></param>
    /// <returns></returns>
    private double HaversineDistance(double lat1, double lon1, double lat2, double lon2)
    {
    double earthRadius = 6371;
    double factor = Math.PI / 180;
    double dLat = (lat2 - lat1) * factor;
    double dLon = (lon2 - lon1) * factor;
    double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + Math.Cos(lat1 * factor) * Math.Cos(lat2 * factor)
    * Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
    double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));

    return earthRadius * c;
    }

    Listing 3 Haversine distance method

    Request Map Image from the Imagery Service

    By specifying a specific zoom level and center point for a map, this ensures that we have at least one point of reference between pixel coordinates and a latitude/longitude coordinate. The following method retrieves an image URL of a map for a specific map view. The image is then retrieves using a stream.

    /// <summary>
    /// Gets map from VE Imagery Web Service
    /// </summary>
    /// <param name="view">Map view</param>
    /// <returns></returns>
    private Image GetMapImage(BestView view)
    {
    //Map uri request object is created MapUriRequest mapUriRequest = new MapUriRequest();

    // Credentials are set using a valid Virtual Earth Token mapUriRequest.Credentials = new VEImageryService.Credentials();
    mapUriRequest.Credentials.Token = clientToken;

    //Pass centerpoint of map mapUriRequest.Center = view.center; // Map style and zoom level are set MapUriOptions mapUriOptions = new MapUriOptions();
    mapUriOptions.Style = MapStyle.Road;

    //Set zoom level of map mapUriOptions.ZoomLevel = view.zoom; // Size of the requested image in pixels is set mapUriOptions.ImageSize = new VEImageryService.SizeOfint();
    mapUriOptions.ImageSize.Height = mapHeight;
    mapUriOptions.ImageSize.Width = mapWidth;

    //Map options are added to the request mapUriRequest.Options = mapUriOptions; //ImageryServiceClient object created ImageryServiceClient imageryService = new ImageryServiceClient();

    //A request for a map uri is made MapUriResponse mapUriResponse = imageryService.GetMapUri(mapUriRequest); //Get image from URI System.Net.WebRequest request =
    System.Net.WebRequest.Create(mapUriResponse.Uri.Replace("{token}", clientToken));
    System.Net.WebResponse response = request.GetResponse(); System.IO.Stream responseStream = response.GetResponseStream(); //return image return Image.FromStream(responseStream);
    }

    Listing 4 Method to retrieve map image from the VE Web Services for best map view

    Calculate Pixel Coordinate for a Location

    Virtual Earth tiles are positioned using quad key tile positioning. In Virtual Earth a map tile is 256 pixels by 256 pixels. For zoom level one, there are 4 map tiles displayed, making the total map width and height to display the whole world 512 pixels by 512 pixels. If the zoom level is 2, there are 16 map tiles being displayed, making the total map width and height to display the whole world 1024 pixels by 1024 pixels. Additional information the Virtual Earth tiling system can be found here: http://msdn.microsoft.com/en-us/library/bb259689.aspx

    With the Virtual Earth tiling system in mind, all pixel coordinates for a set of latitude/longitude coordinates can be calculated. Using the center point of the map image the top left hand corner of the map image that is returned by the Virtual Earth imagery service can be calculated as a pixel location on a map that displays the whole world. Relative pixel coordinates can be then calculated for all location latitude/longitude coordinates.

    The following method takes in a Location object and a BestView object and returns a Point object which refers to the pixel location of a latitude/longitude coordinate on the map image.

    /// <summary>
    /// Converts a latlitude longitude coordinate to a pixel coordinate of a map based on the map view
    /// </summary>
    /// <param name="latlong"></param>
    /// <param name="view"></param>
    /// <returns></returns>
    private Point LatLongToPixel(Location latlong, BestView view)
    {
    //Formulas based on following article: //http://msdn.microsoft.com/en-us/library/bb259689.aspx //calcuate pixel coordinates of center point of map double sinLatitudeCenter = Math.Sin(view.center.Latitude * Math.PI / 180);
    double pixelXCenter = ((view.center.Longitude + 180) / 360) * 256 * Math.Pow(2, view.zoom);
    double pixelYCenter = (0.5 - Math.Log((1 + sinLatitudeCenter) / (1 - sinLatitudeCenter)) / (4 * Math.PI))
    * 256 * Math.Pow(2, view.zoom);

    //calculate pixel coordinate of location double sinLatitude = Math.Sin(latlong.Latitude * Math.PI / 180);
    double pixelX = ((latlong.Longitude + 180) / 360) * 256 * Math.Pow(2, view.zoom);
    double pixelY = (0.5 - Math.Log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI))
    * 256 * Math.Pow(2, view.zoom);

    //calculate top left corner pixel coordiates of map image double topLeftPixelX = pixelXCenter - (mapWidth / 2);
    double topLeftPixelY = pixelYCenter - (mapHeight / 2);

    //calculate relative pixel cooridnates of location double x = pixelX - topLeftPixelX;
    double y = pixelY - topLeftPixelY;

    return new Point((int)Math.Floor(x), (int)Math.Floor(y));
    }

    Listing 5 LatLong to Pixel coordinate method

    Putting it all together

    In order for these methods to work in a WinForm application the following references will have to be made:

    using System.Drawing.Design;
    using System.Drawing.Drawing2D;
    using System.IO;

    Listing 6 Required References

    A reference to the Virtual Earth Imagery Service and the Virtual Earth token service will also be required.

    The following global variables will also be needed:

    string clientToken;
    int mapWidth = 600;
    int mapHeight = 400;

    Listing 7 Global Variables

    The following method will calculate the best map view for an array of locations, retrieve a map image from the imagery service, add custom icons to the map image and then display the map image in a picture box.

    /// <summary>
    /// Generate map image with custom icons
    /// </summary>
    /// <param name="points">Array of Location objects</param>
    private void GenerateImage(Location[] points)
    {
    //Get Best Map View BestView mapView = CalculateMapView(points); //Generate default map for best view Image map = GetMapImage(mapView); //add custom icons to map image AddCustomIcon(mapView, map, points); //display map MapImageBox.Image = map; }

    Listing 8 Method to Generate map image with custom icons

    The following method can be used to add the custom icons to a map image:

    /// <summary>
    /// Adds custom icon image to map image
    /// </summary>
    /// <param name="view"></param>
    /// <param name="map"></param>
    /// <param name="points"></param>
    private void AddCustomIcon(BestView view, Image map, Location[] points)
    {
    //define the map as a gaphics object Graphics graphics = Graphics.FromImage(map); //Define custom icon Image customIcon = Image.FromFile("../../VEDefaultPin.gif");

    //custom icon offset with respect to the top left corner of icon Point imageOffset = new Point(12, 12);

    //Add custom icons to map image for (int i = 0; i < points.Length; i++)
    {
    Point point = LatLongToPixel(points[i], view);
    int x = point.X - imageOffset.X;
    int y = point.Y - imageOffset.Y;
    graphics.DrawImageUnscaled(customIcon, x, y);
    }
    }

    Listing 9 Method to add custom icons to a map image

    This method can add a custom icon image and specify an image offset similar to the VECustomIconSpecification.

    Here is an example of five locations plotted onto a map using Virtual Earth:

    clip_image002

    Figure 2 Points plotted using the JavaScript version of Virtual Earth

    Here is an example of the same points being plotted onto a map image that is generated from the Virtual Earth Imagery Service using the methods in this article.

    clip_image004

    Figure 3 Custom icons on a VE Imagery Service image

    Conclusion

    Combine these methods with a valid client token and you will be able to add your custom icons to a map image. Complete sample code can be found here:
    http://cid-e7dba9a4bfd458c5.skydrive.live.com/self.aspx/VE%20Sample%20code/CustomIconVEService.zip

    This article was written by Richard Brundritt. Richard works for Infusion Development.



    10/19/2008

    Determine the Points of Intersections of Two Overlapping Polygons

    Similar to the method described in the post "Determine if Two Polygons Overlap" (http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!475.entry), we will have to iterate through all the line segments that make up one polygon and see if they overlap with any of the line segments that make up the second polygon. The difference will be that we will create an array of all the points of intersections. The following function takes in two arrays of coordinates that make up two polygons. An array of VELatLong objects will be returned.

    //poly1 and poly2 are arrays of VELatlongs that represent polygons
    function PolygonIntersections(poly1, poly2)
    {
    var intersections = new Array();
    if(poly1.length >= 3 && poly2.length >= 3)
    {
    //close polygons poly1.push(poly1[0]); poly2.push(poly2[0]); for(var i = 0; i < poly1.length-1;i++)
    {
    for(var k = 0; k < poly2.length-1; k++)
    {
    var intersect = SimplePolylineIntersection(poly1[i],poly1[i+1],poly2[k],poly2[k+1]);

    if(intersect!=null)
    intersections.push(intersect);
    }
    }

    return intersections;
    }

    return null;
    }

    The code for the function "SimplePolylineIntersection" can be found here: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!474.entry

    Here is a screen shot of this function being used and the points being plotted on the map:

    image

    Determine if Two Polygons overlap

    In order to determine if two polygons overlap we need to iterate through the line segments that make up each polygon and determine if any of the line segments intersect. Once we find a single point where the lines intersect we can then stop the iterations. The following code can be used to determine if two polygons overlap:

    //poly1 and poly2 are arrays of VELatlongs that represent polygons
    function ArePolygonsOverlapped(poly1, poly2)
    {
    if(poly1.length >= 3 && poly2.length >= 3)
    {
    //close polygons poly1.push(poly1[0]); poly2.push(poly2[0]); for(var i = 0; i < poly1.length-1;i++)
    {
    for(var k = 0; k < poly2.length-1; k++)
    {
    if(SimplePolylineIntersection(poly1[i],poly1[i+1],poly2[k],poly2[k+1])!=null)
    return true;
    }
    }

    return false;
    }

    return null;
    }

    The code for the function "SimplePolylineIntersection" can be found here: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!474.entry

    Approximate Points of Intersection of Two Line Segments

    The theory for calculating the point of intersection of two line segments is similar to the theory described in the post "Approximate Point of Intersection of Two Planes" found here: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!473.entry

    The difference is that in order to be a point of intersection between two line segments, the calculated point must be within both of the bounding boxes in which the line segments are contained in. When can thus use the following two functions to determine the poin tof intersection of two polylines.

    function SimplePolylineIntersection(latlong1,latlong2,latlong3,latlong4)
    {
    //Line segment 1 (p1, p2) var A1 = latlong2.Latitude - latlong1.Latitude;
    var B1 = latlong1.Longitude - latlong2.Longitude;
    var C1 = A1*latlong1.Longitude + B1*latlong1.Latitude;

    //Line segment 2 (p3, p4) var A2 = latlong4.Latitude - latlong3.Latitude;
    var B2 = latlong3.Longitude - latlong4.Longitude;
    var C2 = A2*latlong3.Longitude + B2*latlong3.Latitude;

    var determinate = A1*B2 - A2*B1;

    var intersection;
    if(determinate != 0)
    {
    var x = (B2*C1 - B1*C2)/determinate;
    var y = (A1*C2 - A2*C1)/determinate;

    var intersect = new VELatLong(y,x);

    if(inBoundedBox(latlong1, latlong2, intersect) &&
    inBoundedBox(latlong3, latlong4, intersect))
    intersection = intersect;
    else intersection = null;
    }
    else //lines are parrallel intersection = null;

    return intersection;
    }

    //latlong1 and latlong2 represent two coordinates that make up the bounded box //latlong3 is a point that we are checking to see is inside the box function inBoundedBox(latlong1, latlong2, latlong3)
    {
    var betweenLats;
    var betweenLons;

    if(latlong1.Latitude < latlong2.Latitude)
    betweenLats = (latlong1.Latitude <= latlong3.Latitude &&
    latlong2.Latitude >= latlong3.Latitude);
    else betweenLats = (latlong1.Latitude >= latlong3.Latitude &&
    latlong2.Latitude <= latlong3.Latitude);

    if(latlong1.Longitude < latlong2.Longitude)
    betweenLons = (latlong1.Longitude <= latlong3.Longitude &&
    latlong2.Longitude >= latlong3.Longitude);
    else betweenLons = (latlong1.Longitude >= latlong3.Longitude &&
    latlong2.Longitude <= latlong3.Longitude);

    return (betweenLats && betweenLons);
    }
    A more complex method for calculating the point of intersection of two line segments in 3D space can be found here: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!439.entry

    Approximate Point of Intersection of two planes

    The point of intersection of two planes in 3D space can be calculated using spherical mathematics, as described here: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!438.entry Taking this route requires a significant number of calculation. If we wanted to calculate the points of intersection of a large number of planes this can cause performance issues. The following is a method that can be used to calculate a 2D approximation for the point of intersection.

    Theory

    A 2D plane, commonly known as a line, can be defined using a formula of the following format:   Ax + By = C
    Where x is in the same plane as the longitude coordinates and y is in the same plane as the latitude.
    The components A, B, and C can be calculated using the following formula's:

    A = y2-y1
    B = x1-x2
    C = A*x1+B*y1

    If we were to have two planes defined by the following equations:

    A1x + B1y = C1
    A2x + B2y = C2

    We can then calculate the determinate of these two equations using the following formula:

    determinate = A1*B2 - A2*B1

    If the determinate is 0, this would indicate that the planes are parallel to each other. If the determinate is 1 then this would indicate that the planes are perpendicular to each other.

    Now that we have the determinate of these equations we can now solve for the x and y components of the point of intersection.

    x = (B2*C1 - B1*C2)/determinate
    y = (A1*C2 - A2*C1)/determinate

    Application

    The following code takes in four coordinates, where two coordinates represents the endpoints of a line segment. This code will return either a VELatLong representing the point of intersection or will return null which would indicate that the planes are parallel.

    function SimplePlaneIntersection(latlong1,latlong2,latlong3,latlong4)
    {
    //Line segment 1 (p1, p2) var A1 = latlong2.Latitude - latlong1.Latitude;
    var B1 = latlong1.Longitude - latlong2.Longitude;
    var C1 = A1*latlong1.Longitude + B1*latlong1.Latitude;

    //Line segment 2 (p3, p4) var A2 = latlong4.Latitude - latlong3.Latitude;
    var B2 = latlong3.Longitude - latlong4.Longitude;
    var C2 = A2*latlong3.Longitude + B2*latlong3.Latitude;

    var determinate = A1*B2 - A2*B1;

    var intersection;
    if(determinate != 0)
    {
    var x = (B2*C1 - B1*C2)/determinate;
    var y = (A1*C2 - A2*C1)/determinate;
    intersection = new VELatLong(y,x);
    }
    else //lines are parrallel intersection = null;

    return intersection;
    }



    10/14/2008

    Draw polyline across international dateline

    By default Virtual Earth does not draw polylines across the international dateline (-180/180 degrees longitude) in 2D mode. The following code is an example of how to over come this issue. This code draws a polyline from Sydney Australia to Los Angeles California, which is a common airplane route. What this code does is determine the point of intersection of the path with the international date line. It then creates two polylines to represent this path. The algorithms use din this code are documented in the VE Math section of this blog.

    <!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 start = new VELatLong(-33.93979,151.17625);
    var end = new VELatLong(33.94051,-118.40738);
    var correctedPolyline1;
    var correctedPolyline2;
    var standardPolyline;

    function GetMap()
    {
    map = new VEMap('mymap');
    map.LoadMap(new VELatLong(0,0),1);

    var pin1 = new VEShape(VEShapeType.Pushpin,start);
    pin1.SetTitle("start");

    var pin2 = new VEShape(VEShapeType.Pushpin,end);
    pin2.SetTitle("end");

    standardPolyline = new VEShape(VEShapeType.Polyline,[start,end]);
    standardPolyline.HideIcon();

    var points = crossDateLine(start,end);

    correctedPolyline1 = new VEShape(VEShapeType.Polyline,[points[0],points[1]]);
    correctedPolyline2 = new VEShape(VEShapeType.Polyline,[points[2],points[3]]);

    correctedPolyline1.HideIcon();
    correctedPolyline2.HideIcon();

    map.AddShape([pin1,pin2,standardPolyline,correctedPolyline1,correctedPolyline2]);
    showLines();
    map.AttachEvent("onchangeview",mapModeChanged);
    }

    function mapModeChanged(e)
    {
    if(e!=null)
    {
    showLines();
    }
    }

    function showLines()
    {
    if(map.GetMapMode()==VEMapMode.Mode2D)
    {
    correctedPolyline1.Show();
    correctedPolyline2.Show();
    standardPolyline.Hide();
    }
    else { correctedPolyline1.Hide(); correctedPolyline2.Hide(); standardPolyline.Show(); } } function DegtoRad(x)
    {
    return x*Math.PI/180;
    }

    function RadtoDeg(x)
    {
    return x*180/Math.PI;
    }

    function Cartesian(x,y,z)
    {
    this.X = x;
    this.Y = y;
    this.Z = z;
    }

    function convertSphericalToCartesian(latlong)
    {
    var lat = DegtoRad (latlong.Latitude);
    var lon = DegtoRad (latlong.Longitude);
    var x = earthRadius * Math.cos(lat)*Math.cos(lon);
    var y = earthRadius * Math.cos(lat)*Math.sin(lon);
    var z = earthRadius * Math.sin(lat);

    return new Cartesian(x,y,z);
    }

    function convertCartesianToSpherical(cartesian)
    {
    var r = Math.sqrt(cartesian.X*cartesian.X + cartesian.Y*cartesian.Y+ cartesian.Z*cartesian.Z);
    var lat = RadtoDeg(Math.asin(cartesian.Z/r));
    var lon = lon = RadtoDeg(Math.atan2(cartesian.Y, cartesian.X));

    return new VELatLong(lat,lon);
    }

    function crossProduct(p1,p2)
    {
    var x = p1.Y*p2.Z - p1.Z*p2.Y;
    var y = p1.Z*p2.X - p1.X*p2.Z;
    var z = p1.X*p2.Y - p1.Y*p2.X;

    return new Cartesian(x,y,z);
    }

    function vectorLength(p1)
    {
    return Math.sqrt(p1.X*p1.X+p1.Y*p1.Y+p1.Z*p1.Z);
    }

    function unitVector(p1)
    {
    var length = vectorLength(p1);

    return new Cartesian(p1.X/length,p1.Y/length,p1.Z/length);
    }

    function intersectionOfPlanes(latlong1,latlong2,latlong3,latlong4)
    {
    var point1 = convertSphericalToCartesian(latlong1);
    var point2 = convertSphericalToCartesian(latlong2);
    var point3 = convertSphericalToCartesian(latlong3);
    var point4 = convertSphericalToCartesian(latlong4);

    var vector1 = crossProduct(point1,point2);
    var vector2 = crossProduct(point3,point4);

    var unitVector1 = unitVector(vector1);
    var unitVector2 = unitVector(vector2);

    if((unitVector1.X != unitVector2.X )&& (unitVector1.Y != unitVector2.Y) && (unitVector1.Z != unitVector2.Z))
    {
    var vectorDirector = crossProduct(unitVector1,unitVector2);

    var intersection1 = unitVector(vectorDirector);
    var intersection2 = inverseCartesian(intersection1);

    return new Array(convertCartesianToSpherical(intersection1),convertCartesianToSpherical(intersection2));
    }

    return -1;
    }

    function inverseCartesian(p1)
    {
    return new Cartesian(-1*p1.X,-1*p1.Y,-1*p1.Z);
    }

    function crossDateLine(startLatLong,endLatLong)
    {
    var points = intersectionOfPlanes(startLatLong,endLatLong,new VELatLong(90,-180),new VELatLong(-90,-180));

    var intersection1;

    if(points[0].Longitude==-180)
    intersection1 = points[0];
    else intersection1 = points[1]; return [startLatLong,new VELatLong(intersection1.Latitude,180),intersection1,endLatLong];

    }
    </script> </head> <body onload="GetMap();"> <div id='mymap' style="position:relative; height:600px; width:800px;"></div> </body> </html>


    Calculate midpoint of Polyline

    Sometimes it might be important to calculate the midpoint along a polyline. this can be done by first calculating the total length of a polyline and then to iterate through the points of the polyline, calculating the distance between points until  we find the points where the midpoint falls between. From here we can calculate the coordinate of the midpoint. The following functions can calculate the midpoint coordinate of a polyline.

    function PolylineCentroid(points)
    {
    var totalDistance = PolylineLength(points);

    var midpoint = FindMidPoint(points,totalDistance);
    return midpoint;
    }

    function PolylineLength(points)
    {
    var distance = 0;

    for(var i=0;i<points.length-1;i++)
    {
    distance += haversineDistance(points[i],points[i+1]);
    }

    return distance;
    }

    function FindMidPoint(points,totalDistance)
    {
    var midDistance = totalDistance/2;

    var distance = 0;
    var subDistance=0;
    var i;

    for(i=0;i<points.length-1;i++)
    {
    subDistance = haversineDistance(points[i],points[i+1]);

    if((subDistance+distance)<midDistance)
    distance += subDistance;
    else break;
    }

    subDistance = midDistance - distance;
    var bearing = calculateBearing(points[i],points[i+1]);
    return calculateCoord(points[i], bearing, subDistance);
    }

    Information on the haversineDistance method can be found here: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!317.entry

    Information on the calculateBearing method can be found here: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!393.entry

    Information on the calculateCoord method can be found here: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!400.entry

    Gravitational Acceleration at a given latitude and altitude

    Theory

    The gravitational acceleration that an object experiences decreases the further it gets from the center of the Earth. The gravitational acceleration at sea level at any point on the Earth’s surface can be calculated using the International Gravity formula:

    clip_image002

    When at sea level the gravitational acceleration only changes with respect to the latitude and not the longitude. The Earth is not a smooth surface and as such we need to be able to calculate this acceleration at altitudes with respect to sea level. We can modify the previous formula so that an altitude (h) in meters can be used to calculate the correct gravitational acceleration.

    clip_image004

    Application

    This function will take in a latitude and an altitude in meters. It will return a number representing the gravitational accelerations whose units are in clip_image006

    function gravitationalAcceleration(latitude, altitude)
    {
    var lat = DegToRad(latitude);

    return 9.780327*(1+0.0053024*Math.pow(Math.sin(lat),2)-0.0000058*Math.pow(Math.sin(lat),2))
    -0.000003086*altitude;
    }

    Listing 1 Gravitational Acceleration function
    The following post has additional information on the DegToRad method: http://rbrundritt.spaces.live.com/blog/cns!E7DBA9A4BFD458C5!257.entry