Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

WiiEarth -- Wiimote Interface for Virtual Earth

globe In this article, Brian Peek will demonstrate how to use a Nintendo Wii Remote (Wiimote) as a controller for Microsoft Virtual Earth 3D.
ASPSOFT, Inc.

Difficulty: Intermediate
Time Required: 2-3 hours
Cost: $60 for Wiimote and Nunchuk
Software: Managed Library for Nintendo's Wiimote, Visual Basic or Visual C# Express Editions
Hardware: Nintendo Wii Remote (Wiimote) with Nunchuk, a compatible PC Bluetooth adapter and stack, optional IR sensor bar
 

Note: As of April 18, 2008 this code no longer works due to API changes in VE 3D. We will keep this article up on Coding4Fun.

 

Introduction

Virtual Earth is the 3D interface to Microsoft's Live Maps service.  Normally this control is loaded via the web browser and allows interaction with a keyboard, mouse, and Xbox 360 controller.  In this article, we will take the Virtual Earth control out of the web browser, use it in a WinForms application, and control it with a Nintendo Wii Remote (Wiimote).  Note that use of the Virtual Earth 3D control in this way is undocumented and unsupported at the moment.  Because of this, some of the descriptions in this article are educated guesses and may not be 100% accurate.

Setup

Before we get started, you will need to install the Virtual Earth 3D control.  If you haven't done this already, browse to http://maps.live.com/ and click on the Install 3D link to install the control and supporting software.

Additionally, if you haven't already, please review my Managed Library for Nintendo's Wiimote article on this site.  We will be using the library in this article, but I will not repeat the basic information that is located in the original article.  Note that this application uses a newer version of the Wiimote library which is not yet uploaded.  It will be available in a few days.

Implementation

The Virtual Earth 3D Control

The Virtual Earth 3D (VE3D) control is intended to be used through a well documented JavaScript interface from a web page, however we would not be able to access the Wiimote from a web browser.  Therefore, we will be using the VE3D control through its native, but wholly undocumented interface.  Note that on 10/15/07 a new version of VE3D was released that changed the API drastically from the previous version.  This article reflects the newer version.

Start by creating a new Windows Forms application named WiiEarth in C# or VB.  As with all controls and 3rd party libraries, a reference needs to be set to the Virtual Earth 3D libraries.  Unfortunately this cannot be done from the Visual Studio IDE because of the way the control is installed to the Global Assembly Cache.  So, to set reference to the necessary assemblies, ensure the project is not open in Visual Studio and open the .csproj/.vbproj file in notepad.  Add the following XML in the <ItemGroup> which contains the base references, such as System and System.Data:

<Reference Include="Microsoft.MapPoint.Data">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.MapPoint.Data.CompactMapFile">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.MapPoint.Data.VirtualEarthTileDataSource">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.MapPoint.Geometry">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.MapPoint.Graphics3D">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.MapPoint.GraphicsAPI">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.MapPoint.Rendering3D">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.MapPoint.Utility">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.MapPoint.Rendering3D.Utility">
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.MapPoint.Rendering3D.WorldMemoryDataSource">
<Private>False</Private>
</Reference>

With the references in place, the project file can now be opened and the references will be seen in the References folder in the Solution Explorer as usual.

image

Creating an instance of the control can be done in code just like any other control.  Used in the constructor or load event of the form, the following code will create a VE3D control and add it to the form as fully docked:

C#

// the Virtual Earth 3D control
private GlobeControl globeControl;

private void MainForm_Load(object sender, EventArgs e)
{
// create a new instance of the VE control
this.globeControl = new GlobeControl();

// setup the globeControl to fill the window
this.globeControl.Dock = DockStyle.Fill;
this.globeControl.Location = new System.Drawing.Point(0, 0);
this.globeControl.Name = "Globe";
this.pnlGlobe.Controls.Add(globeControl);
}

VB

' the Virtual Earth 3D control
Private globeControl As GlobeControl

Private Sub MainForm_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
' create a new instance of the VE control
Me.globeControl = New GlobeControl()

' setup the globeControl to fill the window
Me.globeControl.Dock = DockStyle.Fill
Me.globeControl.Location = New System.Drawing.Point(0, 0)
Me.globeControl.Name = "Globe"
Me.pnlGlobe.Controls.Add(globeControl)
End Sub

This sets up the VE3D control in its default state.  If you were to run an application with only this code, you would see nothing but the earth.  The navigation controls and other extras would be missing.  If you wish to add the default navigation controls to the screen, the PlugInLoader object is used.  The PlugInLoader is created by using the CreateLoader static method, passing in an instance of the GlobeControl's Host object.  Then, the NavigationPlugIn can be loaded and activated as shown:

C#

// load all the spiffy UI navigation goodies
PlugInLoader loader = PlugInLoader.CreateLoader(this.globeControl.Host);
loader.LoadPlugIn(typeof(NavigationPlugIn));
loader.ActivatePlugIn(typeof(NavigationPlugIn).GUID, null);

VB

' load all the spiffy UI navigation goodies
Dim loader As PlugInLoader = PlugInLoader.CreateLoader(Me.globeControl.Host)
loader.LoadPlugIn(GetType(NavigationPlugIn))
loader.ActivatePlugIn(GetType(NavigationPlugIn).GUID, Nothing)

The last thing to be added for basic functionality is the data.  As it stands, the only data that will appear on the globe is the image of the continents.  Zooming in only produces a blurry representation of that base image.

Data layers are created from specially formatted data sources provided by local.live.com known as content manifests.  These are XML files which tell the VE3D control how to load the data required for any view.  The following helper method can be used to easily load data layers:

