Ricky's profileRicky's Bing Maps BlogBlogListsSkyDrive Tools Help

Ricky's Bing Maps Blog

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

Ricky Brundritt

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

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


June 15

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>



May 31

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>

May 18

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 polygons 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


May 03

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.

April 16

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>

April 10

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

April 05

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

 
A list of some of the more useful Virtual Earth related sites that I commonly use.