mockLocationBridge.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { type LocationSample } from './locationSource'
  2. export const DEFAULT_MOCK_LOCATION_BRIDGE_URL = 'wss://gs.gotomars.xyz/mock-gps'
  3. export interface MockLocationBridgeCallbacks {
  4. onOpen: () => void
  5. onClose: (message: string) => void
  6. onError: (message: string) => void
  7. onSample: (sample: LocationSample) => void
  8. }
  9. type RawMockGpsMessage = {
  10. type?: string
  11. timestamp?: number
  12. lat?: number
  13. lon?: number
  14. accuracyMeters?: number
  15. speedMps?: number
  16. headingDeg?: number
  17. channelId?: string
  18. }
  19. function normalizeMockChannelId(rawChannelId: string | null | undefined): string {
  20. const trimmed = String(rawChannelId || '').trim()
  21. return trimmed || 'default'
  22. }
  23. function safeParseMessage(data: string): RawMockGpsMessage | null {
  24. try {
  25. return JSON.parse(data) as RawMockGpsMessage
  26. } catch (_error) {
  27. return null
  28. }
  29. }
  30. function toLocationSample(message: RawMockGpsMessage, expectedChannelId: string): LocationSample | null {
  31. if (message.type !== 'mock_gps') {
  32. return null
  33. }
  34. if (!Number.isFinite(message.lat) || !Number.isFinite(message.lon)) {
  35. return null
  36. }
  37. if (normalizeMockChannelId(message.channelId) !== expectedChannelId) {
  38. return null
  39. }
  40. return {
  41. latitude: Number(message.lat),
  42. longitude: Number(message.lon),
  43. accuracy: Number.isFinite(message.accuracyMeters) ? Number(message.accuracyMeters) : undefined,
  44. speed: Number.isFinite(message.speedMps) ? Number(message.speedMps) : null,
  45. headingDeg: Number.isFinite(message.headingDeg) ? Number(message.headingDeg) : null,
  46. timestamp: Number.isFinite(message.timestamp) ? Number(message.timestamp) : Date.now(),
  47. sourceMode: 'mock',
  48. }
  49. }
  50. export class MockLocationBridge {
  51. callbacks: MockLocationBridgeCallbacks
  52. socketTask: WechatMiniprogram.SocketTask | null
  53. connected: boolean
  54. connecting: boolean
  55. url: string
  56. channelId: string
  57. constructor(callbacks: MockLocationBridgeCallbacks) {
  58. this.callbacks = callbacks
  59. this.socketTask = null
  60. this.connected = false
  61. this.connecting = false
  62. this.url = DEFAULT_MOCK_LOCATION_BRIDGE_URL
  63. this.channelId = 'default'
  64. }
  65. setChannelId(channelId: string): void {
  66. this.channelId = normalizeMockChannelId(channelId)
  67. }
  68. connect(url = DEFAULT_MOCK_LOCATION_BRIDGE_URL): void {
  69. if (this.connected || this.connecting) {
  70. return
  71. }
  72. this.url = url
  73. this.connecting = true
  74. try {
  75. const socketTask = wx.connectSocket({
  76. url,
  77. })
  78. this.socketTask = socketTask
  79. socketTask.onOpen(() => {
  80. this.connected = true
  81. this.connecting = false
  82. this.callbacks.onOpen()
  83. })
  84. socketTask.onClose((result) => {
  85. const reason = result && result.reason ? result.reason : '模拟源连接已关闭'
  86. this.connected = false
  87. this.connecting = false
  88. this.socketTask = null
  89. this.callbacks.onClose(reason)
  90. })
  91. socketTask.onError((result) => {
  92. const message = result && result.errMsg ? result.errMsg : '模拟源连接失败'
  93. this.connected = false
  94. this.connecting = false
  95. this.socketTask = null
  96. this.callbacks.onError(message)
  97. })
  98. socketTask.onMessage((result) => {
  99. if (!result || typeof result.data !== 'string') {
  100. return
  101. }
  102. const parsed = safeParseMessage(result.data)
  103. if (!parsed) {
  104. this.callbacks.onError('模拟源消息不是合法 JSON')
  105. return
  106. }
  107. const sample = toLocationSample(parsed, this.channelId)
  108. if (!sample) {
  109. return
  110. }
  111. this.callbacks.onSample(sample)
  112. })
  113. } catch (error) {
  114. this.connected = false
  115. this.connecting = false
  116. this.socketTask = null
  117. const message = error && (error as Error).message ? (error as Error).message : '模拟源连接创建失败'
  118. this.callbacks.onError(message)
  119. }
  120. }
  121. disconnect(): void {
  122. if (!this.socketTask) {
  123. if (this.connected || this.connecting) {
  124. this.connected = false
  125. this.connecting = false
  126. }
  127. return
  128. }
  129. const socketTask = this.socketTask
  130. this.socketTask = null
  131. this.connected = false
  132. this.connecting = false
  133. socketTask.close({})
  134. }
  135. destroy(): void {
  136. this.disconnect()
  137. }
  138. }