C#

private void AddFeederSource(string uri, string layerId, string id, DataSourceUsage usage)
{
// create a data source from a URL
DataSource dataSource = VirtualEarthTileDataSource.ConnectTo(
new Uri(uri),
this.globeControl.Host.WebProxy,
WorldMemoryDataSource.DataPath);

// create a layer from that data source
DataSourceLayerData layer = new DataSourceLayerData(layerId, id, dataSource, usage);

// add it to the globe
this.globeControl.Host.DataSources.Add(layer);
}

VB

Private Sub AddFeederSource(ByVal uri As String, ByVal layerId As String, ByVal id As String, ByVal usage As DataSourceUsage)
' create a data source from a URL
Dim dataSource As DataSource = VirtualEarthTileDataSource.ConnectTo(New Uri(uri), Me.globeControl.Host.WebProxy, WorldMemoryDataSource.DataPath)

' create a layer from that data source
Dim layer As DataSourceLayerData = New DataSourceLayerData(layerId, id, dataSource, usage)

' add it to the globe
Me.globeControl.Host.DataSources.Add(layer)
End Sub

By passing the URL of the content manifest, a name for the layer, and what the manifest represents, a new DataSource is created, which is in turn used to create a DataSourceLayerData object which is then given to the VE3D control to consume.

With this helper method in place, we can add any of the following layers (note that there may be other content manifests provided by local.live.com, but these are the only 5 that I am aware of):

URL

DataSourceUsage Type

Description

http://local.live.com/Manifests/HD.xml ElevationMap Terrain data
http://local.live.com/Manifests/MO.xml Model 3D buildings
http://local.live.com/Manifests/AT.xml TextureMap Unlabeled aerial
http://local.live.com/Manifests/HT.xml TextureMap Labeled aerial
http://local.live.com/Manifests/RT.xml TextureMap Roads only

For the best display, add the ElevationMap, Model and Aerial TextureMap layers as shown:

C#

// setup the default aerial layer, buildings, and elevation data
AddFeederSource("http://local.live.com/Manifests/HD.xml", "Terrain", "Terrain", DataSourceUsage.ElevationMap);
AddFeederSource("http://local.live.com/Manifests/MO.xml", "Model", "Model", DataSourceUsage.Model);
AddFeederSource("http://local.live.com/Manifests/AT.xml", "Roads", "Roads", DataSourceUsage.TextureMap);

VB

' setup the default aerial layer, buildings, and elevation data
AddFeederSource("http:'local.live.com/Manifests/HD.xml", "Terrain", "Terrain", DataSourceUsage.ElevationMap)
AddFeederSource("http:'local.live.com/Manifests/MO.xml", "Model", "Model", DataSourceUsage.Model)
AddFeederSource("http:'local.live.com/Manifests/AT.xml", "Roads", "Roads", DataSourceUsage.TextureMap)

If you were to run the application at this point, you would see a fully functioning Virtual Earth 3D control with proper data and navigation.

 

Control Scheme and Bindings

Controlling VE3D with the Wiimote will be accomplished using a control scheme that is very similar to most first person shooter (FPS) games on the Wii.  The nunchuk, held in the left hand, will move the camera forward/back/left/right using the joystick.  The C and Z buttons on the front of the nunchuk will be used to raise and lower the altitude of the camera.  The Wiimote, held in the right hand, will be used to change the tilt and turn of the camera.  The buttons on the Wiimote will also be hooked up to several VE3D functions.  Home will center the map to an overhead view at the current camera position.  The 1 button will toggle through the road layers.  The 2 button will toggle the overlaid UI off and on.

VE3D bindings allow you to change or create new control schemes for VE3D.  Open your %APPDATA%\Microsoft\Virtual Earth 3D directory.  On Windows XP, %APPDATA% should resolve to \Documents and Settings\<user>\Application Data .  On Windows Vista, it should resolve to \Users\<user>\AppData\Roaming .  In this directory you will find a Bindings.xml file.  This XML schema defines the default keyboard, mouse, Gamepad and other input device properties.  Open the file to see the schema used to define events and parameters.

By default, VE3D will load any file named Bindings*.xml from this directory.  For the Wiimote control scheme, create a new file named BindingsWiimote.xml in this directory.  Set the contents of the file to the following:

<?xml version="1.0" encoding="utf-8" ?>
<Bindings>
<BindingSet Name="wiimoteBindings" AutoUse="True" Cursor="Drag">
<!-- Nunchuk joystick -->
<Bind Event="Wiimote.NunchukX" Action="Strafe" Factor="1" />
<Bind Event="Wiimote.NunchukY" Action="Move" Factor="1" />

<!-- Nunchuk joystick with modifier since we can move and turn at the same time -->
<Bind Event="Wiimote.B+Wiimote.NunchukX" Action="Strafe" Factor="1" />
<Bind Event="Wiimote.B+Wiimote.NunchukY" Action="Move" Factor="1" />

<!-- Nunchuk buttons -->
<Bind Event="Wiimote.NunchukC" Action="Ascend" Factor="0.3" />
<Bind Event="Wiimote.NunchukZ" Action="Ascend" Factor="-0.3" />

<!-- Nunchuk joystick with modifier since we can move and change altitude -->
<Bind Event="Wiimote.B+Wiimote.NunchukC" Action="Ascend" Factor="0.3" />
<Bind Event="Wiimote.B+Wiimote.NunchukZ" Action="Ascend" Factor="-0.3" />

