compassHeadingController.ts 5.7 KB

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