電信網路拓撲圖自動佈局之曲線佈局

圖撲軟體發表於2016-09-19

在前面《電信網路拓撲圖自動佈局之匯流排》一文中,我們重點介紹了自定義 EdgeType 的使用,概括了實現匯流排效果的設計思路,那麼今天話題是基於 HT for Web 的曲線佈局(ShapeLayout)。

ShapeLayout 從字面上的意思理解,就是根據曲線路徑來佈局節點,省去手動佈局節點的繁瑣操作,還能保證平滑整齊地排布,這是手動調整很難做到的。ShapeLayout 結合前面提到的匯流排,是最普遍的應用。

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

我們先來看看最簡單的圓和橢圓是如何實現自動佈局的。我們知道在幾何學中,圓和橢圓是可以用三角函式老表示,那麼我們就可以將圓或者橢圓分成若干份,通過三角函式就可以算出圓或橢圓上的一點,將節點放到計算出來的點的位置,這樣就可以達到自動佈局的效果。具體的核心程式碼如下:

 

var radians = Math.PI * 2 / nodeCount,
    w = width / 2,
    h = height / 2,
    a = Math.max(w, h),
    b = Math.min(w, h),
    x, y, rad, node;
if (shape === 'circle') a = b = Math.min(a, b);
for (var i = 0; i < nodeCount; i++) {
    rad = radians * i;
    x = a * Math.cos(rad) + position.x + offset.x;
    y = b * Math.sin(rad) + position.y + offset.y;
    node = this._nodes[i];
    if (!node) continue;
    if (!anim)
        node.setPosition({ x: x, y: y });
    else {
        anim.action = function(pBegin, pEnd, v) {
            this.setPosition({
                x: pBegin.x + (pEnd.x - pBegin.x) * v,
                y: pBegin.y + (pEnd.y - pBegin.y) * v
            });
        }.bind(node, node.getPosition(), { x: x, y: y });
        ht.Default.startAnim(anim);
    }
}

 

當然,會有人會問,對橢圓按照角度平均分成若干份計算出來的位置並不是等距的,沒錯,確實不是等距的,這這邊就簡單處理了,如果要弧度等距的話,那這個就真麻煩了,在這邊就不做闡述了,也沒辦法闡述,因為我也不懂。

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

如上圖的例子,節點沿著某條曲線均勻佈局,那麼這種不是特殊形狀的連線組合是怎麼實現自動佈局的呢?其實也很簡單,在前面匯流排章節中就有提到,將曲線分割若干小線段,每次計算固定長度,當判斷落點在某條線段上的時候,就可以將問題轉換為求線段上一點的數學問題,和匯流排一樣,曲線的切割精度需要使用者來定義,在不同的應用場景中,需求可能不太一樣。

 

preP = beginP;
var nodeIndex = 0, indexLength, node;
for (; i < pointsCount;) {
    p = this._calculationPoints[i];
    indexLength = padding + resolution * nodeIndex;
    if (p.totalLength < indexLength) {
        preP = p;
        i++;
        continue;
    }
    node = this._nodes[nodeIndex++];
    if (!node) break;
    
    dis = indexLength - preP.totalLength;
    tP = getPointWithLength(dis, preP.point, p.point);
    
    p = { 
        x: tP.x + position.x + offset.x - width / 2,
        y: tP.y + position.y + offset.y - height / 2
    };
    if (!anim)
        node.setPosition(p);
    else {
        anim.action = function(pBegin, pEnd, v) {
            this.setPosition({
                x: pBegin.x + (pEnd.x - pBegin.x) * v,
                y: pBegin.y + (pEnd.y - pBegin.y) * v
            });
        }.bind(node, node.getPosition(), p);
        ht.Default.startAnim(anim);
    }
    
    preP = {
        point: tP,
        distance: dis,
        totalLength: indexLength
    };
}

 

以上就是非特殊形狀的連線組合的核心程式碼,這也只是程式碼片段,可能理解起來還是會比較吃力的,那麼下面我將貼上原始碼,有興趣的朋友可以幫忙瞅瞅,有什麼不妥的,歡迎指出。

 

