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.

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