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.

614 lines
14 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) {
  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. return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]];
  136. }
  137. else {
  138. return undefined;
  139. }
  140. }
  141. /**
  142. * retrieve all edges overlapping with given object, selector is around center
  143. * @param {Object} object An object with parameters left, top, right, bottom
  144. * @return {Number[]} An array with id's of the overlapping nodes
  145. * @private
  146. */
  147. _getEdgesOverlappingWith(object, overlappingEdges) {
  148. let edges = this.body.edges;
  149. for (let i = 0; i < this.body.edgeIndices.length; i++) {
  150. let edgeId = this.body.edgeIndices[i];
  151. if (edges[edgeId].isOverlappingWith(object)) {
  152. overlappingEdges.push(edgeId);
  153. }
  154. }
  155. }
  156. /**
  157. * retrieve all nodes overlapping with given object
  158. * @param {Object} object An object with parameters left, top, right, bottom
  159. * @return {Number[]} An array with id's of the overlapping nodes
  160. * @private
  161. */
  162. _getAllEdgesOverlappingWith(object) {
  163. let overlappingEdges = [];
  164. this._getEdgesOverlappingWith(object,overlappingEdges);
  165. return overlappingEdges;
  166. }
  167. /**
  168. * Place holder. To implement change the getNodeAt to a _getObjectAt. Have the _getObjectAt call
  169. * getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  170. *
  171. * @param pointer
  172. * @returns {undefined}
  173. * @private
  174. */
  175. getEdgeAt(pointer) {
  176. let positionObject = this._pointerToPositionObject(pointer);
  177. let overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
  178. if (overlappingEdges.length > 0) {
  179. return this.body.edges[overlappingEdges[overlappingEdges.length - 1]];
  180. }
  181. else {
  182. return undefined;
  183. }
  184. }
  185. /**
  186. * Add object to the selection array.
  187. *
  188. * @param obj
  189. * @private
  190. */
  191. _addToSelection(obj) {
  192. if (obj instanceof Node) {
  193. this.selectionObj.nodes[obj.id] = obj;
  194. }
  195. else {
  196. this.selectionObj.edges[obj.id] = obj;
  197. }
  198. }
  199. /**
  200. * Add object to the selection array.
  201. *
  202. * @param obj
  203. * @private
  204. */
  205. _addToHover(obj) {
  206. if (obj instanceof Node) {
  207. this.hoverObj.nodes[obj.id] = obj;
  208. }
  209. else {
  210. this.hoverObj.edges[obj.id] = obj;
  211. }
  212. }
  213. /**
  214. * Remove a single option from selection.
  215. *
  216. * @param {Object} obj
  217. * @private
  218. */
  219. _removeFromSelection(obj) {
  220. if (obj instanceof Node) {
  221. delete this.selectionObj.nodes[obj.id];
  222. }
  223. else {
  224. delete this.selectionObj.edges[obj.id];
  225. }
  226. }
  227. /**
  228. * Unselect all. The selectionObj is useful for this.
  229. *
  230. * @private
  231. */
  232. unselectAll() {
  233. for(let nodeId in this.selectionObj.nodes) {
  234. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  235. this.selectionObj.nodes[nodeId].unselect();
  236. }
  237. }
  238. for(let edgeId in this.selectionObj.edges) {
  239. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  240. this.selectionObj.edges[edgeId].unselect();
  241. }
  242. }
  243. this.selectionObj = {nodes:{},edges:{}};
  244. }
  245. /**
  246. * return the number of selected nodes
  247. *
  248. * @returns {number}
  249. * @private
  250. */
  251. _getSelectedNodeCount() {
  252. let count = 0;
  253. for (let nodeId in this.selectionObj.nodes) {
  254. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  255. count += 1;
  256. }
  257. }
  258. return count;
  259. }
  260. /**
  261. * return the selected node
  262. *
  263. * @returns {number}
  264. * @private
  265. */
  266. _getSelectedNode() {
  267. for (let nodeId in this.selectionObj.nodes) {
  268. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  269. return this.selectionObj.nodes[nodeId];
  270. }
  271. }
  272. return undefined;
  273. }
  274. /**
  275. * return the selected edge
  276. *
  277. * @returns {number}
  278. * @private
  279. */
  280. _getSelectedEdge() {
  281. for (let edgeId in this.selectionObj.edges) {
  282. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  283. return this.selectionObj.edges[edgeId];
  284. }
  285. }
  286. return undefined;
  287. }
  288. /**
  289. * return the number of selected edges
  290. *
  291. * @returns {number}
  292. * @private
  293. */
  294. _getSelectedEdgeCount() {
  295. let count = 0;
  296. for (let edgeId in this.selectionObj.edges) {
  297. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  298. count += 1;
  299. }
  300. }
  301. return count;
  302. }
  303. /**
  304. * return the number of selected objects.
  305. *
  306. * @returns {number}
  307. * @private
  308. */
  309. _getSelectedObjectCount() {
  310. let count = 0;
  311. for(let nodeId in this.selectionObj.nodes) {
  312. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  313. count += 1;
  314. }
  315. }
  316. for(let edgeId in this.selectionObj.edges) {
  317. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  318. count += 1;
  319. }
  320. }
  321. return count;
  322. }
  323. /**
  324. * Check if anything is selected
  325. *
  326. * @returns {boolean}
  327. * @private
  328. */
  329. _selectionIsEmpty() {
  330. for(let nodeId in this.selectionObj.nodes) {
  331. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  332. return false;
  333. }
  334. }
  335. for(let edgeId in this.selectionObj.edges) {
  336. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  337. return false;
  338. }
  339. }
  340. return true;
  341. }
  342. /**
  343. * check if one of the selected nodes is a cluster.
  344. *
  345. * @returns {boolean}
  346. * @private
  347. */
  348. _clusterInSelection() {
  349. for(let nodeId in this.selectionObj.nodes) {
  350. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  351. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  352. return true;
  353. }
  354. }
  355. }
  356. return false;
  357. }
  358. /**
  359. * select the edges connected to the node that is being selected
  360. *
  361. * @param {Node} node
  362. * @private
  363. */
  364. _selectConnectedEdges(node) {
  365. for (let i = 0; i < node.edges.length; i++) {
  366. let edge = node.edges[i];
  367. edge.select();
  368. this._addToSelection(edge);
  369. }
  370. }
  371. /**
  372. * select the edges connected to the node that is being selected
  373. *
  374. * @param {Node} node
  375. * @private
  376. */
  377. _hoverConnectedEdges(node) {
  378. for (let i = 0; i < node.edges.length; i++) {
  379. let edge = node.edges[i];
  380. edge.hover = true;
  381. this._addToHover(edge);
  382. }
  383. }
  384. /**
  385. * unselect the edges connected to the node that is being selected
  386. *
  387. * @param {Node} node
  388. * @private
  389. */
  390. _unselectConnectedEdges(node) {
  391. for (let i = 0; i < node.edges.length; i++) {
  392. let edge = node.edges[i];
  393. edge.unselect();
  394. this._removeFromSelection(edge);
  395. }
  396. }
  397. /**
  398. * This is called when someone clicks on a node. either select or deselect it.
  399. * If there is an existing selection and we don't want to append to it, clear the existing selection
  400. *
  401. * @param {Node || Edge} object
  402. * @private
  403. */
  404. blurObject(object) {
  405. if (object.hover === true) {
  406. object.hover = false;
  407. this.body.emitter.emit("blurNode",{node:object.id});
  408. }
  409. }
  410. /**
  411. * This is called when someone clicks on a node. either select or deselect it.
  412. * If there is an existing selection and we don't want to append to it, clear the existing selection
  413. *
  414. * @param {Node || Edge} object
  415. * @private
  416. */
  417. hoverObject(object) {
  418. if (object.hover === false) {
  419. object.hover = true;
  420. this._addToHover(object);
  421. if (object instanceof Node) {
  422. this.body.emitter.emit("hoverNode",{node:object.id});
  423. }
  424. }
  425. if (object instanceof Node) {
  426. this._hoverConnectedEdges(object);
  427. }
  428. }
  429. /**
  430. *
  431. * retrieve the currently selected objects
  432. * @return {{nodes: Array.<String>, edges: Array.<String>}} selection
  433. */
  434. getSelection() {
  435. let nodeIds = this.getSelectedNodes();
  436. let edgeIds = this.getSelectedEdges();
  437. return {nodes:nodeIds, edges:edgeIds};
  438. }
  439. /**
  440. *
  441. * retrieve the currently selected nodes
  442. * @return {String[]} selection An array with the ids of the
  443. * selected nodes.
  444. */
  445. getSelectedNodes() {
  446. let idArray = [];
  447. if (this.options.select === true) {
  448. for (let nodeId in this.selectionObj.nodes) {
  449. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  450. idArray.push(nodeId);
  451. }
  452. }
  453. }
  454. return idArray;
  455. }
  456. /**
  457. *
  458. * retrieve the currently selected edges
  459. * @return {Array} selection An array with the ids of the
  460. * selected nodes.
  461. */
  462. getSelectedEdges() {
  463. let idArray = [];
  464. if (this.options.select === true) {
  465. for (let edgeId in this.selectionObj.edges) {
  466. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  467. idArray.push(edgeId);
  468. }
  469. }
  470. }
  471. return idArray;
  472. }
  473. /**
  474. * select zero or more nodes with the option to highlight edges
  475. * @param {Number[] | String[]} selection An array with the ids of the
  476. * selected nodes.
  477. * @param {boolean} [highlightEdges]
  478. */
  479. selectNodes(selection, highlightEdges = true) {
  480. let i, id;
  481. if (!selection || (selection.length === undefined))
  482. throw 'Selection must be an array with ids';
  483. // first unselect any selected node
  484. this.unselectAll();
  485. for (i = 0; i < selection.length; i++) {
  486. id = selection[i];
  487. let node = this.body.nodes[id];
  488. if (!node) {
  489. throw new RangeError('Node with id "' + id + '" not found');
  490. }
  491. this.selectObject(node,highlightEdges);
  492. }
  493. this.body.emitter.emit('_requestRedraw');
  494. }
  495. /**
  496. * select zero or more edges
  497. * @param {Number[] | String[]} selection An array with the ids of the
  498. * selected nodes.
  499. */
  500. selectEdges(selection) {
  501. let i, id;
  502. if (!selection || (selection.length === undefined))
  503. throw 'Selection must be an array with ids';
  504. // first unselect any selected objects
  505. this.unselectAll();
  506. for (i = 0; i < selection.length; i++) {
  507. id = selection[i];
  508. let edge = this.body.edges[id];
  509. if (!edge) {
  510. throw new RangeError('Edge with id "' + id + '" not found');
  511. }
  512. this.selectObject(edge);
  513. }
  514. this.body.emitter.emit('_requestRedraw');
  515. }
  516. /**
  517. * Validate the selection: remove ids of nodes which no longer exist
  518. * @private
  519. */
  520. updateSelection() {
  521. for (let nodeId in this.selectionObj.nodes) {
  522. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  523. if (!this.body.nodes.hasOwnProperty(nodeId)) {
  524. delete this.selectionObj.nodes[nodeId];
  525. }
  526. }
  527. }
  528. for (let edgeId in this.selectionObj.edges) {
  529. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  530. if (!this.body.edges.hasOwnProperty(edgeId)) {
  531. delete this.selectionObj.edges[edgeId];
  532. }
  533. }
  534. }
  535. }
  536. }
  537. export default SelectionHandler;