jsPlumb-connectors-statemachine-1.3.6-RC1.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. /*
  2. * jsPlumb
  3. *
  4. * Title:jsPlumb 1.3.6
  5. *
  6. * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
  7. * elements, or VML.
  8. *
  9. * This file contains the state machine connectors.
  10. *
  11. * Thanks to Brainstorm Mobile Solutions for supporting the development of these.
  12. *
  13. * Copyright (c) 2010 - 2012 Simon Porritt (simon.porritt@gmail.com)
  14. *
  15. * http://jsplumb.org
  16. * http://github.com/sporritt/jsplumb
  17. * http://code.google.com/p/jsplumb
  18. *
  19. * Dual licensed under the MIT and GPL2 licenses.
  20. */
  21. ;(function() {
  22. var Line = function(x1, y1, x2, y2) {
  23. this.m = (y2 - y1) / (x2 - x1);
  24. this.b = -1 * ((this.m * x1) - y1);
  25. this.rectIntersect = function(x,y,w,h) {
  26. var results = [];
  27. // try top face
  28. // the equation of the top face is y = (0 * x) + b; y = b.
  29. var xInt = (y - this.b) / this.m;
  30. // test that the X value is in the line's range.
  31. if (xInt >= x && xInt <= (x + w)) results.push([ xInt, (this.m * xInt) + this.b ]);
  32. // try right face
  33. var yInt = (this.m * (x + w)) + this.b;
  34. if (yInt >= y && yInt <= (y + h)) results.push([ (yInt - this.b) / this.m, yInt ]);
  35. // bottom face
  36. var xInt = ((y + h) - this.b) / this.m;
  37. // test that the X value is in the line's range.
  38. if (xInt >= x && xInt <= (x + w)) results.push([ xInt, (this.m * xInt) + this.b ]);
  39. // try left face
  40. var yInt = (this.m * x) + this.b;
  41. if (yInt >= y && yInt <= (y + h)) results.push([ (yInt - this.b) / this.m, yInt ]);
  42. if (results.length == 2) {
  43. var midx = (results[0][0] + results[1][0]) / 2, midy = (results[0][1] + results[1][1]) / 2;
  44. results.push([ midx,midy ]);
  45. // now calculate the segment inside the rectangle where the midpoint lies.
  46. var xseg = midx <= x + (w / 2) ? -1 : 1,
  47. yseg = midy <= y + (h / 2) ? -1 : 1;
  48. results.push([xseg, yseg]);
  49. return results;
  50. }
  51. return null;
  52. };
  53. },
  54. _segment = function(x1, y1, x2, y2) {
  55. if (x1 <= x2 && y2 <= y1) return 1;
  56. else if (x1 <= x2 && y1 <= y2) return 2;
  57. else if (x2 <= x1 && y2 >= y1) return 3;
  58. return 4;
  59. },
  60. // the control point we will use depends on the faces to which each end of the connection is assigned, specifically whether or not the
  61. // two faces are parallel or perpendicular. if they are parallel then the control point lies on the midpoint of the axis in which they
  62. // are parellel and varies only in the other axis; this variation is proportional to the distance that the anchor points lie from the
  63. // center of that face. if the two faces are perpendicular then the control point is at some distance from both the midpoints; the amount and
  64. // direction are dependent on the orientation of the two elements. 'seg', passed in to this method, tells you which segment the target element
  65. // lies in with respect to the source: 1 is top right, 2 is bottom right, 3 is bottom left, 4 is top left.
  66. //
  67. // sourcePos and targetPos are arrays of info about where on the source and target each anchor is located. their contents are:
  68. //
  69. // 0 - absolute x
  70. // 1 - absolute y
  71. // 2 - proportional x in element (0 is left edge, 1 is right edge)
  72. // 3 - proportional y in element (0 is top edge, 1 is bottom edge)
  73. //
  74. _findControlPoint = function(midx, midy, segment, sourceEdge, targetEdge, dx, dy, distance, proximityLimit) {
  75. // TODO (maybe)
  76. // - if anchor pos is 0.5, make the control point take into account the relative position of the elements.
  77. if (distance <= proximityLimit) return [midx, midy];
  78. if (segment == 1) {
  79. if (sourceEdge[3] <= 0 && targetEdge[3] >= 1) return [ midx + (sourceEdge[2] < 0.5 ? -1 * dx : dx), midy ];
  80. else if (sourceEdge[2] >= 1 && targetEdge[2] <= 0) return [ midx, midy + (sourceEdge[3] < 0.5 ? -1 * dy : dy) ];
  81. else return [ midx + (-1 * dx) , midy + (-1 * dy) ];
  82. }
  83. else if (segment == 2) {
  84. if (sourceEdge[3] >= 1 && targetEdge[3] <= 0) return [ midx + (sourceEdge[2] < 0.5 ? -1 * dx : dx), midy ];
  85. else if (sourceEdge[2] >= 1 && targetEdge[2] <= 0) return [ midx, midy + (sourceEdge[3] < 0.5 ? -1 * dy : dy) ];
  86. else return [ midx + (1 * dx) , midy + (-1 * dy) ];
  87. }
  88. else if (segment == 3) {
  89. if (sourceEdge[3] >= 1 && targetEdge[3] <= 0) return [ midx + (sourceEdge[2] < 0.5 ? -1 * dx : dx), midy ];
  90. else if (sourceEdge[2] <= 0 && targetEdge[2] >= 1) return [ midx, midy + (sourceEdge[3] < 0.5 ? -1 * dy : dy) ];
  91. else return [ midx + (-1 * dx) , midy + (-1 * dy) ];
  92. }
  93. else if (segment == 4) {
  94. if (sourceEdge[3] <= 0 && targetEdge[3] >= 1) return [ midx + (sourceEdge[2] < 0.5 ? -1 * dx : dx), midy ];
  95. else if (sourceEdge[2] <= 0 && targetEdge[2] >= 1) return [ midx, midy + (sourceEdge[3] < 0.5 ? -1 * dy : dy) ];
  96. else return [ midx + (1 * dx) , midy + (-1 * dy) ];
  97. }
  98. };
  99. /*
  100. Function: StateMachine constructor
  101. Allowed parameters:
  102. curviness - measure of how "curvy" the connectors will be. this is translated as the distance that the
  103. Bezier curve's control point is from the midpoint of the straight line connecting the two
  104. endpoints, and does not mean that the connector is this wide. The Bezier curve never reaches
  105. its control points; they act as gravitational masses. defaults to 10.
  106. margin - distance from element to start and end connectors, in pixels. defaults to 5.
  107. proximityLimit - sets the distance beneath which the elements are consider too close together to bother with fancy
  108. curves. by default this is 80 pixels.
  109. loopbackRadius - the radius of a loopback connector. optional; defaults to 25.
  110. */
  111. jsPlumb.Connectors.StateMachine = function(params) {
  112. var self = this,
  113. currentPoints = null,
  114. _sx, _sy, _tx, _ty, _controlPoint = [],
  115. curviness = params.curviness || 10,
  116. margin = params.margin || 5,
  117. proximityLimit = params.proximityLimit || 80,
  118. clockwise = params.orientation && params.orientation == "clockwise",
  119. loopbackRadius = params.loopbackRadius || 25,
  120. isLoopback = false;
  121. this.type = "StateMachine";
  122. params = params || {};
  123. this.compute = function(sourcePos, targetPos, sourceEndpoint, targetEndpoint, sourceAnchor, targetAnchor, lineWidth, minWidth) {
  124. var w = Math.abs(sourcePos[0] - targetPos[0]),
  125. h = Math.abs(sourcePos[1] - targetPos[1]),
  126. // these are padding to ensure the whole connector line appears
  127. xo = 0.45 * w, yo = 0.45 * h;
  128. // these are padding to ensure the whole connector line appears
  129. w *= 1.9; h *= 1.9;
  130. //ensure at least one pixel width
  131. lineWidth = lineWidth || 1;
  132. var x = Math.min(sourcePos[0], targetPos[0]) - xo,
  133. y = Math.min(sourcePos[1], targetPos[1]) - yo;
  134. if (sourceEndpoint.elementId != targetEndpoint.elementId) {
  135. isLoopback = false;
  136. _sx = sourcePos[0] < targetPos[0] ? xo : w-xo;
  137. _sy = sourcePos[1] < targetPos[1] ? yo:h-yo;
  138. _tx = sourcePos[0] < targetPos[0] ? w-xo : xo;
  139. _ty = sourcePos[1] < targetPos[1] ? h-yo : yo;
  140. // now adjust for the margin
  141. if (sourcePos[2] == 0) _sx -= margin;
  142. if (sourcePos[2] == 1) _sx += margin;
  143. if (sourcePos[3] == 0) _sy -= margin;
  144. if (sourcePos[3] == 1) _sy += margin;
  145. if (targetPos[2] == 0) _tx -= margin;
  146. if (targetPos[2] == 1) _tx += margin;
  147. if (targetPos[3] == 0) _ty -= margin;
  148. if (targetPos[3] == 1) _ty += margin;
  149. //
  150. // these connectors are quadratic bezier curves, having a single control point. if both anchors
  151. // are located at 0.5 on their respective faces, the control point is set to the midpoint and you
  152. // get a straight line. this is also the case if the two anchors are within 'proximityLimit', since
  153. // it seems to make good aesthetic sense to do that. outside of that, the control point is positioned
  154. // at 'curviness' pixels away along the normal to the straight line connecting the two anchors.
  155. //
  156. // there may be two improvements to this. firstly, we might actually support the notion of avoiding nodes
  157. // in the UI, or at least making a good effort at doing so. if a connection would pass underneath some node,
  158. // for example, we might increase the distance the control point is away from the midpoint in a bid to
  159. // steer it around that node. this will work within limits, but i think those limits would also be the likely
  160. // limits for, once again, aesthetic good sense in the layout of a chart using these connectors.
  161. //
  162. // the second possible change is actually two possible changes: firstly, it is possible we should gradually
  163. // decrease the 'curviness' as the distance between the anchors decreases; start tailing it off to 0 at some
  164. // point (which should be configurable). secondly, we might slightly increase the 'curviness' for connectors
  165. // with respect to how far their anchor is from the center of its respective face. this could either look cool,
  166. // or stupid, and may indeed work only in a way that is so subtle as to have been a waste of time.
  167. //
  168. var _midx = (_sx + _tx) / 2, _midy = (_sy + _ty) / 2,
  169. m2 = (-1 * _midx) / _midy, theta2 = Math.atan(m2),
  170. dy = (m2 == Infinity || m2 == -Infinity) ? 0 : Math.abs(curviness / 2 * Math.sin(theta2)),
  171. dx = (m2 == Infinity || m2 == -Infinity) ? 0 : Math.abs(curviness / 2 * Math.cos(theta2)),
  172. segment = _segment(_sx, _sy, _tx, _ty),
  173. distance = Math.sqrt(Math.pow(_tx - _sx, 2) + Math.pow(_ty - _sy, 2));
  174. // calculate the control point. this code will be where we'll put in a rudimentary element avoidance scheme; it
  175. // will work by extending the control point to force the curve to be, um, curvier.
  176. _controlPoint = _findControlPoint(_midx,
  177. _midy,
  178. segment,
  179. sourcePos,
  180. targetPos,
  181. curviness, curviness,
  182. distance,
  183. proximityLimit);
  184. var requiredWidth = Math.max(Math.abs(_controlPoint[0] - _sx) * 3, Math.abs(_controlPoint[0] - _tx) * 3, Math.abs(_tx-_sx), 2 * lineWidth, minWidth),
  185. requiredHeight = Math.max(Math.abs(_controlPoint[1] - _sy) * 3, Math.abs(_controlPoint[1] - _ty) * 3, Math.abs(_ty-_sy), 2 * lineWidth, minWidth);
  186. if (w < requiredWidth) {
  187. var dw = requiredWidth - w;
  188. x -= (dw / 2);
  189. _sx += (dw / 2);
  190. _tx += (dw / 2);
  191. w = requiredWidth;
  192. _controlPoint[0] += (dw / 2);
  193. }
  194. if (h < requiredHeight) {
  195. var dh = requiredHeight - h;
  196. y -= (dh / 2);
  197. _sy += (dh / 2);
  198. _ty += (dh / 2);
  199. h = requiredHeight;
  200. _controlPoint[1] += (dh / 2);
  201. }
  202. currentPoints = [ x, y, w, h, _sx, _sy, _tx, _ty, _controlPoint[0], _controlPoint[1] ];
  203. }
  204. else {
  205. isLoopback = true;
  206. // a loopback connector. draw an arc from one anchor to the other.
  207. // i guess we'll do this the same way as the others. just the control point will be a fair distance away.
  208. var x1 = sourcePos[0], x2 = sourcePos[0], y1 = sourcePos[1] - margin, y2 = sourcePos[1] - margin,
  209. cx = x1, cy = y1 - loopbackRadius;
  210. // canvas sizing stuff, to ensure the whole painted area is visible.
  211. w = ((2 * lineWidth) + (4 * loopbackRadius)), h = ((2 * lineWidth) + (4 * loopbackRadius));
  212. x = cx - loopbackRadius - lineWidth - loopbackRadius, y = cy - loopbackRadius - lineWidth - loopbackRadius;
  213. currentPoints = [ x, y, w, h, cx-x, cy-y, loopbackRadius, clockwise, x1-x, y1-y, x2-x, y2-y];
  214. }
  215. return currentPoints;
  216. };
  217. var _makeCurve = function() {
  218. return [
  219. { x:_tx, y:_ty },
  220. { x:_controlPoint[0], y:_controlPoint[1] },
  221. { x:_controlPoint[0] + 1, y:_controlPoint[1] + 1},
  222. { x:_sx, y:_sy }
  223. ];
  224. };
  225. /**
  226. * returns the point on the connector's path that is 'location' along the length of the path, where 'location' is a decimal from
  227. * 0 to 1 inclusive. for the straight line connector this is simple maths. for Bezier, not so much.
  228. */
  229. this.pointOnPath = function(location) {
  230. if (isLoopback) {
  231. if (location > 0 && location < 1) location = 1- location;
  232. // current points are [ x, y, width, height, center x, center y, radius, clockwise, startx, starty, endx, endy ]
  233. // so the path length is the circumference of the circle
  234. //var len = 2 * Math.PI * currentPoints[6],
  235. // map 'location' to an angle. 0 is PI/2 when the connector is on the top face; if we
  236. // support other faces it will have to be calculated for each one. 1 is also PI/2.
  237. // 0.5 is -PI/2.
  238. var startAngle = (location * 2 * Math.PI) + (Math.PI / 2),
  239. startX = currentPoints[4] + (currentPoints[6] * Math.cos(startAngle)),
  240. startY = currentPoints[5] + (currentPoints[6] * Math.sin(startAngle));
  241. return {x:startX, y:startY};
  242. }
  243. else return jsBezier.pointOnCurve(_makeCurve(), location);
  244. };
  245. /**
  246. * returns the gradient of the connector at the given point.
  247. */
  248. this.gradientAtPoint = function(location) {
  249. if (isLoopback)
  250. return Math.atan(location * 2 * Math.PI);
  251. else
  252. return jsBezier.gradientAtPoint(_makeCurve(), location);
  253. };
  254. /**
  255. * for Bezier curves this method is a little tricky, cos calculating path distance algebraically is notoriously difficult.
  256. * this method is iterative, jumping forward .05% of the path at a time and summing the distance between this point and the previous
  257. * one, until the sum reaches 'distance'. the method may turn out to be computationally expensive; we'll see.
  258. * another drawback of this method is that if the connector gets quite long, .05% of the length of it is not necessarily smaller
  259. * than the desired distance, in which case the loop returns immediately and the arrow is mis-shapen. so a better strategy might be to
  260. * calculate the step as a function of distance/distance between endpoints.
  261. */
  262. this.pointAlongPathFrom = function(location, distance) {
  263. if (isLoopback) {
  264. if (location > 0 && location < 1) location = 1- location;
  265. var circumference = 2 * Math.PI * currentPoints[6],
  266. arcSpan = distance / circumference * 2 * Math.PI,
  267. startAngle = (location * 2 * Math.PI) - arcSpan + (Math.PI / 2),
  268. startX = currentPoints[4] + (currentPoints[6] * Math.cos(startAngle)),
  269. startY = currentPoints[5] + (currentPoints[6] * Math.sin(startAngle));
  270. return {x:startX, y:startY};
  271. }
  272. return jsBezier.pointAlongCurveFrom(_makeCurve(), location, distance);
  273. };
  274. };
  275. /*
  276. * Canvas state machine renderer.
  277. */
  278. jsPlumb.Connectors.canvas.StateMachine = function(params) {
  279. params = params || {};
  280. var self = this, drawGuideline = params.drawGuideline || true, avoidSelector = params.avoidSelector;
  281. jsPlumb.Connectors.StateMachine.apply(this, arguments);
  282. jsPlumb.CanvasConnector.apply(this, arguments);
  283. this._paint = function(dimensions) {
  284. if (dimensions.length == 10) {
  285. self.ctx.beginPath();
  286. self.ctx.moveTo(dimensions[4], dimensions[5]);
  287. self.ctx.quadraticCurveTo(dimensions[8], dimensions[9], dimensions[6], dimensions[7]);
  288. self.ctx.stroke();
  289. /*/ draw the guideline
  290. if (drawGuideline) {
  291. self.ctx.save();
  292. self.ctx.beginPath();
  293. self.ctx.strokeStyle = "silver";
  294. self.ctx.lineWidth = 1;
  295. self.ctx.moveTo(dimensions[4], dimensions[5]);
  296. self.ctx.lineTo(dimensions[6], dimensions[7]);
  297. self.ctx.stroke();
  298. self.ctx.restore();
  299. }
  300. //*/
  301. }
  302. else {
  303. // a loopback connector
  304. self.ctx.save();
  305. self.ctx.beginPath();
  306. var startAngle = 0, // Starting point on circle
  307. endAngle = 2 * Math.PI, // End point on circle
  308. clockwise = dimensions[7]; // clockwise or anticlockwise
  309. self.ctx.arc(dimensions[4],dimensions[5],dimensions[6],0, endAngle, clockwise);
  310. self.ctx.stroke();
  311. self.ctx.closePath();
  312. self.ctx.restore();
  313. }
  314. };
  315. this.createGradient = function(dim, ctx) {
  316. return ctx.createLinearGradient(dim[4], dim[5], dim[6], dim[7]);
  317. };
  318. };
  319. /*
  320. * SVG State Machine renderer
  321. */
  322. jsPlumb.Connectors.svg.StateMachine = function() {
  323. var self = this;
  324. jsPlumb.Connectors.StateMachine.apply(this, arguments);
  325. jsPlumb.SvgConnector.apply(this, arguments);
  326. this.getPath = function(d) {
  327. if (d.length == 10)
  328. return "M " + d[4] + " " + d[5] + " C " + d[8] + " " + d[9] + " " + d[8] + " " + d[9] + " " + d[6] + " " + d[7];
  329. else {
  330. // loopback
  331. return "M" + (d[8] + 4) + " " + d[9] + " A " + d[6] + " " + d[6] + " 0 1,0 " + (d[8]-4) + " " + d[9];
  332. }
  333. };
  334. };
  335. /*
  336. * VML state machine renderer
  337. */
  338. jsPlumb.Connectors.vml.StateMachine = function() {
  339. jsPlumb.Connectors.StateMachine.apply(this, arguments);
  340. jsPlumb.VmlConnector.apply(this, arguments);
  341. var _conv = jsPlumb.vml.convertValue;
  342. this.getPath = function(d) {
  343. if (d.length == 10) {
  344. return "m" + _conv(d[4]) + "," + _conv(d[5]) +
  345. " c" + _conv(d[8]) + "," + _conv(d[9]) + "," + _conv(d[8]) + "," + _conv(d[9]) + "," + _conv(d[6]) + "," + _conv(d[7]) + " e";
  346. }
  347. else {
  348. // loopback
  349. var left = _conv(d[8] - d[6]),
  350. top = _conv(d[9] - (2 * d[6])),
  351. right = left + _conv(2 * d[6]),
  352. bottom = top + _conv(2 * d[6]),
  353. posString = left + "," + top + "," + right + "," + bottom;
  354. var o = "ar " + posString + "," + _conv(d[8]) + ","
  355. + _conv(d[9]) + "," + _conv(d[8]) + "," + _conv(d[9]) + " e";
  356. return o;
  357. }
  358. };
  359. };
  360. })();
  361. /*
  362. // now for a rudimentary avoidance scheme. TODO: how to set this in a cross-library way?
  363. // if (avoidSelector) {
  364. // var testLine = new Line(sourcePos[0] + _sx,sourcePos[1] + _sy,sourcePos[0] + _tx,sourcePos[1] + _ty);
  365. // var sel = jsPlumb.getSelector(avoidSelector);
  366. // for (var i = 0; i < sel.length; i++) {
  367. // var id = jsPlumb.getId(sel[i]);
  368. // if (id != sourceEndpoint.elementId && id != targetEndpoint.elementId) {
  369. // o = jsPlumb.getOffset(id), s = jsPlumb.getSize(id);
  370. //
  371. // if (o && s) {
  372. // var collision = testLine.rectIntersect(o.left,o.top,s[0],s[1]);
  373. // if (collision) {
  374. // set the control point to be a certain distance from the midpoint of the two points that
  375. // the line crosses on the rectangle.
  376. // TODO where will this 75 number come from?
  377. // _controlX = collision[2][0] + (75 * collision[3][0]);
  378. // / _controlY = collision[2][1] + (75 * collision[3][1]);
  379. // }
  380. // }
  381. // }
  382. // }
  383. //}
  384. */