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.

639 lines
15 KiB

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