import autobind from "autobind-decorator"; import cytoscape from 'cytoscape'; import $ from "jquery"; import { observer } from "mobx-react"; import React from 'react'; import pathways from "../data/pathways.json"; // @ts-ignore import compartmentImg from "../images/nodes/compartment.svg"; // @ts-ignore import complexImg from "../images/nodes/complex.svg"; // @ts-ignore import familyImg from "../images/nodes/family.svg"; // @ts-ignore import geneImg from "../images/nodes/gene.svg"; // @ts-ignore import processImg from "../images/nodes/process.svg"; // @ts-ignore import resizeCue from '../images/resizeCue.svg'; import ContextMenuManager from "../managers/ContextMenuManager"; import EditorActionsManager from "../managers/EditorActionsManager"; import QtipManager from "../managers/QtipManager"; import ShareDBManager from "../managers/ShareDBManager"; import ViewOperationsManager from "../managers/ViewOperationsManager"; import CBioPortalAccessor from "../utils/CBioPortalAccessor"; import DragDropNodeAddPlugin from "../utils/DragDropNodeAddPlugin"; import SaveLoadUtility from "../utils/SaveLoadUtility"; import { EModalType, IColorValueMap, IProfileMetaData } from './react-pathway-mapper'; // @ts-ignore window.$ = $; const edgeHandles = require('cytoscape-edgehandles'); const edgeEditing = require('cytoscape-edge-editing'); const fcose = require('cytoscape-fcose'); const nodeEditing = require('cytoscape-node-editing'); const undoRedo = require('cytoscape-undo-redo'); const panzoom = require('cytoscape-panzoom'); const styleSheet = require('../utils/GraphStyleSheet.tsx'); const panzoomOpts = require('../utils/PanzoomOptions.tsx'); const navigator = require('cytoscape-navigator'); const contextMenus = require('cytoscape-context-menus'); const konva = require('konva'); const viewUtilities = require('cytoscape-view-utilities'); const gridGuide = require('cytoscape-grid-guide'); const popper = require('cytoscape-popper'); const layoutUtilities = require('cytoscape-layout-utilities'); type PathwayMapperType = { isCollaborative: boolean; isCbioPortal: boolean; editorHandler: Function; selectedPathway: string; setActiveEdge: Function; profiles: IProfileMetaData[]; pathwayHandler: (pathwayName: string) => void; handleOpen: (modalId: EModalType) => void; onPathwayChangeCompleted: () => void; genomicDataOverlayColorScheme: IColorValueMap; colorSchemeChangeCallback: (IColorValueMap) => void; }; @observer export default class CytoscapeArea extends React.Component{ private cy:any; private cyDiv: HTMLDivElement | undefined; private editor: EditorActionsManager; private edgeAddingMode: any; private viewOperationsManager: ViewOperationsManager; private qtipManager: QtipManager; private genomicDataExplorerView: any; private pathwayDetailsView: any; private viewUtilities: any; private isCollaborative: boolean; private isCbioPortal: boolean; private shareDBManager: ShareDBManager; private cxtMenuManager: ContextMenuManager | undefined; private dragDropNodeAddManager: DragDropNodeAddPlugin; private undoRedoManager: any; private portalAccessor: CBioPortalAccessor; private isMountedFirst = true; private eh: any; private lastSelectedEdge: any; constructor (props: PathwayMapperType) { super(props); this.isCollaborative = props.isCollaborative; this.edgeAddingMode = 0; this.isCbioPortal = props.isCbioPortal; } componentWillUpdate(nextProps: PathwayMapperType) { if (this.props.selectedPathway !== nextProps.selectedPathway) { this.getPathway(nextProps.selectedPathway); } } componentDidUpdate(prevProps: PathwayMapperType) { if (this.isCbioPortal && prevProps.selectedPathway !== this.props.selectedPathway) { this.props.onPathwayChangeCompleted(); } } // This method only opens pathways that are available in pathway.json. Namely, imported or merged pathways are not opened via this method. // Yet, they individually call parsing method. getPathway(selectedPathway: string){ if(!selectedPathway || selectedPathway === '') return; const data = pathways[selectedPathway]; // It might be non-existent due to pathway being created using collaborative mode, or pathway loaded elsewhere (import vs.) if(!data){ return; } // TODO Problematic const data = pathways["../samples/BLCA-2014-RTK-RAS-PI(3)K-pathway.txt"]; const parsedGraph = SaveLoadUtility.parseGraph(data, true); const allEles = parsedGraph; this.editor.loadFile(allEles.nodes, allEles.edges); } render(){ return (
{!this.isCbioPortal &&
}
); } componentDidMount(): void { if(this.isMountedFirst){ this.init(); this.isMountedFirst = false; } this.getPathway(this.props.selectedPathway); } @autobind cyDivHandler(div:HTMLDivElement){ this.cyDiv = div; } init(){ // Create portal accessor this.portalAccessor = new CBioPortalAccessor(); // Initializes cytoscape this.initCyJS(); // Initialize cytoscape based handlers here this.initCyHandlers(); this.initKeyboardHandlers(); this.initUndoRedoFunctionality(); this.initCBioPortalFunctionalities(); this.placePanzoomAndOverlay(); //this.appManager = this; } placePanzoomAndOverlay(){ //TODO place navigator !!! var offset = 5; // @ts-ignore var topCy = $(this.cyDiv).offset().top; //var bottomCy = $('.cyContainer').offset().bottom; // @ts-ignore var leftCy = $(this.cyDiv).offset().left; //var rightCy = $('.cyContainer').offset().right; // @ts-ignore var heightCy = $(this.cyDiv).outerHeight(); // @ts-ignore var widthCy = $(this.cyDiv).outerWidth(); var heightNavigator = !this.isCbioPortal ? $('.cytoscape-navigator-wrapper').outerHeight() : 0; var widthNavigator = !this.isCbioPortal ? $('.cytoscape-navigator-wrapper').outerWidth() : 0; if(!this.isCbioPortal) { $('.cytoscape-navigator-wrapper').css('top', heightCy + topCy - heightNavigator - offset + 16); $('.cytoscape-navigator-wrapper').css('left', widthCy + leftCy - widthNavigator - offset + 24 - 0.5 + 0.35); $('.cytoscape-navigator-wrapper').css('z-index', 1039); } //Relative is used so that its position depends on the below properties $('.cy-panzoom').css('position', 'relative'); $('.cy-panzoom').css('top', 2); $('.cy-panzoom').css('left', widthCy - 51); $('.cy-panzoom').css('z-index', 1039); //Makes the width of panzoom container to 0 $('.cy-panzoom').css('width', 200); } getPathwayData() { return this.pathwayDetailsView.getPathwayData(); } initCyJS() { try { panzoom(cytoscape, $); // register extension } catch(err){ console.log(err); } try { if( !this.isCbioPortal) navigator(cytoscape); // register extension } catch(err){ console.log(err); } try { gridGuide(cytoscape, $); // register extension } catch(err){ console.log(err); } try { undoRedo(cytoscape); // register extension } catch(err){ console.log(err); } try { contextMenus(cytoscape, $); // register extension } catch(err){ console.log(err); } try { nodeEditing(cytoscape, $, konva); // register extension } catch(err){ console.log(err); } try { edgeEditing(cytoscape, $, konva); // register extension } catch(err){ console.log(err); } try { viewUtilities(cytoscape, $); // register extension } catch(err){ console.log(err); } try { edgeHandles(cytoscape, $); } catch(err){ console.log(err); } try { popper(cytoscape); } catch (err) { console.log(err); } try { fcose(cytoscape); } catch (err) { console.log(err); } try { layoutUtilities(cytoscape); } catch (err) { console.log(err); } this.cy = cytoscape({ container: this.cyDiv, boxSelectionEnabled: true, autounselectify: false, wheelSensitivity: 0.1, style: styleSheet, textureOnViewport: false, motionBlur: true, layout: {name: 'preset'} }); this.edgeAddingMode = 0; // var allEles = SaveLoadUtilities.parseGraph(sampleGraph); this.undoRedoManager = this.cy.undoRedo(); // Create Manager Classes this.shareDBManager = new ShareDBManager(() => { const dbDoc = this.shareDBManager.getDoc(); }); this.editor = new EditorActionsManager(this.isCollaborative, this.shareDBManager, this.cy, this.isCbioPortal, this.undoRedoManager, this.portalAccessor, this.props.profiles, this.props.genomicDataOverlayColorScheme, this.props.colorSchemeChangeCallback); this.shareDBManager.setEditor(this.editor); if(this.isCollaborative){ this.shareDBManager.initShareDB(); } this.qtipManager = new QtipManager(this.cy, this.editor); this.cxtMenuManager = !this.isCbioPortal ? new ContextMenuManager(this.cy, this.editor, this.props.handleOpen, this.undoRedoManager, this.props.isCollaborative ) : undefined; this.dragDropNodeAddManager = new DragDropNodeAddPlugin(this.editor, this.cy, this.props.pathwayHandler); // Initialize panzoom this.cy.panzoom(panzoomOpts); this.cy.layoutUtilities({ desiredAspectRatio: this.cy.width() / this.cy.height() }); // Node Add initialization this.cy.nodeadd( { // Once the explanationText is cast to uppercase they will be node types components: [ { container: $('#simpleNodeDiv'), nodeType: 'Gene', icon: geneImg }, { container: $('#familyNodeDiv'), nodeType: 'Family', icon: familyImg }, { container: $('#complexNodeDiv'), nodeType: 'Complex', icon: complexImg }, { container: $('#compartmentNodeDiv'), nodeType: 'Compartment', icon: compartmentImg }, { container: $('#processNodeDiv'), nodeType: 'Process', icon: processImg } ] }); const self = this; const edgeHandleDefaults ={ preview: true, // whether to show added edges preview before releasing selection stackOrder: 4, // Controls stack order of edgehandles canvas element by setting it's z-index handleSize: 10, // the size of the edge handle put on nodes handleColor: '#1abc9c', // the colour of the handle and the line drawn from it handleLineType: 'ghost', // can be 'ghost' for real edge, 'straight' for a straight line, or 'draw' for a draw-as-you-go line handleLineWidth: 1, // width of handle line in pixels handleNodes: 'node', // selector/filter function for whether edges can be made from a given node hoverDelay: 1, // time spend over a target node before it is considered a target selection cxt: false, // whether cxt events trigger edgehandles (useful on touch) enabled: false, // whether to start the extension in the enabled state toggleOffOnLeave: true, // whether an edge is cancelled by leaving a node (true), or whether you need to go over again to cancel (false; allows multiple edges in one pass) edgeType: function( sourceNode, targetNode ) { // can return 'flat' for flat edges between nodes or 'node' for intermediate node between them // returning null/undefined means an edge can't be added between the two nodes return 'flat'; }, handlePosition: function( node ){ return 'middle top'; // sets the position of the handle in the format of "X-AXIS Y-AXIS" such as "left top", "middle top" }, loopAllowed: function( node ) { // for the specified node, return whether edges from itself to itself are allowed return false; }, nodeLoopOffset: -50, // offset for edgeType: 'node' loops nodeParams: function( sourceNode, targetNode ) { // for edges between the specified source and target // return element object to be passed to cy.add() for intermediary node return {}; }, edgeParams: function( sourceNode, targetNode, i ) { // for edges between the specified source and target // return element object to be passed to cy.add() for edge // NB: i indicates edge index in case of edgeType: 'node' return {}; }, start: function( sourceNode ) { // fired when edgehandles interaction starts (drag on handle) var type = self.getGlobalEdgeType(); //self.cy.edgehandles('option', 'ghostEdgeType', type); }, complete: function( sourceNode, targetNodes, addedEntities ) { // // Remove recently added edge ! // // FBI takes this case from now on :O // // We will take care of addition in our manager :) self.cy.remove(addedEntities); self.editor.addEdge({ source: sourceNode.id(), target: targetNodes[0].id(), // @ts-ignore type: self.getGlobalEdgeType(window.edgeAddingMode), pubmedIDs: [], name: "" }); }, stop: function( sourceNode ) { // fired when edgehandles interaction is stopped (either complete with added edges or incomplete) //TODO refactor this, so terrible for now //$('.edge-palette a').blur().removeClass('active'); self.edgeAddingMode = -1; self.eh.disable(); self.eh.hide(); self.props.setActiveEdge(-1); }, }; //Edge Handles initialization this.eh = this.cy.edgehandles(edgeHandleDefaults); this.eh.disable(); this.props.editorHandler(this.editor, this.eh, this.undoRedoManager); if(!this.isCbioPortal) { this.cy.nodeEditing({ padding: 5, // spacing between node and grapples/rectangle undoable: true, // and if cy.undoRedo exists grappleSize: 8, // size of square dots grappleColor: "#ffc90e", // color of grapples inactiveGrappleStroke: "inside 1px blue", boundingRectangle: true, // enable/disable bounding rectangle boundingRectangleLineDash: [4, 8], // line dash of bounding rectangle boundingRectangleLineColor: "ffc90e", boundingRectangleLineWidth: 1.5, zIndex: 999, moveSelectedNodesOnKeyEvents: function () { return true; }, minWidth: function (node) { var data = node.data("resizeMinWidth"); return data ? data : 15; }, // a function returns min width of node minHeight: function (node) { var data = node.data("resizeMinHeight"); return data ? data : 15; }, // a function returns min height of node // Getters for some style properties the defaults returns ele.css('property-name') // you are encouraged to override these getters getCompoundMinWidth: function (node) { return node.style('min-width'); }, getCompoundMinHeight: function (node) { return node.style('min-height'); }, getCompoundMinWidthBiasRight: function (node) { return node.style('min-width-bias-right'); }, getCompoundMinWidthBiasLeft: function (node) { return node.style('min-width-bias-left'); }, getCompoundMinHeightBiasTop: function (node) { return node.style('min-height-bias-top'); }, getCompoundMinHeightBiasBottom: function (node) { return node.style('min-height-bias-bottom'); }, isFixedAspectRatioResizeMode: function (node) { return node.is(".fixedAspectRatioResizeMode") },// with only 4 active grapples (at corners) isNoResizeMode: function (node) { return undefined; }, // no active grapples // These optional function will be executed to set the width/height of a node in this extension // Using node.css() is not a recommended way (http://js.cytoscape.org/#eles.style) to do this. Therefore, overriding these defaults // so that a data field or something like that will be used to set node dimentions instead of directly calling node.css() // is highly recommended (Of course this will require a proper setting in the stylesheet). setWidth: function (node, width) { node.data('w', width) }, setHeight: function (node, height) { node.data('h', height); }, setCompoundMinWidth: function (node, minWidth) { node.style('min-width', minWidth); }, setCompoundMinHeight: function (node, minHeight) { node.style('min-height', minHeight); }, setCompoundMinWidthBiasLeft: function (node, minWidthBiasLeft) { node.style('min-width-bias-left', minWidthBiasLeft); }, setCompoundMinWidthBiasRight: function (node, minHeightBiasRight) { node.style('min-width-bias-right', minHeightBiasRight); }, setCompoundMinHeightBiasTop: function (node, minHeightBiasTop) { node.style('min-height-bias-top', minHeightBiasTop); }, setCompoundMinHeightBiasBottom: function (node, minHeightBiasBottom) { node.style('min-height-bias-bottom', minHeightBiasBottom); }, cursors: { // See http://www.w3schools.com/cssref/tryit.asp?filename=trycss_cursor // May take any "cursor" css property default: "default", // to be set after resizing finished or mouseleave inactive: "not-allowed", nw: "nw-resize", n: "n-resize", ne: "ne-resize", e: "e-resize", se: "se-resize", s: "s-resize", sw: "sw-resize", w: "w-resize" }, resizeToContentCueImage: resizeCue, resizeToContentFunction: this.editor.resizeNodesToContent.bind(this.editor), }); } //Navigator for cytoscape js var navDefaults = { container: '.cytoscape-navigator-wrapper' // can be a HTML or jQuery element or jQuery selector , viewLiveFramerate: 0 // set false to update graph pan only on drag end; set 0 to do it instantly; set a number (frames per second) to update not more than N times per second , thumbnailEventFramerate: 10 // max thumbnail's updates per second triggered by graph updates , thumbnailLiveFramerate: false // max thumbnail's updates per second. Set false to disable , dblClickDelay: 200 // milliseconds , removeCustomContainer: true // destroy the container specified by user on plugin destroy , rerenderDelay: 100 // ms to throttle rerender updates to the panzoom for performance }; //TODO: AMENDMENT declaration removed if( !this.isCbioPortal){ this.cy.navigator(navDefaults); // get navigator instance, nav } const viewUtilitiesOpts = { node: { highlighted: { 'border-width': 2, 'border-color': '#bc1142' }, // styles for when nodes are highlighted. unhighlighted: { 'opacity': function (ele: any) { // We return the same opacity because to override the unhibhlighted ele opacity in view-utilities return ele.css('opacity'); } }// styles for when nodes are unhighlighted.} }, edge: { highlighted: {}, // styles for when edges are highlighted. unhighlighted: { 'opacity': function (ele: any) { // We return the same opacity because to override the unhibhlighted ele opacity in view-utilities return ele.css('opacity'); } } // styles for when edges are unhighlighted. }, setVisibilityOnHide: false, // whether to set visibility on hide/show setDisplayOnHide: true, // whether to set display on hide/show neighbor: () => { // return desired neighbors of tapheld node return false; }, neighborSelectTime: 500 //ms, time to taphold to select desired neighbors }; this.viewUtilities = this.cy.viewUtilities(viewUtilitiesOpts); this.editor.setViewUtilities(this.viewUtilities); this.placePanzoomAndOverlay(); } getGlobalEdgeType() { var type = "NONE"; // @ts-ignore if (window.edgeAddingMode === 1) { type = 'ACTIVATES'; } // @ts-ignore else if (window.edgeAddingMode === 2) { type = 'INHIBITS'; } // @ts-ignore else if (window.edgeAddingMode === 3) { type = 'INDUCES'; } // @ts-ignore else if (window.edgeAddingMode === 4) { type = 'REPRESSES'; } // @ts-ignore else if (window.edgeAddingMode === 5) { type = 'BINDS'; } return type; } initCyHandlers(){ var that = this; var tappedBefore: any; var tappedTimeout: number; this.cy.on('tap', function (event: any) { var tappedNow = event.target; if (tappedTimeout !== -1 && tappedBefore !== -1) { clearTimeout(tappedTimeout); } if (tappedBefore === tappedNow) { tappedNow.trigger('doubleTap'); tappedBefore = -1; } else { // @ts-ignore TODO AMENDMENTsa tappedTimeout = setTimeout(function () { tappedBefore = -1; }, 300); tappedBefore = tappedNow; } }); this.cy.on('doubleTap', 'node', function (e: any) { // if cBioPortal ignore if(that.props.isCbioPortal) return; const eventIsDirect = (e.target === this); if (eventIsDirect) { that.qtipManager.addQtipToElements(e.target); e.target.trigger("showqtipevent"); } }); this.cy.on('doubleTap', 'edge', function (e: any) { // if cBioPortal ignore if(that.props.isCbioPortal) return; const eventIsDirect = (e.target === this); if (eventIsDirect) { that.qtipManager.addQtipToElements(e.target); e.target.trigger("showqtipevent"); } }); this.cy.on('select', 'node', (e: any) => { this.editor.pushSelectedNodeStack(e.target); }); this.cy.on('unselect', 'node', (e: any) => { this.editor.removeElementFromSelectedNodeStack(e.target); }); this.cy.on('free', 'node', (e: any) => { //Collect all nodes with descendants in case of compounds var selectedNodes = this.cy.nodes(':selected'); var nodes = e.target; nodes = nodes.union(nodes.descendants()); nodes = nodes.union(selectedNodes); this.editor.moveElements(nodes); }); this.cy.on('layoutstop', () => { this.editor.postLayout(); }); this.cy.on("nodeediting.resizeend", (_e: any, _type: any, node: any) => { //Updates 'data' properties from 'style' node.data('w', node.width()); node.data('h', node.height()); //Used for collaborative mode this.editor.resizeElements(node); }); this.cy.on('select', 'edge', (e: any) => { this.lastSelectedEdge = e.target; }); this.cy.on('bendPointMovement', () => { this.editor.updateEdgeAnchorPoints(this.lastSelectedEdge); }); this.cy.on('nodeediting.moveend', () => { this.editor.changeNodePositionsByArrows(this.cy.nodes(":selected")); }); } initKeyboardHandlers() { if (!this.isCollaborative && !this.isCbioPortal) { $(document).keydown((e: any) => { if (e.which === 89 && (e.ctrlKey || e.metaKey)) { this.undoRedoManager.redo(); } else if (e.which === 90 && (e.ctrlKey || e.metaKey)) { this.undoRedoManager.undo(); } }); } else { $('a[role="redo"]').hide(); $('a[role="undo"]').hide(); } $(document).keydown((e: any) => { if (e.which === 65 && (e.ctrlKey || e.metaKey)) { // @ts-ignore var tn = document.activeElement.tagName; if (tn != "TEXTAREA" && tn != "INPUT") { e.preventDefault(); this.cy.elements().select(); } } else if (e.which === 8 || e.which === 46) { // @ts-ignore var tn = document.activeElement.tagName; if (tn != "TEXTAREA" && tn != "INPUT") { var selectedElements = this.cy.$(':selected'); this.editor.removeElement(selectedElements); } } }); } initUndoRedoFunctionality() { if (this.isCollaborative || this.isCbioPortal) { $('[role="undo"]').hide(); $('[role="redo"]').hide(); /* TODO: AMENDMENT document.getElementById("localOrCollaborativeToolbar").style.display = "none"; */ } } initCBioPortalFunctionalities() { if (this.isCbioPortal) { const contextMenu = this.cy.contextMenus('get'); contextMenu.destroy(); } } }