;(function(window, ht) {
    var distance = function(p1, p2) {
        var dx = p2.x - p1.x,
            dy = p2.y - p1.y;
        return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
    };
    var bezier2 = function(t, p0, p1, p2) {
        var t1 = 1 - t;
        return t1*t1*p0 + 2*t*t1*p1 + t*t*p2;
    };
    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;
    };
    var getPointWithLength = function(length, p1, p2) {
        var dis = distance(p1, p2),
            temp = length / dis,
            dx = p2.x - p1.x,
            dy = p2.y - p1.y;
        return { x: p1.x + dx * temp, y: p1.y + dy * temp };
    };
    
    var ShapeLayout = ht.ShapeLayout = function() {};

    ht.Default.def('ht.ShapeLayout', Object, {
        ms_fire: 1,
        ms_ac: ['padding', 'offset', 'shape', 'closePath', 'position', 'width', 'height'],
        
        calculationSize: function() {
            if (!this._points) return;
            var min = { x: Infinity, y: Infinity}, 
                max = { x: -Infinity, y: -Infinity}, 
                p, len = this._points.length;
            for (var i = 0; i < len; i++) {
                p = this._points[i];
                min.x = Math.min(min.x, p.x);
                min.y = Math.min(min.y, p.y);
                max.x = Math.max(max.x, p.x);
                max.y = Math.max(max.y, p.y);
            }
            this._width = max.x - min.x;
            this._height = max.y - min.y;
            this._position = {
                x: min.x + this._width / 2,
                y: min.y + this._height / 2
            };
        },
        
        _points: null,
        getPoints: function() { return this._points; },
        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();
        },
        
        _segments: null,
        getSegments: function() { return this._segments; },
        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;
        },
        
        _style: {},
        s: function() { 
            return this.setStyle.apply(this, arguments);
        },
        setStyle: function() {
            var name = arguments[0],
                value = arguments[1];
            if (arguments.length === 1) {
                if (typeof name === 'object'){
                    for (var n in name) 
                        this._style[n] = name[n];
                }
                else 
                    return this._style[name];
            }
            else 
                this._style[name] = value;
        },
        
        _nodes: null,
        getNodes: function() { return this._nodes; },
        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;
        },
        addNode: function(node) {
            if (!this._nodes) this._nodes = [];
            this._nodes.push(node);
        },
        
        _calculationPoints: [],
        splitPoints: function() {
            if (!this._points || this._points.length === 0) {
                alert('Please set points with setPoints method!');
                return;
            }
            
            var points = this._points.slice(0),
                segments;
            if (!this._segments || this._segments.length === 0) {
                segments = points.map(function(p, index) { return 2; });
                segments[0] = 1;
            }
            else {
                segments = this._segments.slice(0);
            }
            
            this._calculationPoints.length = 0;
            var beginPoint = points[0],
                preP = {
                    point: { x: beginPoint.x, y: beginPoint.y },
                    distance: 0,
                    totalLength: 0
                };
            this._calculationPoints.push(preP);
            var length = segments.length,
                pointIndex = 1, seg, p, tP, dis,
                p0, p1, p2, p3, j,
                curveResolution = this.s('curve.resolution') || 50;
                
            var calcuPoints = function(currP) {
                dis = distance(preP.point, currP);
                p = {
                    point: { x: currP.x, y: currP.y },
                    distance: dis,
                    totalLength: preP.totalLength + dis
                };
                this._calculationPoints.push(p);
                preP = p;
            }.bind(this);
            for (var i = 1; i < length; i++) {
                seg = segments[i];
                if (seg === 1) {
                    tP = points[pointIndex++];
                    p = {
                        point: { x: tP.x, y: tP.y },
                        distance: 0,
                        totalLength: preP.totalLength
                    };
                    this._calculationPoints.push(p);
                    preP = p;
                }
                else if (seg === 2) { calcuPoints(points[pointIndex++]); }
                else if (seg === 3) {
                    p1 = points[pointIndex++];
                    p2 = points[pointIndex++];
                    p0 = preP.point;
                    for (j = 1; j <= curveResolution; j++) {
                        tP = {
                            x: bezier2(j / curveResolution, p0.x, p1.x, p2.x),
                            y: bezier2(j / curveResolution, p0.y, p1.y, p2.y)
                        };
                        calcuPoints(tP);
                    }
                }
                else if (seg === 4) {
                    p1 = points[pointIndex++];
                    p2 = points[pointIndex++];
                    p3 = points[pointIndex++];
                    p0 = preP.point;
                    for (j = 1; j <= curveResolution; j++) {
                        tP = {
                            x: bezier3(j / curveResolution, p0.x, p1.x, p2.x, p3.x),
                            y: bezier3(j / curveResolution, p0.y, p1.y, p2.y, p3.y)
                        };
                        calcuPoints(tP);
                    }
                }
                else if (seg === 5) {
                    tP = this._calculationPoints[0].point;
                    calcuPoints(tP);
                }
            }
            this._totalLength = preP.totalLength;
        },
        
        layout: function(anim) {
            if (!this._nodes || this._nodes.length === 0) {
                alert('Please set nodes width setNode method!');
                return;
            }
            
            var nodeCount = this._nodes.length,
                shape = this._shape,
                shapeList = ['circle', 'oval'],
                offset = this._offset || { x: 0, y: 0 },
                position = this._position || { x: 0, y: 0 },
                width = this._width || 0,
                height = this._height || 0;
            if (shape && shapeList.indexOf(shape) >= 0) {
                var radians = Math.PI * 2 / nodeCount,
                    w = width / 2,
                    h = height / 2,
                    a = Math.max(w, h),
                    b = Math.min(w, h),
                    x, y, rad, node;
                if (shape === 'circle') a = b = Math.min(a, b);
                for (var i = 0; i < nodeCount; i++) {
                    rad = radians * i;
                    x = a * Math.cos(rad) + position.x + offset.x;
                    y = b * Math.sin(rad) + position.y + offset.y;
                    node = this._nodes[i];
                    if (!node) continue;
                    if (!anim)
                        node.setPosition({ x: x, y: y });
                    else {
                        anim.action = function(pBegin, pEnd, v) {
                            this.setPosition({
                                x: pBegin.x + (pEnd.x - pBegin.x) * v,
                                y: pBegin.y + (pEnd.y - pBegin.y) * v
                            });
                        }.bind(node, node.getPosition(), { x: x, y: y });
                        ht.Default.startAnim(anim);
                    }
                }
                return;
            }
            
            if (!this._calculationPoints || this.__calcuPoints)
                this.splitPoints();
            
            var padding = this._padding || 0,
                length = this._totalLength - 2 * padding,
                resolution = length / (nodeCount - (this._closePath ? 0 : 1)),
                i = 1, p, preP, beginP, dis,
                pointsCount = this._calculationPoints.length;
            for (; i < pointsCount; i++) {
                p = this._calculationPoints[i];
                if (p.totalLength < padding) continue;
                preP = this._calculationPoints[i - 1];
                dis = padding - preP.totalLength;
                beginP = {
                    point: getPointWithLength(dis, preP.point, p.point),
                    distance: p.distance - dis,
                    totalLength: padding
                };
                break;
            }
            
            preP = beginP;
            var nodeIndex = 0, indexLength, node;
            for (; i < pointsCount;) {
                p = this._calculationPoints[i];
                indexLength = padding + resolution * nodeIndex;
                if (p.totalLength < indexLength) {
                    preP = p;
                    i++;
                    continue;
                }
                node = this._nodes[nodeIndex++];
                if (!node) break;
                
                dis = indexLength - preP.totalLength;
                tP = getPointWithLength(dis, preP.point, p.point);
                
                p = { 
                    x: tP.x + position.x + offset.x - width / 2,
                    y: tP.y + position.y + offset.y - height / 2
                };
                if (!anim)
                    node.setPosition(p);
                else {
                    anim.action = function(pBegin, pEnd, v) {
                        this.setPosition({
                            x: pBegin.x + (pEnd.x - pBegin.x) * v,
                            y: pBegin.y + (pEnd.y - pBegin.y) * v
                        });
                    }.bind(node, node.getPosition(), p);
                    ht.Default.startAnim(anim);
                }
                
                preP = {
                    point: tP,
                    distance: dis,
                    totalLength: indexLength
                };
            }
        }
    });
}(window, ht));

 

 

相關文章