Click here to Skip to main content
15,860,943 members
Articles / Mobile Apps / Windows Phone 7

Bike In City with Windows Phone 7

Rate me:
Please Sign up or sign in to vote.
4.97/5 (62 votes)
5 Jan 2011CPOL33 min read 109.3K   3.1K   101   45
Learn how to build a small mobile application to visualize data from the city bike sharing system. Get the nearest stations, find the number of free bikes, and compute directions to other stations.
Screen1.JPGScreen2.JPG

Contents

This application has a web version on www.bikeincity.com

Update

Since I published this article, I continued working on the application and I've developed a similar application which is now in the process of the market place certification. To pass the certification, you have to correctly implement the "Tombstoning". I added one chapter to this article describing the tombstoning process for this application. If you are interested in just, that you can proceed directly to the Tombstoning chapter.

Introduction

I have a chance to live in Paris this year, which is a great city with a cool bike sharing system called Velib. There are more than 1000 bike stations, where you can just pick up a bike and then return it at some other station when your journey is finished. I was wondering if I could actually get the information about the stations and visualize it using the Bing Maps control. After some research, I found that another French city - Rennes - also possesses a bike sharing system and it has a free developer API to obtain such data. So I said to myself, it's not Paris, but why not. I decided to build an application for Windows Phone 7 which would allow the user to perform the following:

  • Find the nearest stations using the GPS of the telephone and get information about them (number of free bikes, number of places to put the bike).
  • When user selects a station, he can also compute directions to other stations near the entered destination address.

Background

I got the idea of building this application about two months ago; however, at that time, I was thinking only about a web app using the Bing Maps API. When I was in the middle of coding this web application, Windows Phone 7 was announced and so I said to myself that as soon as I finish the web app, I will port it to the mobile.

You can take a look at the web app on this site. Just note that it was left in the middle of the development so it will not work reliably. Well, the idea of porting the application very fast to mobile was quickly forgotten because I have found that it is just not that easy on the phone.

First - the namespaces for the Bing Maps API and Phone Maps are not the same. So for example, the Location class is presented in both namespaces but is not the same, thus you have to change your model classes.

Second - The phone is different. The screen is small, so it took me some time to move things around to fit them all on the screen.

In the future, I am planning to consolidate these two applications so that they could share as much code as possible.

This article is not a complete walkthrough - it is not a step by step article on how to build the application, however I will try to give as much detailed description of the application as I can. I hope that after reading this, you will be able to understand how the application was built and how it works.

I hope to provide you with some useful information about Bing Maps, Windows Phone 7, and Silverlight in general. There are no exact prerequisites, but I assume that you are familiar with C# and that you know the basics of Silverlight and WPF.

Architecture

I tried to keep the application as simple as possible so it is composed of just a few classes. The main class is the MainPage class which contains all the data which is visualized. It uses the data model described below (just two classes). To talk to the Web Services, MainPage makes use of the ServiceCaller class. The ServiceCaller provides methods to access web services and events which get fired when the results have been obtained and processed. GeoCoordinateSimulator emulates the GPS device API and fires an event when the position of the device has changed. Here is a simple schema of the architecture:

Image 3

The application contains several other classes which are not important from the architectural point of view.

Data model

In this part, I will describe the model behind the application - classes which will contain the information needed to be visualized on the map. There are just two classes: BikeStation and BikeRoute. Both of these classes, like the whole project, make use of the GeoCoordinate class from the System.Device.Location namespace. This class represents a location defined by its latitude and longitude. This class also contains some other properties like Course and Speed which are not used in this application. To store the location, we can use the Location class from the Microsoft.Phone.Controls.Maps namespace. This class does not contain these additional information and is part of the Bing Maps API. There is an automatic conversion from Location to GeoCoordinate but not the other way around.

BikeStation holds all the information about a bike rental place. This is basically the address, location, number of total slots, and number of free bikes. There is also a ObservavleCollection<bikeroute> Routes property which holds a collection of routes - these routes are loaded when the user searches for directions from the selected station to other stations near the destination address.

C#
public class BikeStation : INotifyPropertyChanged
{        
    private  GeoCoordinate _location;
    private int _free;
    private bool _isSelected;
    private int _walkDistance;    
    private int _id;
    private string _address;
    private int _total;
    private ObservableCollection<bikeroute> _routes;
...
   public GeoCoordinate Location
    {
      get
      {
        return _location;
      }
      set
      {
        _location = value;
        OnPropertyChanged("Location");
      }
    }
    //all other public properties
}

BikeStation also contains a property called WalkDistance which is the straight distance of the station to the current user location. This distance is computed using the Haversine formula described later.

The BikeRoute class simply represents a route between two bike stations. The most important property here is the Locations property, of type LocationCollection. This is, as the name says, a collection of Locations that are later used to draw the route on the map.

C#
public class BikeRoute : INotifyPropertyChanged
{
    private BikeStation _to;
    private BikeStation _from;
    private double _distance;
    private int _time;
    private LocationCollection _locations;
    private double _opacity;
    private bool _isSelected;
    private int _totalTime;
   
    public LocationCollection Locations
    {
      get {
        if (_locations == null)
        {
          _locations = new LocationCollection();
        }
        return _locations;
      }

      set { 
        _locations = value;
        OnPropertyChanged("Locations");
      }
    }
   //all other public properties
}

To keep it simple, we can maintain all the data which should be visualized in the MainPage, which is the main class, and the Phone page to which the user navigates to after the start of the application.

C#
public class MainPage:PhoneApplicationPage, INotifyPropertyChanged{
    
   private BikeStation[] _stations;
  
    public ObservableCollection<bikestation> DepartureStations
    {
        get
        {
            if (_departureStations == null)
            {
                _departureStations = new ObservableCollection<bikestation>();
            }
            return _departureStations;
        }
        set
        {
            _departureStations = value;
            OnPropertyChanged("DepartureStations");
        }
    }
    public ObservableCollection<bikestation> ArrivalStations {...}
  
    public GeoCoordinate Departure
    {
        get {
            return _from;
        }
        set
        {
            _from = value;
            OnPropertyChanged("From");
        }
    }
   
   public GeoCoordinate Arrival
  
   public BikeRoute CurrentRoute {...}
   public BikeStation CurrentStation {...}
}

To summarize, we have two ObservableCollections which store the departure and arrival stations. The arrival station's collection is filled only when the user searches a route. When the user searches for the nearest stations to his actual position, then these stations are stored in the DepartureStations collection.

The Departure and Arrival properties of type GeoCoordinate serve to visualize on the map the current position and the location of the destination.

CurrentRoute and CurrentStation are just properties which hold the station selected by clicking on the station pushpin or the route selected from the route list.

The private array of BikeStations _station is the collection off all the stations in the city. This collection is queried when the user asks for stations near his location.

The MainPage class implements INotifyPropertyChanged to let the UI know when some of the properties have changed.

Using and emulating GPS

To get the current location of the user, we will make use of the phone's GPS; to do so, we use the GeoCoordinateWatcher class. This class is an API to the phone GPS, and contains a property Position of type GeoPosition<geocoordinate> which is in fact the current Location with a TimeStamp. Also it provides PositionChanged event, which is fired when the device changes its position. It is possible specify the accuracy of received location information in the constructor of GeoCoordinateWatcher. There are two possibilities:

  • GeoPositionAccuracy.Default - This option lets the native framework decide which source of location data should be used (WiFi, GSM Cell information, GPS) to optimize the power consumption.
  • GeoPositionAccuracy.High - This option will force the GeoCoordinateWatcher to always use the GPS, which is the most power consuming, but most accurate.

To control how often the PositionChanged event will fire, we can set the MovementThreshold property (in meters), which denotes the level of position change which will lead to evocation of the event. Here is the code which initializes the GeoCoordinateWatcher and subscribes an event handler to the PositionChanged event:

C#
GeoCoordinateWatcher _watcher = 
   new GeoCoordinateWatcher(GeoPositionAccuracy.Default);
