Ricky's profileRicky's Bing Maps BlogBlogSkyDrive Tools Help

Blog


    5/31/2009

    Drawing arrow heads on Polylines

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

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

    5/18/2009

    Drawing Dashed Routes behind Buildings in Birds eyes

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

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

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

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

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

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

    Here is a screen shot of this code in action:

    image



    Dashed Polylines in Virtual Earth

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

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

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

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

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

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

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

    image


    Multiple Routes in Virtual Earth

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

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

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

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

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

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

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

     

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

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

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

        return newShape;
    }

     

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

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

    Here is a screen shot of this code in action:

    image



    5/3/2009

    Integrating a GPS receiver with Virtual Earth

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

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

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

    This application requires following steps:

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

    Connecting to a GPS Receiver

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

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

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

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

     

    s1

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

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

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

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

     

    Reading and Processing GPS Data

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

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

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

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

     

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

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

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

    Note that field 14 and 15 are not comma separated.

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

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

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

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

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

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

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

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

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

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

    clip_image002

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

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

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

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

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

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

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

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

     

    Displaying data on a Virtual Earth map

    s2

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

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html>
        <head>
            <title></title>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
            <script type="text/javascript" src="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.2"></script>
            <script type="text/javascript">
                var map;
                var pinLayer;
                var pathLayer;
                var points = new Array();
                var pin = null;

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

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

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

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

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

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

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

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

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

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

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

     

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

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

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

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

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

     

    s3

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

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