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.

659 lines
16 KiB

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