1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 sc_require('platform');
  9 
 10 /**
 11   ## Device Motion
 12 
 13   When a device is moved, some platforms will inform us of its movement
 14   (accelerometer) and angle (gyroscope).
 15 
 16 */
 17 SC.mixin(SC.device,
 18 /** @scope SC.device */ {
 19 
 20   // ..........................................................
 21   // PROPERTIES
 22   //
 23 
 24   /*
 25     TODO [CC] There shouldn't be a need for this property, we should overload
 26               the addObserver/removeObserver functions and check for the
 27               rotationX/Y/Z keys.
 28   */
 29   /**
 30     Because the device motion events fire every 50ms, for performance reasons
 31     you must opt into listening for those events.
 32 
 33     @type Boolean
 34     @default NO
 35   */
 36   listenForDeviceMotion: NO,
 37 
 38   /**
 39     Amount, in degrees, that the device is rotated around its x axis. x axis is
 40     the plane of the screen and is positive towards the right hand side of
 41     the screen, regardless of orientation.
 42 
 43     SC.device#listenForDeviceMotion must be true for this property to update.
 44 
 45     @type Number
 46     @default 0
 47   */
 48   rotationX: 0,
 49 
 50   /**
 51     Amount, in degrees, that the device is rotated around its y axis (axis runs
 52     from the top to the bottom of the device). y axis is the plane of the
 53     screen and is positive towards the top of the screen, regardless of
 54     orientation.
 55 
 56     SC.device#listenForDeviceMotion must be true for this property to update.
 57 
 58     @type Number
 59     @default 0
 60   */
 61   rotationY: 0,
 62 
 63   /**
 64     Amount, in degrees, that the device is rotated around its z axis (normal
 65     coming "out" of the screen). z axis is perpendicular to the screen and is
 66     positive "out" of (or normal to) the screen. This property is only useful
 67     if the device has a gyroscope; for devices which lack a gyroscope (use
 68     an accelerometer or nothing), this property remains 0.
 69 
 70     SC.device#listenForDeviceMotion must be true for this property to update.
 71 
 72     @type Number
 73     @default 0
 74   */
 75   rotationZ: 0,
 76 
 77 
 78   // ..........................................................
 79   // SETUP
 80   //
 81 
 82   setupMotion: function() {
 83     SC.RootResponder.responder.listenFor(['devicemotion', 'deviceorientation'], window, this);
 84   },
 85 
 86 
 87   // ..........................................................
 88   // DEVICE MOTION HANDLING
 89   //
 90 
 91   _scd_listenForDeviceMotionDidChange: function() {
 92     if (!SC.RootResponder.responder) return;
 93 
 94     // we only care about the gyro now, we don't need acceleration
 95     if (this.get('listenForDeviceMotion')) {
 96       if (SC.platform.hasGyroscope) {
 97         SC.Event.add(window, 'deviceorientation', this, this._scd_deviceorientationPoll);
 98       } else if (SC.platform.hasAccelerometer) {
 99         SC.Event.add(window, 'devicemotion', this, this._scd_devicemotionPoll);
100       } else {
101         SC.Logger.warn("Can't listen for device motion events on a platform that does not support them");
102       }
103     } else {
104       SC.Event.remove(window, 'deviceorientation', this, this._scd_deviceorientationPoll);
105       SC.Event.remove(window, 'devicemotion', this, this._scd_devicemotionPoll);
106     }
107   },
108 
109   /**
110     Fired once every 50ms, informing us of the gyroscope measurements
111   */
112   deviceorientation: function(evt) {
113     evt = evt.originalEvent;
114 
115     if (!SC.platform.hasGyroscope) SC.platform.hasGyroscope = YES;
116 
117     SC.Event.remove(window, 'deviceorientation', this, this.deviceorientation);
118     SC.Event.remove(window, 'devicemotion', this, this.devicemotion);
119 
120     this.addObserver('listenForDeviceMotion', this, this._scd_listenForDeviceMotionDidChange);
121     this.notifyPropertyChange('listenForDeviceMotion');
122   },
123 
124   /** @private
125     Gets called after the first deviceorientation event, used to actually set
126     the rotation values
127   */
128   _scd_deviceorientationPoll: function(evt) {
129     var orientation = this.get('orientation');
130 
131     evt = evt.originalEvent;
132 
133     SC.run(function() {
134       this.beginPropertyChanges();
135       this.set('rotationX', orientation !== 'landscape' ? evt.beta : evt.gamma);
136       this.set('rotationY', orientation !== 'landscape' ? evt.gamma : -evt.beta);
137       this.set('rotationZ', evt.alpha);
138       this.endPropertyChanges();
139     }, this);
140   },
141 
142   /** @private
143     Because we only want to deal with only the gyroscope, if it is firing its event,
144     we wait until two devicemotion events have been fired to decide to use the
145     accelerometer instead.
146   */
147   _devicemotionCalled: NO,
148 
149   /**
150     Fired every 50ms, informing us of the accelerometer measurements
151   */
152   devicemotion: function(evt) {
153     if (!SC.platform.hasAccelerometer) SC.platform.hasAccelerometer = YES;
154 
155     if (this._devicemotionCalled) {
156       // if this happens, and this event fires (ie. the deviceorientation event hasn't)
157       // then we assume the device does not have a gyroscope
158       SC.Event.remove(window, 'deviceorientation', this, this.deviceorientation);
159       SC.Event.remove(window, 'devicemotion', this, this.devicemotion);
160 
161       this.set('rotationZ', 0); // ensure rotationZ does not get any other value
162       this.addObserver('listenForDeviceMotion', this, this._scd_listenForDeviceMotionDidChange);
163       this.notifyPropertyChange('listenForDeviceMotion');
164     } else {
165       this._devicemotionCalled = YES;
166     }
167   },
168 
169   /*
170     TODO [CC] This needs to be expanded on a bit. If you hold the device in weird
171               positions, the rotation values from the accelerometer no longer match those
172               from the gyroscope. What about people who use their iPad in bed?!
173   */
174   /** @private
175     Gets called after the first devicemotion event, if the device has no gyropscope.
176   */
177   _scd_devicemotionPoll: function(evt) {
178     var min = SC.platform.accelerationMinimum,
179         max = SC.platform.accelerationMaximum,
180         spread = max - min,
181         orientation = this.get('orientation'),
182         aX, aY, rX, rY, swap;
183 
184     evt = evt.originalEvent;
185 
186     aX = evt.accelerationIncludingGravity.x;
187     aY = evt.accelerationIncludingGravity.y;
188 
189     if (aX < min) aX = min;
190     else if (aX > max) aX = max;
191     if (aY < min) aY = min;
192     else if (aY > max) aY = max;
193 
194     if (orientation === 'landscape') {
195       swap = aX;
196       aX = aY;
197       aY = swap;
198     }
199 
200     rX = 90 - ((aY-min)/spread) * 180;
201     rY = -90 + ((aX-min)/spread) * 180;
202 
203     SC.run(function() {
204       this.beginPropertyChanges();
205       this.set('rotationX', rX);
206       this.set('rotationY', rY);
207       this.endPropertyChanges();
208     }, this);
209   }
210 
211 });
212 
213 SC.ready(function() {
214   SC.device.setupMotion();
215 });
216