signal-batch/src/main/resources/static/v2/js/components/map-controller.js
2025-11-19 16:03:16 +09:00

246 lines
8.0 KiB
JavaScript

/**
* 지도 컨트롤러 컴포넌트
*/
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;
}
}
}