import { DEFAULT_MOCK_LOCATION_BRIDGE_URL, MockLocationBridge } from './mockLocationBridge' import { MockLocationSource } from './mockLocationSource' import { RealLocationSource } from './realLocationSource' import { type LocationSample, type LocationSourceCallbacks, type LocationSourceMode } from './locationSource' export interface LocationUpdate extends LocationSample {} export interface LocationControllerDebugState { sourceMode: LocationSourceMode sourceModeText: string listening: boolean mockBridgeConnected: boolean mockBridgeStatusText: string mockBridgeUrlText: string mockChannelIdText: string mockCoordText: string mockSpeedText: string } export interface LocationControllerCallbacks { onLocation: (update: LocationUpdate) => void onStatus: (message: string) => void onError: (message: string) => void onDebugStateChange?: (state: LocationControllerDebugState) => void } function formatSourceModeText(mode: LocationSourceMode): string { return mode === 'mock' ? '模拟定位' : '真实定位' } function formatMockCoordText(sample: LocationSample | null): string { if (!sample) { return '--' } return `${sample.latitude.toFixed(6)}, ${sample.longitude.toFixed(6)}` } function formatMockSpeedText(sample: LocationSample | null): string { if (!sample || !Number.isFinite(sample.speed)) { return '--' } return `${(Number(sample.speed) * 3.6).toFixed(1)} km/h` } function normalizeMockBridgeUrl(rawUrl: string): string { const trimmed = rawUrl.trim() if (!trimmed) { return DEFAULT_MOCK_LOCATION_BRIDGE_URL } let normalized = trimmed if (!/^wss?:\/\//i.test(normalized)) { normalized = `ws://${normalized.replace(/^\/+/, '')}` } if (!/\/mock-gps(?:\?.*)?$/i.test(normalized)) { normalized = normalized.replace(/\/+$/, '') normalized = `${normalized}/mock-gps` } return normalized } export class LocationController { callbacks: LocationControllerCallbacks realSource: RealLocationSource mockSource: MockLocationSource mockBridge: MockLocationBridge sourceMode: LocationSourceMode mockBridgeStatusText: string mockBridgeUrl: string mockChannelId: string constructor(callbacks: LocationControllerCallbacks) { this.callbacks = callbacks this.sourceMode = 'real' this.mockBridgeUrl = DEFAULT_MOCK_LOCATION_BRIDGE_URL this.mockBridgeStatusText = `未连接 (${this.mockBridgeUrl})` this.mockChannelId = 'default' const sourceCallbacks: LocationSourceCallbacks = { onLocation: (sample) => { this.callbacks.onLocation(sample) this.emitDebugState() }, onStatus: (message) => { this.callbacks.onStatus(message) this.emitDebugState() }, onError: (message) => { this.callbacks.onError(message) this.emitDebugState() }, } this.realSource = new RealLocationSource(sourceCallbacks) this.mockSource = new MockLocationSource(sourceCallbacks) this.mockBridge = new MockLocationBridge({ onOpen: () => { this.mockBridgeStatusText = `已连接 (${this.mockBridge.url})` this.callbacks.onStatus('模拟定位源已连接') this.emitDebugState() }, onClose: () => { this.mockBridgeStatusText = `未连接 (${this.mockBridge.url})` this.callbacks.onStatus('模拟定位源已断开') this.emitDebugState() }, onError: (message) => { this.mockBridgeStatusText = `连接失败 (${this.mockBridge.url})` this.callbacks.onError(`模拟定位源错误: ${message}`) this.emitDebugState() }, onSample: (sample) => { this.mockSource.pushSample(sample) this.emitDebugState() }, }) } get listening(): boolean { return this.sourceMode === 'mock' ? this.mockSource.active : this.realSource.active } getDebugState(): LocationControllerDebugState { return { sourceMode: this.sourceMode, sourceModeText: formatSourceModeText(this.sourceMode), listening: this.listening, mockBridgeConnected: this.mockBridge.connected, mockBridgeStatusText: this.mockBridgeStatusText, mockBridgeUrlText: this.mockBridgeUrl, mockChannelIdText: this.mockChannelId, mockCoordText: formatMockCoordText(this.mockSource.lastSample), mockSpeedText: formatMockSpeedText(this.mockSource.lastSample), } } start(): void { this.getActiveSource().start() this.emitDebugState() } stop(): void { this.getActiveSource().stop() this.emitDebugState() } destroy(): void { this.realSource.destroy() this.mockSource.destroy() this.mockBridge.destroy() this.emitDebugState() } setSourceMode(mode: LocationSourceMode): void { if (this.sourceMode === mode) { this.callbacks.onStatus(`${formatSourceModeText(mode)}已启用`) this.emitDebugState() return } const wasListening = this.listening if (wasListening) { this.getActiveSource().stop() } this.sourceMode = mode if (wasListening) { this.getActiveSource().start() } else { this.callbacks.onStatus(`已切换到${formatSourceModeText(mode)}`) } this.emitDebugState() } setMockBridgeUrl(url: string): void { this.mockBridgeUrl = normalizeMockBridgeUrl(url) if (this.mockBridge.connected || this.mockBridge.connecting) { this.mockBridgeStatusText = `已设置新地址,重连生效 (${this.mockBridgeUrl})` this.callbacks.onStatus('模拟定位源地址已更新,重连后生效') } else { this.mockBridgeStatusText = `未连接 (${this.mockBridgeUrl})` this.callbacks.onStatus('模拟定位源地址已更新') } this.emitDebugState() } setMockChannelId(channelId: string): void { const normalized = String(channelId || '').trim() || 'default' this.mockChannelId = normalized this.mockBridge.setChannelId(normalized) this.callbacks.onStatus(`模拟定位通道已切换到 ${normalized}`) this.emitDebugState() } connectMockBridge(url = DEFAULT_MOCK_LOCATION_BRIDGE_URL): void { if (this.mockBridge.connected || this.mockBridge.connecting) { this.callbacks.onStatus('模拟定位源已连接') this.emitDebugState() return } const targetUrl = normalizeMockBridgeUrl(url === DEFAULT_MOCK_LOCATION_BRIDGE_URL ? this.mockBridgeUrl : url) this.mockBridgeUrl = targetUrl this.mockBridgeStatusText = `连接中 (${targetUrl})` this.emitDebugState() this.callbacks.onStatus('模拟定位源连接中') this.mockBridge.connect(targetUrl) } disconnectMockBridge(): void { if (!this.mockBridge.connected && !this.mockBridge.connecting) { this.callbacks.onStatus('模拟定位源未连接') this.emitDebugState() return } this.mockBridge.disconnect() this.mockBridgeStatusText = `未连接 (${this.mockBridge.url})` this.callbacks.onStatus('模拟定位源已断开') this.emitDebugState() } getActiveSource(): RealLocationSource | MockLocationSource { return this.sourceMode === 'mock' ? this.mockSource : this.realSource } emitDebugState(): void { if (this.callbacks.onDebugStateChange) { this.callbacks.onDebugStateChange(this.getDebugState()) } } }