/** * Chunked Streaming GIS Application */ import { WebSocketAPI } from '../api/websocket-api.js'; import { ChunkedDataManager } from '../components/chunked-data-manager.js'; import { ChunkedAnimationController } from '../components/chunked-animation-controller.js'; import { ChunkedMapRenderer } from '../components/chunked-map-renderer.js'; import { getDefaultTimeRange, createTimeRange, updateViewportDisplay, addLog, clearLog, toggleDisplay, updateButtonState, updateProgressBar, updateSpeedButtons, updateTimelineUI, updateStatistics, getRenderOptions, safeAddEventListener, validateQueryParams, showError, showSuccess, getMapViewport } from '../utils/chunked-utils.js'; export class ChunkedStreamingApp { constructor() { this.map = null; this.websocketAPI = new WebSocketAPI(); this.dataManager = new ChunkedDataManager(); this.animationController = new ChunkedAnimationController(); this.mapRenderer = null; this.updateLayersTimer = null; this.queryStartTime = null; } /** * 애플리케이션 초기화 */ async initialize() { try { this.initializeMap(); this.setupEventHandlers(); this.setupWebSocketHandlers(); this.setDefaultTimeRange(); addLog('시스템 준비 완료', 'success'); } catch (error) { console.error('초기화 실패:', error); showError('시스템 초기화에 실패했습니다'); } } /** * 맵 초기화 */ initializeMap() { this.map = new maplibregl.Map({ container: 'mapContainer', style: { version: 8, sources: { 'raster-tiles': { type: 'raster', tiles: ['/api/tiles/enc/{z}/{x}/{y}.webp'], tileSize: 256, attribution: 'Local Tile Server' } }, layers: [{ id: 'simple-tiles', type: 'raster', source: 'raster-tiles', minzoom: 0, maxzoom: 22 }] }, center: [128, 35.5], zoom: 6 }); // 맵 렌더러 초기화 this.mapRenderer = new ChunkedMapRenderer(this.map); this.mapRenderer.initialize(); this.mapRenderer.setDataManager(this.dataManager); // 맵 이벤트 핸들러 this.map.on('moveend', () => updateViewportDisplay(this.map)); this.map.on('zoomend', () => updateViewportDisplay(this.map)); } /** * 이벤트 핸들러 설정 */ setupEventHandlers() { // 연결 버튼 safeAddEventListener('connectBtn', 'click', () => this.connect()); safeAddEventListener('disconnectBtn', 'click', () => this.disconnect()); // 쿼리 제어 safeAddEventListener('startBtn', 'click', () => this.startQuery()); safeAddEventListener('cancelBtn', 'click', () => this.cancelQuery()); // 시간 범위 단축키 [1, 6, 24, 72, 168].forEach(hours => { document.addEventListener('click', (e) => { if (e.target.textContent === `${hours === 1 ? '1시간' : hours === 6 ? '6시간' : hours === 24 ? '1일' : hours === 72 ? '3일' : '7일'}`) { this.setTimeRange(hours); } }); }); // 애니메이션 제어 safeAddEventListener('playBtn', 'click', () => this.togglePlay()); // 속도 제어 document.addEventListener('click', (e) => { if (e.target.classList.contains('btn-speed')) { const speed = parseFloat(e.target.textContent.replace('x', '')); this.setSpeed(speed); } }); // 타임라인 슬라이더 safeAddEventListener('timeSlider', 'input', (e) => { const progress = parseFloat(e.target.value); this.animationController.setTimeFromProgress(progress); }); // 렌더링 옵션 변경 ['showTracks', 'showVessels', 'colorBySpeed', 'trackOpacity'].forEach(id => { safeAddEventListener(id, 'change', () => this.updateRenderOptions()); }); // 로그 초기화 safeAddEventListener('logContainer', 'click', (e) => { if (e.target.textContent === 'Clear') { clearLog(); } }); // 전체 초기화 (닫기 버튼) document.addEventListener('click', (e) => { if (e.target.closest('.btn-close')) { this.resetAll(); } }); } /** * WebSocket 핸들러 설정 */ setupWebSocketHandlers() { this.websocketAPI.onConnectionStatusChange((connected) => { this.updateConnectionStatus(connected); }); this.websocketAPI.setHandlers({ response: (response) => this.handleResponse(response), status: (status) => this.handleStatus(status), chunk: (message) => this.handleChunk(message), error: (error) => this.handleError(error) }); // 애니메이션 컨트롤러 이벤트 this.animationController.setEventHandlers({ onTimeUpdate: (currentTime) => this.onTimeUpdate(currentTime), onPlayStateChange: (playing) => this.onPlayStateChange(playing) }); } /** * 기본 시간 범위 설정 */ setDefaultTimeRange() { const timeRange = getDefaultTimeRange(); document.getElementById('startTime').value = timeRange.startTime; document.getElementById('endTime').value = timeRange.endTime; } /** * 시간 범위 설정 */ setTimeRange(hours) { const timeRange = createTimeRange(hours); document.getElementById('startTime').value = timeRange.startTime; document.getElementById('endTime').value = timeRange.endTime; } /** * WebSocket 연결 */ async connect() { try { await this.websocketAPI.connect(); addLog('WebSocket 연결 성공', 'success'); } catch (error) { console.error('연결 실패:', error); showError('WebSocket 연결에 실패했습니다'); } } /** * WebSocket 연결 해제 */ async disconnect() { try { await this.websocketAPI.disconnect(); addLog('WebSocket 연결 해제', 'info'); } catch (error) { console.error('연결 해제 실패:', error); } } /** * 쿼리 시작 */ async startQuery() { // 입력값 검증 const startTime = document.getElementById('startTime').value + ':00'; const endTime = document.getElementById('endTime').value + ':00'; const validation = validateQueryParams(startTime, endTime); if (!validation.valid) { validation.errors.forEach(error => showError(error)); return; } try { // 초기화 this.dataManager.reset(); this.queryStartTime = Date.now(); // 맵 정보 가져오기 const { viewport, zoom } = getMapViewport(this.map); // 애니메이션 시간 설정 const startTimeMs = new Date(startTime).getTime(); const endTimeMs = new Date(endTime).getTime(); this.animationController.setTimeRange(startTimeMs, endTimeMs); // WebSocket 쿼리 시작 await this.websocketAPI.startQuery({ startTime, endTime, viewport, zoomLevel: zoom, chunkSize: 20000 }); // UI 업데이트 toggleDisplay('startBtn', false); toggleDisplay('cancelBtn', true); updateProgressBar(0); addLog('쿼리 시작...', 'info'); addLog(`요청 시간: ${startTime} ~ ${endTime}`, 'info'); addLog(`현재 줌 레벨: ${zoom}`, 'info'); addLog(`뷰포트: [${viewport.minLon.toFixed(4)}, ${viewport.minLat.toFixed(4)}] - [${viewport.maxLon.toFixed(4)}, ${viewport.maxLat.toFixed(4)}]`, 'info'); } catch (error) { console.error('쿼리 시작 실패:', error); showError('쿼리 시작에 실패했습니다'); } } /** * 쿼리 취소 */ cancelQuery() { this.websocketAPI.cancelQuery(); this.stopTimers(); this.animationController.stop(); updateButtonState('playBtn', true, ''); addLog('쿼리 취소됨', 'info'); } /** * 청크 데이터 처리 */ handleChunk(message) { const result = this.dataManager.processChunk(message); const { chunk, stats } = result; this.updateStatistics(); // 첫 청크 수신 시 UI 활성화 및 애니메이션 자동 시작 if (this.dataManager.getStatistics().chunksReceived === 1) { toggleDisplay('timelinePanel', true); toggleDisplay('legend', true); this.setSpeed(50); // 50x 속도 설정 // 애니메이션 자동 시작 this.animationController.animationState.playing = true; this.animationController.forceStart(); this.onPlayStateChange(true); } // 주기적 레이어 업데이트 (1초마다) if (!this.updateLayersTimer) { this.updateLayersTimer = setInterval(() => { this.updateLayers(); }, 1000); } // 로그 메시지 const timeProgress = stats.timeProgress || 0; const vesselProgress = stats.vesselProgress || 0; const dataStats = this.dataManager.getStatistics(); addLog( `청크 ${chunk.chunkIndex + 1}/${chunk.totalChunks || '?'}: ${chunk.compactTracks?.length || 0}척, ${dataStats.pointCount.toLocaleString()}포인트 | 시간: ${timeProgress.toFixed(1)}% | 선박: ${vesselProgress.toFixed(1)}%`, 'chunk' ); } /** * 응답 처리 */ handleResponse(response) { if (response.status === 'STARTED') { this.websocketAPI.setCurrentQueryId(response.queryId); } } /** * 상태 처리 */ handleStatus(status) { if (status.progressPercentage >= 0) { updateProgressBar(status.progressPercentage); } if (status.status === 'COMPLETED') { this.handleComplete(); } } /** * 에러 처리 */ handleError(error) { showError(error); } /** * 완료 처리 */ handleComplete() { const duration = ((Date.now() - this.queryStartTime) / 1000).toFixed(1); const stats = this.dataManager.getStatistics(); updateStatistics({ processTime: duration }); showSuccess(`완료! ${stats.vesselCount}척, ${duration}초 소요`); // UI 초기화 toggleDisplay('startBtn', true); toggleDisplay('cancelBtn', false); updateProgressBar(0); // 타이머 정리 this.stopTimers(); // 마지막 레이어 업데이트 this.updateLayers(); // 맵 범위 조정 this.mapRenderer.fitMapBounds(); } /** * 애니메이션 토글 */ togglePlay() { // 데이터가 없으면 재생하지 않음 if (!this.dataManager.hasData()) { addLog('재생할 데이터가 없습니다. 먼저 스트리밍을 시작하세요.', 'info'); return; } // 시간 범위가 설정되지 않았으면 설정 const timeRange = this.animationController.getTimeRange(); if (timeRange.startTime === 0 || timeRange.endTime === 0) { addLog('애니메이션 시간 범위를 설정 중...', 'info'); // 현재 입력된 시간으로 설정 const startTime = new Date(document.getElementById('startTime').value + ':00').getTime(); const endTime = new Date(document.getElementById('endTime').value + ':00').getTime(); this.animationController.setTimeRange(startTime, endTime); } this.animationController.togglePlay(); } /** * 애니메이션 시작 */ startAnimation() { if (!this.dataManager.hasData()) { return; } this.animationController.start(); } /** * 애니메이션 속도 설정 */ setSpeed(speed) { this.animationController.setSpeed(speed); updateSpeedButtons(speed); } /** * 시간 업데이트 콜백 */ onTimeUpdate(currentTime) { const timelineData = this.animationController.getTimelineData(); updateTimelineUI(timelineData); this.updateLayers(); } /** * 재생 상태 변경 콜백 */ onPlayStateChange(playing) { const icon = playing ? '' : ''; updateButtonState('playBtn', true, icon); } /** * 렌더링 옵션 업데이트 */ updateRenderOptions() { const options = getRenderOptions(); this.mapRenderer.setRenderOptions(options); this.updateLayers(); // 범례 표시/숨김 if (options.showTracks || options.showVessels) { toggleDisplay('legend', true); } } /** * 레이어 업데이트 */ updateLayers() { const currentTime = this.animationController.getCurrentTime(); this.mapRenderer.updateLayers(currentTime); } /** * 연결 상태 업데이트 */ updateConnectionStatus(connected) { updateButtonState('connectBtn', !connected); updateButtonState('disconnectBtn', connected); updateButtonState('startBtn', connected); } /** * 통계 정보 업데이트 */ updateStatistics() { const stats = this.dataManager.getStatistics(); updateStatistics(stats); } /** * 전체 초기화 */ resetAll() { // 애니메이션 및 타이머 정리 this.animationController.reset(); this.stopTimers(); // 데이터 초기화 this.dataManager.reset(); // 맵 레이어 초기화 this.mapRenderer.clearLayers(); // UI 초기화 toggleDisplay('timelinePanel', false); toggleDisplay('legend', false); toggleDisplay('startBtn', true); toggleDisplay('cancelBtn', false); updateProgressBar(0); updateButtonState('playBtn', true, ''); // 통계 초기화 this.updateStatistics(); showSuccess('시스템 초기화 완료'); } /** * 타이머 정리 */ stopTimers() { if (this.updateLayersTimer) { clearInterval(this.updateLayersTimer); this.updateLayersTimer = null; } } } // 전역 인스턴스 생성 및 초기화 let app; window.addEventListener('load', async () => { app = new ChunkedStreamingApp(); await app.initialize(); }); // 전역 함수들 (기존 HTML에서 onclick 이벤트용) window.connect = () => app?.connect(); window.disconnect = () => app?.disconnect(); window.startQuery = () => app?.startQuery(); window.cancelQuery = () => app?.cancelQuery(); window.togglePlay = () => app?.togglePlay(); window.setSpeed = (speed) => app?.setSpeed(speed); window.setTimeRange = (hours) => app?.setTimeRange(hours); window.resetAll = () => app?.resetAll(); window.clearLog = () => clearLog(); // 디버깅용 전역 함수들 window.debugAnimation = () => { if (app) { console.log('Animation State:', app.animationController.debugState()); console.log('Data Manager:', { hasData: app.dataManager.hasData(), statistics: app.dataManager.getStatistics() }); } }; window.debugApp = () => app;