Source: js/controls.js

'use strict';

/**
 * Return true if object is a [link]{@link Link}.
 * @private
 * @param   {Object} data
 * @returns {boolean}
 */
ge.GraphEditor.prototype.isLinkData = function isLinkData(obj) {
	return obj.source !== undefined;
};

/**
 * Get node/link SVG element.
 * @param   {?Reference}   el
 * @returns {?D3Selection}
 */
ge.GraphEditor.prototype.getElement = function getElement(el) {
	if(el === null) {
		return null;
	}
	if(el.select) {
		return el;
	}
	if(el instanceof SVGElement) {
		return d3.select(el);
	}
	if(typeof el !== 'object') {
		el = this.nodeById[el] || this.linkById[el];
	}
	if(!el) {
		return null;
	}
	return this.svg.select(el.selector);
};

/**
 * Get node/link data.
 * @param   {?Reference}   el
 * @returns {?(Node|Link)}
 */
ge.GraphEditor.prototype.getData = function getData(el) {
	if(el === null) {
		return null;
	}
	if(el.select) {
		return el.datum();
	}
	if(el instanceof SVGElement) {
		return d3.select(el).datum();
	}
	if(typeof el !== 'object') {
		return this.nodeById[el] || this.linkById[el];
	}
	return el;
};

/**
 * Get unused node ID.
 * @private
 * @returns {ID}
 */
ge.GraphEditor.prototype.nodeId = function nodeId() {
	var id = 1 + d3.max(this.data.nodes, function(d) { return d.id; });
	if(isNaN(id)) {
		id = 0;
	}
	return id;
};

/**
 * Get link ID.
 * @private
 * @param {ge.Node} source  Link source.
 * @param {ge.Node} target  Link target.
 * @returns {ID}
 */
ge.GraphEditor.prototype.linkId = function linkId(source, target) {
	var s = source.id;
	var t = target.id;
	if(!this.options.directed && s > t) {
		var tmp = s;
		s = t;
		t = tmp;
	}
	var id = s + '-' + t;
	return id;
};

/**
 * Add a node if it does not exist.
 * @param   {ImportNodeData} node                Node data.
 * @param   {boolean}        [skipUpdate=false]  Skip DOM update.
 * @returns {?ge.Node}                           Added node.
 */
ge.GraphEditor.prototype.addNode = function addNode(node, skipUpdate) {
	node = new ge.Node(this, node);
	if(this.nodeById[node.id]) {
		return null;
	}
	this.nodeById[node.id] = node;
	this.data.nodes.push(node);
	if(!skipUpdate) {
		this.update();
	}
	return node;
};

/**
 * Add a link if it does not exist.
 * @param   {ImportLinkData} link                Link data.
 * @param   {boolean}        [skipUpdate=false]  Skip DOM update.
 * @returns {?ge.Link}                           Added link.
 */
ge.GraphEditor.prototype.addLink = function addLink(link, skipUpdate) {
	link = new ge.Link(this, link);
	if(this.linkById[link.id]) {
		return null;
	}
	this.linkById[link.id] = link;
	this.data.links.push(link);
	if(!skipUpdate) {
		this.update();
	}
	return link;
};

/**
 * Add multiple nodes.
 * @param   {Array<ImportNodeData>} nodes               Node data.
 * @param   {boolean}               [skipUpdate=false]  Skip DOM update.
 */
ge.GraphEditor.prototype.addNodes = function addNodes(nodes, skipUpdate) {
	for(var i = 0; i < nodes.length; ++i) {
		this.addNode(nodes[i], true);
	}
	if(!skipUpdate) {
		this.update();
	}
};

/**
 * Add multiple links.
 * @param   {Array<ImportLinkData>} links               Link data.
 * @param   {boolean}               [skipUpdate=false]  Skip DOM update.
 * @returns {Array<Link>}                               Added links.
 */
