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.

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