svg_utils.js 13 KB

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