ge.GraphEditor.prototype.addLinks = function addLinks(links, skipUpdate) {
	for(var i = 0; i < links.length; ++i) {
		this.addLink(links[i], true);
	}
	if(!skipUpdate) {
		this.update();
	}
};

/**
 * Add one or multiple nodes/links.
 * @param   {ImportNodeData|ImportLinkData|Array<(ImportNodeData|ImportLinkData)>} data  Data.
 * @param   {boolean} [skipUpdate=false]  Skip DOM update.
 */
ge.GraphEditor.prototype.add = function add(data, skipUpdate) {
	var self = this;
	if(Array.isArray(data)) {
		data.forEach(function(d) {
			if(self.isLinkData(d)) {
				self.addLink(d, true);
			}
			else {
				self.addNode(d, true);
			}
		});
		if(!skipUpdate) {
			self.update();
		}
	}
	else {
		data = self.getData(data);
		if(self.isLinkData(data)) {
			self.addLink(data, skipUpdate);
		}
		else {
			self.addNode(data, skipUpdate);
		}
	}
};

/**
 * Remove a link by index.
 * @private
 * @param {number} idx  Link index.
 */
ge.GraphEditor.prototype._removeLink = function _removeLink(idx) {
	if(this.state.selectedLink === this.data.links[idx]) {
		this.selectLink(null);
	}
	delete this.linkById[this.data.links[idx].id];
	this.data.links.splice(idx, 1);
};

/**
 * Remove a link.
 * @param   {Reference} data                Link.
 * @param   {boolean}   [skipUpdate=false]  Skip DOM update.
 * @returns {boolean}                       False if link does not exist.
 */
ge.GraphEditor.prototype.removeLink = function removeLink(data, skipUpdate) {
	if(!(data = this.getData(data))) {
		return false;
	}

	var i = this.data.links.indexOf(data);
	if(i < 0) {
		console.error('removeLink', data, 'indexOf() < 0');
		return false;
	}

	this._removeLink(i);

	if(!skipUpdate) {
		this.update();
	}
	return true;
};

/**
 * Remove a node.
 * @param   {Reference} data                Node.
 * @param   {boolean}   [skipUpdate=false]  Skip DOM update.
 * @returns {boolean}                       False if node does not exist.
 */
ge.GraphEditor.prototype.removeNode = function removeNode(data, skipUpdate) {
	if(!(data = this.getData(data))) {
		return false;
	}

	var i = this.data.nodes.indexOf(data);
	if(i < 0) {
		console.error('removeNode', data, ' indexOf() < 0');
		return false;
	}

	if(this.state.selectedNode === data) {
		this.selectNode(null);
	}

	this.data.nodes.splice(i, 1);
	delete this.nodeById[data.id];

	i = 0;
	var links = this.data.links;
	while(i < links.length) {
		if(links[i].source === data || links[i].target === data) {
			this._removeLink(i);
		}
		else {
			++i;
		}
	}

	if(!skipUpdate) {
		this.update();
	}
	return true;
};

/**
 * Remove a node or a link.
 * @private
 * @param   {Reference} data                Reference.
 * @param   {boolean}   [skipUpdate=false]  Skip DOM update.
 * @returns {boolean}                       False if object does not exist.
 */
ge.GraphEditor.prototype._remove = function _remove(data, skipUpdate) {
	data = this.getData(data);
	if(this.isLinkData(data)) {
		return this.removeLink(data, skipUpdate);
	}
	return this.removeNode(data, skipUpdate);
};

/**
 * Remove one or multiple nodes/links.
 * @param   {(Reference|Array<Reference>)} data                References.
 * @param   {boolean}                      [skipUpdate=false]  Skip DOM update.
 */
ge.GraphEditor.prototype.remove = function remove(data, skipUpdate) {
	var self = this;

	if(Array.isArray(data)) {
		data.forEach(function(d) {
			self._remove(d, true);
		});

		if(!skipUpdate) {
			self.update();
		}
		return;
	}

	return self._remove(data, skipUpdate);
};

