/** * 지도 컨트롤러 컴포넌트 */ import { MAP_CONFIG, LAYER_CONFIG, COLORS } from '../utils/constants.js'; import { getColorByType } from '../utils/helpers.js'; export class MapController { constructor(containerId = 'map') { this.containerId = containerId; this.map = null; this.deckOverlay = null; this.parsedGeoJsonCache = new Map(); } /** * 지도 초기화 */ async init() { return new Promise((resolve) => { this.map = new maplibregl.Map({ container: this.containerId, style: { version: 8, sources: { 'raster-tiles': { type: 'raster', tiles: ['/api/tiles/world/{z}/{x}/{y}.webp'], tileSize: MAP_CONFIG.TILE_SIZE, attribution: 'Local Tile Server' } }, layers: [{ id: 'simple-tiles', type: 'raster', source: 'raster-tiles', minzoom: MAP_CONFIG.MIN_ZOOM, maxzoom: MAP_CONFIG.MAX_ZOOM }] }, center: MAP_CONFIG.DEFAULT_CENTER, zoom: MAP_CONFIG.DEFAULT_ZOOM }); this.map.on('load', () => { this.deckOverlay = new deck.MapboxOverlay({ layers: [] }); this.map.addControl(this.deckOverlay); this.map.addControl(new maplibregl.NavigationControl()); resolve(); }); }); } /** * 지도 업데이트 * @param {Array} tracks - 표시할 트랙 데이터 * @param {Set} selectedTracks - 선택된 트랙 ID * @param {string} activeVesselId - 활성 선박 ID */ updateMap(tracks, selectedTracks = new Set(), activeVesselId = null) { if (!this.deckOverlay) return; const pathData = []; const pointData = []; // 렌더링할 트랙 결정 let tracksToRender = []; if (activeVesselId) { tracksToRender = tracks.filter(t => t.vesselId === activeVesselId); } else if (selectedTracks.size > 0) { tracksToRender = tracks.filter(t => selectedTracks.has(String(t.id))); } else { tracksToRender = tracks; } // 트랙 데이터 처리 tracksToRender.forEach(track => { const geojson = this.parseTrackGeoJson(track); if (!geojson) return; const isSelected = selectedTracks.has(String(track.id)); const isInActiveGroup = activeVesselId && track.vesselId === activeVesselId; const color = getColorByType(track.abnormalType).slice(0, 3); // 색상 및 너비 결정 let finalColor = color; let width = LAYER_CONFIG.PATH.DEFAULT_WIDTH; let opacity = LAYER_CONFIG.PATH.ACTIVE_OPACITY; if (isInActiveGroup) { if (isSelected) { width = LAYER_CONFIG.PATH.SELECTED_WIDTH; opacity = LAYER_CONFIG.PATH.SELECTED_OPACITY; finalColor = COLORS.SELECTED; } else { width = LAYER_CONFIG.PATH.ACTIVE_WIDTH; opacity = LAYER_CONFIG.PATH.ACTIVE_OPACITY; } } else if (selectedTracks.size > 0 && !isSelected) { finalColor = COLORS.DEFAULT_TRACK; opacity = LAYER_CONFIG.PATH.INACTIVE_OPACITY; } pathData.push({ path: geojson.coordinates.map(coord => [coord[0], coord[1]]), color: finalColor, width: width, opacity: opacity, trackId: String(track.id), vesselId: track.vesselId, selected: isSelected, isInActiveGroup: isInActiveGroup }); // 선택된 트랙의 포인트 표시 if (isSelected) { geojson.coordinates.forEach((coord, idx) => { pointData.push({ position: [coord[0], coord[1]], vesselId: track.vesselId, trackId: track.id, time: new Date((coord[2] || 0) * 1000), index: idx, totalPoints: geojson.coordinates.length }); }); } }); // 선택 상태에 따라 정렬 pathData.sort((a, b) => { if (a.selected && !b.selected) return 1; if (!a.selected && b.selected) return -1; return 0; }); this.renderLayers(pathData, pointData); } /** * 레이어 렌더링 */ renderLayers(pathData, pointData) { const layers = [ new deck.PathLayer({ id: 'tracks', data: pathData, getPath: d => d.path, getColor: d => [...d.color, d.opacity], getWidth: d => d.width, widthMinPixels: LAYER_CONFIG.PATH.WIDTH_MIN, widthMaxPixels: LAYER_CONFIG.PATH.WIDTH_MAX, pickable: true, autoHighlight: true, highlightColor: [255, 255, 0, 200], onClick: ({object}) => { if (object && object.trackId) { this.onTrackClick?.(object.trackId); } } }), new deck.ScatterplotLayer({ id: 'points', data: pointData, getPosition: d => d.position, getFillColor: [255, 255, 255], getRadius: LAYER_CONFIG.POINT.RADIUS, radiusMinPixels: LAYER_CONFIG.POINT.RADIUS_MIN, radiusMaxPixels: LAYER_CONFIG.POINT.RADIUS_MAX, stroked: true, lineWidthMinPixels: LAYER_CONFIG.POINT.STROKE_WIDTH, getLineColor: [0, 0, 0], pickable: true, onHover: ({object, x, y}) => { this.onPointHover?.(object, x, y); } }) ]; this.deckOverlay.setProps({ layers }); } /** * GeoJSON 파싱 */ parseTrackGeoJson(track) { let geojson = this.parsedGeoJsonCache.get(track.id); if (!geojson && track.trackGeoJson) { try { geojson = JSON.parse(track.trackGeoJson); this.parsedGeoJsonCache.set(track.id, geojson); } catch (e) { console.error('Failed to parse GeoJSON:', e); return null; } } if (geojson && geojson.type === 'LineString') { return geojson; } return null; } /** * 지도를 트랙에 맞게 조정 */ fitToTracks(tracks) { const bounds = new maplibregl.LngLatBounds(); tracks.forEach(track => { const geojson = this.parseTrackGeoJson(track); if (geojson) { geojson.coordinates.forEach(coord => { bounds.extend([coord[0], coord[1]]); }); } }); if (!bounds.isEmpty()) { this.map.fitBounds(bounds, { padding: 100 }); } } /** * 캐시 초기화 */ clearCache() { this.parsedGeoJsonCache.clear(); } /** * 이벤트 핸들러 설정 */ setEventHandlers(handlers) { if (handlers.onTrackClick) { this.onTrackClick = handlers.onTrackClick; } if (handlers.onPointHover) { this.onPointHover = handlers.onPointHover; } } }