├── .travis.yml ├── .gitignore ├── ChangeLog ├── test ├── mocha.js ├── integration.html └── compass.coffee ├── gem ├── compassjs.rb └── compassjs.gemspec ├── package.json ├── LICENSE ├── README.md ├── Cakefile └── lib └── compass.js /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | 4 | node_modules 5 | 6 | build 7 | pkg 8 | 9 | .gen/.bundle 10 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | == 0.1.1 (Ferdinand Magellan) 2 | * Fix GPS hack detection on Nexus 4. 3 | 4 | == 0.1 (James Cook) 5 | * Initial release. 6 | -------------------------------------------------------------------------------- /test/mocha.js: -------------------------------------------------------------------------------- 1 | window = global; 2 | navigator = { }; 3 | 4 | sinon = require('sinon'); 5 | 6 | chai = require('chai'); 7 | sinonChai = require('sinon-chai'); 8 | chai.should(); 9 | chai.use(sinonChai); 10 | -------------------------------------------------------------------------------- /gem/compassjs.rb: -------------------------------------------------------------------------------- 1 | # Used only for Ruby on Rails gem to tell, that gem contain `lib/assets` with 2 | # compass.js file. 3 | module CompassJs 4 | module Rails 5 | class Engine < ::Rails::Engine 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compass.js", 3 | "version": "0.1.1", 4 | "devDependencies": { 5 | "coffee-script": "1.6.3", 6 | "mocha": "1.13.0", 7 | "chai": "1.8.0", 8 | "sinon": "1.7.3", 9 | "sinon-chai": "2.4.0", 10 | "fs-extra": "0.6.4" 11 | }, 12 | "scripts": { 13 | "test": "mocha --reporter spec --require ./test/mocha --require ./lib/compass --compilers coffee:coffee-script" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /gem/compassjs.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | Gem::Specification.new do |s| 4 | s.platform = Gem::Platform::RUBY 5 | s.name = 'compassjs' 6 | s.version = VERSION 7 | s.summary = 'Compass.js allow you to get compass heading in JavaScript' 8 | s.description = 'Compass.js allow you to get compass heading in JavaScript ' + 9 | 'by PhoneGap, iOS API or GPS hack.' 10 | 11 | s.files = ['lib/assets/javascripts/compass.js', 'lib/compassjs.rb', 12 | 'LICENSE', 'README.md', 'ChangeLog'] 13 | s.extra_rdoc_files = ['LICENSE', 'README.md', 'ChangeLog'] 14 | s.require_path = 'lib' 15 | 16 | s.authors = ['Andrey "A.I." Sitnik'] 17 | s.email = ['andrey@sitnik.ru'] 18 | s.homepage = 'http://ai.github.io/compass.js/' 19 | s.license = 'MIT' 20 | 21 | s.add_dependency 'sprockets', '>= 2' 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2012 Andrey Sitnik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/integration.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Compass.js Integration Test 6 | 7 | 41 | 42 | 43 |
initializing
44 |
45 |
N
46 |
47 |
48 |
49 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compass.js [![Build Status](https://travis-ci.org/ai/compass.js.svg)](https://travis-ci.org/ai/compass.js) 2 | 3 | Compass.js allows you to get compass heading in JavaScript. 4 | Today we haven’t any standard way to get compass data, 5 | but there are two proprietary APIs and one hack: 6 | 7 | * [PhoneGap has] `navigator.compass` API. 8 | * iOS [Safari adds] `webkitCompassHeading` property to `deviceorientation` event. 9 | * We can enable GPS and ask user to go forward. GPS will send current heading, 10 | so we can calculate difference between real North and zero in 11 | `deviceorientation` event. Next we use this difference to get compass heading 12 | only by device orientation. 13 | 14 | This library hides all this magic and APIs from you, autodetects available 15 | way and provides clean and simple API for your geolocation web app. 16 | 17 | 18 | Sponsored by Evil Martians 19 | 20 | 21 | [PhoneGap has]: http://docs.phonegap.com/phonegap_compass_compass.md.html 22 | [Safari adds]: http://developer.apple.com/library/safari/#documentation/SafariDOMAdditions/Reference/DeviceOrientationEventClassRef/DeviceOrientationEvent/DeviceOrientationEvent.html 23 | 24 | ## Usage 25 | 26 | Hide compass for desktop users (without compass, GPS and accelerometers): 27 | 28 | ```js 29 | Compass.noSupport(function () { 30 | $('.compass').hide(); 31 | }); 32 | ``` 33 | 34 | Show instructions for Android users: 35 | 36 | ```js 37 | Compass.needGPS(function () { 38 | $('.go-outside-message').show(); // Step 1: we need GPS signal 39 | }).needMove(function () { 40 | $('.go-outside-message').hide() 41 | $('.move-and-hold-ahead-message').show(); // Step 2: user must go forward 42 | }).init(function () { 43 | $('.move-and-hold-ahead-message').hide(); // GPS hack is enabled 44 | }); 45 | ``` 46 | 47 | Add compass heading listener: 48 | 49 | ```js 50 | Compass.watch(function (heading) { 51 | $('.degrees').text(heading); 52 | $('.compass').css('transform', 'rotate(' + (-heading) + 'deg)'); 53 | }); 54 | ``` 55 | 56 | ### Method Name 57 | 58 | Library will detect method asynchronously, so you can’t just read 59 | `Compass.method`, because it can be empty yet. It will be better to 60 | use `Compass.init` method: 61 | 62 | ```js 63 | Compass.init(function (method) { 64 | console.log('Compass heading by ' + method); 65 | }); 66 | ``` 67 | 68 | If library is already initialized, callback will be executed instantly, 69 | without reinitialization. 70 | 71 | ### Unwatch 72 | 73 | You can remove compass listener by `Compass.unwatch` method: 74 | 75 | ```js 76 | var watchID = Compass.watch(function (heading) { 77 | $('.degrees').text(heading); 78 | }); 79 | 80 | Compass.unwatch(watchID); 81 | ``` 82 | 83 | ## Installing 84 | 85 | ### Ruby on Rails 86 | 87 | For Ruby on Rails you can use gem for Assets Pipeline. 88 | 89 | 1. Add `compassjs` gem to `Gemfile`: 90 | 91 | ```ruby 92 | gem "compassjs" 93 | ``` 94 | 95 | 2. Install gems: 96 | 97 | ```sh 98 | bundle install 99 | ``` 100 | 101 | 3. Include Compass.js to your `application.js.coffee`: 102 | 103 | ```coffee 104 | #= require compass 105 | ``` 106 | 107 | ### Others 108 | 109 | If you don’t use any assets packaging manager (it’s very bad idea), you can use 110 | already minified version of the library. 111 | 112 | Take it from: [ai.github.io/compass.js/compass.js]. 113 | 114 | [ai.github.io/compass.js/compass.js]: http://ai.github.io/compass.js/compass.js 115 | 116 | ## Contributing 117 | 118 | 1. To run tests you need node.js and npm. For example, in Ubuntu run: 119 | 120 | ```sh 121 | sudo apt-get install nodejs npm 122 | ``` 123 | 124 | 2. Next install npm dependencies: 125 | 126 | ```sh 127 | npm install 128 | ``` 129 | 130 | 3. Run all tests: 131 | 132 | ```sh 133 | npm test 134 | ``` 135 | 136 | 4. Run test server: 137 | 138 | ```sh 139 | ./node_modules/.bin/cake server 140 | ``` 141 | 142 | 5. Open tests in browser: [localhost:8000]. 143 | 6. Also you can see real usage example in integration test: 144 | [localhost:8000/integration]. 145 | 146 | [localhost:8000]: http://localhost:8000 147 | [localhost:8000/integration]: http://localhost:8000/integration 148 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | fs = require('fs-extra') 2 | url = require('url') 3 | exec = require('child_process').exec 4 | http = require('http') 5 | path = require('path') 6 | coffee = require('coffee-script') 7 | 8 | project = 9 | 10 | package: -> 11 | JSON.parse(fs.readFileSync('package.json')) 12 | 13 | name: -> 14 | @package().name 15 | 16 | version: -> 17 | @package().version 18 | 19 | tests: -> 20 | fs.readdirSync('test/'). 21 | filter( (i) -> i.match /\.coffee$/ ). 22 | map( (i) -> "test/#{i}" ) 23 | 24 | libs: -> 25 | fs.readdirSync('lib/').map( (i) -> "lib/#{i}" ) 26 | 27 | title: -> 28 | @name()[0].toUpperCase() + @name()[1..-1] 29 | 30 | mocha = 31 | 32 | template: """ 33 | 34 | 35 | 36 | #title# Tests 37 | 38 | 50 | #system# 51 | 58 | #libs# 59 | #tests# 60 | 61 | 62 | see also integration test → 63 | 64 |
65 | 66 | 67 | """ 68 | 69 | html: -> 70 | @render @template, 71 | system: @system() 72 | libs: @scripts project.libs() 73 | tests: @scripts project.tests() 74 | title: project.title() 75 | 76 | render: (template, params) -> 77 | html = template 78 | for name, value of params 79 | html = html.replace("##{name}#", value.replace(/\$/g, '$$$$')) 80 | html 81 | 82 | scripts: (files) -> 83 | files.map( (i) -> "" ).join("\n ") 84 | 85 | style: -> 86 | fs.readFileSync('node_modules/mocha/mocha.css') 87 | 88 | system: -> 89 | @scripts ['node_modules/mocha/mocha.js', 90 | 'node_modules/chai/chai.js', 91 | 'node_modules/sinon/lib/sinon.js', 92 | 'node_modules/sinon/lib/sinon/spy.js', 93 | 'node_modules/sinon/lib/sinon/stub.js', 94 | 'node_modules/sinon/lib/sinon/match.js', 95 | 'node_modules/sinon/lib/sinon/util/fake_timers.js', 96 | 'node_modules/sinon-chai/lib/sinon-chai.js'] 97 | 98 | task 'server', 'Run test server', -> 99 | server = http.createServer (req, res) -> 100 | pathname = url.parse(req.url).pathname 101 | 102 | if pathname == '/' 103 | res.writeHead 200, 'Content-Type': 'text/html' 104 | res.write mocha.html() 105 | 106 | else if pathname == '/style.css' 107 | res.writeHead 200, 'Content-Type': 'text/css' 108 | res.write mocha.style() 109 | 110 | else if pathname == '/integration' 111 | res.writeHead 200, 'Content-Type': 'text/html' 112 | res.write fs.readFileSync('test/integration.html') 113 | 114 | else if fs.existsSync('.' + pathname) 115 | file = fs.readFileSync('.' + pathname).toString() 116 | if pathname.match(/\.coffee$/) 117 | file = coffee.compile(file) 118 | if pathname.match(/\.(js|coffee)$/) 119 | res.writeHead 200, 'Content-Type': 'application/javascript' 120 | res.write file 121 | 122 | else 123 | res.writeHead 404, 'Content-Type': 'text/plain' 124 | res.write 'Not Found' 125 | res.end() 126 | 127 | server.listen 8000 128 | console.log('Open http://localhost:8000/') 129 | 130 | task 'clean', 'Remove all generated files', -> 131 | fs.removeSync('build/') if fs.existsSync('build/') 132 | fs.removeSync('pkg/') if fs.existsSync('pkg/') 133 | 134 | task 'gem', 'Build RubyGem package', -> 135 | fs.removeSync('build/') if fs.existsSync('build/') 136 | fs.mkdirsSync('build/lib/assets/javascripts/') 137 | 138 | copy = require('fs-extra/lib/copy').copyFileSync 139 | gem = project.name().replace('.', '') 140 | 141 | gemspec = fs.readFileSync("gem/#{gem}.gemspec").toString() 142 | gemspec = gemspec.replace('VERSION', "'#{project.version()}'") 143 | fs.writeFileSync("build/#{gem}.gemspec", gemspec) 144 | 145 | copy("gem/#{gem}.rb", "build/lib/#{gem}.rb") 146 | copy('README.md', 'build/README.md') 147 | copy('ChangeLog', 'build/ChangeLog') 148 | copy('LICENSE', 'build/LICENSE') 149 | for file in project.libs() 150 | copy(file, file.replace('lib/', 'build/lib/assets/javascripts/')) 151 | 152 | exec "cd build/; gem build #{gem}.gemspec", (error, message) -> 153 | if error 154 | console.error(error.message) 155 | process.exit(1) 156 | else 157 | fs.mkdirsSync('pkg/') unless fs.existsSync('pkg/') 158 | gemFile = fs.readdirSync('build/').filter( (i) -> i.match(/\.gem$/) )[0] 159 | copy('build/' + gemFile, 'pkg/' + gemFile) 160 | fs.removeSync('build/') 161 | -------------------------------------------------------------------------------- /lib/compass.js: -------------------------------------------------------------------------------- 1 | ;(function(undefined) { 2 | "use strict"; 3 | 4 | // Shortcut to check, that `variable` is not `undefined` or `null`. 5 | var defined = function (variable) { 6 | return (variable != null || variable != undefined); 7 | }; 8 | 9 | // Fire `type` callbacks with `args`. 10 | var fire = function (type, args) { 11 | var callbacks = self._callbacks[type]; 12 | for (var i = 0; i < callbacks.length; i++) { 13 | callbacks[i].apply(window, args); 14 | } 15 | }; 16 | 17 | // Calculate average value for last 5 `array` items; 18 | var average5 = function (array) { 19 | var sum = 0; 20 | for (var i = array.length - 1; i > array.length - 6; i--) { 21 | sum += array[i]; 22 | } 23 | return sum / 5; 24 | }; 25 | 26 | // Compass.js allow you to get compass heading in JavaScript. 27 | // We can get compass data by two proprietary APIs and one hack: 28 | // * PhoneGap have `navigator.compass` API. 29 | // * iOS Safari add `webkitCompassHeading` to `deviceorientation` event. 30 | // * We can enable GPS and ask user to go forward. GPS will send 31 | // current heading, so we can calculate difference between real North 32 | // and zero in `deviceorientation` event. Next we use this difference 33 | // to get compass heading only by device orientation. 34 | // 35 | // Hide compass, when there isn’t any method: 36 | // 37 | // Compass.noSupport(function () { 38 | // $('.compass').hide(); 39 | // }); 40 | // 41 | // Show instructions for GPS hack: 42 | // 43 | // Compass.needGPS(function () { 44 | // $('.go-outside-message').show(); 45 | // }).needMove(function () { 46 | // $('.go-outside-message').hide() 47 | // $('.move-and-hold-ahead-message').show(); 48 | // }).init(function () { 49 | // $('.move-and-hold-ahead-message').hide(); 50 | // }); 51 | var self = window.Compass = { 52 | 53 | // Name of method to get compass heading. It will have value only after 54 | // library initialization from `init` method. So better way to get 55 | // method name is to use `init`: 56 | // 57 | // Compass.init(function (method) { 58 | // console.log('Compass by ' + method); 59 | // }); 60 | // 61 | // Available methods: 62 | // * `phonegap` take from PhoneGap’s `navigator.compass`. 63 | // * `webkitOrientation` take from iPhone’s proprietary 64 | // `webkitCompassHeading` proprerty in `DeviceOrientationEvent`. 65 | // * `orientationAndGPS` take from device orientation with GPS hack. 66 | // 67 | // If browser hasn’t access to compass, `method` will be `false`. 68 | method: undefined, 69 | 70 | // Watch for compass heading changes and execute `callback` with degrees 71 | // relative to magnetic north (from 0 to 360). 72 | // 73 | // Method return watcher ID to use it in `unwatch`. 74 | // 75 | // var watchID = Compass.watch(function (heading) { 76 | // $('.degrees').text(heading); 77 | // // Don’t forget to change degree sign, when rotate compass. 78 | // $('.compass').css({ transform: 'rotate(' + (-heading) + 'deg)' }); 79 | // }); 80 | // 81 | // someApp.close(function () { 82 | // Compass.unwatch(watchID); 83 | // }); 84 | watch: function (callback) { 85 | var id = ++self._lastId; 86 | 87 | self.init(function (method) { 88 | 89 | if ( method == 'phonegap' ) { 90 | self._watchers[id] = self._nav.compass.watchHeading(callback); 91 | 92 | } else if ( method == 'webkitOrientation' ) { 93 | var watcher = function (e) { 94 | callback(e.webkitCompassHeading); 95 | }; 96 | self._win.addEventListener('deviceorientation', watcher); 97 | self._watchers[id] = watcher; 98 | 99 | } else if ( method == 'orientationAndGPS' ) { 100 | var degrees; 101 | var watcher = function (e) { 102 | degrees = -e.alpha + self._gpsDiff; 103 | if ( degrees < 0 ) { 104 | degrees += 360; 105 | } else if ( degrees > 360 ) { 106 | degrees -= 360; 107 | } 108 | callback(degrees); 109 | }; 110 | self._win.addEventListener('deviceorientation', watcher); 111 | self._watchers[id] = watcher; 112 | 113 | } 114 | }); 115 | 116 | return id; 117 | }, 118 | 119 | // Remove watcher by watcher ID from `watch`. 120 | // 121 | // Compass.unwatch(watchID) 122 | unwatch: function (id) { 123 | self.init(function (m) { 124 | 125 | if ( m == 'phonegap' ) { 126 | self._nav.compass.clearWatch(self._watchers[id]); 127 | 128 | } else if ( m == 'webkitOrientation' || m == 'orientationAndGPS' ) { 129 | self._win.removeEventListener( 130 | 'deviceorientation', self._watchers[id]); 131 | 132 | } 133 | delete self._watchers[id]; 134 | }); 135 | return self; 136 | }, 137 | 138 | // Execute `callback`, when GPS hack activated to detect difference between 139 | // device orientation and real North from GPS. 140 | // 141 | // You need to show to user some message, that he must go outside to be able 142 | // to receive GPS signal. 143 | // 144 | // Callback must be set before `init` or `watch` executing. 145 | // 146 | // Compass.needGPS(function () { 147 | // $('.go-outside-message').show(); 148 | // }); 149 | // 150 | // Don’t forget to hide message by `needMove` callback in second step. 151 | needGPS: function (callback) { 152 | self._callbacks.needGPS.push(callback); 153 | return self; 154 | }, 155 | 156 | // Execute `callback` on second GPS hack step, when library has GPS signal, 157 | // but user must move and hold the device straight ahead. Library will use 158 | // `heading` from GPS movement tracking to detect difference between 159 | // device orientation and real North. 160 | // 161 | // Callback must be set before `init` or `watch` executing. 162 | // 163 | // Compass.needMove(function () { 164 | // $('.go-outside-message').hide() 165 | // $('.move-and-hold-ahead-message').show(); 166 | // }); 167 | // 168 | // Don’t forget to hide message in `init` callback: 169 | // 170 | // Compass.init(function () { 171 | // $('.move-and-hold-ahead-message').hide(); 172 | // }); 173 | needMove: function (callback) { 174 | self._callbacks.needMove.push(callback); 175 | return self; 176 | }, 177 | 178 | // Execute `callback` if browser hasn’t any way to get compass heading. 179 | // 180 | // Compass.noSupport(function () { 181 | // $('.compass').hide(); 182 | // }); 183 | // 184 | // On Firefox detecting can take about 0.5 second. So, it will be better 185 | // to show compass in `init`, than to hide it in `noSupport`. 186 | noSupport: function (callback) { 187 | if ( self.method === false ) { 188 | callback(); 189 | } else if ( !defined(self.method) ) { 190 | self._callbacks.noSupport.push(callback); 191 | } 192 | return self; 193 | }, 194 | 195 | // Detect compass method and execute `callback`, when library will be 196 | // initialized. Callback will get method name (or `false` if library can’t 197 | // detect compass) in first argument. 198 | // 199 | // It is best way to check `method` property. 200 | // 201 | // Compass.init(function (method) { 202 | // console.log('Compass by ' + method); 203 | // }); 204 | init: function (callback) { 205 | if ( defined(self.method) ) { 206 | callback(self.method); 207 | return; 208 | } 209 | self._callbacks.init.push(callback); 210 | 211 | if ( self._initing ) { 212 | return; 213 | } 214 | self._initing = true; 215 | 216 | if ( self._nav.compass ) { 217 | self._start('phonegap'); 218 | 219 | } else if ( self._win.DeviceOrientationEvent ) { 220 | self._checking = 0; 221 | self._win.addEventListener('deviceorientation', self._checkEvent); 222 | setTimeout(function () { 223 | if ( self._checking !== false ) { 224 | self._start(false); 225 | } 226 | }, 500); 227 | 228 | } else { 229 | self._start(false); 230 | } 231 | return self; 232 | }, 233 | 234 | // Last watch ID. 235 | _lastId: 0, 236 | 237 | // Hash of internal ID to watcher to use it in `unwatch`. 238 | _watchers: { }, 239 | 240 | // Window object for testing. 241 | _win: window, 242 | 243 | // Navigator object for testing. 244 | _nav: navigator, 245 | 246 | // List of callbacks. 247 | _callbacks: { 248 | 249 | // Callbacks from `init` method. 250 | init: [], 251 | 252 | // Callbacks from `noSupport` method. 253 | noSupport: [], 254 | 255 | // Callbacks from `needGPS` method. 256 | needGPS: [], 257 | 258 | // Callbacks from `needMove` method. 259 | needMove: [] 260 | 261 | }, 262 | 263 | // Is library now try to detect compass method. 264 | _initing: false, 265 | 266 | // Difference between `alpha` orientation and real North from GPS. 267 | _gpsDiff: undefined, 268 | 269 | // Finish library initialization and use `method` to get compass heading. 270 | _start: function (method) { 271 | self.method = method; 272 | self._initing = false; 273 | 274 | fire('init', [method]); 275 | self._callbacks.init = []; 276 | 277 | if ( method === false ) { 278 | fire('noSupport', []); 279 | } 280 | self._callbacks.noSupport = []; 281 | }, 282 | 283 | // Tell, that we wait for `DeviceOrientationEvent`. 284 | _checking: false, 285 | 286 | // Check `DeviceOrientationEvent` to detect compass method. 287 | _checkEvent: function (e) { 288 | self._checking += 1; 289 | var wait = false; 290 | 291 | if ( defined(e.webkitCompassHeading) ) { 292 | self._start('webkitOrientation'); 293 | 294 | } else if ( defined(e.alpha) && self._nav.geolocation ) { 295 | self._gpsHack(); 296 | 297 | } else if ( self._checking > 1 ) { 298 | self._start(false); 299 | 300 | } else { 301 | wait = true; 302 | } 303 | 304 | if ( !wait ) { 305 | self._checking = false; 306 | self._win.removeEventListener('deviceorientation', self._checkEvent); 307 | } 308 | }, 309 | 310 | // Use GPS to detect difference between `alpha` orientation and real North. 311 | _gpsHack: function () { 312 | var first = true; 313 | var alphas = []; 314 | var headings = []; 315 | 316 | fire('needGPS'); 317 | 318 | var saveAlpha = function (e) { 319 | alphas.push(e.alpha); 320 | } 321 | self._win.addEventListener('deviceorientation', saveAlpha); 322 | 323 | var success = function (position) { 324 | var coords = position.coords 325 | if ( !defined(coords.heading) ) { 326 | return; // Position not from GPS 327 | } 328 | 329 | if ( first ) { 330 | first = false; 331 | fire('needMove'); 332 | } 333 | 334 | if ( coords.speed > 1 ) { 335 | headings.push(coords.heading); 336 | if ( headings.length >= 5 && alphas.length >= 5 ) { 337 | self._win.removeEventListener('deviceorientation', saveAlpha); 338 | self._nav.geolocation.clearWatch(watcher); 339 | 340 | self._gpsDiff = average5(headings) + average5(alphas); 341 | self._start('orientationAndGPS'); 342 | } 343 | } else { 344 | headings = []; 345 | } 346 | }; 347 | var error = function () { 348 | self._win.removeEventListener('deviceorientation', saveAlpha); 349 | self._start(false); 350 | }; 351 | 352 | var watcher = self._nav.geolocation. 353 | watchPosition(success, error, { enableHighAccuracy: true }); 354 | } 355 | 356 | }; 357 | 358 | })(); 359 | -------------------------------------------------------------------------------- /test/compass.coffee: -------------------------------------------------------------------------------- 1 | describe 'Compass', -> 2 | 3 | beforeEach -> 4 | Compass.method = undefined 5 | Compass._initing = false 6 | Compass._watchers = { } 7 | Compass._gpsDiff = undefined 8 | Compass._checking = false 9 | 10 | Compass._win = 11 | addEventListener: sinon.spy() 12 | removeEventListener: sinon.spy() 13 | Compass._nav = 14 | geolocation: 15 | watchPosition: sinon.spy() 16 | clearWatch: sinon.spy() 17 | 18 | Compass._callbacks[i] = [] for i of Compass._callbacks 19 | Compass[i]?.restore?() for i of Compass 20 | 21 | afterEach -> 22 | @clock?.restore() 23 | 24 | describe '.watch()', -> 25 | 26 | it 'should use init()', -> 27 | Compass._initing = true 28 | Compass._nav.compass = { watchHeading: sinon.spy() } 29 | sinon.spy(Compass, 'init') 30 | 31 | Compass.watch( -> ) 32 | 33 | Compass.init.should.have.been.called 34 | Compass._nav.compass.watchHeading.should.not.have.been.called 35 | 36 | Compass._start('phonegap') 37 | Compass._nav.compass.watchHeading.should.have.been.called 38 | 39 | it 'should generate new watcher ID', -> 40 | id1 = Compass.watch( -> ) 41 | id2 = Compass.watch( -> ) 42 | 43 | id1.should.not.eql(id2) 44 | 45 | it 'should watch for phonegap compass', -> 46 | Compass.method = 'phonegap' 47 | Compass._nav.compass = { watchHeading: sinon.stub().returns(3) } 48 | callback = -> 49 | 50 | id = Compass.watch(callback) 51 | 52 | Compass._nav.compass.watchHeading.should.have.been.calledWith(callback) 53 | Compass._watchers[id].should.eql(3) 54 | 55 | it 'should watch for webkitOrientation compass', -> 56 | Compass.method = 'webkitOrientation' 57 | callback = sinon.spy() 58 | 59 | id = Compass.watch(callback) 60 | 61 | Compass._watchers[id].should.be.a('function') 62 | Compass._win.addEventListener.should.have.been. 63 | calledWith('deviceorientation', Compass._watchers[id]) 64 | 65 | callback.should.not.have.been.called 66 | Compass._watchers[id]({ webkitCompassHeading: 90 }) 67 | callback.should.have.been.calledWith(90) 68 | 69 | it 'should watch for orientationAndGPS compass', -> 70 | Compass.method = 'orientationAndGPS' 71 | Compass._gpsDiff = 10 72 | callback = sinon.spy() 73 | 74 | id = Compass.watch(callback) 75 | 76 | Compass._watchers[id].should.be.a('function') 77 | Compass._win.addEventListener.should.have.been. 78 | calledWith('deviceorientation', Compass._watchers[id]) 79 | 80 | callback.should.not.have.been.called 81 | Compass._watchers[id]({ alpha: 90 }) 82 | callback.should.have.been.calledWith(280) 83 | 84 | describe '.unwatch()', -> 85 | 86 | it 'should delete watcher', -> 87 | Compass.method = 'supermethod' 88 | Compass._watchers[1] = 2 89 | 90 | Compass.unwatch(1) 91 | Compass._watchers.should.eql({ }) 92 | 93 | it 'should remove phonegap watcher', -> 94 | Compass.method = 'phonegap' 95 | Compass._nav.compass = { clearWatch: sinon.spy() } 96 | Compass._watchers[1] = 3 97 | 98 | Compass.unwatch(1) 99 | 100 | Compass._nav.compass.clearWatch.should.have.been.calledWith(3) 101 | 102 | it 'should remove webkitOrientation watcher', -> 103 | Compass.method = 'webkitOrientation' 104 | callback = -> 105 | Compass._watchers[1] = callback 106 | 107 | Compass.unwatch(1) 108 | 109 | Compass._win.removeEventListener.should.have.been. 110 | calledWith('deviceorientation', callback) 111 | 112 | it 'should remove orientationAndGPS watcher', -> 113 | Compass.method = 'orientationAndGPS' 114 | callback = -> 115 | Compass._watchers[1] = callback 116 | 117 | Compass.unwatch(1) 118 | 119 | Compass._win.removeEventListener.should.have.been. 120 | calledWith('deviceorientation', callback) 121 | 122 | it 'should return Compass', -> 123 | Compass.unwatch(1).should.eql(Compass) 124 | 125 | describe '.noSupport()', -> 126 | 127 | callback = null 128 | 129 | beforeEach -> 130 | callback = sinon.spy() 131 | 132 | it 'should save callback if method is not still detected', -> 133 | Compass.noSupport(callback) 134 | callback.should.not.have.been.called 135 | Compass._callbacks.noSupport.should.be.eql([callback]) 136 | 137 | it 'execute callback if we detect, that there is no support', -> 138 | Compass.method = false 139 | Compass.noSupport(callback) 140 | callback.should.have.been.called 141 | Compass._callbacks.noSupport.should.be.empty 142 | 143 | it 'should forget callback if we can get compass', -> 144 | Compass.method = 'supermethod' 145 | Compass.noSupport(callback) 146 | callback.should.not.have.been.called 147 | Compass._callbacks.noSupport.should.be.empty 148 | 149 | it 'should return Compass', -> 150 | Compass.noSupport( -> ).should.eql(Compass) 151 | 152 | describe '.init()', -> 153 | 154 | callback = null 155 | 156 | beforeEach -> 157 | sinon.stub(Compass, '_start') 158 | callback = sinon.spy() 159 | 160 | it 'should execute callback if method already detected', -> 161 | Compass.method = 'supermethod' 162 | 163 | Compass.init(callback) 164 | 165 | callback.should.have.been.calledWith('supermethod') 166 | Compass._callbacks.init.should.be.empty 167 | Compass._initing.should.be.false 168 | 169 | it 'should add listener and set initing', -> 170 | Compass.init(callback) 171 | 172 | Compass._initing.should.be.true 173 | Compass._callbacks.init.should.eql([callback]) 174 | 175 | it 'should detect no support', -> 176 | Compass.init(callback) 177 | Compass._start.should.have.been.calledWith(false) 178 | 179 | it 'should not initialize twice', -> 180 | Compass._initing = true 181 | 182 | Compass.init(callback) 183 | 184 | Compass._callbacks.init.should.eql([callback]) 185 | Compass._start.should.have.not.been.calledWith(false) 186 | 187 | it 'should detect phonegap support', -> 188 | Compass._nav.compass = { watchHeading: -> } 189 | Compass.init(callback) 190 | Compass._start.should.have.been.calledWith('phonegap') 191 | 192 | it 'should detect webkitOrientation support', -> 193 | Compass._win.DeviceOrientationEvent = -> 194 | Compass.init(callback) 195 | Compass._win.addEventListener.should.have.been. 196 | calledWith('deviceorientation', Compass._checkEvent) 197 | 198 | Compass._checkEvent({ webkitCompassHeading: -> }) 199 | Compass._start.should.have.been.calledWith('webkitOrientation') 200 | Compass._win.removeEventListener.should.have.been. 201 | calledWith('deviceorientation', Compass._checkEvent) 202 | 203 | it 'should detect no support in orientation event', -> 204 | @clock = sinon.useFakeTimers() 205 | Compass._win.DeviceOrientationEvent = -> 206 | Compass.init(callback) 207 | Compass._win.addEventListener.should.have.been. 208 | calledWith('deviceorientation', Compass._checkEvent) 209 | 210 | Compass._checkEvent({ alpha: null }) 211 | Compass._start.should.not.have.been.calledWith(false) 212 | 213 | @clock.tick(1000) 214 | Compass._start.should.have.been.calledWith(false) 215 | 216 | it 'should start GPS hack with orientation and geolocation', -> 217 | @clock = sinon.useFakeTimers() 218 | Compass._win.DeviceOrientationEvent = -> 219 | sinon.stub(Compass, '_gpsHack'); 220 | 221 | Compass.init(callback) 222 | 223 | Compass._checkEvent({ alpha: 10 }) 224 | Compass._start.should.have.not.been.calledWith(false) 225 | Compass._gpsHack.should.have.been.called 226 | 227 | @clock.tick(1000) 228 | Compass._start.should.have.not.been.called 229 | 230 | it 'should use 2 orientation check', -> 231 | @clock = sinon.useFakeTimers() 232 | Compass._win.DeviceOrientationEvent = -> 233 | sinon.stub(Compass, '_gpsHack'); 234 | 235 | Compass.init(callback) 236 | 237 | Compass._checkEvent({ alpha: null }) 238 | Compass._start.should.have.not.been.calledWith(false) 239 | Compass._gpsHack.should.not.have.been.called 240 | 241 | Compass._checkEvent({ alpha: 10 }) 242 | Compass._start.should.have.not.been.calledWith(false) 243 | Compass._gpsHack.should.have.been.called 244 | 245 | it 'should have timeout for orientation event', -> 246 | @clock = sinon.useFakeTimers() 247 | 248 | Compass._win.DeviceOrientationEvent = -> 249 | Compass.init(callback) 250 | Compass._start.should.not.have.been.calledWith(false) 251 | 252 | @clock.tick(1000) 253 | Compass._start.should.have.been.calledWith(false) 254 | 255 | it 'should return Compass', -> 256 | Compass.init( -> ).should.eql(Compass) 257 | 258 | describe '._start()', -> 259 | 260 | it 'should set state variables', -> 261 | Compass._initing = true 262 | Compass._start('supermethod') 263 | 264 | Compass.method.should.eql('supermethod') 265 | Compass._initing.should.be.false 266 | 267 | it 'should execute all init callbacks with method', -> 268 | callback1 = sinon.spy() 269 | callback2 = sinon.spy() 270 | callback3 = sinon.spy() 271 | Compass._callbacks.init = [callback1, callback2] 272 | Compass._callbacks.noSupport = [callback3] 273 | 274 | Compass._start('supermethod') 275 | 276 | callback1.should.have.been.calledWith('supermethod') 277 | callback2.should.have.been.calledWith('supermethod') 278 | Compass._callbacks.init.should.be.empty 279 | 280 | callback3.should.not.have.been.called 281 | Compass._callbacks.noSupport.should.be.empty 282 | 283 | it 'should execute noSupport callbacks if there is no method', -> 284 | callback1 = sinon.spy() 285 | callback2 = sinon.spy() 286 | Compass._callbacks.noSupport = [callback1, callback2] 287 | 288 | Compass._start(false) 289 | 290 | callback1.should.have.been.called 291 | callback2.should.have.been.called 292 | Compass._callbacks.noSupport.should.be.empty 293 | 294 | describe '._gpsHack()', -> 295 | 296 | it 'should detect no support on geolocation error', -> 297 | Compass._nav.geolocation.watchPosition = (success, error) -> error() 298 | sinon.stub(Compass, '_start') 299 | 300 | Compass._gpsHack() 301 | 302 | Compass._start.should.have.been.calledWith(false) 303 | Compass._win.removeEventListener.should.have.been.called 304 | 305 | it 'should detect _gpsDiff', -> 306 | geolocation = null 307 | orientation = null 308 | 309 | Compass._nav.geolocation.watchPosition = (c) -> geolocation = c 310 | sinon.spy(Compass._nav.geolocation, 'watchPosition') 311 | 312 | Compass._win.addEventListener = (n, c) -> orientation = c 313 | sinon.spy(Compass._win, 'addEventListener') 314 | 315 | needGPS = sinon.spy() 316 | needMove = sinon.spy() 317 | Compass.needGPS(needGPS).should.eql(Compass) 318 | Compass.needMove(needMove).should.eql(Compass) 319 | sinon.stub(Compass, '_start') 320 | 321 | Compass._gpsHack() 322 | 323 | orientation.should.be.a('function') 324 | Compass._win.addEventListener.should.have.been. 325 | calledWith('deviceorientation', orientation) 326 | 327 | geolocation.should.be.a('function') 328 | Compass._nav.geolocation.watchPosition.should.have.been. 329 | calledWith(geolocation, sinon.match.func, { enableHighAccuracy: true }) 330 | 331 | needGPS.should.have.been.called 332 | needMove.should.not.have.been.called 333 | 334 | geolocation(coords: { speed: null, heading: null }) 335 | needMove.should.not.have.been.called 336 | 337 | geolocation(coords: { speed: 0, heading: 0 }) 338 | needMove.should.have.been.called 339 | 340 | geolocation(coords: { speed: 2, heading: 0 }) 341 | needMove.should.have.been.calledOnce 342 | Compass._start.should.not.have.been.called 343 | 344 | orientation(alpha: 10) 345 | geolocation(coords: { speed: 2, heading: 0 }) 346 | Compass._start.should.not.have.been.calledWith('orientationAndGPS') 347 | 348 | geolocation(coords: { speed: 0, heading: 0 }) 349 | 350 | orientation(alpha: 10) 351 | orientation(alpha: 15) 352 | orientation(alpha: 20) 353 | orientation(alpha: 20) 354 | geolocation(coords: { speed: 2, heading: 0 }) 355 | geolocation(coords: { speed: 2, heading: 0 }) 356 | geolocation(coords: { speed: 2, heading: 5 }) 357 | geolocation(coords: { speed: 2, heading: 10 }) 358 | Compass._start.should.not.have.been.calledWith('orientationAndGPS') 359 | 360 | geolocation(coords: { speed: 2, heading: 10 }) 361 | Compass._start.should.have.been.calledWith('orientationAndGPS') 362 | Compass._gpsDiff.should.eql(20) 363 | --------------------------------------------------------------------------------