/**
 * Return selected node.
 * @returns {?Node}
 *//**
 * Select a node.
 * @param   {?Reference}   [node]          Node to select.
 * @param   {boolean}      [update=false]  Update DOM if the node is already selected.
 * @returns {ge.GraphEditor}
 */
ge.GraphEditor.prototype.selectNode = function selectNode(node, update) {
	if(node === undefined) {
		return this.state.selectedNode;
	}

	node = this.getData(node);

	if(this.state.selectedNode === node && !update) {
		return this;
	}

	var cls = this.options.css.selection.node;

	this.state.selectedNode = node;

	if(!node) {
		this.svg.classed(cls, false);
		this.nodes.classed(cls, false);
		this.links.classed(cls, false);
		return this;
	}

	var selectedNode = function(d) { return d === node; };
	var selectedLink;

	if(this.options.directed) {
		selectedLink = function(d) { return d.source === node; };
	}
	else {
		selectedLink = function(d) {
			return d.source === node || d.target === node;
		};
	}

	this.svg.classed(cls, true);
	this.nodes.classed(cls, selectedNode);
	this.links.classed(cls, selectedLink);

	this.nodes.filter(selectedNode).raise();
	this.links.filter(selectedLink).raise();

	return this;
};

/**
 * Return selected link.
 * @returns {?Link}
 *//**
 * Selected a link.
 * @param   {?Reference}   [link]          Link to select.
 * @param   {boolean}      [update=false]  Update DOM if the link is already selected.
 * @returns {ge.GraphEditor}
 */
ge.GraphEditor.prototype.selectLink = function selectLink(link, update) {
	if(link === undefined) {
		return this.state.selectedLink;
	}

	link = this.getData(link);

	if(this.state.selectedLink === link && !update) {
		return this;
	}

	var cls = this.options.css.selection.link;

	this.state.selectedLink = link;

	if(!link) {
		this.svg.classed(cls, false);
		this.links.classed(cls, false);
		return this;
	}

	var selectedLink = function(d) { return d === link; };

	this.svg.classed(cls, true);
	this.links.classed(cls, selectedLink);
	this.links.filter(selectedLink).raise();

	return this;
};

/**
 * Return selected node/link.
 * @returns {Selection}
 *//**
 * Select a node or a link.
 * @param   {?Reference}   [data]          Node or link to select.
 * @param   {boolean}      [update=false]  Update DOM if the node/link is already selected.
 * @returns {ge.GraphEditor}
 */
ge.GraphEditor.prototype.select = function select(data, update) {
	if(data === undefined) {
		return {
			node: this.state.selectedNode,
			link: this.state.selectedLink
		};
	}
	if(data === null) {
		return this.selectNode(null)
			.selectLink(null);
	}
	if(!(data = this.getData(data))) {
		return this;
	}
	if(this.isLinkData(data)) {
		return this.selectLink(data, update);
	}
	return this.selectNode(data, update);
};

/**
 * Start force simulation.
 * @param   {boolean} [stopOnEnd=this.options.simulation.stop]  Stop the simulation when it converges.
 * @fires   simulation-start
 * @returns {ge.GraphEditor}
 */
ge.GraphEditor.prototype.startSimulation = function startSimulation(stopOnEnd) {
	var self = this;

	if(stopOnEnd === undefined) {
		stopOnEnd = this.options.simulation.stop;
	}

	if(this.state.simulationStarted) {
		if(stopOnEnd) {
			this.state.simulation.on(
				'end',
				function() { self.stopSimulation(); }
			);
		}
		else {
			this.state.simulation.on('end', null);
		}
		return this;
	}

	this.state.simulationStarted = true;

	this.state.simulation = this.options.simulation.create.call(
		this,
		this.state.simulation,
		this.data.nodes,
		this.data.links
	);

	var step = 0;

	this.state.simulation.on(
		'tick',
		function() {
			if(++step >= self.options.simulation.step) {
				step = 0;
				self.update(true);
			}
		}
	);

	if(stopOnEnd) {
		this.state.simulation.on(
			'end',
			function() { self.stopSimulation(); }
		);
	}
	else {
		this.state.simulation.on('end', null);
	}

	//this.data.nodes.forEach(function(d) { d.fx = d.fy = null; });
	this.state.simulation.alpha(1).restart();
	this.dispatch.call('simulation-start', this);

	return this;
};

