This chapter is taken from the book "Programming Windows Phone 7" by Charles Petzold published by Microsoft Press http://www.charlespetzold.com/phone/index.html
Windows Phone 7 provides information about the outside world. With the user's permission, the location service lets your application obtain the phone's location on the earth in the traditional geographic coordinates of longitude and latitude, whereas the accelerometer tells your program which way is down. The accelerometer and location service are related in that neither of them will work very well in outer space.
Accelerometer
Windows Phones contain an accelerometer-a small hardware device that essentially measures force, which elementary physics tells us is proportional to acceleration. It is convenient to represent the accelerometer output as a vector in three-dimensional space. Vectors are commonly written in boldface, so the acceleration vector can be symbolized as (x, y, z). XNA defines a three-dimensional vector type; Silverlight does not.
The magnitude of the vector (x, y, z) is calculable from the three-dimensional form of the Pythagorean Theorem.
This is a traditional three-dimensional coordinate system, the same coordinate system used in XNA 3D programming. It's termed a right-hand coordinate system: Point the index finger of your right hand to increasing X, the middle finger to increasing Y, and your thumb points to increasing Z. Or, curve the fingers of your right hand from the positive X axis to the positive Y axis. Your thumb again points to increasing Z.
When the phone is still, the accelerometer vector points towards the Earth. The magnitude is 1, meaning 1 g, which is the force of gravity on the earth's surface. When holding your phone in the upright position, the acceleration vector is (0, -1, 0), that is, straight down.
Turn the phone 90 digs. counter-clockwise (called landscape left) and the acceleration vector becomes (-1, 0, 0), upside down it's (0, 1, 0), and another 90 digs. the counter-clockwise turn brings you to the landscape right orientation and an accelerometer value of (1, 0, 0). Set the phone down on the desk with the display facing up, and the acceleration vector is (0, 0, -1).
In your program, you create an instance of the Accelerometer class, set an event handler for the ReadingChanging event, and call Start.
And then it gets a little tricky. Let's take a look at a project named SilverlightAccelerometer. that simply displays the current reading in its content grid. A centered TextBlock is defined in the XAML file:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<TextBlock Name="txtblk"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
This is a program that will display the accelerometer vector throughout its lifetime, so it creates the Accelerometer class in its constructor and calls Start:
public MainPage()
{
InitializeComponent();
Accelerometer acc = new Accelerometer();
acc.ReadingChanged += OnAccelerometerReadingChanged;
try
{
acc.Start();
}
catch (Exception exc)
{
txtblk.Text = exc.Message;
}
}
The documentation warns that calling Start might raise an exception, so the program protects itself against that eventuality. The Accelerometer also supports Stop and Dispose methods, but this program doesn't make use of them. State property is also available if you need to know if the accelerometer is available and what it's currently doing.
A Reading Changed event is accompanied by the Accelerometer Reading Event Args event arguments. A little background: All the user-interface elements and objects in a Silverlight application are created and accessed in a main thread of execution often called the user interface thread or the UI thread. An instance of this Dispatcher is readily available. The DependencyObject class defines a property named Dispatcher of type Dispatcher, and many Silverlight classes derive from DependencyObject. The Dispatcher class defines a method named CheckAccess that returns true if you can access a particular user interface object from the current thread.
The Silverlight Accelerometer project implements a syntactically elaborate version of the code, but then I'll show you how to chop it down in size. The verbose version requires a delegate and a method defined in accordance with that delegate. The delegate (and method) should have no return value, but as many arguments as you need to do the job, in this case, the job of setting a string to the Text property of a TextBlock:
delegate void SetTextBlockTextDelegate(TextBlock txtblk, string text);
void SetTextBlockText(TextBlock txtblk, string text)
{
txtblk.Text = text;
}
The OnAccelerometerReadingChanged is responsible for calling SetTextBlockText. It first makes use of CheckAccess to see if it can just call the SetTextBlockText method directly. If not, then the handler calls the BeginInvoke method. The first argument is an instantiation of the delegate with the SetTextBlockText method; this is followed by all the arguments that SetTextBlockText requires:
void OnAccelerometerReadingChanged(object sender, AccelerometerReadingEventArgs args)
{
string str = String.Format("X = {0:F2}\n" +
"Y = {1:F2}\n" +
"Z = {2:F2}\n\n" +
"Magnitude = {3:F2}\n\n" +
"{4}",
args.X, args.Y, args.Z,
Math.Sqrt(args.X * args.X + args.Y * args.Y + args.Z * args.Z), args.Timestamp);
if (txtblk.CheckAccess())
{
SetTextBlockText(txtblk, str);
}
else
{
txtblk.Dispatcher.BeginInvoke(new SetTextBlockTextDelegate(SetTextBlockText), txtblk, str);
}
}
The Windows Phone 7 emulator doesn't contain any actual accelerometer, so it always reports a value of (0, 0, -1), which indicates the phone is lying on a flat surface. The program only makes sense when running on an actual phone.
The values here indicate the phone is roughly upright but tilted back a bit, which is a very natural orientation in actual use.
Simple Bubble Level
One handy tool found in any workshop is a bubble level, also called a spirit level. A little bubble always floats to the top of a liquid, so it visually indicates whether something is parallel or orthogonal to the earth, or tilted in some way. The XnaAccelerometer project includes a 48-by-48 pixel bitmap named Bubble.bmp that consists of a red circle.
As with the Silverlight program, you'll need a reference to the Microsoft.Devices.Sensors library and a using directive for Microsoft.Devices.Sensors namespace. The fields in the Game1 class mostly involve variables necessary to position that bitmap on the screen:
namespace XnaAccelerometer
{
public class Game1 : Microsoft.Xna.Framework.Game
{
const float BUBBLE_RADIUS_MAX = 25;
const float BUBBLE_RADIUS_MIN = 12;
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Vector2 screenCenter;
float screenRadius; // less BUBBLE_RADIUS_MAX
Texture2D bubbleTexture;
Vector2 bubbleCenter;
Vector2 bubblePosition;
float bubbleScale;
Vector3 accelerometerVector;
object accelerometerVectorLock = new object();
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
// Frame rate is 30 fps by default for Windows Phone.
TargetElapsedTime = TimeSpan.FromTicks(333333);
}
protected override void Initialize()
{
Accelerometer accelerometer = new Accelerometer();
accelerometer.ReadingChanged += OnAccelerometerReadingChanged;
try
{
accelerometer.Start();
}
catch
{
}
base.Initialize();
}
void OnAccelerometerReadingChanged(object sender, AccelerometerReadingEventArgs args)
{
lock (accelerometerVectorLock)
{
accelerometerVector = new Vector3((float)args.X, (float)args.Y, (float)args.Z);
}
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
Viewport viewport = this.GraphicsDevice.Viewport;
screenCenter = new Vector2(viewport.Width / 2, viewport.Height / 2);
screenRadius = Math.Min(screenCenter.X, screenCenter.Y) - BUBBLE_RADIUS_MAX;
bubbleTexture = this.Content.Load<Texture2D>("Bubble");
bubbleCenter = new Vector2(bubbleTexture.Width / 2, bubbleTexture.Height / 2);
}
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
Vector3 accVector;
lock (accelerometerVectorLock)
{
accVector = accelerometerVector;
}
int sign = this.Window.CurrentOrientation == DisplayOrientation.LandscapeLeft ? 1 : -1;
bubblePosition = new Vector2(screenCenter.X + sign * screenRadius * accVector.Y,
screenCenter.Y + sign * screenRadius * accVector.X);
float bubbleRadius = BUBBLE_RADIUS_MIN + (1 - accVector.Z) / 2 * (BUBBLE_RADIUS_MAX - BUBBLE_RADIUS_MIN);
bubbleScale = bubbleRadius / (bubbleTexture.Width / 2);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Navy);
spriteBatch.Begin();
spriteBatch.Draw(bubbleTexture, bubblePosition, null, Color.White, 0,
bubbleCenter, bubbleScale, SpriteEffects.None, 0);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
OnAccelerometerReadingChanged and Update run in separate threads. One is setting the field; the other is accessing the field. This is no problem if the field is set or accessed in a single machine code instruction.
While that could hardly be classified as a catastrophe in this program, let's play it entirely safe and use the C# lock statement to make sure the Vector3 value is stored and retrieved by the two threads without interruption. That's the purpose of the accelerometerVectorLock variable among the fields.
The LoadContent method loads the bitmap used for the bubble and initializes several variables used for positioning the bitmap.
The Update method safely accesses the accelerometer vector field and calculates the bubble position based on the X and Y components. It might seem like I've mixed up the X and Y components in the calculation, but that's because the default screen orientation is portrait in XNA, so it's opposite the coordinates of the acceleration vector. Because both landscape modes are supported by default, it's also necessary to multiply the acceleration vector values by -1 when the phone has been tilted into the LandscapeRight mode.
The program doesn't look like much and is even more boring running on the emulator. Here's an indication that the phone is roughly upright and tilted back a bit.
Geographic Location
Windows Phone 7 program can obtain the geographic location of the phone using a technique called Assisted-GPS or A-GPS. The most accurate method of determining location is accessing signals from Global Positioning System (GPS) satellites. However, GPS can be slow. It doesn't work well in cities or indoors, and it's considered expensive in terms of battery use. To work more cheaply and quickly, an A-GPS system can attempt to determine the location from cell phone towers or the network. These methods are faster and more reliable, but less accurate.
Although the System.Device.Location namespace includes classes that use the geographic coordinates to determine civic address (streets and cities), these are not implemented in the initial release of Windows Phone 7.
The XnaLocation project simply displays numeric values:
namespace XnaLocation
{
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont segoe14;
string text = "Obtaining location...";
Viewport viewport;
Vector2 textPosition;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
// Frame rate is 30 fps by default for Windows Phone.
TargetElapsedTime = TimeSpan.FromTicks(333333);
}
protected override void Initialize()
{
GeoCoordinateWatcher geoWatcher = new GeoCoordinateWatcher();
geoWatcher.PositionChanged += OnGeoWatcherPositionChanged;
geoWatcher.Start();
base.Initialize();
}
void OnGeoWatcherPositionChanged(object sender, GeoPositionChangedEventArgs<GeoCoordinate> args)
{
text = String.Format("Latitude: {0:F3}\r\n" +
"Longitude: {1:F3}\r\n" +
"Altitude: {2}\r\n\r\n" +
"{3}",
args.Position.Location.Latitude,
args.Position.Location.Longitude,
args.Position.Location.Altitude,
args.Position.Timestamp);
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
segoe14 = this.Content.Load<SpriteFont>("Segoe14");
viewport = this.GraphicsDevice.Viewport;
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
Vector2 textSize = segoe14.MeasureString(text);
textPosition = new Vector2((viewport.Width - textSize.X) / 2,
(viewport.Height - textSize.Y) / 2);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Navy);
spriteBatch.Begin();
spriteBatch.DrawString(segoe14, text, textPosition, Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
Because the GeoCoordinateWatcher is left running for the duration of the program, it should update the location as the phone is moved. Here's where I live.
Map Service
In a real phone application, you'd probably be using Bing Maps, particularly considering the existence of a Bing Maps Silverlight Control tailored for the phone. Unfortunately, making use of Bing Maps in a program involves opening a developer account, and getting a maps key and a credential token. This is all free and straightforward but it doesn't work well for a program that will be shared among all the readers of a book.
In the MainPage.xaml file, I left the SupportedOrientations property at its default setting of Portrait, I removed the page title to free up more space, and I moved the title panel below the content grid just in case there was a danger of something spilling out of the content grid and obscuring the title. Moving the title panel below the content grid in the XAML file ensures that it will be visually on top. Here's the content grid:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<TextBlock Name="statusText"
HorizontalAlignment="Center" VerticalAlignment="Center"
TextWrapping="Wrap" />
<Image Source="Images/usgslogoFooter.png"
Stretch="None"
HorizontalAlignment="Right"
VerticalAlignment="Bottom" />
</Grid>
The code-behind file has just two fields, one for the GeoCoordinateWatcher that supplies the location information, and the other for the proxy class created when the web service was added:
public partial class MainPage : PhoneApplicationPage
{
GeoCoordinateWatcher geoWatcher = new GeoCoordinateWatcher();
TerraServiceSoapClient proxy = new TerraServiceSoapClient();
public MainPage()
{
InitializeComponent();
Loaded += OnMainPageLoaded;
}
void OnMainPageLoaded(object sender, RoutedEventArgs args)
{
// Set event handlers for TerraServiceSoapClient proxy
proxy.GetAreaFromPtCompleted += OnProxyGetAreaFromPtCompleted;
proxy.GetTileCompleted += OnProxyGetTileCompleted;
// Start GeoCoordinateWatcher going
statusText.Text = "Obtaining geographic location...";
geoWatcher.PositionChanged += OnGeoWatcherPositionChanged;
geoWatcher.Start();
}
void OnGeoWatcherPositionChanged(object sender,
GeoPositionChangedEventArgs<GeoCoordinate> args)
{
// Turn off GeoWatcher
geoWatcher.PositionChanged -= OnGeoWatcherPositionChanged;
geoWatcher.Stop();
// Set coordinates to title text
GeoCoordinate coord = args.Position.Location;
ApplicationTitle.Text += ": " + String.Format("{0:F2}°{1} {2:F2}°{3}",
Math.Abs(coord.Latitude),
coord.Latitude > 0 ? 'N' : 'S',
Math.Abs(coord.Longitude),
coord.Longitude > 0 ? 'E' : 'W');
// Query proxy for AreaBoundingBox
LonLatPt center = new LonLatPt();
center.Lon = args.Position.Location.Longitude;
center.Lat = args.Position.Location.Latitude;
statusText.Text = "Accessing Microsoft Research Maps Service...";
proxy.GetAreaFromPtAsync(center, 1, Scale.Scale16m, (int)ContentPanel.ActualWidth,
(int)ContentPanel.ActualHeight);
}
void OnProxyGetAreaFromPtCompleted(object sender, GetAreaFromPtCompletedEventArgs args)
{
if (args.Error != null)
{
statusText.Text = args.Error.Message;
return;
}
statusText.Text = "Getting map tiles...";
AreaBoundingBox box = args.Result;
int xBeg = box.NorthWest.TileMeta.Id.X;
int yBeg = box.NorthWest.TileMeta.Id.Y;
int xEnd = box.NorthEast.TileMeta.Id.X;
int yEnd = box.SouthWest.TileMeta.Id.Y;
// Loop through the tiles
for (int x = xBeg; x <= xEnd; x++)
for (int y = yBeg; y >= yEnd; y--)
{
// Create Image object to display tile
Image img = new Image();
img.Stretch = Stretch.None;
img.HorizontalAlignment = HorizontalAlignment.Left;
img.VerticalAlignment = VerticalAlignment.Top;
img.Margin = new Thickness((x - xBeg) * 200 - box.NorthWest.Offset.XOffset,
(yBeg - y) * 200 - box.NorthWest.Offset.YOffset,
0, 0);
// Insert after TextBlock but before Image with logo
ContentPanel.Children.Insert(1, img);
// Define the tile ID
TileId tileId = box.NorthWest.TileMeta.Id;
tileId.X = x;
tileId.Y = y;
// Call proxy to get the tile (Notice that Image is user object)
proxy.GetTileAsync(tileId, img);
}
}
void OnProxyGetTileCompleted(object sender, GetTileCompletedEventArgs args)
{
if (args.Error != null)
{
return;
}
Image img = args.UserState as Image;
BitmapImage bmp = new BitmapImage();
bmp.SetSource(new MemoryStream(args.Result));
img.Source = bmp;
}
}
It is my experience that in most cases, the program doesn't get all the tiles it requests. If you're very lucky and you happen to be running the program somewhere in my neighborhood display might look like this.
If you change the second argument of the proxy.GetAreaFromPtAsync calls from a 1 to a 2, you get back images of an actual map rather than an aerial view.