group.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. /*******************************************************************************
  2. AToMPM - A Tool for Multi-Paradigm Modelling
  3. Copyright (c) 2011 Raphael Mannadiar (raphael.mannadiar@mail.mcgill.ca)
  4. This file is part of AToMPM.
  5. AToMPM is free software: you can redistribute it and/or modify it under the
  6. terms of the GNU Lesser General Public License as published by the Free Software
  7. Foundation, either version 3 of the License, or (at your option) any later
  8. version.
  9. AToMPM is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
  11. PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
  12. You should have received a copy of the GNU Lesser General Public License along
  13. with AToMPM. If not, see <http://www.gnu.org/licenses/>.
  14. *******************************************************************************/
  15. /* inspired by https://github.com/rhyolight/Raphael-Plugins (raphael.group.js) */
  16. Raphael.fn.group = function() {
  17. var r = this;
  18. function Group()
  19. {
  20. var inst,
  21. set = r.set(),
  22. group = r.raphael.vml ?
  23. document.createElement("group") :
  24. document.createElementNS("http://www.w3.org/2000/svg", "g"),
  25. overlay = r.raphael.vml ?
  26. document.createElement("group") :
  27. document.createElementNS("http://www.w3.org/2000/svg", "g");
  28. /* transform a series of translate|rotate|scale commands into t|r|s
  29. NOTE :: the parameters for 'SVG.scale()' are not exactly the same
  30. as those for 'Raphael.scale()'... this because the former
  31. scales wrt. (0,0) while the latter defaults to scaling wrt.
  32. the center of the element... for consistency, we force Raphael
  33. transform() to scale around (0,0)
  34. NOTE :: the same goes for SVG.rotate() and Raphael.rotate() */
  35. function svg2raphaelTransformString()
  36. {
  37. var rT = '';
  38. group.getAttribute('transform').split(' ').forEach(
  39. function(svgt)
  40. {
  41. if( (args = svgt.match(/translate\((.*),(.*)\)/)) )
  42. rT += 't'+args[1]+','+args[2];
  43. else if( (args = svgt.match(/rotate\((.*),(.*),(.*)\)/)) )
  44. rT += 'r'+args[1]+','+args[2]+','+args[3];
  45. else if( (args = svgt.match(/rotate\((.*)\)/)) )
  46. rT += 'r'+args[1]+',0,0';
  47. else if( (args = svgt.match(/scale\((.*),(.*)\)/)) )
  48. rT += 's'+args[1]+','+args[2]+',0,0';
  49. else if( (args = svgt.match(/scale\((.*)\)/)) )
  50. rT += 's'+args[1]+','+args[1]+',0,0';
  51. else if( (args = svgt.match(
  52. /matrix\((.*),(.*),(.*),(.*),(.*),(.*)\)/)) )
  53. rT += 'm'+args[1]+','+args[2]+','+args[3]+','+
  54. args[4]+','+args[5]+','+args[6];
  55. });
  56. return rT;
  57. }
  58. r.canvas.appendChild(group);
  59. group.setAttribute('transform','');
  60. inst =
  61. {'forEach':
  62. function(callback)
  63. {
  64. for( var i=0; i<group.childNodes.length; i++ )
  65. callback(group.childNodes[i],i,group.childNodes);
  66. },
  67. 'getAttr':
  68. function(attr)
  69. {
  70. return group.getAttribute(attr);
  71. },
  72. /* NOTE :: set.getBBox() doesn't include group's transformations... to
  73. get an appropriate bbox, i do a little hack
  74. 1. create a Rect identical to set.getBBox()
  75. 2. apply the groups transformations on that Rect
  76. 3. save the Rect's bbox
  77. 4. delete the Rect
  78. 5. return the computed bbox
  79. the above procedure yields imperfect results when shapes are
  80. rotated... we end up returning the bbox of the shapes post-
  81. transform bbox... to correct this, we would need a way to get
  82. the 'world bbox' (i.e., parallel to screen) of a transformed
  83. shape
  84. NOTE :: Paths can produce bboxes with 0 height or width... to
  85. avoid this, before proceeding with step 1. (above), we
  86. ensure that the result of set.getBBox() has at least 1px
  87. width and height */
  88. 'getBBox':
  89. function()
  90. {
  91. var bbox = set.getBBox();
  92. if( bbox.height == 0 )
  93. {
  94. bbox.y -= 0.5;
  95. bbox.height = 1;
  96. }
  97. if( bbox.width == 0 )
  98. {
  99. bbox.x -= 0.5;
  100. bbox.width = 1;
  101. }
  102. var rect = r.rect(bbox.x,bbox.y,bbox.width,bbox.height);
  103. rect.transform( svg2raphaelTransformString() );
  104. bbox = rect.getBBox();
  105. rect.remove();
  106. return bbox;
  107. },
  108. 'hasAttr':
  109. function(attr)
  110. {
  111. return group.hasAttribute(attr);
  112. },
  113. 'hide':
  114. function()
  115. {
  116. group.style.display = 'none';
  117. },
  118. /* render a highlighting effect
  119. 1. remove current highlighting if any
  120. 2. extract effect arguments
  121. 3. construct rectangle matching set bbox (not group's because we
  122. want bbox in group coordinates, not world coordinates)
  123. 4. setup rectangle appearance based on step 2.
  124. 5. add rectangle to group.overlay */
  125. 'highlight':
  126. function(args)
  127. {
  128. this.unhighlight();
  129. var color = args['color'],
  130. opacity = args['opacity'] || 0.3,
  131. width = args['width'] || 20,
  132. bbox = set.getBBox(),
  133. rect = r.rect(bbox.x,bbox.y,bbox.width,bbox.height,10);
  134. rect.attr('stroke',color);
  135. rect.attr('opacity',opacity);
  136. rect.attr('stroke-width',width);
  137. rect.node.setAttribute('__highlight',1);
  138. if( args['fill'] )
  139. rect.attr('fill',color);
  140. overlay.appendChild(rect.node);
  141. if( group.lastChild != overlay )
  142. group.appendChild(overlay);
  143. },
  144. 'insertBefore':
  145. function(obj)
  146. {
  147. for( var i=0; i<r.canvas.childNodes.length; i++ )
  148. if( r.canvas.childNodes[i] == obj )
  149. return r.canvas.insertBefore(
  150. r.canvas.removeChild(group), obj);
  151. throw 'can\'t insert before unknown element :: '+obj ;
  152. },
  153. 'isVisible':
  154. function()
  155. {
  156. return group.style.display != 'none';
  157. },
  158. 'matrixTransform':
  159. function(a,b,c,d,e,f)
  160. {
  161. var mstr =
  162. (b == undefined ?
  163. a :
  164. 'matrix('+a+','+b+','+c+','+d+','+e+','+f+')');
  165. group.setAttribute(
  166. 'transform',
  167. mstr+' '+group.getAttribute('transform'));
  168. },
  169. 'node': group,
  170. 'pop':
  171. function()
  172. {
  173. var res = set.pop();
  174. group.removeChild(group.lastChild);
  175. return res;
  176. },
  177. 'push':
  178. function(item)
  179. {
  180. function __push(it)
  181. {
  182. if( it.type == 'set' )
  183. it.items.forEach( __push );
  184. else
  185. {
  186. group.appendChild(it.node);
  187. set.push(it);
  188. }
  189. }
  190. __push(item);
  191. },
  192. 'remove':
  193. function()
  194. {
  195. group.parentNode.removeChild(group);
  196. },
  197. 'rotate':
  198. function(deg,cx,cy)
  199. {
  200. var params =
  201. (deg + (cx == undefined || cy == undefined ?
  202. '' :
  203. ','+cx+','+cy));
  204. group.setAttribute(
  205. 'transform',
  206. 'rotate('+params+') '+group.getAttribute('transform'));
  207. },
  208. 'scale':
  209. function(sx,sy)
  210. {
  211. var params =
  212. (sx + (sy == undefined ?
  213. ','+sx :
  214. ','+sy ));
  215. group.setAttribute(
  216. 'transform',
  217. 'scale('+params+') '+group.getAttribute('transform'));
  218. },
  219. 'setAttr':
  220. function(attr,val)
  221. {
  222. group.setAttribute(attr,val);
  223. },
  224. 'show':
  225. function()
  226. {
  227. group.style.display = 'inline';
  228. },
  229. /* render a tag
  230. 1. if 'text' is empty or 'append' is false, remove any current
  231. tags
  232. 1. otherwise, compute total height of current tags (to know what
  233. Y-offset to give to future tag)
  234. 2. if 'text' is not empty, setup new tag given 'text' and
  235. 'style' and add it to group.overlay */
  236. 'tag':
  237. function(text,style,append)
  238. {
  239. var offsetY = 0;
  240. if( !append || !text )
  241. {
  242. for( var i=overlay.childNodes.length-1; i>=0; i-- )
  243. if( overlay.childNodes[i].hasAttribute('__tag') )
  244. overlay.removeChild(overlay.childNodes[i]);
  245. }
  246. else
  247. {
  248. for( var i=0; i<overlay.childNodes.length; i++ )
  249. if( overlay.childNodes[i].hasAttribute('__tag') )
  250. offsetY += overlay.childNodes[i].offsetHeight;
  251. }
  252. if( text )
  253. {
  254. var t = __canvas.text(0,offsetY,text);
  255. t.attr('text-anchor','start');
  256. t.attr(style);
  257. t.node.setAttribute('__tag',1);
  258. t.node.setAttribute('class','tag unselectable');
  259. overlay.appendChild(t.node);
  260. }
  261. if( group.lastChild != overlay )
  262. group.appendChild(overlay);
  263. },
  264. /* make the current group the first/last element of the canvas
  265. (i.e., the first/last one to be rendered) */
  266. 'toBack':
  267. function()
  268. {
  269. var firstChild = group.parentNode.firstChild;
  270. if( firstChild == group )
  271. return;
  272. else
  273. group.parentNode.insertBefore(
  274. group.parentNode.removeChild(group),
  275. firstChild);
  276. },
  277. 'toFront' :
  278. function()
  279. {
  280. if( group.parentNode.lastChild == group )
  281. return;
  282. else
  283. group.parentNode.appendChild(
  284. group.parentNode.removeChild(group));
  285. },
  286. 'translate':
  287. function(x,y)
  288. {
  289. group.setAttribute(
  290. 'transform',
  291. 'translate('+x+','+y+') '+group.getAttribute('transform'));
  292. },
  293. 'type': 'group',
  294. /* remove current highlighting effect, if any */
  295. 'unhighlight':
  296. function(args)
  297. {
  298. for( var i=0; i < overlay.childNodes.length; i++ )
  299. if( overlay.childNodes[i].hasAttribute('__highlight') )
  300. {
  301. overlay.removeChild(overlay.childNodes[i]);
  302. break;
  303. }
  304. }};
  305. return inst;
  306. }
  307. return Group();
  308. };