Sample Sensors App: Windows 8* Compass and Inclinometer driving Google Street View* API using WinJS

Download Article

Download Sample Sensors App: Windows 8* Compass and Inclinometer driving Google Street View* API using WinJS [PDF 326KB]


MapPanningJS is an example Windows Store app written in WinJS using two sensors on Ultrabook™ devices to control Google's popular Street View API. The Compass class is used to control horizontal panning (heading), and the Inclinometer class is used to control vertical tilt (pitch). The user can see different views by pointing in different directions or looking up and down simply by physically manipulating the Ultrabook.

Technical Overview

The Compass and Inclinometer classes are part of the Windows.Devices.Sensors namespace available in Windows 8 via the WinRT API used by Windows Store apps. This namespace is available to all WinRT language projections including C#, C/C++, and JavaScript*. This sample is written in the Windows Library for JavaScript commonly referred to as WinJS. The sensors in our example send data to a full-screen viewport rendering of street level pictures using Google Maps Street View API. The Street View API is a web service freely provided by Google. If you are going to use Google Street View API in your business applications, you will need an API key. Use the link below to find out more.

Technical Details

The following pseudo-code describes building the sample app at a high level. See the supplied sample code for concrete implementation details.

  1. Initialize the Compass and Inclinometer
  2. Initialize the Street View Google API viewport
  3. Process ReadingChanged events for each sensor
  4. Create a low-pass filter
  5. Start a timer and communicate with the viewport

Initialize the Compass and Inclinometer

Both the Compass and Inclinometer are members of the Windows.Devices.Sensors namespace. Simply assign a variable to the getDefault() for each sensor. The sensor will not generate readings until a reportInterval is set. Luckily the sensor can provide the minimum reporting interval so we don't need to guess.


 var compass = Windows.Devices.Sensors.Compass.getDefault(); var inclinometer = Windows.Devices.Sensors.Inclinometer.getDefault(); compass.reportInterval = compass.minimumReportInterval; inclinometer.reportInterval = inclinometer.minimumReportInterval; 

Initialize the Street View Google API viewport

Google Street View Image API is a service that renders a viewport based on a number of parameters. Using external APIs with WinJS can be tricky. Google Street View does not play nicely with WinJS out of the box, but there is a simple way for them to co-exist and that is by using an iFrame to host the viewport. To ensure that the Google API works properly it needs to run under the web context (as opposed to local context) so our iFrame src attribute must be prefixed with the ms-appx-web schema. The loading of Google API will happen in the iFrame while the sensor processing will happen in the host app. We'll communicate between our app JS and the iFrame JS using HTML5 Web Messaging.


 <body> <iframe id="map" src="ms-appx-web:///map.html" style="width:100%;height:100%"> </iframe> </body> 


 <html> <head> <title></title> <script src=""> </script> <link href="/map.css" rel="stylesheet" /> <script src="/map.js"></script> </head> <body style="background-color: #008CC4"> <div id="map"></div> </body> </html> 


 var loc = new google.maps.LatLng(40.758692, -73.985341, true); //Times Square var pov = { heading: 0, pitch: 0, zoom: 0 }; var streetViewOptions = { position: loc, pov: pov, linksControl: false, panControl: false, zoomControl: false, disableDefaultUI: true, clickToGo: false, addressControl: false, scrollwheel: false }; var streetView = new google.maps.StreetViewPanorama(document.getElementById("map"), streetViewOptions); 

Once initialized, the full screen viewport will render a panorama of Times Square, New York.

Process ReadingChanged events for each sensor

