@rcsb/rcsb-saguaro
Version:
RCSB 1D Feature Viewer
314 lines (313 loc) • 11.4 kB
JavaScript
import { __awaiter } from "tslib";
import { jsx as _jsx } from "react/jsx-runtime";
import { createRoot } from "react-dom/client";
import { RcsbFvBoard } from "./RcsbFvBoard/RcsbFvBoard";
import { EventType, RcsbFvContextManager } from "./RcsbFvContextManager/RcsbFvContextManager";
import { RcsbSelection } from "../RcsbBoard/RcsbSelection";
import { RcsbD3ScaleFactory } from "../RcsbBoard/RcsbD3/RcsbD3ScaleFactory";
import { BoardDataState } from "./RcsbFvBoard/Utils/BoardDataState";
import uniqid from "uniqid";
import { RcsbFvStateManager } from "./RcsbFvState/RcsbFvStateManager";
import { RcsbFvDefaultConfigValues } from "./RcsbFvConfig/RcsbFvDefaultConfigValues";
import { asyncScheduler } from "rxjs";
/**
* Protein Feature Viewer entry point
*/
export class RcsbFv {
constructor(props) {
/**rxjs event based handler used to communicate events (click, highlight, move) between board tracks*/
this.contextManager = new RcsbFvContextManager();
/**Flag indicating that the React component has been mounted*/
this.mounted = false;
/**Global d3 Xscale object shared among all board tracks*/
this.xScale = RcsbD3ScaleFactory.getLinearScale();
/**Global selection shared among all tracks*/
this.selection = new RcsbSelection();
this.boardId = uniqid("RcsbFvBoard_");
this.boardConfigData = props.boardConfigData;
this.elementId = props.elementId;
const node = document.getElementById(this.elementId);
if (!node)
throw new Error(`HTML element ${this.elementId} not found`);
this.node = node;
this.boardDataSate = new BoardDataState({
contextManager: this.contextManager,
boardId: this.boardId,
rowConfigData: props.rowConfigData
});
this.rcsbFvStateManager = new RcsbFvStateManager({
xScale: this.xScale,
selection: this.selection,
contextManager: this.contextManager,
boardId: this.boardId
});
if (this.boardConfigData != null) {
this.init().then(() => {
console.info(`PFV ${this.elementId} is ready. Configuration provided during object instantiation`);
});
}
}
/**
* Loads the configuration for each row of the board
* @param rowConfigData Array of configurations for each row in the board
*/
setBoardData(rowConfigData) {
this.boardDataSate.setBoardData(rowConfigData);
return this.updateBoardData();
}
/**
* Gets the configuration for each row of the board
*/
getBoardData() {
return this.boardDataSate.getBoardData();
}
/**
* Loads the configuration of the board
* @param config Configuration of the board
*/
setBoardConfig(config) {
return this.updateBoardConfig({ boardConfigData: config });
}
/**
* @return board configuration
*/
getBoardConfig() {
return this.boardConfigData;
}
then(f) {
if (this.rcsbFvPromise)
this.rcsbFvPromise.then(() => {
f();
});
else
throw "RcsbFv init method was not called";
return this;
}
catch(f) {
if (this.rcsbFvPromise)
this.rcsbFvPromise.catch(() => {
f();
});
else
throw "RcsbFv init method was not called";
return this;
}
/**Renders the board*/
init() {
this.rcsbFvPromise = new Promise((resolve, reject) => {
if (!this.mounted && this.boardConfigData != undefined) {
this.reactRoot = createRoot(this.node);
this.reactRoot.render(_jsx(RcsbFvBoard, { boardId: this.boardId, rowConfigData: this.boardDataSate.getBoardData(), boardConfigData: this.boardConfigWithTrackWidth(), contextManager: this.contextManager, xScale: this.xScale, selection: this.selection, resolve: resolve }));
this.mounted = true;
}
else {
reject("FATAL ERROR: RcsvFvBoard is mounted or board configuration was not loaded");
}
});
return this.rcsbFvPromise.then(() => this.addResizeObserver());
}
/**Unmount the board*/
unmount() {
var _a;
if (this.reactRoot != null) {
this.reactRoot.unmount();
}
this.boardDataSate.unsubscribe();
this.rcsbFvStateManager.unsubscribe();
(_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.unobserve(this.node);
}
/**Returns all track Ids in the same order that are visualised in the board*/
getTrackIds() {
return this.boardDataSate.getBoardData().map(r => r.trackId);
}
/**Adds new annotations for a particular board track
* @param trackId Id that identifies the track
* @param trackData Annotations to be added in the track
* */
addTrackData(trackId, trackData) {
this.boardDataSate.addTrackData({ trackId, trackData });
return this.updateBoardData();
}
/**Replaces annotations a particular board track
* @param trackId Id that identifies the track
* @param trackData New annotations to be displayed
* @param displayId optional Id that identifies track from a composite track to update
* */
updateTrackData(trackId, trackData, displayId) {
this.boardDataSate.updateTrackData({ trackId, trackData, displayId });
return this.updateBoardData();
}
/**Method used to update board global and all-tracks configuration
* @param newConfig New board configuration data
* */
updateBoardConfig(newConfig) {
if (newConfig.rowConfigData)
this.boardDataSate.setBoardData(newConfig.rowConfigData);
else
this.boardDataSate.refresh();
if (newConfig.boardConfigData)
this.boardConfigData = Object.assign(Object.assign({}, this.boardConfigData), newConfig.boardConfigData);
const configDataObj = {
rowConfigData: this.boardDataSate.getBoardData(),
boardConfigData: newConfig.boardConfigData ? this.boardConfigData : undefined
};
return new Promise((resolve, reject) => {
this.contextManager.next({
eventType: EventType.UPDATE_BOARD_CONFIG,
eventData: configDataObj,
eventResolve: resolve
});
});
}
/**Rerender the board track
* @param trackId Id that identifies the track
* */
resetTrack(trackId) {
this.boardDataSate.resetTrack(trackId);
return this.updateBoardData();
}
/**Adds a new track to the board
* @param trackConfig Track configuration data
* */
addTrack(trackConfig) {
this.boardDataSate.addTrack(trackConfig);
return this.updateBoardData();
}
/**Changes track visibility (true/false)
* @param obj Track visibility event data
**/
changeTrackVisibility(obj) {
this.boardDataSate.changeTrackVisibility(obj);
return this.updateBoardData();
}
/**Change board view range
* @param domain new xScale domain
**/
setDomain(domain) {
this.contextManager.next({
eventType: EventType.DOMAIN_VIEW,
eventData: { domain: domain }
});
}
/**Get board view range
**/
getDomain() {
return [this.xScale.domain()[0], this.xScale.domain()[1]];
}
/**Select board range
* @param selection region/elements
**/
setSelection(selection) {
this.contextManager.next({
eventType: EventType.SET_SELECTION,
eventData: selection
});
}
/**Get selected board ranges
* @param mode selection type
**/
getSelection(mode) {
return this.selection.getSelected(mode);
}
/**Add selection to board
* @param selection region/elements
**/
addSelection(selection) {
this.contextManager.next({
eventType: EventType.ADD_SELECTION,
eventData: selection
});
}
/**
* Clear Selection
**/
clearSelection(mode) {
this.contextManager.next({
eventType: EventType.SET_SELECTION,
eventData: {
elements: null,
mode: mode !== null && mode !== void 0 ? mode : 'select'
}
});
}
/**
* Move board track
* @param oldIndex original position
* @param newIndex new position
* **/
moveTrack(oldIndex, newIndex) {
this.boardDataSate.moveTrack({ oldIndex, newIndex });
return this.updateBoardData();
}
/**
* reset Selection and Scale
**/
reset() {
this.selection.reset();
this.xScale.reset();
}
updateBoardData() {
return new Promise((resolve, reject) => {
this.contextManager.next({
eventType: EventType.UPDATE_BOARD_CONFIG,
eventData: {
rowConfigData: this.boardDataSate.getBoardData()
},
eventResolve: resolve
});
});
}
boardConfigWithTrackWidth() {
var _a;
return Object.assign(Object.assign({}, this.boardConfigData), { trackWidth: (_a = this.boardConfigData.trackWidth) !== null && _a !== void 0 ? _a : (this.node.getBoundingClientRect().width - this.rowTitleWidth()) });
}
addResizeObserver() {
if (this.boardConfigData.trackWidth)
return;
this.resizeObserver = resizeBoard(this.node, (width) => __awaiter(this, void 0, void 0, function* () {
const trackWidth = width - this.rowTitleWidth();
if (trackWidth <= 0) {
console.debug(`Element width ${width} is too small. Row title width ${this.rowTitleWidth()}. Not rendering`);
return;
}
const selected = this.getSelection("select").map(s => ({
begin: s.rcsbFvTrackDataElement.begin,
end: s.rcsbFvTrackDataElement.end
}));
const domain = [this.xScale.domain()[0], this.xScale.domain()[1]];
const data = this.boardDataSate.getBoardData();
yield this.updateBoardConfig({
boardConfigData: {
trackWidth: trackWidth
},
rowConfigData: []
});
this.setDomain(domain);
this.setSelection({
mode: "select",
elements: selected
});
yield this.updateBoardConfig({
rowConfigData: data
});
}));
}
rowTitleWidth() {
var _a;
return ((_a = this.boardConfigData.rowTitleWidth) !== null && _a !== void 0 ? _a : RcsbFvDefaultConfigValues.rowTitleWidth) + 40;
}
}
function resizeBoard(node, callback) {
let width = node.getBoundingClientRect().width;
let task;
const resizeObserver = new ResizeObserver((entries, observer) => {
if (width == entries[0].contentRect.width)
return;
width = entries[0].contentRect.width;
if (task)
task.unsubscribe();
task = asyncScheduler.schedule(() => callback(width), 50);
});
resizeObserver.observe(node);
return resizeObserver;
}