blob: fdbac044e91bbbe1f10e68bf36d52581b0052666 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Sets the size of the graph editing window.
* The graph is always centered in the container according to these dimensions.
*/
Editor.prototype.setSize = function() {
this.width = $(this.container).width();
this.height = $(this.container).height();
}
/*
* Resize the force layout. The D3 force layout controls the movement of the
* svg elements within the container.
*/
Editor.prototype.resizeForce = function() {
this.force.size([this.width, this.height])
.linkDistance(this.linkDistance)
.charge(-500 - (this.linkDistance - 150)*2);
}
/*
* Returns the detailed view of each row in the table view.
*/
Editor.prototype.getRowDetailsHtml = function(row) {
var outerContainer = $('<div />');
var navContainer = $('<ul />')
.attr('class', 'nav nav-tabs')
.attr('id', 'tablet-nav')
.appendTo(outerContainer);
navContainer.append($('<li class="active"><a data-toggle="tab" data-name="outgoingMessages">Outgoing Messages</a></li>'));
navContainer.append($('<li><a data-toggle="tab" data-name="incomingMessages">Incoming Messages</a></li>'));
navContainer.append($('<li><a data-toggle="tab" data-name="neighbors">Neighbors</a></li>'));
var dataContainer = $('<div />')
.attr('class', 'tablet-data-container')
.appendTo(outerContainer);
return {
'outerContainer' : outerContainer,
'dataContainer' : dataContainer,
'navContainer' : navContainer
};
}
Editor.prototype.initTable = function() {
var jqueryTableContainer = $(this.tablet[0]);
var jqueryTable = $('<table id="editor-tablet-table" class="editor-tablet-table table display">' +
'<thead><tr><th></th><th>Vertex ID</th><th>Vertex Value</th><th>Outgoing Msgs</th>' +
'<th>Incoming Msgs</th><th>Neighbors</th></tr></thead></table>');
jqueryTableContainer.append(jqueryTable);
// Define the table schema and initialize DataTable object.
this.dataTable = $("#editor-tablet-table").DataTable({
'columns' : [
{
'class' : 'tablet-details-control',
'orderable' : false,
'data' : null,
'defaultContent' : ''
},
{ 'data' : 'vertexId' },
{ 'data' : 'vertexValue' },
{ 'data' : 'outgoingMessages.numOutgoingMessages' },
{ 'data' : 'incomingMessages.numIncomingMessages' },
{ 'data' : 'neighbors.numNeighbors'}
]
});
}
/*
* Zooms the svg element with the given translate and scale factors.
* Use translate = [0,0] and scale = 1 for original zoom level (unzoomed).
*/
Editor.prototype.zoomSvg = function(translate, scale) {
this.currentZoom.translate = translate;
this.currentZoom.scale = scale;
this.svg.attr("transform", "translate(" + translate + ")"
+ " scale(" + scale + ")");
}
Editor.prototype.redraw = function() {
this.zoomSvg(d3.event.translate, d3.event.scale);
}
/*
* Initializes the SVG element, along with marker and defs.
*/
Editor.prototype.initElements = function() {
// Create the tabular view and hide it for now.
this.tablet = d3.select(this.container)
.insert('div')
.attr('class', 'editor-tablet')
.style('display', 'none');
this.initTable();
// Creates the main SVG element and appends it to the container as the first child.
// Set the SVG class to 'editor'.
this.svgRoot = d3.select(this.container)
.insert('svg')
this.zoomHolder = this.svgRoot
.attr('class','editor')
.attr('pointer-events', 'all')
.append('svg:g');
this.svg = this.zoomHolder.append('svg:g');
this.svgRect = this.svg.append('svg:rect')
// Defines end arrow marker for graph links.
this.svg.append('svg:defs')
.append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 6)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
// Defines start arrow marker for graph links.
this.svgRoot.append('svg:defs')
.append('svg:marker')
.attr('id', 'start-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 4)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M10,-5L0,0L10,5')
.attr('fill', '#000');
// Append the preloader
// Dimensions of the image are 128x128
var preloaderX = this.width / 2 - 64;
var preloaderY = this.height / 2 - 64;
this.preloader = this.svgRoot.append('svg:g')
.attr('transform', 'translate(' + preloaderX + ',' + preloaderY + ')')
.attr('opacity', 0);
this.preloader.append('svg:image')
.attr('xlink:href', 'img/preloader.gif')
.attr('width', '128')
.attr('height', '128');
this.preloader.append('svg:text')
.text('Loading')
.attr('x', '40')
.attr('y', '128');
// Aggregators
this.aggregatorsContainer = this.svg.append('svg:g');
this.aggregatorsContainer.append('text')
.attr('class', 'editor-aggregators-heading')
.text('Aggregators');
// d3 selector for global key-value pairs
this.globs = this.aggregatorsContainer.append('text').selectAll('tspan');
}
/*
* Binds the mouse and key events to the appropriate methods.
*/
Editor.prototype.initEvents = function() {
// Mouse event vars - These variables are set (and reset) when the corresponding event occurs.
this.selected_node = null;
this.selected_link = null;
this.mousedown_link = null;
this.mousedown_node = null;
this.mouseup_node = null;
// Binds mouse down/up/move events on main SVG to appropriate methods.
// Used to create new nodes, create edges and dragging the graph.
this.svg.on('mousedown', this.mousedown.bind(this))
.on('mousemove', this.mousemove.bind(this))
.on('mouseup', this.mouseup.bind(this));
// Binds Key down/up events on the window to appropriate methods.
d3.select(window)
.on('keydown', this.keydown.bind(this))
.on('keyup', this.keyup.bind(this));
}
/*
* Initializes D3 force layout to update node/link location and orientation.
*/
Editor.prototype.initForce = function() {
this.force = d3.layout.force()
.nodes(this.nodes)
.links(this.links)
.size([this.width, this.height])
.linkDistance(this.linkDistance)
.charge(-500 )
.on('tick', this.tick.bind(this))
}
/*
* Reset the mouse event variables to null.
*/
Editor.prototype.resetMouseVars = function() {
this.mousedown_node = null;
this.mouseup_node = null;
this.mousedown_link = null;
}
/*
* Called at a fixed time interval to update the nodes and edge positions.
* Gives the fluid appearance to the editor.
*/
Editor.prototype.tick = function() {
// draw directed edges with proper padding from node centers
this.path.attr('d', function(d) {
var sourcePadding = getPadding(d.source);
var targetPadding = getPadding(d.target);
var deltaX = d.target.x - d.source.x,
deltaY = d.target.y - d.source.y,
dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
normX = deltaX / dist,
normY = deltaY / dist,
sourcePadding = d.left ? sourcePadding[0] : sourcePadding[1],
targetPadding = d.right ? targetPadding[0] : targetPadding[1],
sourceX = d.source.x + (sourcePadding * normX),
sourceY = d.source.y + (sourcePadding * normY),
targetX = d.target.x - (targetPadding * normX),
targetY = d.target.y - (targetPadding * normY);
return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
});
this.circle.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
}
/*
* Returns the radius of the node.
* Radius is not fixed since nodes with longer identifiers need a bigger circle.
* @param {int} node - Node object whose radius is required.
*/
function getRadius(node) {
// Radius is detemined by multiplyiing the max of length of node ID
// and node value (first attribute) by a factor and adding a constant.
// If node value is not present, only node id length is used.
return 14 + Math.max(node.id.length, getAttrForDisplay(node.attrs).length) * 3;
}
/*
* Truncates the attribute value so that it fits propertly on the editor node
* without exploding the circle.
*/
function getAttrForDisplay(attr) {
if (attr && attr.length > 11) {
return attr.slice(0, 4) + "..." + attr.slice(attr.length - 4);
}
return attr ? attr : '';
}
/*
* Returns the padding of the node.
* Padding is used by edges as an offset from the node center.
* Padding is not fixed since nodes with longer identifiers need bigger circle.
* @param {int} node - Node object whose padding is required.
*/
function getPadding(node) {
// Offset is detemined by multiplyiing the max of length of node ID
// and node value (first attribute) by a factor and adding a constant.
// If node value is not present, only node id length is used.
var nodeOffset = Math.max(node.id.length, getAttrForDisplay(node.attrs).length) * 3;
return [19 + nodeOffset, 12 + nodeOffset];
}
/*
* Returns a new node object.
* @param {string} id - Identifier of the node.
*/
Editor.prototype.getNewNode = function(id) {
return {id : id, reflexive : false, attrs : null, x: Math.random(), y: Math.random(), enabled: true, color: this.defaultColor};
}
/*
* Returns a new edge object.
* @param {object} source - Object for the source node.
* @param {object) target - Object for the target node.
* @param {object} edgeVal - Any edge value object.
*/
Editor.prototype.getNewEdge = function(source, target, edgeValue) {
return {source: source, target: target, edgeValue: edgeValue};
}
/*
* Returns a new link (edge) object from the node IDs of the logical edge.
* @param {string} sourceNodeId - The ID of the source node in the logical edge.
* @param {string} targetNodeId - The ID of the target node in the logical edge.
* @param {string} [edgeValue] - Value associated with the edge. Optional parameter.
* @desc - Logical edge means, "Edge from node with ID x to node with ID y".
* It implicitly captures the direction. However, the link objects have
* the 'left' and 'right' properties to denote direction. Also, source strictly < target.
* Therefore, the source and target may not match that of the logical edge, but the
* direction will compensate for the mismatch.
*/
Editor.prototype.getNewLink = function(sourceNodeId, targetNodeId, edgeValue) {
var source, target, direction, leftValue = null, rightValue = null;
if (sourceNodeId < targetNodeId) {
source = sourceNodeId;
target = targetNodeId;
direction = 'right';
rightValue = edgeValue;
} else {
source = targetNodeId;
target = sourceNodeId;
direction = 'left';
leftValue = edgeValue;
}
// Every link has an ID - Added to the SVG element to show edge value as textPath
if (!this.maxLinkId) {
this.maxLinkId = 0;
}
link = {source : this.getNodeWithId(source), target : this.getNodeWithId(target),
id : this.maxLinkId++, leftValue : leftValue, rightValue : rightValue, left : false, right : false};
link[direction] = true;
return link;
}
/*
* Returns the logical edge(s) from a link object.
* @param {object} link - Link object.
* This method is required because a single link object may encode
* two edges using the left/right attributes.
*/
Editor.prototype.getEdges = function(link) {
var edges = [];
if (link.left || this.undirected) {
edges.push(this.getNewEdge(link.target, link.source, link.leftValue));
}
if (link.right || this.undirected) {
edges.push(this.getNewEdge(link.source, link.target, link.rightValue));
}
return edges;
}
/*
* Adds a new link object to the links array or updates an existing link.
* @param {string} sourceNodeId - Id of the source node in the logical edge.
* @param {string} targetNodeid - Id of the target node in the logical edge.
* @param {string} [edgeValue] - Value associated with the edge. Optional parameter.
*/
Editor.prototype.addEdge = function(sourceNodeId, targetNodeId, edgeValue) {
// console.log('Adding edge: ' + sourceNodeId + ' -> ' + targetNodeId);
// Get the new link object.
var newLink = this.getNewLink(sourceNodeId, targetNodeId, edgeValue);
// Check if a link with these source and target Ids already exists.
var existingLink = this.links.filter(function(l) {
return (l.source === newLink.source && l.target === newLink.target);
})[0];
// Add link to graph (update if exists).
if (existingLink) {
// Set the existingLink directions to true if either
// newLink or existingLink denote the edge.
existingLink.left = existingLink.left || newLink.left;
existingLink.right = existingLink.right || newLink.right;
if (edgeValue != undefined) {
if (sourceNodeId < targetNodeId) {
existingLink.rightValue = edgeValue;
} else {
existingLink.leftValue = edgeValue;
}
}
return existingLink;
} else {
this.links.push(newLink);
return newLink;
}
}
/*
* Adds node with nodeId to the graph (or ignores if already exists).
* Returns the added (or already existing) node.
* @param [{string}] nodeId - ID of the node to add. If not provided, adds
* a new node with a new nodeId.
* TODO(vikesh): Incremental nodeIds are buggy. May cause conflict. Use unique identifiers.
*/
Editor.prototype.addNode = function(nodeId) {
if (!nodeId) {
nodeId = (this.numNodes + 1).toString();
}
var newNode = this.getNodeWithId(nodeId);
if (!newNode) {
newNode = this.getNewNode(nodeId);
this.nodes.push(newNode);
this.numNodes++;
}
return newNode;
}
/*
* Updates existing links and adds new links.
*/
Editor.prototype.restartLinks = function() {
// path (link) group
this.path = this.path.data(this.links);
this.pathLabels = this.pathLabels.data(this.links);
// Update existing links
this.path.classed('selected', (function(d) {
return d === this.selected_link;
}).bind(this))
.style('marker-start', (function(d) {
return d.left && !this.undirected ? 'url(#start-arrow)' : '';
}).bind(this))
.style('marker-end', (function(d) {
return d.right && !this.undirected ? 'url(#end-arrow)' : '';
}).bind(this));
// Add new links.
// For each link in the bound data but not in elements group, enter()
// selection calls everything that follows once.
// Note that links are stored as source, target where source < target.
// If the link is from source -> target, it's a 'right' link.
// If the link is from target -> source, it's a 'left' link.
// A right link has end marker at the target side.
// A left link has a start marker at the source side.
this.path.enter()
.append('svg:path')
.attr('class', 'link')
.attr('id', function(d) { return d.id })
.classed('selected', (function(d) {
return d === this.selected_link;
}).bind(this))
.style('marker-start', (function(d) {
if(d.left && !this.undirected) {
return 'url(#start-arrow)';
}
return '';
}).bind(this))
.style('marker-end', (function(d) {
if(d.right && !this.undirected) {
return 'url(#end-arrow)';
}
return '';
}).bind(this))
.on('mousedown', (function(d) {
// Select link
this.mousedown_link = d;
// If edge was selected with shift key, call the openEdge handler and return.
if (d3.event.shiftKey) {
this.onOpenEdge({ event: d3.event, link: d, editor: this });
return;
}
if (this.mousedown_link === this.selected_link) {
this.selected_link = null;
} else {
this.selected_link = this.mousedown_link;
}
this.selected_node = null;
this.restartGraph();
}).bind(this));
// Add edge value labels for the new edges.
// Note that two tspans are required for
// left and right links (represented by the same 'link' object)
var textPaths = this.pathLabels.enter()
.append('svg:text')
.append('svg:textPath')
.attr('xlink:href', function(d) { return '#' + d.id })
.attr('startOffset', '35%');
textPaths.append('tspan')
.attr('dy', -6)
.attr('data-orientation', 'right')
textPaths.append('tspan')
.attr('dy', 20)
.attr('x', 5)
.attr('data-orientation', 'left')
// Update the tspans with the edge value
this.pathLabels.selectAll('tspan').text(function(d) {
return $(this).attr('data-orientation') === 'right'
? ( d.right ? d.rightValue : null )
: ( d.left ? d.leftValue : null );
});
// Remove old links.
this.path.exit().remove();
this.pathLabels.exit().remove();
}
/*
* Adds new nodes to the graph and binds mouse events.
* Assumes that the data for this.circle is already set by the caller.
* Creates 'circle' elements for each new node in this.nodes
*/
Editor.prototype.addNodes = function() {
// Adds new nodes.
// The enter() call appends a 'g' element for each node in this.nodes.
// that is not present in this.circle already.
var g = this.circle.enter().append('svg:g');
g.attr('class', 'node-container')
.append('svg:circle')
.attr('class', 'node')
.attr('r', (function(d) {
return getRadius(d);
}).bind(this))
.style('fill', this.defaultColor)
.style('stroke', '#000000')
.classed('reflexive', function(d) { return d.reflexive; })
.on('mouseover', (function(d) {
if (!this.mousedown_node || d === this.mousedown_node) {
return;
}
// Enlarge target node.
d3.select(d3.event.target).attr('transform', 'scale(1.1)');
}).bind(this))
.on('mouseout', (function(d) {
if (!this.mousedown_node || d === this.mousedown_node) {
return;
}
// Unenlarge target node.
d3.select(d3.event.target).attr('transform', '');
}).bind(this))
.on('mousedown', (function(d) {
if (d3.event.shiftKey || this.readonly) {
return;
}
// Select node.
this.mousedown_node = d;
if (this.mousedown_node === this.selected_node) {
this.selected_node = null;
} else {
this.selected_node = this.mousedown_node;
}
this.selected_link = null;
// Reposition drag line.
this.drag_line
.style('marker-end', 'url(#end-arrow)')
.classed('hidden', false)
.attr('d', 'M' + this.mousedown_node.x + ',' + this.mousedown_node.y + 'L' + this.mousedown_node.x + ',' + this.mousedown_node.y);
this.restartGraph();
}).bind(this))
.on('mouseup', (function(d) {
if (!this.mousedown_node) {
return;
}
this.drag_line
.classed('hidden', true)
.style('marker-end', '');
// Check for drag-to-self.
this.mouseup_node = d;
if (this.mouseup_node === this.mousedown_node) {
this.resetMouseVars();
return;
}
// Unenlarge target node to default size.
d3.select(d3.event.target).attr('transform', '');
// Add link to graph (update if exists).
var newLink = this.addEdge(this.mousedown_node.id, this.mouseup_node.id);
this.selected_link = newLink;
this.restartGraph();
}).bind(this))
.on('dblclick', (function(d) {
if (this.onOpenNode) {
this.onOpenNode({ event: d3.event, node: d , editor: this });
this.restartGraph();
}
}).bind(this));
// Show node IDs
g.append('svg:text')
.attr('x', 0)
.attr('y', 4)
.attr('class', 'tid')
}
/*
* Updates existing nodes and adds new nodes.
*/
Editor.prototype.restartNodes = function() {
// Set the circle group's data to this.nodes.
// Note that nodes are identified by id, not their index in the array.
this.circle = this.circle.data(this.nodes, function(d) { return d.id; });
// NOTE: addNodes must only be called after .data is set to the latest
// this.nodes. This is done at the beginning of this method.
this.addNodes();
// Update existing nodes (reflexive & selected visual states)
this.circle.selectAll('circle')
.style('fill', function(d) { return d.color; })
.classed('reflexive', function(d) { return d.reflexive; })
.classed('selected', (function(d) { return d === this.selected_node }).bind(this))
.attr('r', function(d) { return getRadius(d); });
// If node is not enabled, set its opacity to 0.2
this.circle.transition().style('opacity', function(d) { return d.enabled === true ? 1 : 0.2; });
// Update node IDs
var el = this.circle.selectAll('text').text('');
el.append('tspan')
.text(function(d) {
return d.id;
})
.attr('x', 0)
.attr('dy', function(d) {
return d.attrs != null && d.attrs.trim() != '' ? '-8' : '0 ';
})
.attr('class', 'id');
// Node value (if present) is added/updated here
el.append('tspan')
.text(function(d) {
return getAttrForDisplay(d.attrs);
})
.attr('x', 0)
.attr('dy', function(d) {
return d.attrs != null && d.attrs.trim() != '' ? '18' : '0';
})
.attr('class', 'vval');
// remove old nodes
this.circle.exit().remove();
}
/*
* Restarts (refreshes, just using 'restart' for consistency) the aggregators.
*/
Editor.prototype.restartAggregators = function() {
this.aggregatorsContainer.attr('transform', 'translate(' + (this.width - 250) + ', 25)')
this.aggregatorsContainer.transition().style('opacity', Utils.count(this.aggregators) > 0 ? 1 : 0);
// Remove all values
this.globs = this.globs.data([]);
this.globs.exit().remove();
// Convert JSON to array of 2-length arrays for d3
var data = $.map(this.aggregators, function(value, key) { return [[key, value]]; });
// Set new values
this.globs = this.globs.data(data);
this.globs.enter().append('tspan').classed('editor-aggregators-value', true)
.attr('dy', '2.0em')
.attr('x', 0)
.text(function(d) { return "{0} -> {1}".format(d[0], d[1]); });
}
/*
* Restarts the table with the latest currentScenario.
*/
Editor.prototype.restartTable = function() {
// Remove all rows of the table and add again.
this.dataTable.rows().remove();
// Modify the scenario object to suit dataTables format
for (var nodeId in this.currentScenario) {
var dataRow = {};
var scenario = this.currentScenario[nodeId];
dataRow.vertexId = nodeId;
dataRow.vertexValue = scenario.vertexValue ? scenario.vertexValue : '-',
dataRow.outgoingMessages = {
numOutgoingMessages : Utils.count(scenario.outgoingMessages),
data : scenario.outgoingMessages
},
dataRow.incomingMessages = {
numIncomingMessages : Utils.count(scenario.incomingMessages),
data : scenario.incomingMessages
},
dataRow.neighbors = {
numNeighbors : Utils.count(scenario.neighbors),
data : scenario.neighbors
}
this.dataTable.row.add(dataRow).draw();
}
// Bind click event for rows.
$('#editor-tablet-table td.tablet-details-control').click((function(event) {
var tr = $(event.target).parents('tr');
var row = this.dataTable.row(tr);
if ( row.child.isShown()) {
// This row is already open - close it.
row.child.hide();
tr.removeClass('shown');
} else {
// Open this row.
var rowData = row.data();
var rowHtml = this.getRowDetailsHtml(rowData);
var dataContainer = rowHtml.dataContainer;
row.child(rowHtml.outerContainer).show();
// Now attach events to the tabs
// NOTE: MUST attach events after the row.child call.
$(rowHtml.navContainer).on('click', 'li a', (function(event) {
// Check which tab was clicked and populate data accordingly.
var dataContainer = rowHtml.dataContainer;
var tabName = $(event.target).data('name');
// Clear the data container
$(dataContainer).empty();
if (tabName === 'outgoingMessages') {
var mainTable = $('<table><thead><th>Receiver ID</th><th>Outgoing Message</th></thead></table>')
.attr('class', 'table')
.appendTo(dataContainer)
var outgoingMessages = rowData.outgoingMessages.data;
for (var receiverId in outgoingMessages) {
$(mainTable).append("<tr><td>{0}</td><td>{1}</td></tr>".format(receiverId, outgoingMessages[receiverId]));
}
$(mainTable).DataTable();
} else if (tabName === 'incomingMessages') {
var mainTable = $('<table><thead><th>Incoming Message</th></thead></table>')
.attr('class', 'table')
.appendTo(dataContainer);
var incomingMessages = rowData.incomingMessages.data;
for (var i = 0; i < incomingMessages.length; i++) {
$(mainTable).append("<tr><td>{0}</td></tr>".format(incomingMessages[i]));
}
} else if (tabName === 'neighbors') {
var mainTable = $('<table><thead><th>Neighbor ID</th><th>Edge Value</th></thead></table>')
.attr('class', 'table')
.appendTo(dataContainer)
var neighbors = rowData.neighbors.data;
console.log(neighbors);
for (var i = 0 ; i < neighbors.length; i++) {
$(mainTable).append("<tr><td>{0}</td><td>{1}</td></tr>"
.format(neighbors[i].neighborId, neighbors[i].edgeValue ? neighbors[i].edgeValue : '-'));
}
$(mainTable).DataTable();
}
}).bind(this));
// Click the first tab of the navContainer - ul>li>a
$(rowHtml.navContainer).children(':first').children(':first').click();
tr.addClass('shown');
}
}).bind(this));
}
/*
* Returns the index of the node with the given id in the nodes array.
* @param {string} id - The identifier of the node.
*/
Editor.prototype.getNodeIndex = function(id) {
return this.nodes.map(function(e) { return e.id }).indexOf(id);
}
/*
* Returns the node object with the given id, null if node is not present.
* @param {string} id - The identifier of the node.
*/
Editor.prototype.getNodeWithId = function(id) {
var index = this.getNodeIndex(id);
return index >= 0 ? this.nodes[index] : null;
}
/*
* Returns the link objeccts with the given id as the source.
* Note that source here implies that all these links are outgoing from this node.
* @param {string} sourceId - The identifier of the source node.
*/
Editor.prototype.getEdgesWithSourceId = function(sourceId) {
var edges = [];
$.each(this.links, (function(i, link) {
$.each(this.getEdges(link), function(index, edge) {
if (edge.source.id === sourceId) {
edges.push(edge);
}
});
}).bind(this));
return edges;
}
/*
* Returns true if the node with the given ID is present in the graph.
* @param {string} id - The identifier of the node.
*/
Editor.prototype.containsNode = function(id) {
return this.getNodeIndex(id) >= 0;
}
/*
* Removes the links associated with a given node.
* Used when a node is deleted.
*/
Editor.prototype.spliceLinksForNode = function(node) {
var toSplice = this.links.filter(function(l) {
return (l.source === node || l.target === node);
});
toSplice.map((function(l) {
this.links.splice(this.links.indexOf(l), 1);
}).bind(this));
}
/*
* Puts the graph in readonly state.
*/
Editor.prototype.setReadonly = function(_readonly) {
this.readonly = _readonly;
if (this.readonly) {
// Support zooming in readonly mode.
this.zoomHolder.call(d3.behavior.zoom().on('zoom', this.redraw.bind(this)))
// Remove double click zoom since we display node attrs on double click.
this.zoomHolder.on('dblclick.zoom', null);
} else {
// Remove zooming in edit mode.
this.zoomHolder.on('.zoom', null);
this.zoomSvg([0,0], 1);
}
}