_watcher.MovementThreshold = 20;
_watcher.PositionChanged+= 
  new EventHandler<geopositionchangedeventargs<geocoordinate>>(
  _watcher_PositionChanged);
_watcher.Start();

GeoCoordinateWatcher implements IDisposable so we should call its Dispose method when we are finished with it, or enclose it with using directives.

C#
_watcher.Stop();
_watcher.Dispose();

GPS in emulator

When running the application in the emulator, it is not possible to use the GeoCoordinateWatcher; however, we can simulate the GPS device by defining our own class that implements IGeoPositionWatcher<t>. GeoCoordinateWatcher uses GeoCoordinate as a template class to implement the IGeoPositionWatcher<t> interface. That means that it uses GeoCoordinate as the class which stores the location data. That's why the Position property is of type GeoPosition<geocoordinate>. We can define our own class which will implement IGeoPositionWatcher<geocoordinate> and implement its methods and properties. My implementation suites only the case of this application - I have a city and I want to simulate the user's movement around the city. To achieve this, I have to know the borders of the city and then using the timer, simulate a change of position within the city borders every few seconds (or minutes). So let us start by defining a class called GeoCoordinateSimulator which will implement the IGeoCoordinateWatcher<geocoordinate> interface.

C#
public class GeoCoordinateSimulator : IGeoPositionWatcher<geocoordinate>
{
  //represents left down corner of city bordering rectangle
  private GeoCoordinate _leftCorner;

  //represents right up corner of city bordering rectangle
  private GeoCoordinate _rightCorner;

  //direction in which the current position will change
  private double _dLat;
  private double _dLong;

  //Time interval between 2 changes of position
  private int _interval;

  private GeoPosition<geocoordinate> _position;

  //timer to fire position changes
  private Timer _timer;
  public Object _timerState;
}

The borders of the city will be determined by two GeoCoordinate fields representing the lower left corner and upper right corner. The GeoCoordinate object _position will provide the current location of the simulator. To simulate the movement, I declare two double variables which represent the change of the position in X (longitude) and Y (latitude) directions. As the last piece of the puzzle, we have a Timer which will fire regularly on predefined intervals and will apply the changes to the current position and fire the PositionChanged event. The constructor has three parameters, two of them to describe the corner points of the city and the third is the interval at which the timer should fire. In the constructor, besides some checks to assure that the corner points are in good order, the position is set to the middle of the city.

C#
public GeoCoordinateSimulator(GeoCoordinate left, GeoCoordinate right, int interval)
{
  ...
  double latRange = _rightCorner.Latitude - _leftCorner.Latitude;
  double longRange = _rightCorner.Longitude - _leftCorner.Longitude;

  //setting current position to the midle of the city
  _position = new GeoPosition<geocoordinate>(DateTime.Now,
              new GeoCoordinate(_leftCorner.Latitude + latRange / 2, 
                                _leftCorner.Longitude + longRange / 2));

  //set the interval at which the timer should fire
  _interval = interval;
}

The Start() method just creates the timer and sets the callback. The callback method adds the values of the change in the latitude and longitude directions to the actual position. If the resulting point would be out of the borders of the city, it will randomly generate a new direction to stay in the city.

C#
public void TimerCallBack(Object obj)
{
  Random r = new Random();
  double newLatitude, newLongitude;

  while (!IsInRange(newLatitude = this.Position.Location.Latitude + _dLat,
  newLongitude = this.Position.Location.Longitude + _dLong) || 
                     (_dLat==0.0 && _dLong==0.0))
  {
    _dLat = (r.NextDouble() - 0.5) * BikeConst.GPS_SIMULATOR_STEP;
    _dLong = (r.NextDouble() - 0.5) * BikeConst.GPS_SIMULATOR_STEP;
  }

  //set new position
  _position = new GeoPosition<geocoordinate>(DateTime.Now, 
              new GeoCoordinate(newLatitude,newLongitude));

  //fire the event if there are any subscribers
  if (this.PositionChanged != null)
  {
    PositionChanged(this, 
      new GeoPositionChangedEventArgs<geocoordinate>(this.Position));
  }
}

To obtain the new direction of movement, I generate a random value between -0.5 and 0.5 and then multiply it by a double constant which represents the size of the change. Also note that you have to check both values of change being zero, because the position would never change. When the new position is set, then the PositionChanged event will fire to let the observers know about the change. There is a call to the IsInRange() method which just checks if the given point is still in the rectangle of the city.

C#
public bool IsInRange(double lat,double lng)
{
  return (lat > _leftCorner.Latitude && lng > _leftCorner.Longitude
    && lat < _rightCorner.Latitude && lng < _rightCorner.Longitude) ;
}

To use this simulator, we will just use GeoCoordinateSimulator instead of the GeoPositionWatcher class. It will throw us the PositionChanged event as if we would use a real device which changes its position.

C#
//these are lower left and upper right corners of Rennes
GeoCoordinate leftCorner = new GeoCoordinate(48.094133, -1.705112);
GeoCoordinate rightCorner = new GeoCoordinate(48.123018,-1.642971);
_watcher = new GeoCoordinateSimulator(leftCorner, 
               rightCorner, BikeConst.GPS_SIMULATOR_INTERVAL);
_watcher.Start();

Computing distance on the Earth's surface

In order to see which stations are closest to the current phone's location, we will have to compute the distance between two points defined by spherical coordinates. To do so, the Haversine formula is used which allows computation of the distance of two points on the surface of a sphere. More information can be found on the Wikipedia page and on this site. The computation is implemented in a static method ComputeDistance in the class GeoMath.

C#
public static int ComputeDistance(Location start, Location end)
{
  var R = 6371;
  double lat1 = ToRad(start.Latitude);
  double lat2 = ToRad(end.Latitude);

  double lng1 = ToRad(start.Longitude);
  double lng2 = ToRad(end.Longitude);

  double dlng = lng2 - lng1;
  double dlat = lat2 - lat1;

  var a = Math.Pow(Math.Sin(dlat / 2),2) + Math.Cos(lat1) * 
                   Math.Cos(lat2) * Math.Pow(Math.Sin(dlng/2),2);
  var c = 2*Math.Asin(Math.Min(1,Math.Sqrt(a)));

  var d = R * c;
  return (int)(d * 1000);
}

Getting the data

This part describes how to call the Bing Maps Web Services to geocode addresses and obtain directions and how to call the Rennes city Web Services to obtain data from the bike system. The project contains a class called ServiceCaller which provides access to all of the data stores. More specifically, it provides methods which asynchronously call the Web Services and return results to the MainPage.

Getting information from the bike system

The data that I need is accessible via a Web Service provided by the Rennes city. To obtain access to this data, you have to register at http://data.keolis-rennes.com/ as a developer. On this address, you will also find the documentation of the REST API. After registering, you will obtain a developer's key which you will pass to the Web Service to obtain the data. Basically, to obtain any data from the system, you need to compose an HTTP GET request in the following form:

http://data.keolis-rennes.com/xml/?version=1.0&key=XXXXXXXXXXXXXXX&cmd=command

Here, the key parameter will be your developer's key and cmd will be the command describing your operation.

Getting a list of all stations

To get the list of all stations, the ServiceCaller class contains a method GetAllStations(). This method uses a WebClient class which lets us asynchronously receive data identified by its URL. When the data is downloaded, ServiceCaller will fire the StationsLoaded event. Before we call for the data, we register a method which will be executed when the download process is completed.

C#
public void GetAllStations()
{
  WebClient ws = new WebClient();
  string url = "http://data.keolis-rennes.com/xml/?" + 
               "version=1.0&key=key&cmd=getstation";
  ws.DownloadStringCompleted += 
     new DownloadStringCompletedEventHandler(StationsListRecieved);
  ws.DownloadStringAsync(new Uri(url)); 
}

After invoking the Web Service with the "getstation" command, we will obtain the XML, which is not hard to parse using LINQ with the following structure.

