在前面《电信网络拓扑图自动布局之总线》一文中,我们重点介绍了自定义 EdgeType 的使用,概括了实现总线效果的设计思路,那么今天话题是基于 HT for Web 的曲线布局(ShapeLayout)。

ShapeLayout 从字面上的意思理解,就是根据曲线路径来布局节点,省去手动布局节点的繁琐操作,还能保证平滑整齐地排布,这是手动调整很难做到的。ShapeLayout 结合前面提到的总线,是最普遍的应用。

电信网络拓扑图自动布局之曲线布局-LMLPHP

http://www.hightopo.com/demo/EdgeType/ShapeLayout-Oval.html

我们先来看看最简单的圆和椭圆是如何实现自动布局的。我们知道在几何学中,圆和椭圆是可以用三角函数老表示,那么我们就可以将圆或者椭圆分成若干份,通过三角函数就可以算出圆或椭圆上的一点,将节点放到计算出来的点的位置,这样就可以达到自动布局的效果。具体的核心代码如下:

点击(此处)折叠或打开

  1. var radians = Math.PI * 2 / nodeCount,
  2.     w = width / 2,
  3.     h = height / 2,
  4.     a = Math.max(w, h),
  5.     b = Math.min(w, h),
  6.     x, y, rad, node; if (shape === 'circle') a = b = Math.min(a, b); for (var i = 0; i < nodeCount; i++) {
  7.     rad = radians * i;
  8.     x = a * Math.cos(rad) + position.x + offset.x;
  9.     y = b * Math.sin(rad) + position.y + offset.y;
  10.     node = this._nodes[i]; if (!node) continue; if (!anim)
  11.         node.setPosition({ x: x, y: y }); else {
  12.         anim.action = function(pBegin, pEnd, v) { this.setPosition({
  13.                 x: pBegin.x + (pEnd.x - pBegin.x) * v,
  14.                 y: pBegin.y + (pEnd.y - pBegin.y) * v
  15.             });
  16.         }.bind(node, node.getPosition(), { x: x, y: y });
  17.         ht.Default.startAnim(anim);
  18.     }
  19. }

当然,会有人会问,对椭圆按照角度平均分成若干份计算出来的位置并不是等距的,没错,确实不是等距的,这这边就简单处理了,如果要弧度等距的话,那这个就真麻烦了,在这边就不做阐述了,也没办法阐述,因为我也不懂。

电信网络拓扑图自动布局之曲线布局-LMLPHP

http://www.hightopo.com/demo/EdgeType/ShapeLayout.html

如上图的例子,节点沿着某条曲线均匀布局,那么这种不是特殊形状的连线组合是怎么实现自动布局的呢?其实也很简单,在前面总线章节中就有提到,将曲线分割若干小线段,每次计算固定长度,当判断落点在某条线段上的时候,就可以将问题转换为求线段上一点的数学问题,和总线一样,曲线的切割精度需要用户来定义,在不同的应用场景中,需求可能不太一样。

点击(此处)折叠或打开

  1. preP = beginP; var nodeIndex = 0, indexLength, node; for (; i < pointsCount;) {
  2.     p = this._calculationPoints[i];
  3.     indexLength = padding + resolution * nodeIndex; if (p.totalLength < indexLength) {
  4.         preP = p;
  5.         i++; continue;
  6.     }
  7.     node = this._nodes[nodeIndex++]; if (!node) break;
  8.     
  9.     dis = indexLength - preP.totalLength;
  10.     tP = getPointWithLength(dis, preP.point, p.point);
  11.     
  12.     p = {
  13.         x: tP.x + position.x + offset.x - width / 2,
  14.         y: tP.y + position.y + offset.y - height / 2 }; if (!anim)
  15.         node.setPosition(p); else {
  16.         anim.action = function(pBegin, pEnd, v) { this.setPosition({
  17.                 x: pBegin.x + (pEnd.x - pBegin.x) * v,
  18.                 y: pBegin.y + (pEnd.y - pBegin.y) * v
  19.             });
  20.         }.bind(node, node.getPosition(), p);
  21.         ht.Default.startAnim(anim);
  22.     }
  23.     
  24.     preP = {
  25.         point: tP,
  26.         distance: dis,
  27.         totalLength: indexLength
  28.     };
  29. }

以上就是非特殊形状的连线组合的核心代码,这也只是代码片段,可能理解起来还是会比较吃力的,那么下面我将贴上源代码,有兴趣的朋友可以帮忙瞅瞅,有什么不妥的,欢迎指出。

