connection_utils.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. /* This file is part of AToMPM - A Tool for Multi-Paradigm Modelling
  2. * Copyright 2011 by the AToMPM team and licensed under the LGPL
  3. * See COPYING.lesser and README.md in the root of this project for full details
  4. */
  5. ConnectionUtils = function(){
  6. var connectionPathEditingOverlay = {};
  7. var currentControlPoint = undefined;
  8. var connectionSource = undefined;
  9. var connectionPath = undefined;
  10. this.getConnectionSource = function(){
  11. return connectionSource;
  12. };
  13. this.getConnectionPath = function(){
  14. return connectionPath;
  15. };
  16. /**
  17. * "Confirm"s the entire current connection path.
  18. * TODO: update this documentation
  19. */
  20. this.addConnectionSegment = function(){
  21. connectionPath.node.setAttribute('_d',connectionPath.attr('path'));
  22. };
  23. /**
  24. * Adds a new control point to the current path
  25. * @param x the x-coordinate
  26. * @param y the y-coordinate
  27. * @param overlay
  28. */
  29. this.addControlPoint = function(x,y,overlay) {
  30. ConnectionUtils.addOrDeleteControlPoint('+',overlay,x,y);
  31. };
  32. /* Explanation of the Add/Delete algorithm:
  33. addition:
  34. . clicked overlay corresponds to Lx2,y2
  35. Mx0,y0 Lx1,y1 Lx2,y2 Lx3,y3
  36. becomes
  37. Mx0,y0 Lx1,y1 Lx2,y2 Lx2,y2 Lx3,y3
  38. deletion:
  39. . clicked overlay corresponds to Lx2,y2
  40. Mx0,y0 Lx1,y1 Lx2,y2 Lx3,y3
  41. becomes
  42. Mx0,y0 Lx1,y1 Lx3,y3
  43. after making the described modifications to the corresponding edge's segments
  44. property,
  45. 1 the edge is redrawn
  46. 2 a request is sent to the csworker to update the edge's Link's $segments
  47. property
  48. 3 the connection path editing overlay is refreshed (this will cause newly
  49. added control points to appear, and deleted ones to disappear)
  50. NOTE:: the first and last control points can never be deleted */
  51. /**
  52. * Adds or deletes the control point associated with the given overlay.
  53. */
  54. this.addOrDeleteControlPoint = function(op,overlay,x,y){
  55. if( ! overlay.hasAttribute('__edgeId') )
  56. return;
  57. var edgeId = overlay.getAttribute('__edgeId'),
  58. num = parseInt( overlay.getAttribute('__num') ),
  59. offset = parseInt( overlay.getAttribute('__offset') ),
  60. segments = __edges[edgeId]['segments'],
  61. points = segments.match(/([\d\.]*,[\d\.]*)/g);
  62. if( op == '-' )
  63. /* delete a control point */
  64. {
  65. if( num+offset == 0 || num+offset == points.length-1 )
  66. return;
  67. points.splice(num+offset,1);
  68. }
  69. else
  70. /* add a control point */
  71. points.splice(num+offset,0,x+','+y);
  72. var newpath = 'M'+points.join('L'),
  73. edgeIds = utils.keys(connectionPathEditingOverlay),
  74. linkuri = __edgeId2linkuri(edgeId),
  75. changes = {};
  76. changes[edgeId] = newpath;
  77. __redrawEdge(edgeId,newpath);
  78. DataUtils.updatecs(
  79. linkuri,
  80. {'$segments':utils.mergeDicts([__linkuri2segments(linkuri),changes])});
  81. ConnectionUtils.hideConnectionPathEditingOverlay();
  82. ConnectionUtils.showConnectionPathEditingOverlay(edgeIds);
  83. };
  84. /**
  85. * Removes the current control point
  86. * @param overlay - the overlay to be used to identify the control point
  87. */
  88. this.deleteControlPoint = function(overlay) {
  89. ConnectionUtils.addOrDeleteControlPoint('-',overlay);
  90. };
  91. /**
  92. * "Unconfirm" the last segment of the connection path (ie
  93. * remove it). Do nothing if all segments have been "confirmed"
  94. */
  95. this.deleteConnectionSegment = function() {
  96. var d = String(connectionPath.attr('path')),
  97. matches = d.match(/(M.*,.*)L(.*),(.*)/),
  98. _d = connectionPath.node.getAttribute('_d'),
  99. _matches = _d.match(/(M.*,.*)L.*,.*/);
  100. if( ! _matches )
  101. ; // do nothing
  102. else if( matches )
  103. {
  104. var x = matches[2], y = matches[3];
  105. connectionPath.node.setAttribute('_d',_matches[1]);
  106. ConnectionUtils.updateConnectionSegment(x,y);
  107. }
  108. };
  109. /*
  110. NOTE:: connectionSource is used to remember the uri of the icon at the
  111. start of the path
  112. NOTE:: _d is used ro remember the 'confirmed' portions of the path
  113. NOTE:: the call to connectionPath.toBack() causes mouse events that occur
  114. on top of icons to be captured by those (as opposed to by the in-
  115. progress connection path which would capture them otherwise)
  116. */
  117. /**
  118. * Initializes a Raphael Path starting at (x, y) and that reports the
  119. * mouseup event as if it were the canvas
  120. */
  121. this.initConnectionPath = function(x,y,target){
  122. if( connectionPath != undefined )
  123. return;
  124. connectionSource = __vobj2uri(target);
  125. connectionPath = __canvas.path('M'+x+','+y);
  126. connectionPath.node.setAttribute('_d','M'+x+','+y);
  127. connectionPath.toBack();
  128. connectionPath.node.onmouseup = function(event) {
  129. if( event.button == 0 )
  130. BehaviorManager.handleUserEvent(__EVENT_LEFT_RELEASE_CANVAS,event);
  131. else if( event.button == 1 )
  132. BehaviorManager.handleUserEvent(__EVENT_MIDDLE_RELEASE_CANVAS,event);
  133. else if( event.button == 2 )
  134. BehaviorManager.handleUserEvent(__EVENT_RIGHT_RELEASE_CANVAS,event);
  135. };
  136. };
  137. /**
  138. * Saves the Raphael element associated with the specific overlay as the current
  139. * control point.
  140. *
  141. * This provides a more robust defense against moving the mouse so quickly that it
  142. * exits the overlay we're dragging.
  143. */
  144. this.initControlPointTranslation = function(overlay){
  145. if( overlay.hasAttribute('__edgeId') )
  146. /* set currentControlPoint to normal overlay */
  147. {
  148. var edgeId = overlay.getAttribute('__edgeId'),
  149. num = overlay.getAttribute('__num');
  150. currentControlPoint = connectionPathEditingOverlay[edgeId][num];
  151. }
  152. else
  153. /* set currentControlPoint to central overlay */
  154. {
  155. var linkuri = overlay.getAttribute('__linkuri');
  156. currentControlPoint = connectionPathEditingOverlay[linkuri][0];
  157. }
  158. };
  159. /**
  160. * Hide and delete the connection path
  161. */
  162. this.hideConnectionPath = function(){
  163. connectionPath.remove();
  164. connectionPath = undefined;
  165. connectionSource = undefined;
  166. };
  167. /**
  168. * Hides the current connection path overlay
  169. */
  170. this.hideConnectionPathEditingOverlay = function(){
  171. for( var _ in connectionPathEditingOverlay )
  172. connectionPathEditingOverlay[_].forEach(
  173. function(overlay)
  174. {
  175. overlay.remove();
  176. });
  177. connectionPathEditingOverlay = {};
  178. currentControlPoint = undefined;
  179. };
  180. /**
  181. * Moves the control point and its overlay to the specified coordinates
  182. */
  183. this.previewControlPointTranslation = function (x, y, ctrl_key_down) {
  184. // if the control key is not down,
  185. // restrict control point to within bounding box
  186. if (!ctrl_key_down) {
  187. let new_points = this.restrictControlPoint(x, y);
  188. x = new_points[0];
  189. y = new_points[1];
  190. }
  191. let _x = parseInt(currentControlPoint.node.getAttribute('_x')),
  192. _y = parseInt(currentControlPoint.node.getAttribute('_y'));
  193. currentControlPoint.translate(x - _x, y - _y);
  194. currentControlPoint.node.setAttribute('_x', x);
  195. currentControlPoint.node.setAttribute('_y', y);
  196. ConnectionUtils.updateConnectionPath(true);
  197. };
  198. /**
  199. * Restricts the control point to within an icon's bounding box
  200. */
  201. this.restrictControlPoint = function (x, y) {
  202. let start = currentControlPoint.node.getAttribute("__start");
  203. let end = currentControlPoint.node.getAttribute("__end");
  204. // something went wrong, or we're not an
  205. // outside edge, so return the points
  206. if (start == undefined && end == undefined) {
  207. return [x, y];
  208. }
  209. //get the bounding box rectangle
  210. let icon = __getIcon(start || end);
  211. let bbox = icon.getBBox();
  212. //get the dimensions
  213. let iconX = bbox.x;
  214. let iconY = bbox.y;
  215. let width = bbox.width;
  216. let height = bbox.height;
  217. //restrict x and y to within the bounding box
  218. if (x < iconX) {
  219. x = iconX;
  220. } else if (x > iconX + width) {
  221. x = iconX + width;
  222. }
  223. if (y < iconY) {
  224. y = iconY;
  225. } else if (y > iconY + height) {
  226. y = iconY + height;
  227. }
  228. return [Math.round(x), Math.round(y)];
  229. };
  230. /**
  231. * Show the connection path editing overlay. This shows draggable circles
  232. * above every control point along the selected edges.
  233. */
  234. this.showConnectionPathEditingOverlay = function(_edgeIds){
  235. var edgeIds =
  236. (_edgeIds ? _edgeIds : __selection['items']).
  237. filter( function(it) {return it in __edges;} ),
  238. onmousedown =
  239. function(event)
  240. {
  241. if( event.button == 0 )
  242. BehaviorManager.handleUserEvent(__EVENT_LEFT_PRESS_CTRL_POINT,event);
  243. },
  244. onmouseup =
  245. function(event)
  246. {
  247. if( event.button == 0 )
  248. BehaviorManager.handleUserEvent(__EVENT_LEFT_RELEASE_CTRL_POINT,event);
  249. else if( event.button == 1 )
  250. BehaviorManager.handleUserEvent(__EVENT_MIDDLE_RELEASE_CTRL_POINT,event);
  251. else if( event.button == 2 )
  252. BehaviorManager.handleUserEvent(__EVENT_RIGHT_RELEASE_CTRL_POINT,event);
  253. };
  254. edgeIds.forEach(
  255. function(edgeId)
  256. {
  257. var points = __edges[edgeId]['segments'].match(/([\d\.]*,[\d\.]*)/g),
  258. linkuri = __edgeId2linkuri(edgeId),
  259. edgeToLink = edgeId.match(linkuri+'$');
  260. /* setup normal overlay */
  261. connectionPathEditingOverlay[edgeId] = [];
  262. (edgeToLink ?
  263. points.slice(0,points.length-1) :
  264. points.slice(1)).forEach(
  265. function(p)
  266. {
  267. var xy = p.split(','),
  268. x = xy[0],
  269. y = xy[1],
  270. overlay = __canvas.circle(x,y,5);
  271. overlay.node.setAttribute('class','ctrl_point_overlay');
  272. overlay.node.setAttribute('__edgeId',edgeId);
  273. overlay.node.setAttribute('__offset',(edgeToLink ? 0 : 1));
  274. overlay.node.setAttribute('__num',
  275. connectionPathEditingOverlay[edgeId].length);
  276. overlay.node.setAttribute('_x',x);
  277. overlay.node.setAttribute('_y',y);
  278. overlay.node.onmouseup = onmouseup;
  279. overlay.node.onmousedown = onmousedown;
  280. connectionPathEditingOverlay[edgeId].push(overlay);
  281. });
  282. /* enhance start/end */
  283. if( edgeToLink )
  284. utils.head(connectionPathEditingOverlay[edgeId]).node.
  285. setAttribute('__start', __edges[edgeId]['start']);
  286. else
  287. utils.tail(connectionPathEditingOverlay[edgeId]).node.
  288. setAttribute('__end', __edges[edgeId]['end']);
  289. /* setup central overlay */
  290. var edgeListAttr = (edgeToLink ? '__edgesTo' : '__edgesFrom');
  291. if( ! (linkuri in connectionPathEditingOverlay) )
  292. {
  293. var xy = (edgeToLink ?
  294. __edges[edgeId]['segments'].match(/.*L(.*)/) :
  295. __edges[edgeId]['segments'].match(/M([\d\.]*,[\d\.]*)/))[1].split(','),
  296. x = xy[0],
  297. y = xy[1],
  298. overlay = __canvas.circle(x,y,8);
  299. overlay.node.setAttribute('class','ctrl_point_center_overlay');
  300. overlay.node.setAttribute('_x',x);
  301. overlay.node.setAttribute('_y',y);
  302. overlay.node.setAttribute('_x0',x);
  303. overlay.node.setAttribute('_y0',y);
  304. overlay.node.setAttribute('__linkuri',linkuri);
  305. overlay.node.setAttribute('__edgesTo',utils.jsons([]));
  306. overlay.node.setAttribute('__edgesFrom',utils.jsons([]));
  307. overlay.node.onmouseup = onmouseup;
  308. overlay.node.onmousedown = onmousedown;
  309. connectionPathEditingOverlay[linkuri] = [overlay];
  310. }
  311. var centerOverlay = connectionPathEditingOverlay[linkuri][0],
  312. edgeList = utils.jsonp(centerOverlay.node.getAttribute(edgeListAttr));
  313. edgeList.push(edgeId);
  314. centerOverlay.node.setAttribute(edgeListAttr,utils.jsons(edgeList));
  315. });
  316. };
  317. /**
  318. * Snaps the current segment to the x or y axis depending on its proximity
  319. * to both axes
  320. */
  321. this.snapConnectionSegment = function(x,y){
  322. var _d = connectionPath.node.getAttribute('_d'),
  323. _matches = _d.match(/.*[L|M](.*),(.*)/),
  324. _x = parseInt( _matches[1] ),
  325. _y = parseInt( _matches[2] ),
  326. d = String(connectionPath.attr('path')),
  327. matches = d.match(/.*[L|M](.*),(.*)/),
  328. x = parseInt( matches[1] ),
  329. y = parseInt( matches[2] );
  330. if( Math.abs(x-_x) > Math.abs(y-_y) )
  331. y = _y;
  332. else
  333. x = _x;
  334. ConnectionUtils.updateConnectionSegment(x,y);
  335. };
  336. /**
  337. * Snap the current control point, if any
  338. */
  339. this.snapControlPoint = function(){
  340. if( currentControlPoint == undefined )
  341. return;
  342. var cpn = currentControlPoint.node,
  343. _x = cpn.getAttribute('_x'),
  344. _y = cpn.getAttribute('_y');
  345. if( cpn.hasAttribute('__edgeId') )
  346. /* snapping normal overlay */
  347. {
  348. var edgeId = cpn.getAttribute('__edgeId'),
  349. num = parseInt(cpn.getAttribute('__num')),
  350. offset = parseInt(cpn.getAttribute('__offset')),
  351. points = __edges[edgeId]['segments'].match(/([\d\.]*,[\d\.]*)/g),
  352. prevXY = points[num+offset-1];
  353. if( num+offset == 0 || num+offset == points.length-1 )
  354. /* don't snap end points */
  355. return;
  356. }
  357. else
  358. /* snapping central overlay */
  359. var edgeId = utils.jsonp(cpn.getAttribute('__edgesTo'))[0],
  360. points = __edges[edgeId]['segments'].match(/([\d\.]*,[\d\.]*)/g),
  361. prevXY = points[points.length-2];
  362. prevXY = prevXY.split(',');
  363. if( Math.abs(prevXY[0]-_x) > Math.abs(prevXY[1]-_y) )
  364. _y = prevXY[1];
  365. else
  366. _x = prevXY[0];
  367. ConnectionUtils.previewControlPointTranslation(_x,_y);
  368. ConnectionUtils.updateConnectionPath();
  369. };
  370. /* NOTE:: when 'local' is false/omitted, edge and center-piece alterations are
  371. not merely displayed, but also persisted to the csworker
  372. */
  373. /**
  374. * Alters edges and/or center-pieces to ensure they follow the changes
  375. * effected to their overlays by ConnectionUtils.previewControlPointTranslation()
  376. * and ConnectionUtils.snapConnectionSegment(). This function redraws edges and/or
  377. * moves center pieces
  378. */
  379. this.updateConnectionPath = function(local){
  380. var cpn = currentControlPoint.node,
  381. _x = cpn.getAttribute('_x'),
  382. _y = cpn.getAttribute('_y');
  383. function updatedCenterPiecePosition()
  384. {
  385. var linkuri = cpn.getAttribute('__linkuri'),
  386. x0 = parseInt( cpn.getAttribute('_x0') ),
  387. y0 = parseInt( cpn.getAttribute('_y0') ),
  388. icon = __icons[linkuri]['icon'];
  389. cpn.setAttribute('_x0',_x);
  390. cpn.setAttribute('_y0',_y);
  391. return [(_x-x0) + parseFloat(icon.getAttr('__x')),
  392. (_y-y0) + parseFloat(icon.getAttr('__y'))];
  393. }
  394. function updateEdgeExtremity(edgeId,start)
  395. {
  396. var matches = __edges[edgeId]['segments'].
  397. match(/(M[\d\.]*,[\d\.]*)(.*)(L.*)/),
  398. newpath = (start ?
  399. 'M'+_x+','+_y+matches[2]+matches[3] :
  400. matches[1]+matches[2]+'L'+_x+','+_y);
  401. __redrawEdge(edgeId,newpath);
  402. return newpath;
  403. }
  404. function updateInnerEdge(edgeId,idx)
  405. {
  406. var points = __edges[edgeId]['segments'].match(/([\d\.]*,[\d\.]*)/g);
  407. points.splice(idx,1,_x+','+_y);
  408. var newpath = 'M'+points.join('L');
  409. __redrawEdge(edgeId,newpath);
  410. return newpath;
  411. }
  412. if( cpn.hasAttribute('__edgeId') )
  413. /* dragging normal overlay */
  414. {
  415. var edgeId = cpn.getAttribute('__edgeId'),
  416. num = cpn.getAttribute('__num'),
  417. offset = cpn.getAttribute('__offset'),
  418. linkuri = __edgeId2linkuri(edgeId),
  419. changes = {};
  420. changes[edgeId] = updateInnerEdge(edgeId,parseInt(num)+parseInt(offset));
  421. }
  422. else
  423. /* dragging central overlay */
  424. {
  425. var linkuri = cpn.getAttribute('__linkuri'),
  426. changes = {};
  427. utils.jsonp( cpn.getAttribute('__edgesTo') ).forEach(
  428. function(edgeId)
  429. {
  430. changes[edgeId] = updateEdgeExtremity(edgeId,false);
  431. });
  432. utils.jsonp( cpn.getAttribute('__edgesFrom') ).forEach(
  433. function(edgeId)
  434. {
  435. changes[edgeId] = updateEdgeExtremity(edgeId,true);
  436. });
  437. }
  438. if( ! local )
  439. DataUtils.updatecs(
  440. linkuri,
  441. utils.mergeDicts([
  442. {'$segments':utils.mergeDicts(
  443. [__linkuri2segments(linkuri),changes])},
  444. (cpn.hasAttribute('__linkuri') ?
  445. {'position' :updatedCenterPiecePosition()} : {})]));
  446. };
  447. /**
  448. * Redraws the current segment such that its end is at (x, y)
  449. */
  450. this.updateConnectionSegment = function(x,y){
  451. connectionPath.attr(
  452. 'path',
  453. connectionPath.node.getAttribute('_d')+'L'+x+','+y);
  454. };
  455. return this;
  456. }();