<!-- Wiimote buttons -->
<Bind Event="Wiimote.Home" Action="ResetOnCenter" />
<Bind Event="Wiimote.One" Action="ToggleRoads,WiiEarth, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<Bind Event="Wiimote.Two" Action="ToggleUI,WiiEarth, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<Bind Event="Wiimote.A" Factor="1" Action="Locations,WiiEarth, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>

<!-- Wiimote IR for pitch/turn -->
<Bind Event="Wiimote.B+Wiimote.IRX" Action="Turn" Factor="0.05" />
<Bind Event="Wiimote.B+Wiimote.IRY" Action="Pitch" Factor="-0.05" />

<!-- Wiimote accel for pitch/turn -->
<Bind Event="Wiimote.B+Wiimote.AX" Action="Turn" Factor="-0.025" />
<Bind Event="Wiimote.B+Wiimote.AY" Action="Pitch" Factor="-0.025" />

<Bind Event="Wiimote.Left" Factor="-1" Action="Locations,WiiEarth, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<Bind Event="Wiimote.Up" Factor="-1" Action="LocationsMove,WiiEarth, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<Bind Event="Wiimote.Down" Factor="1" Action="LocationsMove,WiiEarth, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>

<!-- FPS-style keyboard controls in case we don't have a nunchuk -->
<Bind Event="Key.W" Action="Move" Factor="22" />
<Bind Event="Key.S" Action="Move" Factor="-22" />
<Bind Event="Key.D" Action="Strafe" Factor="22" />
<Bind Event="Key.A" Action="Strafe" Factor="-22" />
<Bind Event="Key.Space" Action="Ascend" Factor="20" />
<Bind Event="Key.C" Action="Ascend" Factor="-20" />
<Bind Event="Key.Z" Action="ToggleRoads,WiiEarth, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<Bind Event="Key.X" Action="ToggleUI,WiiEarth, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</BindingSet>
</Bindings>

The <BindingSet> tags wrap groups of control bindings.  It requires a Name and optionally a Cursor.  If the binding set is to be used automatically, as it would be in most cases, set the AutoUse parameter to True.  Inside of that are <Bind> tags.  The tag requires the Event, Action parameters and optionally the Factor parameter.  The Event parameter will be used to match the binding to its handler which will be written later.  The syntax is <Handler Name>.<Event Name>.  The Action parameter is used to map the specific binding to a particular method.  The Factor parameter is optional and can be used to scale the data value up or down to increase or decrease sensitivity of the input method.  Once the handler is written, these will make more sense.

The bindings above create the control scheme described above:  NunchukX/Y describe what happens when the analog joystick is moved, NunchukC/Z describe what happens with the C/Z buttons are pressed, and so on.

The bindings also allow for several variations.  Bindings are defined for both the IR position (IRX, IRY) and accelerometer values (AX, AY).  If an IR sensor bar is not available, the accelerometer values of the Wiimote can be used instead.  Additionally, keyboard bindings are created in the style of a first person shooter using WASD.  These can be used if a Nunchuk is not available.

Note that some bindings append two Events together with a + sign.  This allows for button combinations.  In this case, for the accelerometer and/or IR sensor, we only want to register the action if a button is pressed down.  So, those events which require the button to be held down contain Wiimote.B+ and the event it is combined with.  Also note that the combination events override other events that don't have a combination listed.  So, for example, NunchukC is listed as working alone and with the Wiimote.B event.

For those events which require a custom action that will be written separately and not part of the VE3D control, the Action parameter must contain the action name, followed by a comma, and then the full assembly name:

<Bind Event="Wiimote.One" Action="ToggleRoads,
WiiEarth, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
/>

You can change any of these button bindings simply by changing this XML file and deploying to the directory above.  So, for example, if you wanted the trigger button to be A, you would just change Wiimote.B to Wiimote.A in the above lines and re-deploy the bindings file.

The XML file also binds several keyboard keys in a first-person shooter style layout in the event the user does not have a nunchuk for the left hand functions.

Event Source

An EventSource is needed which will grab data from the Wiimote and pass it along to VE3D as defined by the bindings file above.  Create a new class named WiimoteEventSource which derives from Microsoft.MapPoint.Binding.EventSource  as follows:

