locationController.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import { DEFAULT_MOCK_LOCATION_BRIDGE_URL, MockLocationBridge } from './mockLocationBridge'
  2. import { MockLocationSource } from './mockLocationSource'
  3. import { RealLocationSource } from './realLocationSource'
  4. import { type LocationSample, type LocationSourceCallbacks, type LocationSourceMode } from './locationSource'
  5. export interface LocationUpdate extends LocationSample {}
  6. export interface LocationControllerDebugState {
  7. sourceMode: LocationSourceMode
  8. sourceModeText: string
  9. listening: boolean
  10. mockBridgeConnected: boolean
  11. mockBridgeStatusText: string
  12. mockBridgeUrlText: string
  13. mockChannelIdText: string
  14. mockCoordText: string
  15. mockSpeedText: string
  16. }
  17. export interface LocationControllerCallbacks {
  18. onLocation: (update: LocationUpdate) => void
  19. onStatus: (message: string) => void
  20. onError: (message: string) => void
  21. onDebugStateChange?: (state: LocationControllerDebugState) => void
  22. }
  23. function formatSourceModeText(mode: LocationSourceMode): string {
  24. return mode === 'mock' ? '模拟定位' : '真实定位'
  25. }
  26. function formatMockCoordText(sample: LocationSample | null): string {
  27. if (!sample) {
  28. return '--'
  29. }
  30. return `${sample.latitude.toFixed(6)}, ${sample.longitude.toFixed(6)}`
  31. }
  32. function formatMockSpeedText(sample: LocationSample | null): string {
  33. if (!sample || !Number.isFinite(sample.speed)) {
  34. return '--'
  35. }
  36. return `${(Number(sample.speed) * 3.6).toFixed(1)} km/h`
  37. }
  38. function normalizeMockBridgeUrl(rawUrl: string): string {
  39. const trimmed = rawUrl.trim()
  40. if (!trimmed) {
  41. return DEFAULT_MOCK_LOCATION_BRIDGE_URL
  42. }
  43. let normalized = trimmed
  44. if (!/^wss?:\/\//i.test(normalized)) {
  45. normalized = `ws://${normalized.replace(/^\/+/, '')}`
  46. }
  47. if (!/\/mock-gps(?:\?.*)?$/i.test(normalized)) {
  48. normalized = normalized.replace(/\/+$/, '')
  49. normalized = `${normalized}/mock-gps`
  50. }
  51. return normalized
  52. }
  53. export class LocationController {
  54. callbacks: LocationControllerCallbacks
  55. realSource: RealLocationSource
  56. mockSource: MockLocationSource
  57. mockBridge: MockLocationBridge
  58. sourceMode: LocationSourceMode
  59. mockBridgeStatusText: string
  60. mockBridgeUrl: string
  61. mockChannelId: string
  62. constructor(callbacks: LocationControllerCallbacks) {
  63. this.callbacks = callbacks
  64. this.sourceMode = 'real'
  65. this.mockBridgeUrl = DEFAULT_MOCK_LOCATION_BRIDGE_URL
  66. this.mockBridgeStatusText = `未连接 (${this.mockBridgeUrl})`
  67. this.mockChannelId = 'default'
  68. const sourceCallbacks: LocationSourceCallbacks = {
  69. onLocation: (sample) => {
  70. this.callbacks.onLocation(sample)
  71. this.emitDebugState()
  72. },
  73. onStatus: (message) => {
  74. this.callbacks.onStatus(message)
  75. this.emitDebugState()
  76. },
  77. onError: (message) => {
  78. this.callbacks.onError(message)
  79. this.emitDebugState()
  80. },
  81. }
  82. this.realSource = new RealLocationSource(sourceCallbacks)
  83. this.mockSource = new MockLocationSource(sourceCallbacks)
  84. this.mockBridge = new MockLocationBridge({
  85. onOpen: () => {
  86. this.mockBridgeStatusText = `已连接 (${this.mockBridge.url})`
  87. this.callbacks.onStatus('模拟定位源已连接')
  88. this.emitDebugState()
  89. },
  90. onClose: () => {
  91. this.mockBridgeStatusText = `未连接 (${this.mockBridge.url})`
  92. this.callbacks.onStatus('模拟定位源已断开')
  93. this.emitDebugState()
  94. },
  95. onError: (message) => {
  96. this.mockBridgeStatusText = `连接失败 (${this.mockBridge.url})`
  97. this.callbacks.onError(`模拟定位源错误: ${message}`)
  98. this.emitDebugState()
  99. },
  100. onSample: (sample) => {
  101. this.mockSource.pushSample(sample)
  102. this.emitDebugState()
  103. },
  104. })
  105. }
  106. get listening(): boolean {
  107. return this.sourceMode === 'mock' ? this.mockSource.active : this.realSource.active
  108. }
  109. getDebugState(): LocationControllerDebugState {
  110. return {
  111. sourceMode: this.sourceMode,
  112. sourceModeText: formatSourceModeText(this.sourceMode),
  113. listening: this.listening,
  114. mockBridgeConnected: this.mockBridge.connected,
  115. mockBridgeStatusText: this.mockBridgeStatusText,
  116. mockBridgeUrlText: this.mockBridgeUrl,
  117. mockChannelIdText: this.mockChannelId,
  118. mockCoordText: formatMockCoordText(this.mockSource.lastSample),
  119. mockSpeedText: formatMockSpeedText(this.mockSource.lastSample),
  120. }
  121. }
  122. start(): void {
  123. this.getActiveSource().start()
  124. this.emitDebugState()
  125. }
  126. stop(): void {
  127. this.getActiveSource().stop()
  128. this.emitDebugState()
  129. }
  130. destroy(): void {
  131. this.realSource.destroy()
  132. this.mockSource.destroy()
  133. this.mockBridge.destroy()
  134. this.emitDebugState()
  135. }
  136. setSourceMode(mode: LocationSourceMode): void {
  137. if (this.sourceMode === mode) {
  138. this.callbacks.onStatus(`${formatSourceModeText(mode)}已启用`)
  139. this.emitDebugState()
  140. return
  141. }
  142. const wasListening = this.listening
  143. if (wasListening) {
  144. this.getActiveSource().stop()
  145. }
  146. this.sourceMode = mode
  147. if (wasListening) {
  148. this.getActiveSource().start()
  149. } else {
  150. this.callbacks.onStatus(`已切换到${formatSourceModeText(mode)}`)
  151. }
  152. this.emitDebugState()
  153. }
  154. setMockBridgeUrl(url: string): void {
  155. this.mockBridgeUrl = normalizeMockBridgeUrl(url)
  156. if (this.mockBridge.connected || this.mockBridge.connecting) {
  157. this.mockBridgeStatusText = `已设置新地址,重连生效 (${this.mockBridgeUrl})`
  158. this.callbacks.onStatus('模拟定位源地址已更新,重连后生效')
  159. } else {
  160. this.mockBridgeStatusText = `未连接 (${this.mockBridgeUrl})`
  161. this.callbacks.onStatus('模拟定位源地址已更新')
  162. }
  163. this.emitDebugState()
  164. }
  165. setMockChannelId(channelId: string): void {
  166. const normalized = String(channelId || '').trim() || 'default'
  167. this.mockChannelId = normalized
  168. this.mockBridge.setChannelId(normalized)
  169. this.callbacks.onStatus(`模拟定位通道已切换到 ${normalized}`)
  170. this.emitDebugState()
  171. }
  172. connectMockBridge(url = DEFAULT_MOCK_LOCATION_BRIDGE_URL): void {
  173. if (this.mockBridge.connected || this.mockBridge.connecting) {
  174. this.callbacks.onStatus('模拟定位源已连接')
  175. this.emitDebugState()
  176. return
  177. }
  178. const targetUrl = normalizeMockBridgeUrl(url === DEFAULT_MOCK_LOCATION_BRIDGE_URL ? this.mockBridgeUrl : url)
  179. this.mockBridgeUrl = targetUrl
  180. this.mockBridgeStatusText = `连接中 (${targetUrl})`
  181. this.emitDebugState()
  182. this.callbacks.onStatus('模拟定位源连接中')
  183. this.mockBridge.connect(targetUrl)
  184. }
  185. disconnectMockBridge(): void {
  186. if (!this.mockBridge.connected && !this.mockBridge.connecting) {
  187. this.callbacks.onStatus('模拟定位源未连接')
  188. this.emitDebugState()
  189. return
  190. }
  191. this.mockBridge.disconnect()
  192. this.mockBridgeStatusText = `未连接 (${this.mockBridge.url})`
  193. this.callbacks.onStatus('模拟定位源已断开')
  194. this.emitDebugState()
  195. }
  196. getActiveSource(): RealLocationSource | MockLocationSource {
  197. return this.sourceMode === 'mock' ? this.mockSource : this.realSource
  198. }
  199. emitDebugState(): void {
  200. if (this.callbacks.onDebugStateChange) {
  201. this.callbacks.onDebugStateChange(this.getDebugState())
  202. }
  203. }
  204. }