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.

615 lines
14 KiB

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