UNPKG

@rcsb/rcsb-saguaro

Version:
362 lines (361 loc) 13.2 kB
import { zoom, zoomIdentity } from "d3-zoom"; import { RcsbD3Manager } from "./RcsbD3/RcsbD3Manager"; import * as classes from "../scss/RcsbBoard.module.scss"; import { MOUSE } from "./RcsbD3/RcsbD3Constants"; import { CONDITIONAL_FLAG, EventType } from "../RcsbFv/RcsbFvContextManager/RcsbFvContextManager"; import { RcsbD3EventDispatcher } from "./RcsbD3/RcsbD3EventDispatcher"; import { RcsbFvDefaultConfigValues } from "../RcsbFv/RcsbFvConfig/RcsbFvDefaultConfigValues"; import { asyncScheduler, Subject } from "rxjs"; import { RcsbWindowEventManager } from "./RcsbWindowEventManager"; export class RcsbBoard { constructor(elementId, xScale, selection, contextManager) { this.d3Manager = new RcsbD3Manager(); this._width = 920; this._bgColor = "#FFFFFF"; this._innerPadding = 10; this.tracks = new Array(); this.highlightHoverElementFlag = false; this.limits = { max: 1000000000, min: -1.5, maxZoom: 1000000000, minZoom: 20 }; this.currentLocationView = { from: 1, to: 500 }; this.updateTask = null; this.updateDelay = 300; this.upToDate = true; this.isIntersecting = true; this.zoomEventHandler = zoom(); this.boardSubject = { mousemove: new Subject(), mouseenter: new Subject(), mouseleave: new Subject() }; this.mouseHoverSubject = new Subject(); this.elementClickSubject = new Subject(); this.scrollEvent = (isIntersecting) => { this.isIntersecting = isIntersecting; if (!this.upToDate && this.isIntersecting) this.updateAndMove(); }; this.domId = elementId; this.contextManager = contextManager; this._xScale = xScale; this.selection = selection; const boardDiv = document.getElementById(this.domId); if (boardDiv == null) { throw "Board DOM [" + this.domId + "] element not found. Removing scroll event handler from window"; } this.boardDiv = boardDiv; RcsbWindowEventManager.intersectionObserve(this.boardDiv, this.scrollEvent); } removeScrollEvent() { RcsbWindowEventManager.intersectionUnobserve(this.boardDiv); } addSVG() { const svgConfig = { elementId: this.domId, svgClass: classes.rcsbSvg, domClass: classes.rcsbDom, width: this._width, pointerEvents: "all", boardSubject: this.boardSubject, xScale: this._xScale }; this.d3Manager.buildSvgNode(svgConfig); this.addMainG(); } addMainG() { const innerConfig = { masterClass: classes.rcsbMasterG, innerClass: classes.rcsbInnerG, mouseUp: (event) => { if (event.button === MOUSE.RIGHT) { event.preventDefault(); event.stopImmediatePropagation(); RcsbD3EventDispatcher.boardMouseup(event, this); } }, mouseDown: (event) => { if (event.button === MOUSE.RIGHT) { event.preventDefault(); event.stopImmediatePropagation(); RcsbD3EventDispatcher.boardMousedown(event, this); } }, dblClick: (event) => { this.highlightRegion(null, 'set', 'select', false); this.elementClickSubject.next({}); }, mouseEnter: (event) => { if (RcsbD3EventDispatcher.keepSelectingFlag) { RcsbD3EventDispatcher.changeTrack(event, this); } }, mouseLeave: (event) => { if (RcsbD3EventDispatcher.keepSelectingFlag) { RcsbD3EventDispatcher.leavingTrack(event, this); } } }; this.d3Manager.addMainG(innerConfig); const paneConfig = { bgColor: this._bgColor, elementId: this.domId + "_pane", paneClass: classes.rcsbPane }; this.d3Manager.addPane(paneConfig); } setElementClickCallback(f) { this.elementClickSubject.subscribe(({ d, e }) => f(d, e)); } setHighlightHoverPosition() { this.boardSubject.mousemove.subscribe((d) => { if (this.contextManager.getCondition(CONDITIONAL_FLAG.STOP_MOUSE_MOVE_HOVERING_HIGHLIGHT)) return; this.highlightRegion({ begin: d.n, nonSpecific: true }, 'set', 'hover'); }); } addHoverCallback(f) { this.mouseHoverSubject.subscribe(f); } setRange(from, to) { this.currentLocationView.from = from; this.currentLocationView.to = to; this.limits.min = from; this.limits.max = to; if (this.limits.minZoom > (to - from)) { const delta = RcsbFvDefaultConfigValues.increasedView * 0.5; this.currentLocationView.from = from + delta; this.currentLocationView.to = to - delta; this.limits.min = from + delta; this.limits.max = to - delta; this.limits.minZoom = this.limits.max - this.limits.min; } if ((this.limits.max - this.limits.min) < this.limits.maxZoom) { this.limits.maxZoom = this.limits.max - this.limits.min; } } setSelection(boardId, mode) { if (this.domId != boardId) this.highlightRegion(null, 'set', mode, true); } highlightRegion(d, operation, mode, propFlag) { if (d != null) { if (operation === 'set') this.selection.setSelected({ rcsbFvTrackDataElement: d, domId: this.domId }, mode); else if (operation === 'add' || operation === 'replace-last') this.selection.addSelected({ rcsbFvTrackDataElement: d, domId: this.domId }, mode, operation === 'replace-last'); } else if (propFlag === false) { this.selection.clearSelection(mode); } if (propFlag != true) { this.triggerSelectionEvent({ eventType: EventType.SELECTION, eventData: { trackId: this.domId, mode: mode } }); if (mode === 'hover') this.mouseHoverSubject.next(this.selection.getSelected('hover').map(r => r.rcsbFvTrackDataElement)); } if (this.selection.getSelected(mode).length > 0) { this.tracks.forEach((track) => { track.highlightRegion(this.selection.getSelected(mode).map(d => d.rcsbFvTrackDataElement), mode === 'hover' ? { color: "#FFCCCC", rectClass: classes.rcsbHoverRect } : undefined); }); } else { this.tracks.forEach((track) => { track.highlightRegion(null, mode === 'hover' ? { color: "#FFCCCC", rectClass: classes.rcsbHoverRect } : undefined); }); } } moveSelection() { if (this.selection.getSelected('select').length > 0) { this.tracks.forEach((track) => { track.moveSelection('select'); }); } if (this.selection.getSelected('hover').length > 0) { this.tracks.forEach((track) => { track.moveSelection('hover'); }); } } startBoard() { if ((this.currentLocationView.to - this.currentLocationView.from) < this.limits.minZoom) { this.currentLocationView.to = this.currentLocationView.from + this.limits.minZoom; } else if ((this.currentLocationView.to - this.currentLocationView.from) > this.limits.maxZoom) { this.currentLocationView.to = this.currentLocationView.from + this.limits.maxZoom; } this.addSVG(); if (!this.xScale().checkAndSetScale([this.currentLocationView.from, this.currentLocationView.to], [this._innerPadding, this._width - this._innerPadding]) && this.selection.getSelected("select").length > 0) { this.selection.clearSelection("select"); } this.d3Manager.addZoom({ zoomEventHandler: this.zoomEventHandler, zoomCallback: this.moveBoard.bind(this) }); this.startTracks(); } startTracks() { this.tracks.forEach(track => { track.init(this._xScale); }); this.setBoardHeight(); this.tracks.forEach((track) => { track.update(); }); } reset() { this.tracks = new Array(); this.d3Manager.resetAllTracks(); } setHighlightHoverElement(flag) { this.highlightHoverElementFlag = flag; } addHighlightHoverElement(t) { t.subscribeElementHighlight({ enter: (d) => { this.contextManager.setCondition(CONDITIONAL_FLAG.STOP_MOUSE_MOVE_HOVERING_HIGHLIGHT, true); this.highlightRegion(d, 'set', 'hover'); }, leave: (d) => { this.highlightRegion(null, 'set', 'hover', false); this.contextManager.setCondition(CONDITIONAL_FLAG.STOP_MOUSE_MOVE_HOVERING_HIGHLIGHT, false); } }); } addTrack(track, options) { if (track instanceof Array) { track.forEach((t) => { this.addTrackCallbacks(t); }); } else { this.addTrackCallbacks(track); } } addTrackCallbacks(t) { t.setManagers(this.d3Manager, this.contextManager); t.setBoardHighlight(this.highlightRegion.bind(this)); if (this.highlightHoverElementFlag) this.addHighlightHoverElement(t); this.boardSubject.mouseleave.subscribe(e => t.trackSubject.mouseleave.next(e)); this.boardSubject.mouseenter.subscribe(e => t.trackSubject.mouseenter.next(e)); this.boardSubject.mousemove.subscribe(d => t.trackSubject.mousemove.next(d)); this.tracks.push(t); } setBoardHeight() { let h = 0; this.tracks.forEach(track => { h += track.height(); }); this.d3Manager.setBoardHeight(h); } setBoardWidth(w) { this._width = w; } setLocation(from, to) { this.currentLocationView = { from: from, to: to }; } updateAllTracks() { const location = this.xScale().domain(); this.setLocation(~~location[0], ~~location[1]); this.tracks.forEach(track => { track.update(); }); } moveAllTracks() { this.tracks.forEach(track => { track.move(); }); } xScale() { return this._xScale; } moveBoard(event, newTransform, propFlag) { let transform; const isNotIdentity = (transform) => { return !(transform.x === 0 && transform.y === 0 && transform.k === 1); }; if (typeof newTransform === "object") { transform = newTransform; } else if (isNotIdentity(event.transform)) { transform = event.transform; } else { return; } let newDomain = transform.rescaleX(this._xScale.getScale()).domain(); let length = newDomain[1] - newDomain[0]; if (length < this.limits.minZoom) { this.d3Manager.zoomG().call(this.zoomEventHandler.transform, zoomIdentity); return; } if (length > this.limits.maxZoom) { newDomain = [this.limits.min, this.limits.max]; } else if (newDomain[0] < this.limits.min) { newDomain = [this.limits.min, this.limits.min + length]; } else if (newDomain[1] > this.limits.max) { newDomain = [this.limits.max - length, this.limits.max]; } this._xScale.domain(newDomain); this.d3Manager.zoomG().call(this.zoomEventHandler.transform, zoomIdentity); this.updateAndMove(); if (!propFlag) { this.triggerScaleEvent({ eventType: EventType.SCALE, eventData: this.domId }); } } ; updateAndMove() { if (this.isIntersecting) { this.moveAllTracks(); this.moveSelection(); this.updateWithDelay(); this.upToDate = true; } else { this.upToDate = false; } } updateWithDelay() { var _a; (_a = this.updateTask) === null || _a === void 0 ? void 0 : _a.unsubscribe(); this.updateTask = asyncScheduler.schedule(() => { this.updateAllTracks(); }, this.updateDelay); } ; setScale(domId) { if (domId != this.domId) { this.updateAndMove(); } } getSelection() { return this.selection; } triggerScaleEvent(geoTrans) { this.contextManager.next(geoTrans); } triggerSelectionEvent(selection) { this.contextManager.next(selection); } }