Bike In City with Windows Phone 7
Contents
Contents
- Contents
- Introduction
- Background
- Architecture
- Data Model
- Using and emulating GPS
- Computing distance on Earth’s surface
- Getting the data
- Getting information from the bike system
- Getting list of all stations
- Getting the details of a station
- Using Bing Maps Services
- Preparing the GUI
- Handling users actions
- Visualizing on map
- Technical tips
- Summary
- History
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 using the Bing Map control. After some research I have 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 the information about them (number of free bikes, number of places to put the bike).
- When user selects one station he can also compute directions to other stations near to entered destination address.
Background
I got the idea of building this application came to me about 2 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 is not working really 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 thinks around to fit them all on the screen.
In the future I am planning to consolidate these two application so that they could share as much code as possible.
This article is not a complete walkthrough - it will not lead you step by step to build the application however I am trying to give as much detailed description of the application as I can. I hope that after reading everybody could understand how the application was build and how it works.
I hope to provide you with some useful information about the 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 have some basics of Silverlight or WPF.
Architecture
I tried to keep the application as simple as possible so it is composed of just few classes. The main class is MainPage class which contains all the data which is visualized. It uses the data model described lower (just two classes). To talk to the web services the MainPage makes use of ServiceCaller class. Service caller provides methods to access web services and events which get fired when the results had been obtained and processed. The 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.

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 as the whole project make use of GeoCoordinate class from System.Device.Location namespace. This class represents a location defined by its latitude and longitude. This class contains also some other properties like Course or Speed which are not used in this application. To store the location we can also use the Location class from Microsoft.Phone.Controls.Maps namespace. This class does not contain these additional information and is part of the Bing Maps API. There is automatic conversion from Location to GeoCoordinate but not the other way around.
BikeStation holds all the information about one bike rental place. This is basically the address, location, number of total slots and number of free bikes. There is also ObservavleCollection
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 _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.
BikeRoute class simply represents one 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.
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 is navigates after the start of the application.
Public class MainPage:PhoneApplicationPage, INotifyPropertyChanged{
private BikeStation[] _stations;
public ObservableCollection DepartureStations
{
get
{
if (_departureStations == null)
{
_departureStations = new ObservableCollection();
}
return _departureStations;
}
set
{
_departureStations = value;
OnPropertyChanged("DepartureStations");
}
}
public ObservableCollection 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. Arrival stations collection is filled only when the user searches a route. When the user searches for nearest stations to his actual position then these stations are stored in the DepartureStations collection.
Departure and Arrival properties of type GeoCoordinate serve to visualize on the map the current position and the location of destination place.
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 to his location.
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 GeoCoordinateWatcher class. This class is an API to the phone GPS and contains a property Position of type GeoPosition
GeoCoordinateWatcher _watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.Default);
_watcher.MovementThreshold = 20;
_watcher.PositionChanged+= new EventHandler>(_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.
_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
public class GeoCoordinateSimulator : IGeoPositionWatcher
{
//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 _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. 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 of some checks to assure that the corner points are in good order the position is set to the middle of the city.
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(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 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 generate randomly a new direction to stay in the city.
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(DateTime.Now, new GeoCoordinate(newLatitude,newLongitude));
//fire the event if there are any subscribers
if (this.PositionChanged != null)
{
PositionChanged(this, new GeoPositionChangedEventArgs(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 than the PositionChanged event will fire to let the observers now about the change. There is a call to IsInRange() method which just checks if the given point is still in the rectangle of the city.
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.
//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 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 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 class GeoMath.
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 describe 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 the access to all of the data stores. More specifically it provides methods which asynchronously call the web services and return results to MainPage.
Getting information from the bike system
The data that I need is accessible via web service provided by the Rennes city. To obtain access to this data you have to register at http://data.keolis-rennes.com/ as developer. On this address you will find also the documentation of the REST API. After registering you will obtain a developers 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 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 a cmd will be command describing your operation.
Getting list of all stations
To get the list of all station 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 will be downloaded ServiceCaller will fire StationsLoaded event. Before we call for the data we register a method which will be executed when the download process is completed.
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 xml which is not hard to parse using LINQ with following structure.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<opendata>
<request>http://data.keolis-rennes.com/xml/?version=1.0&key=1T0DZW0R6JZVGA6&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 is in the form of XML so we can you 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.
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.
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 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 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 we will create a Guid for the BikeStation and store it in a dictionary. Then we will call DownloadStringAsync method with 2 parameters, passing the Guid as 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. Method GetStationInformation(BikeStation) shows how to call the web service.
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 parameter “number” corresponding to the ID of the station.
The data that we will receive will be a 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.
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 Trainings Kit – so here 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 service accessible over 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 reference pointing to the right URL. The list of URL's of Bing Maps SOAP services can be found on this site. If you will have any trouble getting access to the services you can consult this page which is general article describing how to develop Silverlight application interacting with Bing Maps SOAP services.
.Geocoding address
Here I can use the 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 the standard Http binding, however there is also secure binding using SSL accessible.
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( 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 the Bing services we will fire BikePlaceGeocoded event and we pass the geocoded GeoCoordinate object as the argument of this event in order to deliver it to MainPage class.
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 of the AddressGeocodedEventArgs.
public class AddressGeocodedEventArgs : EventArgs
{
public GeoCoordinate Location {get;set;}
public AddressGeocodedEventArgs(GeoCoordinate c, State s)
{
this.Location = c;
this.StateType = s;
}
}
Calculating 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.
public void CalculateRoute(BikeRoute route)
{
RouteServiceClient routeClient = new RouteServiceClient("BasicHttpBinding_IRouteService");
routeClient.CalculateRouteCompleted += new EventHandler(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 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 RouteOptions object we specify that we want directions for Driving and minimize the distance. That should give us a good results for biking (well even that sometimes we think that on the bike we can do everything we should obey the 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.
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 RoutePath.Point collection of the Result. We add them to the Locations properties 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 starting creating the GUI it’s good to know, that there is 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.
<SolidColorBrush x:Name="LimeBrush" Color="#8CBF26"/>
<SolidColorBrush x:Name="OrangeBrush" Color="#F09609"/>
Now 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.
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 binded to the Zoom property which is exposed on the MainPage class, the same counts for CredentialsProvider. The CredentialsProvider property should contain your bing maps developer key.
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 in the ones which are used in the official WP7 Training Kit and to for one simple reason – they look better than anything I came up with, so I took them as a starting point and I just simplified the styling a bit. So here is Zoom-In-Button.
<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 separated DefaultStyles.xaml file.I will describe the style here in little details because the same type of style is used for other buttons in the project.
<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 is overriding the Template property of the button. We are defining a new ControlTemplate which contains one Grid with two images inside (one is overlying the other). We are using VisualStates to set the Visibility of the top picture to Collapsed and hence show the Image bellow when the user presses the button.
If you are not familiar with the concept of VisuaStates, you can look at them as declarations which describe how the component looks like in an exact state. VisualStates are implemented only in Silverlight and work as Triggers and they can be seen as replacement of 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 way we can say that the concept is less powerful than the Triggers(where the template designer has more of a freedom and is not tied by a group of predefined states).
Here inside the Grid we have a VisualStateManager which contains one group of states “CommonStates”. This is a predefined group for the Button component and contains for states: Normal, MouseOver, Pressed and Disabled. I am interested only in the Normal and Pressed state. By leaving the Normal state without further changes I am declaring that I don’t want any changes to look of 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 to us perform changes of a Property of a Component on some describe 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 2 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.
<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 – one Border component with a horizontal 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.

<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 a Opacity set to 0.8 so the map underneath can be seen. Over this Border a StackPanel is placed which has DataContext property bound CurrentStation property of 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 CurrentStation property.
The route list has its ItemsSource bounded to the Routes property of the BikeStation class. The list has ItemContainerStyle set as well as ItemTemplateStyle. 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.
<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>
I 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 did not contain ContentPresenter tag, because there was 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 ContenPresenter. The ItemTemplate specifies the DataTemplate for each list item.
<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 Application Bar – the semi transparent panel in the bottom of the phones display. Its xaml declaration is actually already commented in the template provided by Visual Studio. You can put maximally 4 buttons directly to the panel and also you have the possibility to put menu items bellow this buttons. I have here just two buttons with following functions:
<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 users 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.
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 of the desired location and then we zoom to the station by using the maps 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.
private ObservableCollection GetNearStations(GeoCoordinate coordinate, double distance)
{
ObservableCollection collection = new ObservableCollection();
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 is 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 to 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 fir here was to eliminate the number of stations for each 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 the GeocodeAddress is asynchronous operation we have to assign an event handler to perform actions when address has been geocoded.
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 the 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 arrival stations.
Then a loop over all arrival stations creates a new BikeRoute for each route from 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 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. Two visualize a single point the Pushpin component is used.
<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 bounded the Departure and Arrival property of MainPage class, which both are of type GeoCoordinate. Both of these components use the PlaceMarkStyle.
<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.
<map:MapItemsControl ItemTemplate="{StaticResource StationTemplate}"
ItemsSource="{Binding DepartureStations}"/>
<map:MapItemsControl ItemTemplate="{StaticResource StationTemplate}"
ItemsSource="{Binding ArrivalStations}"/>
The ItemSource properties are bound respectively to the DepartureStations and ArrivalStations collection – basically saying that anytime we want to visualize all the stations in these two collections. This time we do not apply a style but rather create 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 little more complicated because the Pushpin has to show the detail information about the station when it is selected.
<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 one Path object and the right column contains the information about the object. Again this time we have to use the render transform to put 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 s part of this class. In this method we are just changing the currently selected BikeStation to the one on which the user clicks.
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. It is the Visibility of the right column containing detail information and also the color of the pushpin. Thus the Boolean value has to be converted to appropriate type. In my application I have used the Generic Boolean to Value Converter idea which I have found on the blog of Anthony Jones, so all credit for this goes to him.
Visualizing the routes
Last items to be placed on the map are the routes. I have decided to visualize always only the selected route which is in the CurrentRoute property. This time we use MapPolyline component. This component has its Location property bound to the Locations property of BikeRoute which is of type LocationCollection.
<map:MapPolyline Locations="{Binding Path=CurrentRoute.Locations}" Stroke="Black" StrokeThickness="3"/>
Technical tips
During playing with the Bing maps I have discovered some interesting things so let me share:
- 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 desired type.
- You cannot bind the Stroke property of 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.
- GeoCoordinate and Location are problematic to serialize. I am doing some further work on this application specially to implement the tombstoning process however when I serialize my BikeRoute and BikeStation objects I obtain FormatException during the deserialization. I know that it is caused by Location and GeoCoordianete classes however I still don’t know why. So probably they are not a good choice for the model and in the model I should just store the latitude and longitude coordinates as doubles.
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 decide to describe all the application, so maybe for someone there is lot of general Silverlight knowledge which they already possess – on the other hand I hope it will be useful for the 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: implement the tombstoning, allow user to search near stations of any location, speed up the GUI by processing the asynchronous callback on separated threads. I will continue working on it and update the article in the future.
This is my first real article so any feedback will be really highly appreciated.
History
12/4/2010 - First version