UNPKG

webgme-hfsm

Version:

WebGME Domain for creating Executable Heirarchical Finite State Machines (HFSMs). Contains metamodel, visualization, simulation, and code generation for Heirarchical Finite State Machines (HFSMs) following the UML State Machine specification.

1,545 lines (1,388 loc) 73.2 kB
/*globals define, WebGMEGlobal*/ /*jshint browser: true*/ /** * Generated by VisualizerGenerator 1.7.0 from webgme on Thu May 11 2017 10:42:38 GMT-0700 (PDT). */ define([ // local "text!./HFSM.html", "./Dialog/Dialog", "./Simulator/Simulator", "./Simulator/Choice", // built-ins "js/Constants", "js/Utils/GMEConcepts", "js/Controls/ContextMenu", "js/DragDrop/DropTarget", "js/DragDrop/DragConstants", "decorators/DocumentDecorator/DiagramDesigner/DocumentEditorDialog", // cytoscape "bower/cytoscape/dist/cytoscape.min", "cytoscape-edgehandles", "cytoscape-context-menus", "cytoscape-panzoom", "bower/cytoscape-cose-bilkent/cytoscape-cose-bilkent", // utils "bower/handlebars/handlebars.min", "./typeahead.jquery", "bower/mustache.js/mustache.min", "bower/blob-util/dist/blob-util.min", "text!./style2.css", "q", "underscore", // css "css!bower/cytoscape-context-menus/cytoscape-context-menus.css", "css!bower/cytoscape-panzoom/cytoscape.js-panzoom.css", "css!./styles/HFSMVizWidget.css"], function ( // local HFSMHtml, Dialog, Simulator, Choice, // built-ins CONSTANTS, GMEConcepts, ContextMenu, dropTarget, DROP_CONSTANTS, DocumentEditorDialog, // cytoscape cytoscape, cyEdgehandles, cyContext, cyPanZoom, coseBilkent, // utils handlebars, typeahead, mustache, blobUtil, styleText, Q, _) { "use strict"; //console.log(cytoscape); //console.log(cyEdgehandles); //console.log(cyContext); //console.log(cyPanZoom); //console.log(coseBilkent); cytoscape.use( cyEdgehandles, _.debounce.bind( _ ), _.throttle.bind( _ ) ); cytoscape.use( cyContext, $ ); cytoscape.use( cyPanZoom, $ ); cytoscape.use( coseBilkent ); var rootTypes = ["State Machine","Library"]; var HFSMVizWidget, WIDGET_CLASS = "h-f-s-m-viz"; var minPanelWidth = 10; // percent var gmeIdToCySelector = function(gmeId) { return "#" + gmeId.replace(/\//gm, "\\/"); }; HFSMVizWidget = function (logger, container, client) { this._logger = logger.fork("Widget"); this._el = container; this._client = client; GMEConcepts.initialize(client); this._initialize(); this._logger.debug("ctor finished"); }; HFSMVizWidget.prototype._relativeToWindowPos = function( relativePos ) { var self = this; var windowPos = { x: relativePos.x, y: relativePos.y }; var splitPos = $(self._container).parents(".panel-base-wh").parent().position(); var centerPanelPos = $(".ui-layout-pane-center").position(); // X OFFSET windowPos.x += splitPos.left; windowPos.x += centerPanelPos.left; // Y OFFSET windowPos.y += splitPos.top; windowPos.y += centerPanelPos.top; return windowPos; }; HFSMVizWidget.prototype._getContainerPosFromEvent = function( e ) { var self = this; var x = e.pageX || e.position.x, y = e.pageY || e.position.y; var selector = $(self._el).find(self._containerTag); var splitPos = $(self._container).parents(".panel-base-wh").parent().position(); var centerPanelPos = $(".ui-layout-pane-center").position(); // X OFFSET x -= splitPos.left; x -= centerPanelPos.left; // Y OFFSET y -= splitPos.top; y -= centerPanelPos.top; return { x: x, y: y }; }; HFSMVizWidget.prototype.initializeSimulator = function() { if (this._simulator) { delete this._simulator; } // SIMULATOR this._simulator = new Simulator(); this._simulator.initialize( this._left, this.nodes, this._client ); this._simulator.onStateChanged( this.showActiveState.bind(this) ); this._simulator.onAnimateElement( this.animateElement.bind(this) ); this._simulator.onShowTransitions( this.showTransitions.bind(this) ); this._simulator.setLogDisplay( this._right.find("#simulator-logs").first() ); }; HFSMVizWidget.prototype._stateActiveSelectionChanged = function(model, activeSelection, opts) { var self = this, selectedIDs = [], len = activeSelection ? activeSelection.length : 0; if (opts.invoker !== self) { if (self._cy) { self.clear(); if (len) { var sel = activeSelection.reduce((s, gmeID) => { var idTag = gmeIdToCySelector(gmeID); if (s.length) { s += ","; } return s + " " + idTag; }, ""); var nodes = self._cy.$(sel); nodes.select(); self.highlightNodes( nodes ); } } } }; HFSMVizWidget.prototype._branchChanged = function(args) { var self = this; self.branchChanged = false; self._readOnly = self._client.isReadOnly(); if (self._cy) { self._cy.autoungrabify(self._readOnly); } }; HFSMVizWidget.prototype._branchStatusChanged = function(args) { var self = this; self._readOnly = self._client.isReadOnly(); if (self._cy) { self._cy.autoungrabify(self._readOnly); } if (!self.branchChanged) { self._unsavedNodePositions = {}; self.branchChanged = true; } }; HFSMVizWidget.prototype._initialize = function () { this.branchChanged = false; // set widget class this._el.addClass(WIDGET_CLASS); // add html to element this._el.append(HFSMHtml); // container this._containerTag = "#HFSM_VIZ_DIV"; this._container = this._el.find(this._containerTag).first(); this._cy_container = this._el.find("#cy"); var width = this._el.width(), height = this._el.height(), self = this; // is the project readonly? this._readOnly = this._client.isReadOnly(); // Root Info this.HFSMName = ""; // NODE RELATED DATA this.nodes = {}; this.hiddenNodes = {}; this.dependencies = { "nodes": {}, "edges": {} }; this.waitingNodes = {}; // LAYOUT RELATED DATA this._handle = this._el.find("#hfsmVizHandle"); this._left = this._el.find("#hfsmVizLeft"); this._right = this._el.find("#hfsmVizRight"); // SEARCH Functionality const searchLimit = 25; handlebars.registerHelper('equals', function(a, b, options) { if (a === b) { return options.fn(this); } else { return options.inverse(this); } }); const notFoundTemplate = handlebars.compile([ "<span>", "<span class=\"dataset-not-found\">No results for '{{query}}'</span>", "</span>", ].join('')); const headerTemplate = handlebars.compile([ "<span>", "{{#equals suggestions.length 25}}", "<span class=\"dataset-header\">Showing first {{suggestions.length}} results:</span>", "{{else}}", "<span class=\"dataset-header\">Found {{suggestions.length}} results:</span>", "{{/equals}}", "</span>", ].join('')); const suggestionTemplate = handlebars.compile([ "<span>", "{{#if isConnection}}", "<span class=\"dataset-transition fa fa-repeat\"/>", "<span class=\"dataset-transition\">{{LABEL}}</span>", "<span class=\"dataset-sep fa fa-angle-double-left\"/>", "<span class=\"dataset-src\">{{src.LABEL}}</span>", "<span class=\"dataset-arrow fa fa-sign-out\"/>", "<span class=\"dataset-dst\">{{dst.LABEL}}</span>", "<span class=\"dataset-sep fa fa-angle-double-right\"/>", "{{else}}", "<span class=\"dataset-state fa fa-square\"/>", "<span class=\"dataset-state\">{{LABEL}}</span>", "<span class=\"dataset-sep fa fa-angle-double-left\"/>", "<span class=\"dataset-parent\">{{parent.LABEL}}</span>", "<span class=\"dataset-sep fa fa-angle-double-right\"/>", "{{/if}}", "</span>", ].join('')); const displayTemplate = handlebars.compile([ "{{LABEL}}" ].join('\n')); const mapResult = (result) => { if (result.isConnection) { let src = self.nodes[result.src]; let dst = self.nodes[result.dst]; return { isConnection: result.isConnection, LABEL: result.LABEL, src: src, dst: dst, id: result.id }; } else { let parent = self.nodes[result.parentId]; return { isConnection: result.isConnection, LABEL: result.LABEL, parent: parent, id: result.id }; } }; const search = (text) => { var results = []; // find related nodes let nodes = Object.values(self.nodes).filter((n) => n.LABEL.length); results.push(...(nodes.filter((n) => { var matches = n.LABEL.toLowerCase() === text.toLowerCase(); return matches; })).sort((a,b) => { return (a.LABEL.toLowerCase().localeCompare(b.LABEL.toLowerCase())) + (a.isConnection ? 1 : -1) + (b.isConnection ? -1 : 1); })); results.push(...(nodes.filter((n) => { var matches = n.LABEL.toLowerCase().includes(text.toLowerCase()); return matches; })).sort((a,b) => { return (a.LABEL.toLowerCase().localeCompare(b.LABEL.toLowerCase())) + (a.isConnection ? 1 : -1) + (b.isConnection ? -1 : 1); })); return _.uniq(results).map((r) => mapResult(r)); }; this._debouncedSearch = _.debounce(search.bind(self), 500); this._search = this._el.find("#search"); $(this._search).typeahead({ minLength: 1, highlight: true }, { name: 'node-dataset', limit: searchLimit, source(query, syncResults) { syncResults(search(query)); }, templates: { notFound: notFoundTemplate, header: headerTemplate, suggestion: suggestionTemplate }, display(result) { return displayTemplate(result); } }); $(this._search).bind('typeahead:select', function(ev, suggestion) { var idTag = gmeIdToCySelector(suggestion.id); var node = self._cy.$(idTag); self.clear(); node.select(); self.highlightNode( node ); //self._cy.fit( node, 500); self._cy .animate({ fit: { eles: self._cy.elements(), padding: 50 }, duration: 500 }) .delay(500) .animate({ fit: { eles: node, padding: 350 }, duration: 500 }); }); this._left.css("width", "19.5%"); this._right.css("width", "80%"); // SIMULATOR this.initializeSimulator(); // DRAGGING INFO this.isDragging = false; this._handle.mousedown(function(e) { self.isDragging = true; e.preventDefault(); }); this._container.mouseup(function() { self.isDragging = false; self._cy.resize(); }).mousemove(function(e) { if (self.isDragging) { var selector = $(self._el).find(self._containerTag); var mousePosX = self._getContainerPosFromEvent( e ).x; var maxWidth = selector.width(); var handlePercent = 0.5; var minX = 0; var maxX = selector.width() + minX; var leftWidth = mousePosX - minX; var leftPercent = Math.max(minPanelWidth, (leftWidth / maxWidth) * 100); var rightPercent = Math.max(minPanelWidth, 100 - leftPercent - handlePercent); leftPercent = 100 - rightPercent - handlePercent; self._left.css("width", leftPercent + "%"); self._right.css("width", rightPercent + "%"); } }); /* var DOMURL = window.URL || window.webkitURL || window; var img = new Image(); var svg = new Blob([data], {type: "image/svg+xml"}); var url = DOMURL.createObjectURL(svg); */ this._cytoscape_options = { container: this._cy_container, //style: styleText, style: styleText, //+ 'node { background-image: '+url + ';}', // interaction options: minZoom: 1e-50, maxZoom: 1e50, zoomingEnabled: true, userZoomingEnabled: true, panningEnabled: true, userPanningEnabled: true, boxSelectionEnabled: true, selectionType: "single", touchTapThreshold: 8, desktopTapThreshold: 4, autolock: false, autoungrabify: this._readOnly, autounselectify: false, // rendering options: headless: false, styleEnabled: true, hideEdgesOnViewport: false, hideLabelsOnViewport: false, textureOnViewport: false, motionBlur: false, motionBlurOpacity: 0.2, wheelSensitivity: 1, pixelRatio: "auto", }; this._layout_options = { "name": "cose-bilkent", // Called on `layoutready` ready () { }, // Called on `layoutstop` stop () { }, // Whether to fit the network view after when done fit: true, // Padding on fit padding: 10, // Whether to enable incremental mode randomize: true, // Node repulsion (non overlapping) multiplier nodeRepulsion: 5500, // 4500 // Ideal edge (non nested) length idealEdgeLength: 100, // 50 // Divisor to compute edge forces edgeElasticity: 0.45, // Nesting factor (multiplier) to compute ideal edge length for nested edges nestingFactor: 0.1, // Gravity force (constant) gravity: 0.1, // 0.25 // Maximum number of iterations to perform numIter: 2500, // For enabling tiling tile: false, // true // Type of layout animation. The option set is {'during', 'end', false} animate: "end", // Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function) tilingPaddingVertical: 10, // Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function) tilingPaddingHorizontal: 50, // Gravity range (constant) for compounds gravityRangeCompound: 1.5, // Gravity force (constant) for compounds gravityCompound: 1.0, // Gravity range (constant) gravityRange: 3.8 }; this._cytoscape_options.layout = self._layout_options; this._cy = cytoscape(self._cytoscape_options); // for search this._cy.on("click", () => { $(this._search).blur(); }); var edgeHandleIcon = new Image(10,10); edgeHandleIcon.src = "/assets/DecoratorSVG/svgs/edgeIcon.svg"; // the default values of each option are outlined below: var 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: 7.5, // the size of the edge handle put on nodes handleHitThreshold: 1, // a threshold for hit detection that makes it easier to grab the handle handleIcon: edgeHandleIcon, handleColor: "#00235b", // 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 handleOutlineColor: null,//'#ff0000', // the colour of the handle outline handleOutlineWidth: 1, // the width of the handle outline in pixels handleNodes( node ) { //'node', // selector/filter function var desc = self.nodes[node.id()]; return self.isValidSource( desc ); }, handlePosition: "right bottom", // sets the position of the handle in the format of "X-AXIS Y-AXIS" such as "left top", "middle top" hoverDelay: 150, // time spend over a target node before it is considered a target selection cxt: false, // whether cxt events trigger edgehandles (useful on touch) enabled: true, // whether to start the plugin 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( 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 var srcDesc = self.nodes[sourceNode.id()]; var dstDesc = self.nodes[targetNode.id()]; var isValid = self.validEdge( srcDesc, dstDesc ); if (isValid) { return "flat"; } else { return null; } }, loopAllowed( node ) { // for the specified node, return whether edges from itself to itself are allowed var desc = self.nodes[node.id()]; return self.validEdgeLoop( desc ); }, nodeLoopOffset: -50, // offset for edgeType: 'node' loops nodeParams( sourceNode, targetNode ) { // for edges between the specified source and target // return element object to be passed to cy.add() for intermediary node return {}; }, edgeParams( 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( sourceNode ) { // fired when edgehandles interaction starts (drag on handle) }, complete( sourceNode, targetNodes, addedEntities, position ) { // fired when edgehandles is done and entities are added self.edgeContextMenu( sourceNode, targetNodes, addedEntities, position ); }, stop( sourceNode ) { // fired when edgehandles interaction is stopped (either complete with added edges or incomplete) }, cancel( sourceNode, renderedPosition, invalidTarget ){ // fired when edgehandles are cancelled ( // incomplete - nothing has been added ) - // renderedPosition is where the edgehandle was // released, invalidTarget is // a collection on which the handle was released, // but which for other reasons (loopAllowed | // edgeType) is an invalid target } }; // EDGE HANDLES this._cy.edgehandles( edgeHandleDefaults ); var childAvailableSelector = "node[NodeType = \"State\"],node[NodeType =\"State Machine\"],node[NodeType =\"Library\"]"; // CONTEXT MENUS var options = { // List of initial menu items menuItems: [ { id: "toggleCollapse", content: "(Un-)Show Children", tooltipText: "Toggle the display of children.", selector: childAvailableSelector, onClickFunction ( e ) { //var node = this; var node = e.target; if (self._readOnly) { return; } if (node === self._cy) { } else self.toggleShowChildren( node ); }, coreAsWell: false, hasTrailingDivider: true, // Whether the item will have a trailing divider }, { id: "setActive", content: "Set Active", tooltipText: "Set as the active state.", selector: "node[NodeType = \"State\"]", onClickFunction ( e ) { var node = e.target; if (node === self._cy) { } else { self._simulator.setActiveState( node.id() ); } }, coreAsWell: false }, { id: "newChild", content: "Add child...", tooltipText: "Create a new state, internal transition, etc.", selector: childAvailableSelector, onClickFunction ( e ) { var node = e.target; if (self._readOnly) { return; } if (node === self._cy) { } else { var childPosition = e.position; var dialog = new Dialog(); dialog.initialize( self.nodes[ node.id() ], self._client, childPosition ); dialog.show(); } }, coreAsWell: false }, { id: "Remove", content: "Remove This and Selected Objects", tooltipText: "Remove this object and all currently selected objects (and their outgoing or incoming transitions)", selector: "node, edge", onClickFunction ( e ) { // The function to be executed on click var node = e.target; if (self._readOnly) { return; } if (node === self._cy) { } else { self.selectNodes( [node.id()] ); node.select(); self.highlightNode(node); self.deleteSelection(); } }, coreAsWell: false // Whether core instance have this item on cxttap }, { id: "DocumentView", content: "View/Edit Documentation", tooltipText: "Edit and View the rendered Markdown Documentation", selector: "node[NodeType = \"Documentation\"]", onClickFunction ( e ) { // The function to be executed on click var node = e.target; if (self._readOnly) { return; } if (node === self._cy) { } else { self.onEditDocumentation(node.id()); } }, coreAsWell: false // Whether core instance have this item on cxttap }, { id: "arrangeSelection", content: "Auto-Arrange Selected Nodes Here", tooltipText: "Arrange selected nodes into a grid with a top left where the user clicked.", selector: "node", onClickFunction ( e ) { var node = e.target; if (self._readOnly) { return; } if (node === self._cy) { } else { self._arrangeNodes( self._selectedNodes, e.position ); } }, coreAsWell: false, hasTrailingDivider: true, // Whether the item will have a trailing divider }, { id: "reparentSelection", content: "Move Selected Nodes Here", tooltipText: "Makes the node that was right clicked the parent of the selected node(s).", selector: childAvailableSelector, onClickFunction ( e ) { var node = e.target; if (self._readOnly) { return; } if (node === self._cy) { } else { self._moveNodes( self._selectedNodes, node.id(), self.cyPosToScreenPos(e.position) ); } }, coreAsWell: false, hasTrailingDivider: true, // Whether the item will have a trailing divider }, ], // css classes that menu items will have menuItemClasses: [ // add class names to this list ], // css classes that context menu will have contextMenuClasses: [ // add class names to this list ] }; var ctxMenuInstance = this._cy.contextMenus( options ); // PAN ZOOM WIDGET: // the default values of each option are outlined below: var panZoom_defaults = { zoomFactor: 0.05, // zoom factor per zoom tick zoomDelay: 45, // how many ms between zoom ticks minZoom: 0.1, // min zoom level maxZoom: 10, // max zoom level fitPadding: 50, // padding when fitting panSpeed: 10, // how many ms in between pan ticks panDistance: 10, // max pan distance per tick panDragAreaSize: 75, // the length of the pan drag box in which the vector for panning is calculated (bigger = finer control of pan speed and direction) panMinPercentSpeed: 0.25, // the slowest speed we can pan by (as a percent of panSpeed) panInactiveArea: 8, // radius of inactive area in pan drag box panIndicatorMinOpacity: 0.5, // min opacity of pan indicator (the draggable nib); scales from this to 1.0 zoomOnly: false, // a minimal version of the ui only with zooming (useful on systems with bad mousewheel resolution) //fitSelector: undefined, // selector of elements to fit animateOnFit(){ // whether to animate on fit return false; }, fitAnimationDuration: 1000, // duration of animation on fit // icon class names sliderHandleIcon: "fa fa-minus", zoomInIcon: "fa fa-plus", zoomOutIcon: "fa fa-minus", resetIcon: "fa fa-expand" }; self._cy.panzoom( panZoom_defaults ); // USED FOR DRAG ABILITY self._hoveredNodeId = null; self._cy.on("mouseover", childAvailableSelector, function(e) { var node = this; self._hoveredNodeId = node.id(); if (self._isDropping) { self.showDropStatus(); } else { self.clearDropStatus(); } }); self._cy.on("mouseout", childAvailableSelector, function(e) { self._hoveredNodeId = null; if (self._isDropping) { self.showDropStatus(); } else { self.clearDropStatus(); } }); self._el.on("mouseout", function(e) { self._hoveredNodeId = null; self.clearDropStatus(); }); // USED FOR NODE SELECTION AND MULTI-SELECTION self._selectedNodes = []; self.multiSelectionEnabled = false; self._cy.on("tap", "node, edge", function(e){ self.multiSelectionEnabled = e.originalEvent.ctrlKey; // is there a better way of doing this? if (self.multiSelectionEnabled) { self._cy._private.selectionType = "additive"; } else { self._cy._private.selectionType = "single"; } }); self._cy.on("select", "node, edge", function(e){ var node = this; var id = node.id(); if (id) { self.selectNodes([id]); self.highlightNode(node); } }); self._cy.on("unselect", "node, edge", function(e){ var node = this; var id = node.id(); if (id) { self.unselectNodes([id]); } }); // USED FOR KNOWING WHEN NODES ARE MOVED self._webGME_to_cy_scale = 1; self._grabbedNode = null; self._cy.on("grabon", "node", function(e) { var node = this; if (node.id()) { self._grabbedNode = node; } }); self._cy.on("free", "node", function(e) { self._grabbedNode = null; }); self._debouncedSaveNodePositions = _.debounce(self.saveNodePositions.bind(self), 500); self._unsavedNodePositions = {}; self._cy.on("position", "node", function(e) { if (self._grabbedNode) { var node = this; var type = node.data("type"); var id = node.id(); if (type && rootTypes.indexOf(type) == -1 && self.nodes[id] !== undefined) { var pos = self.cyPosToGmePos( node ); self._unsavedNodePositions[id] = pos; self._debouncedSaveNodePositions(); } } }); // USED FOR ZOOMING AFTER INITIALLY LOADING ALL THE NODES (in CreateNode()) self._debounced_one_time_zoom = _.debounce(_.once(self.onZoomClicked.bind(self)), 250); self._debouncedSelectNodes = _.debounce(self.selectNodes.bind(self), 200); this._attachClientEventListeners(); this._makeDroppable(); }; /* * * * * * * * Display Functions * * * * * * * */ HFSMVizWidget.prototype.selectNodes = function(ids) { var self = this; // update selected nodes self._selectedNodes = _.union(self._selectedNodes, ids); // register updated selection state WebGMEGlobal.State.registerActiveSelection( self._selectedNodes.slice(0), {invoker: self} ); }; HFSMVizWidget.prototype.unselectNodes = function(ids) { var self = this; // update selected nodes self._selectedNodes = _.difference(self._selectedNodes, ids) // ensure cytoscape unselects the selected nodes self.clear(); // register updated selection state self._debouncedSelectNodes([]); }; HFSMVizWidget.prototype.unselectAll = function() { var self = this; // update selected nodes self._selectedNodes = []; // ensure cytoscape unselects the selected nodes self.clear(); // register updated selection state self._debouncedSelectNodes([]); }; function download(filename, text) { var element = document.createElement("a"); var imgData = text.split(",")[1]; // after the comma is the actual image data blobUtil.base64StringToBlob( imgData.toString() ).then(function(blob) { var blobURL = blobUtil.createObjectURL(blob); element.setAttribute("href", blobURL); element.setAttribute("download", filename); element.style.display = "none"; document.body.appendChild(element); element.click(); document.body.removeChild(element); }).catch(function(err) { console.log("Couldnt make blob from image!"); console.log(err); }); } HFSMVizWidget.prototype.onEditDocumentation = function(gmeId) { var self = this; var documentation = self.nodes[gmeId].documentation; var editorDialog = new DocumentEditorDialog(); editorDialog.initialize(documentation, function (text) { try { self._client.setAttribute(gmeId, "documentation", text, "updated documentation for " + gmeId); } catch (e) { console.error("Could not save documentation: "); console.error(e); } }); editorDialog.show(); }; HFSMVizWidget.prototype.onPanningClicked = function() { var self = this; self._cy.userPanningEnabled(true); self._cy.boxSelectionEnabled(false); self._cy.autoungrabify(false); }; HFSMVizWidget.prototype.onBoxSelectClicked = function() { var self = this; self._cy.userPanningEnabled(false); self._cy.boxSelectionEnabled(true); self._cy.autoungrabify(true); }; HFSMVizWidget.prototype.onZoomClicked = function() { var self = this; var layoutPadding = 50; self._cy.fit( self._cy.elements(), layoutPadding); /* self._cy.animate({ fit: { eles: self._cy.elements(), padding: layoutPadding }, duration: layoutDuration }); */ }; HFSMVizWidget.prototype._addSplitPanelToolbarBtns = function(toolbarEl) { var self = this; // BUTTON EVENT HANDLERS var printEl = [ '<span id="print" class="split-panel-toolbar-btn fa fa-print">', '</span>', ].join('\n'); var moveEl = [ '<span id="pan" class="split-panel-toolbar-btn fa fa-arrows">', '</span>', ].join('\n'); var selectEl = [ '<span id="select" class="split-panel-toolbar-btn fa fa-crop">', '</span>', ].join('\n'); var zoomEl = [ '<span id="zoom" class="split-panel-toolbar-btn fa fa-home">', '</span>', ].join('\n'); var layoutEl = [ '<span id="layout" class="split-panel-toolbar-btn fa fa-random">', '</span>', ].join('\n'); //toolbarEl.append(moveEl); //toolbarEl.append(selectEl); toolbarEl.append(printEl); toolbarEl.append(zoomEl); toolbarEl.append(layoutEl); toolbarEl.find('#print').on('click', function(){ var png = self._cy.png({ full: true, scale: 6, bg: 'white' }); download( self.HFSMName + '-HFSM.png', png ); }); toolbarEl.find('#pan').on('click', function() { self.onPanningClicked(); }); toolbarEl.find('#select').on('click', function() { self.onBoxSelectClicked(); }); toolbarEl.find('#zoom').on('click', function(){ self.onZoomClicked(); }); toolbarEl.find('#layout').on('click', function(){ // ask if they really want to randomize the layout var choice = new Choice(); var choices = [ "Yes, run cose-bilkent layout.", "No, do not change any positions" ]; choice.initialize( choices, "Really change the layout?" ); choice.show(); return choice.waitForChoice() .then(function(choice) { if (choice == choices[0]) self.reLayout(); }); }); }; HFSMVizWidget.prototype.highlightNode = function(node) { var self = this; self._simulator.hideStateInfo(); self._simulator.displayStateInfo( node.id() ); }; HFSMVizWidget.prototype.highlightNodes = function(nodes) { var self = this; self._simulator.hideStateInfo(); }; HFSMVizWidget.prototype.clear = function() { var self = this; self._cy.$(":selected").unselect(); self._simulator.hideStateInfo(); }; /* * * * * * * * Transition Selection * * * * * * * */ HFSMVizWidget.prototype.showTransitions = function( transitionIDs ) { var self = this; self.clear(); // update selected nodes self._selectedNodes = []; // update selection state WebGMEGlobal.State.registerActiveSelection( self._selectedNodes.slice(0), {invoker: this} ); var tidSelector = transitionIDs.reduce((sel, id) => { self._selectedNodes.push(id); // highlight the Transition var idTag = gmeIdToCySelector(id); if (sel.length) { sel += ","; } return sel + " " + idTag; }, ""); var edges = self._cy.$(tidSelector); edges.select(); self.highlightNodes( edges ); // update selection state WebGMEGlobal.State.registerActiveSelection( self._selectedNodes.slice(0), {invoker: this} ); }; /* * * * * * * * Node Position Functions * * * * * * * */ HFSMVizWidget.prototype.cyPosition = function(cyNode, pos) { // assumes pos is GME position - origin is top left of the // node let w = cyNode.width(), h = cyNode.height(), x = cyNode.position("x"), y = cyNode.position("y"), x1 = x - w/2, y1 = y - h/2; if (pos == undefined) { // always return position with respect to top left return { x: x1, y: y1 }; } else { // cytoscape uses origin at center of the node, so we // must convert before setting let newPos = { x: pos.x + w/2, y: pos.y + h/2 } cyNode.position(newPos); } }; HFSMVizWidget.prototype.cyPosToScreenPos = function(pos) { var self = this; var extent = self._cy.extent(); // returns bounding box of model positions visible var width = $(self._cy_container).width(); var height = $(self._cy_container).height(); var newPos = { x: (pos.x - extent.x1) / extent.w * width, y: (pos.y - extent.y1) / extent.h * height, }; return newPos; }; HFSMVizWidget.prototype.screenPosToCyPos = function(pos) { var self = this; var extent = self._cy.extent(); // returns bounding box of model positions visible var width = $(self._cy_container).width(); var height = $(self._cy_container).height(); var newPos = { x: (pos.x / width) * extent.w + extent.x1, y: (pos.y / height) * extent.h + extent.y1, }; return newPos; }; HFSMVizWidget.prototype.gmePosToCyPos = function(desc) { var self = this; var cyPos = desc.position; return cyPos; }; HFSMVizWidget.prototype.cyPosToGmePos = function(cyNode) { var self = this; var gmePos = self.cyPosition( cyNode ); return gmePos; }; HFSMVizWidget.prototype.needToUpdatePosition = function(pos1, pos2) { var dx = Math.abs(pos1.x - pos2.x); var dy = Math.abs(pos1.y - pos2.y); var dyThresh = 0.01; var dxThresh = 0.01; return (dy > dyThresh || dx > dxThresh); }; HFSMVizWidget.prototype.saveNodePositions = function() { var self = this; if (_.isEmpty(self._unsavedNodePositions)) return; var keys = Object.keys(self._unsavedNodePositions); self._client.startTransaction(); keys.map(function(k) { var id = k; if (self.nodes[id] && rootTypes.indexOf(self.nodes[id].type) == -1 ) { var pos = self._unsavedNodePositions[id]; var originalPos = self.nodes[id].position; if (!_.isEqual(pos, originalPos)) { //console.log('saving for '+id); //console.log(originalPos); //console.log(pos); self._client.setRegistry(id, "position", pos); } } }); self._client.completeTransaction("", (err, result) => { if (err) { } else { self._unsavedNodePositions = {}; } }); }; /* * * * * * * * Graph Creation Functions * * * * * * * */ HFSMVizWidget.prototype.checkDependencies = function(desc) { var self = this; // dependencies will always be either parentId (nodes & edges) or connection (edges) var deps = []; if (desc.parentId && !self.nodes[desc.parentId]) { deps.push(desc.parentId); } if (desc.isConnection) { if (!self.nodes[desc.src]) deps.push(desc.src); if (!self.nodes[desc.dst]) deps.push(desc.dst); } var depsMet = (deps.length == 0); if (!depsMet) { if (desc.isConnection) self.dependencies.edges[desc.id] = deps; else self.dependencies.nodes[desc.id] = deps; self.waitingNodes[desc.id] = desc; if (self.nodes[desc.id]) delete self.nodes[desc.id]; } return depsMet; }; HFSMVizWidget.prototype.updateDependencies = function() { var self = this; var nodePaths = Object.keys(self.dependencies.nodes); var edgePaths = Object.keys(self.dependencies.edges); // create any nodes whose depenencies are fulfilled now nodePaths.map(function(nodePath) { var depPaths = self.dependencies.nodes[nodePath]; if (depPaths && depPaths.length > 0) { depPaths = depPaths.filter(function(objPath) { return self.nodes[objPath] == undefined; }); if (!depPaths.length) { var desc = self.waitingNodes[nodePath]; delete self.waitingNodes[nodePath]; delete self.dependencies.nodes[nodePath]; self.createNode(desc); } else { self.dependencies.nodes[nodePath] = depPaths; } } else { delete self.dependencies.nodes[nodePath]; } }); // Create any edges whose dependencies are fulfilled now edgePaths.map(function(edgePath) { var depPaths = self.dependencies.edges[edgePath]; if (depPaths && depPaths.length > 0) { depPaths = depPaths.filter(function(objPath) { return self.nodes[objPath] == undefined; }); if (!depPaths.length) { var connDesc = self.waitingNodes[edgePath]; delete self.waitingNodes[edgePath]; delete self.dependencies.edges[edgePath]; self.createEdge(connDesc); } else { self.dependencies.edges[edgePath] = depPaths; } } else { delete self.dependencies.edges[edgePath]; } }); }; HFSMVizWidget.prototype.reLayout = function() { var self = this; var layout = self._cy.layout(self._layout_options); layout.run(); }; HFSMVizWidget.prototype.getDescData = function(desc) { var self = this; var data = {}; if (desc.isConnection) { var from = self.nodes[desc.src]; var to = self.nodes[desc.dst]; if (from && to) { data = { id: desc.id, type: desc.type, interaction: desc.type, source: from.id, target: to.id, name: desc.name, // source-label // target-label label: desc.LABEL, Enabled: (desc.Enabled) ? "True" : "False" }; } } else { data = { id: desc.id, parent: desc.parentId, type: desc.type, NodeType: desc.type, name: desc.name, label: desc.LABEL, isIncomplete: (desc.type == "State" && !desc.isComplete) ? "True" : "False" }; } return data; }; HFSMVizWidget.prototype.createEdge = function(desc) { var self = this; if (desc && desc.src && desc.dst) { self.forceShowBranch( desc.parentId ); var data = self.getDescData(desc); if (data) { self._cy.add({ group: "edges", data: data, }); self.nodes[desc.id] = desc; self.updateDependencies(); } } }; HFSMVizWidget.prototype.createNode = function(desc) { var self = this; self.forceShowBranch( desc.parentId ); var data = self.getDescData(desc); var node = { group: "nodes", data: data }; var n = self._cy.add(node); var pos = self.gmePosToCyPos(desc); if (pos) { self.cyPosition(n, pos); } self.nodes[desc.id] = desc; self.updateDependencies(); self._debounced_one_time_zoom(); }; // Adding/Removing/Updating items HFSMVizWidget.prototype.addNode = function (desc) { var self = this; if (self._el && self.nodes && desc) { if ( rootTypes.indexOf( desc.type ) > -1 ) { self.HFSMName = desc.name; } var depsMet = self.checkDependencies(desc); // Add node to a table of nodes if (desc.isConnection) { // if this is an edge if (depsMet) { // ready to make edge self.createEdge(desc); } } else { if (depsMet) { // ready to make node self.createNode(desc); } } self._simulator.update( ); } }; HFSMVizWidget.prototype.removeNode = function (gmeId) { // TODO: need to have this take into account hidden nodes! var self = this; if (self._el && self.nodes) { var idTag = gmeIdToCySelector(gmeId); var desc = self.nodes[gmeId]; if (desc) { self.forceShowBranch( gmeId ); if (!desc.isConnection) { delete self.dependencies.nodes[gmeId]; self._cy.$(idTag).neighborhood().forEach(function(ele) { if (ele && ele.isEdge()) { var edgeId = ele.data( "id" ); var edgeDesc = self.nodes[edgeId]; self.checkDependencies(edgeDesc); } }); } else { delete self.dependencies.edges[gmeId]; } if (self._selectedNodes.indexOf(gmeId) > -1) { self._selectedNodes = self._selectedNodes.filter((id) => { return id != gmeId; }); WebGMEGlobal.State.registerActiveSelection(self._selectedNodes.slice(0), {invoker: this}); } delete self.nodes[gmeId]; delete self.waitingNodes[gmeId]; self._cy.remove(idTag); self.updateDependencies(); self._simulator.update( ); } } }; HFSMVizWidget.prototype.updateNode = function (desc) { var self = this; // TODO: need to have this take into account hidden nodes! if (self._el && desc) { if ( rootTypes.indexOf( desc.type ) > -1 ) { self.HFSMName = desc.name; } var oldDesc = this.nodes[desc.id]; if (oldDesc) { var idTag = gmeIdToCySelector(desc.id); var cyNode = this._cy.$(idTag); if (desc.isConnection) { if (desc.src != oldDesc.src || desc.dst != oldDesc.dst) { this._cy.remove(idTag); let depsMet = self.checkDependencies( desc ); if (depsMet) { self.createEdge(desc); } } else { cyNode.data( this.getDescData(desc) ); } } else { cyNode.data( this.getDescData(desc) ); // update position from model if (!_.isEqual(oldDesc.position, desc.position)) { //console.log(`${desc.name}: (old, new)`); //console.log(oldDesc.position); //console.log(desc.position); if (rootTypes.indexOf(desc.type) == -1) { self.cyPosition(cyNode, self.gmePosToCyPos(desc)); delete this._unsavedNodePositions[desc.id]; } } } } else { // we haven't seen this node before (might be due to branch change) self.addNode(desc); } this.nodes[desc.id] = desc; self._simulator.update( ); } }; /* * * * * * * * Active State Display * * * * * * * */ HFSMVizWidget.prototype.animateElement = function( eleId, _class="active" ) { var self = this; var idTag = gmeIdToCySelector(eleId); var eles = self._cy.$(idTag); if (eles.length) { eles.flashClass(_class, 1000); } }; HFSMVizWidget.prototype.animateElements = function( eleIds, _class="active" ) { var self = this; var idTag = eleIds.reduce((tag, id) => { var s = gmeIdToCySelector(id); if (tag.length) { tag += ","; } return tag + " " + s; }, ""); var eles = self._cy.$(idTag); if (eles.length) { eles.flashClass(_class, 1000); } }; HFSMVizWidget.prototype.showActiveState = function( stateId ) { var self = this; var previousActiveState = self._cy.nodes("[ActiveState]"); if (previousActiveState.length) { var data = previousActiveState.data(); data.ActiveState = undefined; previousActiveState.data( data ); } if (stateId) { var idTag = gmeIdToCySelector(stateId); var node = self._cy.$(idTag); if (node.length) { var data = node.data(); data.ActiveState = true; node.data( data ); } } }; /* * * * * * * * Context Menu Functions * * * * * * * */ HFSMVizWidget.prototype.createWebGMEContextMenu = function(menuItems, fnCallback, position) { var self = this; var menu = new ContextMenu({ items: menuItems, callback: function(key) { if (fnCallback) fnCallback(key); } }); position = position || {x: 200, y:200}; menu.show(position); }; HFSMVizWidget.prototype.deleteNode = function( nodeId ) { var self = this; var edgesTo = self._simulator.getEdgesToNode( nodeId ); var edgesFrom = self._simulator.getEdgesFromNode( nodeId ); self._client.startTransaction(); if (edgesTo) { edgesTo.map(function(eid) { self._client.deleteNode( eid, "Removing dependent (dst) transition: " + eid ); }); } if (edgesFrom) { edgesFrom.map(function(eid) { self._client.deleteNode( eid, "Removing dependent (src) transition: " + eid ); }); } self._client.deleteNode( nodeId, "Removing " + nodeId ); self._client.completeTransaction(); }; HFSMVizWidget.prototype.deleteSelection = function( ) { var self = this; var selection = self._selectedNodes, edgesTo = [], edgesFrom = []; selection.map(id => { edgesTo = _.union(edgesTo, self._simulator.getEdgesToNode( id )); edgesFrom = _.union(edgesFrom, self._simulator.getEdgesFromNode( id )); }); self._client.startTransaction(); selection.map(id => { self._client.deleteNode( id, "Removing selected " + id ); }); if (edgesTo) { edgesTo.map(function(eid) { self._client.deleteNode( eid, "Removing dependent (dst) transition: " + eid ); }); } if (edgesFrom) {