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.

623 lines
15 KiB

  1. let Node = require("./components/Node");
  2. let util = require('../../util');
  3. class SelectionHandler {
  4. constructor(body, canvas) {
  5. this.body = body;
  6. this.canvas = canvas;
  7. this.selectionObj = {nodes: [], edges: []};
  8. this.options = {};
  9. this.defaultOptions = {
  10. select: true,
  11. selectConnectedEdges: true
  12. };
  13. util.extend(this.options, this.defaultOptions);
  14. this.body.emitter.on("_dataChanged", () => {
  15. this.updateSelection()
  16. });
  17. }
  18. setOptions(options) {
  19. if (options !== undefined) {
  20. util.deepExtend(this.options, options);
  21. }
  22. }
  23. /**
  24. * handles the selection part of the tap;
  25. *
  26. * @param {Object} pointer
  27. * @private
  28. */
  29. selectOnPoint(pointer) {
  30. let selected = false;
  31. if (this.options.select === true) {
  32. this.unselectAll();
  33. let obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);;
  34. if (obj !== undefined) {
  35. selected = this.selectObject(obj);
  36. }
  37. this.body.emitter.emit("_requestRedraw");
  38. }
  39. return selected;
  40. }
  41. selectAdditionalOnPoint(pointer) {
  42. let selectionChanged = false;
  43. if (this.options.select === true) {
  44. let obj = this.getNodeAt(pointer) || this.getEdgeAt(pointer);;
  45. if (obj !== undefined) {
  46. selectionChanged = true;
  47. if (obj.isSelected() === true) {
  48. this.deselectObject(obj);
  49. }
  50. else {
  51. this.selectObject(obj);
  52. }
  53. this.body.emitter.emit("_requestRedraw");
  54. }
  55. }
  56. return selectionChanged;
  57. }
  58. _generateClickEvent(eventType, pointer, oldSelection) {
  59. let properties = this.getSelection();
  60. properties['pointer'] = {
  61. DOM: {x: pointer.x, y: pointer.y},
  62. canvas: this.canvas.DOMtoCanvas(pointer)
  63. }
  64. if (oldSelection !== undefined) {
  65. properties['previousSelection'] = oldSelection;
  66. }
  67. this.body.emitter.emit(eventType, properties);
  68. }
  69. selectObject(obj, highlightEdges = this.options.selectConnectedEdges) {
  70. if (obj !== undefined) {
  71. if (obj instanceof Node) {
  72. if (highlightEdges === true) {
  73. this._selectConnectedEdges(obj);
  74. }
  75. }
  76. obj.select();
  77. this._addToSelection(obj);
  78. return true;
  79. }
  80. return false;
  81. }
  82. deselectObject(obj) {
  83. if (obj.isSelected() === true) {
  84. obj.selected = false;
  85. this._removeFromSelection(obj);
  86. }
  87. }
  88. /**
  89. * retrieve all nodes overlapping with given object
  90. * @param {Object} object An object with parameters left, top, right, bottom
  91. * @return {Number[]} An array with id's of the overlapping nodes
  92. * @private
  93. */
  94. _getAllNodesOverlappingWith(object) {
  95. let overlappingNodes = [];
  96. let nodes = this.body.nodes;
  97. for (let i = 0; i < this.body.nodeIndices.length; i++) {
  98. let nodeId = this.body.nodeIndices[i];
  99. if (nodes[nodeId].isOverlappingWith(object)) {
  100. overlappingNodes.push(nodeId);
  101. }
  102. }
  103. return overlappingNodes;
  104. }
  105. /**
  106. * Return a position object in canvasspace from a single point in screenspace
  107. *
  108. * @param pointer
  109. * @returns {{left: number, top: number, right: number, bottom: number}}
  110. * @private
  111. */
  112. _pointerToPositionObject(pointer) {
  113. let canvasPos = this.canvas.DOMtoCanvas(pointer);
  114. return {
  115. left: canvasPos.x - 1,
  116. top: canvasPos.y + 1,
  117. right: canvasPos.x + 1,
  118. bottom: canvasPos.y - 1
  119. };
  120. }
  121. /**
  122. * Get the top node at the a specific point (like a click)
  123. *
  124. * @param {{x: Number, y: Number}} pointer
  125. * @return {Node | undefined} node
  126. * @private
  127. */
  128. getNodeAt(pointer, returnNode = true) {
  129. // we first check if this is an navigation controls element
  130. let positionObject = this._pointerToPositionObject(pointer);
  131. let overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
  132. // if there are overlapping nodes, select the last one, this is the
  133. // one which is drawn on top of the others
  134. if (overlappingNodes.length > 0) {
  135. if (returnNode === true) {
  136. return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]];
  137. }
  138. else {
  139. return overlappingNodes[overlappingNodes.length - 1];
  140. }
  141. }
  142. else {
  143. return undefined;
  144. }
  145. }
  146. /**
  147. * retrieve all edges overlapping with given object, selector is around center
  148. * @param {Object} object An object with parameters left, top, right, bottom
  149. * @return {Number[]} An array with id's of the overlapping nodes
  150. * @private
  151. */
  152. _getEdgesOverlappingWith(object, overlappingEdges) {
  153. let edges = this.body.edges;
  154. for (let i = 0; i < this.body.edgeIndices.length; i++) {
  155. let edgeId = this.body.edgeIndices[i];
  156. if (edges[edgeId].isOverlappingWith(object)) {
  157. overlappingEdges.push(edgeId);
  158. }
  159. }
  160. }
  161. /**
  162. * retrieve all nodes overlapping with given object
  163. * @param {Object} object An object with parameters left, top, right, bottom
  164. * @return {Number[]} An array with id's of the overlapping nodes
  165. * @private
  166. */
  167. _getAllEdgesOverlappingWith(object) {
  168. let overlappingEdges = [];
  169. this._getEdgesOverlappingWith(object,overlappingEdges);
  170. return overlappingEdges;
  171. }
  172. /**
  173. * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call
  174. * getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  175. *
  176. * @param pointer
  177. * @returns {undefined}
  178. * @private
  179. */
  180. getEdgeAt(pointer, returnEdge = true) {
  181. let positionObject = this._pointerToPositionObject(pointer);
  182. let overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
  183. if (overlappingEdges.length > 0) {
  184. if (returnEdge === true) {
  185. return this.body.edges[overlappingEdges[overlappingEdges.length - 1]];
  186. }
  187. else {
  188. return overlappingEdges[overlappingEdges.length - 1];
  189. }
  190. }
  191. else {
  192. return undefined;
  193. }
  194. }
  195. /**
  196. * Add object to the selection array.
  197. *
  198. * @param obj
  199. * @private
  200. */
  201. _addToSelection(obj) {
  202. if (obj instanceof Node) {
  203. this.selectionObj.nodes[obj.id] = obj;
  204. }
  205. else {
  206. this.selectionObj.edges[obj.id] = obj;
  207. }
  208. }
  209. /**
  210. * Add object to the selection array.
  211. *
  212. * @param obj
  213. * @private
  214. */
  215. _addToHover(obj) {
  216. if (obj instanceof Node) {
  217. this.hoverObj.nodes[obj.id] = obj;
  218. }
  219. else {
  220. this.hoverObj.edges[obj.id] = obj;
  221. }
  222. }
  223. /**
  224. * Remove a single option from selection.
  225. *
  226. * @param {Object} obj
  227. * @private
  228. */
  229. _removeFromSelection(obj) {
  230. if (obj instanceof Node) {
  231. delete this.selectionObj.nodes[obj.id];
  232. }
  233. else {
  234. delete this.selectionObj.edges[obj.id];
  235. }
  236. }
  237. /**
  238. * Unselect all. The selectionObj is useful for this.
  239. *
  240. * @private
  241. */
  242. unselectAll() {
  243. for(let nodeId in this.selectionObj.nodes) {
  244. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  245. this.selectionObj.nodes[nodeId].unselect();
  246. }
  247. }
  248. for(let edgeId in this.selectionObj.edges) {
  249. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  250. this.selectionObj.edges[edgeId].unselect();
  251. }
  252. }
  253. this.selectionObj = {nodes:{},edges:{}};
  254. }
  255. /**
  256. * return the number of selected nodes
  257. *
  258. * @returns {number}
  259. * @private
  260. */
  261. _getSelectedNodeCount() {
  262. let count = 0;
  263. for (let nodeId in this.selectionObj.nodes) {
  264. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  265. count += 1;
  266. }
  267. }
  268. return count;
  269. }
  270. /**
  271. * return the selected node
  272. *
  273. * @returns {number}
  274. * @private
  275. */
  276. _getSelectedNode() {
  277. for (let nodeId in this.selectionObj.nodes) {
  278. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  279. return this.selectionObj.nodes[nodeId];
  280. }
  281. }
  282. return undefined;
  283. }
  284. /**
  285. * return the selected edge
  286. *
  287. * @returns {number}
  288. * @private
  289. */
  290. _getSelectedEdge() {
  291. for (let edgeId in this.selectionObj.edges) {
  292. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  293. return this.selectionObj.edges[edgeId];
  294. }
  295. }
  296. return undefined;
  297. }
  298. /**
  299. * return the number of selected edges
  300. *
  301. * @returns {number}
  302. * @private
  303. */
  304. _getSelectedEdgeCount() {
  305. let count = 0;
  306. for (let edgeId in this.selectionObj.edges) {
  307. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  308. count += 1;
  309. }
  310. }
  311. return count;
  312. }
  313. /**
  314. * return the number of selected objects.
  315. *
  316. * @returns {number}
  317. * @private
  318. */
  319. _getSelectedObjectCount() {
  320. let count = 0;
  321. for(let nodeId in this.selectionObj.nodes) {
  322. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  323. count += 1;
  324. }
  325. }
  326. for(let edgeId in this.selectionObj.edges) {
  327. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  328. count += 1;
  329. }
  330. }
  331. return count;
  332. }
  333. /**
  334. * Check if anything is selected
  335. *
  336. * @returns {boolean}
  337. * @private
  338. */
  339. _selectionIsEmpty() {
  340. for(let nodeId in this.selectionObj.nodes) {
  341. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  342. return false;
  343. }
  344. }
  345. for(let edgeId in this.selectionObj.edges) {
  346. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  347. return false;
  348. }
  349. }
  350. return true;
  351. }
  352. /**
  353. * check if one of the selected nodes is a cluster.
  354. *
  355. * @returns {boolean}
  356. * @private
  357. */
  358. _clusterInSelection() {
  359. for(let nodeId in this.selectionObj.nodes) {
  360. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  361. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  362. return true;
  363. }
  364. }
  365. }
  366. return false;
  367. }
  368. /**
  369. * select the edges connected to the node that is being selected
  370. *
  371. * @param {Node} node
  372. * @private
  373. */
  374. _selectConnectedEdges(node) {
  375. for (let i = 0; i < node.edges.length; i++) {
  376. let edge = node.edges[i];
  377. edge.select();
  378. this._addToSelection(edge);
  379. }
  380. }
  381. /**
  382. * select the edges connected to the node that is being selected
  383. *
  384. * @param {Node} node
  385. * @private
  386. */
  387. _hoverConnectedEdges(node) {
  388. for (let i = 0; i < node.edges.length; i++) {
  389. let edge = node.edges[i];
  390. edge.hover = true;
  391. this._addToHover(edge);
  392. }
  393. }
  394. /**
  395. * unselect the edges connected to the node that is being selected
  396. *
  397. * @param {Node} node
  398. * @private
  399. */
  400. _unselectConnectedEdges(node) {
  401. for (let i = 0; i < node.edges.length; i++) {
  402. let edge = node.edges[i];
  403. edge.unselect();
  404. this._removeFromSelection(edge);
  405. }
  406. }
  407. /**
  408. * This is called when someone clicks on a node. either select or deselect it.
  409. * If there is an existing selection and we don't want to append to it, clear the existing selection
  410. *
  411. * @param {Node || Edge} object
  412. * @private
  413. */
  414. blurObject(object) {
  415. if (object.hover === true) {
  416. object.hover = false;
  417. this.body.emitter.emit("blurNode",{node:object.id});
  418. }
  419. }
  420. /**
  421. * This is called when someone clicks on a node. either select or deselect it.
  422. * If there is an existing selection and we don't want to append to it, clear the existing selection
  423. *
  424. * @param {Node || Edge} object
  425. * @private
  426. */
  427. hoverObject(object) {
  428. if (object.hover === false) {
  429. object.hover = true;
  430. this._addToHover(object);
  431. if (object instanceof Node) {
  432. this.body.emitter.emit("hoverNode",{node:object.id});
  433. }
  434. }
  435. if (object instanceof Node) {
  436. this._hoverConnectedEdges(object);
  437. }
  438. }
  439. /**
  440. *
  441. * retrieve the currently selected objects
  442. * @return {{nodes: Array.<String>, edges: Array.<String>}} selection
  443. */
  444. getSelection() {
  445. let nodeIds = this.getSelectedNodes();
  446. let edgeIds = this.getSelectedEdges();
  447. return {nodes:nodeIds, edges:edgeIds};
  448. }
  449. /**
  450. *
  451. * retrieve the currently selected nodes
  452. * @return {String[]} selection An array with the ids of the
  453. * selected nodes.
  454. */
  455. getSelectedNodes() {
  456. let idArray = [];
  457. if (this.options.select === true) {
  458. for (let nodeId in this.selectionObj.nodes) {
  459. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  460. idArray.push(nodeId);
  461. }
  462. }
  463. }
  464. return idArray;
  465. }
  466. /**
  467. *
  468. * retrieve the currently selected edges
  469. * @return {Array} selection An array with the ids of the
  470. * selected nodes.
  471. */
  472. getSelectedEdges() {
  473. let idArray = [];
  474. if (this.options.select === true) {
  475. for (let edgeId in this.selectionObj.edges) {
  476. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  477. idArray.push(edgeId);
  478. }
  479. }
  480. }
  481. return idArray;
  482. }
  483. /**
  484. * select zero or more nodes with the option to highlight edges
  485. * @param {Number[] | String[]} selection An array with the ids of the
  486. * selected nodes.
  487. * @param {boolean} [highlightEdges]
  488. */
  489. selectNodes(selection, highlightEdges = true) {
  490. let i, id;
  491. if (!selection || (selection.length === undefined))
  492. throw 'Selection must be an array with ids';
  493. // first unselect any selected node
  494. this.unselectAll();
  495. for (i = 0; i < selection.length; i++) {
  496. id = selection[i];
  497. let node = this.body.nodes[id];
  498. if (!node) {
  499. throw new RangeError('Node with id "' + id + '" not found');
  500. }
  501. this.selectObject(node,highlightEdges);
  502. }
  503. this.body.emitter.emit('_requestRedraw');
  504. }
  505. /**
  506. * select zero or more edges
  507. * @param {Number[] | String[]} selection An array with the ids of the
  508. * selected nodes.
  509. */
  510. selectEdges(selection) {
  511. let i, id;
  512. if (!selection || (selection.length === undefined))
  513. throw 'Selection must be an array with ids';
  514. // first unselect any selected objects
  515. this.unselectAll();
  516. for (i = 0; i < selection.length; i++) {
  517. id = selection[i];
  518. let edge = this.body.edges[id];
  519. if (!edge) {
  520. throw new RangeError('Edge with id "' + id + '" not found');
  521. }
  522. this.selectObject(edge);
  523. }
  524. this.body.emitter.emit('_requestRedraw');
  525. }
  526. /**
  527. * Validate the selection: remove ids of nodes which no longer exist
  528. * @private
  529. */
  530. updateSelection() {
  531. for (let nodeId in this.selectionObj.nodes) {
  532. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  533. if (!this.body.nodes.hasOwnProperty(nodeId)) {
  534. delete this.selectionObj.nodes[nodeId];
  535. }
  536. }
  537. }
  538. for (let edgeId in this.selectionObj.edges) {
  539. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  540. if (!this.body.edges.hasOwnProperty(edgeId)) {
  541. delete this.selectionObj.edges[edgeId];
  542. }
  543. }
  544. }
  545. }
  546. }
  547. export default SelectionHandler;