/**
 * Stop force simulation.
 * @fires   simulation-stop
 * @returns {ge.GraphEditor}
 */
ge.GraphEditor.prototype.stopSimulation = function stopSimulation() {
	if(!this.state.simulationStarted) {
		return this;
	}

	this.state.simulation.stop();
	this.state.simulationStarted = false;
	this.dispatch.call('simulation-stop', this);

	return this;
};

/**
 * Set or return force simulation state.
 * @param   {boolean|string} [on]  state | 'start' | 'stop' | 'restart' | 'toggle'
 * @fires   simulation-start
 * @fires   simulation-stop
 * @returns {(boolean|ge.GraphEditor)}
 */
ge.GraphEditor.prototype.simulation = function simulation(on) {
	if(on === undefined) {
		return this.state.simulationStarted;
	}
	if(on === 'restart') {
		if(!this.state.simulationStarted) {
			return this.startSimulation();
		}
		this.state.simulation.alpha(1).restart();
		return this;
	}
	if(on === 'toggle') {
		on = !this.state.simulationStarted;
	}
	if(on && on !== 'stop') {
		return this.startSimulation();
	}
	return this.stopSimulation();
};

/**
 * Return true if 'drag to link nodes' is enabled.
 * @returns {boolean}
 *//**
 * Set 'drag to link nodes' state.
 * @param   {boolean|string}           [on]  state | 'toggle'
 * @returns {ge.GraphEditor}
 */
ge.GraphEditor.prototype.dragToLink = function dragToLink(on) {
	if(on === undefined) {
		return this.state.dragToLink;
	}
	if(on === 'toggle') {
		on = !this.state.dragToLink;
	}
	this.state.dragToLink = on;
	return this;
};


/**
 * Clear graph DOM.
 * @param   {boolean}        [clearData=true]  Clear graph data.
 * @fires   simulation-stop
 * @returns {ge.GraphEditor}
 */
ge.GraphEditor.prototype.clear = function clear(clearData) {
	clearData = clearData || (clearData === undefined);

	this.simulation(false);

	this.nodes = this.nodes.data([], ge.id);
	this.nodes.exit().remove();

	this.links = this.links.data([], ge.id);
	this.links.exit().remove();

	this.defs = this.defs.data([], ge.id);
	this.defs.exit().remove();

	if(clearData) {
		this.nodeById = {};
		this.linkById = {};
		this.data = {
			nodes: [],
			links: []
		};
		this.initState();
	}

	return this;
};


/**
 * Regenerate graph DOM.
 * @fires   simulation-stop
 * @returns {ge.GraphEditor}
 */
ge.GraphEditor.prototype.redraw = function redraw() {
	return this.clear(false).update();
};

/**
 * Destroy the graph.
 * @fires   simulation-stop
 * @returns {ge.GraphEditor}
 */
ge.GraphEditor.prototype.destroy = function destroy() {
	var cls = this.options.css;

	this.clear();

	this.svg
		.classed(cls.graph, false)
		.classed(cls.digraph, false)
		.classed(cls.selection.node, false)
		.classed(cls.selection.link, false)
		.on('.zoom', null)
		.html('');

	window.removeEventListener('resize', this.onresize);

	return this;
};

/**
 * Convert to JSON.
 * @returns {ExportGraphData}
 */
ge.GraphEditor.prototype.toJson = function toJson() {
	var self = this;
	return {
		nodes: this.data.nodes.map(function(node) {
			return node.toJson(self, self.bbox[0][0], self.bbox[0][1]);
		}),
		links: this.data.links.map(function(link) {
			return link.toJson(self);
		})
	};
};