Implementation of Touring Feature for Virtual Earth

by Gong Liu February 21, 2009 20:52

If you have ever used Google Earth, you will probably be familiar with the touring feature, which plays back a series of points, i.e. putting a point on the map center, opening its balloon which may contain photos and/or text, staying for a fixed time period, and then moving on to the next point. It is almost like a slide show, but at the same time shows the location of current point on the map. Virtual Earth does not have the touring feature built-in. However, we can implement the feature ourselves using Virtual Earth SDK. Please first visit this link to see a demo of the touring feature in Virtual Earth, and then read the rest of the post to see how it is implemented.

In the demo, there are 5 pushpins and their data is defined by the following 2D array:

    var data = new Array(
        new Array(34.042182, -118.232889, "homer0.jpg", 289, 300, "Homer screaming"), 
        new Array(34.045917, -118.239462, "homer1.gif", 290, 267, "D'OH!"),   
        new Array(34.049360, -118.245736, "homer2.jpg", 210, 210, "Homer having a donut"), 
        new Array(34.051448, -118.252511, "homer3.jpg", 300, 400, "Homer having a beer"), 
        new Array(34.057442, -118.259504, "homer4.jpg", 219, 167, "Homer choking Bart") );

where each pushpin has these fields: latitude, longitude, image name, image width, image height and caption. Of course, in a real application the data will probably come from a back end database. The data is loaded to the map with this script:

    function LoadData()
    {  
        var layer = new VEShapeLayer();
        map.AddShapeLayer(layer);
        //create array of VEShape and bulk add to the layer
        var shapes = new Array();
        for (var i = 0; i < data.length; i++)
        {
            var shape = new VEShape(VEShapeType.Pushpin, new VELatLong(data[i][0], data[i][1]));
            shape.SetTitle(data[i][2]);
            var desc = "<div style=\"width:200px\"><img src=\"photos/" + data[i][2] + "\" width=\"200\" height=\"" + Math.round(200 * data[i][4]/data[i][3],0) + "\" border=\"0\"></div><div style=\"width:200px\">" + data[i][5] + "</div>";
            shape.SetDescription(desc);
            shapes[i] = shape;
        }
        layer.AddShape(shapes);  
        AdjustView(layer);
    }

This script first creates a layer, then creates an array of shapes (pushpins) based on the data, and then adds the pushpins to the layer in bulk. Notice that an image width is fixed to 200 pixels and the infobox width is fixed to 233 pixels accordingly. This is to prevent a too big infobox from showing. In other words, images may be dynamically resized to fit the infobox.

Once the data is loaded, the following script is used to adjust the map view so that all the pushpins in the layer are visible on the map:

    function AdjustView(layer)
    {
        var rect = layer.GetBoundingRectangle();
        var count = layer.GetShapeCount();
        map.SetMapView(rect);
        if (count <= 1)
        {
            map.SetZoomLevel(7);
        }
    } 

The following script and variables are directly related to playing a tour:

    var currShapeIdx = -1;
    var theShape = null;
    var pause = true;
    var customBehavior = false;

    function Start()
    {
        currShapeIdx = 0;
        pause = false;
        Play();
    }

    function Stop()
    {
        currShapeIdx = -1;
        pause = true;
    }

    function Pause()
    {
        pause = true;
    }
            
    function Restart()
    {
        pause = false;
        Play();
    }
            
    function Play()
    {
        var layer = map.GetShapeLayerByIndex(1);  
        var n = layer.GetShapeCount();
        var img = document.getElementById("currImg");
        if (currShapeIdx >= 0 && currShapeIdx < n && !pause)
        {
            var sideWidth = (typeof( window.innerWidth ) == 'number' )? Math.round(window.innerWidth * 0.2) : Math.round(document.documentElement.clientWidth * 0.2);  
            sideWidth -= 10;     
            img.src = "photos/" + data[currShapeIdx][2];
            img.width = (data[currShapeIdx][3] > sideWidth)? sideWidth : data[currShapeIdx][3];
            img.style.visibility = "visible";  
            theShape = layer.GetShapeByIndex(currShapeIdx);
            var pts = theShape.GetPoints();
            customBehavior = true;
            map.PanToLatLong(pts[0]);
            //map.SetCenter(pts[0]);
            currShapeIdx++;
            var chkLoop = document.getElementById("loop");
            if (currShapeIdx == n && chkLoop.checked)
                currShapeIdx = 0;
            var selInterval = document.getElementById("interval");
            var i = selInterval.options[selInterval.selectedIndex].value;
            window.setTimeout("Play()", 1000 * i);  
        }
        else
        {
            img.style.visibility = "hidden";
            theShape = null;
        }
    }
            
    function OnMapMoved()
    {
        if (customBehavior)
            window.setTimeout("map.ShowInfoBox(theShape)", 10);
        customBehavior = false;
    }

