電信網路拓撲圖自動佈局之匯流排
在前面《電信網路拓撲圖自動佈局》一文中,我們大體介紹了 HT for Web 電信網路拓撲圖自動佈局的相關知識,但是都沒有深入地描述各種自動佈局的用法,我們今天在這邊就重點介紹匯流排的具體實現方案。
在 HT for Web 的連線手冊中,有說明可以自定義連線型別,通過 ht.Default.setEdgeType(type, func, mutual) 函式定義,我們今天要描述的匯流排也是通過這樣的方法來實現的。
這個函式名是 setEdgeType,顧名思義,它是用來自定義一個 EdgeType 的,那麼第一個引數 type 就是用來定義這個 EdgeType 的名稱,在 Edge 的樣式上設定 edge.type 屬性為 type 值,就可以讓這條連線使用是我們自定義的 EdgeType。
那麼第二個引數呢,就是用來計算連線的走線資訊的函式,這個回撥函式將會傳入四個引數,分別是:edge、gap、graphView、sameSourceWithFirstEdge,其中 edge 就是樣式上設定 edge.type 屬性為 type 值的連線物件,這個引數是最重要的,通常有這個引數就可以完成格式各樣的連線了。其他引數在手冊中都描述得很清楚,可以轉到手冊中閱讀,http://www.hightopo.com/guide/guide/plugin/edgetype/ht-edgetype-guide.html。
那這第三個引數呢,是決定連線是否影響起始或結束節點上的所有連線,這個引數解釋起來比較複雜,後續有機會的話,我們再詳細說明。
上圖中,可以看到節點間的連線並不是普通的直線,或者簡單的折線,而是漂亮的曲線,那麼這樣的曲線是怎麼生成的呢?既然將這個例子放到這邊作為案例,那麼它一定使用了自定義 EdgeType 的功能,觀察圖片可以發現曲線其實可以用二次方曲線來表示,所以呢,我們在 setEdgeType 函式的回撥中返回的連線走向資訊中,將其描述為一條二次方曲線就可以了。說得有些繞,我們來看看程式碼實現吧。
ht.Default.setEdgeType('custom', function(edge, gap, graphView, sameSourceWithFirstEdge){ var sourcePoint = edge.getSourceAgent().getPosition(), targetPoint = edge.getTargetAgent().getPosition(), points = new ht.List(); points.add(sourcePoint); points.add({ x: (sourcePoint.x + targetPoint.x)/2, y: (sourcePoint.y + targetPoint.y)/2 + 300 }); points.add(targetPoint); return { points: points, segments: new ht.List([1, 3]) }; });
從程式碼中可以看出,返回到頂點是連線的起點和終點,還有中間的二次方曲線的控制點,還有設定了頂點的連線方式,就是在 return 中的 segments,1 代表是路徑的起點,3 代表的是二次方曲線,這些相關知識點在 HT for Web 的形狀手冊中描述得很清楚,不懂的可以轉到手冊詳細瞭解,http://www.hightopo.com/guide/guide/core/shape/ht-shape-guide.html。
上圖就是一個匯流排的簡單例子,所有的節點都通過線條連結黑色的匯流排,連線的走向都是節點到匯流排的最短距離。
來講講整體的設計思路吧,其實總的來說,就是點的線的垂直點計算問題。那麼問題來了,碰到曲線怎麼辦?其實曲線也是可以微分成線條來處理的,至於這個線段的劃分精細度就需要使用者來自定義了。
但是,像上圖所示的橢圓形匯流排該如何處理呢?對於這種有固定表示式的形狀,我們就不需要用曲線分割的方法來做匯流排佈局了,我們完全可以獲取到圓或者橢圓上的一點,所以在處理圓和橢圓上,我們獲取 Edge 連邊節點中線連成線,然後計算出夾角,通過圓或者橢圓的三角函式表示法計算出總線上的一點,這樣來構成連線。
在上圖中,我們用到了 ShapeLayout 來自動佈局和匯流排相連的節點,讓其相對均勻地分佈在匯流排周圍,對於 ShapeLayout 的相關設計思路我們在後面的章節中再具體介紹。
那麼我們今天的內容就到這裡了,對於匯流排的設計是不是很簡單呢,下面附上匯流排的所有程式碼,有需要的話,可以直接複製出來,在頁面中引入 HT for Web 的核心包 ht.js 後面引入以下程式碼就可以直接使用匯流排功能了。程式碼不多,也就將近 200 行,感興趣的朋友可以通讀一遍,有什麼問題,還請不吝賜教。
;(function(window, ht) {
var getPoint = function(node, outPoint) {
var rect = node.getRect(),
pos = node.getPosition(),
p = ht.Default.intersectionLineRect(pos, outPoint, rect);
if (p) return { x: p[0], y: p[1] };
return pos;
};
var pointToInsideLine = function(p1, p2, p) {
var x1 = p1.x,
y1 = p1.y,
x2 = p2.x,
y2 = p2.y,
x = p.x,
y = p.y,
result = {},
dx = x2 - x1,
dy = y2 - y1,
d = Math.sqrt(dx * dx + dy * dy),
ca = dx / d, // cosine
sa = dy / d, // sine
mX = (-x1 + x) * ca + (-y1 + y) * sa;
result.x = x1 + mX * ca;
result.y = y1 + mX * sa;
if (!isPointInLine(result, p1, p2)) {
result.x = Math.abs(result.x - p1.x) < Math.abs(result.x - p2.x) ? p1.x : p2.x;
result.y = Math.abs(result.y - p1.y) < Math.abs(result.y - p2.y) ? p1.y : p2.y;
}
dx = x - result.x;
dy = y - result.y;
result.z = Math.sqrt(dx * dx + dy * dy);
return result;
};
var isPointInLine = function(p, p1, p2) {
return p.x >= Math.min(p1.x, p2.x) &&
p.x <= Math.max(p1.x, p2.x) &&
p.y >= Math.min(p1.y, p2.y) &&
p.y <= Math.max(p1.y, p2.y);
};
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 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 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 getPointInOval = function(l, r, p1, p2) {
var a = Math.atan2(p2.y - p1.y, p2.x - p1.x);
return { x: l * Math.cos(a) + p1.x, y: r * Math.sin(a) + p1.y };
};
ht.Default.setEdgeType('bus', function(edge, gap, graphView, sameSourceWithFirstEdge) {
var source = edge.getSourceAgent(),
target = edge.getTargetAgent(),
shapeList = ['circle', 'oval'],
shape, beginNode, endNode;
if (shapeList.indexOf(source.s('shape')) >= 0) {
shape = source.s('shape');
beginNode = source;
endNode = target;
}
else if (shapeList.indexOf(target.s('shape')) >= 0) {
shape = target.s('shape');
beginNode = target;
endNode = source;
}
if (shapeList.indexOf(shape) >= 0) {
var w = beginNode.getWidth(),
h = beginNode.getHeight(),
l = Math.max(w, h) / 2,
r = Math.min(w, h) / 2;
if (shape === 'circle') l = r = Math.min(l, r);
var p = getPointInOval(l, r, beginNode.getPosition(), endNode.getPosition());
return {
points: new ht.List([ p, getPoint(endNode, p) ]),
segments: new ht.List([ 1, 2 ])
};
}
var segments, points, endPoint;
if (source instanceof ht.Shape) {
segments = source.getSegments();
points = source.getPoints();
beginNode = source;
endPoint = target.getPosition();
endNode = target;
}
else if (target instanceof ht.Shape) {
segments = target.getSegments();
points = target.getPoints();
beginNode = target;
endPoint = source.getPosition();
endNode = source;
}
if (!points) {
return {
points: new ht.List([
getPoint(source, target.getPosition()),
getPoint(target, source.getPosition())
]),
segments: new ht.List([ 1, 2 ])
};
}
if (!segments && points) {
segments = new ht.List();
points.each(function() { segments.add(2); });
segments.set(0, 1);
}
var segLen = segments.size(),
segV, segNextV, beginPoint, j,
p1, p2, p3, p4, p, tP1, tP2, tRes,
curveResolution = beginNode.a('edge.curve.resolution') || 50,
pointsIndex = 0;
for (var i = 0; i < segLen - 1; i++) {
segNextV = segments.get(i + 1);
if (segNextV === 1) {
pointsIndex++;
continue;
}
p1 = points.get(pointsIndex++);
if (segNextV === 2 || segNextV === 5) {
p2 = points.get((segNextV === 5) ? 0 : pointsIndex);
p = pointToInsideLine(p1, p2, endPoint);
if (!beginPoint || beginPoint.z > p.z)
beginPoint = p;
}
else if (segNextV === 3) {
p2 = points.get(pointsIndex++);
p3 = points.get(pointsIndex);
tP2 = { x: p1.x, y: p1.y };
for (j = 1; j <= curveResolution; j++) {
tP1 = tP2;
tRes = j / curveResolution;
tP2 = {
x: bezier2(tRes, p1.x, p2.x, p3.x),
y: bezier2(tRes, p1.y, p2.y, p3.y),
};
p = pointToInsideLine(tP1, tP2, endPoint);
if (!beginPoint || beginPoint.z > p.z)
beginPoint = p;
}
}
else if (segNextV === 4) {
p2 = points.get(pointsIndex++);
p3 = points.get(pointsIndex++);
p4 = points.get(pointsIndex);
tP2 = { x: p1.x, y: p1.y };
for (j = 1; j <= curveResolution; j++) {
tP1 = tP2;
tRes = j / curveResolution;
tP2 = {
x: bezier3(tRes, p1.x, p2.x, p3.x, p4.x),
y: bezier3(tRes, p1.y, p2.y, p3.y, p4.y),
};
p = pointToInsideLine(tP1, tP2, endPoint);
if (!beginPoint || beginPoint.z > p.z)
beginPoint = p;
}
}
}
endPoint = getPoint(endNode, beginPoint);
return {
points: new ht.List([
{ x: beginPoint.x, y: beginPoint.y },
endPoint
]),
segments: new ht.List([ 1, 2 ])
};
});
}(window, ht));