├── .gitignore ├── package.json ├── LICENSE.md ├── run-tests.html ├── tests ├── testharness.css ├── EventTarget-tests.js ├── generic-sensor-tests.js ├── testharnessreport.js └── testharness.js ├── index.html ├── README.md ├── src ├── sensor.js ├── geolocation-sensor.js └── motion-sensors.js └── run-geolocation.html /.gitignore: -------------------------------------------------------------------------------- 1 | motion-sensors.js 2 | sensor.js 3 | geolocation-sensor.js 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "motion-sensors-polyfill", 3 | "version": "0.3.7", 4 | "description": "A polyfill for the motion sensors based on the W3C Generic Sensor API", 5 | "main": "motion-sensors.js", 6 | "module": "motion-sensors.js", 7 | "files": [ 8 | "motion-sensors.js", 9 | "sensor.js", 10 | "geolocation-sensor.js", 11 | "/src/" 12 | ], 13 | "directories": { 14 | "test": "tests" 15 | }, 16 | "scripts": { 17 | "build": "cp src/*.js .", 18 | "test": "npm run build", 19 | "checksize": "uglifyjs motion-sensors.js -mc --toplevel | gzip -9 | wc -c" 20 | }, 21 | "author": "Kenneth Rohde Christiansen ", 22 | "homepage": "https://github.com/kenchris/lit-element", 23 | "license": "BSD-3-Clause", 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/kenchris/sensor-polyfills.git" 27 | }, 28 | "devDependencies": { 29 | "eslint": "^5.3.0", 30 | "eslint-config-google": "^0.9.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Intel Corporation 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Intel Corporation nor the names of its contributors 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /run-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generic Sensor API polyfills tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 53 | 54 | 55 |
56 | 57 | -------------------------------------------------------------------------------- /tests/testharness.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans; 3 | } 4 | 5 | #log .warning, 6 | #log .warning a { 7 | color: black; 8 | background: yellow; 9 | } 10 | 11 | #log .error, 12 | #log .error a { 13 | color: white; 14 | background: red; 15 | } 16 | 17 | section#summary { 18 | margin-bottom:1em; 19 | } 20 | 21 | table#results { 22 | border-collapse:collapse; 23 | table-layout:fixed; 24 | width:100%; 25 | } 26 | 27 | table#results th:first-child, 28 | table#results td:first-child { 29 | width:4em; 30 | } 31 | 32 | table#results th:last-child, 33 | table#results td:last-child { 34 | width:50%; 35 | } 36 | 37 | table#results.assertions th:last-child, 38 | table#results.assertions td:last-child { 39 | width:35%; 40 | } 41 | 42 | table#results th { 43 | padding:0; 44 | padding-bottom:0.5em; 45 | border-bottom:medium solid black; 46 | } 47 | 48 | table#results td { 49 | padding:1em; 50 | padding-bottom:0.5em; 51 | border-bottom:thin solid black; 52 | } 53 | 54 | tr.pass > td:first-child { 55 | color:green; 56 | } 57 | 58 | tr.fail > td:first-child { 59 | color:red; 60 | } 61 | 62 | tr.timeout > td:first-child { 63 | color:red; 64 | } 65 | 66 | tr.notrun > td:first-child { 67 | color:blue; 68 | } 69 | 70 | .pass > td:first-child, .fail > td:first-child, .timeout > td:first-child, .notrun > td:first-child { 71 | font-variant:small-caps; 72 | } 73 | 74 | table#results span { 75 | display:block; 76 | } 77 | 78 | table#results span.expected { 79 | font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace; 80 | white-space:pre; 81 | } 82 | 83 | table#results span.actual { 84 | font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace; 85 | white-space:pre; 86 | } 87 | 88 | span.ok { 89 | color:green; 90 | } 91 | 92 | tr.error { 93 | color:red; 94 | } 95 | 96 | span.timeout { 97 | color:red; 98 | } 99 | 100 | span.ok, span.timeout, span.error { 101 | font-variant:small-caps; 102 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generic Sensor API polyfills example 5 | 20 | 21 | 26 | 27 | 100 | 101 | 102 |
103 |
104 |
105 |
106 | 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | W3C Generic Sensor API polyfills 2 | === 3 | 4 | This is a polyfill for [Generic Sensor](https://w3c.github.io/sensors/)-based [motions sensors](https://w3c.github.io/motion-sensors/) to make migration from the old [DeviceOrientationEvent](https://w3c.github.io/deviceorientation/spec-source-orientation.html#deviceorientation)/[DeviceMotionEvent](https://w3c.github.io/deviceorientation/spec-source-orientation.html#devicemotion) to the new APIs a smoother experience. 5 | 6 | In particular, this polyfill will allow the users of modern browsers to get a feel of the new API shape before it ships more broadly. 7 | 8 | `src/motion-sensors.js` implements the following interfaces: 9 | 10 | - [`Sensor`](https://w3c.github.io/sensors/#the-sensor-interface) 11 | - [`Accelerometer`](https://w3c.github.io/accelerometer/#accelerometer-interface) 12 | - [`LinearAccelerationSensor`](https://w3c.github.io/accelerometer/#linearaccelerationsensor-interface) 13 | - [`GravitySensor`](https://w3c.github.io/accelerometer/#gravitysensor-interface) 14 | - [`Gyroscope`](https://w3c.github.io/gyroscope/#gyroscope-interface) 15 | - [`RelativeOrientationSensor`](https://w3c.github.io/orientation-sensor/#relativeorientationsensor-interface) 16 | - [`AbsoluteOrientationSensor`](https://w3c.github.io/orientation-sensor/#absoluteorientationsensor-interface) 17 | 18 | `src/geolocation.js` implements the following interface: 19 | 20 | - [`GeolocationSensor`](https://w3c.github.io/geolocation-sensor/#geolocationsensor-interface) 21 | 22 | How to use the polyfill 23 | === 24 | 25 | - Copy [`src/motion-sensors.js`](https://raw.githubusercontent.com/kenchris/sensor-polyfills/master/src/motion-sensors.js) ([source](https://github.com/kenchris/sensor-polyfills/blob/master/src/motion-sensors.js)) into your project, or install via [npm](https://www.npmjs.com/package/motion-sensors-polyfill ) (`$ npm i motion-sensors-polyfill`). 26 | - Import the motion sensor objects in your HTML (see [` 39 | ``` 40 | - That's it. See [AbsoluteOrientationSensor demo](https://intel.github.io/generic-sensor-demos/orientation-phone/) and [RelativeOrientationSensor demo](https://intel.github.io/generic-sensor-demos/orientation-phone/?relative=1) ([code](https://github.com/intel/generic-sensor-demos/blob/master/orientation-phone/index.html)) for examples. 41 | 42 | How to enable the native implementation in Chrome 43 | === 44 | 45 | *Chrome 67 or later:* the native implementation is enabled by default. 46 | 47 | The Generic Sensor Extra Classes (`chrome://flags/#enable-generic-sensor-extra-classes`) feature flag can be activated to enable a few additional sensor types: 48 | - `AmbientLightSensor` 49 | - `Magnetometer` 50 | 51 | Test suite 52 | === 53 | 54 | Run [web-platform-tests](https://github.com/w3c/web-platform-tests/) with this polyfill enabled [here](https://kenchris.github.io/sensor-polyfills/run-tests.html). 55 | 56 | Known issues 57 | === 58 | 59 | - `AbsoluteOrientationSensor` on iOS uses non-standard [`webkitCompassHeading`](https://developer.apple.com/documentation/webkitjs/deviceorientationevent/1804777-webkitcompassheading) that reports wrong readings if the device is held in its [`portrait-secondary`](https://w3c.github.io/screen-orientation/#dom-orientationtype-portrait-secondary) orientation. Specifically, the `webkitCompassHeading` flips by 180 degrees when tilted only slightly. 60 | 61 | Learn more 62 | === 63 | 64 | - [Sensors For The Web article on Google's Web Fundaments](https://developers.google.com/web/updates/2017/09/sensors-for-the-web) - a web developer-oriented article explaining how to use the Generic Sensor-based APIs. 65 | 66 | Reporting a security issue 67 | === 68 | If you have information about a security issue or vulnerability with an Intel-maintained open source project on https://github.com/intel, please send an e-mail to secure@intel.com. Encrypt sensitive information using our PGP public key. For issues related to Intel products, please visit https://security-center.intel.com. 69 | -------------------------------------------------------------------------------- /tests/EventTarget-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function runEventTarget_addEventListener(eventTarget) { 4 | test(() => { 5 | const target = new eventTarget(); 6 | 7 | assert_equals(target.addEventListener("x", null, false), undefined); 8 | assert_equals(target.addEventListener("x", null, true), undefined); 9 | assert_equals(target.addEventListener("x", null), undefined); 10 | }, "Adding a null event listener should succeed"); 11 | } 12 | 13 | function runEventTarget_removeEventListener(eventTarget) { 14 | test(() => { 15 | const target = new eventTarget(); 16 | 17 | assert_equals(target.removeEventListener("x", null, false), undefined); 18 | assert_equals(target.removeEventListener("x", null, true), undefined); 19 | assert_equals(target.removeEventListener("x", null), undefined); 20 | }, "removing a null event listener should succeed"); 21 | } 22 | 23 | setup({ 24 | "allow_uncaught_exception": true, 25 | }) 26 | 27 | function runEventTarget_dispatchEvent(testElement) { 28 | customElements.define('test-b', testElement); 29 | 30 | test(function() { 31 | assert_throws(new TypeError(), function() { document.dispatchEvent(null) }) 32 | }, "Calling dispatchEvent(null).") 33 | 34 | var dispatch_dispatch = async_test("If the event's dispatch flag is set, an InvalidStateError must be thrown.") 35 | dispatch_dispatch.step(function() { 36 | var e = document.createEvent("Event") 37 | e.initEvent("type", false, false) 38 | var target = document.createElement("test-b"); 39 | target.addEventListener("type", dispatch_dispatch.step_func(function() { 40 | assert_throws("InvalidStateError", function() { 41 | target.dispatchEvent(e) 42 | }) 43 | assert_throws("InvalidStateError", function() { 44 | document.dispatchEvent(e) 45 | }) 46 | }), false) 47 | assert_equals(target.dispatchEvent(e), true, "dispatchEvent must return true") 48 | dispatch_dispatch.done() 49 | }) 50 | 51 | test(function() { 52 | // https://www.w3.org/Bugs/Public/show_bug.cgi?id=17713 53 | // https://www.w3.org/Bugs/Public/show_bug.cgi?id=17714 54 | var e = document.createEvent("Event") 55 | e.initEvent("type", false, false) 56 | var called = [] 57 | var target = document.createElement("test-b"); 58 | target.addEventListener("type", function() { 59 | called.push("First") 60 | throw new Error() 61 | }, false) 62 | target.addEventListener("type", function() { 63 | called.push("Second") 64 | }, false) 65 | assert_equals(target.dispatchEvent(e), true, "dispatchEvent must return true") 66 | assert_array_equals(called, ["First", "Second"], 67 | "Should have continued to call other event listeners") 68 | }, "Exceptions from event listeners must not be propagated.") 69 | 70 | async_test(function() { 71 | var results = [] 72 | var outerb = document.createElement("test-b") 73 | var middleb = outerb.appendChild(document.createElement("test-b")) 74 | var innerb = middleb.appendChild(document.createElement("test-b")) 75 | outerb.addEventListener("x", this.step_func(function() { 76 | middleb.addEventListener("x", this.step_func(function() { 77 | results.push("middle") 78 | }), true) 79 | results.push("outer") 80 | }), true) 81 | innerb.dispatchEvent(new Event("x")) 82 | console.log(results); 83 | assert_array_equals(results, ["outer", "middle"]) 84 | this.done() 85 | }, "Event listeners added during dispatch should be called"); 86 | 87 | async_test(function() { 88 | var results = [] 89 | var b = document.createElement("test-b") 90 | b.addEventListener("x", this.step_func(function() { 91 | results.push(1) 92 | }), true) 93 | b.addEventListener("x", this.step_func(function() { 94 | results.push(2) 95 | }), false) 96 | b.addEventListener("x", this.step_func(function() { 97 | results.push(3) 98 | }), true) 99 | b.dispatchEvent(new Event("x")) 100 | assert_array_equals(results, [1, 2, 3]) 101 | this.done() 102 | }, "Event listeners should be called in order of addition") 103 | 104 | test(function() { 105 | var event_type = "foo"; 106 | var target = document.createElement("test-b"); 107 | var parent = document.createElement("test-b"); 108 | parent.appendChild(target); 109 | 110 | var default_prevented; 111 | parent.addEventListener(event_type, function(e) {}, true); 112 | target.addEventListener(event_type, function(e) { 113 | evt.preventDefault(); 114 | default_prevented = evt.defaultPrevented; 115 | }, true); 116 | target.addEventListener(event_type, function(e) {}, true); 117 | var evt = document.createEvent("Event"); 118 | evt.initEvent(event_type, true, true); 119 | assert_true(parent.dispatchEvent(evt)); 120 | assert_false(target.dispatchEvent(evt)); 121 | assert_true(default_prevented); 122 | }, "Return value of EventTarget.dispatchEvent."); 123 | } 124 | 125 | function runEventTarget_constructible(eventTarget) { 126 | test(() => { 127 | const target = new eventTarget(); 128 | const event = new Event("foo", { bubbles: true, cancelable: false }); 129 | let callCount = 0; 130 | 131 | function listener(e) { 132 | assert_equals(e, event); 133 | ++callCount; 134 | } 135 | 136 | target.addEventListener("foo", listener); 137 | 138 | target.dispatchEvent(event); 139 | assert_equals(callCount, 1); 140 | 141 | target.dispatchEvent(event); 142 | assert_equals(callCount, 2); 143 | 144 | target.removeEventListener("foo", listener); 145 | target.dispatchEvent(event); 146 | assert_equals(callCount, 2); 147 | }, "A constructed EventTarget can be used as expected"); 148 | 149 | test(() => { 150 | class NicerEventTarget extends eventTarget { 151 | on(...args) { 152 | this.addEventListener(...args); 153 | } 154 | 155 | off(...args) { 156 | this.removeEventListener(...args); 157 | } 158 | 159 | dispatch(type, detail) { 160 | this.dispatchEvent(new CustomEvent(type, { detail })); 161 | } 162 | } 163 | 164 | const target = new NicerEventTarget(); 165 | const event = new Event("foo", { bubbles: true, cancelable: false }); 166 | const detail = "some data"; 167 | let callCount = 0; 168 | 169 | function listener(e) { 170 | assert_equals(e.detail, detail); 171 | ++callCount; 172 | } 173 | 174 | target.on("foo", listener); 175 | 176 | target.dispatch("foo", detail); 177 | assert_equals(callCount, 1); 178 | 179 | target.dispatch("foo", detail); 180 | assert_equals(callCount, 2); 181 | 182 | target.off("foo", listener); 183 | target.dispatch("foo", detail); 184 | assert_equals(callCount, 2); 185 | }, "EventTarget can be subclassed"); 186 | } -------------------------------------------------------------------------------- /src/sensor.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | function defineProperties(target, descriptions) { 4 | /* eslint-disable-next-line guard-for-in */ 5 | for (const property in descriptions) { 6 | Object.defineProperty(target, property, { 7 | configurable: true, 8 | value: descriptions[property], 9 | }); 10 | } 11 | } 12 | 13 | const privates = new WeakMap(); 14 | 15 | export const EventTargetMixin = (superclass, ...eventNames) => class extends superclass { 16 | constructor(...args) { 17 | // @ts-ignore 18 | super(args); 19 | const eventTarget = document.createDocumentFragment(); 20 | privates.set(this, eventTarget); 21 | } 22 | 23 | addEventListener(type, ...args) { 24 | const eventTarget = privates.get(this); 25 | return eventTarget.addEventListener(type, ...args); 26 | } 27 | 28 | removeEventListener(...args) { 29 | const eventTarget = privates.get(this); 30 | // @ts-ignore 31 | return eventTarget.removeEventListener(...args); 32 | } 33 | 34 | dispatchEvent(event) { 35 | defineProperties(event, {currentTarget: this}); 36 | if (!event.target) { 37 | defineProperties(event, {target: this}); 38 | } 39 | 40 | const eventTarget = privates.get(this); 41 | const retValue = eventTarget.dispatchEvent(event); 42 | 43 | if (retValue && this.parentNode) { 44 | this.parentNode.dispatchEvent(event); 45 | } 46 | 47 | defineProperties(event, {currentTarget: null, target: null}); 48 | 49 | return retValue; 50 | } 51 | }; 52 | 53 | export class EventTarget extends EventTargetMixin(Object) {} 54 | 55 | const __abort__ = Symbol('__abort__'); 56 | 57 | export class AbortSignal extends EventTarget { 58 | constructor() { 59 | super(); 60 | 61 | this[__abort__] = { 62 | aborted: false, 63 | }; 64 | 65 | defineOnEventListener(this, 'abort'); 66 | Object.defineProperty(this, 'aborted', { 67 | get: () => this[__abort__].aborted, 68 | }); 69 | } 70 | 71 | dispatchEvent(event) { 72 | if (event.type === 'abort') { 73 | this[__abort__].aborted = true; 74 | 75 | const methodName = `on${event.type}`; 76 | if (typeof this[methodName] == 'function') { 77 | this[methodName](event); 78 | } 79 | } 80 | super.dispatchEvent(event); 81 | } 82 | 83 | toString() { 84 | return '[object AbortSignal]'; 85 | } 86 | } 87 | 88 | export class AbortController { 89 | constructor() { 90 | const signal = new AbortSignal(); 91 | Object.defineProperty(this, 'signal', { 92 | get: () => signal, 93 | }); 94 | } 95 | 96 | abort() { 97 | let abort = new Event('abort'); 98 | this.signal.dispatchEvent(abort); 99 | } 100 | 101 | toString() { 102 | return '[object AbortController]'; 103 | } 104 | } 105 | 106 | function defineOnEventListener(target, name) { 107 | Object.defineProperty(target, `on${name}`, { 108 | enumerable: true, 109 | configurable: false, 110 | writable: true, 111 | value: null, 112 | }); 113 | } 114 | 115 | export function defineReadonlyProperties(target, slot, descriptions) { 116 | const propertyBag = target[slot]; 117 | /* eslint-disable-next-line guard-for-in */ 118 | for (const property in descriptions) { 119 | propertyBag[property] = descriptions[property]; 120 | Object.defineProperty(target, property, { 121 | get: () => propertyBag[property], 122 | }); 123 | } 124 | } 125 | 126 | export class SensorErrorEvent extends Event { 127 | constructor(type, errorEventInitDict) { 128 | super(type, errorEventInitDict); 129 | 130 | if (!errorEventInitDict || !(errorEventInitDict.error instanceof DOMException)) { 131 | throw TypeError( 132 | 'Failed to construct \'SensorErrorEvent\':' + 133 | '2nd argument much contain \'error\' property' 134 | ); 135 | } 136 | 137 | Object.defineProperty(this, 'error', { 138 | configurable: false, 139 | writable: false, 140 | value: errorEventInitDict.error, 141 | }); 142 | } 143 | } 144 | 145 | const SensorState = { 146 | IDLE: 1, 147 | ACTIVATING: 2, 148 | ACTIVE: 3, 149 | }; 150 | 151 | export const __sensor__ = Symbol('__sensor__'); 152 | const slot = __sensor__; 153 | 154 | export const notifyError = Symbol('Sensor.notifyError'); 155 | export const notifyActivatedState = Symbol('Sensor.notifyActivatedState'); 156 | 157 | export const activateCallback = Symbol('Sensor.activateCallback'); 158 | export const deactivateCallback = Symbol('Sensor.deactivateCallback'); 159 | 160 | export class Sensor extends EventTarget { 161 | [activateCallback]() {} 162 | [deactivateCallback]() {} 163 | 164 | [notifyError](message, name) { 165 | let error = new SensorErrorEvent('error', { 166 | error: new DOMException(message, name), 167 | }); 168 | this.dispatchEvent(error); 169 | this.stop(); 170 | } 171 | 172 | [notifyActivatedState]() { 173 | let activate = new Event('activate'); 174 | this[slot].activated = true; 175 | this.dispatchEvent(activate); 176 | this[slot].state = SensorState.ACTIVE; 177 | } 178 | 179 | constructor(options) { 180 | super(); 181 | 182 | this[__sensor__] = { 183 | // Internal slots 184 | state: SensorState.IDLE, 185 | frequency: null, 186 | 187 | // Property backing 188 | activated: false, 189 | hasReading: false, 190 | timestamp: null, 191 | }; 192 | 193 | defineOnEventListener(this, 'reading'); 194 | defineOnEventListener(this, 'activate'); 195 | defineOnEventListener(this, 'error'); 196 | 197 | Object.defineProperty(this, 'activated', { 198 | get: () => this[slot].activated, 199 | }); 200 | Object.defineProperty(this, 'hasReading', { 201 | get: () => this[slot].hasReading, 202 | }); 203 | Object.defineProperty(this, 'timestamp', { 204 | get: () => this[slot].timestamp, 205 | }); 206 | 207 | if (window && window.parent != window.top) { 208 | throw new DOMException( 209 | 'Only instantiable in a top-level browsing context', 210 | 'SecurityError' 211 | ); 212 | } 213 | 214 | if (options && typeof(options.frequency) == 'number') { 215 | if (options.frequency > 60) { 216 | this.frequency = options.frequency; 217 | } 218 | } 219 | } 220 | 221 | dispatchEvent(event) { 222 | switch (event.type) { 223 | case 'reading': 224 | case 'error': 225 | case 'activate': 226 | { 227 | const methodName = `on${event.type}`; 228 | if (typeof this[methodName] == 'function') { 229 | this[methodName](event); 230 | } 231 | super.dispatchEvent(event); 232 | break; 233 | } 234 | default: 235 | super.dispatchEvent(event); 236 | } 237 | } 238 | 239 | start() { 240 | if (this[slot].state === SensorState.ACTIVATING || 241 | this[slot].state === SensorState.ACTIVE) { 242 | return; 243 | } 244 | this[slot].state = SensorState.ACTIVATING; 245 | this[activateCallback](); 246 | } 247 | 248 | stop() { 249 | if (this[slot].state === SensorState.IDLE) { 250 | return; 251 | } 252 | this[slot].activated = false; 253 | this[slot].hasReading = false; 254 | this[slot].timestamp = null; 255 | this[deactivateCallback](); 256 | 257 | this[slot].state = SensorState.IDLE; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /run-geolocation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Realtime Positioning 6 | 7 | 8 | 9 | 40 | 134 | 195 | 196 | 197 |
198 |

199 | We're using the navigator.permissions.query() API to find out 200 | whether you allow this site to access your location. Use your 201 | browser UI to grant location access to this site if you wish 202 | to experience this example. 203 |

204 |

205 | Chrome: Secure badge > Location > Always allow on this site 206 |

207 |
208 |
209 |

210 | refresh 211 | 212 | 213 |

214 |
215 | 216 | -------------------------------------------------------------------------------- /tests/generic-sensor-tests.js: -------------------------------------------------------------------------------- 1 | const properties = { 2 | 'Accelerometer' : ['timestamp', 'x', 'y', 'z'], 3 | 'LinearAccelerationSensor' : ['timestamp', 'x', 'y', 'z'], 4 | "GravitySensor" : ['timestamp', 'x', 'y', 'z'], 5 | 'Gyroscope' : ['timestamp', 'x', 'y', 'z'], 6 | 'Magnetometer' : ['timestamp', 'x', 'y', 'z'], 7 | 'AbsoluteOrientationSensor' : ['timestamp', 'quaternion'], 8 | 'RelativeOrientationSensor' : ['timestamp', 'quaternion'], 9 | 'GeolocationSensor' : [ 10 | 'timestamp', 11 | 'latitude', 12 | 'longitude' 13 | ] 14 | }; 15 | 16 | function assert_reading_not_null(sensor) { 17 | for (let property in properties[sensor.constructor.name]) { 18 | let propertyName = properties[sensor.constructor.name][property]; 19 | assert_not_equals(sensor[propertyName], null); 20 | } 21 | } 22 | 23 | function assert_reading_null(sensor) { 24 | for (let property in properties[sensor.constructor.name]) { 25 | let propertyName = properties[sensor.constructor.name][property]; 26 | assert_equals(sensor[propertyName], null); 27 | } 28 | } 29 | 30 | function reading_to_array(sensor) { 31 | const arr = new Array(); 32 | for (let property in properties[sensor.constructor.name]) { 33 | let propertyName = properties[sensor.constructor.name][property]; 34 | let value = sensor[propertyName]; 35 | // Quaternions are arrays, compare as strings. 36 | if (Array.isArray(value)) { 37 | value = value.toString(); 38 | } 39 | arr[property] = value; 40 | } 41 | return arr; 42 | } 43 | 44 | function runGenericSensorTests(sensorType) { 45 | promise_test(async t => { 46 | const sensor = new sensorType(); 47 | const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]); 48 | sensor.start(); 49 | 50 | await sensorWatcher.wait_for("reading"); 51 | assert_reading_not_null(sensor); 52 | assert_true(sensor.hasReading); 53 | 54 | sensor.stop(); 55 | assert_reading_null(sensor); 56 | assert_false(sensor.hasReading); 57 | }, `${sensorType.name}: Test that 'onreading' is called and sensor reading is valid`); 58 | 59 | promise_test(async t => { 60 | const sensor1 = new sensorType(); 61 | const sensor2 = new sensorType(); 62 | const sensorWatcher = new EventWatcher(t, sensor1, ["reading", "error"]); 63 | sensor2.start(); 64 | sensor1.start(); 65 | await sensorWatcher.wait_for("reading"); 66 | 67 | // Reading values are correct for both sensors. 68 | assert_reading_not_null(sensor1); 69 | assert_reading_not_null(sensor2); 70 | 71 | //After first sensor stops its reading values are null, 72 | //reading values for the second sensor remains 73 | sensor1.stop(); 74 | assert_reading_null(sensor1); 75 | assert_reading_not_null(sensor2); 76 | sensor2.stop(); 77 | assert_reading_null(sensor2); 78 | }, `${sensorType.name}: sensor reading is correct`); 79 | 80 | promise_test(async t => { 81 | const sensor = new sensorType(); 82 | const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]); 83 | sensor.start(); 84 | 85 | await sensorWatcher.wait_for("reading"); 86 | const cachedTimeStamp1 = sensor.timestamp; 87 | 88 | await sensorWatcher.wait_for("reading"); 89 | const cachedTimeStamp2 = sensor.timestamp; 90 | 91 | assert_greater_than(cachedTimeStamp2, cachedTimeStamp1); 92 | sensor.stop(); 93 | }, `${sensorType.name}: sensor timestamp is updated when time passes`); 94 | 95 | promise_test(async t => { 96 | const sensor = new sensorType(); 97 | const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]); 98 | assert_false(sensor.activated); 99 | sensor.start(); 100 | assert_false(sensor.activated); 101 | 102 | await sensorWatcher.wait_for("activate"); 103 | assert_true(sensor.activated); 104 | 105 | sensor.stop(); 106 | assert_false(sensor.activated); 107 | }, `${sensorType.name}: Test that sensor can be successfully created and its states are correct.`); 108 | 109 | promise_test(async t => { 110 | const sensor = new sensorType(); 111 | const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]); 112 | const start_return = sensor.start(); 113 | 114 | await sensorWatcher.wait_for("activate"); 115 | assert_equals(start_return, undefined); 116 | sensor.stop(); 117 | }, `${sensorType.name}: sensor.start() returns undefined`); 118 | 119 | promise_test(async t => { 120 | const sensor = new sensorType(); 121 | const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]); 122 | sensor.start(); 123 | sensor.start(); 124 | 125 | await sensorWatcher.wait_for("activate"); 126 | assert_true(sensor.activated); 127 | sensor.stop(); 128 | }, `${sensorType.name}: no exception is thrown when calling start() on already started sensor`); 129 | 130 | promise_test(async t => { 131 | const sensor = new sensorType(); 132 | const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]); 133 | sensor.start(); 134 | 135 | await sensorWatcher.wait_for("activate"); 136 | const stop_return = sensor.stop(); 137 | assert_equals(stop_return, undefined); 138 | }, `${sensorType.name}: sensor.stop() returns undefined`); 139 | 140 | promise_test(async t => { 141 | const sensor = new sensorType(); 142 | const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]); 143 | sensor.start(); 144 | 145 | await sensorWatcher.wait_for("activate"); 146 | sensor.stop(); 147 | sensor.stop(); 148 | assert_false(sensor.activated); 149 | }, `${sensorType.name}: no exception is thrown when calling stop() on already stopped sensor`); 150 | 151 | promise_test(async t => { 152 | const sensor = new sensorType(); 153 | const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]); 154 | const visibilityChangeWatcher = new EventWatcher(t, document, "visibilitychange"); 155 | sensor.start(); 156 | 157 | await sensorWatcher.wait_for("reading"); 158 | assert_reading_not_null(sensor); 159 | const cachedSensor1 = reading_to_array(sensor); 160 | 161 | const win = window.open('', '_blank'); 162 | await visibilityChangeWatcher.wait_for("visibilitychange"); 163 | const cachedSensor2 = reading_to_array(sensor); 164 | 165 | win.close(); 166 | sensor.stop(); 167 | assert_array_equals(cachedSensor1, cachedSensor2); 168 | }, `${sensorType.name}: sensor readings can not be fired on the background tab`); 169 | } 170 | 171 | function runGenericSensorInsecureContext(sensorType) { 172 | test(() => { 173 | assert_false(sensorType in window, `${sensorType} must not be exposed`); 174 | }, `${sensorType} is not exposed in an insecure context`); 175 | } 176 | 177 | function runGenericSensorOnerror(sensorType) { 178 | promise_test(async t => { 179 | const sensor = new sensorType(); 180 | const sensorWatcher = new EventWatcher(t, sensor, ["error", "activate"]); 181 | sensor.start(); 182 | 183 | const event = await sensorWatcher.wait_for("error"); 184 | assert_false(sensor.activated); 185 | assert_equals(event.error.name, 'NotReadableError'); 186 | }, `${sensorType.name}: 'onerror' event is fired when sensor is not supported`); 187 | } -------------------------------------------------------------------------------- /src/geolocation-sensor.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { 3 | __sensor__, 4 | Sensor, 5 | SensorErrorEvent, 6 | activateCallback, 7 | deactivateCallback, 8 | notifyActivatedState, 9 | notifyError, 10 | // AbortController, 11 | // AbortSignal, 12 | } from './sensor.js'; 13 | 14 | const slot = __sensor__; 15 | 16 | async function obtainPermission() { 17 | let state = 'prompt'; // Default for geolocation. 18 | // @ts-ignore 19 | if (navigator.permissions) { 20 | // @ts-ignore 21 | const permission = await navigator.permissions.query({name: 'geolocation'}); 22 | state = permission.state; 23 | } 24 | 25 | return new Promise((resolve) => { 26 | if (state === 'granted') { 27 | return resolve(state); 28 | } 29 | 30 | const successFn = (_) => { 31 | resolve('granted'); 32 | }; 33 | 34 | const errorFn = (err) => { 35 | if (err.code === err.PERMISSION_DENIED) { 36 | resolve('denied'); 37 | } else { 38 | resolve(state); 39 | } 40 | }; 41 | 42 | const options = {maximumAge: Infinity, timeout: 0}; 43 | navigator.geolocation.getCurrentPosition(successFn, errorFn, options); 44 | }); 45 | } 46 | 47 | function register(options, onreading, onerror, onactivated) { 48 | const handleEvent = (position) => { 49 | const timestamp = position.timestamp - performance.timing.navigationStart; 50 | const coords = position.coords; 51 | 52 | onreading(timestamp, coords); 53 | }; 54 | 55 | const handleError = (error) => { 56 | let type; 57 | switch (error.code) { 58 | case error.TIMEOUT: 59 | type = 'TimeoutError'; 60 | break; 61 | case error.PERMISSION_DENIED: 62 | type = 'NotAllowedError'; 63 | break; 64 | case error.POSITION_UNAVAILABLE: 65 | type = 'NotReadableError'; 66 | break; 67 | default: 68 | type = 'UnknownError'; 69 | } 70 | onerror(error.message, type); 71 | }; 72 | 73 | const watchOptions = { 74 | enableHighAccuracy: false, 75 | maximumAge: 0, 76 | timeout: Infinity, 77 | }; 78 | 79 | const watchId = navigator.geolocation.watchPosition( 80 | handleEvent, handleError, watchOptions 81 | ); 82 | 83 | return watchId; 84 | } 85 | 86 | function deregister(watchId) { 87 | navigator.geolocation.clearWatch(watchId); 88 | } 89 | 90 | // Old geolocation API is FILO; on Chrome at least. 91 | class FIFOGeolocationEvents { 92 | constructor() { 93 | if (!this.constructor.instance) { 94 | this.constructor.instance = this; 95 | } 96 | 97 | // You can iterate through the elements of a map in insertion order. 98 | this.subscribers = new Map(); 99 | this.options = {}; 100 | this.watchId = null; 101 | 102 | this.lastReading = null; 103 | 104 | return this.constructor.instance; 105 | } 106 | 107 | unsubscribe(obj) { 108 | this.subscribers.delete(obj); 109 | if (!this.subscribers.size && this.watchId) { 110 | deregister(this.watchId); 111 | this.watchId = null; 112 | } 113 | } 114 | 115 | subscribe(obj, options, onreading, onerror) { 116 | const fifoOnReading = (...args) => { 117 | this.lastReading = args; 118 | for ({onreading} of this.subscribers.values()) { 119 | if (typeof onreading === 'function') { 120 | onreading(...args); 121 | } 122 | } 123 | }; 124 | 125 | const fifoOnError = (...args) => { 126 | for ({onerror} of this.subscribers.values()) { 127 | if (typeof onerror === 'function') { 128 | onerror(...args); 129 | } 130 | } 131 | }; 132 | 133 | // TODO(spec): Generate the most precise options here 134 | // ie. lowest maximum-age and highest precision. 135 | this.options = options; 136 | 137 | if (this.watchId) { 138 | deregister(this.watchId); 139 | } 140 | // TODO(spec): Ensure lastReading is still valid. 141 | if (this.lastReading && typeof onreading === 'function') { 142 | onreading(...this.lastReading); 143 | } 144 | this.subscribers.set(obj, {onreading, onerror}); 145 | this.watchId = register(this.options, 146 | fifoOnReading, fifoOnError 147 | ); 148 | } 149 | } 150 | 151 | // @ts-ignore 152 | export const GeolocationSensor = window.GeolocationSensor || 153 | class GeolocationSensor extends Sensor { 154 | static async read(options = {}) { 155 | return new Promise(async (resolve, reject) => { 156 | const onerror = (message, name) => { 157 | let error = new SensorErrorEvent('error', { 158 | error: new DOMException(message, name), 159 | }); 160 | deregister(watchId); 161 | reject(error); 162 | }; 163 | 164 | const onreading = (timestamp, coords) => { 165 | const reading = { 166 | timestamp, 167 | accuracy: coords.accuracy, 168 | altitude: coords.altitude, 169 | altitudeAccuracy: coords.altitudeAccuracy, 170 | heading: coords.heading, 171 | latitude: coords.latitude, 172 | longitude: coords.longitude, 173 | speed: coords.speed, 174 | }; 175 | deregister(watchId); 176 | resolve(reading); 177 | }; 178 | 179 | const signal = options.signal; 180 | if (signal && signal.aborted) { 181 | return reject(new DOMException('Read was cancelled', 'AbortError')); 182 | } 183 | 184 | const permission = await obtainPermission(); 185 | if (permission !== 'granted') { 186 | onerror('Permission denied.', 'NowAllowedError'); 187 | return; 188 | } 189 | const watchId = register(options, onreading, onerror); 190 | 191 | if (signal) { 192 | signal.addEventListener('abort', () => { 193 | deregister(watchId); 194 | reject(new DOMException('Read was cancelled', 'AbortError')); 195 | }); 196 | } 197 | }); 198 | }; 199 | 200 | constructor(options = {}) { 201 | super(options); 202 | 203 | this[slot].options = options; 204 | this[slot].fifo = new FIFOGeolocationEvents(); 205 | 206 | const props = { 207 | latitude: null, 208 | longitude: null, 209 | altitude: null, 210 | accuracy: null, 211 | altitudeAccuracy: null, 212 | heading: null, 213 | speed: null, 214 | }; 215 | 216 | const propertyBag = this[slot]; 217 | /* eslint-disable-next-line guard-for-in */ 218 | for (const propName in props) { 219 | propertyBag[propName] = props[propName]; 220 | Object.defineProperty(this, propName, { 221 | get: () => propertyBag[propName], 222 | }); 223 | } 224 | } 225 | 226 | async [activateCallback]() { 227 | const onreading = (timestamp, coords) => { 228 | this[slot].timestamp = timestamp; 229 | 230 | this[slot].accuracy = coords.accuracy; 231 | this[slot].altitude = coords.altitude; 232 | this[slot].altitudeAccuracy = coords.altitudeAccuracy; 233 | this[slot].heading = coords.heading; 234 | this[slot].latitude = coords.latitude; 235 | this[slot].longitude = coords.longitude; 236 | this[slot].speed = coords.speed; 237 | 238 | this[slot].hasReading = true; 239 | this.dispatchEvent(new Event('reading')); 240 | }; 241 | 242 | const onerror = (message, type) => { 243 | this[notifyError](message, type); 244 | }; 245 | 246 | const permission = await obtainPermission(); 247 | if (permission !== 'granted') { 248 | onerror('Permission denied.', 'NowAllowedError'); 249 | return; 250 | } 251 | 252 | this[slot].fifo.subscribe( 253 | this, this[slot].options, 254 | onreading, onerror 255 | ); 256 | 257 | if (!this[slot].activated) { 258 | this[notifyActivatedState](); 259 | } 260 | } 261 | 262 | [deactivateCallback]() { 263 | this[slot].fifo.unsubscribe(this); 264 | this[slot].timestamp = null; 265 | 266 | this[slot].accuracy = null; 267 | this[slot].altitude = null; 268 | this[slot].altitudeAccuracy = null; 269 | this[slot].heading = null; 270 | this[slot].latitude = null; 271 | this[slot].longitude = null; 272 | this[slot].speed = null; 273 | 274 | this[slot].hasReading = false; 275 | } 276 | }; 277 | -------------------------------------------------------------------------------- /src/motion-sensors.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { 3 | Sensor, 4 | defineReadonlyProperties, 5 | __sensor__, 6 | notifyError, 7 | notifyActivatedState, 8 | activateCallback, 9 | deactivateCallback, 10 | } from './sensor.js'; 11 | 12 | const slot = __sensor__; 13 | const handleEventCallback = Symbol('handleEvent'); 14 | 15 | let orientation; 16 | 17 | // @ts-ignore 18 | if (screen.orientation) { 19 | // @ts-ignore 20 | orientation = screen.orientation; 21 | } else if (screen.msOrientation) { 22 | orientation = screen.msOrientation; 23 | } else { 24 | orientation = {}; 25 | Object.defineProperty(orientation, 'angle', { 26 | get: () => { 27 | return (window.orientation || 0); 28 | }, 29 | }); 30 | } 31 | 32 | const rotationToRadian = (function() { 33 | // Returns Chrome version, or null if not Chrome. 34 | const match = navigator.userAgent.match(/.*Chrome\/([0-9]+)/); 35 | const chromeVersion = match ? parseInt(match[1], 10) : null; 36 | 37 | // DeviceMotion/Orientation API return deg/s (except Chrome, 38 | // but fixing in M66). Gyroscope needs rad/s. 39 | const returnsDegrees = chromeVersion === null || chromeVersion >= 66; 40 | const conversion = returnsDegrees ? Math.PI / 180 : 1.0; 41 | return function(value) { 42 | return value * conversion; 43 | }; 44 | })(); 45 | 46 | const DeviceOrientationMixin = (superclass, ...eventNames) => class extends superclass { 47 | constructor(...args) { 48 | // @ts-ignore 49 | super(args); 50 | 51 | for (const eventName of eventNames) { 52 | if (`on${eventName}` in window) { 53 | this[slot].eventName = eventName; 54 | break; 55 | } 56 | } 57 | } 58 | 59 | [activateCallback]() { 60 | window.addEventListener(this[slot].eventName, this[handleEventCallback].bind(this), {capture: true}); 61 | } 62 | 63 | [deactivateCallback]() { 64 | window.removeEventListener(this[slot].eventName, this[handleEventCallback].bind(this), {capture: true}); 65 | } 66 | }; 67 | 68 | function toQuaternionFromEuler(alpha, beta, gamma) { 69 | const degToRad = Math.PI / 180; 70 | 71 | const x = (beta || 0) * degToRad; 72 | const y = (gamma || 0) * degToRad; 73 | const z = (alpha || 0) * degToRad; 74 | 75 | const cZ = Math.cos(z * 0.5); 76 | const sZ = Math.sin(z * 0.5); 77 | const cY = Math.cos(y * 0.5); 78 | const sY = Math.sin(y * 0.5); 79 | const cX = Math.cos(x * 0.5); 80 | const sX = Math.sin(x * 0.5); 81 | 82 | const qx = sX * cY * cZ - cX * sY * sZ; 83 | const qy = cX * sY * cZ + sX * cY * sZ; 84 | const qz = cX * cY * sZ + sX * sY * cZ; 85 | const qw = cX * cY * cZ - sX * sY * sZ; 86 | 87 | return [qx, qy, qz, qw]; 88 | } 89 | 90 | function rotateQuaternionByAxisAngle(quat, axis, angle) { 91 | const sHalfAngle = Math.sin(angle / 2); 92 | const cHalfAngle = Math.cos(angle / 2); 93 | 94 | const transformQuat = [ 95 | axis[0] * sHalfAngle, 96 | axis[1] * sHalfAngle, 97 | axis[2] * sHalfAngle, 98 | cHalfAngle, 99 | ]; 100 | 101 | function multiplyQuaternion(a, b) { 102 | const qx = a[0] * b[3] + a[3] * b[0] + a[1] * b[2] - a[2] * b[1]; 103 | const qy = a[1] * b[3] + a[3] * b[1] + a[2] * b[0] - a[0] * b[2]; 104 | const qz = a[2] * b[3] + a[3] * b[2] + a[0] * b[1] - a[1] * b[0]; 105 | const qw = a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]; 106 | 107 | return [qx, qy, qz, qw]; 108 | } 109 | 110 | function normalizeQuaternion(quat) { 111 | const length = Math.sqrt(quat[0] ** 2 + quat[1] ** 2 + quat[2] ** 2 + quat[3] ** 2); 112 | if (length === 0) { 113 | return [0, 0, 0, 1]; 114 | } 115 | 116 | return quat.map((v) => v / length); 117 | } 118 | 119 | return normalizeQuaternion(multiplyQuaternion(quat, transformQuat)); 120 | } 121 | 122 | function toMat4FromQuat(mat, q) { 123 | const typed = mat instanceof Float32Array || mat instanceof Float64Array; 124 | 125 | if (typed && mat.length >= 16) { 126 | mat[0] = 1 - 2 * (q[1] ** 2 + q[2] ** 2); 127 | mat[1] = 2 * (q[0] * q[1] - q[2] * q[3]); 128 | mat[2] = 2 * (q[0] * q[2] + q[1] * q[3]); 129 | mat[3] = 0; 130 | 131 | mat[4] = 2 * (q[0] * q[1] + q[2] * q[3]); 132 | mat[5] = 1 - 2 * (q[0] ** 2 + q[2] ** 2); 133 | mat[6] = 2 * (q[1] * q[2] - q[0] * q[3]); 134 | mat[7] = 0; 135 | 136 | mat[8] = 2 * (q[0] * q[2] - q[1] * q[3]); 137 | mat[9] = 2 * (q[1] * q[2] + q[0] * q[3]); 138 | mat[10] = 1 - 2 * (q[0] ** 2 + q[1] ** 2); 139 | mat[11] = 0; 140 | 141 | mat[12] = 0; 142 | mat[13] = 0; 143 | mat[14] = 0; 144 | mat[15] = 1; 145 | } 146 | 147 | return mat; 148 | } 149 | 150 | function deviceToScreen(quaternion) { 151 | return !quaternion ? null : 152 | rotateQuaternionByAxisAngle( 153 | quaternion, 154 | [0, 0, 1], 155 | - orientation.angle * Math.PI / 180 156 | ); 157 | } 158 | 159 | // @ts-ignore 160 | export const RelativeOrientationSensor = window.RelativeOrientationSensor || 161 | class RelativeOrientationSensor extends DeviceOrientationMixin(Sensor, 'deviceorientation') { 162 | constructor(options = {}) { 163 | super(options); 164 | 165 | switch (options.referenceFrame || 'device') { 166 | case 'screen': 167 | Object.defineProperty(this, 'quaternion', { 168 | get: () => deviceToScreen(this[slot].quaternion), 169 | }); 170 | break; 171 | default: // incl. case 'device' 172 | Object.defineProperty(this, 'quaternion', { 173 | get: () => this[slot].quaternion, 174 | }); 175 | } 176 | } 177 | 178 | [handleEventCallback](event) { 179 | // If there is no sensor we will get values equal to null. 180 | if (event.absolute || event.alpha === null) { 181 | // Spec: The implementation can still decide to provide 182 | // absolute orientation if relative is not available or 183 | // the resulting data is more accurate. In either case, 184 | // the absolute property must be set accordingly to reflect 185 | // the choice. 186 | this[notifyError]('Could not connect to a sensor', 'NotReadableError'); 187 | return; 188 | } 189 | 190 | if (!this[slot].activated) { 191 | this[notifyActivatedState](); 192 | } 193 | 194 | this[slot].timestamp = performance.now(); 195 | 196 | this[slot].quaternion = toQuaternionFromEuler( 197 | event.alpha, 198 | event.beta, 199 | event.gamma 200 | ); 201 | 202 | this[slot].hasReading = true; 203 | this.dispatchEvent(new Event('reading')); 204 | } 205 | 206 | [deactivateCallback]() { 207 | super[deactivateCallback](); 208 | this[slot].quaternion = null; 209 | } 210 | 211 | populateMatrix(mat) { 212 | toMat4FromQuat(mat, this.quaternion); 213 | } 214 | }; 215 | 216 | // @ts-ignore 217 | export const AbsoluteOrientationSensor = window.AbsoluteOrientationSensor || 218 | class AbsoluteOrientationSensor extends DeviceOrientationMixin( 219 | Sensor, 'deviceorientationabsolute', 'deviceorientation') { 220 | constructor(options = {}) { 221 | super(options); 222 | 223 | switch (options.referenceFrame || 'device') { 224 | case 'screen': 225 | Object.defineProperty(this, 'quaternion', { 226 | get: () => deviceToScreen(this[slot].quaternion), 227 | }); 228 | break; 229 | default: // incl. case 'device' 230 | Object.defineProperty(this, 'quaternion', { 231 | get: () => this[slot].quaternion, 232 | }); 233 | } 234 | } 235 | 236 | [handleEventCallback](event) { 237 | // If absolute is set, or webkitCompassHeading exists, 238 | // absolute values should be available. 239 | const isAbsolute = event.absolute === true || 'webkitCompassHeading' in event; 240 | const hasValue = event.alpha !== null || event.webkitCompassHeading !== undefined; 241 | 242 | if (!isAbsolute || !hasValue) { 243 | // Spec: If an implementation can never provide absolute 244 | // orientation information, the event should be fired with 245 | // the alpha, beta and gamma attributes set to null. 246 | this[notifyError]('Could not connect to a sensor', 'NotReadableError'); 247 | return; 248 | } 249 | 250 | if (!this[slot].activated) { 251 | this[notifyActivatedState](); 252 | } 253 | 254 | this[slot].hasReading = true; 255 | this[slot].timestamp = performance.now(); 256 | 257 | const heading = event.webkitCompassHeading != null ? 360 - event.webkitCompassHeading : event.alpha; 258 | 259 | this[slot].quaternion = toQuaternionFromEuler( 260 | heading, 261 | event.beta, 262 | event.gamma 263 | ); 264 | 265 | this.dispatchEvent(new Event('reading')); 266 | } 267 | 268 | [deactivateCallback]() { 269 | super[deactivateCallback](); 270 | this[slot].quaternion = null; 271 | } 272 | 273 | populateMatrix(mat) { 274 | toMat4FromQuat(mat, this.quaternion); 275 | } 276 | }; 277 | 278 | // @ts-ignore 279 | export const Gyroscope = window.Gyroscope || 280 | class Gyroscope extends DeviceOrientationMixin(Sensor, 'devicemotion') { 281 | constructor(options) { 282 | super(options); 283 | defineReadonlyProperties(this, slot, { 284 | x: null, 285 | y: null, 286 | z: null, 287 | }); 288 | } 289 | 290 | [handleEventCallback](event) { 291 | // If there is no sensor we will get values equal to null. 292 | if (event.rotationRate.alpha === null) { 293 | this[notifyError]('Could not connect to a sensor', 'NotReadableError'); 294 | return; 295 | } 296 | 297 | if (!this[slot].activated) { 298 | this[notifyActivatedState](); 299 | } 300 | 301 | this[slot].timestamp = performance.now(); 302 | 303 | this[slot].x = rotationToRadian(event.rotationRate.alpha); 304 | this[slot].y = rotationToRadian(event.rotationRate.beta); 305 | this[slot].z = rotationToRadian(event.rotationRate.gamma); 306 | 307 | this[slot].hasReading = true; 308 | this.dispatchEvent(new Event('reading')); 309 | } 310 | 311 | [deactivateCallback]() { 312 | super[deactivateCallback](); 313 | this[slot].x = null; 314 | this[slot].y = null; 315 | this[slot].z = null; 316 | } 317 | }; 318 | 319 | // @ts-ignore 320 | export const Accelerometer = window.Accelerometer || 321 | class Accelerometer extends DeviceOrientationMixin(Sensor, 'devicemotion') { 322 | constructor(options) { 323 | super(options); 324 | defineReadonlyProperties(this, slot, { 325 | x: null, 326 | y: null, 327 | z: null, 328 | }); 329 | } 330 | 331 | [handleEventCallback](event) { 332 | // If there is no sensor we will get values equal to null. 333 | if (event.accelerationIncludingGravity.x === null) { 334 | this[notifyError]('Could not connect to a sensor', 'NotReadableError'); 335 | return; 336 | } 337 | 338 | if (!this[slot].activated) { 339 | this[notifyActivatedState](); 340 | } 341 | 342 | this[slot].timestamp = performance.now(); 343 | 344 | this[slot].x = event.accelerationIncludingGravity.x; 345 | this[slot].y = event.accelerationIncludingGravity.y; 346 | this[slot].z = event.accelerationIncludingGravity.z; 347 | 348 | this[slot].hasReading = true; 349 | this.dispatchEvent(new Event('reading')); 350 | } 351 | 352 | [deactivateCallback]() { 353 | super[deactivateCallback](); 354 | this[slot].x = null; 355 | this[slot].y = null; 356 | this[slot].z = null; 357 | } 358 | }; 359 | 360 | // @ts-ignore 361 | export const LinearAccelerationSensor = window.LinearAccelerationSensor || 362 | class LinearAccelerationSensor extends DeviceOrientationMixin(Sensor, 'devicemotion') { 363 | constructor(options) { 364 | super(options); 365 | defineReadonlyProperties(this, slot, { 366 | x: null, 367 | y: null, 368 | z: null, 369 | }); 370 | } 371 | 372 | [handleEventCallback](event) { 373 | // If there is no sensor we will get values equal to null. 374 | if (event.acceleration.x === null) { 375 | this[notifyError]('Could not connect to a sensor', 'NotReadableError'); 376 | return; 377 | } 378 | 379 | if (!this[slot].activated) { 380 | this[notifyActivatedState](); 381 | } 382 | 383 | this[slot].timestamp = performance.now(); 384 | 385 | this[slot].x = event.acceleration.x; 386 | this[slot].y = event.acceleration.y; 387 | this[slot].z = event.acceleration.z; 388 | 389 | this[slot].hasReading = true; 390 | this.dispatchEvent(new Event('reading')); 391 | } 392 | 393 | [deactivateCallback]() { 394 | super[deactivateCallback](); 395 | this[slot].x = null; 396 | this[slot].y = null; 397 | this[slot].z = null; 398 | } 399 | }; 400 | 401 | // @ts-ignore 402 | export const GravitySensor = window.GravitySensor || 403 | class GravitySensor extends DeviceOrientationMixin(Sensor, 'devicemotion') { 404 | constructor(options) { 405 | super(options); 406 | defineReadonlyProperties(this, slot, { 407 | x: null, 408 | y: null, 409 | z: null, 410 | }); 411 | } 412 | 413 | [handleEventCallback](event) { 414 | // If there is no sensor we will get values equal to null. 415 | if (event.acceleration.x === null || event.accelerationIncludingGravity.x === null) { 416 | this[notifyError]('Could not connect to a sensor', 'NotReadableError'); 417 | return; 418 | } 419 | 420 | if (!this[slot].activated) { 421 | this[notifyActivatedState](); 422 | } 423 | 424 | this[slot].timestamp = performance.now(); 425 | 426 | this[slot].x = event.accelerationIncludingGravity.x - event.acceleration.x; 427 | this[slot].y = event.accelerationIncludingGravity.y - event.acceleration.y; 428 | this[slot].z = event.accelerationIncludingGravity.z - event.acceleration.z; 429 | 430 | this[slot].hasReading = true; 431 | this.dispatchEvent(new Event('reading')); 432 | } 433 | 434 | [deactivateCallback]() { 435 | super[deactivateCallback](); 436 | this[slot].x = null; 437 | this[slot].y = null; 438 | this[slot].z = null; 439 | } 440 | }; 441 | -------------------------------------------------------------------------------- /tests/testharnessreport.js: -------------------------------------------------------------------------------- 1 | /* global add_completion_callback */ 2 | /* global setup */ 3 | 4 | /* 5 | * This file is intended for vendors to implement 6 | * code needed to integrate testharness.js tests with their own test systems. 7 | * 8 | * The default implementation extracts metadata from the tests and validates 9 | * it against the cached version that should be present in the test source 10 | * file. If the cache is not found or is out of sync, source code suitable for 11 | * caching the metadata is optionally generated. 12 | * 13 | * The cached metadata is present for extraction by test processing tools that 14 | * are unable to execute javascript. 15 | * 16 | * Metadata is attached to tests via the properties parameter in the test 17 | * constructor. See testharness.js for details. 18 | * 19 | * Typically test system integration will attach callbacks when each test has 20 | * run, using add_result_callback(callback(test)), or when the whole test file 21 | * has completed, using 22 | * add_completion_callback(callback(tests, harness_status)). 23 | * 24 | * For more documentation about the callback functions and the 25 | * parameters they are called with see testharness.js 26 | */ 27 | 28 | var metadata_generator = { 29 | 30 | currentMetadata: {}, 31 | cachedMetadata: false, 32 | metadataProperties: ['help', 'assert', 'author'], 33 | 34 | error: function(message) { 35 | var messageElement = document.createElement('p'); 36 | messageElement.setAttribute('class', 'error'); 37 | this.appendText(messageElement, message); 38 | 39 | var summary = document.getElementById('summary'); 40 | if (summary) { 41 | summary.parentNode.insertBefore(messageElement, summary); 42 | } 43 | else { 44 | document.body.appendChild(messageElement); 45 | } 46 | }, 47 | 48 | /** 49 | * Ensure property value has contact information 50 | */ 51 | validateContact: function(test, propertyName) { 52 | var result = true; 53 | var value = test.properties[propertyName]; 54 | var values = Array.isArray(value) ? value : [value]; 55 | for (var index = 0; index < values.length; index++) { 56 | value = values[index]; 57 | var re = /(\S+)(\s*)<(.*)>(.*)/; 58 | if (! re.test(value)) { 59 | re = /(\S+)(\s+)(http[s]?:\/\/)(.*)/; 60 | if (! re.test(value)) { 61 | this.error('Metadata property "' + propertyName + 62 | '" for test: "' + test.name + 63 | '" must have name and contact information ' + 64 | '("name " or "name http(s)://")'); 65 | result = false; 66 | } 67 | } 68 | } 69 | return result; 70 | }, 71 | 72 | /** 73 | * Extract metadata from test object 74 | */ 75 | extractFromTest: function(test) { 76 | var testMetadata = {}; 77 | // filter out metadata from other properties in test 78 | for (var metaIndex = 0; metaIndex < this.metadataProperties.length; 79 | metaIndex++) { 80 | var meta = this.metadataProperties[metaIndex]; 81 | if (test.properties.hasOwnProperty(meta)) { 82 | if ('author' == meta) { 83 | this.validateContact(test, meta); 84 | } 85 | testMetadata[meta] = test.properties[meta]; 86 | } 87 | } 88 | return testMetadata; 89 | }, 90 | 91 | /** 92 | * Compare cached metadata to extracted metadata 93 | */ 94 | validateCache: function() { 95 | for (var testName in this.currentMetadata) { 96 | if (! this.cachedMetadata.hasOwnProperty(testName)) { 97 | return false; 98 | } 99 | var testMetadata = this.currentMetadata[testName]; 100 | var cachedTestMetadata = this.cachedMetadata[testName]; 101 | delete this.cachedMetadata[testName]; 102 | 103 | for (var metaIndex = 0; metaIndex < this.metadataProperties.length; 104 | metaIndex++) { 105 | var meta = this.metadataProperties[metaIndex]; 106 | if (cachedTestMetadata.hasOwnProperty(meta) && 107 | testMetadata.hasOwnProperty(meta)) { 108 | if (Array.isArray(cachedTestMetadata[meta])) { 109 | if (! Array.isArray(testMetadata[meta])) { 110 | return false; 111 | } 112 | if (cachedTestMetadata[meta].length == 113 | testMetadata[meta].length) { 114 | for (var index = 0; 115 | index < cachedTestMetadata[meta].length; 116 | index++) { 117 | if (cachedTestMetadata[meta][index] != 118 | testMetadata[meta][index]) { 119 | return false; 120 | } 121 | } 122 | } 123 | else { 124 | return false; 125 | } 126 | } 127 | else { 128 | if (Array.isArray(testMetadata[meta])) { 129 | return false; 130 | } 131 | if (cachedTestMetadata[meta] != testMetadata[meta]) { 132 | return false; 133 | } 134 | } 135 | } 136 | else if (cachedTestMetadata.hasOwnProperty(meta) || 137 | testMetadata.hasOwnProperty(meta)) { 138 | return false; 139 | } 140 | } 141 | } 142 | for (var testName in this.cachedMetadata) { 143 | return false; 144 | } 145 | return true; 146 | }, 147 | 148 | appendText: function(elemement, text) { 149 | elemement.appendChild(document.createTextNode(text)); 150 | }, 151 | 152 | jsonifyArray: function(arrayValue, indent) { 153 | var output = '['; 154 | 155 | if (1 == arrayValue.length) { 156 | output += JSON.stringify(arrayValue[0]); 157 | } 158 | else { 159 | for (var index = 0; index < arrayValue.length; index++) { 160 | if (0 < index) { 161 | output += ',\n ' + indent; 162 | } 163 | output += JSON.stringify(arrayValue[index]); 164 | } 165 | } 166 | output += ']'; 167 | return output; 168 | }, 169 | 170 | jsonifyObject: function(objectValue, indent) { 171 | var output = '{'; 172 | var value; 173 | 174 | var count = 0; 175 | for (var property in objectValue) { 176 | ++count; 177 | if (Array.isArray(objectValue[property]) || 178 | ('object' == typeof(value))) { 179 | ++count; 180 | } 181 | } 182 | if (1 == count) { 183 | for (var property in objectValue) { 184 | output += ' "' + property + '": ' + 185 | JSON.stringify(objectValue[property]) + 186 | ' '; 187 | } 188 | } 189 | else { 190 | var first = true; 191 | for (var property in objectValue) { 192 | if (! first) { 193 | output += ','; 194 | } 195 | first = false; 196 | output += '\n ' + indent + '"' + property + '": '; 197 | value = objectValue[property]; 198 | if (Array.isArray(value)) { 199 | output += this.jsonifyArray(value, indent + 200 | ' '.substr(0, 5 + property.length)); 201 | } 202 | else if ('object' == typeof(value)) { 203 | output += this.jsonifyObject(value, indent + ' '); 204 | } 205 | else { 206 | output += JSON.stringify(value); 207 | } 208 | } 209 | if (1 < output.length) { 210 | output += '\n' + indent; 211 | } 212 | } 213 | output += '}'; 214 | return output; 215 | }, 216 | 217 | /** 218 | * Generate javascript source code for captured metadata 219 | * Metadata is in pretty-printed JSON format 220 | */ 221 | generateSource: function() { 222 | /* "\/" is used instead of a plain forward slash so that the contents 223 | of testharnessreport.js can (for convenience) be copy-pasted into a 224 | script tag without issue. Otherwise, the HTML parser would think that 225 | the script ended in the middle of that string literal. */ 226 | var source = 227 | '