compassHeadingController.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. export interface CompassHeadingControllerCallbacks {
  2. onHeading: (headingDeg: number) => void
  3. onError: (message: string) => void
  4. }
  5. type SensorSource = 'compass' | 'motion' | null
  6. const ABSOLUTE_HEADING_CORRECTION = 0.24
  7. function normalizeHeadingDeg(headingDeg: number): number {
  8. const normalized = headingDeg % 360
  9. return normalized < 0 ? normalized + 360 : normalized
  10. }
  11. function normalizeHeadingDeltaDeg(deltaDeg: number): number {
  12. let normalized = deltaDeg
  13. while (normalized > 180) {
  14. normalized -= 360
  15. }
  16. while (normalized < -180) {
  17. normalized += 360
  18. }
  19. return normalized
  20. }
  21. function interpolateHeadingDeg(currentDeg: number, targetDeg: number, factor: number): number {
  22. return normalizeHeadingDeg(currentDeg + normalizeHeadingDeltaDeg(targetDeg - currentDeg) * factor)
  23. }
  24. export class CompassHeadingController {
  25. callbacks: CompassHeadingControllerCallbacks
  26. listening: boolean
  27. source: SensorSource
  28. compassCallback: ((result: WechatMiniprogram.OnCompassChangeCallbackResult) => void) | null
  29. motionCallback: ((result: WechatMiniprogram.OnDeviceMotionChangeCallbackResult) => void) | null
  30. absoluteHeadingDeg: number | null
  31. pitchDeg: number | null
  32. rollDeg: number | null
  33. motionReady: boolean
  34. compassReady: boolean
  35. constructor(callbacks: CompassHeadingControllerCallbacks) {
  36. this.callbacks = callbacks
  37. this.listening = false
  38. this.source = null
  39. this.compassCallback = null
  40. this.motionCallback = null
  41. this.absoluteHeadingDeg = null
  42. this.pitchDeg = null
  43. this.rollDeg = null
  44. this.motionReady = false
  45. this.compassReady = false
  46. }
  47. start(): void {
  48. if (this.listening) {
  49. return
  50. }
  51. this.absoluteHeadingDeg = null
  52. this.pitchDeg = null
  53. this.rollDeg = null
  54. this.motionReady = false
  55. this.compassReady = false
  56. this.source = null
  57. if (typeof wx.startCompass === 'function' && typeof wx.onCompassChange === 'function') {
  58. this.startCompassSource()
  59. return
  60. }
  61. this.callbacks.onError('当前环境不支持罗盘方向监听')
  62. }
  63. stop(): void {
  64. this.detachCallbacks()
  65. if (this.motionReady) {
  66. wx.stopDeviceMotionListening({ complete: () => {} })
  67. }
  68. if (this.compassReady) {
  69. wx.stopCompass({ complete: () => {} })
  70. }
  71. this.listening = false
  72. this.source = null
  73. this.absoluteHeadingDeg = null
  74. this.pitchDeg = null
  75. this.rollDeg = null
  76. this.motionReady = false
  77. this.compassReady = false
  78. }
  79. destroy(): void {
  80. this.stop()
  81. }
  82. startMotionSource(previousMessage: string): void {
  83. if (typeof wx.startDeviceMotionListening !== 'function' || typeof wx.onDeviceMotionChange !== 'function') {
  84. this.callbacks.onError(previousMessage)
  85. return
  86. }
  87. const callback = (result: WechatMiniprogram.OnDeviceMotionChangeCallbackResult) => {
  88. if (typeof result.alpha !== 'number' || Number.isNaN(result.alpha)) {
  89. return
  90. }
  91. this.pitchDeg = typeof result.beta === 'number' && !Number.isNaN(result.beta)
  92. ? result.beta * 180 / Math.PI
  93. : null
  94. this.rollDeg = typeof result.gamma === 'number' && !Number.isNaN(result.gamma)
  95. ? result.gamma * 180 / Math.PI
  96. : null
  97. const alphaDeg = result.alpha * 180 / Math.PI
  98. this.applyAbsoluteHeading(normalizeHeadingDeg(360 - alphaDeg), 'motion')
  99. }
  100. this.motionCallback = callback
  101. wx.onDeviceMotionChange(callback)
  102. wx.startDeviceMotionListening({
  103. interval: 'ui',
  104. success: () => {
  105. this.motionReady = true
  106. this.listening = true
  107. this.source = 'motion'
  108. },
  109. fail: (res) => {
  110. this.detachMotionCallback()
  111. const motionMessage = res && res.errMsg ? res.errMsg : 'startDeviceMotionListening failed'
  112. this.callbacks.onError(`${previousMessage};${motionMessage}`)
  113. },
  114. })
  115. }
  116. startCompassSource(): void {
  117. const callback = (result: WechatMiniprogram.OnCompassChangeCallbackResult) => {
  118. if (typeof result.direction !== 'number' || Number.isNaN(result.direction)) {
  119. return
  120. }
  121. this.applyAbsoluteHeading(normalizeHeadingDeg(result.direction), 'compass')
  122. }
  123. this.compassCallback = callback
  124. wx.onCompassChange(callback)
  125. wx.startCompass({
  126. success: () => {
  127. this.compassReady = true
  128. this.listening = true
  129. this.source = 'compass'
  130. },
  131. fail: (res) => {
  132. this.detachCompassCallback()
  133. this.callbacks.onError(res && res.errMsg ? res.errMsg : 'startCompass failed')
  134. },
  135. })
  136. }
  137. applyAbsoluteHeading(headingDeg: number, source: 'compass' | 'motion'): void {
  138. if (this.absoluteHeadingDeg === null) {
  139. this.absoluteHeadingDeg = headingDeg
  140. } else {
  141. this.absoluteHeadingDeg = interpolateHeadingDeg(this.absoluteHeadingDeg, headingDeg, ABSOLUTE_HEADING_CORRECTION)
  142. }
  143. this.source = source
  144. this.callbacks.onHeading(this.absoluteHeadingDeg)
  145. }
  146. detachCallbacks(): void {
  147. this.detachMotionCallback()
  148. this.detachCompassCallback()
  149. }
  150. detachMotionCallback(): void {
  151. if (!this.motionCallback) {
  152. return
  153. }
  154. if (typeof wx.offDeviceMotionChange === 'function') {
  155. wx.offDeviceMotionChange(this.motionCallback)
  156. }
  157. this.motionCallback = null
  158. }
  159. detachCompassCallback(): void {
  160. if (!this.compassCallback) {
  161. return
  162. }
  163. if (typeof wx.offCompassChange === 'function') {
  164. wx.offCompassChange(this.compassCallback)
  165. }
  166. this.compassCallback = null
  167. }
  168. }