import _ from "lodash"; import { IColorValueMap } from "../ui/react-pathway-mapper"; import GraphUtilities from "../utils/GraphUtilities"; import EditorActionsManager from "./EditorActionsManager"; let sharedb; let socket; let connection; export default class ShareDBManager { readonly NODEMAP_NAME = 'nodes'; readonly EDGEMAP_NAME = 'edges'; readonly LAYOUT_PROPS_NAME = 'layoutProperties'; readonly GLOBAL_OPTS_NAME = 'globalOptions'; //For storing genomic data information per gene readonly GENOMIC_DATA_MAP_NAME = 'genomicDataMap'; //For storing visibility information of genomic data according to the cancer type readonly VISIBLE_GENOMIC_DATA_MAP_NAME = 'visibleGenomicDataMapByType'; readonly GENOMIC_DATA_GROUP_NAME = 'genomicDataGroupList'; readonly GENOMIC_DATA_GROUP_COUNT = 'genomicDataGroupCount'; readonly GENOMIC_DATA_COLOR_SCHEME_NAME = 'genomicDataColorScheme'; doc: any; postFileLoad: any; editor: EditorActionsManager; constructor(postFileLoadCallback) { //Doc data maps names and keys this.postFileLoad = postFileLoadCallback; }; getDoc(){ return this.doc; } setEditor(editor: EditorActionsManager){ this.editor = editor; } //Applies any given sharDB opeation to shared document and notifies other clients applyShareDBOperation(op) { this.doc.submitOp(op, this.shareDBError); }; //Clears genomic data in shared document and notifies other clients clearShareDBGenomicData() { var ops = []; var genomicMap = this.doc.data[this.GENOMIC_DATA_MAP_NAME]; var visMap = this.doc.data[this.VISIBLE_GENOMIC_DATA_MAP_NAME]; var genomicDataGroupMap = this.doc.data[this.GENOMIC_DATA_GROUP_NAME]; var genomicDataGroupCount = this.doc.data[this.GENOMIC_DATA_GROUP_COUNT]; //Reset all genomic maps for (const key of Object.keys(genomicMap)) { ops.push({p: [this.GENOMIC_DATA_MAP_NAME, key], od: genomicMap[key]}); } for (const key of Object.keys(visMap)) { ops.push({p: [this.VISIBLE_GENOMIC_DATA_MAP_NAME, key], od: visMap[key]}); } for (const key of Object.keys(genomicDataGroupMap)) { ops.push({p: [this.GENOMIC_DATA_GROUP_NAME, key], od: genomicDataGroupMap[key]}); } //Reset genomic data group count to 0 ops.push({p: [this.GENOMIC_DATA_GROUP_COUNT], na: -genomicDataGroupCount}); this.doc.submitOp(ops, this.shareDBError); }; /* * Updates shared document object * @param mapName: map name of the object * @param objectKey: key of the object * @param object: new object * */ updateShareDBObject(mapName, objectKey, object) { this.doc.submitOp([{ p: [mapName, objectKey], od: this.doc.data[mapName][objectKey], oi: object }], this.shareDBError); }; /* * Inserts a new shared document object * @param mapName: map name of the object * @param objectKey: key of the object * @param object: new object * */ insertShareDBObject(mapName, objectKey, object) { this.doc.submitOp([{p: [mapName, objectKey], oi: object}], this.shareDBError); }; /* * Deletes a shared document object * @param mapName: map name of the object * @param objectKey: key of the object * */ deleteShareDBObject(mapName, objectKey) { this.doc.submitOp([{p: [mapName, objectKey], od: this.doc.data[mapName][objectKey]}], this.shareDBError); }; //Initializes layout properties of the shared document initializeShareDBLayoutProperties() { this.doc.submitOp([{ p: [this.LAYOUT_PROPS_NAME, 0], li: [this.editor.layoutProperties] }], this.shareDBError); }; //Initializes global options of the shared document initializeShareDBGlobalOptions() { this.doc.submitOp([{ p: [this.GLOBAL_OPTS_NAME, 0], li: [this.editor.getGlobalOptions()] }], this.shareDBError); }; /* * Updates layout properties of the shared document and notifies other clients * @param object: new layout properties object * */ updateShareDBLayoutProperties(object) { this.doc.submitOp([{ p: [this.LAYOUT_PROPS_NAME, 0], ld: this.doc.data[this.LAYOUT_PROPS_NAME][0], li: object }], this.shareDBError); }; /* * Updates global options of the shared document and notifies other clients * @param object: new global options object * */ updateShareDBGlobalOptions(object) { this.doc.submitOp([{ p: [this.GLOBAL_OPTS_NAME, 0], ld: this.doc.data[this.GLOBAL_OPTS_NAME][0], li: object }], this.shareDBError); }; updateShareDBGenomicDataOverlayColorScheme(object : IColorValueMap) { this.doc.submitOp([{ p: [this.GENOMIC_DATA_COLOR_SCHEME_NAME, 0], ld: this.doc.data[this.GENOMIC_DATA_COLOR_SCHEME_NAME][0], li: object }], this.shareDBError); }; //Increments shared data group count //Use this function to increment and keep the group count synchronized incrementShareDBGroupCount() { this.doc.submitOp([{p: [this.GENOMIC_DATA_GROUP_COUNT], na: 1}], this.shareDBError); }; //Checks whether given operation is a replace or add/delete operation isShareDBReplaceEvent(op) { return (op.hasOwnProperty("oi") && op.hasOwnProperty("od")) || (op.hasOwnProperty("li") && op.hasOwnProperty("ld")); }; /* * Gets the initial value of the shared document * without this function shared document values cannot be reached */ initializeSharedDBDoc() { this.doc.subscribe(); }; shareDBError(err) { if (err) { console.error(err); } }; initShareDB() { sharedb = require('sharedb/lib/client'); socket = new WebSocket('ws://' + window.location.host); connection = new sharedb.Connection(socket); var self = this; var id = this.getParam('id'); var loadFileCallback = function () { self.onFileLoaded(); }; //Creates new shared db document and initializes values var createNewDocument = () => { var new_id = self.getCustomObjId(); var data = { nodes: {}, edges: {}, layoutProperties: [this.editor.layoutProperties], globalOptions: [this.editor.getGlobalOptions()], genomicDataMap: {}, visibleGenomicDataMapByType: {}, genomicDataGroupList: {}, genomicDataGroupCount: 0, genomicDataColorScheme: [{ '-100' : "#0000ff", '0' : "#ffffff", '100' : "#ff0000" }] }; window.history.pushState(null, null, '?id=' + new_id); self.doc = connection.get('cy', new_id); self.doc.create(data, loadFileCallback); }; //Check if id exists in parameters //If exists open the shared document if (id) { // Check any document exists with given id this.doc = connection.get('cy', id); this.doc.fetch(function (err) { if (err) throw err; if (self.doc.type === null) { createNewDocument(); return; } self.doc.subscribe(loadFileCallback); }); } else { //Create new shared document createNewDocument(); } }; /* * After a file has been initialized and loaded, we can access the * document. We will wire up the data in shared document to the UI. * */ onFileLoaded() { this.initializeSharedDBDoc(); this.syncInitialSharedDocData(); this.initSharedDocEventHandlers(); this.postFileLoad(); }; /* * Synchronizes initial data in the shared document to * client's application. */ syncInitialSharedDocData() { var self = this; var nodeMap = self.doc.data[this.NODEMAP_NAME]; var edgeMap = self.doc.data[this.EDGEMAP_NAME]; var shareDBLayoutProperties = self.doc.data[this.LAYOUT_PROPS_NAME][0]; var globalOptions = self.doc.data[this.GLOBAL_OPTS_NAME][0]; var genomicDataMap = self.doc.data[this.GENOMIC_DATA_MAP_NAME]; var visDataMap = self.doc.data[this.VISIBLE_GENOMIC_DATA_MAP_NAME]; var groupedGenomicDataMap = self.doc.data[this.GENOMIC_DATA_GROUP_NAME]; var groupedGenomicDataCount = self.doc.data[this.GENOMIC_DATA_GROUP_COUNT]; let genomicDataColorScheme; if (self.doc.data[this.GENOMIC_DATA_COLOR_SCHEME_NAME]) { genomicDataColorScheme = self.doc.data[this.GENOMIC_DATA_COLOR_SCHEME_NAME][0]; } var invalidGenes = []; var highlightedGenes = []; var invalidHighlightedGenes = []; var hiddenGenes = []; for (const key of Object.keys(nodeMap)) { var tmpNode = nodeMap[key]; var tmpNodeId = tmpNode.id; if (tmpNode.isInvalidGene && tmpNode.isHighlighted) { invalidHighlightedGenes.push(tmpNodeId); } else if (tmpNode.isInvalidGene) { invalidGenes.push(tmpNodeId); } else if (tmpNode.isHighlighted) { highlightedGenes.push(tmpNodeId); } if (tmpNode.isHidden) { hiddenGenes.push(tmpNodeId); } } var highlightedEdges = []; for (const key of Object.keys(edgeMap)) { var tmpEdge = edgeMap[key]; var tmpEdgeId = tmpEdge.id; if (tmpEdge.isHighlighted) { highlightedEdges.push(tmpEdgeId); } } //TODO Workaround for legacy pathways // Workaround for backward compatibility of legacy pathways // Addition of pubmed id field on server if legacy collaborative // pathways does not have ! for (const key of Object.keys(edgeMap)) { const tmpEdge = edgeMap[key]; if (tmpEdge.pubmedIDs == undefined || tmpEdge.name == undefined || tmpEdge.bendPoint == undefined || tmpEdge.anchorPoints == undefined || tmpEdge.edgeCurveStyle == undefined) { const pubmedIDs = (tmpEdge.pubmedIDs == undefined) ? [] : tmpEdge.pubmedID; const edgeLabel = (tmpEdge.name == undefined) ? "" : tmpEdge.name; let anchorPoints = []; if (tmpEdge.bendPoint) { anchorPoints = tmpEdge.bendPoint; } else if (tmpEdge.anchorPoints) { anchorPoints = tmpEdge.anchorPoints; } const edgeCurveStyle = tmpEdge.edgeCurveStyle ? tmpEdge.edgeCurveStyle : "bezier"; const param = { type: tmpEdge.type, source: tmpEdge.source, id: self.getCustomObjId(), target: tmpEdge.target, pubmedID: pubmedIDs, name: edgeLabel, anchorPoints: anchorPoints, edgeCurveStyle: edgeCurveStyle }; const newEdge: any = self.edgeInitializer(param); const tmpEdgeID = tmpEdge.id; const newEdgeID = newEdge.id; const ops = [ {p: [self.EDGEMAP_NAME, tmpEdgeID], od: tmpEdge}, {p: [self.EDGEMAP_NAME, newEdgeID], oi: newEdge} ] self.applyShareDBOperation(ops); } } //Add real time nodes to local graph this.editor.addNewElementsLocally(nodeMap, edgeMap); //Adds different type of highlight to nodes and hides if their property is hidden this.editor.highlightElementsInitially(invalidHighlightedGenes, invalidGenes, highlightedGenes, highlightedEdges, hiddenGenes); //Update layout properties & global options!! if (shareDBLayoutProperties.name == "cose-bilkent") { shareDBLayoutProperties.name = "fcose"; this.updateLayoutProperties(shareDBLayoutProperties); } this.editor.updateLayoutPropertiesCallback({li: shareDBLayoutProperties}); this.editor.changeGlobalOptions({li: globalOptions}); //Sync already available genomic data ! if (!groupedGenomicDataMap) { self.insertShareDBObject(self.GENOMIC_DATA_MAP_NAME, '0', []); for (const key of Object.keys(visDataMap)) { var currentMap = _.clone(groupedGenomicDataMap['0']); // currentMap.push(visibilityMapKeys[key]); self.updateShareDBObject(self.GENOMIC_DATA_MAP_NAME, '0', currentMap); } } if (!groupedGenomicDataCount) { var count = self.doc.data[self.GENOMIC_DATA_GROUP_COUNT]; var op = [{ p: [self.GENOMIC_DATA_GROUP_COUNT], na: -count }]; self.applyShareDBOperation(op); groupedGenomicDataCount = self.doc.data[self.GENOMIC_DATA_GROUP_COUNT]; } if (!genomicDataColorScheme) { genomicDataColorScheme = this.editor.getGenomicDataOverlayColorScheme(); const op = [{ p: [self.GENOMIC_DATA_COLOR_SCHEME_NAME], oi: [genomicDataColorScheme] }]; this.applyShareDBOperation(op); } this.editor.updateGenomicDataColorSchemeHandler({li: genomicDataColorScheme}); for (const key_g of Object.keys(genomicDataMap)) { this.editor.genomicDataOverlayManager.genomicDataMap[key_g] = genomicDataMap[key_g]; } for (const key_g of Object.keys(groupedGenomicDataMap)) { this.editor.genomicDataOverlayManager.groupedGenomicDataMap[key_g] = groupedGenomicDataMap[key_g]; const data = groupedGenomicDataMap[key_g]; if(data.length !== 1){ console.log("Grouped genomic data expected to be of length 1 (from sync)"); } data.forEach((profileId: any) => {this.editor.addToProfiles(profileId)}); } for (const key_g of Object.keys(visDataMap)) { this.editor.genomicDataOverlayManager.visibleGenomicDataMapByType[key_g] = visDataMap[key_g]; this.editor.adjustVisibilityShareDB(key_g, visDataMap[key_g]); } //Does not seem necessary for not but just for sake of completeness this.editor.genomicDataOverlayManager.groupedGenomicDataCount = groupedGenomicDataCount; this.editor.genomicDataOverlayManager.showGenomicData((node: any) => { this.editor.resizeElements(node); }); this.editor.genomicDataOverlayManager.notifyObservers(); this.editor.cy.style().update(); this.editor.cy.fit(50); }; /* * Initialize event listeners for any operation coming from shareDB * */ initSharedDocEventHandlers () { var self = this; //Setup event handlers for maps var nodeAddRemoveHandler = function (op) { self.editor.shareDBNodeAddRemoveEventCallBack(op); }; var edgeAddRemoveHandler = function (op) { self.editor.shareDBEdgeAddRemoveEventCallBack(op); }; var genomicDataAddRemoveHandler = function (op) { self.editor.shareDBGenomicDataHandler(op); }; var genomicDataVisibilityChangeHandler = function (op) { self.editor.shareDBGenomicDataVisibilityHandler(op); self.checkShareDBGenomicData(); }; var genomicDataGroupChangeHandler = function (op) { self.editor.shareDBGenomicDataGroupChangeHandler(op); }; var updateElementHandler = function (op) { self.editor.updateElementCallback(op); }; var updateLayoutPropsHandler = function (op) { self.editor.updateLayoutPropertiesCallback(op); }; var updateGlobalOptionsHandler = function (op) { self.editor.changeGlobalOptions(op); }; var updateGenomicDataColorSchemeHandler = function (op) { self.editor.updateGenomicDataColorSchemeHandler(op); } //Event listeners for maps this.doc.on('op', function (op, source) { for (var i = 0; i < op.length; i++) { var handleOp = op[i]; var path = handleOp.p[0]; var isReplaceEvent = self.isShareDBReplaceEvent(handleOp); if (!isReplaceEvent) { if (path === self.NODEMAP_NAME) { nodeAddRemoveHandler(handleOp); } else if (path === self.EDGEMAP_NAME) { edgeAddRemoveHandler(handleOp); } else if (path === self.GENOMIC_DATA_MAP_NAME) { genomicDataAddRemoveHandler(handleOp); } else if (path === self.VISIBLE_GENOMIC_DATA_MAP_NAME) { genomicDataVisibilityChangeHandler(handleOp); } else if (path === self.GENOMIC_DATA_GROUP_NAME) { genomicDataGroupChangeHandler(handleOp); } } else { //Then it is update event if (path === self.NODEMAP_NAME) { updateElementHandler(handleOp); } else if (path === self.EDGEMAP_NAME) { updateElementHandler(handleOp); } else if (path === self.LAYOUT_PROPS_NAME) { updateLayoutPropsHandler(handleOp); } else if (path === self.GLOBAL_OPTS_NAME) { updateGlobalOptionsHandler(handleOp); } else if (path === self.GENOMIC_DATA_COLOR_SCHEME_NAME) { updateGenomicDataColorSchemeHandler(handleOp); } } } }); }; /* * Make sure that genomic cloud data is syncronized */ checkShareDBGenomicData () { var self = this; var genomicDataMap = self.doc.data[this.GENOMIC_DATA_MAP_NAME]; var visDataMap = self.doc.data[this.VISIBLE_GENOMIC_DATA_MAP_NAME]; var groupedGenomicDataMap = self.doc.data[this.GENOMIC_DATA_GROUP_NAME]; var groupedGenomicDataCount = self.doc.data[this.GENOMIC_DATA_GROUP_COUNT]; for (var key in genomicDataMap) { this.editor.genomicDataOverlayManager.genomicDataMap[key] = genomicDataMap[key]; } for (var key in visDataMap) { this.editor.genomicDataOverlayManager.visibleGenomicDataMapByType[key] = visDataMap[key]; } for (var key in groupedGenomicDataMap) { this.editor.genomicDataOverlayManager.groupedGenomicDataMap[key] = groupedGenomicDataMap[key]; } this.editor.genomicDataOverlayManager.groupedGenomicDataCount = groupedGenomicDataCount; this.editor.genomicDataOverlayManager.showGenomicData((node: any) => { this.editor.resizeElements(node); }); this.editor.genomicDataOverlayManager.notifyObservers(); }; /* * Gets the first empty index from the shared document * genomic data group count and increments counter by 1 * */ getEmptyGroupID() { var returnCount = this.doc.data[this.GENOMIC_DATA_GROUP_COUNT]; this.incrementShareDBGroupCount(); return returnCount; }; /* * Gets the first empty index from the shared document * Initializes & inserts a new genomic data * by group id or group name * */ groupGenomicData(cancerNames, inGroupId) { var genomicGroupMap = this.doc.data[this.GENOMIC_DATA_GROUP_NAME]; var genomicVisMap = this.doc.data[this.VISIBLE_GENOMIC_DATA_MAP_NAME]; var groupID = "" + inGroupId; var currentGroup = []; if (genomicGroupMap.hasOwnProperty(groupID)) currentGroup = _.clone(genomicGroupMap[groupID]); for (var i in cancerNames) { if (!genomicVisMap.hasOwnProperty(cancerNames[i])) currentGroup.push(cancerNames[i]); } // If group id already exists change existing object if (genomicGroupMap.hasOwnProperty(groupID)) { this.updateShareDBObject(this.GENOMIC_DATA_GROUP_NAME, groupID, currentGroup); } else { //Insert new group this.insertShareDBObject(this.GENOMIC_DATA_GROUP_NAME, groupID, currentGroup); } }; //Clears genomic data on shared document clearGenomicData() { this.clearShareDBGenomicData(); }; addGenomicData(geneData) { var genomicMap = this.doc.data[this.GENOMIC_DATA_MAP_NAME]; //Iterate over all genmoic data which is mapped by geneSymbol to list of alteration values //that are also mapped by cancer name and associated value var ops = []; for (var geneSymbol in geneData) { var genomicMapEntry = {}; if (genomicMap.hasOwnProperty(geneSymbol)) genomicMapEntry = _.clone(genomicMap[geneSymbol]); for (var cancerType in geneData[geneSymbol]) { if (!(cancerType in genomicMapEntry)) genomicMapEntry[cancerType] = parseInt(geneData[geneSymbol][cancerType]).toFixed(2); } ops.push({ p: [this.GENOMIC_DATA_MAP_NAME, geneSymbol], oi: genomicMapEntry }); } this.applyShareDBOperation(ops); }; addGenomicVisibilityData(visMap) { var ops = []; for (var cancerStudy in visMap) { ops.push({ p: [this.VISIBLE_GENOMIC_DATA_MAP_NAME, cancerStudy], oi: visMap[cancerStudy] }); } this.applyShareDBOperation(ops); }; changeVisibility(nodesToHide, isHidden) { var self = this; var nodeMap = self.doc.data[this.NODEMAP_NAME]; nodesToHide.forEach(function (ele) { var nodeID = ele.id(); if (nodeMap.hasOwnProperty(nodeID)) { var realTimeNode = nodeMap[nodeID]; realTimeNode.isHidden = isHidden; self.updateShareDBObject(self.NODEMAP_NAME, nodeID, realTimeNode); } }); }; changeHighlight(elementsToHighlight, isHighlighted) { var self = this; var nodeMap = self.doc.data[this.NODEMAP_NAME]; var edgeMap = self.doc.data[this.EDGEMAP_NAME]; elementsToHighlight.forEach(function (ele) { var elementID = ele.id(); if (nodeMap.hasOwnProperty(elementID)) { var realTimeNode = nodeMap[elementID]; realTimeNode.isHighlighted = isHighlighted; self.updateShareDBObject(self.NODEMAP_NAME, elementID, realTimeNode); } if (edgeMap.hasOwnProperty(elementID)) { var realTimeEdge = edgeMap[elementID]; realTimeEdge.isHighlighted = isHighlighted; self.updateShareDBObject(self.EDGEMAP_NAME, elementID, realTimeEdge); } }); }; addNewNode(nodeData, posData) { var realTimeGeneratedID = this.getCustomObjId(); var params = { name: nodeData.name, type: nodeData.type, id: realTimeGeneratedID, parent: nodeData.parent, w: nodeData.w, h: nodeData.h, minWidth: nodeData.minWidth, minWidthBiasLeft: nodeData.minWidthBiasLeft, minWidthBiasRight: nodeData.minWidthBiasRight, minHeight: nodeData.minHeight, minHeightBiasTop: nodeData.minHeightBiasTop, minHeightBiasBottom: nodeData.minHeightBiasBottom }; //Ensures new node is compatible with the other nodes var newNode = this.nodeInitializer(params); if (posData) { newNode.x = posData.x; newNode.y = posData.y; } this.insertShareDBObject(this.NODEMAP_NAME, realTimeGeneratedID, newNode); }; addNewEdge(edgeData) { var realTimeGeneratedID = this.getCustomObjId(); var params = { type: edgeData.type, id: realTimeGeneratedID, source: edgeData.source, target: edgeData.target, pubmedIDs: edgeData.pubmedIDs, name: edgeData.name, anchorPoints: edgeData.anchorPoints }; //Ensures new edge is compatible with the other edge var newEdge = this.edgeInitializer(params); this.insertShareDBObject(this.EDGEMAP_NAME, realTimeGeneratedID, newEdge); }; reconnectEdge = function(sourceID, targetID, edgeData) { var edgeMap = this.doc.data[this.EDGEMAP_NAME]; var edge = edgeMap[edgeData.id]; var edgeID = edge.id; edge.source = sourceID; edge.target = targetID; if(edgeMap.hasOwnProperty(edgeID)) { this.updateShareDBObject(this.EDGEMAP_NAME, edgeID, edge); } else { throw new Error('Element does not exist in Real Time'); } }; removeElement (elementID) { var edgeMap = this.doc.data[this.EDGEMAP_NAME]; var nodeMap = this.doc.data[this.NODEMAP_NAME]; if (nodeMap.hasOwnProperty(elementID)) { this.deleteShareDBObject(this.NODEMAP_NAME, elementID); } else if (edgeMap.hasOwnProperty(elementID)) { this.deleteShareDBObject(this.EDGEMAP_NAME, elementID); } else { throw new Error('Element does not exist in Real Time'); } }; moveElement (ele) { var nodeMap = this.doc.data[this.NODEMAP_NAME]; var elementID = ele.id(); var newPos = ele.position(); if (nodeMap.hasOwnProperty(elementID)) { var tmpNode = nodeMap[elementID]; tmpNode.x = newPos.x; tmpNode.y = newPos.y; this.updateShareDBObject(this.NODEMAP_NAME, elementID, tmpNode); } else { throw new Error('Element does not exist in nodes !!! '); } }; //This function is used for movements of all selected elements wrt alignment selected changeElementsPositionByAlignment (coll) { var self = this; var nodeMap = self.doc.data[this.NODEMAP_NAME]; coll.forEach(function (ele) { var elementID = ele.node.id(); if (nodeMap.hasOwnProperty(elementID)) { var tmpNode = nodeMap[elementID]; tmpNode.x = ele.nextPosition.x; tmpNode.y = ele.nextPosition.y; self.updateShareDBObject(self.NODEMAP_NAME, elementID, tmpNode); } else { throw new Error('Element does not exist in nodes !!! '); } }); }; resizeElement (ele, previousWidth, previousHeight) { var nodeMap = this.doc.data[this.NODEMAP_NAME]; var elementID = ele.id(); var newWidth = ele.width(); var newHeight = ele.height(); var currentX = ele.position('x'); var currentY = ele.position('y'); if (nodeMap.hasOwnProperty(elementID)) { var tmpNode = nodeMap[elementID]; tmpNode.x = currentX + newWidth - previousWidth; tmpNode.y = currentY + newHeight - previousHeight; tmpNode.w = newWidth; tmpNode.h = newHeight; this.updateShareDBObject(this.NODEMAP_NAME, elementID, tmpNode); } else { throw new Error('Element does not exist in nodes !!! '); } }; setSizeOfElement (ele, newWidth, newHeight) { var nodeMap = this.doc.data[this.NODEMAP_NAME]; var elementID = ele.id(); if (nodeMap.hasOwnProperty(elementID)) { var tmpNode = nodeMap[elementID]; tmpNode.w = newWidth; tmpNode.h = newHeight; this.updateShareDBObject(this.NODEMAP_NAME, elementID, tmpNode); } else { throw new Error('Element does not exist in nodes !!! '); } }; resizeCompound (ele, minWidth, minWidthBiasLeft, minWidthBiasRight, minHeight, minHeightBiasTop, minHeightBiasBottom) { var nodeMap = this.doc.data[this.NODEMAP_NAME]; var elementID = ele.id(); var currentX = ele.position('x'); var currentY = ele.position('y'); if (nodeMap.hasOwnProperty(elementID)) { var tmpNode = nodeMap[elementID]; tmpNode.x = currentX; tmpNode.y = currentY; tmpNode.minWidth = minWidth; tmpNode.minWidthBiasLeft = minWidthBiasLeft; tmpNode.minWidthBiasRight = minWidthBiasRight; tmpNode.minHeight = minHeight; tmpNode.minHeightBiasTop = minHeightBiasTop; tmpNode.minHeightBiasBottom = minHeightBiasBottom; this.updateShareDBObject(this.NODEMAP_NAME, elementID, tmpNode); } else { throw new Error('Element does not exist in nodes !!! '); } }; changeNodePositionsShareDB (nodes) { var self = this; var nodeMap = self.doc.data[self.NODEMAP_NAME]; nodes.forEach(function (ele) { var nodeID = ele.id(); if (nodeMap.hasOwnProperty(nodeID)) { var realTimeNode = nodeMap[nodeID]; realTimeNode.x = ele.position('x'); realTimeNode.y = ele.position('y'); self.updateShareDBObject(self .NODEMAP_NAME, nodeID, realTimeNode); } else { throw new Error('Element does not exist in nodes !!! '); } }); }; changeHighlightInvalidGenes (nodeIDs, isInvalid) { var nodeMap = this.doc.data[this.NODEMAP_NAME]; //TODO check compound operation inside or outside of for ? for (var i in nodeIDs) { var nodeID = nodeIDs[i]; if (nodeMap.hasOwnProperty(nodeID)) { var collaborativeNode = nodeMap[nodeID]; collaborativeNode.isInvalidGene = isInvalid; this.updateShareDBObject(this.NODEMAP_NAME, nodeID, collaborativeNode); } } }; addPubmedIDs (edgeID, pubmedIDs) { var edgeMap = this.doc.data[this.EDGEMAP_NAME]; if (edgeMap.hasOwnProperty(edgeID)) { var tmpEdge = edgeMap[edgeID]; var nonDuplicateArray = []; for (var i = 0; i < pubmedIDs.length; i++) { if (tmpEdge.pubmedIDs.indexOf(pubmedIDs[i]) < 0) { nonDuplicateArray.push(pubmedIDs[i]); } } tmpEdge.pubmedIDs = tmpEdge.pubmedIDs.concat(nonDuplicateArray); this.updateShareDBObject(this.EDGEMAP_NAME, edgeID, tmpEdge); } else { throw new Error('Edge does not exist in real time !!! '); } }; removePubmedID(edgeID, pubmedIDs) { var edgeMap = this.doc.data[this.EDGEMAP_NAME]; if (edgeMap.hasOwnProperty(edgeID)) { var tmpEdge = edgeMap[edgeID]; var removedIndices = []; for (var i = 0; i < pubmedIDs.length; i++) { var tmpID = pubmedIDs[i]; var index = tmpEdge.pubmedIDs.indexOf(tmpID); if (index >= 0) { removedIndices.push(index); } } for (var i = 0; i < removedIndices.length; i++) { tmpEdge.pubmedIDs.remove(removedIndices[i]); } this.updateShareDBObject(this.EDGEMAP_NAME, edgeID, tmpEdge); } else { throw new Error('Edge does not exist in real time !!! '); } }; updateEdgeAnchorPoints(edgeID, anchorPointsArray, edgeCurveStyle) { var edgeMap = this.doc.data[this.EDGEMAP_NAME]; if (edgeMap.hasOwnProperty(edgeID)) { var tmpEdge = edgeMap[edgeID]; tmpEdge.anchorPoints = anchorPointsArray; tmpEdge.edgeCurveStyle = edgeCurveStyle; this.updateShareDBObject(this.EDGEMAP_NAME, edgeID, tmpEdge); } else { throw new Error('Edge does not exist in real time !!! '); } }; changeName(ele, newName) { var nodeMap = this.doc.data[this.NODEMAP_NAME]; var edgeMap = this.doc.data[this.EDGEMAP_NAME]; var elementID = ele.id(); if (ele.isNode()) { if (nodeMap.hasOwnProperty(elementID)) { var tmpNode = nodeMap[elementID]; tmpNode.name = newName; this.updateShareDBObject(this.NODEMAP_NAME, elementID, tmpNode); } else { throw new Error('Element does not exist in nodes !!! '); } } else { if (edgeMap.hasOwnProperty(elementID)) { var tmpEdge = edgeMap[elementID]; tmpEdge.name = newName; this.updateShareDBObject(this.EDGEMAP_NAME, elementID, tmpEdge); } else { throw new Error('Element does not exist in edges !!! '); } } }; changeParent(rootNode, newParentId, connectedEdges) { var self = this; var nodeLookupTable = {}; function traverseFromRoot(rootNode, parId) { /* remove outermost node, create new real time node with given parentId, pass id of this real time node to children, repeat in a recursive manner after that restore the edges that dissapear by removed nodes during change parent */ var refNode = rootNode.nodeRef; var children = rootNode.children; var newParentId = parId; if (refNode) { var refNodeId = refNode.id(); var nodeData = refNode.data(); var posData = refNode.position(); var currentWidth = refNode.data('w'); var currentHeight = refNode.data('h'); var newNodeData = { name: nodeData.name, type: nodeData.type, x: posData.x, y: posData.y, w: currentWidth, h: currentHeight, parent: -1 }; if (parId) { newNodeData.parent = parId; } self.removeElement(refNodeId); var newNode = self.nodeInitializer(newNodeData); var newNodeId = newNode.id; self.insertShareDBObject(self.NODEMAP_NAME, newNodeId, newNode); newParentId = newNodeId; nodeLookupTable[refNodeId] = newNodeId; } for (const childNode of children) { traverseFromRoot(childNode, newParentId); } } //Begin traversing from given root node traverseFromRoot(rootNode, newParentId); //Restore edges that dissapear by the change parent operation //TODO compound operations ? connectedEdges.forEach(function (edge, index) { var edgeData = edge.data(); self.removeElement(edge.id()); var newSource = nodeLookupTable[edgeData.source]; var newTarget = nodeLookupTable[edgeData.target]; if (newSource) { edgeData.source = newSource; } if (newTarget) { edgeData.target = newTarget; } self.addNewEdge(edgeData); }); }; removeAllElements() { var nodeMap = this.doc.data[this.NODEMAP_NAME]; var edgeMap = this.doc.data[this.EDGEMAP_NAME]; var ops = []; //Remove all real time nodes for (const key of Object.keys(nodeMap)) { ops.push({ p: [this.NODEMAP_NAME, key], od: nodeMap[key] }); } //Remove all real time edges for (const key of Object.keys(edgeMap)) { ops.push({ p: [this.EDGEMAP_NAME, key], od: edgeMap[key] }); } this.applyShareDBOperation(ops); }; loadGraph(nodes, edges) { var self = this; this.removeAllElements(); //Function that traverses graph tree recursively. var oldIdNewIdMap = {}; function traverseTree(node, newParentId) { node.data.x = node.position.x; node.data.y = node.position.y; //Update parent ! if (newParentId) { var parent = node.data.parent; if (parent) { node.data.parent = newParentId; } } //Create new real time node var newNodeId = self.getCustomObjId(); var params = node.data; oldIdNewIdMap[params.id] = newNodeId; var newNode = self.nodeInitializer(params); newNode.id = newNodeId; self.insertShareDBObject(self.NODEMAP_NAME, newNodeId, newNode); //If node has children recursively traverse sub graphs and update parent field of child nodes if (node.children.length > 0) { for (const tmpNode of node.children) { traverseTree(tmpNode, newNodeId); } } } //Create graph hierarchy from given list of flat nodes var tree = GraphUtilities.createGraphHierarchyRealTime(nodes); //Traverse from root nodes of tree for (const rootLevelNode of tree) { traverseTree(rootLevelNode, undefined); } /* Create real time edges, update the source and target fields, since new ids will be generated for the nodes in real time */ for (const edge of edges) { edge.data.source = oldIdNewIdMap[edge.data.source]; edge.data.target = oldIdNewIdMap[edge.data.target]; var edgeCurveStyle = "bezier"; if (edge.data.bendPointPositions && edge.data.bendPointPositions.length > 0) { edge.data['anchorPoints'] = edge.data['bendPointPositions']; delete edge.data['bendPointPositions']; edgeCurveStyle = "segments"; } else if (edge.data.controlPointPositions && edge.data.controlPointPositions.length > 0) { edge.data['anchorPoints'] = edge.data['controlPointPositions']; delete edge.data['controlPointPositions']; edgeCurveStyle = "unbundled-bezier"; } var params = edge.data; params.edgeCurveStyle = edgeCurveStyle; var newEdgeID = self.getCustomObjId(); params.id = newEdgeID; var newEdge = self.edgeInitializer(params); self.insertShareDBObject(self.EDGEMAP_NAME, newEdgeID, newEdge); } }; mergeGraph (nodes, edges) { var self = this; var nodeMap = self.doc.data[this.NODEMAP_NAME]; var realTimeNodeMap = nodeMap; var realTimeNodeLookupTable = {}; var realTimeNodeNameLookupTable = {}; var oldIdNewIdMap = {}; //Create lookup table for real time nodes //items are stored in an array in the resulting array of nodeMap.items() // [0] - id, [1] - object for (var i in realTimeNodeMap) { var nodeMapItem = realTimeNodeMap[i]; realTimeNodeLookupTable[nodeMapItem.id] = nodeMapItem; realTimeNodeNameLookupTable[nodeMapItem.name] = nodeMapItem; } //Recursive traverse definition function traverseTree(node, newParentId) { //Search by name ! //We have not found a node that exist in the graph, add normally if (!(node.data.name in realTimeNodeNameLookupTable)) { node.data.x = node.position.x; node.data.y = node.position.y; //Update parent ! if (newParentId) { var parent = node.data.parent; if (parent) { node.data.parent = newParentId; } } //Create new real time node var newNodeId = self.getCustomObjId(); oldIdNewIdMap[node.data.id] = newNodeId; var newNode = self.nodeInitializer(node.data); newNode.id = newNodeId; self.insertShareDBObject(self.NODEMAP_NAME, newNodeId, newNode); //If node has children recursively traverse sub graphs and update parent field of child nodes if (node.children.length > 0) { for (const tmpNode of node.children) { traverseTree(tmpNode, newNodeId); } } } // At this point there exists another node in the graph with the same name as 'node' // we need to update parent field of children of this node if any else { var sameNameNode = realTimeNodeNameLookupTable[node.data.name]; var sameNodeId = sameNameNode.id; oldIdNewIdMap[node.data.id] = sameNodeId; //If node has children recursively traverse sub graphs and update parent field of child nodes if (node.children.length > 0) { for (const tmpNode of node.children) { traverseTree(tmpNode, sameNodeId); } } } } //Traverse from root nodes of tree var tree = GraphUtilities.createGraphHierarchyRealTime(nodes); for (const rootLevelNode of tree) { traverseTree(rootLevelNode, rootLevelNode.data.id); } /* Create real time edges, update the source and target fields, since new ids will be generated for the nodes in real time */ var ops = []; for (const edge of edges) { edge.data.source = oldIdNewIdMap[edge.data.source]; edge.data.target = oldIdNewIdMap[edge.data.target]; var newEdge = self.edgeInitializer(edge.data); var newEdgeID = this.getCustomObjId(); newEdge.id = newEdgeID; ops.push({ p: [self.EDGEMAP_NAME, newEdgeID], oi: newEdge }); } self.applyShareDBOperation(ops); }; updateLayoutProperties (newLayoutProperties) { this.updateShareDBLayoutProperties(newLayoutProperties); }; updateGlobalOptions (newOptions) { this.updateShareDBGlobalOptions(newOptions); }; updateGenomicDataOverlayColorScheme(newColorScheme: IColorValueMap) { this.updateShareDBGenomicDataOverlayColorScheme(newColorScheme); } /* * Creates graph hierarchy from given flat list of nodes list, nodes list is assumed to have parent-child * relationship by a field 'parent' which represents to the id of the parent node This function is specific * for the needs of TCGA Pathway Curation Tool 04/07/2016 * * @param nodes {array}: flat list of nodes of a graph * @return {array}: Tree representation in array, entries are root level nodes. node.children gives children nodes * of each node in the returned array. * a node in corresponding level. * * */ createGraphHierarchy (nodes) { //Some arrays and maps for creating graph hierarchy var tree = []; var mappedArr = {}; // First map the nodes of the array to an object -> create a hash table. for (var i = 0, len = nodes.length; i < len; i++) { var arrElem = nodes[i]; mappedArr[arrElem.data.id] = arrElem; mappedArr[arrElem.data.id].children = []; } for (const id of Object.keys(mappedArr)) { var mappedElem = mappedArr[id]; // If the element is not at the root level, add it to its parent array of children. if (mappedElem.data.parent) { mappedArr[mappedElem.data.parent].children.push(mappedElem); } // If the element is at the root level, add it to first level elements array. else { tree.push(mappedElem); } } return tree; }; //Makes sure that edge is compatible with edges in shared document edgeInitializer (params) { var edge: any = {}; edge.id = params.id || this.getCustomObjId(); edge.type = params.type || "undefined"; edge.source = params.source || "undefined"; edge.target = params.target || "undefined"; edge.name = params.name || ""; edge.isHighlighted = params.isHighlighted || false; edge.anchorPoints = params.anchorPoints || []; edge.edgeCurveStyle = params.edgeCurveStyle || "bezier" if (params.pubmedIDs) { if (edge.pubmedIDs == undefined) { edge.pubmedIDs = []; } if(params.pubmedIDs.length > 0) edge.pubmedIDs.push(params.pubmedIDs); } else { edge.pubmedIDs = []; } if (params.anchorPoints) { if (edge.anchorPoints == undefined) { edge.anchorPoints = []; } // legacy workaround if (Array.isArray(params.anchorPoints[0])) { edge.anchorPoints = params.anchorPoints[0]; } else { edge.anchorPoints = params.anchorPoints; } } else { edge.anchorPoints = []; } return edge; }; //Makes sure that node is compatible with nodes in shared document nodeInitializer(params) { var node: any = {}; node.id = params.id || this.getCustomObjId(); node.name = params.name || "undefined"; node.type = params.type || "undefined"; node.parent = params.parent || "undefined"; node.x = params.x || "undefined"; node.y = params.y || "undefined"; node.w = params.w || "undefined"; node.h = params.h || "undefined"; node.minWidth = params.minWidth || 0; node.minWidthBiasLeft = params.minWidth || 0; node.minWidthBiasRight = params.minWidth || 0; node.minHeight = params.minWidth || 0; node.minHeightBiasTop = params.minWidth || 0; node.minHeightBiasBottom = params.minWidth || 0; node.isHidden = params.isHidden || false; node.isInvalidGene = params.isInvalidGene || false; node.isHighlighted = params.isHighlighted || false; return node; }; //Makes sure that layout properties is compatible with layout properties in shared document layoutPropertiesInitializer (params) { var layoutProperties: any = {}; layoutProperties.name = params.name || 'undefined'; layoutProperties.nodeRepulsion = params.nodeRepulsion || 'undefined'; // this.nodeOverlap = params.nodeOverlap || 'undefined'; layoutProperties.idealEdgeLength = params.idealEdgeLength || 'undefined'; layoutProperties.edgeElasticity = params.edgeElasticity || 'undefined'; layoutProperties.nestingFactor = params.nestingFactor || 'undefined'; layoutProperties.gravity = params.gravity || 'undefined'; layoutProperties.numIter = params.numIter || 'undefined'; layoutProperties.tile = params.tile || 'undefined'; layoutProperties.animate = params.animate || 'undefined'; layoutProperties.randomize = params.randomize || false; layoutProperties.gravityRangeCompound = params.gravityRangeCompound || 'undefined'; layoutProperties.gravityCompound = params.gravityCompound || 'undefined'; layoutProperties.gravityRange = params.gravityRange || 'undefined'; layoutProperties.tilingPaddingVertical = params.tilingPaddingVertical || 'undefined'; layoutProperties.tilingPaddingHorizontal = params.tilingPaddingHorizontal || 'undefined'; layoutProperties.initialEnergyOnIncremental = params.initialEnergyOnIncremental || 'undefined'; return layoutProperties; }; //Makes sure that global options is compatible with global options in shared document globalOptionsInitializer (params) { var globalOptions: any = {}; globalOptions.zoomLevel = params.zoomLevel || 'undefined'; globalOptions.panLevel = params.panLevel || 'undefined'; return globalOptions; }; //Create unique ID for elements getCustomObjId () { // see http://stackoverflow.com/a/8809472 // we need to take care of our own IDs because the ones automatically generated by cytoscape (also UUID) // don't comply with xsd:SID type that must not begin with a number // Public Domain/MIT var d = Date.now(); if (typeof performance !== 'undefined' && typeof performance.now === 'function') { d += performance.now(); //use high-precision timer if available } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); }; getParam (urlParam) { const regExp = new RegExp(urlParam + '=(.*?)($|&)', 'g'); let match: any = window.location.search.match(regExp); if (match && match.length) { match = match[0]; match = match.replace(urlParam + '=', '').replace('&', ''); } else { match = null; } return match; }; }