点击(此处)折叠或打开

  1. ;(function(window, ht) { var distance = function(p1, p2) { var dx = p2.x - p1.x,
  2.             dy = p2.y - p1.y; return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
  3.     }; var bezier2 = function(t, p0, p1, p2) { var t1 = 1 - t; return t1*t1*p0 + 2*t*t1*p1 + t*t*p2;
  4.     }; var bezier3 = function(t, p0, p1, p2, p3 ) { var t1 = 1 - t; return t1*t1*t1*p0 + 3*t1*t1*t*p1 + 3*t1*t*t*p2 + t*t*t*p3;
  5.     }; var getPointWithLength = function(length, p1, p2) { var dis = distance(p1, p2),
  6.             temp = length / dis,
  7.             dx = p2.x - p1.x,
  8.             dy = p2.y - p1.y; return { x: p1.x + dx * temp, y: p1.y + dy * temp };
  9.     }; var ShapeLayout = ht.ShapeLayout = function() {};

  10.     ht.Default.def('ht.ShapeLayout', Object, {
  11.         ms_fire: 1,
  12.         ms_ac: ['padding', 'offset', 'shape', 'closePath', 'position', 'width', 'height'],
  13.         
  14.         calculationSize: function() { if (!this._points) return; var min = { x: Infinity, y: Infinity},
  15.                 max = { x: -Infinity, y: -Infinity},
  16.                 p, len = this._points.length; for (var i = 0; i < len; i++) {
  17.                 p = this._points[i];
  18.                 min.x = Math.min(min.x, p.x);
  19.                 min.y = Math.min(min.y, p.y);
  20.                 max.x = Math.max(max.x, p.x);
  21.                 max.y = Math.max(max.y, p.y);
  22.             } this._width = max.x - min.x; this._height = max.y - min.y; this._position = {
  23.                 x: min.x + this._width / 2,
  24.                 y: min.y + this._height / 2 };
  25.         },
  26.         
  27.         _points: null,
  28.         getPoints: function() { return this._points; },
  29.         setPoints: function(value) { if (value instanceof Array) this._points = value.slice(0); else if (value instanceof ht.List) this._points = value._as.slice(0); else this._points = null; this.__calcuPoints = !!this._points; this.calculationSize();
  30.         },
  31.         
  32.         _segments: null,
  33.         getSegments: function() { return this._segments; },
  34.         setSegments: function(value) { if (value instanceof Array) this._segments = value.slice(0); else if (value instanceof ht.List) this._segments = value._as.slice(0); else this._segments = null; this.__calcuPoints = !!this._segments;
  35.         },
  36.         
  37.         _style: {},
  38.         s: function() { return this.setStyle.apply(this, arguments);
  39.         },
  40.         setStyle: function() { var name = arguments[0],
  41.                 value = arguments[1]; if (arguments.length === 1) { if (typeof name === 'object'){ for (var n in name) this._style[n] = name[n];
  42.                 } else return this._style[name];
  43.             } else this._style[name] = value;
  44.         },
  45.         
  46.         _nodes: null,
  47.         getNodes: function() { return this._nodes; },
  48.         setNodes: function(value) { if (value instanceof Array) this._nodes = value.slice(0); else if (value instanceof ht.List) this._nodes = value._as.slice(0); else this._nodes = null;
  49.         },
  50.         addNode: function(node) { if (!this._nodes) this._nodes = []; this._nodes.push(node);
  51.         },
  52.         
  53.         _calculationPoints: [],
  54.         splitPoints: function() { if (!this._points || this._points.length === 0) {
  55.                 alert('Please set points with setPoints method!'); return;
  56.             } var points = this._points.slice(0),
  57.                 segments; if (!this._segments || this._segments.length === 0) {
  58.                 segments = points.map(function(p, index) { return 2; });
  59.                 segments[0] = 1;
  60.             } else {
  61.                 segments = this._segments.slice(0);
  62.             } this._calculationPoints.length = 0; var beginPoint = points[0],
  63.                 preP = {
  64.                     point: { x: beginPoint.x, y: beginPoint.y },
  65.                     distance: 0,
  66.                     totalLength: 0 }; this._calculationPoints.push(preP); var length = segments.length,
  67.                 pointIndex = 1, seg, p, tP, dis,
  68.                 p0, p1, p2, p3, j,
  69.                 curveResolution = this.s('curve.resolution') || 50; var calcuPoints = function(currP) {
  70.                 dis = distance(preP.point, currP);
  71.                 p = {
  72.                     point: { x: currP.x, y: currP.y },
  73.                     distance: dis,
  74.                     totalLength: preP.totalLength + dis
  75.                 }; this._calculationPoints.push(p);
  76.                 preP = p;
  77.             }.bind(this); for (var i = 1; i < length; i++) {
  78.                 seg = segments[i]; if (seg === 1) {
  79.                     tP = points[pointIndex++];
  80.                     p = {
  81.                         point: { x: tP.x, y: tP.y },
  82.                         distance: 0,
  83.                         totalLength: preP.totalLength
  84.                     }; this._calculationPoints.push(p);
  85.                     preP = p;
  86.                 } else if (seg === 2) { calcuPoints(points[pointIndex++]); } else if (seg === 3) {
  87.                     p1 = points[pointIndex++];
  88.                     p2 = points[pointIndex++];
  89.                     p0 = preP.point; for (j = 1; j <= curveResolution; j++) {
  90.                         tP = {
  91.                             x: bezier2(j / curveResolution, p0.x, p1.x, p2.x),
  92.                             y: bezier2(j / curveResolution, p0.y, p1.y, p2.y)
  93.                         };
  94.                         calcuPoints(tP);
  95.                     }
  96.                 } else if (seg === 4) {
  97.                     p1 = points[pointIndex++];
  98.                     p2 = points[pointIndex++];
  99.                     p3 = points[pointIndex++];
  100.                     p0 = preP.point; for (j = 1; j <= curveResolution; j++) {
  101.                         tP = {
  102.                             x: bezier3(j / curveResolution, p0.x, p1.x, p2.x, p3.x),
  103.                             y: bezier3(j / curveResolution, p0.y, p1.y, p2.y, p3.y)
  104.                         };
  105.                         calcuPoints(tP);
  106.                     }
  107.                 } else if (seg === 5) {
  108.                     tP = this._calculationPoints[0].point;
  109.                     calcuPoints(tP);
  110.                 }
  111.             } this._totalLength = preP.totalLength;
  112.         },
  113.         
  114.         layout: function(anim) { if (!this._nodes || this._nodes.length === 0) {
  115.                 alert('Please set nodes width setNode method!'); return;
  116.             } var nodeCount = this._nodes.length,
  117.                 shape = this._shape,
  118.                 shapeList = ['circle', 'oval'],
  119.                 offset = this._offset || { x: 0, y: 0 },
  120.                 position = this._position || { x: 0, y: 0 },
  121.                 width = this._width || 0,
  122.                 height = this._height || 0; if (shape && shapeList.indexOf(shape) >= 0) { var radians = Math.PI * 2 / nodeCount,
  123.                     w = width / 2,
  124.                     h = height / 2,
  125.                     a = Math.max(w, h),
  126.                     b = Math.min(w, h),
  127.                     x, y, rad, node; if (shape === 'circle') a = b = Math.min(a, b); for (var i = 0; i < nodeCount; i++) {
  128.                     rad = radians * i;
  129.                     x = a * Math.cos(rad) + position.x + offset.x;
  130.                     y = b * Math.sin(rad) + position.y + offset.y;
  131.                     node = this._nodes[i]; if (!node) continue; if (!anim)
  132.                         node.setPosition({ x: x, y: y }); else {
  133.                         anim.action = function(pBegin, pEnd, v) { this.setPosition({
  134.                                 x: pBegin.x + (pEnd.x - pBegin.x) * v,
  135.                                 y: pBegin.y + (pEnd.y - pBegin.y) * v
  136.                             });
  137.                         }.bind(node, node.getPosition(), { x: x, y: y });
  138.                         ht.Default.startAnim(anim);
  139.                     }
  140.                 } return;
  141.             } if (!this._calculationPoints || this.__calcuPoints) this.splitPoints(); var padding = this._padding || 0,
  142.                 length = this._totalLength - 2 * padding,
  143.                 resolution = length / (nodeCount - (this._closePath ? 0 : 1)),
  144.                 i = 1, p, preP, beginP, dis,
  145.                 pointsCount = this._calculationPoints.length; for (; i < pointsCount; i++) {
  146.                 p = this._calculationPoints[i]; if (p.totalLength < padding) continue;
  147.                 preP = this._calculationPoints[i - 1];
  148.                 dis = padding - preP.totalLength;
  149.                 beginP = {
  150.                     point: getPointWithLength(dis, preP.point, p.point),
  151.                     distance: p.distance - dis,
  152.                     totalLength: padding
  153.                 }; break;
  154.             }
  155.             
  156.             preP = beginP; var nodeIndex = 0, indexLength, node; for (; i < pointsCount;) {
  157.                 p = this._calculationPoints[i];
  158.                 indexLength = padding + resolution * nodeIndex; if (p.totalLength < indexLength) {
  159.                     preP = p;
  160.                     i++; continue;
  161.                 }
  162.                 node = this._nodes[nodeIndex++]; if (!node) break;
  163.                 
  164.                 dis = indexLength - preP.totalLength;
  165.                 tP = getPointWithLength(dis, preP.point, p.point);
  166.                 
  167.                 p = {
  168.                     x: tP.x + position.x + offset.x - width / 2,
  169.                     y: tP.y + position.y + offset.y - height / 2 }; if (!anim)
  170.                     node.setPosition(p); else {
  171.                     anim.action = function(pBegin, pEnd, v) { this.setPosition({
  172.                             x: pBegin.x + (pEnd.x - pBegin.x) * v,
  173.                             y: pBegin.y + (pEnd.y - pBegin.y) * v
  174.                         });
  175.                     }.bind(node, node.getPosition(), p);
  176.                     ht.Default.startAnim(anim);
  177.                 }
  178.                 
  179.                 preP = {
  180.                     point: tP,
  181.                     distance: dis,
  182.                     totalLength: indexLength
  183.                 };
  184.             }
  185.         }
  186.     });
  187. }(window, ht));

 

10-17 16:17