In the previous article of this series I mentioned that we would next discuss how to visualize the GPS information we’ve been collecting and storing in SSDS.   This article looks at using Virtual Earth to visualize the location history for members. 

For this implementation I chose to try out the brand new ASP.Net Virtual Earth server-side control.  In the past developers looking to integrate with Virtual Earth had to be pretty knowledgeable about JavaScript.  While JavaScript is pretty powerful and it does let you do some pretty cool things on the client side it can be a bear to work with and debug.    The Virtual Earth ASP.Net control lets ASP.Net developers work in c# on the server side without the need to resort to JavaScript.  That said, the use of the server-side control does not prevent the use of client side interactions.  You can in fact use the server-side map control in conjunction with the client side interactivity options afforded by the JavaScript based Virtual Earth SDK.

Download and install the new ASP.Net Map Control here: http://dev.live.com/tools/

 

Quick Refresh about SSDS

In Part IV of the series we discussed the new SSDS (SQL Server Data Services) and the approach for querying and storing entities into an SSDS container.   That article focused on accessing SSDS from the mobile client, but the steps for accessing SSDS from an ASP.Net application are very similar.  You add a web service reference to your web project and generate a web service proxy that you will use to make calls against the SSDS service.   Please refer to Part IV for details.

Note: One area of difference is that you are able to use WCF for connecting to the web service in an ASP.Net application whereas the mobile platform has limited support for WCF.

Assuming we have infrastructure in place to make calls to the SSDS web service, we now need to add and implement a set of methods on our Model (data access layer) that will allow the retrieval of lists of members and the location history for a member.

Querying SSDS for List of Members

For simplicity this will retrieve the entire membership.  In reality, we would want to restrict a web site visitor to only seeing his or her “friends” rather than the entire membership.  

Note: In the current beta release of SSDS, joins are not directly supported, so I’ve deferred implementing the “friends” logic until later (although it is fairly simple to simulate a join and I will likely do this in a later article.)

GetMembers()

Returns all the members of the community as a list of WiEMemberDataObjects.

class WiERemoteModelSSDS : IWiEModel{

   public List<WiEMemberDataObject> GetMembers()

