@rcsb/rcsb-saguaro
Version:
RCSB 1D Feature Viewer
362 lines (361 loc) • 13.2 kB
JavaScript
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);
}
}