Virtual Earth as Gadget for Windows Vista Sidebar

Introduction

If you are using Windows Vista you probably had a look at the Sidebar Gadgets as well.

image

There is a already pretty big gallery available on the web but since the Gadgets are basically small HTML-documents automated through JavaScript everybody can build his own Gadgets as well. I like the idea of having some condensed information in one place and open a Flyout which get’s you full functionality in a bigger window. Of course I was aware that there are already Gadgets which implement some of the Virtual Earth functions so I wanted to demonstrate how easy it can be to implement your own existing Virtual Earth applications in the Sidebar. As a starting point we use the tracking application which I described in a previous posting. The idea is to have a small Gadget as shown above in the Sidebar and then to create a Flyout in which I have advanced functionality like replaying previous days.

Getting Started

If you are new to the Vista Gadgets this might be a good starting point but don’t worry it is pretty easy and the following steps will be explained in detail. As mentioned before, we will build upon the code for that simple tracking application which I have described here. In the meantime I had modified that sample so that we have a replay-function as well. You will find the source code here:

The Gadget

Particularly for development and testing it is a good idea to create a folder directly in your personal Gadget-folder (e.g. C:UsersjkebeckAppDataLocalMicrosoftWindows SidebarGadgets) rather than in the standard repository for your Visual Studio project. The folder shall follow the naming convention "SomeName.gadget". In my case it is "MyTracking.gadget".

The Manifest (gadget.xml)

The first piece of the Gadget is the XML-Manifest which describes where everything is. The name of this file must always be gadget.xml. A detailed description of the manifest can be found here and my manifest looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<gadget>
  <name>Where is Hannes?</name>
  <version>1.0.0.0</version>
  <author name="Johannes Kebeck">
    <info url="http://johanneskebeck.spaces.live.com" text="My Blog" />
    <logo src="logo.jpg" />
  </author>
  <copyright>© feel free to re-use</copyright>
  <description>The Vista-Sidebar-Gadget-Version of my Tracking Sample</description>
  <icons>
    <icon width="64" height="64" src="icon.jpg" />
  </icons>
  <hosts>
    <host name="sidebar">
      <base type="HTML" apiVersion="1.0.0" src="gadget.html" />
      <permissions>Full</permissions>
      <platform minPlatformVersion="1.0" />
    </host>
  </hosts>
</gadget>

As you can see the source of the gadget has to be a HTML-file and I called it gadget.html but you can choose any other name as well.

The HTML-document for the Gadget (gadget.html)

As you can see the source of the gadget is a very basic HTML-document. It contains references to a style-sheet, the Virtual Earth MapControl and our own JavaScript-file which we have still to create. In the body we have a DIV-element for the title as well as another DIV-element which will host the Virtual Earth MapControl. The DIV-element for the title also contains a link to a JavaScript-function which will be discussed later.

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=Unicode" />
    <title>Hannes Tracking Gadget</title>
    <link href="gadget.css" rel="stylesheet" type="text/css" />
    <script src="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6" type="text/javascript"></script>
    <script src="gadget.js" type="text/javascript"></script>
</head>
<body>
    <div style="font-weight:bold; text-align:center; width:130px"><a href='#' onclick='javascript:showFlyout();'>Where is Hannes?</a></div><br />
    <div id='divMap' style="position:absolute; top:20px; left:0px; width:130px; height:130px;"></div>
</body>
</html>

The CSS for the Gadget (gadget.css)

Docked gadgets must be at least 60 pixels high and anywhere from 25 pixels to 130 pixels wide to fit within the maximum width of the Sidebar. The size of the gadget is defined by the height and width of the HTML-body. As you can see in the CSS below I override the styles for scalebar, copyrights and logo. This is something that you should normally never do and in fact it violates the Terms of Use but since the map in my sidebar is so small that the attribution would obstruct the map completely and since my flyout will have the complete attribution anyway, I hope that it is OK. My Style-sheet looks like this.

html
{
    overflow:hidden;
}

body 
{
    font-family:Tahoma;
    font-size:12px;
    background-color:#464646;
    color:#FFFFFF;
    position:absolute;
    left:0px;
    top:0px;
    margin-left:0px;
    margin-top:0px;
    width:130px;
    height:150px;
}

h1
{
    font-family:Tahoma;
    text-align:center;
}