Here are some key points about this script. First, the Play() function is called recursively via the javascript function window.setTimeout(). The calling interval is specified by the user. Second, each time the Play() function is called, the current pushpin is retrieved and the map is moved via the VE method PanToLatLong() so that the current pushpin is at the map center. The current pushpin is tracked by the variables currShapeIdx and theShape. Third, when the map is moved in position, the OnEndPan event is fired and the event handler OnMapMoved() is executed, which opens the infobox of the current pushpin. It is important to call the ShowInfoBox() method via the javascript function window.setTimeout(), and only call it once right after the Play() function is called. Fourth, the pause/restart status is tracked by the boolean variable pause. Once the touring is started, pressing the pause/restart button toggles the call between Pause() and Restart() functions. Finally, the width of the image on the sidebar is determined by the width of the sidebar or the original image width, whichever is smaller. The width of the sidebar is 20% of the width of the browser window.

Again, to see the touring feature in action and the complete javascript code, click here.

Note if you set the interval too small while the points are too far apart, you may find that some points are skipped during touring because it takes too long time to pan from one point to the next. In this case you will have to either increase the playback interval or zoom out the map a few notches.

Using the Icon Editor in Visual Studio 2005

by Gong Liu February 13, 2009 20:04

The Icon Editor in Visual Studio 2003/2005 is somewhat under utilized and under-documented. Admittedly, the Icon Editor is awkward to use and has quite a few limitations, such as you cannot create an icon with more than 256 colors or a compressed icon as those used in Vista. However, if you are a developer who occasionally needs to create some icons, the Icon Editor in Visual Studio is a viable choice. Especially if you combine your favorite image editing tool, such as Photoshop, with the Icon Editor, you can get the job done fairly easily. And you don't have to buy any third-party icon tools or plug-ins.

In this post I'll show you how to create an icon that contains multiple image types (size and color depth) using the Icon Editor and Photoshop. Before we dive in, let's first take a look at the Windows standard icon sizes.

Size 16x16 32x32 48x48 96x96 256x256
XP List view/Details view/Windows title bar Icons view Tiles view Thumbnails view Does not support
Vista List view/Details view/Windows title bar Classic view Medium icons Large icons Extra large icons

When you create an icon for your Windows application (especially the one at the top-left corner of a Windows Form), you want to pack all these standard sized icon images into one .ico file. This way, depending on the view, the icon image with the best-fit size will be used for display. If you try to use one size to fit all, sometimes you will get a fuzzy or jagged image.

Now here are the steps to create an icon that contains multiple image types:

  1. Prepare some standard-sized images with Photoshop and save them as transparent .gif files. Don't use .png or other formats, as the Icon Editor can't handle an image with more than 256 colors. In our example I have these images.
    16x16 32x32 48x48 96x96 256x256

  2. Save the color palette for each image. In Photoshop, you click Image -> Mode -> Color Table to bring out the Color Table dialog box. Click Save button to bring out the Save dialog box. Specify a file name and the Microsoft Palette .PAL format, and then click Save button. Leave Photoshop open. We will need it in later steps.


  3. Launch Visual Studio 2005 and create a new Windows Application project with a project name, say IconTest.
  4. Right click IconTest project in Solution Explorer, and then click Add -> New Item.
  5. In the Add New Item dialog box, select Icon File, gave a name to the icon to be created, say GPS.ico, and click Add button.


  6. By default Icon Editor gives you two image types 16x16 16 colors and 32x32 16 colors. We need to delete the two default types and add 5 new types 16x16 256 colors, 32x32 256 colors, 48x48 256 colors, 96x96 256 colors and 256x256 256 colors. To add a new image type, click Image -> New Image Type, and from the New Icon Image Type dialog box select the type you want. To delete an image type, click Image -> Current Icon Image Type, select the type you want to delete, and then click Image -> Delete Image Type. After adding and deleting image types, we should have the 5 new image types under Current Icon Image Types submenu:


  7. Make one of the 5 image types the current type and load the corresponding palette saved in Step 2. To do this, click Image -> Load Palette, in the Load Palette dialog box, browse to the palette file and press Open button.
  8. Go to Photoshop, select the corresponding image, and copy it to clipboard.
  9. Go back to Icon Editor, right click any empty space, and select Paste.


  10. Repeat Steps 7 ~ 9 for all other image types.
  11. Click Save button on the tool bar. The icon GPS.ico is now successfully created.
  12. Now you can add the icon to a WinForm. Double click the WinForm, in the Properties panel select Icon and click . In the Open dialog box, browse to GPS.ico, and press Open button.