XML
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> 
<opendata> 
    <request>http://data.keolis-rennes.com/xml/?
             version=1.0&key=yourkey&cmd=getstation</request> 
    <answer> 
        <status code="0" message="OK"/> 
        <data> 
            <station> 
                <id>75</id> 
                <number>75</number> 
                <name>ZAC SAINT SULPICE</name> 
                <state>1</state> 
                <latitude>48.1321</latitude> 
                <longitude>-1.63528</longitude> 
                <slotsavailable>21</slotsavailable> 
                <bikesavailable>8</bikesavailable> 
                <pos>0</pos> 
                <district>Maurepas - Patton</district> 
                <lastupdate>2010-12-05T01:29:06+01:00</lastupdate> 
            </station> 
            <station> 
                <id>52</id> 
                <number>52</number> 
                <name>VILLEJEAN-UNIVERSITE</name> 
                <state>1</state> 
                <latitude>48.121075</latitude> 
                <longitude>-1.704122</longitude> 
                <slotsavailable>14</slotsavailable> 
                <bikesavailable>11</bikesavailable> 
                <pos>1</pos> 
                <district>Villejean-Beauregard</district> 
                <lastupdate>2010-12-05T01:29:06+01:00</lastupdate> 
            </station>
        </data>
    </answer>
</opendata>

When the data is downloaded, the StationsListRecieved event handler executes. The data that you need is stored in the form of XML so we can use LINQ to XML to parse it and obtain an array of BikeStation classes. We can check if there are any subscribers for the StationsLoaded event, and we fire it giving it an argument of type StationsLoadedEventArgs.

C#
if (e.Result != null)
{
  XDocument xDoc = XDocument.Parse(e.Result);
  BikeStation[] result = null;
  
  result = (from c in xDoc.Descendants("opendata").Descendants(
                 "answer").Descendants("data").Descendants("station")
            select new BikeStation
            {
              Address = (string)c.Element("name").Value,
              Id = Convert.ToInt16(c.Element("id").Value),
              Location = new GeoCoordinate(
                             Convert.ToDouble(c.Element("latitude").Value), 
                             Convert.ToDouble(c.Element("longitude").Value)),
              Free = Convert.ToInt16(c.Element("bikesavailable").Value),
              FreePlaces = Convert.ToInt16(c.Element("slotsavailable").Value)
            }).ToArray();

  //Send event containing the received array of bike station to the MainPage
  if (this.StationsLoaded != null)
  {
    this.StationsLoaded(this, new StationsLoadedEventArgs(result));
  }
}

StationsLoadedEventArgs is a simple class deriving from EventArgs and encapsulating the BikeStation[] array.

C#
public class StationsLoadedEventArgs:EventArgs
{
  public BikeStation[] Stations { get; set; }
  public StationsLoadedEventArgs(BikeStation[] stations)
  {
    this.Stations = stations;
  }
}

In the constructor, the MainPage class uses ServiceCaller to get all the stations. When the collection is received, ServiceCaller will fire its StationsLoaded event and MainPage will just assign this collection to its private _stations collection, which is later queried to obtain the nearest stations.

Getting the details of a station

Here, the situation is a little bit different. We already have a BikeStation object and we just want to update the information inside. Again, we will use the WebClient to obtain the data, but before that, we will create a GUID for the BikeStation and store it in a dictionary. Then we will call the DownloadStringAsync method with two parameters, passing the GUID as the second parameter. This will cause that on the reception of the "completed" event, we can recuperate the GUID and assign the received values to the right BikeStation object. The method GetStationInformation(BikeStation) shows how to call the Web Service.

C#
public void GetStationInformation(BikeStation station)
{
  string url = String.Format("http://data.keolis-rennes.com/xml/" + 
               "?version=1.0&key={0}&cmd=getstation¶m[request]" + 
               "=number¶m[value]={1}",
    BikeConst.RENNES_KEY,station.Id);
    Guid stationGuid = Guid.NewGuid();
    _stationsDict.Add(stationGuid, station);
    WebClient webClient = new WebClient();
    webClient.DownloadStringCompleted += 
       new DownloadStringCompletedEventHandler(StationInformationReceived);
    webClient.DownloadStringAsync(new Uri(url), stationGuid);
}

The URL to the Rennes data service differs from the one before. The "getstation" command can have a parameter "number" corresponding to the ID of the station.

The data that we will receive will be XML with the same structure as when calling for the list of stations (described above), but the XML will contain only one station.

When we receive the data, first we recuperate the GUID of the station. This arrives in the UserState parameter of the event arguments. Later, we parse the data again using LINQ to XML and we can update the selected station. After updating, we can remove the GUID from the dictionary.

C#
if(e.Result!=null){
  string xmlString = e.Result;

  XDocument xDoc = XDocument.Parse(xmlString);
  Guid stationGuid = (Guid)e.UserState;
  BikeStation station = _stationsDict[stationGuid];

 
  var stInfo = (from c in xDoc.Descendants("opendata").Descendants(
                     "answer").Descendants("data").Descendants("station")
                select new BikeStation
                {
                  Free = Convert.ToInt16(c.Element("bikesavailable").Value),
                }).First();

  station.Free = stInfo.Free;
 _stationsDict.Remove(stationGuid);       
}

Using Bing Maps Services

This part is well explained in the WP7 Developers Training Kit - so here is just a brief explanation and how I adapted it to my exact scenario. Note that in order to use Bing Maps Services and components, you need to register at the Bing Maps portal to obtain your developer key. Bing Maps API exposes several WCF services accessible over the internet. This application makes use of two of them: Geocode Service and Route Service.

To access each of these WCF services, you need to add to your project service a reference pointing to the right URL. The list of URLs of Bing Maps SOAP services can be found on this site. If you have any trouble getting access to the services, you can consult this page which is a general article describing how to develop a Silverlight application interacting with the Bing Maps SOAP services.

Geocoding an address

Here I can use code really similar to the one provided on MSDN. We create a new GeoCodeRequest, to which we pass the address we wish to geocode as a query. When creating the GeocodeServiceClient, we specify the endpoint of the service as the constructor. Here we can use standard HTTP binding, however there is also secure binding using SSL accessible.

C#
public void GeocodeAddress(string address,State state)
{
  if (address != String.Empty)
  {
    GeocodeRequest geocodeRequest = new GeocodeRequest();

    // Your key should be in stored in the _mapID
    geocodeRequest.Credentials = new Credentials();
    geocodeRequest.Credentials.ApplicationId = _mapID;

    //set the address which we search
    geocodeRequest.Query = address;

    // Make the geocode request
    GeocodeServiceClient geocodeService = 
      new GeocodeServiceClient("BasicHttpBinding_IGeocodeService");
    geocodeService.GeocodeCompleted += 
      new EventHandler<geocodecompletedeventargs>( GeocodeCompleted);

    //passing the state argument - either this
    //is to just location or to get directions
    geocodeService.GeocodeAsync(geocodeRequest, state);
  }
}

When we obtain the results from Bing Services, we will fire the BikePlaceGeocoded event and we pass the geocoded GeoCoordinate object as the argument of this event in order to deliver it to the MainPage class.

C#
void GeocodeCompleted(object sender, GeocodeCompletedEventArgs e)
{
  if (e.Result.ResponseSummary.StatusCode == 
      GeocodeService.ResponseStatusCode.Success)
  {
    if (e.Result.Results.Count > 0)
    {
      GeoCoordinate coordinate = e.Result.Results[0].Locations[0];
      if (this.BikePlaceGeocoded != null)
      {
        BikePlaceGeocoded(this, 
          new AddressGeocodedEventArgs(coordinate,(State)e.UserState));
      }
    }
  }
}

Just to complete your idea, here is the code for AddressGeocodedEventArgs:

C#
public class AddressGeocodedEventArgs : EventArgs
{
  public GeoCoordinate Location {get;set;}

  public AddressGeocodedEventArgs(GeoCoordinate c, State s)
  {
    this.Location = c;
    this.StateType = s;
  }
}

Calculating the Route