We have two options for getting readings from the sensors: build a "game loop" and periodically pole the sensors by calling getCurrentReading() or respond to the sensors readingchanged event. We'll do the latter and collect readings in arrays for later processing. When the readingchanged event fires it sends a specific class as the argument depending on the sensor type. For the Compass it's the CompassReadingChangedEventArgs, and for the Inclinometer we'll see InclinometerReadingChangedEventArgs. Both of these classes contain a reading property of type CompassReading or InclinometerReading, respectively. For the Compass we are interested in the readings headingMagneticNorth property and for the Inclinometer we are interested in pitchDegrees. We also want to record the timestamp in both cases.


 var headings = new Array(); var pitches = new Array(); // listen to sensor readingchanged events // record the reading and the timestamp compass.addEventListener("readingchanged", function (args) { headings[headings.length] = { reading: args.reading.headingMagneticNorth, timeStamp: args.reading.timestamp }; }); inclinometer.addEventListener("readingchanged", function (args) { pitches[pitches.length] = { reading: args.reading.pitchDegrees, timeStamp: args.reading.timestamp }; }); 

Create a low-pass filter

Sensor data can be chatty and noisy. Physically moving the Ultrabook can fire sensors every dozen milliseconds, and the magnitude of those readings can be surprisingly varied. To get a good reading that reflects the intention of the user, we'll pass this array of readings through a low-pass filter. Filtering the readings will reduce the noise and result in a less shaky experience with our viewport. There are many implementations of low-pass filtering, and we'll use one of the simplest for this example. Our low-pass filter takes two arguments: the array of reading/timestamp pairs and a smoothing factor. We arbitrarily use a smoothing factor of 2 that can be modified to increase/decrease the smoothing. I'm not going to explain the algorithm in detail, but I'll sum it up by saying it looks at the array and splits the difference between readings accounting for time between samples thus removing exaggerations and outliers.


 function lowPassfilter(readings, smoothing) { var value = readings[0].reading; var lastUpdate = readings[0].timeStamp; for (var i = 1; i < readings.length; ++i) { var now = readings[i].timeStamp; var currentValue = readings[i].reading; var elapsedTime = (now - lastUpdate) / 1000; var filteredValue = value + (currentValue - value) / (smoothing / elapsedTime); readings[i].reading = filteredValue; lastUpdate = now; value = filteredValue; } } 

Create a timer

Timers in JavaScript are easy to create using setInterval(). We’ll use a timer to send messages to the viewport hosted in the iFrame. Once every second feels like a proper frame rate for a full-screen Google Street View app. Sending orientation data to the viewport at a faster rate than it can generate the images results in a loss of fidelity. You may see partially rendered views and/or image panels that don't line up properly. Play around with frame rates until you find one that best suits your needs. During every iteration of the timer, both reading arrays are processed through our filter, and the final result is sent on to the iFrame using HTML Web Messaging. Inside the iFrame our message is picked up, and the new orientation data is sent on to the Google API to render a new viewport.


 setInterval(function () { if (pitches.length > 0) { lowPassfilter(pitches, 2); var pitch = pitches[pitches.length - 1].reading; if (settings.walking) { if (pitch > 10) { iframe.postMessage("walk", "*"); } } else { iframe.postMessage("pitch:" + pitch, "*"); } } if (headings.length > 0) { lowPassfilter(headings, 2); iframe.postMessage("heading:" + headings[headings.length - 1].reading, "*"); } pitches = new Array(); headings = new Array(); }, 1000); 


 function receiveMessage(sender) { var opts =":"); switch (opts[0]) { case "heading": pov.heading = parseFloat(opts[1]); streetView.setPov(pov); break; case "pitch": pov.pitch = parseFloat(opts[1]); streetView.setPov(pov); break; } } 

Performance / Results

On reference Ultrabook hardware, the sensor readingchanged events fire at 16-ms intervals. This rate is much faster than the refresh rate of the viewport that is streaming image panels over the Internet from Google’s API. Observations indicate the readingchanged events are also queued. Attempting to perform viewport updates with each reading change has two undesirable effects: partial updates to the viewport resulting in image misalignment and continued viewport updates after physical orientation changes have stopped. Introducing a reading collection mechanism and passing readings through a low-pass filter combined with matching the viewport update interval to the observed frame rate mitigated these challenges.


Driving publically available APIs from Ultrabook sensors is straightforward in Windows 8. The Windows Runtime (WinRT) provides easy access to the sensors via the Windows.Devices.Sensors namespace.

For more complete information about compiler optimizations, see our Optimization Notice.