svg_utils.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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. /* enables/disables scrolling the canvas */
  6. function __setCanvasScrolling(on)
  7. {
  8. //document.body.style.overflow = (on ? 'auto' : 'hidden');
  9. $(document.body).css("overflow", on ? 'auto' : 'hidden');
  10. }
  11. /* draw and return an appropriately styled rectangle matching the give bbox */
  12. function __bbox2rect(bbox,className)
  13. {
  14. var rect = __canvas.rect(
  15. bbox.x,
  16. bbox.y,
  17. bbox.width,
  18. bbox.height);
  19. rect.node.setAttribute('class',className);
  20. return rect;
  21. }
  22. /* given the knowledge that a transformation matrix looks like
  23. [a c e
  24. b d f
  25. 0 0 1],
  26. and that translation, rotation and scaling matrices respectively look like
  27. [1 0 tx [cosA -sinA 0 [sx 0 0
  28. 0 1 ty sinA cosA 0 0 sy 0
  29. 0 0 1] 0 0 1] 0 0 1],
  30. this function extracts tx, ty, A, sx and sy from the given transformation
  31. matrix */
  32. function __decomposeTransformationMatrix(m)
  33. {
  34. /* returns the angle that produces the given values of cos and sin.. this
  35. function's main use is to encapsulate and get around the fact that
  36. Image(acos) = 0..180 and Image(asin) = -90..90 */
  37. function angle(cos,sin)
  38. {
  39. var acos = Math.acos(cos),
  40. asin = Math.asin(sin);
  41. return (asin > 0 ? acos : 2*Math.PI - acos);
  42. }
  43. var matches = m.match(/matrix\((.*),(.*),(.*),(.*),(.*),(.*)\)/),
  44. a = matches[1],
  45. b = matches[2],
  46. c = matches[3],
  47. d = matches[4],
  48. e = matches[5],
  49. f = matches[6],
  50. b2 = b*b,
  51. d2 = d*d,
  52. b2d2 = b2 + d2;
  53. if( b2/b2d2 + d2/b2d2 == 1 )
  54. {
  55. var sy = Math.sqrt(b2d2),
  56. sx = Math.sqrt(a*a+c*c),
  57. r = angle(a/sx,b/sy) * (180/Math.PI);
  58. }
  59. else
  60. {
  61. var sy = Math.sqrt(c*c+d2),
  62. sx = Math.sqrt(a*a+b2),
  63. r = angle(a/sx,b/sx) * (180/Math.PI);
  64. }
  65. return {'tx':parseFloat(e),'ty':parseFloat(f),'sx':sx,'sy':sy,'r':r%360};
  66. }
  67. /* draw specified polygon... this function's reason for being is simply to hide
  68. some details related to the how Raphael draws Polygons...
  69. 1. draw the requested polygon
  70. 2. save its construction attributes within it
  71. 3. place its top-left corner at (x,y)
  72. detail #1: (0,0)
  73. the drawn polygon is centered on (x,y)... we move each of its control points
  74. by (w/2,h/2) to ensure its (0,0) is at its top-left corner
  75. detail #2: paths...
  76. internally, Polygons are just paths... and SVG.Paths have no 'r' or 'sides'
  77. attributes... these 2 variables are merely used to compute the internal path
  78. describing the requested polygon... hence, to support altering the above
  79. variables (and getting appropriate visible feedback), they need to be saved
  80. inside as attributes for future reference... for more on how updating 'r' and
  81. 'sides' alters the rendered polygon, see __editPolygon(..) */
  82. function __drawPolygon(canvas,x,y,r,sides)
  83. {
  84. var pgn = canvas.polygon(x,y,r,sides),
  85. bbox = pgn.getBBox();
  86. pgn.node.setAttribute('___x',x);
  87. pgn.node.setAttribute('___y',y);
  88. pgn.node.setAttribute('___r',r);
  89. pgn.node.setAttribute('___sides',sides);
  90. __translatePath(pgn,bbox.width/2,bbox.height/2);
  91. return pgn;
  92. }
  93. /* see comments for __drawPolygon(..) */
  94. function __drawStar(canvas,x,y,r,rays)
  95. {
  96. var star = canvas.star(x,y,r,undefined,rays),
  97. bbox = star.getBBox();
  98. star.node.setAttribute('___x',x);
  99. star.node.setAttribute('___y',y);
  100. star.node.setAttribute('___r',r);
  101. star.node.setAttribute('___rays',rays);
  102. __translatePath(star,bbox.width/2,bbox.height/2);
  103. return star;
  104. }
  105. /* alters a polygon's 'r' or 'sides'... this is a non-trivial operation because
  106. polygon's aren't SVG primitives, they're just paths drawn to look like
  107. polygons... thus, changing a polygon's 'r' or 'sides' implies changing its
  108. path...
  109. 1. draw a polygon matching the old one but with the new 'r'/'sides'
  110. 2. update the old polygon's internal attribute corresponding to 'attr'
  111. (see __drawPolygon(..))
  112. 3. update the old polygon's path to match that of the newly drawn polygon
  113. 4. remove the new polygon
  114. NOTE:: an alternative to this approach would be to return the new polygon and
  115. use Raphael.insertBefore/After() to place the new polygon on the same
  116. layer the old one was on */
  117. function __editPolygon(pgn,attr,val)
  118. {
  119. var _pgn = __canvas.polygon(
  120. parseFloat( pgn.node.getAttribute('___x') ),
  121. parseFloat( pgn.node.getAttribute('___y') ),
  122. parseFloat(attr == 'r' ? val : pgn.node.getAttribute('___r')),
  123. parseFloat(attr == 'sides' ? val : pgn.node.getAttribute('___sides')) );
  124. pgn.node.setAttribute('___'+attr,val);
  125. pgn.attr('path', _pgn.node.getAttribute('d') );
  126. _pgn.remove();
  127. }
  128. /* see comments for __editPolygon(..) */
  129. function __editStar(star,attr,val)
  130. {
  131. var _star = __canvas.star(
  132. parseFloat( star.node.getAttribute('___x') ),
  133. parseFloat( star.node.getAttribute('___y') ),
  134. parseFloat(attr == 'r' ? val : star.node.getAttribute('___r')),
  135. undefined,
  136. parseFloat(attr == 'rays' ? val : star.node.getAttribute('___rays')) );
  137. star.node.setAttribute('___'+attr,val);
  138. star.attr('path', _star.node.getAttribute('d') );
  139. _star.remove();
  140. }
  141. /* wrapper around group.js' getBBox() that takes into account to-be-applied icon
  142. transformations
  143. icon: icon uri
  144. context: map of icon uris to pending position/scale/orientation changes */
  145. function __getBBox(icon,context)
  146. {
  147. if( context && icon in context )
  148. {
  149. var bbox = __getIcon(icon).node.getBBox(),
  150. rect = __canvas.rect(0,0,bbox.width,bbox.height);
  151. rect.transform(
  152. 't'+('position' in context[icon] ?
  153. context[icon]['position'][0]+','+
  154. context[icon]['position'][1] :
  155. __getIcon(icon).getAttr('__x')+','+
  156. __getIcon(icon).getAttr('__y'))+
  157. 's'+('scale' in context[icon] ?
  158. context[icon]['scale'][0]+','+
  159. context[icon]['scale'][1] :
  160. __getIcon(icon).getAttr('__sx')+','+
  161. __getIcon(icon).getAttr('__sy'))+',0,0'+
  162. 'r'+('orientation' in context[icon] ?
  163. context[icon]['orientation'] :
  164. __getIcon(icon).getAttr('__r'))+',0,0');
  165. bbox = rect.getBBox();
  166. rect.remove();
  167. return bbox;
  168. }
  169. else
  170. return __getIcon(icon).getBBox();
  171. }
  172. /* returns the bbox of a collection of icons and/or edges (specified via uri) */
  173. function __getGlobalBBox(icons)
  174. {
  175. var bbox = {'x':Infinity,'y':Infinity,'width':0,'height':0};
  176. icons.forEach(
  177. function(it)
  178. {
  179. var _bbox = __getIcon(it).getBBox();
  180. if( _bbox.x < bbox.x )
  181. {
  182. /* pull leftmost, fix rightmost (except when Infinity) */
  183. if( bbox.x != Infinity )
  184. bbox.width += bbox.x - _bbox.x;
  185. bbox.x = _bbox.x;
  186. }
  187. if( _bbox.y < bbox.y )
  188. {
  189. /* pull topmost, fix bottommost (except when Infinity) */
  190. if( bbox.y != Infinity )
  191. bbox.height += bbox.y - _bbox.y;
  192. bbox.y = _bbox.y;
  193. }
  194. if( _bbox.x + _bbox.width > bbox.x + bbox.width )
  195. /* pull rightmost, leftmost is fixed */
  196. bbox.width += (_bbox.x + _bbox.width) - (bbox.x + bbox.width);
  197. if( _bbox.y + _bbox.height > bbox.y + bbox.height )
  198. /* pull bottommost, topmost is fixed */
  199. bbox.height += (_bbox.y + _bbox.height) - (bbox.y + bbox.height);
  200. });
  201. return bbox;
  202. }
  203. /* returns true if bbox1 is inside bbox2 */
  204. function __isBBoxInside(bbox1,bbox2)
  205. {
  206. return bbox1.x > bbox2.x &&
  207. bbox1.y > bbox2.y &&
  208. bbox1.x + bbox1.width < bbox2.x + bbox2.width &&
  209. bbox1.y + bbox1.height < bbox2.y + bbox2.height;
  210. }
  211. /* returns true if bbox1 and bbox2 are disjoint */
  212. function __isBBoxDisjoint(bbox1,bbox2)
  213. {
  214. return bbox1.x > bbox2.x + bbox2.width ||
  215. bbox1.x + bbox1.width < bbox2.x ||
  216. bbox1.y > bbox2.y + bbox2.height ||
  217. bbox1.y + bbox1.height < bbox2.y;
  218. }
  219. /* returns an L-based SVG path from a C-based one... does so by stripping away
  220. the control points in the C-path... this function won't modify the path
  221. provided it is composed only of straight lines */
  222. function __pathC2L(cpath)
  223. {
  224. return cpath.replace(
  225. /C[\d\.]*,[\d\.]*,[\d\.]*,[\d\.]*,([\d\.]*),([\d\.]*)/g,
  226. 'L$1,$2');
  227. }
  228. /* given a path, return 2 path strings that each represent one half of the path,
  229. as well as the center of the original path (for use in Link $segments)
  230. NOTE:: Raphael.path.getSubpath() appears to return incorrect results when its
  231. 2nd parameter is greater than the path length... incidently, probable
  232. internal rounding issues appear to confuse Raphael.path.getSubpath()
  233. when the 2nd parameter is equal to the path length... to avoid this
  234. 'bug' (which causes the 2nd half of the segment to be 'M0,0'), we
  235. substract a small epsilon from the path length for path2_2 */
  236. function __path2segments(path)
  237. {
  238. var eps = 0.001,
  239. length = path.getTotalLength(),
  240. center = path.getPointAtLength(length/2.0),
  241. path1_2 = __pathC2L(path.getSubpath(0,length/2.0)),
  242. path2_2 = __pathC2L(path.getSubpath(length/2.0,length-eps));
  243. return [path1_2,center,path2_2];
  244. }
  245. /* redraw the specified edge if it has changed */
  246. function __redrawEdge(edgeId,segments,linkStyle)
  247. {
  248. var icon = __edges[edgeId]['icon'],
  249. path = icon.pop(),
  250. _segments = String( path.attr('path') );
  251. if( segments != _segments )
  252. {
  253. path.attr('path', segments);
  254. __edges[edgeId]['segments'] = segments;
  255. }
  256. if( linkStyle != undefined )
  257. path.attr(linkStyle);
  258. icon.push( path );
  259. }
  260. /* remove an edge from the canvas and from internal data structures (i.e.,
  261. __edges and icon.edgesOut/In) if it isn't already removed */
  262. function __removeEdge(edgeId,ends)
  263. {
  264. ends = (utils.isArray(ends) ? ends : __edgeId2ends(edgeId));
  265. __edges[edgeId]['icon'].remove();
  266. delete __edges[edgeId];
  267. if( ends[0] in __icons )
  268. __icons[ends[0]]['edgesOut'] =
  269. __icons[ends[0]]['edgesOut'].filter(
  270. function(eId) {return eId != edgeId;});
  271. if( ends[1] in __icons )
  272. __icons[ends[1]]['edgesIn'] =
  273. __icons[ends[1]]['edgesIn'].filter(
  274. function(eId) {return eId != edgeId;});
  275. }
  276. /* sets specified icon's SVG.transform attribute based on its layout attributes
  277. 1. extract the given icon's position, orientation and scale attributes
  278. 2. use them to construct an appropriate SVG transformation string
  279. 3. set the icon's SVG.transform to the resulting transformation string
  280. NOTE:: we do not accumulate transformations... i.e., any transformation
  281. described by the icon's previous SVG.transform is overwritten */
  282. function __setIconTransform(uri)
  283. {
  284. var icon = __icons[uri]['icon'],
  285. x = icon.getAttr('__x'),
  286. y = icon.getAttr('__y'),
  287. r = icon.getAttr('__r'),
  288. sx = icon.getAttr('__sx'),
  289. sy = icon.getAttr('__sy');
  290. icon.setAttr('transform',
  291. 'translate('+x+','+y+') scale('+sx+','+sy+') rotate('+r+') ');
  292. }
  293. /* sets specified vobject's Raphael.transform attribute based on its layout
  294. attributes... see comments for __setIconTransform() for more info
  295. NOTE:: rotations and scales are relative to the *initial* top-left corner of
  296. the vobject (i.e., to its untransformed top-left corner) and are
  297. compounded with the *initial* scale and orientation
  298. NOTE:: for translations, both the 'x' and 'y' values are scaled against the
  299. vobject's width... this is because the ",__%" position component is
  300. used by the link decorator positioner to account for the *x* offset
  301. needed for vobjects to end at the same point as their associated links
  302. ... i.e., the ",__%" encodes a translation by a certain % of the
  303. vobject's width
  304. TBI:: eliminate need for trick in 2nd note by enabling bbox computations from
  305. backend (i.e., s.t. vobject width and absolute offset can be computed
  306. on the backend) */
  307. function __setVobjectTransform(vobj)
  308. {
  309. var x = __getVobjGeomAttrVal(vobj.node.getAttribute('__x')),
  310. y = __getVobjGeomAttrVal(vobj.node.getAttribute('__y')),
  311. r = __getVobjGeomAttrVal(vobj.node.getAttribute('__r')),
  312. sx = __getVobjGeomAttrVal(vobj.node.getAttribute('__sx')),
  313. sy = __getVobjGeomAttrVal(vobj.node.getAttribute('__sy')),
  314. bbox = vobj.getBBox();
  315. vobj.transform('t'+__getAbsoluteCoordinate(x.latest, bbox.width)+','
  316. +__getAbsoluteCoordinate(y.latest, bbox.width)+
  317. 'r'+r.latest+','+x.initial+','+y.initial+
  318. 's'+sx.latest+','+sy.latest+','+x.initial+','+y.initial);
  319. }
  320. /* translate a path by (dx,dy) without altering its transform property... this
  321. done by translating all of the path's control points */
  322. function __translatePath(path,dx,dy)
  323. {
  324. var _path = '';
  325. path.attr('path').forEach(
  326. function(cp)
  327. {
  328. _path += cp[0];
  329. for( var i=1; i<cp.length; i+=2 )
  330. _path += (cp[i]+dx)+','+(cp[i+1]+dy);
  331. });
  332. path.attr('path',_path);
  333. }
  334. /* sets the given text element's y property to half of its height... this
  335. function is used to compensate for the fact that an SVG.Text's (0,0) is
  336. located at (0,height/2) rather than at its top-left corner */
  337. function __valignText(t)
  338. {
  339. var lines = $(t.node).children().length,
  340. lineH = t.getBBox().height/2/lines,
  341. _lines = t.node.getAttribute('__valignLines') || 0,
  342. _lineH = t.node.getAttribute('__valignLineH') || 0,
  343. delta = lines*lineH - _lines*_lineH;
  344. t.attr('y',t.attr('y')+delta);
  345. t.node.setAttribute('__valignLineH',lineH);
  346. t.node.setAttribute('__valignLines',lines);
  347. }