locationController.ts 6.7 KB

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