ServiceCaller exposes a method CalculateRoute which accepts BikeRoute as its parameter. So here we assume that we already have a BikeRoute object containing the starting and ending point and we want to calculate the route - obtain the exact directions and the total time.

C#
public void CalculateRoute(BikeRoute route)
{
  RouteServiceClient routeClient = 
    new RouteServiceClient("BasicHttpBinding_IRouteService");
  routeClient.CalculateRouteCompleted += 
    new EventHandler<calculateroutecompletedeventargs>(
    CalculatedRoute_Completed);


  RouteRequest routeRequest = new RouteRequest();
  
  routeRequest.Options = new RouteOptions();
  routeRequest.Options.Mode = TravelMode.Driving;
  routeRequest.Options.Optimization = RouteOptimization.MinimizeDistance;
  routeRequest.Credentials = new Credentials();
  routeRequest.Credentials.ApplicationId = _mapID;

  routeRequest.Waypoints = new ObservableCollection<waypoint>();

  Waypoint from = new Waypoint();
  from.Location = route.From.Location;
  routeRequest.Waypoints.Add(from);

  Waypoint to = new Waypoint();
  to.Location = route.To.Location;
  routeRequest.Waypoints.Add(to);


  Guid routeGuid = Guid.NewGuid();
  _routesDict.Add(routeGuid, route);

  routeClient.CalculateRouteAsync(routeRequest, routeGuid);
}

In the RouteOptions object, we specify that we want directions for driving and minimize the distance. That should give us good results for biking (well, even though sometimes we think that on the bike we can do anything, we should obey traffic rules). Then we will add two Waypoints to the RouteRequest corresponding to the bike rental stations. As in the case of updating info for a BikeStation, I store the BikeRoute object in the dictionary and the key (GUID) I pass to the request.

C#
void CalculatedRoute_Completed(object sender, CalculateRouteCompletedEventArgs e)
{
  if ((e.Result.ResponseSummary.StatusCode == 
                RouteService.ResponseStatusCode.Success))
  {
    Guid routeGuid = (Guid)e.UserState;
    //get the route from the route table
    BikeRoute route = _routesDict[routeGuid];

    //add all the points in the route to the LocationCollection
    //which is later binded to the object.
    foreach (Location p in e.Result.Result.RoutePath.Points)
    {
      route.Locations.Add(p);
    }

    route.Distance = e.Result.Result.Summary.Distance;
    //calculate time estimation in minutes
    route.Time = (int)e.Result.Result.Summary.TimeInSeconds / 60 * 
                      BikeConst.DRIVE_TO_BIKE;
    //remove the route from the dictionary
    _routesDict.Remove(routeGuid);
  }
}

In the event handler, when the route is calculated, I recuperate the GUID to get the concerned route. The points which build the desired route are stored in the RoutePath.Point collection of the Result. We add them to the Locations property of the BikeRoute. As said before, this property is of a special type LocationCollection, to which the MapPolyline object can be bound - that comes in the following part.

Preparing the GUI

Before we start creating the GUI, it's good to know that there is a UI Design and Interaction Guide for Windows Phone 7, which gives us the color schemes. So there are Brushes which I use in the application and which I found in this document.

XML
<SolidColorBrush x:Name="LimeBrush" Color="#8CBF26"/>
<SolidColorBrush x:Name="OrangeBrush" Color="#F09609"/>

Now the first thing we want to do is just put the map on the place. The map resides in the Microsoft.Phone.Controls.Maps namespace so we have to add the declaration to the top of the XAML.

XML
xmlns:map="clr-namespace:Microsoft.Phone.Controls.Maps;
           assembly=Microsoft.Phone.Controls.Maps"
...

<map:Map x:Name="map" CredentialsProvider="{Binding CredentialsProvider}"
                         CopyrightVisibility="Collapsed" 
                         LogoVisibility="Collapsed"
                         ZoomLevel="{Binding Zoom,Mode=TwoWay}"
                         HorizontalAlignment="Stretch" 
                         VerticalAlignment="Stretch"></map>

You can see that the ZoomLevel is bound to the Zoom property which is exposed on the MainPage class; the same counts for CredentialsProvider. The CredentialsProvider property should contain your Ming Maps developer key.

C#
public double Zoom
{
  get { return _zoom; }
  set
  {
    var coercedZoom = Math.Max(MinZoomLevel, Math.Min(MaxZoomLevel, value));
    if (_zoom != coercedZoom)
    {
      _zoom = value;
      OnPropertyChanged("Zoom");
    }
  }
}

public CredentialsProvider CredentialsProvider
{
  get { return _credentialsProvider; }
}

When setting the zoom level, we assure that it will be greater than the maximal and minimal levels which are stored in constants. I have decided to use the same buttons for zooming as the ones which are used in the official WP7 Training Kit and for one simple reason - they look better than anything I came up with, so I took them as a starting point and just simplified the styling a bit. So here is the zoom-in button.

XML
<Button x:Name="ButtonZoomIn" Style="{StaticResource ButtonZoomInStyle}"
                            HorizontalAlignment="Left" VerticalAlignment="Top"
                            Height="56" Width="56" Margin="8,250,0,0"
                            Click="ButtonZoomIn_Click"/>

You can see that I am positioning the button on the map by setting the Margin property. Also, it is visible that ButtonZoomInStyle is applied to this button. This one is defined in a separate DefaultStyles.xaml file. I will describe the style here in a little detail because the same type of style is used for other buttons in the project.

XML
<Style x:Key="ButtonZoomInStyle" TargetType="Button"
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Grid Background="Transparent" Width="48" Height="48">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal"/>
                            <VisualState x:Name="Pressed">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                           Storyboard.TargetProperty="Visibility" 
                                           Storyboard.TargetName="image">
                                        <DiscreteObjectKeyFrame KeyTime="0" 
                                           Value="Visible"/>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames 
                                              Storyboard.TargetProperty="Visibility" 
                                              Storyboard.TargetName="image1">
                                        <DiscreteObjectKeyFrame KeyTime="0" 
                                              Value="Collapsed"/>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Image x:Name="image" 
                        Source="/BikeInCity;component/Icons/Zoom/ZoomIn_White.png" 
                        Stretch="Fill" Visibility="Collapsed"/>
                    <Image x:Name="image1" 
                        Source="/BikeInCity;component/Icons/Zoom/ZoomIn_Black.png" 
                        Stretch="Fill"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The style overrides the Template property of the button. We are defining a new ControlTemplate which contains a Grid with two images inside (one overlying the other). We are using VisualStates to set the Visibility of the top picture to Collapsed and thus show the image below when the user presses the button.

If you are not familiar with the concept of VisualStates, you can look at them as declarations which describe how the component looks like in the exact state. VisualStates are implemented only in Silverlight and work like Triggers and they can be seen as a replacement for Triggers from WPF. The basic idea is that the creator of a component will define several states and has to anticipate which state will be important for the user - so in some ways, we can say that the concept is less powerful than Triggers (where the template designer has more freedom and is not tied by a group of predefined states).

Here, inside the Grid, we have a VisualStateManager which contains a group of states "CommonStates". This is a predefined group for the Button component and contains four states: Normal, MouseOver, Pressed, and Disabled. I am interested only in the Normal and Pressed states. By leaving the Normal state without further changes, I am declaring that I don't want any changes to look for the component in the state.

On the other hand, the Pressed state contains a Storyboard which provides a timeline for some animation that we might want to perform on the component. In this example, we will just use ObjectAnimationUsingKeyFrame which allows us to perform changes on a property of a component at specific times. Here we are just saying when the Button is clicked, set the Visibility of the first image to Collapsed.

To control the zoom level, we just add two handlers for the zoom button where we increment or decrement the current value of the Zoom property.

Adding the address panel

This panel will be visible when the user wants to get directions to a different location.

