vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

858 lines
23 KiB

var Node = require('./components/Node').default;
var Edge = require('./components/Edge').default;
let util = require('../../util');
/**
* The handler for selections
*/
class SelectionHandler {
/**
* @param {Object} body
* @param {Canvas} canvas
*/
constructor(body, canvas) {
this.body = body;
this.canvas = canvas;
this.selectionObj = {nodes: [], edges: []};
this.hoverObj = {nodes:{},edges:{}};
this.options = {};
this.defaultOptions = {
multiselect: false,
selectable: true,
selectConnectedEdges: true,
hoverConnectedEdges: true
};
util.extend(this.options, this.defaultOptions);
this.body.emitter.on("_dataChanged", () => {
this.updateSelection()
});
}
/**
*
* @param {Object} [options]
*/
setOptions(options) {
if (options !== undefined) {
let fields = ['multiselect','hoverConnectedEdges','selectable','selectConnectedEdges'];
util.selectiveDeepExtend(fields,this.options, options);
}
}
/**
* handles the selection part of the tap;
*
* @param {{x: number, y: number}} pointer
* @returns {boolean}
*/
selectOnPoint(pointer) {
let selected = false;
if (this.options.selectable === true) {
let obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);
// unselect after getting the objects in order to restore width and height.
this.unselectAll();
if (obj !== undefined) {
selected = this.selectObject(obj);
}
this.body.emitter.emit("_requestRedraw");
}
return selected;
}
/**
*
* @param {{x: number, y: number}} pointer
* @returns {boolean}
*/
selectAdditionalOnPoint(pointer) {
let selectionChanged = false;
if (this.options.selectable === true) {
let obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);
if (obj !== undefined) {
selectionChanged = true;
if (obj.isSelected() === true) {
this.deselectObject(obj);
}
else {
this.selectObject(obj);
}
this.body.emitter.emit("_requestRedraw");
}
}
return selectionChanged;
}
/**
* Create an object containing the standard fields for an event.
*
* @param {Event} event
* @param {{x: number, y: number}} pointer Object with the x and y screen coordinates of the mouse
* @returns {{}}
* @private
*/
_initBaseEvent(event, pointer) {
let properties = {};
properties['pointer'] = {
DOM: {x: pointer.x, y: pointer.y},
canvas: this.canvas.DOMtoCanvas(pointer)
};
properties['event'] = event;
return properties;
}
/**
* Generate an event which the user can catch.
*
* This adds some extra data to the event with respect to cursor position and
* selected nodes and edges.
*
* @param {string} eventType Name of event to send
* @param {Event} event
* @param {{x: number, y: number}} pointer Object with the x and y screen coordinates of the mouse
* @param {Object|undefined} oldSelection If present, selection state before event occured
* @param {boolean|undefined} [emptySelection=false] Indicate if selection data should be passed
*/
_generateClickEvent(eventType, event, pointer, oldSelection, emptySelection = false) {
let properties = this._initBaseEvent(event, pointer);
if (emptySelection === true) {
properties.nodes = [];
properties.edges = [];
}
else {
let tmp = this.getSelection();
properties.nodes = tmp.nodes;
properties.edges = tmp.edges;
}
if (oldSelection !== undefined) {
properties['previousSelection'] = oldSelection;
}
if (eventType == 'click') {
// For the time being, restrict this functionality to
// just the click event.
properties.items = this.getClickedItems(pointer);
}
this.body.emitter.emit(eventType, properties);
}
/**
*
* @param {Object} obj
* @param {boolean} [highlightEdges=this.options.selectConnectedEdges]
* @returns {boolean}
*/
selectObject(obj, highlightEdges = this.options.selectConnectedEdges) {
if (obj !== undefined) {
if (obj instanceof Node) {
if (highlightEdges === true) {
this._selectConnectedEdges(obj);
}
}
obj.select();
this._addToSelection(obj);
return true;
}
return false;
}
/**
*
* @param {Object} obj
*/
deselectObject(obj) {
if (obj.isSelected() === true) {
obj.selected = false;
this._removeFromSelection(obj);
}
}
/**
* retrieve all nodes overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {number[]} An array with id's of the overlapping nodes
* @private
*/
_getAllNodesOverlappingWith(object) {
let overlappingNodes = [];
let nodes = this.body.nodes;
for (let i = 0; i < this.body.nodeIndices.length; i++) {
let nodeId = this.body.nodeIndices[i];
if (nodes[nodeId].isOverlappingWith(object)) {
overlappingNodes.push(nodeId);
}
}
return overlappingNodes;
}
/**
* Return a position object in canvasspace from a single point in screenspace
*
* @param {{x: number, y: number}} pointer
* @returns {{left: number, top: number, right: number, bottom: number}}
* @private
*/
_pointerToPositionObject(pointer) {
let canvasPos = this.canvas.DOMtoCanvas(pointer);
return {
left: canvasPos.x - 1,
top: canvasPos.y + 1,
right: canvasPos.x + 1,
bottom: canvasPos.y - 1
};
}
/**
* Get the top node at the passed point (like a click)
*
* @param {{x: number, y: number}} pointer
* @param {boolean} [returnNode=true]
* @return {Node | undefined} node
*/
getNodeAt(pointer, returnNode = true) {
// we first check if this is an navigation controls element
let positionObject = this._pointerToPositionObject(pointer);
let overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
// if there are overlapping nodes, select the last one, this is the
// one which is drawn on top of the others
if (overlappingNodes.length > 0) {
if (returnNode === true) {
return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]];
}
else {
return overlappingNodes[overlappingNodes.length - 1];
}
}
else {
return undefined;
}
}
/**
* retrieve all edges overlapping with given object, selector is around center
* @param {Object} object An object with parameters left, top, right, bottom
* @param {number[]} overlappingEdges An array with id's of the overlapping nodes
* @private
*/
_getEdgesOverlappingWith(object, overlappingEdges) {
let edges = this.body.edges;
for (let i = 0; i < this.body.edgeIndices.length; i++) {
let edgeId = this.body.edgeIndices[i];
if (edges[edgeId].isOverlappingWith(object)) {
overlappingEdges.push(edgeId);
}
}
}
/**
* retrieve all nodes overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {number[]} An array with id's of the overlapping nodes
* @private
*/
_getAllEdgesOverlappingWith(object) {
let overlappingEdges = [];
this._getEdgesOverlappingWith(object,overlappingEdges);
return overlappingEdges;
}
/**
* Get the edges nearest to the passed point (like a click)
*
* @param {{x: number, y: number}} pointer
* @param {boolean} [returnEdge=true]
* @return {Edge | undefined} node
*/
getEdgeAt(pointer, returnEdge = true) {
// Iterate over edges, pick closest within 10
var canvasPos = this.canvas.DOMtoCanvas(pointer);
var mindist = 10;
var overlappingEdge = null;
var edges = this.body.edges;
for (var i = 0; i < this.body.edgeIndices.length; i++) {
var edgeId = this.body.edgeIndices[i];
var edge = edges[edgeId];
if (edge.connected) {
var xFrom = edge.from.x;
var yFrom = edge.from.y;
var xTo = edge.to.x;
var yTo = edge.to.y;
var dist = edge.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, canvasPos.x, canvasPos.y);
if(dist < mindist){
overlappingEdge = edgeId;
mindist = dist;
}
}
}
if (overlappingEdge !== null) {
if (returnEdge === true) {
return this.body.edges[overlappingEdge];
}
else {
return overlappingEdge;
}
}
else {
return undefined;
}
}
/**
* Add object to the selection array.
*
* @param {Object} obj
* @private
*/
_addToSelection(obj) {
if (obj instanceof Node) {
this.selectionObj.nodes[obj.id] = obj;
}
else {
this.selectionObj.edges[obj.id] = obj;
}
}
/**
* Add object to the selection array.
*
* @param {Object} obj
* @private
*/
_addToHover(obj) {
if (obj instanceof Node) {
this.hoverObj.nodes[obj.id] = obj;
}
else {
this.hoverObj.edges[obj.id] = obj;
}
}
/**
* Remove a single option from selection.
*
* @param {Object} obj
* @private
*/
_removeFromSelection(obj) {
if (obj instanceof Node) {
delete this.selectionObj.nodes[obj.id];
this._unselectConnectedEdges(obj);
}
else {
delete this.selectionObj.edges[obj.id];
}
}
/**
* Unselect all. The selectionObj is useful for this.
*/
unselectAll() {
for(let nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
this.selectionObj.nodes[nodeId].unselect();
}
}
for(let edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
this.selectionObj.edges[edgeId].unselect();
}
}
this.selectionObj = {nodes:{},edges:{}};
}
/**
* return the number of selected nodes
*
* @returns {number}
* @private
*/
_getSelectedNodeCount() {
let count = 0;
for (let nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
count += 1;
}
}
return count;
}
/**
* return the selected node
*
* @returns {number}
* @private
*/
_getSelectedNode() {
for (let nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
return this.selectionObj.nodes[nodeId];
}
}
return undefined;
}
/**
* return the selected edge
*
* @returns {number}
* @private
*/
_getSelectedEdge() {
for (let edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
return this.selectionObj.edges[edgeId];
}
}
return undefined;
}
/**
* return the number of selected edges
*
* @returns {number}
* @private
*/
_getSelectedEdgeCount() {
let count = 0;
for (let edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
count += 1;
}
}
return count;
}
/**
* return the number of selected objects.
*
* @returns {number}
* @private
*/
_getSelectedObjectCount() {
let count = 0;
for(let nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
count += 1;
}
}
for(let edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
count += 1;
}
}
return count;
}
/**
* Check if anything is selected
*
* @returns {boolean}
* @private
*/
_selectionIsEmpty() {
for(let nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
return false;
}
}
for(let edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
return false;
}
}
return true;
}
/**
* check if one of the selected nodes is a cluster.
*
* @returns {boolean}
* @private
*/
_clusterInSelection() {
for(let nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
return true;
}
}
}
return false;
}
/**
* select the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
_selectConnectedEdges(node) {
for (let i = 0; i < node.edges.length; i++) {
let edge = node.edges[i];
edge.select();
this._addToSelection(edge);
}
}
/**
* select the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
_hoverConnectedEdges(node) {
for (let i = 0; i < node.edges.length; i++) {
let edge = node.edges[i];
edge.hover = true;
this._addToHover(edge);
}
}
/**
* unselect the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
_unselectConnectedEdges(node) {
for (let i = 0; i < node.edges.length; i++) {
let edge = node.edges[i];
edge.unselect();
this._removeFromSelection(edge);
}
}
/**
* Remove the highlight from a node or edge, in response to mouse movement
*
* @param {Event} event
* @param {{x: number, y: number}} pointer object with the x and y screen coordinates of the mouse
* @param {Node|vis.Edge} object
* @private
*/
emitBlurEvent(event, pointer, object) {
let properties = this._initBaseEvent(event, pointer);
if (object.hover === true) {
object.hover = false;
if (object instanceof Node) {
properties.node = object.id;
this.body.emitter.emit("blurNode", properties);
}
else {
properties.edge = object.id;
this.body.emitter.emit("blurEdge", properties);
}
}
}
/**
* Create the highlight for a node or edge, in response to mouse movement
*
* @param {Event} event
* @param {{x: number, y: number}} pointer object with the x and y screen coordinates of the mouse
* @param {Node|vis.Edge} object
* @returns {boolean} hoverChanged
* @private
*/
emitHoverEvent(event, pointer, object) {
let properties = this._initBaseEvent(event, pointer);
let hoverChanged = false;
if (object.hover === false) {
object.hover = true;
this._addToHover(object);
hoverChanged = true;
if (object instanceof Node) {
properties.node = object.id;
this.body.emitter.emit("hoverNode", properties);
}
else {
properties.edge = object.id;
this.body.emitter.emit("hoverEdge", properties);
}
}
return hoverChanged;
}
/**
* Perform actions in response to a mouse movement.
*
* @param {Event} event
* @param {{x: number, y: number}} pointer | object with the x and y screen coordinates of the mouse
*/
hoverObject(event, pointer) {
let object = this.getNodeAt(pointer);
if (object === undefined) {
object = this.getEdgeAt(pointer);
}
let hoverChanged = false;
// remove all node hover highlights
for (let nodeId in this.hoverObj.nodes) {
if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
if (object === undefined || (object instanceof Node && object.id != nodeId) || object instanceof Edge) {
this.emitBlurEvent(event, pointer, this.hoverObj.nodes[nodeId]);
delete this.hoverObj.nodes[nodeId];
hoverChanged = true;
}
}
}
// removing all edge hover highlights
for (let edgeId in this.hoverObj.edges) {
if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
// if the hover has been changed here it means that the node has been hovered over or off
// we then do not use the emitBlurEvent method here.
if (hoverChanged === true) {
this.hoverObj.edges[edgeId].hover = false;
delete this.hoverObj.edges[edgeId];
}
// if the blur remains the same and the object is undefined (mouse off) or another
// edge has been hovered, or another node has been hovered we blur the edge.
else if (object === undefined || (object instanceof Edge && object.id != edgeId) || (object instanceof Node && !object.hover)) {
this.emitBlurEvent(event, pointer, this.hoverObj.edges[edgeId]);
delete this.hoverObj.edges[edgeId];
hoverChanged = true;
}
}
}
if (object !== undefined) {
hoverChanged = hoverChanged || this.emitHoverEvent(event, pointer, object);
if (object instanceof Node && this.options.hoverConnectedEdges === true) {
this._hoverConnectedEdges(object);
}
}
if (hoverChanged === true) {
this.body.emitter.emit('_requestRedraw');
}
}
/**
*
* retrieve the currently selected objects
* @return {{nodes: Array.<string>, edges: Array.<string>}} selection
*/
getSelection() {
let nodeIds = this.getSelectedNodes();
let edgeIds = this.getSelectedEdges();
return {nodes:nodeIds, edges:edgeIds};
}
/**
*
* retrieve the currently selected nodes
* @return {string[]} selection An array with the ids of the
* selected nodes.
*/
getSelectedNodes() {
let idArray = [];
if (this.options.selectable === true) {
for (let nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
idArray.push(this.selectionObj.nodes[nodeId].id);
}
}
}
return idArray;
}
/**
*
* retrieve the currently selected edges
* @return {Array} selection An array with the ids of the
* selected nodes.
*/
getSelectedEdges() {
let idArray = [];
if (this.options.selectable === true) {
for (let edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
idArray.push(this.selectionObj.edges[edgeId].id);
}
}
}
return idArray;
}
/**
* Updates the current selection
* @param {{nodes: Array.<string>, edges: Array.<string>}} selection
* @param {Object} options Options
*/
setSelection(selection, options = {}) {
let i, id;
if (!selection || (!selection.nodes && !selection.edges))
throw 'Selection must be an object with nodes and/or edges properties';
// first unselect any selected node, if option is true or undefined
if (options.unselectAll || options.unselectAll === undefined) {
this.unselectAll();
}
if (selection.nodes) {
for (i = 0; i < selection.nodes.length; i++) {
id = selection.nodes[i];
let node = this.body.nodes[id];
if (!node) {
throw new RangeError('Node with id "' + id + '" not found');
}
// don't select edges with it
this.selectObject(node, options.highlightEdges);
}
}
if (selection.edges) {
for (i = 0; i < selection.edges.length; i++) {
id = selection.edges[i];
let edge = this.body.edges[id];
if (!edge) {
throw new RangeError('Edge with id "' + id + '" not found');
}
this.selectObject(edge);
}
}
this.body.emitter.emit('_requestRedraw');
}
/**
* select zero or more nodes with the option to highlight edges
* @param {number[] | string[]} selection An array with the ids of the
* selected nodes.
* @param {boolean} [highlightEdges]
*/
selectNodes(selection, highlightEdges = true) {
if (!selection || (selection.length === undefined))
throw 'Selection must be an array with ids';
this.setSelection({nodes: selection}, {highlightEdges: highlightEdges});
}
/**
* select zero or more edges
* @param {number[] | string[]} selection An array with the ids of the
* selected nodes.
*/
selectEdges(selection) {
if (!selection || (selection.length === undefined))
throw 'Selection must be an array with ids';
this.setSelection({edges: selection});
}
/**
* Validate the selection: remove ids of nodes which no longer exist
* @private
*/
updateSelection() {
for (let nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (!this.body.nodes.hasOwnProperty(nodeId)) {
delete this.selectionObj.nodes[nodeId];
}
}
}
for (let edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
if (!this.body.edges.hasOwnProperty(edgeId)) {
delete this.selectionObj.edges[edgeId];
}
}
}
}
/**
* Determine all the visual elements clicked which are on the given point.
*
* All elements are returned; this includes nodes, edges and their labels.
* The order returned is from highest to lowest, i.e. element 0 of the return
* value is the topmost item clicked on.
*
* The return value consists of an array of the following possible elements:
*
* - `{nodeId:number}` - node with given id clicked on
* - `{nodeId:number, labelId:0}` - label of node with given id clicked on
* - `{edgeId:number}` - edge with given id clicked on
* - `{edge:number, labelId:0}` - label of edge with given id clicked on
*
* ## NOTES
*
* - Currently, there is only one label associated with a node or an edge,
* but this is expected to change somewhere in the future.
* - Since there is no z-indexing yet, it is not really possible to set the nodes and
* edges in the correct order. For the time being, nodes come first.
*
* @param {point} pointer mouse position in screen coordinates
* @returns {Array.<nodeClickItem|nodeLabelClickItem|edgeClickItem|edgeLabelClickItem>}
* @private
*/
getClickedItems(pointer) {
let point = this.canvas.DOMtoCanvas(pointer);
var items = [];
// Note reverse order; we want the topmost clicked items to be first in the array
// Also note that selected nodes are disregarded here; these normally display on top
let nodeIndices = this.body.nodeIndices;
let nodes = this.body.nodes;
for (let i = nodeIndices.length - 1; i >= 0; i--) {
let node = nodes[nodeIndices[i]];
let ret = node.getItemsOnPoint(point);
items.push.apply(items, ret); // Append the return value to the running list.
}
let edgeIndices = this.body.edgeIndices;
let edges = this.body.edges;
for (let i = edgeIndices.length - 1; i >= 0; i--) {
let edge = edges[edgeIndices[i]];
let ret = edge.getItemsOnPoint(point);
items.push.apply(items, ret); // Append the return value to the running list.
}
return items;
}
}
export default SelectionHandler;