Yesterday Bronwen and I introduced 500 Brisbane schoolgirls to the Microsoft Surface and taught them some geography via a simple game using Bing Maps, you can read about the event and their reactions here. In this post I’d like to share some more technical information about the application itself.
The game is a simple find the location as a fast as you can geography challenge. It is effective with a large group as the they can combine their knowledge and help each other to find the location. The locations themselves are famous landmarks represented by real life replica pencil sharpeners, we have stuck a Surface tag on the underneath of each so the table can see them. To win the game you zoom into the location as close as you can, around the physical object a circle appears with your time and indicates red if your not even in view or green when it is. When you when the Circle is yellow and the time stops. I think we need to launch some serious fireworks and add some sound in the next version.
Having been away in Poland for the Imagine Cup we built the application in only a few evenings this week, it really is simple as we leveraged our WPF skills, the Bing Maps control and an awesome Codeplex project called Infostrat.VE that provides a ready to use Surface control for Bing Maps.
Setting up your development environment is easy once you know how, a bit like the light bulb however it took me a number of failures. Firstly I had the VHD with all the Surface development tools ready to go, this is the really easy way to get started with Surface as you boot it up, get a registration key for Visual Studio 2008 express, and go. But you don’t get 3D support inside the virtual machine, and this means the Bing Maps 3D control is not going to work.
Now like you your probably running Windows7 and Visual Studio 2010, in my case I used my second machine with these under 32bit, sounds like 64bit users may face additional issues. The Surface SDK is designed for Windows Vista and Visual Studio 2008 express, I found that installing VS2008 c# express on 32bit Windows7 and then the Surface SDK worked fine. Unfortunately my second machine, a DELL XPS1330, is only 1280×800, not high enough to run the Surface simulator, solved by plugging into my DELL 24”.
Get all the Surface bits here.
Once up and running the SDK is pretty powerful for developing without an actual Surface. The simulator supports multiple mice (my trackpad and external mice counted as two) to simulate two points, left click and the right click to drop a point on the screen and even test the tags. Their were a couple of things we had to fix up once we got a real Surface that I’ll cover later but I was very impressed by the tools and could really see developers getting 90-95% there with the application without an actual device.
The main control we used is the Infostrat.VE NUI control from codeplex, I downloaded the latest source and rebuilt without issue. The project is well supported with lots of recent questions in the forum and good answers from the owner. The control wraps the Bing Maps 3D control from Microsoft, this control is now very stable and well documented, however at this time there is no future work planned for the control, what you see is what you get.
Gotcha #1, use the SurfaceVEMap control from the InfoStrat.VE.NUI.dll. I initially used the VEMap control from the InfoStrat.VE dll which worked fine as a WPF application but had no interactivity in the simulator or on the Surface, simple to swap around.
The main interface for the application is the Map control, pretty simple really. We wrapped this control in a TagVisualizer in order to detect our landmarks placed on the map:
<s:SurfaceWindow x:Class="SoulSolutions.LocationGame.MainSurfaceWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="http://schemas.microsoft.com/surface/2008" xmlns:m="clr-namespace:InfoStrat.VE.NUI;assembly=InfoStrat.VE.NUI" Title="SoulSolutions Location Game" > <s:TagVisualizer x:Name="Visualizer" VisualizationAdded="Visualizer_VisualizationAdded" > <Grid> <m:SurfaceVEMap x:Name="map" MapStyle="Hybrid" /> </Grid> </s:TagVisualizer> </s:SurfaceWindow>
We used ByteTags for our landmarks, these are really small and come supplied with the Surface or you can download from the Surface site and print.
Gotcha #2, The tags supplied with the surface are numbered 1-256, the tags when used in code are written in Hex. The numbers on the tags supplied are already in Hex, they seem to have just skipped the A-F (or mine were missing) so you don’t need to convert, eg tag 65 is 0x65. This is was something I didn’t discover until I was actually on the real Surface as the Simulator you type in the code again in hex so was happily using 0x41 for 65.
I simply hardcoded the values of the tags and their corresponding Latitudes and Longitudes for this first version, we’ve had a request to make it an XML file for easy maintenance and additions.
private void InitializeDefinitions() { locations = new Dictionary<byte, VELatLong>(); locations.Add(0x60, new VELatLong(0, 0)); locations.Add(0x69, new VELatLong(48.85827,2.29438)); locations.Add(0x64, new VELatLong(37.81907,-122.4786)); locations.Add(0x52, new VELatLong(29.97694,31.12963)); locations.Add(0x53, new VELatLong(43.72303, 10.39658)); locations.Add(0x61, new VELatLong(51.50056,-0.12523)); locations.Add(0x62, new VELatLong(47.62052,-122.34938)); locations.Add(0x51, new VELatLong(40.68939,-74.04477)); locations.Add(0x65, new VELatLong(38.89767,-77.03682)); locations.Add(0x56, new VELatLong(-33.85661,151.21515)); locations.Add(0x55, new VELatLong(41.89021,12.49231)); locations.Add(0x59, new VELatLong(-22.95121,-43.21318)); locations.Add(0x50, new VELatLong(50.08643,14.41095)); foreach (var location in locations) { ByteTagVisualizationDefinition tagDef = new ByteTagVisualizationDefinition(); tagDef.Value = location.Key; tagDef.Source = new Uri("LocationTagVisualization.xaml", UriKind.Relative); tagDef.MaxCount = 1; tagDef.LostTagTimeout = 2000.0; tagDef.TagRemovedBehavior = TagRemovedBehavior.Fade; Visualizer.Definitions.Add(tagDef); } }
Really the logic is very simple, the LocationTagVisulization control does all the work. When the tag is placed on the table the Visualizer_VisualizationAdded event is fired, we set the location and pass a reference to map control and let that control manage itself. The benefit of this approach is that multiple Tags can be placed on the map surface at once, each operating independently and creating a more advanced challenge for users.
We will post the full code once we have made a few changes based on feedback but for those super keen I’ll post the full LocationTagVisualization source as is at the end of the post.
When deploying the application you need to complete the XML file with your application name, the path you have copied it to on the Surface computer itself and the set of tags. You should also make an icon and image for the menu system. This XML file is deployed to the %PROGRAMDATA%\Microsoft\Surface\Programs folder on the Surface computer.
We had install the latest Bing Maps 3D control on both our develop machine (I had it already) as well as on the Surface, if it has an old Virtual Earth 3D control the uninstall it and install the latest from here.
The two programming errors we didn’t discover until we actual got on the Surface unit are the two gotchas above, we used the wrong infostrat control, just thought the Simulator wasn’t working, and had converted to Hex on the tags by mistake. Plan to spend at least a good afternoon with a real unit before showing anyone
I hope you found this post useful and we will post the full source once we make a few minor tweaks based on feedback if you’re interested. You can reach me at http://twitter.com/soulsolutions
All the game logic sits in the LocationTagVisualization class, the object that is added to the Surface when the Tag is placed on the surface:
public partial class LocationTagVisualization : TagVisualization { DispatcherTimer timer; public enum StatusType { Normal, OutOfView, InView, Found, Admin } public LocationTagVisualization() { InitializeComponent(); } private VEMap map; public VEMap Map { get { return map; } set { if (map != null) { //detach events map.CameraChanged -= map_CameraChanged; } map = value; if (map != null) { //attach events map.CameraChanged += map_CameraChanged; checkLocation(); } } } void map_CameraChanged(object sender, VECameraChangedEventArgs e) { checkLocation(); } private VELatLong location; public VELatLong Location { get { return location; } set { location = value; checkLocation(); } } private StatusType status; public StatusType Status { get { return status; } set { status = value; gotoState(true); } } private DateTime start; private void LocationTagVisualization_Loaded(object sender, RoutedEventArgs e) { start = DateTime.Now; //start timer here timer = new DispatcherTimer(); timer.Interval = TimeSpan.FromMilliseconds(50); timer.Tick += timer_Tick; timer.Start(); } void timer_Tick(object sender, EventArgs e) { if (start != null) { TimerDisplay.Text = DateTime.Now.Subtract(start).TotalSeconds.ToString("0.0"); } } private void LocationTagVisualization_Unloaded(object sender, RoutedEventArgs e) { Map = null; if (timer != null) { timer.Stop(); timer = null; } } private void checkLocation() { //if you have won then we ignore this. if (Status != StatusType.Found && Location != null && Map != null) { //detemine status based on bounds of the map and the zoomlevel, set state. var topRight = Map.PointToLatLong(new Point(0, 0)); var bottomLeft = Map.PointToLatLong(new Point(Map.GlobeWidth, Map.GlobeHeight)); //if either edge is null it is in space or tilted beyond what we can handle if (topRight == null || bottomLeft == null) { Status = StatusType.Normal; return; } //is the location inside the bounds? if ( ((Location.Latitude >= topRight.Latitude && Location.Latitude <= bottomLeft.Latitude) || (Location.Latitude <= topRight.Latitude && Location.Latitude >= bottomLeft.Latitude)) && ((Location.Longitude >= topRight.Longitude && Location.Longitude <= bottomLeft.Longitude) || (Location.Longitude <= topRight.Longitude && Location.Longitude >= bottomLeft.Longitude)) ) { //is the zoomlevel close enough for win? if (map.Altitude < 1000d) { Status = StatusType.Found; return; } Status = StatusType.InView; return; } Status = StatusType.OutOfView; } } private void gotoState(bool useTransitions) { if (Face != null) { switch (Status) { case StatusType.Normal: Face.Fill = new SolidColorBrush(Colors.Black); break; case StatusType.InView: Face.Fill = new SolidColorBrush(ColorFromHexString("FF22760C")); break; case StatusType.OutOfView: Face.Fill = new SolidColorBrush(ColorFromHexString("FF76150C")); break; case StatusType.Found: if (timer != null) timer.Stop(); Face.Fill = new SolidColorBrush(ColorFromHexString("FFE2ED20")); break; case StatusType.Admin: if (timer != null) timer.Stop(); Face.Fill = new SolidColorBrush(ColorFromHexString("FF760C72")); TimerDisplay.Visibility = Visibility.Collapsed; break; } } } private static Color ColorFromHexString(string HexColor) { try { //The input at this point could be HexColor = "FF00FF1F" byte Alpha = byte.Parse(HexColor.Substring(0, 2), NumberStyles.HexNumber); byte Red = byte.Parse(HexColor.Substring(2, 2), NumberStyles.HexNumber); byte Green = byte.Parse(HexColor.Substring(4, 2), NumberStyles.HexNumber); byte Blue = byte.Parse(HexColor.Substring(6, 2), NumberStyles.HexNumber); return Color.FromArgb(Alpha, Red, Green, Blue); } catch (Exception) { return Colors.Black; } } }