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
JavaScript
/*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) {