Next, add an enumeration named WiimoteEvent (the name isn't important) which contains all of the Name items from the bindings XML file above.  It should look like this:

C#

// all events handled by this event source from XML file
public enum WiimoteEvent
{
IRX, // IR X position
IRY, // IR Y position
NunchukX, // Nunchuk joystick X position
NunchukY, // Nunchuk joystick Y position
NunchukC, // Nunchuk C button
NunchukZ, // Nunchuk Z button
AX, // Wiimote accelerometer X
AY, // Wiimote accelerometer Y
Up, // Dpad up
Down, // Dpad down
Left, // Dpad left
Right, // Dpad right
A, // A button
B, // B button
Minus, // Minus button
Home, // Wiimote Home button
Plus, // Plus button
One, // Wiimote One button
Two, // Wiimote Two button
}

VB

Public Enum WiimoteEvent
IRX ' IR X position
IRY ' IR Y position
NunchukX ' Nunchuk joystick X position
NunchukY ' Nunchuk joystick Y position
NunchukC ' Nunchuk C button
NunchukZ ' Nunchuk Z button
AX ' Wiimote accelerometer X
AY ' Wiimote accelerometer Y
Up ' Dpad up
Down ' Dpad down
Left ' Dpad left
Right ' Dpad right
A ' A button
B ' B button
Minus ' Minus button
Home ' Wiimote Home button
Plus ' Plus button
One ' Wiimote One button
Two ' Wiimote Two button
End Enum

Next, several methods from the EventSource object need to be overridden:  GetEventData, IsModifier, CanModify, TryGetEventId, TryGetEventName, Name.  The methods do the following:

Method/Property

Description

GetEventData Unsure at the moment...does not need to be implemented?
IsModifier Returns a boolean stating whether the passed in event ID is a modifier (such as the Wiimote.B event above)
CanModify Returns a boolean stating whether the current event is allowed as a modifier
TryGetEventID Maps a string event name from the bindings file to the integer value in the enumeration above
TryGetEventName Maps an integer event ID to the string name in the enumeration above
Name (property) Returns the name of the handler which must match the name in the XML file above (Wiimote in this case)

The code for these methods is presented below:

C#

// return out value of the passed enum
public override bool TryGetEventId(string eventName, out int eventId)
{
eventId = (int)Enum.Parse(typeof(WiimoteEvent), eventName);
return true;
}

// return out the string name of the passed in enum value
public override bool TryGetEventName(int eventId, out string eventName)
{
eventName = ((WiimoteEvent)eventId).ToString();
return true;
}

// unknown
public override EventData GetEventData(int eventId, EventActivateState state)
{
throw new NotImplementedException();
}

// can the event be used as a modifier?
public override bool IsModifier(int eventId)
{
// yes to all for now
return true;
}

// can the supplied event be used as a modifier?
public override bool CanModify(int eventId, EventKey other)
{
// only if it's from us
return (other.Source == this);
}

// this must match the Source name in the bindings XML file
public override string Name
{
get { return "Wiimote"; }
}

VB

' return out value of the passed enum
Public Overrides Function TryGetEventId(ByVal eventName As String, <System.Runtime.InteropServices.Out()> ByRef eventId As Integer) As Boolean
eventId = CInt(Fix(System.Enum.Parse(GetType(WiimoteEvent), eventName)))
Return True
End Function

' return out the string name of the passed in enum value
Public Overrides Function TryGetEventName(ByVal eventId As Integer, <System.Runtime.InteropServices.Out()> ByRef eventName As String) As Boolean
eventName = (CType(eventId, WiimoteEvent)).ToString()
Return True
End Function

' unknown
Public Overrides Function GetEventData(ByVal eventId As Integer, ByVal state As EventActivateState) As EventData
Throw New NotImplementedException()
End Function

' can the event be used as a modifier?
Public Overrides Function IsModifier(ByVal eventId As Integer) As Boolean
' yes to all for now
Return True
End Function

' can the supplied event be used as a modifier?
Public Overrides Function CanModify(ByVal eventId As Integer, ByVal other As EventKey) As Boolean
' only if it's from us
Return (other.Source Is Me)
End Function

' this must match the Source name in the bindings XML file
Public Overrides ReadOnly Property Name() As String
Get
Return "Wiimote"
End Get
End Property

With that in place, the constructor can be implemented which will call the base constructor and connect to the Wiimote.  It is assumed you read the Wiimote article above and know how the library works.

The constructor must take one argument passed from the main from:  an instance of the GlobeControl's ActionSystem.  This just gets passed directly to the parent object's constructor untouched.  The constructor code looks like the following:

C#

public WiimoteEventSource(ActionSystem actionSystem, MainForm form) : base(actionSystem)
{
// store away an instance of the main form
_form = form;

// setup wiimote and event handlers
_wm = new Wiimote();
_wm.OnWiimoteChanged += new WiimoteChangedEventHandler(OnWiimoteChanged);
_wm.OnWiimoteExtensionChanged += new WiimoteExtensionChanged(OnWiimoteExtensionChanged);
_wm.Connect();

// if we don't have an extension, set the report type to IR and accel's only
if(!_wm.WiimoteState.Extension)
_wm.SetReportType(Wiimote.InputReport.IRAccel, true);

// turn off all LEDs
_wm.SetLEDs(0x00);
}

VB

Public Sub New(ByVal actionSystem As ActionSystem, ByVal form As MainForm)
MyBase.New(actionSystem)
' store away an instance of the main form
_form = form

' setup wiimote and event handlers
_wm = New Wiimote()
AddHandler _wm.OnWiimoteChanged, AddressOf OnWiimoteChanged
AddHandler _wm.OnWiimoteExtensionChanged, AddressOf OnWiimoteExtensionChanged
_wm.Connect()

' if we don't have an extension, set the report type to IR and accel's only
If (Not _wm.WiimoteState.Extension) Then
_wm.SetReportType(Wiimote.InputReport.IRAccel, True)
End If

' turn off all LEDs
_wm.SetLEDs(&H00)
End Sub

The OnWiimoteExtensionChanged method simply sets the report mode for the Wiimote based on whether or not a Nunchuk is inserted as shown:

C#

private void OnWiimoteExtensionChanged(object sender, WiimoteExtensionChangedEventArgs args)
{
// if nunchuk inserted, set the report type to return extension data
if(args.ExtensionType == ExtensionType.Nunchuk && args.Inserted)
_wm.SetReportType(Wiimote.InputReport.IRExtensionAccel, true);
else // in all other cases, set it to the default IR and accel's
_wm.SetReportType(Wiimote.InputReport.IRAccel, true);
}

VB

Private Sub OnWiimoteExtensionChanged(ByVal sender As Object, ByVal args As WiimoteExtensionChangedEventArgs)
' if nunchuk inserted, set the report type to return extension data
If args.ExtensionType = ExtensionType.Nunchuk AndAlso args.Inserted Then
_wm.SetReportType(Wiimote.InputReport.IRExtensionAccel, True)
Else ' in all other cases, set it to the default IR and accel's
_wm.SetReportType(Wiimote.InputReport.IRAccel, True)
End If
End Sub

The OnWiimoteChanged event handler is where the Wiimote data is handled and sent off to the VE3D control to reflect the changes.  First, let's handle the IR and accelerometer data.  The IR midpoint of the X and Y axes will be used from the WiimoteState object to activate the IRX and IRY events we defined above in the bindings XML file.  The accelerometer X and Y values will be used to activate the AX and AY events.

This snippet assumes that there is a boolean property named UseIR created in the project to determine whether IR or motion values are used.  Additionally, it assumes there are property settings created which contain values for the X/Y "dead zones" for the IR and accelerometers.  These dead zones are used as a way to only activate the event when the values are pushed beyond the thresholds.  This allows there to be a margin where the user's hand will not be read as movement, allowing the user to not have to worry about keeping a steady hand.

The application linked above uses the following values for dead zones:

  • NunchukDeadX/Y -> 0.025
  • WiimoteDeadX/Y -> 0.15
  • IRDeadX/Y -> 0.1

C#

// if we're using the IR
if(Properties.Settings.Default.UseIR)
{
// and both LEDs are found
if(ws.IRState.Found1 && ws.IRState.Found2)
{
// normalize the midpoints to -0.5 to 0.5 (from 0 to 1.0)
float x = ws.IRState.MidX - 0.5f;
float y = ws.IRState.MidY - 0.5f;

// if we're beyond the thresholds, activate the events
if(x > Properties.Settings.Default.IRDeadX || x < -Properties.Settings.Default.IRDeadX)
this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.IRX), x));
if(y > Properties.Settings.Default.IRDeadY || y < -Properties.Settings.Default.IRDeadY)
this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.IRY), y));

