Files
cmr-mini/miniprogram/engine/sensor/compassHeadingController.ts

215 lines
5.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export interface CompassHeadingControllerCallbacks {
onHeading: (headingDeg: number) => void
onError: (message: string) => void
}
type SensorSource = 'compass' | 'motion' | null
export type CompassTuningProfile = 'smooth' | 'balanced' | 'responsive'
const HEADING_CORRECTION_BY_PROFILE: Record<CompassTuningProfile, number> = {
smooth: 0.3,
balanced: 0.4,
responsive: 0.54,
}
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
tuningProfile: CompassTuningProfile
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
this.tuningProfile = 'balanced'
}
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()
}
setTuningProfile(profile: CompassTuningProfile): void {
this.tuningProfile = profile
}
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
: null
this.rollDeg = typeof result.gamma === 'number' && !Number.isNaN(result.gamma)
? result.gamma
: null
this.applyAbsoluteHeading(normalizeHeadingDeg(360 - result.alpha), '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 {
const headingCorrection = HEADING_CORRECTION_BY_PROFILE[this.tuningProfile]
if (this.absoluteHeadingDeg === null) {
this.absoluteHeadingDeg = headingDeg
} else {
this.absoluteHeadingDeg = interpolateHeadingDeg(this.absoluteHeadingDeg, headingDeg, headingCorrection)
}
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
}
}