/* Set Link-Colors */
a:link {color:#FFFFFF}
a:visited {color:#FFFFFF}
a:hover {color:#FFFF00}
a:active {color:#FFFFFF}

/* Removing the Microsoft logo, scalebar, and copyright logos off of the map */ 
#divMap .MSVE_PoweredByLogo 
{ 
    display:none; 
} 
#divMap .MSVE_ScaleBar 
{ 
    display:none; 
} 
#divMap .MSVE_ScaleBarLabel 
{ 
    display:none; 
} 
#divMap .MSVE_Copyright 
{ 
    display:none; 
}

The JavaScript for the Gadget (gadget.js)

The first difference you see is the reference to the flyout-document. The rest is pretty much the same as the original website for my tracking application which was explained in my previous posting with a few exceptions:

  • The default size of the dashboard would be too big for the gadget so I choose a smaller size:
      map.SetDashboardSize(VEDashboardSize.Tiny);
    Alternatively you could switch of the interactivity of the map completely which would remove the dashboard and disable all mouse- and keyboard-events:
      map.LoadMap(new VELatLong(51.461962075378054, -0.9260702133178665), 13, VEMapStyle.Shaded, true);
  • I added a function showFlyout(). This function is being executed when you click on the link in the title of the gadget and will basically open the flyout windows.
window.onload = GetMap;

//TheGadget Flyout-file
System.Gadget.Flyout.file = "flyout.html";

//Map
var map = null;

//VEShapeLayer
var slTracks = new VEShapeLayer();

function GetMap()
{
    map = new VEMap('divMap');
    map.SetDashboardSize(VEDashboardSize.Tiny);
    map.LoadMap(new VELatLong(51.461962075378054, -0.9260702133178665), 13, VEMapStyle.Shaded);
    map.AddShapeLayer(slTracks);
    //Start Tracking and Set Intervall to 5 minutes
    showtime=setInterval("Track()", 300000);
    Track();
}

//Tracking
function Track()
{
    //Delete previous tracks
    slTracks.DeleteAllShapes();
    
    //Build the URL
    //The random URL-parameter avoids caching
    var url="http://YourServer/Tracking.ashx?" + Math.random();

    //Get the appropriate XMLHTTP object for the browser
    var xmlhttp = GetXmlHttp();
    
    //if we have a valid XMLHTTP object
    if (xmlhttp)
    {
        xmlhttp.Open("GET", url, true); //varAsynx = true
        
        //set the callback
        xmlhttp.onreadystatechange = function()
        {
            if (xmlhttp.readystate ==4) //4 is a success
            {
                //server code creates JavaScript "on the fly" for us to
                //execute using eval()
                var result = xmlhttp.responseText
                eval(result);
            }
        }
        xmlhttp.send(null);
    }
}

function GetXmlHttp()
{
    var x = null;
    try
    {
        x = new ActiveXObject("Msxml2.XMLHTTP");
    }
    catch (e)
    {
        try
        {
            x = new ActiveXObject("Microsoft.XMLHTTP");
        }
        catch (e)
        {
            x = null;
        }
    }
    if (!x && typeof XMLHttpRequest != "undefined")
    {
        x = new XMLHttpRequest();
    }
    return x;
}

function showFlyout()
{
    System.Gadget.Flyout.show = true;
}

All right that’s already the first part. Click in the "+" symbol in the sidebar to bring up the "Gadget Gallery".

image

Your new gadget should be visible. Double-click to add it to the sidebar or just drag-and-drop it onto the sidebar.

image

The Flyout

The flyout is a bigger window which will appear when you set the property "System.Gadget.Flyout.show = true;" It will be attached to the docked gadget in the sidebar and is often used to configure the content of the docked gadget or – as in my example – to provide additional information and functions. The approach is very similar to the docked gadget mentioned above and I will use the main web site for the tracking application which I described previously a a template.

The HTML-document for Flyout (flyout.html)

Well the original posting was just providing the tracking functionality and not the function to replay a day so we will have to make some modifications to provide a replay of previous days. In the header we reference the Virtual Earth MapControl as well as our own CSS and JavaScript.

Please note: even though we did define the style for background-colour in the external CSS this would not be applied to the flyout. Apparently this is a bug in the sidebar. The workaround is to define the style in the HTML-document directly.

The body contains a first DIV-element which is basically a header. The second DIV-element contains some checkboxes to start and stop live tracking as well as a replay of a previous day. The days which are available for replay – i.e. the days when I had my GPS switched on – will be displayed in an HTML-SELECT-element. And finally there is a DIV-element which will host our Virtual Earth map.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Tracking Gadget</title>
    <link href="flyout.css" rel="stylesheet" type="text/css" />
    <script src="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6" type="text/javascript"></script>
    <script src="flyout.js" type="text/javascript"></script>
</head>
<body style="background-color:#5b86ce">
    <div style="position:absolute; top:0px; left:0px; width:100%; height:60px;" class="header">
        <table style="width:100%" >
            <tr style="vertical-align:top">
                <td style="width:140px; min-width:140px"><img src="VirtualEarth.gif" alt="Virtual Earth Logo" style="margin-left:5px;" /></td>
                <td><h1 style="min-width:400px">Tracking Gadget</h1></td>
                <td style="width:140px; min-width:140px; text-align:right"><a>Version 6<br /></a></td>
            </tr>
        </table>
    </div>
    <div id='divCtrl' style="position:absolute; top:65px; left:5px; width:195px; height:400px;">
        <input id="cbTracking" type="checkbox" onclick="StartStopTracking('cbTracking')" />Start/Stop Tracking<br /><br />
        <hr /><br />
        <b>Select a day for replay</b><br />
        <select id="ddReplayDate" style="width:190px"></select><br />
        <input id="cbReplay" type="checkbox" onclick="StartStopReplay('cbReplay')" />Start/Stop Replay<br /><br />
        <hr /><br />
        <a href='#' onclick='javascript:hideFlyout();'>Close Flyout</a>
    </div>
    <div id='divMap' style="position:absolute; top:65px; left:205px; width:590px; height:530px;"></div>
</body>
</html>

The CSS for the Flyout (flyout.css)

There are no surprises in the CSS. As mentioned before the height and width of the body determine the size of the flyout-window.

html
{
    overflow:hidden;
}

body 
{
    font-family:Tahoma;
    font-size:12px;
    background-color:#5b86ce;
    color:#00284A;
    position:absolute;
    left:0px;
    top:0px;
    margin-left:0px;
    margin-top:0px;
    width:800px;
    height:600px;
}

h1
{
    font-family:Tahoma;
    text-align:center;
}

.header
{
    font-family:Tahoma;
    background-color:#00284A;
    color:#5b86ce;
}

a

The JavaScript for the Flyout (flyout.js)

The live-tracking part is the same as in the docked gadget with the exception that I use the default dashboard-size for the map and that I provide a checkbox which allows me to start and stop tracking.

I also call a function GetDates(). This function creates an AJAX-call to a new web-handler (GetDates.ashx). The source for that web handler is included in the sample code linked above and also listed at the end of this posting. It basically connects to the database and executes a "SELECT DISTINCT GPSdate FROM Tracks ORDER BY GPSdate DESC". The result will be used to create a JavaScript which populates my HTML-SELECT-element in the flyout.html site with the available days for replay. The general approach to integrate Virtual Earth with a database is described in one of my previous posting.

Another AJAX-call will be executed when the checkbox for the replay is activated. It calls my web handler Replay.ashx. This web handler is as well included the sample code linked above and listed at the end of this posting.

Finally there is also a JavaScript hideFlyout() which allows us to close the Flyout-window but in fact the flyout would be closed anyway when the flyout-window looses it’s focus.

window.onload = GetMap;

//Map
var map = null;

//VEShapeLayer
var slTracks = new VEShapeLayer();

function GetMap()
{
    //Populate Dropdown-List
    GetDates();
    
    map = new VEMap('divMap');
    map.LoadMap(new VELatLong(51.461962075378054, -0.9260702133178665), 13, VEMapStyle.Shaded);
    map.AddShapeLayer(slTracks);
}

//Start/Stop Tracking
function StartStopTracking(control)
{
    if (document.getElementById(control).checked == false) {
        clearInterval(showtime);
        slTracks.DeleteAllShapes();
    }
    else {
        //Start Tracking and Set Intervall to 5 minutes
        showtime=setInterval("Track()", 300000);
        Track();
    }
}

//Tracking
function Track()
{
    //Delete previous tracks
    slTracks.DeleteAllShapes();
    
    //Build the URL
    //The random URL-parameter avoids caching
    var url="http://YourServer/Tracking.ashx?" + Math.random();

    //Get the appropriate XMLHTTP object for the browser
    var xmlhttp = GetXmlHttp();
    
    //if we have a valid XMLHTTP object
    if (xmlhttp)
    {
        xmlhttp.Open("GET", url, true); //varAsynx = true
        
        //set the callback
        xmlhttp.onreadystatechange = function()
        {
            if (xmlhttp.readystate ==4) //4 is a success
            {
                //server code creates JavaScript "on the fly" for us to
                //execute using eval()
                var result = xmlhttp.responseText
                eval(result);
            }
        }
        xmlhttp.send(null);
    }
}

function GetXmlHttp()
{
    var x = null;
    try
    {
        x = new ActiveXObject("Msxml2.XMLHTTP");
    }
    catch (e)
    {
        try
        {
            x = new ActiveXObject("Microsoft.XMLHTTP");
        }
        catch (e)
        {
            x = null;
        }
    }
    if (!x && typeof XMLHttpRequest != "undefined")
    {
        x = new XMLHttpRequest();
    }
    return x;
}

//Fill DropDownList
function GetDates()
{
    //Build the URL
    //The random URL-parameter avoids caching
    var url="http://YourServer/GetDates.ashx?" + Math.random();

    //Get the appropriate XMLHTTP object for the browser
    var xmlhttp = GetXmlHttp();
    
    //if we have a valid XMLHTTP object
    if (xmlhttp)
    {
        xmlhttp.Open("GET", url, true); //varAsynx = true
        
        //set the callback
        xmlhttp.onreadystatechange = function()
        {
            if (xmlhttp.readystate ==4) //4 is a success
            {
                //server code creates JavaScript "on the fly" for us to
                //execute using eval()
                var result = xmlhttp.responseText
                eval(result);
            }
        }
        xmlhttp.send(null);
    }
}

//Start/Stop Replay
function StartStopReplay(control)
{
    if (document.getElementById(control).checked == false) {
        slTracks.DeleteAllShapes();
    }
    else {
        Replay();
    }
}

//Replay
function Replay()
{
    //Build the URL
    var url="http://YourServer/Replay.ashx?myDate=" + document.getElementById("ddReplayDate").value;

    //Get the appropriate XMLHTTP object for the browser
    var xmlhttp = GetXmlHttp();
    
    //if we have a valid XMLHTTP object
    if (xmlhttp)
    {
        xmlhttp.Open("GET", url, true); //varAsynx = true
        
        //set the callback
        xmlhttp.onreadystatechange = function()
        {
            if (xmlhttp.readystate ==4) //4 is a success
            {
                //server code creates JavaScript "on the fly"
                //execute using eval()
                var result = xmlhttp.responseText
                eval(result);
            }
        }
        xmlhttp.send(null);
    }
}

function hideFlyout()
{
    System.Gadget.Flyout.show = false;
}

That’s it. Now my boss can always see where the "Virtual Hannes" is without even opening his browser.

image

The complete source code for the web application is linked here. The code for the gadget is here:

Other Listing

GetDates.ashx

<%@ WebHandler Language="VB" Class="GetDates" %>

Imports System
Imports System.Web
Imports System.Data.SqlClient

Public Class GetDates : Implements IHttpHandler
    
    Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
        'set culture to en-UK to avoid potential problems with decimal-separators
        System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.CreateSpecificCulture("en-UK")

        'Retrieve Dates
        Dim settings As ConnectionStringSettings = ConfigurationManager.ConnectionStrings("Tracking")
        Dim sb As StringBuilder = New StringBuilder
        sb.Append("var sOpts = " + """" + "<SELECT id='ddReplayDate' style='width:190px'>" + """" + ";")
        '
        Dim myDates As String = ""
        Dim myConn As New SqlConnection(settings.ConnectionString)
        Dim myQuery As String = "SELECT DISTINCT GPSdate FROM Tracks ORDER BY GPSdate DESC"
        Dim myCMD As New SqlCommand(myQuery, myConn)
        myConn.Open()
        Dim myReader As SqlDataReader = myCMD.ExecuteReader()
        While myReader.Read()
            myDates = myDates + _
                "sOpts += " + """" + "<OPTION VALUE='" + myReader(0).ToString + "'>" + myReader(0).ToString + "</OPTION>n" + """" + ";"
        End While
        myReader.Close()
        myConn.Close()
        sb.Append(myDates)
        sb.Append("ddReplayDate.outerHTML = sOpts  + " + """" + "</SELECT>" + """" + ";")
        context.Response.Write(sb.ToString())
    End Sub
 
    Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
        Get
            Return False
        End Get
    End Property

End Class

Replay.ashx

<%@ WebHandler Language="VB" Class="Replay" %>

Imports System
Imports System.Web
Imports System.Data.SqlClient

Public Class Replay : Implements IHttpHandler
    
    Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
        'set culture to en-UK to avoid potential problems with decimal-separators
        System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.CreateSpecificCulture("en-UK")

        'Fetch the URL-parameter
        Dim myDate As String = context.Request.Params("myDate")

        'Retrieve GPS-Positions
        Dim settings As ConnectionStringSettings = ConfigurationManager.ConnectionStrings("Tracking")
        Dim sb As StringBuilder = New StringBuilder
        Dim myPins As String = ""
        Dim myIcon As String = ""
        Dim myConn As New SqlConnection(settings.ConnectionString)
        Dim myQuery As String = "SELECT Lat, Long, DevName, GPSdate, GPStime, Speed, Heading FROM Tracks WHERE GPSdate LIKE '" + myDate + "' ORDER BY GPStime"
        Dim myCMD As New SqlCommand(myQuery, myConn)
        myConn.Open()
        Dim i As Integer = 0
        Dim myReader As SqlDataReader = myCMD.ExecuteReader()
        While myReader.Read()
            i = i + 1
            'Determine the CustomIcon based on the heading and speed
            If myReader(5).ToString = "0" Then
                myIcon = "stationary.png"
            Else
                Select Case myReader(6)
                    Case 0 To 22
                        myIcon = "n.png"
                    Case 23 To 57
                        myIcon = "ne.png"
                    Case 58 To 112
                        myIcon = "e.png"
                    Case 113 To 147
                        myIcon = "se.png"
                    Case 148 To 202
                        myIcon = "s.png"
                    Case 203 To 247
                        myIcon = "sw.png"
                    Case 248 To 292
                        myIcon = "w.png"
                    Case 293 To 337
                        myIcon = "nw.png"
                    Case 338 To 360
                        myIcon = "n.png"
                End Select
            End If
                
            myPins = myPins + _
                "setTimeout('slTracks.DeleteAllShapes();var " + myReader(2).ToString + "=new VEShape(VEShapeType.Pushpin, new VELatLong(" + myReader(0).ToString + ", " + myReader(1).ToString + "));" + _
                myReader(2).ToString + ".SetTitle(" + """" + myReader(2).ToString + """" + ");" + _
                myReader(2).ToString + ".SetDescription(" + """" + "Date: " + myReader(3).ToString + "<br>Time: " + myReader(4).ToString + "<br>Speed: " + myReader(5).ToString + "<br>Heading: " + myReader(6).ToString + """" + ");" + _
                myReader(2).ToString + ".SetPhotoURL(" + """" + "http://YourServer/Hannes.JPG" + """" + ");" + _
                myReader(2).ToString + ".SetMoreInfoURL(" + """" + "http://johanneskebeck.spaces.live.com/" + """" + ");" + _
                myReader(2).ToString + ".SetCustomIcon(" + """" + myIcon + """" + ");" + _
                "slTracks.AddShape(" + myReader(2).ToString + ");" + _
                "map.SetCenter(new VELatLong(" + myReader(0).ToString + ", " + myReader(1).ToString + "));'," + (i * 1000).ToString + ");"
        End While
        i = i + 1
        myPins = myPins + _
            "setTimeout('alert(" + """" + "Replay Complete" + """" + ");'," + (i * 1000).ToString + ");"
        sb.Append(myPins)
        myReader.Close()
        myConn.Close()
        context.Response.Write(sb.ToString())
    End Sub
 
    Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
        Get
            Return False
        End Get
    End Property

End Class

Advertisements
This entry was posted in Virtual Earth. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s