export interface CompassHeadingControllerCallbacks { onHeading: (headingDeg: number) => void onError: (message: string) => void } type SensorSource = 'compass' | 'motion' | null const ABSOLUTE_HEADING_CORRECTION = 0.24 function normalizeHeadingDeg(headingDeg: number): number { const normalized = headingDeg % 360 return normalized < 0 ? normalized + 360 : normalized } function normalizeHeadingDeltaDeg(deltaDeg: number): number { let normalized = deltaDeg while (normalized > 180) { normalized -= 360 } while (normalized < -180) { normalized += 360 } return normalized } function interpolateHeadingDeg(currentDeg: number, targetDeg: number, factor: number): number { return normalizeHeadingDeg(currentDeg + normalizeHeadingDeltaDeg(targetDeg - currentDeg) * factor) } export class CompassHeadingController { callbacks: CompassHeadingControllerCallbacks listening: boolean source: SensorSource compassCallback: ((result: WechatMiniprogram.OnCompassChangeCallbackResult) => void) | null motionCallback: ((result: WechatMiniprogram.OnDeviceMotionChangeCallbackResult) => void) | null absoluteHeadingDeg: number | null pitchDeg: number | null rollDeg: number | null motionReady: boolean compassReady: boolean constructor(callbacks: CompassHeadingControllerCallbacks) { this.callbacks = callbacks this.listening = false this.source = null this.compassCallback = null this.motionCallback = null this.absoluteHeadingDeg = null this.pitchDeg = null this.rollDeg = null this.motionReady = false this.compassReady = false } start(): void { if (this.listening) { return } this.absoluteHeadingDeg = null this.pitchDeg = null this.rollDeg = null this.motionReady = false this.compassReady = false this.source = null if (typeof wx.startCompass === 'function' && typeof wx.onCompassChange === 'function') { this.startCompassSource() return } this.callbacks.onError('当前环境不支持罗盘方向监听') } stop(): void { this.detachCallbacks() if (this.motionReady) { wx.stopDeviceMotionListening({ complete: () => {} }) } if (this.compassReady) { wx.stopCompass({ complete: () => {} }) } this.listening = false this.source = null this.absoluteHeadingDeg = null this.pitchDeg = null this.rollDeg = null this.motionReady = false this.compassReady = false } destroy(): void { this.stop() } startMotionSource(previousMessage: string): void { if (typeof wx.startDeviceMotionListening !== 'function' || typeof wx.onDeviceMotionChange !== 'function') { this.callbacks.onError(previousMessage) return } const callback = (result: WechatMiniprogram.OnDeviceMotionChangeCallbackResult) => { if (typeof result.alpha !== 'number' || Number.isNaN(result.alpha)) { return } this.pitchDeg = typeof result.beta === 'number' && !Number.isNaN(result.beta) ? result.beta * 180 / Math.PI : null this.rollDeg = typeof result.gamma === 'number' && !Number.isNaN(result.gamma) ? result.gamma * 180 / Math.PI : null const alphaDeg = result.alpha * 180 / Math.PI this.applyAbsoluteHeading(normalizeHeadingDeg(360 - alphaDeg), 'motion') } this.motionCallback = callback wx.onDeviceMotionChange(callback) wx.startDeviceMotionListening({ interval: 'ui', success: () => { this.motionReady = true this.listening = true this.source = 'motion' }, fail: (res) => { this.detachMotionCallback() const motionMessage = res && res.errMsg ? res.errMsg : 'startDeviceMotionListening failed' this.callbacks.onError(`${previousMessage};${motionMessage}`) }, }) } startCompassSource(): void { const callback = (result: WechatMiniprogram.OnCompassChangeCallbackResult) => { if (typeof result.direction !== 'number' || Number.isNaN(result.direction)) { return } this.applyAbsoluteHeading(normalizeHeadingDeg(result.direction), 'compass') } this.compassCallback = callback wx.onCompassChange(callback) wx.startCompass({ success: () => { this.compassReady = true this.listening = true this.source = 'compass' }, fail: (res) => { this.detachCompassCallback() this.callbacks.onError(res && res.errMsg ? res.errMsg : 'startCompass failed') }, }) } applyAbsoluteHeading(headingDeg: number, source: 'compass' | 'motion'): void { if (this.absoluteHeadingDeg === null) { this.absoluteHeadingDeg = headingDeg } else { this.absoluteHeadingDeg = interpolateHeadingDeg(this.absoluteHeadingDeg, headingDeg, ABSOLUTE_HEADING_CORRECTION) } this.source = source this.callbacks.onHeading(this.absoluteHeadingDeg) } detachCallbacks(): void { this.detachMotionCallback() this.detachCompassCallback() } detachMotionCallback(): void { if (!this.motionCallback) { return } if (typeof wx.offDeviceMotionChange === 'function') { wx.offDeviceMotionChange(this.motionCallback) } this.motionCallback = null } detachCompassCallback(): void { if (!this.compassCallback) { return } if (typeof wx.offCompassChange === 'function') { wx.offCompassChange(this.compassCallback) } this.compassCallback = null } }