DirectionsPanel.JPG
XML
<Border Background="{StaticResource LimeBrush}" Width="400" Height="100" 
        x:Name="DirectionsPanel" BorderThickness="2" BorderBrush="Black"
        Visibility="Collapsed">

    <StackPanel Orientation="Horizontal">
        <TextBlock Text="To:" VerticalAlignment="Center" Margin="3,0,0,0" 
                   FontSize="28" FontWeight="Bold" Foreground="Black"/>
        <TextBox Name="txtAddressTo" Width="300" Text="71 Rue d'Inkermann" 
                 FontSize="22" FontWeight="Bold" TextWrapping="Wrap"/>
        <Button Name="bntSearch" Style="{StaticResource ButtonPlayStyle}" 
                    Click="ComputeDirections_Click"/>
    </StackPanel>
</Border>

That is nothing too complicated - a Border component with a horizontally oriented StackPanel. The style which is applied to the Button is analogical to the one applied to the zoom buttons. You see that there is a callback assigned to this button - we will get to it later.

Adding the station panel

The panel which displays the station details is added to the same grid column as the map component - it is actually placed over the map component.

StationsPanel.jpg
XML
<Grid x:Name="StationPanel" Width="400" 
           MinHeight="40" Margin="0,10,0,0" VerticalAlignment="Top">
    <Border Background="Black" BorderBrush="White" 
           BorderThickness="2" Opacity="0.8"/>
    <StackPanel DataContext="{Binding CurrentStation}">
        <Grid DataContext="{Binding}">
            <Grid.RowDefinitions>
                <RowDefinition  Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="30"/>
                <ColumnDefinition/>
                <ColumnDefinition/>
                <ColumnDefinition/>
                <ColumnDefinition Width="30"/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="{Binding Address}" Margin="3,0,0,3" 
               VerticalAlignment="Center" FontSize="28" 
               HorizontalAlignment="Center" Grid.ColumnSpan="5"/>

            <StackPanel Orientation="Horizontal" Grid.Column="1" 
                     Margin="6,1,6,0" Grid.Row="1"
                     VerticalAlignment="Center" HorizontalAlignment="Center">
                <TextBlock Text="{Binding WalkDistance}" 
                   VerticalAlignment="Center" FontSize="28"/>
                <TextBlock Text=" m" FontSize="30" VerticalAlignment="Center"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal" Grid.Column="2" 
                    Margin="6,1,6,0" Grid.Row="1"
                    VerticalAlignment="Center" HorizontalAlignment="Center">
                <Image Source="/Icons/Others/BicycleWhite.png" 
                       Height="40" Width="45"/>
                <TextBlock Text="{Binding Path=Free}" 
                    VerticalAlignment="Center" FontSize="28"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal" 
                    Grid.Column="3" Margin="6,1,6,0" Grid.Row="1"
                    VerticalAlignment="Center" HorizontalAlignment="Center">
                <Image Source="/Icons/Others/HouseWhite.png" 
                       Height="40" Width="45" />
                <TextBlock Text="{Binding Path=FreePlaces}" 
                    VerticalAlignment="Center" FontSize="28"/>
            </StackPanel>
        </Grid>

        <!-- Route List -->
        <ListBox x:Name="RouteList" ItemsSource="{Binding Routes}"
             ItemTemplate="{StaticResource RouteListTemplate}"
             VerticalAlignment="Top" SelectionChanged="RouteList_SelectionChanged"
             MaxHeight="120" ItemContainerStyle="{StaticResource ListItemStyle}" 
                 Width="395" Margin="0,0,0,5"/>
    </StackPanel>
</Grid>

The station panel is composed of a border with Opacity set to 0.8 so the map underneath can be seen. Over this Border, a StackPanel is placed which has the DataContext property bound CurrentStation property of the MainPage. There are two components on the StackPanel: a Grid with the information about the station and the list containing the routes from the station to the destination stations. All the textboxes in the Grid are bound to the properties of the BikeStation object in the CurrentStation property.

The route list has its ItemsSource bound to the Routes property of the BikeStation class. The list has ItemContainerStyle set as well as ItemTemplateStyle. The item container style sets the style of the container for each item. Here I have a simple style which just changes the color of the selected item.

RouteList.JPG

XML
<Style x:Key="ListItemStyle" TargetType="ListBoxItem">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListBoxItem">
                <Grid x:Name="Container" 
                         Background="{StaticResource LimeBrush}" 
                         Margin="5,3,5,3">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="SelectionStates">
                            <VisualState x:Name="Unselected"/>
                            <VisualState x:Name="Selected">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                               Storyboard.TargetProperty="Background" 
                                               Storyboard.TargetName="Container">
                                        <DiscreteObjectKeyFrame KeyTime="0" 
                                            Value="{StaticResource OrangeBrush}" />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="SelectedUnfocused">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                             Storyboard.TargetProperty="Background" 
                                             Storyboard.TargetName="Container">
                                        <DiscreteObjectKeyFrame KeyTime="0" 
                                             Value="{StaticResource OrangeBrush}" />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                        <VisualStateGroup x:Name="FocusStates">
                            <VisualState x:Name="Unfocused"/>
                            <VisualState x:Name="Focused">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                              Storyboard.TargetProperty="Background" 
                                              Storyboard.TargetName="Container">
                                        <DiscreteObjectKeyFrame KeyTime="0" 
                                              Value="{StaticResource OrangeBrush}" />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <ContentPresenter />
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

If you have read the part about styling the buttons, you can see that the concept is the same. For some of the states in which the item can be at a time, I am changing the background color of the container. There is only one difference in contradiction to styling the buttons. The buttons do not contain the ContentPresenter tag, because there is no need to put anything inside of the button. However, here we have just styling for the container and the content of each list item will be different. It will be the ItemTemplate which will be placed into the ContentPresenter. The ItemTemplate specifies the DataTemplate for each list item.

XML
<Grid Width="395" Background="Transparent" HorizontalAlignment="Stretch">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="95"/>
        <ColumnDefinition Width="25"/>
        <ColumnDefinition Width="210"/>
        <ColumnDefinition Width="65"/>
    </Grid.ColumnDefinitions>

    <StackPanel Orientation="Horizontal">
        <Image Source="/Icons/Others/ClockWhite.png/" 
               Height="30" Width="30"/>
        <TextBlock Text="{Binding TotalTime}"/>
        <TextBlock Text=" min"/>
    </StackPanel>
    <Image Source="/Icons/Others/NextWhite.png" 
            Height="25" Width="25" Grid.Column="1"/>
    <TextBlock Text="{Binding Path=To.Address}" Grid.Column="2"/>
    <StackPanel Grid.Column="3" Orientation="Horizontal">
        <Image Source="/Icons/Others/HouseWhite.png" 
               Height="30" Width="30" />
        <TextBlock Text="{Binding Path=To.FreePlaces}"/>
    </StackPanel>
</Grid>

The application bar

In order to allow the user to perform some action, the easiest way is to use the Application Bar - the semi-transparent panel in the bottom of the phone's display. Its XAML declaration is actually already commented in the template provided by Visual Studio. You can put a maximum of four buttons directly to the panel, and you also have the possibility to put the menu items below these buttons. I have here just two buttons with the following functions:

  • Button to get the directions (from one bike station to stations near the entered address).
  • Button to get the nearest stations to the actual position provided by the phone's GPS.
XML
<phone:PhoneApplicationPage.ApplicationBar>
    <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True" Opacity="0.8">
        <shell:ApplicationBarIconButton 
           IconUri="/Icons/ApplicationBar/Directions.png" 
           Text="Directions" Click="GetDirections_Click"/>
        <shell:ApplicationBarIconButton 
           IconUri="/Icons/ApplicationBar/Location.png" 
           Text="Here" Click="ShowNearStations_Click"/>
    </shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

Handling user's actions

This chapter generally describes the actions that are taken when the user presses one of the ApplicationBar buttons.

Getting the nearest stations

The ShowNearStations_Click method which is the event handler for the first application bar button will just set the current position to the actual position of the GeoCoordinateWatcher and call the ShowNearStations method. This method takes as the parameter the current location.