You can download the icon GPS.ico here (right click the link and select Save Target As).

Using Formatting Functions with GridView/Template Field

by Gong Liu February 05, 2009 15:32

This article is also published on The Code Project

A TemplateField of a GridView control offers certain level of flexibility to combine data fields and format them for display. For example, we can use a TemplateField to display FirstName and LastName in a single GridView column, and even add a link to the combined column. However, if the formatting is too complex, using a formatting function with a TemplateField is the way to go.

Let's explore the ways to use formatting functions in the following example, where our data source is a table of POIs (points of interest) resulted from a spatial search, and the table includes these data fields:

  • POIName
  • Address
  • City
  • County
  • State
  • Zip
  • Country
  • Phone
  • CatCode (Category Code)
  • Latitude
  • Longitude
  • Distance (Distance from search center)

What we want to display to a user is (see screenshot at the end of this post):

  • The POIName field with a link to show the POI on Google Maps based on the POI's Latitude and Longitude fields
  • A single Address field which combines Address, City, County, State, Zip, and Country fields
  • The Phone field
  • The CatCode field
  • The Distance field

The first two fields are of particular interest. To display the POIName field with a link to Google Maps, we can use the following TemplateField:

<asp:GridView ID="gvwResults" runat="server" AutoGenerateColumns="False"
  EmptyDataText="No result found." CellPadding="2" EnableViewState="False">
  <Columns>
    <asp:TemplateField HeaderText="POI Name">
      <itemtemplate>
        <asp:LinkButton ID="LinkButton1" runat="server" title="map it"
          PostBackUrl="<%# GoGoogleMaps(Eval(&quot;Latitude&quot;),
          Eval(&quot;Longitude&quot;)) %>">
          <%# Eval("POIName") %>
        </asp:LinkButton>
      </itemtemplate>
    </asp:TemplateField>
    ......
  </Columns>
</asp:GridView>

The TemplateField contains a LinkButton server control. The LinkButton control's text is the POIName field, and its PostBackUrl attribute is bound to the formatting function GoGoogleMaps with Latitude and Longitude fields as input parameters. Notice that we have to use the xml entity &quot; to surround a data field name because we can't have double quotes within double quotes. The formatting function GoGoogleMaps looks like this in C#:

protected string GoGoogleMaps(object lat, object lon)
{
    if (Convert.IsDBNull(lat) || Convert.IsDBNull(lon))
        return null;
    else
        return string.Format(
            "http://maps.google.com/maps?f=q&hl=en&geocode=&q={0},{1}&z=15", lat, lon);
}

The Eval method of DataBinder class always evaluates a data field to an object and that's why we declare so for all parameters of the formatting function. The latitude and longitude fields of a POI could be null, and if so, we simply don't link it to Google Maps. Formatting rules like this would be hard to implement without a formatting function.

Instead of using a LinkButton control, we can also use a Literal control and construct the link ourselves within the formatting function. This way we have more control over the link's target and other aspects. 

<asp:TemplateField HeaderText="POI Name">
  <itemtemplate>
    <asp:Literal ID="Literal1" runat="server" Text=
      "<%# GetGoogleMapsLink(Eval(&quot;POIName&quot;),
      Eval(&quot;Latitude&quot;),
      Eval(&quot;Longitude&quot;)) %>">
    </asp:Literal>
  </itemtemplate>
</asp:TemplateField>

The corresponding formatting function GetGoogleMapsLink looks like:

