// 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); })();