246 lines
8.0 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
} |