C#
this.DepartureStations = GetNearStations(location, BikeConst.ANGLE_DISTANCE);
this.map.SetView(location, BikeConst.ZOOM_DETAIL);
this.StationPanel.Visibility = System.Windows.Visibility.Visible;

//deselecte the currently selected station
if (this.CurrentStation != null)
{
  this.CurrentStation.IsSelected = false;
}

//selecte on of the new stations
if (this.DepartureStations.Count > 0)
{
  this.CurrentStation = this.DepartureStations[0];
  this.CurrentStation.IsSelected = true;
}

In this method, first we obtain the stations which are near the desired location, and then we zoom to the station by using the map's SetView method. After that, we just assure that one of the stations will be selected to show the information about it. Let's observe now the GetNearStations method which is in fact the most important one.

C#
private ObservableCollection<bikestation> 
        GetNearStations(GeoCoordinate coordinate, double distance)
{
  ObservableCollection<bikestation> collection = 
            new ObservableCollection<bikestation>();
  
  if (this.Stations != null)
  {

    double lat = coordinate.Latitude;
    double lng = coordinate.Longitude;

    var stationList = from s in this.Stations
                      where (Math.Abs(s.Location.Latitude - lat) < 
                             distance & Math.Abs(s.Location.Longitude - lng) 
                             < distance)
                      select s;


    foreach (BikeStation station in stationList)
    {
      station.WalkDistance = 
              GeoMath.ComputeDistance(station.Location, coordinate);
    }

    var result = stationList.Where(x => x.WalkDistance < 400);

    foreach (BikeStation station in result)
    {
      collection.Add(station);
    }
    
    //get the information just for the closest stations
    foreach (BikeStation station in collection)
    {
      _serviceCaller.GetStationInformation(station);
    }
  }
  return collection;
}

Here we use LINQ to get the closest stations. We select stations of which latitude and longitude are not too "far away" from the actual location. This in fact will not give us stations in a circular distance but rather all stations in a square around the actual location.

To compute the exact distance of each station in this collection, we call the ComputeDistance method which uses the spherical law of cosines to compute the distance.

When all the distances are computed, then the Where method is used to reduce the collection only to those stations where the distance from the place is lower than 400 meters.

The reason to use LINQ here was to eliminate the number of stations for which I will have to compute the spherical distance, because it is a costly operation.

To summarize, the GetNearStations method will fill the ObservableCollection with the nearest stations. Later, we will bind the content of this collection to the map.

Getting the directions

Here we call the GeocodeAddress of the ServiceCaller classes which was described above in the chapter dedicated to "Getting the data". Because GeocodeAddress is an asynchronous operation, we have to assign an event handler to perform the actions when the address has been geocoded.

C#
void BikePlaceGeocoded(object sender, AddressGeocodedEventArgs e)
{
  this.CurrentStation.Routes.Clear();
  this.CurrentRoute = null;

  //set the destination place
  this.Arrival = e.Location;

  //retrieve the stations arround destination place
  this.ArrivalStations =  
       GetNearStations(this.Arrival, BikeConst.ANGLE_DISTANCE);

    
  foreach (BikeStation destination in this.ArrivalStations)
  {
    BikeStation[] list = { this.CurrentStation, destination };
    BikeRoute route = new BikeRoute();
    route.From = this.CurrentStation;
    route.To = destination;
    

    //add item to collection, keep just couple
    //fastest routes in the collection
    this.CurrentStation.Routes.Add(route);

    //call the web service to calculate the directions of the route
    _serviceCaller.CalculateRoute(route);
  }
}

In the event handler, we will first clear the Routes collection of the current stations, to erase any routes which might be there from previous searches, and also deselect the current route. In AddressGeocodedEventArgs, we obtain the location of the station, which is assigned to the Arrival property. We call the GetNearStations method this time to obtain all the arrival stations.

Then a loop over all arrival stations creates a new BikeRoute for each route from the actually selected station to the arrival station. In this loop, the CalculateRoute method of ServiceCaller is called which asks the Bing services to get the driving directions. When the directions are obtained, the BikeRoute object will be altered.

Visualizing on map

Here we assume that we have all the data in the properties of the MainPage class and we just have to show it on the map.

Visualizing the user location

Let's start by visualizing the current position and the destination position. To visualize a single point, the Pushpin component is used.

XML
<map:map>
  <map:Pushpin Location="{Binding Departure}" 
         Style="{StaticResource PlaceMarkStyle}"/>
  <map:Pushpin Location="{Binding Arrival}" 
         Style="{StaticResource PlaceMarkStyle}"/>
</map:map>

We have two Pushpins with Location properties bounding the Departure and Arrival properties of the MainPage class, both of which are of type GeoCoordinate. Both of these components use the PlaceMarkStyle.

PlaceMark.JPG

XML
<Style x:Key="PlaceMarkStyle" TargetType="map:Pushpin">
  <Setter Property="Template">
      <Setter.Value>
          <ControlTemplate>
              <Canvas>
                  <Path Width="16" Height="15" 
                        Canvas.Top="13" Stretch="Fill" 
                        Stroke="#FF000000" Fill="#FF000000" 
                        Data="F1 M 8,28 L 0,16L 16,16L 8,28 Z "/>
                  <Rectangle Width="6" Height="13" 
                         Canvas.Left="5" Stretch="Fill" 
                         Stroke="#FF000000" Fill="#FF000000"/>
                  <Canvas.RenderTransform>
                      <CompositeTransform TranslateX="-16" 
                                TranslateY="-14"/>
                  </Canvas.RenderTransform>
              </Canvas>
          </ControlTemplate>
      </Setter.Value>
  </Setter>
</Style>

The style overrides the control template from the standard to a little arrow pointing to the location. The arrow is composed of two objects: a rectangle and a triangle being the top of the arrow. What is important here is that the default relative point when zooming is the lower left corner of the Pushpin. That is why the RenderTransform is used which translates the Pushpin to the lower left corner of the component.

Visualizing the stations

Because there can be several stations on the map at the same time, the MapItemsControl object is used which serves for visualizing several objects of the same type on the map.

XML
<map:MapItemsControl ItemTemplate="{StaticResource StationTemplate}"
                     ItemsSource="{Binding DepartureStations}"/>
<map:MapItemsControl ItemTemplate="{StaticResource StationTemplate}"
                     ItemsSource="{Binding ArrivalStations}"/>

Station.JPG

The ItemSource properties are bound respectively to the DepartureStations and ArrivalStations collections - basically saying that we want to visualize all the stations in these two collections. This time, we do not apply a style but rather create a new DataTemplate which will be applied to all the items in the collection. We want to show each station as a customized Pushpin. This time the Pushpin customization is a little more complicated because the Pushpin has to show the detailed information about the station when it is selected.

XML
<DataTemplate x:Key="StationTemplate">
    <map:Pushpin Location="{Binding Location}" 
               MouseLeftButtonDown="Pushpin_MouseLeftButtonDown">
        <map:Pushpin.Template>
            <ControlTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                    </Grid.ColumnDefinitions>
                    <Canvas VerticalAlignment="Bottom">
                        <Ellipse x:Name="Ellipse" Width="35" Height="35" 
                                 Stretch="Fill" StrokeThickness="4" Stroke="Black" 
                                 Fill="{Binding IsSelected,Converter=
                                       {StaticResource BoolToBrush}"/>
                        <Path x:Name="Path" Width="16" 
                              Height="27" Canvas.Left="10" 
                              Canvas.Top="30" Stretch="Fill" 
                              StrokeThickness="3" StrokeLineJoin="Round" 
                              Stroke="Black" Fill="Black" 
                              Data="F1 M 35,41L 23,81L 11,41"/>
                        <Canvas.RenderTransform>
                            <CompositeTransform TranslateX="-17.5" 
                                TranslateY="-30.5"/>
                        </Canvas.RenderTransform>
                    </Canvas>
                    
                    <Border Background="Black" Opacity="0.8" 
                            Grid.Column="1" Margin="20,0,0,0"
                            Visibility="{Binding IsSelected, 
                                        Converter={StaticResource BootToVisibility}}"
                            HorizontalAlignment="Center">
                        <StackPanel>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition/>
                                    <ColumnDefinition/>
                                </Grid.ColumnDefinitions>
                                <StackPanel HorizontalAlignment="Left">
                                    <Image Source="/Icons/Others/BicycleWhite.png" 
                                          Height="30" Width="30"/>
                                    <Image Source="/Icons/Others/HouseWhite.png" 
                                          Height="30" Width="30" />
                                </StackPanel>
                                <StackPanel Margin="3" Grid.Column="1">
                                    <TextBlock Text="{Binding Path=Free}"/>
                                    <TextBlock Text="{Binding Path=FreePlaces}"/>
                                </StackPanel>
                            </Grid>
                        </StackPanel>
                    </Border>
                </Grid>
            </ControlTemplate>
        </map:Pushpin.Template>
    </map:Pushpin>
