connection_utils.js 15 KB

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