protected string GetGoogleMapsLink(object poiName, object lat, object lon)
{
    string poi = (Convert.IsDBNull(poiName)) ? "Unknown POI" : (string)poiName;
    if (Convert.IsDBNull(lat) || Convert.IsDBNull(lon))
        return poi;
    else
        return string.Format(
            "<a href=\"#\" title=\"map it\" onclick=\"window.open(
            'http://maps.google.com/maps?f=q&hl=en&geocode=&q={0},{1}&z=15', 'GM')\">
            {2}</a>", lat, lon, poi);
}

The formatting function creates and returns a link that opens Google Maps in a separate browser window. It also handles the possibility of a null POIName field. If we compare the server generated pages from the above two ways (in a browser, right click the page and select View Source), we will see that the result of the second way is much more readable and less clustered.

Now let's see how we do about the combined Address field. The traditional way is to have a TemplateField with several Label controls, one for each address element, such as:

<asp:TemplateField HeaderText="Address">
  <itemtemplate>
    <asp:Label ID="Label1" runat="server" Text='<%# Bind("Address") %>'></asp:Label>,
    <asp:Label ID="Label2" runat="server" Text='<%# Bind("City") %>'></asp:Label>,
    <asp:Label ID="Label3" runat="server" Text='<%# Bind("County") %>'></asp:Label>,
    <asp:Label ID="Label4" runat="server" Text='<%# Bind("State") %>'></asp:Label>,
    <asp:Label ID="Label5" runat="server" Text='<%# Bind("Zip") %>'></asp:Label>,
    <asp:Label ID="Label6" runat="server" Text='<%# Bind("Country") %>'></asp:Label>
  </itemtemplate>
</asp:TemplateField>

Notice that we use commas to separate address elements from each other. This is all fine until we want a little bit formatting logic - we want to trim off any excess commas at both ends of the combined address string due to possible missing address elements. However, if an address element in the middle is missing, we'll still keep the extra comma. To do this we use a single Label control and bind its Text attribute to a formatting function that takes all the address fields as input parameters:

<asp:TemplateField HeaderText="Address">
  <itemtemplate>
    <asp:Label ID="Label1" runat="server" Text="<%# FormatAddress(
      Eval(&quot;Address&quot;),
      Eval(&quot;City&quot;),
      Eval(&quot;County&quot;),
      Eval(&quot;State&quot;),
      Eval(&quot;Zip&quot;),
      Eval(&quot;Country&quot;)) %>">
    </asp:Label>
  </itemtemplate>
</asp:TemplateField> 

where, the formatting function FormatAddress is:

protected string FormatAddress(object address, object city, object county,
    object state, object zip, object country)
{
    string addr = string.Format("{0},{1},{2},{3},{4},{5}", address,
        city, county, state, zip, country);
    return addr.Trim(",".ToCharArray());

An improvement over the above use of a formatting function with a TemplateField is that we can pass the entire data row to the formatting function in stead of individual data fields, especially when more than one or two data fields are involved. For instance, instead of passing 6 data fields in the above case, we can pass the current data row to FormatAddress:

<asp:TemplateField HeaderText="Address">
  <itemtemplate>
    <asp:Label ID="Label1" runat="server"
      Text="<%# FormatAddress(((System.Data.DataRowView)Container.DataItem).Row) %>">
    </asp:Label>
  </itemtemplate>
</asp:TemplateField>

Here, Container.DataItem returns a DataRowView object corresponding to the DataSource record bound to the GridViewRow. Its Row property returns a DataRow object. The new formatting function FormatAddress now looks like:

protected string FormatAddress(DataRow dr)
{
    string addr = string.Format("{0},{1},{2},{3},{4},{5}",
        dr["Address"], dr["City"], dr["County"], dr["State"], dr["Zip"], dr["Country"]);
    return addr.Trim(",".ToCharArray());

This is obviously a much cleaner way than passing multiple data fields. The following screenshot shows the GridView with search result of some airports and airport terminals near Los Angeles.

In conclusion, a formatting function is a great way to extent the power of TemplateField. It allows developers to implement complex formatting rules that usually involve multiple data fields and their interrelationships.

About

A seasoned computer professional. A tofu culture evangelist...
more >>

Tag Cloud

Calendar

<<  April 2017  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar
Copyright © 2008-2011 Gong Liu. All rights reserved. | credits | contact me
The content on this site represents my own personal opinions, and does not reflect those of my employer in any way.