</DataTemplate>

The pushpin is composed of a grid with two columns. The left column contains the actual marker build of on Ellipse and a Path object, and the right column contains the information about the object. Again, this time we have to use the render transform to translate the Pushpin to the lower left corner. This DataTemplate is defined directly in the MainPage class because it has a MouseLeftButtonDown event wired to a method which is part of this class. In this method, we are just changing the currently selected BikeStation to the one on which the user clicks.

C#
private void Pushpin_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
  if (this.CurrentStation != null)
  {
    this.CurrentStation.IsSelected = false;
  }

  BikeStation station = (BikeStation)((Pushpin)sender).DataContext;
  station.IsSelected = true;
  this.CurrentStation = station;
}

In the DataTemplate, two dependency properties depend on the IsSelected property of the BikeStation: the Visibility of the right column containing the detailed information and also the color of the pushpin. Thus the boolean value has to be converted to the appropriate type. In my application, I have used the Generic Boolean to Value Converter idea which I found on the blog of Anthony Jones, so all credit for this goes to him.

Visualizing the routes

The last items to be placed on the map are the routes. I have decided to always visualize only the selected route which is in the CurrentRoute property. This time we use the MapPolyline component. This component has its Location property bound to the Locations property of BikeRoute which is of type LocationCollection.

XML
<map:MapPolyline Locations="{Binding Path=CurrentRoute.Locations}" 
                 Stroke="Black" StrokeThickness="3"/>

Tombstoning

Tombstoning is the name for the process of saving an application's state each time the application is either being closed or only deactivated. You have to implement tombstoning because WP7 will only allow one application to be running at one time (with the exception of some "Choosers" and "Launchers"). So when the user, for example, receives a phone call, the application has to save its current state and after the user finishes, come back and pretend that nothing happened. Furthermore, even when the user closes the application correctly, the next time he will open it, maybe he should see it as before closing.

General explanation

In order to explain it correctly, I have drawn the following diagram. There are basically three states for your application and several events which allow transitions between these states.

StateDiagram.jpg

In your App.xaml.cs file, there are already empty event handlers for all of these events - so basically, waiting for the code to be written and implement tombstoning.

When the user launches the application, it gets to its "Running" state. Then we have two possibilities. Either the user closes the application correctly by clicking the "back" button, or he will receive a call, start a search, and take a picture or perform any other interrupting activity. If that happens, we have to tombstone the application - save its current state. After the application is "tombstoned" in its deactivated state, there are again two possibilities. Either the user will press the "back" button and come back to the application directly, or he will go to the menu and launch the application again. We should implement the tombstoning so that the previous two scenarios will end up the same.

General approach description

We will wrap all the data which needs to be stored to a data model class. This class in my case is called BikeSituation and contains all the stations and routes visualized on the map. After that, this class can be persisted. When the application is closed, this class will be serialized and stored in IsolatedStorage. When the application is just deactivated, this class will be stored in the PhoneApplicationService.Current.State dictionary. The PhoneApplicationService class is dedicated to managing the application life cycle.

Implementation

The first important step is to wrap the stations and the selected route to a data class called BikeSituation. We can also add the "Zoom" property if we want to persist the zoom.

C#
public class BikeSituation implements INotifyPropertyChanged{
    private ObservableCollection<bikestation> _arrivalStations;
    private ObservableCollection<bikestation> _departureStations;
    private BikeCoordinate _departure;
    private BikeCoordinate _arrival;
    private BikeRoute _currentRoute;
    private BikeStation _currentStation;
    private double _zoom;

    public ObservableCollection<bikestation> ArrivalStations
    {
      get
      {
        if (_arrivalStations == null)
        {
          _arrivalStations = new ObservableCollection<bikestation>();
        }
        return _arrivalStations;
      }
      set
      {
        _arrivalStations = value;
        OnPropertyChanged("ArrivalStations");
      }
    }
    //all other public properties
}

Now when this class is ready, the MainPage will contain only this class - representing the current situation on the map. The BikeSituation object can be stored directly in the DataContext property.

C#
public class MainPage{
...
    public BikeSituation Situation
    {
      get
      {
        return (BikeSituation)this.DataContext;
      }
      set
      {
        this.DataContext = value;
      }
    }
}

So now let's start to implement the methods in App.xaml.cs. I will start with the description of the Application_Closing method.

C#
private void Application_Closing(object sender, ClosingEventArgs e)
{
  //when closing the application save to situation to isolated file storage
  MainPage page = RootFrame.Content as MainPage;
  BikeSituation situation = page.Situation;
  SaveLocalCopy(situation);
}

Here, we first get the current situation, and then a method to serialize the situation to the local IsolatedStorage is called.

C#
private void SaveLocalCopy(BikeSituation situation)
{
  Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
  using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
  {
    using (IsolatedStorageFileStream fs = isf.CreateFile("Situation.dat"))
    {
      XmlSerializer ser = new XmlSerializer(typeof(BikeSituation));
      ser.Serialize(fs, situation);
    }
  }
}

This method will be used again later while performing the deactivation. IsolatedStorage classes are used here, which implement IDisposable so we should use the using directive to dispose the resources when we are finished.

Now let's take a look at Application_Deactivated. This method is an event handler which gets fired when the user interrupts the application (phone call, search...).

C#
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
  MainPage page = RootFrame.Content as MainPage;
  BikeSituation situation = page.Situation;
  SaveLocalCopy(situation);
  if (situation != null)
  {
    if (PhoneApplicationService.Current.State.ContainsKey("Situation"))
    {
      PhoneApplicationService.Current.State.Remove("Situation");
    }
    PhoneApplicationService.Current.State.Add("Situation", situation);
  }
}

Here we obtain the situation and perform the Save as in the "Closing" method. But in addition, we also store this situation object to the application state dictionary. If the application was interrupted and then the user comes back to the application, we can load the data object directly from PhoneApplicationService.Current.State. If he will instead go to the menu and start the application again, we will still have the copy of the latest situation in the IsolatedStorage.

Now my Application_Activated event handler stays empty - and that is for a reason. If there is a data object in the dictionary, then I perform the necessary actions in the constructor.

C#
private voeid Application_Activated(object sender, ActivatedEventArgs e)
{
}

The last piece of the puzzle from the App file is the Application_Launching event handler which gets called when the user launches the application (from the menu or application tile).