// save the last IR settings...these get used if we go beyond the range of the IRs.
// in that case, the last used positions will be used until the Wiimote comes back in range
this._lastIRX = x;
this._lastIRY = y;
}
else // one or both LEDs aren't seen
{
// activate events based on the last known positions
if(this._lastIRX > Properties.Settings.Default.IRDeadX || this._lastIRX < -Properties.Settings.Default.IRDeadX)
this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.IRX), this._lastIRX));
if(this._lastIRY > Properties.Settings.Default.IRDeadY || this._lastIRY < -Properties.Settings.Default.IRDeadY)
this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.IRY), this._lastIRY));
}
}
else // we're using motion controls
{
// activate the events based on the accelerometer values
if(ws.AccelState.X > Properties.Settings.Default.WiimoteDeadX || ws.AccelState.X < -Properties.Settings.Default.WiimoteDeadX)
this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.AX), ws.AccelState.X));
if(ws.AccelState.Y > Properties.Settings.Default.WiimoteDeadY || ws.AccelState.Y < -Properties.Settings.Default.WiimoteDeadY)
this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.AY), ws.AccelState.Y));
}

VB

' if we're using the IR
If My.Settings.Default.UseIR Then
' and both LEDs are found
If ws.IRState.Found1 AndAlso ws.IRState.Found2 Then
' normalize the midpoints to -0.5 to 0.5 (from 0 to 1.0)
Dim x As Single = ws.IRState.MidX - 0.5f
Dim y As Single = ws.IRState.MidY - 0.5f

' if we're beyond the thresholds, activate the events
If x > My.Settings.Default.IRDeadX OrElse x < -My.Settings.Default.IRDeadX Then
Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.IRX))), x))
End If
If y > My.Settings.Default.IRDeadY OrElse y < -My.Settings.Default.IRDeadY Then
Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.IRY))), y))
End If

' save the last IR settings...these get used if we go beyond the range of the IRs.
' in that case, the last used positions will be used until the Wiimote comes back in range
Me._lastIRX = x
Me._lastIRY = y
Else ' one or both LEDs aren't seen
' activate events based on the last known positions
If Me._lastIRX > My.Settings.Default.IRDeadX OrElse Me._lastIRX < -My.Settings.Default.IRDeadX Then
Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.IRX))), Me._lastIRX))
End If
If Me._lastIRY > My.Settings.Default.IRDeadY OrElse Me._lastIRY < -My.Settings.Default.IRDeadY Then
Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.IRY))), Me._lastIRY))
End If
End If
Else ' we're using motion controls
' activate the events based on the accelerometer values
If ws.AccelState.X > My.Settings.Default.WiimoteDeadX OrElse ws.AccelState.X < -My.Settings.Default.WiimoteDeadX Then
Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.AX))), ws.AccelState.X))
End If
If ws.AccelState.Y > My.Settings.Default.WiimoteDeadY OrElse ws.AccelState.Y < -My.Settings.Default.WiimoteDeadY Then
Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.AY))), ws.AccelState.Y))
End If
End If

This code looks at the appropriate values, determines if they are beyond the specified thresholds for the dead zones, and, if they are, activates the event for that value using the Execute method.  Execute is a method in the base EventSource class.  This method will activate the event specified from the enumeration (which, remember, is contained in the bindings XML file) with the value associated with that event.  An EventData object of some type must be created and passed to the Execute method.  There are two EventData types to know about:  AxisEventData and ButtonEventDataAxisEventData should be used when an event is activated that will modify the map position in some way.  That is, if the map is being turned, elevation is changing, etc.  ButtonEventData should be used if the event is a simple toggle like pressing a button down and releasing it.

Next, the nunchuk values need to be read and the associated events activated.  This is done as follows:

C#