   {

       List<WiEMemberDataObject> members = null;

 

       // Build the querystring

       string queryString = @"from e in entities where (e.Kind == """ + MEMBER_KIND + @""") select e";

 

       // call the helper method to query and sort the results

       members = QueryForMembers(queryString, WiEMemberDataObject.CompareMemberNameForAscendingOrder);

 

       // return the results

       return (members);

   }

 

Querying SSDS for the Location History of a Member

QueryLocationHistoryForMemberBetween(memberID,startDateTimeUTC,endDateTimeUTC)

Queries the SSDS container for all the location history for the specified member that occurred between the start and end times and returns a list of WiELocationDataObjects sorted by DateCollected in descending order (most recent location first).

public List<WiELocationDataObject> GetLocationHistoryForMemberBetween(Guid p_guidMemberID,

                                             DateTime p_dtStartTimeUTC,

                                             DateTime p_dtStopTimeUTC)

{

   List<WiELocationDataObject> locationHistory = null;

 

   // Build the querystring

   string queryString = @"from e in entities where (e.Kind == """ + LOCATION_KIND + @""") && (e[""" +

             WiELocationDataObject.KEY_DATECOLLECTED + @"""] >= DateTime(""" +

             p_dtStartTimeUTC.ToString() + @""")) && (e[""" +

             WiELocationDataObject.KEY_DATECOLLECTED + @"""] <= DateTime(""" +

             p_dtStopTimeUTC.ToString() + @""")) && (e[""" +

             WiELocationDataObject.KEY_MEMBERID + @"""] == """ +

             p_guidMemberID.ToString() + @""") select e";

 

   // call the helper method to query and sort the results

   locationHistory = QueryForLocationHistory(queryString,

                        WiELocationDataObject.CompareDateCollectedForDescendingOrder);

 

   // return the results

   return (locationHistory);

}

 

Note: The current beta release of SSDS does not yet support sorting, so we implement sorting using the sort capabilities included in the .Net collection classes.  Here is what the comparison function for sorting by date collected on a WiELocationDataObject looks like:

class WiELocationDataObject : WiEDataObject

{

  public static int CompareDateCollectedForDescendingOrder(WiELocationDataObject p_obj1,

                                                           WiELocationDataObject p_obj2)

  {

     // Less than 0  p_obj1 is less than p_obj2.

     // 0 p_obj1 equals p_obj2.

     // Greater than 0 p_obj1 is greater than p_obj2.

 

     if (p_obj1.DateCollected > p_obj2.DateCollected)

        return -1;

     else if (p_obj1.DateCollected < p_obj2.DateCollected)

        return 1;

     else

        return 0;

   }

}

 

QueryForLocationHistory(queryString,sortComparisonFunction)

This is a helper method that supports all the GetLocationHistoryXX() methods.  It takes a properly formatted SSDS query along with a comparison function to sort the results of the query.

private List<WiELocationDataObject> QueryForLocationHistory(string p_queryString, Comparison<WiELocationDataObject> p_comparisonFunction)

{

   List<WiELocationDataObject> locationHistory = null;

   List<Entity> entities = null;

 

   Scope containerScope = new Scope();

   containerScope.AuthorityId = AUTHORITY_NAME;

   containerScope.ContainerId = CONTAINER_NAME;

 

   try

   {

       entities = m_sitkaSoapService.Query(containerScope, p_queryString);

   }

   catch(Exception ex)

   {

       // There was a problem querying for location history.

       System.Diagnostics.Trace.WriteLine ("There was a problem querying for the location history: " +

              ex.ToString());

   }

 

   if ((entities != null) && (entities.Count > 0))

   {

       locationHistory = new List<WiELocationDataObject>();

 

       // Turn all the generic entities into WiELocationDataObjects

       foreach (Entity currentEntity in entities)

       {

           WiELocationDataObject locationObject = new WiELocationDataObject(currentEntity.Properties);

           locationHistory.Add(locationObject);

       }

 

       if (p_comparisonFunction != null)

       {

            // Sort the resulting list

            locationHistory.Sort(p_comparisonFunction);

       }

  }

 

  return (locationHistory);

}

 

 

Using the Virtual Earth ASP.Net Map Control

I’m going to assume you already know how to create a new ASP.Net web project using Visual Studio and have installed the Virtual Earth ASP.Net Map control referenced earlier in the article.   I hope that is a fair assumption, please let me know if you’d like me to detail the process of creating a web application.

After creating a new web project, open the Default.aspx page and place an instance of the Map Control and name it m_mainMap.   The ASP.Net VE Map Control relies on the script manager component and you will need to place a ScriptManager control on the page as well.

In addition to the map and script manager controls I will also add a couple more controls: a radio button list to allow the user to pick a timeframe to display (current, last 15 minutes or last 24 hours) and a checkbox list to allow the user to pick one or more members to display.   Finally I add a button [Refresh] with a server side handler for the button press to trigger the handling of plotting the members’ location history.

Note: I wrap the selection and button controls within an ASP.Net UpdatePanel to enable partial rendering of the page and minimize the visual effect of refreshing the entire page.

Here is a subset of the page:

  <body>

<form id="m_mainForm" runat="server">

  <asp:scriptmanager ID="m_scriptManager" runat="server" EnablePageMethods="true">

  </asp:scriptmanager>

  <div>

    <asp:Table ID="m_mainFormTable" runat="server" Width="100%" Height="100%">

      <asp:TableRow>

        <asp:TableCell Width="250px" VerticalAlign="Top">

          <asp:UpdatePanel ID="m_updatePanel" runat="server">

            <ContentTemplate>

              <asp:Table ID="m_tableFriends" runat="server" BorderColor="Black" BorderWidth="1">

                <asp:TableRow>

                  <asp:TableCell>

                     <asp:Label ID="m_labelFriendsHeader" runat="server" Text="Friends:"></asp:Label>

                  </asp:TableCell>

                </asp:TableRow>

                <asp:TableRow>

                  <asp:TableCell>

                    <asp:CheckBoxList ID="m_checkBoxListFriends" runat="server">

                    </asp:CheckBoxList>

                  </asp:TableCell>

                </asp:TableRow>

                  <asp:TableRow>

                    <asp:TableCell>

                      <asp:Label ID="m_labelTimePeriod" runat="server" Text="Time Period:"></asp:Label>

                    </asp:TableCell>

                  </asp:TableRow>

                  <asp:TableRow>

                    <asp:TableCell>

                       <asp:RadioButtonList ID="m_radioButtonListSelectTimePeriod"

                               runat="server" BorderColor="Aqua" BorderWidth="1">

                         <asp:ListItem Text="Current Location" Value="_mostRecent"></asp:ListItem>

                         <asp:ListItem Text="Last 15 minutes" Value="_lastFifteenMinutes"></asp:ListItem>

                         <asp:ListItem Text="Last 24 hours" Value="_last24Hours"></asp:ListItem>

                       </asp:RadioButtonList>

                    </asp:TableCell>

                  </asp:TableRow>

                  <asp:TableRow>

                    <asp:TableCell>                       

                        <asp:Label ID="m_labelRefreshStatus" runat="server"

                               Text="" ForeColor="Red"></asp:Label>

                    </asp:TableCell>

                  </asp:TableRow>

                  <asp:TableRow>

                    <asp:TableCell>

                         <asp:Button ID="m_btnRefresh" runat="server" Text="Refresh" />

                    </asp:TableCell>

                  </asp:TableRow>

              </asp:Table>

            </ContentTemplate>

          </asp:UpdatePanel>

        </asp:TableCell>

        <asp:TableCell ColumnSpan="2">

          <!-- Use the Live.com Virtual Earth Control -->

          <ve:Map ID="m_mainMap" runat="server" Height="600px" Width="100%" ZoomLevel="4" />

        </asp:TableCell>

        <asp:TableCell>

        </asp:TableCell>

      </asp:TableRow>

 

In the code behind class for Default.aspx we will implement a method to plot a member’s location history on the map.  

Virtual Earth supports adding “Shapes” to a map.  There are three types of shapes supported: PushPins (point), PolyLines (open) and Polygons (closed).  I will use PushPins to highlight a single location or the final point in a segment and will use a polyline to show the “trail” or history of locations for a segment of time.

The code to add a new shape to a map is pretty simple:

Adding a PushPin to a Map:

The PushPin shape is useful to visualize a single point or known location.   You specify the location of the PushPin using longitude and latitude through an VE object called LatLongWithAltitude.   Also VE allows you to specify your own icons rather than use the standard PushPin but for now I will stick with the built in PushPin icon.

Shape currentLocationShape = new Shape(ShapeType.Pushpin, new LatLongWithAltitude(lat, lon));

currentLocationShape.Description = “Description Here”;

 

m_mainMap.AddShape(currentLocationShape);

 

Adding a PolyLine to a Map:

A PolyLine is initialized by passing in a list of points specified by LatLongWithAltitude objects.   PolyLines can also have a pushpin associated with them and the placement of the PushPin can be controlled using the IconAnchor property.  

List<LatLongWithAltitude> pointsForPolyline = new List<LatLongWithAltitude>();

 

// Add points to the list for the polyline

 

// Create a polyline shape

Shape historySegmentShape = new Shape(ShapeType.Polyline, pointsForPolyline);

historySegmentShape.IconVisible = true;

historySegmentShape.IconAnchor = pointsForPolyline[0]; // Most recent location on the line (stop point)

historySegmentShape.Description = “Description can be any valid text or HTML”;

 

// Add the shape to the map.

m_mainMap.AddShape(historySegmentShape);

 

 

Descriptions for PushPins

The description associated with a PushPin can be any valid HTML and is shown when the user places the mouse cursor over the PushPin on a map.   I use the description to show some aggregate and status information about the member, for example the peak speed during the period and the member’s name and contact information.  

The following function builds an HTML table to show summary information for the member and the member’s location history:

BuildMemberLocationHistoryDescription(member,locationHistory)

string BuildMemberLocationHistoryDescription(WiEMemberDataObject p_member, List<WiELocationDataObject> p_locationHistory)

{

   string strDescription = "<table>";

   strDescription += ((p_member.FirstName != null) && (p_member.LastName != null)) ?

                      "<tr><td><b>" + p_member.FirstName

                                    + " " + p_member.LastName +

                      "</td></tr>" :

                      "</td></tr>";

   strDescription += (p_member.PhoneNumber != null) ?

                       "<tr><td><b>Phone Number: </b>" + p_member.PhoneNumber : "</td></tr>";

   strDescription += (p_member.Email != null) ?

                       "<tr><td><b>E-mail: </b>" + p_member.Email : "</td></tr>";

 

   // Try to calculate some interesting tidbits, like max speed, total distance, etc.

   double fMaxSpeed = 0;

   double fDistance = 0;

   DateTime dtStart = DateTime.MaxValue;

   DateTime dtStop = DateTime.MinValue;

   WiELocationDataObject prevLocation = null;

   foreach (WiELocationDataObject location in p_locationHistory)

   {

        // Keep track of the maximum speed we reached

        if (location.Speed != null)

             if (location.Speed > fMaxSpeed)

                  fMaxSpeed = (double) location.Speed;

 

         if (prevLocation != null)

         {

             // Keep track of the total distance between all the points.

             fDistance += location.DistanceTo(prevLocation);

 

             // Keep track of the start / stop time (technically we could cheat and grap first and last

             // element since these lists are expected to be sorted.

             dtStart = (dtStart > location.DateCollected) ? (DateTime)location.DateCollected : dtStart;

             dtStop = (dtStop < location.DateCollected) ? (DateTime)location.DateCollected : dtStop;

         }

         else

         {

             dtStart = (DateTime) location.DateCollected;

             dtStop = (DateTime) location.DateCollected;

         }

               

         // Remember previous location for next iteration

          prevLocation = location;

   }

 

   // Show the peak speed during the segment

   strDescription += (fMaxSpeed > 0) ?

            "<tr><td></td></tr><tr><td><b>Max Speed: </b>" +

            String.Format("{0:F}", fMaxSpeed) + " mph." : "</td></tr>";

 

   // Show the total distance

   strDescription += (fDistance > 0) ?

            "<tr><td><b>Est. Distance: </b>" +

            String.Format("{0:F}",fDistance) + " meters." : "</td></tr>";

 

   // Show the start & stop time

   strDescription += "<tr><td></td></tr><tr><td><b>Start Time (UTC): </b>" +

             dtStart.ToString() + "</td></tr>";

   strDescription += "<tr><td></td></tr><tr><td><b>Stop Time (UTC): </b>" +

             dtStop.ToString() + "</td></tr>";

 

   strDescription += "</table>";

 

   return strDescription;

}

 

Putting it all together

Now that we have the basics covered, let’s put them together to implement a full “historical” visualization for a member, showing the history as line segments and pushpins for end points.

To make the visualization more interesting, I break up the history into segments to capture the times when a member may have stopped tracking his or her location and also show some interesting tidbits of information like the peak speed during that period of time.  Of course you could do a number of other interesting things like generate a graph of the speed the member travelled over the journey, or highlight points of interests that were nearby (future article).

Here is the current implementation of showing the location history for a selected member.  

AddFriendToMap(memberID)

 

public partial class _Default : System.Web.UI.Page

{

    protected void AddFriendToMap(System.Guid p_guidMemberID)

    {

      WiEMemberDataObject member = null;

      string strSelectedTimePeriod = m_radioButtonListSelectTimePeriod.Items[0].Value;

 

      // Retrieve the metadata about the member so we can show his or her name

      // and contact information

      member = m_model.GetMember(p_guidMemberID);

           

      // Retrieve the selected time period

      ListItem selectedTimePeriod = m_radioButtonListSelectTimePeriod.SelectedItem;

      if (selectedTimePeriod != null)

      {

         strSelectedTimePeriod = selectedTimePeriod.Value;

      }

 

      if (strSelectedTimePeriod == "_mostRecent")

      {

          // Show only the most recent position / location

          WiELocationDataObject mostRecentLocation = m_model.GetCurrentLocationForMember(member.MemberID);

          if (mostRecentLocation != null)

          {

             double lat = (mostRecentLocation.Latitude != null) ? (double)mostRecentLocation.Latitude : 0;

             double lon = (mostRecentLocation.Longitude != null) ? (double)mostRecentLocation.Longitude : 0;

 

             // string strDescription = BuildDescription(p_member, mostRecentLocation);

             Shape currentLocationShape = new Shape(ShapeType.Pushpin, new LatLongWithAltitude(lat, lon));

             currentLocationShape.Description = BuildMemberLocationDescription(member, mostRecentLocation);

 

             m_mainMap.AddShape(currentLocationShape);

 

             m_mainMap.Center = new LatLong(lat, lon);

 

             m_mainMap.ZoomLevel = 10;

      }

      else

      {

             m_labelRefreshStatus.Text += "There was no current or recent GPS data for " +

                                            member.FirstName + ". ";

      }

   }

   else

   {

      // Show history trail of the last 15 minutes or last 24 hours

      int nMinutes = (strSelectedTimePeriod == "_lastFifteenMinutes") ? -15 : (-1 * 24 * 60);

 

      // Query the model for the location history in the specified time period (in UTC)

      List<WiELocationDataObject> locationHistory = m_model.GetLocationHistoryForMemberBetween(

                      member.MemberID,

                      DateTime.UtcNow.AddMinutes(nMinutes),

                      DateTime.UtcNow);

 

      // If there was any history, plot the history as line segment(s).

      if ((locationHistory != null) && (locationHistory.Count > 0))

      {

           // Now we attempt to split the history into "segments" of time, looking

           // for any significant gaps between locations that could indicate the member

           // had turned off tracking or was stopped.

           List<List<WiELocationDataObject>> locationHistorySegments = SplitLocationHistory(locationHistory);

 

           // We keep a list of all the points added to the map so we can ask the map

           // to set its view to encompass all the points.

           List<LatLongWithAltitude> pointsForMapView = new List<LatLongWithAltitude>(); ;

 

           // We now want to draw a line segment for each "segment" of the history

           foreach (List<WiELocationDataObject> locationHistorySegment in locationHistorySegments)

           {

               // Create a Virtual Earth polyline shape to represent / plot the locations

               // for that time period segment.   Shapes are specified using a list of

               // VE LatLongWithAltitude objects, so we convert from our location objects

               // to VE LatLong objects.

               List<LatLongWithAltitude> pointsForPolyline = new List<LatLongWithAltitude>();

               foreach (WiELocationDataObject location in locationHistorySegment)

               {

                   pointsForPolyline.Add(new LatLongWithAltitude((double)location.Latitude,

                                 (double)location.Longitude));

               }

 

               if (pointsForPolyline.Count > 1)

               {

                   // Now create the polyline shape with the points and show pin Icon at the

                   // begining (latest point in time) of the line with summary information about

                   // that segment of time.

                   Shape historySegmentShape = new Shape(ShapeType.Polyline, pointsForPolyline);

                   historySegmentShape.IconVisible = true;

                   historySegmentShape.IconAnchor = pointsForPolyline[0];

                   historySegmentShape.Description = BuildMemberLocationHistoryDescription(member,

                                                          locationHistorySegment);

 

                   // Add the shape to the map.

                   m_mainMap.AddShape(historySegmentShape);

               }

               else

               {

                   // Special case, there was only one point in the time period, so we use a Pin rather

                   // than a polyline.

                   Shape pointShape = new Shape(ShapeType.Pushpin, pointsForPolyline[0]);

                   pointShape.Description = BuildMemberLocationDescription(member, locationHistorySegment[0]);

                   m_mainMap.AddShape(pointShape);

               }

 

               // Keep track of all the points we've added to the map so we can ask

               // the map to set a view that encompasses all the points.

               pointsForMapView.AddRange(pointsForPolyline);

           }

 

           // Ask the map to set a view that encompasses all the data points.

           m_mainMap.SetMapView(pointsForMapView);

      }

      else

      {

            m_labelRefreshStatus.Text += "There was no GPS data for the selected period for " +

                                                member.FirstName;

      }

    }           

 }

 

What’s Next?

In the next set of articles we will start implementing the “spatial rules engine” using SQL Server 2008 that will notify members of the proximity of other friends and points of interests.

Have a great week,


Olivier