C#
private void Application_Launching(object sender, LaunchingEventArgs e)
{
  BikeSituation situation = new BikeSituation();
  try
  {
    using (IsolatedStorageFile isf = 
           IsolatedStorageFile.GetUserStoreForApplication())
    {
      if (isf.FileExists("Situation.dat"))
      {
        XmlSerializer ser = new XmlSerializer(typeof(BikeSituation));
        object obj = ser.Deserialize(isf.OpenFile("Situation.dat", 
                                     System.IO.FileMode.Open)) as BikeSituation;
        if (obj != null && obj is BikeSituation)
        {
          situation = obj as BikeSituation;
          PhoneApplicationService.Current.State.Add("Situation", situation);
        }
      }
    }
  }
  catch (Exception ex)
  {     //LOG your exceptions }
}

This is somewhat interesting. If there is a data object in the IsolatedStorage, than I will load the object and place it in the PhoneApplicationService.Current.State dictionary. This way, the "situation" will be loaded in the constructor. To complete the explanation, here is the part of the constructor which loads the BikeSituation object from the application state dictionary and places it in the this.Situation property. Remember that this property is mapped directly to the DataContext of the MainPage class.

C#
public void MainPage(){
   ...
   //if there is some Situation in the State of the application
   if (PhoneApplicationService.Current.State.ContainsKey("Situation"))
   {        
     this.Situation = 
       PhoneApplicationService.Current.State["Situation"] as BikeSituation;
     PhoneApplicationService.Current.State.Remove("Situation"); 
   }
   else
   {
     this.Situation = new BikeSituation();
   }
}

You can see that if there is no object in the dictionary (definitely the first time the application runs), than we create a new BikeSituation and assign it to the DataContext.

Some details

  • Storing objects to the IsolatedStorage is slower than storing in the application state. IsolatedStorage is done on the phone's hard disk where the application state stays in memory. If you have some large data, you should think whether it is necessary to store it in the IsolatedStorage.
  • XMLSerialization can be tricky. Here are three issues which I have encountered:
    • Circular references - In my example, I have a BikeSituation which contains BikeRoutes starting from a station and later the BikeRoute object itself is composed of two BikeStation objects (starting and ending point stations of the route). This results in an error during the serialization. You can use [XMLIgnore] to one of the properties to avoid this error.
    • GeoCoordinate serialization - I kept getting a FormatException while serializing the GeoCoordinate inside of BikeStation and BikeSituation classes. I am not sure what was the cause - I ended up writing my own class "BikeCoordinate" which contains just the latitude and longitude and it serializes just fine. This means that later Converters are needed to bind the map objects (Pushpins) to these new coordinates types. All of this is presented in the source code.
    • One object deserialized into two different objects: In the BikeSituation, I have the CurrentStation property which contains a reference to just one of the Stations in the DepartureStations collection. However, serialization and later deserialization will result in two different objects: one in the collection and the other in the CurrentStation property. There are several ways to avoid it, for example, ignore the CurrentStation property during the serialization and then setting to it the right reference to the object from the collection.
  • 5 second rule - Note that if you want to pass the certification process, your application should start within 5 seconds. So generally avoid storing large objects to the IsolatedStorage during the tombstoning process.

Technical tips

When playing with Bing Maps, I discovered some interesting things, let me share them here:

  • Bing Maps for Silverlight and Bing Maps for WP7 are different. To be specific, the namespaces used are different so it is little bit harder to reuse some code. I thought that I will be able to reuse the Model from my previous Bing Maps Silverlight, but it is not possible. For example, the Location and LocationCollection classes are present in both APIs but they are not the same. So if you would like to reuse the model of your phone and web application, you should probably store the locations as double values and then use converters to convert them to the desired type.
  • You cannot bind the Stroke property of the MapPolyline component. This property is not a Dependency Property - I discovered that when I wanted to show more routes on the map and assign different colors to each of them. Here the binding is not possible, however you can override the Loaded event and assign the color when the MapPolyline object is added to the map. This assumes that the color will not change.

Summary

I tried here to show how to build an application which visualizes data on the Bing Map component on WP7. This time, the data is coming from the bike sharing system of Rennes, specifically from its Web Services. However, the same way, you could visualize any other geo data from any other source.

In order to keep it coherent, I decided to describe all the applications, so maybe for some readers, there is a lot of general Silverlight knowledge here which they already possess - on the other hand, I hope it will be useful for Silverlight beginners (like me).

Also please note that right now, I don't have a device to try it on, so I am not sure about the performance on the actual device.

There are lots of further improvements that can be done: allow user to search nearby stations of any location, speed up the GUI by processing the asynchronous callback on separate threads. I will continue to work on it and update the article in future.

This is my first real article so any feedback will be highly appreciated.

History

  • 12/4/2010 - First version.
  • 12/23/2010 - Tombstoning implemented and description added.
  • 1/5/2011 - Added the link to the online version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Junior) OCTO Technology
Czech Republic Czech Republic
Writing software at ITG RFQ-hub.
LinkedIn
Blog
GitHub
Articles at OCTO blog

Comments and Discussions

 
QuestionWeb Service substitute? Pin
Laurentiu LAZAR14-Oct-12 23:54
Laurentiu LAZAR14-Oct-12 23:54 
QuestionNeed one info regarding class diagram Pin
Tridip Bhattacharjee25-Sep-12 20:26
professionalTridip Bhattacharjee25-Sep-12 20:26 
QuestionSuperb Pin
Dave Kerr15-Feb-12 22:18
mentorDave Kerr15-Feb-12 22:18 
GeneralMy vote of 5 Pin
yumn20044-Mar-11 15:49
yumn20044-Mar-11 15:49 
GeneralCongratulations for winning the December WP7 contest Pin
Katka Vaughan22-Jan-11 23:15
Katka Vaughan22-Jan-11 23:15 
General5 from me Pin
Jeff Circeo9-Jan-11 7:17
Jeff Circeo9-Jan-11 7:17 
GeneralRe: 5 from me Pin
hoonzis11-Jan-11 5:58
hoonzis11-Jan-11 5:58 
GeneralExcellent Article Pin
The Manoj Kumar7-Jan-11 6:23
The Manoj Kumar7-Jan-11 6:23 
GeneralRe: Excellent Article Pin
hoonzis11-Jan-11 5:58
hoonzis11-Jan-11 5:58 
GeneralMy vote of 5 Pin
Member 40123927-Jan-11 2:22
Member 40123927-Jan-11 2:22 
GeneralRe: My vote of 5 Pin
hoonzis11-Jan-11 5:57
hoonzis11-Jan-11 5:57 
GeneralVery interesting article - and maybe some info you might be interested in [modified] Pin
Bogdan Zamfir7-Jan-11 1:45
Bogdan Zamfir7-Jan-11 1:45 
GeneralRe: Very interesting article - and maybe some info you might be interested in Pin
hoonzis11-Jan-11 5:56
hoonzis11-Jan-11 5:56 
GeneralMy vote of 5 Pin
Kunal Chowdhury «IN»6-Jan-11 3:27
professionalKunal Chowdhury «IN»6-Jan-11 3:27 
GeneralRe: My vote of 5 Pin
hoonzis6-Jan-11 11:33
hoonzis6-Jan-11 11:33 
GeneralMy vote of 5 Pin
SledgeHammer015-Jan-11 12:59
SledgeHammer015-Jan-11 12:59 
GeneralRe: My vote of 5 Pin
hoonzis6-Jan-11 11:32
hoonzis6-Jan-11 11:32 
GeneralMy vote of 5 Pin
Luc Pattyn5-Jan-11 12:34
sitebuilderLuc Pattyn5-Jan-11 12:34 
GeneralRe: My vote of 5 Pin
hoonzis6-Jan-11 11:31
hoonzis6-Jan-11 11:31 
GeneralAwesome Pin
Marcelo Ricardo de Oliveira3-Jan-11 4:59
mvaMarcelo Ricardo de Oliveira3-Jan-11 4:59 
GeneralRe: Awesome Pin
hoonzis5-Jan-11 12:02
hoonzis5-Jan-11 12:02 
GeneralMy vote of 5 Pin
Member 425698326-Dec-10 22:52
Member 425698326-Dec-10 22:52 
GeneralRe: My vote of 5 Pin
hoonzis5-Jan-11 12:01
hoonzis5-Jan-11 12:01 
GeneralMy vote of 5 Pin
RaviRanjanKr25-Dec-10 18:37
professionalRaviRanjanKr25-Dec-10 18:37 
GeneralRe: My vote of 5 Pin
hoonzis26-Dec-10 7:19
hoonzis26-Dec-10 7:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.