// if the nunchuk is connected
if(ws.Extension && ws.ExtensionType == ExtensionType.Nunchuk)
{
// activate the nunchuk-based events
if(ws.NunchukState.X > Properties.Settings.Default.NunchukDeadX || ws.NunchukState.X < -Properties.Settings.Default.NunchukDeadX)
this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.NunchukX), ws.NunchukState.X));
if(ws.NunchukState.Y > Properties.Settings.Default.NunchukDeadY || ws.NunchukState.Y < -Properties.Settings.Default.NunchukDeadY)
this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.NunchukY), ws.NunchukState.Y));
if(ws.NunchukState.C)
this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.NunchukC), 1.0f));
if(ws.NunchukState.Z)
this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.NunchukZ), 1.0f));
}

VB

' if the nunchuk is connected
If ws.Extension AndAlso ws.ExtensionType = ExtensionType.Nunchuk Then
' activate the nunchuk-based events
If ws.NunchukState.X > My.Settings.Default.NunchukDeadX OrElse ws.NunchukState.X < -My.Settings.Default.NunchukDeadX Then
Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.NunchukX))), ws.NunchukState.X))
End If
If ws.NunchukState.Y > My.Settings.Default.NunchukDeadY OrElse ws.NunchukState.Y < -My.Settings.Default.NunchukDeadY Then
Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.NunchukY))), ws.NunchukState.Y))
End If
If ws.NunchukState.C Then
Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.NunchukC))), 1.0f))
End If
If ws.NunchukState.Z Then
Me.Execute(New AxisEventData(New EventKey(Me, CInt(Fix(WiimoteEvent.NunchukZ))), 1.0f))
End If
End If

Finally, the button events need to be activated.  A helper method which will check the current button state will be used for determining which button of all the Wiimote buttons is pressed.  For those that are, the appropriate event is activated with a call to Execute.

C#

private void HandleButton(WiimoteEvent we, bool buttonState, bool lastButtonState)
{
if(buttonState == lastButtonState)
return;
else
{
if(buttonState)
this.Execute(new ButtonEventData(new EventKey(this, (int)we), EventActivateState.Activate));
else
this.Execute(new ButtonEventData(new EventKey(this, (int)we), EventActivateState.Deactivate));
}
}

// handle all the Wiimote buttons
HandleButton(WiimoteEvent.Up, ws.ButtonState.Up, _lastBS.Up);
HandleButton(WiimoteEvent.Down, ws.ButtonState.Down, _lastBS.Down);
HandleButton(WiimoteEvent.Left, ws.ButtonState.Left, _lastBS.Left);
HandleButton(WiimoteEvent.Right, ws.ButtonState.Right, _lastBS.Right);
HandleButton(WiimoteEvent.A, ws.ButtonState.A, _lastBS.A);
HandleButton(WiimoteEvent.B, ws.ButtonState.B, _lastBS.B);
HandleButton(WiimoteEvent.Minus, ws.ButtonState.Minus, _lastBS.Minus);
HandleButton(WiimoteEvent.Home, ws.ButtonState.Home, _lastBS.Home);
HandleButton(WiimoteEvent.Plus, ws.ButtonState.Plus, _lastBS.Plus);
HandleButton(WiimoteEvent.One, ws.ButtonState.One, _lastBS.One);
HandleButton(WiimoteEvent.Two, ws.ButtonState.Two, _lastBS.Two);

...

// save off the current button state for next time
_lastBS = ws.ButtonState;
_lastNunchuk = ws.NunchukState;

VB

Private Sub HandleButton(ByVal we As WiimoteEvent, ByVal buttonState As Boolean, ByVal lastButtonState As Boolean)
If buttonState = lastButtonState Then
Return
Else
If buttonState Then
Me.Execute(New ButtonEventData(New EventKey(Me, CInt(Fix(we))), EventActivateState.Activate))
Else
Me.Execute(New ButtonEventData(New EventKey(Me, CInt(Fix(we))), EventActivateState.Deactivate))
End If
End If
End Sub

...

' handle all the Wiimote buttons
HandleButton(WiimoteEvent.Up, ws.ButtonState.Up, _lastBS.Up)
HandleButton(WiimoteEvent.Down, ws.ButtonState.Down, _lastBS.Down)
HandleButton(WiimoteEvent.Left, ws.ButtonState.Left, _lastBS.Left)
HandleButton(WiimoteEvent.Right, ws.ButtonState.Right, _lastBS.Right)
HandleButton(WiimoteEvent.A, ws.ButtonState.A, _lastBS.A)
HandleButton(WiimoteEvent.B, ws.ButtonState.B, _lastBS.B)
HandleButton(WiimoteEvent.Minus, ws.ButtonState.Minus, _lastBS.Minus)
HandleButton(WiimoteEvent.Home, ws.ButtonState.Home, _lastBS.Home)
HandleButton(WiimoteEvent.Plus, ws.ButtonState.Plus, _lastBS.Plus)
HandleButton(WiimoteEvent.One, ws.ButtonState.One, _lastBS.One)
HandleButton(WiimoteEvent.Two, ws.ButtonState.Two, _lastBS.Two)

' save off the current button state for next time
_lastBS = ws.ButtonState
_lastNunchuk = ws.NunchukState

And, the current button values are stored away to check on the next event so button events are only fired once.

Now that the event source object is written, it needs to be hooked up to the globeControl so it can be used.  This can be done by creating an instance of the WiimoteEventSource object, passing in the VE3D's ActionSystem from the BindingsManager object.  Then, the event source instance is passed to the ActionSystem's EventSourceManager and registered using the RegisterEventSource method.  Event sources should re registered before the control is added to the form.

C#

