215 lines
5.7 KiB
TypeScript
215 lines
5.7 KiB
TypeScript
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
|
||
}
|
||
}
|