222 lines
8.2 KiB
JavaScript
222 lines
8.2 KiB
JavaScript
// igv_custom.js
|
|
// IGV 브라우저가 생성된 후, 파란색 라인 2개를 ideogram/viewport에 추가하고 드래그로 이동 가능하게 함
|
|
// 라인 위치 변경 시 window에 커스텀 이벤트('igv-blue-lines-changed')를 dispatch
|
|
|
|
(function() {
|
|
// 설정값
|
|
const LINE_COLOR = '#007bff';
|
|
const LINE_WIDTH = 2;
|
|
const INIT_RATIO_1 = 0.3; // 30% 위치
|
|
const INIT_RATIO_2 = 0.7; // 70% 위치
|
|
|
|
// 내부 상태
|
|
let viewportRect = null;
|
|
let dragging = null; // 'start' or 'end' or null
|
|
let dragOffset = 0;
|
|
let startRatio = INIT_RATIO_1;
|
|
let endRatio = INIT_RATIO_2;
|
|
|
|
// 라인 DOM
|
|
let viewportLines = [];
|
|
let viewportFill = null;
|
|
let ideogramLines = [];
|
|
let ideogramFill = null;
|
|
|
|
// IGV가 준비되면 실행
|
|
function waitForIGVAndInit() {
|
|
const check = () => {
|
|
const viewport = document.querySelector('.igv-viewport-content');
|
|
if (viewport) {
|
|
setupLines();
|
|
} else {
|
|
setTimeout(check, 300);
|
|
}
|
|
};
|
|
check();
|
|
}
|
|
|
|
// 라인 생성 및 이벤트 바인딩
|
|
function setupLines() {
|
|
// 모든 파란 라인 삭제
|
|
document.querySelectorAll('.igv-blue-line').forEach(el => el.remove());
|
|
// 기존 fill 오버레이 삭제
|
|
document.querySelectorAll('.igv-blue-fill').forEach(el => el.remove());
|
|
|
|
// 모든 .igv-viewport-content 중, height가 16px 초과인 첫 번째를 메인 뷰포트로 간주
|
|
const viewports = Array.from(document.querySelectorAll('.igv-viewport-content'));
|
|
const mainViewport = viewports.find(vp => {
|
|
const h = vp.offsetHeight || parseInt(vp.style.height, 10);
|
|
return h > 16;
|
|
});
|
|
if (!mainViewport) return;
|
|
|
|
// 이미 있으면 중복 생성 방지
|
|
if (mainViewport.parentNode.querySelector('.igv-blue-line')) return;
|
|
|
|
viewportRect = mainViewport.getBoundingClientRect();
|
|
|
|
// 라인 2개 생성 (뷰포트)
|
|
viewportLines = [createLine('start', 'viewport', mainViewport), createLine('end', 'viewport', mainViewport)];
|
|
viewportLines.forEach(line => mainViewport.parentNode.appendChild(line));
|
|
// fill 오버레이 생성 및 추가 (뷰포트)
|
|
viewportFill = document.createElement('div');
|
|
viewportFill.className = 'igv-blue-fill';
|
|
Object.assign(viewportFill.style, {
|
|
position: 'absolute',
|
|
top: (viewportRect.top - mainViewport.getBoundingClientRect().top) + 'px',
|
|
height: viewportRect.height + 'px',
|
|
background: 'rgba(0,123,255,0.18)',
|
|
zIndex: 999,
|
|
pointerEvents: 'none',
|
|
borderRadius: '2px',
|
|
transition: 'left 0.08s, width 0.08s',
|
|
});
|
|
mainViewport.parentNode.appendChild(viewportFill);
|
|
|
|
// === ideogram(미니맵)에도 라인/오버레이 추가 ===
|
|
const ideogram = document.querySelector('.igv-ideogram-content') || document.querySelector('.igv-ideogram');
|
|
if (ideogram) {
|
|
const ideogramRect = ideogram.getBoundingClientRect();
|
|
ideogramLines = [createLine('start', 'ideogram', ideogram), createLine('end', 'ideogram', ideogram)];
|
|
ideogramLines.forEach(line => ideogram.parentNode.appendChild(line));
|
|
ideogramFill = document.createElement('div');
|
|
ideogramFill.className = 'igv-blue-fill';
|
|
Object.assign(ideogramFill.style, {
|
|
position: 'absolute',
|
|
top: (ideogramRect.top - ideogram.parentNode.getBoundingClientRect().top) + 'px',
|
|
height: ideogramRect.height + 'px',
|
|
background: 'rgba(0,123,255,0.18)',
|
|
zIndex: 999,
|
|
pointerEvents: 'none',
|
|
borderRadius: '2px',
|
|
transition: 'left 0.08s, width 0.08s',
|
|
});
|
|
ideogram.parentNode.appendChild(ideogramFill);
|
|
}
|
|
|
|
updateLinePositions();
|
|
bindDragEvents();
|
|
}
|
|
|
|
// 라인 DOM 생성
|
|
function createLine(which, area, parentEl) {
|
|
const line = document.createElement('div');
|
|
line.className = 'igv-blue-line';
|
|
line.dataset.which = which;
|
|
line.dataset.area = area;
|
|
line._parentEl = parentEl; // 커스텀 속성으로 부모 저장
|
|
Object.assign(line.style, {
|
|
position: 'absolute',
|
|
top: area === 'ideogram' ? (viewportRect.top - parentEl.getBoundingClientRect().top) + 'px' : (viewportRect.top - parentEl.getBoundingClientRect().top) + 'px',
|
|
width: LINE_WIDTH + 'px',
|
|
height: area === 'ideogram' ? viewportRect.height + 'px' : viewportRect.height + 'px',
|
|
background: LINE_COLOR,
|
|
zIndex: 1000,
|
|
cursor: 'ew-resize',
|
|
userSelect: 'none',
|
|
});
|
|
line.addEventListener('mousedown', (e) => startDrag(which, e));
|
|
return line;
|
|
}
|
|
|
|
// 라인 위치 갱신
|
|
function updateLinePositions() {
|
|
viewportRect = document.querySelector('.igv-viewport-content').getBoundingClientRect();
|
|
let baseArea = document.querySelector('.igv-viewport-content canvas, .igv-viewport-content svg');
|
|
let baseRect = baseArea ? baseArea.getBoundingClientRect() : viewportRect;
|
|
// viewport 라인 위치 (실제 베이스 표시 영역 기준)
|
|
const baseWidth = baseRect.width;
|
|
const baseLeft = baseRect.left;
|
|
const startX_view = baseLeft + baseWidth * startRatio;
|
|
const endX_view = baseLeft + baseWidth * endRatio;
|
|
viewportLines[0].style.left = (startX_view - viewportLines[0]._parentEl.getBoundingClientRect().left) + 'px';
|
|
viewportLines[1].style.left = (endX_view - viewportLines[1]._parentEl.getBoundingClientRect().left) + 'px';
|
|
// fill 오버레이 위치/크기 갱신
|
|
if (viewportFill) {
|
|
const left = Math.min(startX_view, endX_view) - viewportFill.parentNode.getBoundingClientRect().left;
|
|
const width = Math.abs(endX_view - startX_view);
|
|
viewportFill.style.left = left + 'px';
|
|
viewportFill.style.width = width + 'px';
|
|
}
|
|
// === ideogram(미니맵) 라인/오버레이 위치 갱신 ===
|
|
const ideogram = document.querySelector('.igv-ideogram-content') || document.querySelector('.igv-ideogram');
|
|
if (ideogram && ideogramLines.length === 2 && ideogramFill) {
|
|
const ideogramRect = ideogram.getBoundingClientRect();
|
|
const ideogramWidth = ideogramRect.width;
|
|
const ideogramLeft = ideogramRect.left;
|
|
const startX_ideo = ideogramLeft + ideogramWidth * startRatio;
|
|
const endX_ideo = ideogramLeft + ideogramWidth * endRatio;
|
|
ideogramLines[0].style.left = (startX_ideo - ideogram.parentNode.getBoundingClientRect().left) + 'px';
|
|
ideogramLines[1].style.left = (endX_ideo - ideogram.parentNode.getBoundingClientRect().left) + 'px';
|
|
// fill
|
|
const left = Math.min(startX_ideo, endX_ideo) - ideogramFill.parentNode.getBoundingClientRect().left;
|
|
const width = Math.abs(endX_ideo - startX_ideo);
|
|
ideogramFill.style.left = left + 'px';
|
|
ideogramFill.style.width = width + 'px';
|
|
}
|
|
}
|
|
|
|
// 드래그 이벤트 바인딩
|
|
function bindDragEvents() {
|
|
document.addEventListener('mousemove', onDrag);
|
|
document.addEventListener('mouseup', stopDrag);
|
|
}
|
|
|
|
function startDrag(which, e) {
|
|
dragging = which;
|
|
dragOffset = e.clientX;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
function onDrag(e) {
|
|
if (!dragging) return;
|
|
const areaWidth = viewportRect.width;
|
|
let x = e.clientX - viewportRect.left;
|
|
let ratio = x / areaWidth;
|
|
ratio = Math.max(0, Math.min(1, ratio));
|
|
if (dragging === 'start') {
|
|
startRatio = Math.min(ratio, endRatio - 0.01);
|
|
} else {
|
|
endRatio = Math.max(ratio, startRatio + 0.01);
|
|
}
|
|
updateLinePositions();
|
|
fireChangeEvent();
|
|
}
|
|
|
|
function stopDrag() {
|
|
dragging = null;
|
|
}
|
|
|
|
// 라인 위치 변경 이벤트 발생
|
|
function fireChangeEvent() {
|
|
console.log('fireChangeEvent 호출됨', { startRatio, endRatio });
|
|
window.dispatchEvent(new CustomEvent('igv-blue-lines-changed', {
|
|
detail: {
|
|
startRatio,
|
|
endRatio
|
|
}
|
|
}));
|
|
}
|
|
|
|
// 외부에서 라인 위치를 강제로 지정할 수 있도록 export
|
|
window.igvCustom = {
|
|
setLineRatios: (start, end) => {
|
|
startRatio = start;
|
|
endRatio = end;
|
|
updateLinePositions();
|
|
fireChangeEvent();
|
|
},
|
|
getLineRatios: () => ({ startRatio, endRatio }),
|
|
setupLines: setupLines
|
|
};
|
|
|
|
// 스타일 추가
|
|
const style = document.createElement('style');
|
|
style.innerHTML = `.igv-blue-line { pointer-events: auto !important; }
|
|
.igv-blue-fill { pointer-events: none !important; }`;
|
|
document.head.appendChild(style);
|
|
|
|
// IGV가 준비되면 실행
|
|
setTimeout(waitForIGVAndInit, 1000);
|
|
})();
|