// wiimote events
private WiimoteEventSource _wiimoteEventSource;

...

// create a new instance of the Wiimote event handler
this._wiimoteEventSource = new WiimoteEventSource(this.globeControl.Host.BindingsManager.ActionSystem, this);

// register it in the event source list
this.globeControl.Host.BindingsManager.ActionSystem.EventSourceManager.RegisterEventSource(this._wiimoteEventSource);

VB

' wiimote events
Private _wiimoteEventSource As WiimoteEventSource

...

' create a new instance of the Wiimote event handler
Me._wiimoteEventSource = New WiimoteEventSource(Me.globeControl.Host.BindingsManager.ActionSystem, Me)

' register it in the event source list
Me.globeControl.Host.BindingsManager.ActionSystem.EventSourceManager.RegisterEventSource(Me._wiimoteEventSource)

Our binding list contains three action types that are not defined by the default VE3D actions:  ToggleRoads, Locations, LocationsMove, and ToggleUI.  These actions and their handlers must be registered with the VE3D control.  After the WiimoteEventSource is registered, the four actions can be registered as follows:

C#

this.globeControl.Host.BindingsManager.RegisterAction(asmName, "ToggleRoads", new Action(this.ToggleRoadsHandler));
this.globeControl.Host.BindingsManager.RegisterAction(asmName, "Locations", new Action(this.LocationsHandler));
this.globeControl.Host.BindingsManager.RegisterAction(asmName, "LocationsMove", new Action(this.LocationsMoveHandler));
this.globeControl.Host.BindingsManager.RegisterAction(asmName, "ToggleUI", new Action(this.ToggleUIHandler));

VB

Me.globeControl.Host.BindingsManager.RegisterAction(asmName, "ToggleRoads", New Action(AddressOf Me.ToggleRoadsHandler))
Me.globeControl.Host.BindingsManager.RegisterAction(asmName, "Locations", New Action(AddressOf Me.LocationsHandler))
Me.globeControl.Host.BindingsManager.RegisterAction(asmName, "LocationsMove", New Action(AddressOf Me.LocationsMoveHandler))
Me.globeControl.Host.BindingsManager.RegisterAction(asmName, "ToggleUI", New Action(AddressOf Me.ToggleUIHandler))

With the actions registered and handlers associated with them, the actual handlers need to be implemented.  All event handler methods must be of the following signature:

C#

public bool EventHandler(EventData cause)

VB

Public Function EventHandler(ByVal cause As EventData) As Boolean

Let's look at the ToggleRoads event implementation:

C#

private delegate void UIEventHandlerDelegate(EventData cause);

...

public bool ToggleRoadsHandler(EventData cause)
{
if(cause.Activate)
{
// toggle through road types
_roadState++;

if(_roadState > RoadState.Roads)
_roadState = RoadState.Aerial;

// remove whatever one is currently there
this.globeControl.Host.DataSources.Remove("Roads", "Roads");

// add the new one
switch(_roadState)
{
case RoadState.Aerial:
AddFeederSource("http://local.live.com/Manifests/AT.xml", "Roads", "Roads", DataSourceUsage.TextureMap);
break;
case RoadState.Hybrid:
AddFeederSource("http://local.live.com/Manifests/HT.xml", "Roads", "Roads", DataSourceUsage.TextureMap);
break;
case RoadState.Roads:
AddFeederSource("http://local.live.com/Manifests/RT.xml", "Roads", "Roads", DataSourceUsage.TextureMap);
break;
}

// update the status bar
this.BeginInvoke(new UIEventHandlerDelegate(ToggleRoadsUI), cause);
}
return true;
}

private void ToggleRoadsUI(EventData cause)
{
lblRoads.Text = _roadState.ToString();
this.globeControl.Refresh();
}

VB

Private Delegate Sub UIEventHandlerDelegate(ByVal cause As EventData)

...

Public Function ToggleRoadsHandler(ByVal cause As EventData) As Boolean
If cause.Activate Then
' toggle through road types
_roadState += 1

If _roadState > RoadState.Roads Then
_roadState = RoadState.Aerial
End If

' remove whatever one is currently there
Me.globeControl.Host.DataSources.Remove("Roads", "Roads")

' add the new one
Select Case _roadState
Case RoadState.Aerial
AddFeederSource("http:'local.live.com/Manifests/AT.xml", "Roads", "Roads", DataSourceUsage.TextureMap)
Case RoadState.Hybrid
AddFeederSource("http:'local.live.com/Manifests/HT.xml", "Roads", "Roads", DataSourceUsage.TextureMap)
Case RoadState.Roads
AddFeederSource("http:'local.live.com/Manifests/RT.xml", "Roads", "Roads", DataSourceUsage.TextureMap)
End Select

' update the status bar
Me.BeginInvoke(New UIEventHandlerDelegate(AddressOf ToggleRoadsUI), cause)
End If
Return True
End Function

Private Sub ToggleRoadsUI(ByVal cause As EventData)
lblRoads.Text = _roadState.ToString()
Me.globeControl.Refresh()
End Sub

The implementations above simply check to see if the event is being activated and, if so, removes the current road layer and adds the next one in the series.  The one thing to keep in mind is that the event handler methods are called inside the rendering thread of the VE3D control, which is not the thread the windows form UI is located on.  Therefore, if the form UI needs to be updated in any way, one must use the BeginInvoke method and a delegate method to update any UI controls.

Next, let's look at the implementation of ToggleUI:

C#

