signal-batch/src/main/resources/static/v2/js/pages/chunked-streaming-app.js
2025-11-19 16:03:16 +09:00

542 lines
16 KiB
JavaScript

/**
* 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, '<i class="bi bi-play-fill"></i>');
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 ? '<i class="bi bi-pause-fill"></i>' : '<i class="bi bi-play-fill"></i>';
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, '<i class="bi bi-play-fill"></i>');
// 통계 초기화
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;