// toggle UI items off/on
public bool ToggleUIHandler(EventData cause)
{
if(cause.Activate)
{
this.globeControl.Host.WorldEngine.ShowNavigationControl = !this.globeControl.Host.WorldEngine.ShowNavigationControl;
this.globeControl.Host.WorldEngine.ShowCursorLocationInformation = !this.globeControl.Host.WorldEngine.ShowCursorLocationInformation;
this.globeControl.Host.WorldEngine.ShowScale = !this.globeControl.Host.WorldEngine.ShowScale;
}
return true;
}

VB

' toggle UI items off/on
Public Function ToggleUIHandler(ByVal cause As EventData) As Boolean
If cause.Activate Then
Me.globeControl.Host.WorldEngine.ShowNavigationControl = Not Me.globeControl.Host.WorldEngine.ShowNavigationControl
Me.globeControl.Host.WorldEngine.ShowCursorLocationInformation = Not Me.globeControl.Host.WorldEngine.ShowCursorLocationInformation
Me.globeControl.Host.WorldEngine.ShowScale = Not Me.globeControl.Host.WorldEngine.ShowScale
End If
Return True
End Function

This code simply changes the boolean values of several UI items.  The WorldEngine object contains several other UI elements.  The ShowUI property overrides all other properties and determines if anything is shown at all.  This property also determines whether the globe in the lower-left corner is displayed.

Be sure to check the source code for the full demo linked above for the location handler methods.  I omitted them here since it is just more of the same type of code above.

Running the Demo

To run the demo, do the following:

  1. Copy BindingsWiimote.xml to the %APPDATA%\Microsoft\Virtual Earth 3D directory
  2. Pair the Wiimote to the computer.  See the WiimoteLib article for more information on how to do that
  3. Run the executable

Conclusion

With the above code, we have written a Wiimote-driven interface for Virtual Earth 3D.  The demo and source code linked above contain a few more features and bindings which enhance the application a bit more.  Be sure to give the full demo a try and check out the full source code for a few more implementation details.

Additional Information

Thanks

Thanks to Michelle Leavitt and Giovanni Montrone for testing the control scheme and helping to determine the best feel using the Wiimote.

Bio

Brian is a Microsoft C# MVP and a recognized .NET expert with over 6 years experience developing .NET solutions, and over 9 years of professional experience architecting and developing solutions using Microsoft technologies and platforms, although he has been "coding for fun" for as long as he can remember.  Outside the world of .NET and business applications, Brian enjoys developing both hardware and software projects in the areas of gaming, robotics, and whatever else strikes his fancy for the next ten minutes. He rarely passes up an opportunity to dive into a C/C++ or assembly language project.  You can reach Brian via his blog at http://www.brianpeek.com/.

Tags:

Follow the Discussion

  • Noticias externasNoticias externas

    Check out this cool article on Coding4Fun that shows you how you can control the Wiimote (the Nintendo

  • Beth Massi - Sharing the goodness that is VBBeth Massi - Sharing the goodness that is VB

    Check out this cool article on Coding4Fun that shows you how you can control the Wiimote (the Nintendo

  • JumbaliaJumbalia

    apparently when you copy the line

    "http:\\local.live.com/Manifests/HD.xml"

    and paste it into VS it is changed to

    "http:'local.live.com/Manifests/HD.xml"

    problem solved..though I still can't find the source code...

  • JumbaliaJumbalia

    Am I going crazy or is there no link for downloading the source code?   The Download link goes to Channel9, and once there it states "Code is available in C# and Visual Basic." But there is no link!

    (Again, I may just be crazy)

    Also, I am trying to DL the code because I am getting the error: "Invalid URI: The Authority/Host could not be parsed."

    When I try to run AddFeederSource.  I was hoping the source code could shed some light.

  • ValeriaValeria

    Could you tell me what the 'delta' value in AxisEventData does?  For example 'x' in this line of code:

    this.Execute(new AxisEventData(new EventKey(this, (int)WiimoteEvent.IRX), x));

  • Clint RutkasClint I'm a "developer"

    @Valeria:  Delta implies change in value.

    So If I was at point 2 and moved to point 5, my delta would be 3.

  • shailendrashailendra

    Hi,

    The above code is working fine and its greate.

    now after creating the globe control and adding layers on it. i am trying to add push pin at a perticular latitued and langitued.But i am not able to add it.

    Please help me.

    Please tell me the way how to add it

    my email id is shailendradamodare@gmail.com

    Thanks

    Shailendra  

  • RobRob

    The recent Virtual Earth update has broken this demo. The executable will not run saying it cannot find certain resources. Checking the Global Assembly Cache shows the Microsoft.MapPoint assemblies have be updated from 2.0 to 2.5.

    The project will no longer compile, showing errors with Microsoft.MapPoint.Binding.BindingsManager.RegisterAction, Microsoft.MapPoint.Rendering3D.Host, etc.

    Anyone know how to fix this to work?

  • MaxMax

    HELP! WHEN I OPEN UP THE CSPROJ FILE IN NOTEPAD THERE IS NO ITEM GROUP. WHEN I MAKE ONE IT MALFUNCTIONS. HELP!!!!

  • Clint RutkasClint I'm a "developer"

    @Max:  Why do you need to do this

  • annapengannapeng

    weew!!! it's amazing!!

    i got troubled, understanding those codes... however earth was really challenging... with best regars! thankz=)...

  • BrianPeek.comBrianPeek.​com

    Time to talk a little bit about my PDC demo… About a year ago, I wrote an application to control Virtual

  • BrianPeek.comBrianPeek.​com

    Here is a list of my current Coding4Fun articles: WiiEarthVR Animated Musical Holiday Light Show - Version

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.