最近有客户提到自定义链路的需求,个人感觉非常有代表意义,现在共享出来给大家参考一下。先来看看需求:
- 链路要分成两半,用两种颜色填充。
- 填充百分比在不同值域时,用不同颜色。
- 显示刻度
- 有个开关,可以控制链路变短,变短后,链路只画开始和结束部分(相当于原始链路的缩影),中间不画
- 如果有多条链路,链路合并后两端分别显示这些链路中的最高填充百分比
合并前:
合并后:
- 进入子网后,节点上显示和上层节点的连线信息
进入子网前,节点2和子网内节点4之间有链路:
进入子网后,节点4上也显示此链路:
先看看实现的效果,后面我们慢慢解释如何定制链路:
前5个需求可以通过自定义Link和LinkUI实现,需要注意:
- 用Link#getBundleLinks获取所有的捆绑链路
- 用LinkUI#drawLinePoints画线
完整代码如下:
1 // 自定义Link构造函数
2 demo.ScaleLink = function(id, from, to) {
3 // 调用基类构造函数
4 demo.ScaleLink.superClass.constructor.call(this, id, from, to);
5 // 设置链路宽度为10个像素
6 this.setStyle('link.width', 10);
7 //this.setStyle('link.color', 'rgba(0, 0, 0, 0)');
8 // 设置Link类型为平行
9 this.setStyle('link.type', 'parallel');
10 // 设置链路捆绑的间距为40
11 this.setStyle('link.bundle.offset', 40);
12 // 设置刻度颜色
13 this.setClient('scaleColor', 'black');
14 // 设置刻度宽度
15 this.setClient('scaleWidth', 1);
16 // 设置刻度个数
17 this.setClient('scaleNumbers', 4);
18 // 设置是否变短
19 this.setClient('shortened', false);
20 // 设置变短后的长度
21 this.setClient('shortenLength', 100);
22 // 设置分割线颜色
23 this.setClient('splitterColor', 'black');
24 // 设置起始填充百分比
25 this.setClient('fromFillPercent', 0);
26 // 设置结束填充百分比
27 this.setClient('toFillPercent', 0);
28 };
29 // 设置自定义Link继承twaver.Link
30 twaver.Util.ext('demo.ScaleLink', twaver.Link, {
31 // 重载获取UI类方法,返回自定义UI类
32 getCanvasUIClass : function () {
33 return demo.ScaleLinkUI;
34 },
35 // 根据百分比获取填充颜色
36 getFillColor: function(percent) {
37 if (percent < 0.25) {
38 return 'green';
39 }
40 if (percent < 0.5) {
41 return 'yellow';
42 }
43 if (percent < 0.75) {
44 return 'magenta';
45 }
46 return 'red';
47 },
48 // 获取起始填充颜色
49 getFromFillColor: function () {
50 return this.getFillColor(this.getFromFillPercent());
51 },
52 // 获取结束填充颜色
53 getToFillColor: function () {
54 return this.getFillColor(this.getToFillPercent());
55 },
56 // 获取起始百分比
57 getFromFillPercent: function () {
58 // 如果是链路捆绑代理,返回所有捆绑链路中填充百分比最大的值
59 if (this.isBundleAgent()) {
60 var fromAgent = this.getFromAgent(),
61 percentKey, maxPercent = 0, percent;
62 this.getBundleLinks().forEachSiblingLink(function (link) {
63 percentKey = fromAgent === link.getFromAgent() ? 'fromFillPercent' : 'toFillPercent';
64 percent = link.getClient(percentKey);
65 maxPercent = percent > maxPercent ? percent : maxPercent;
66 });
67 return maxPercent;
68 } else {
69 return this.getClient('fromFillPercent');
70 }
71 },
72 // 获取结束百分比
73 getToFillPercent: function () {
74 // 如果是链路捆绑代理,返回所有捆绑链路中填充百分比最大的值
75 if (this.isBundleAgent()) {
76 var toAgent = this.getToAgent(),
77 percentKey, maxPercent = 0, percent;
78 this.getBundleLinks().forEachSiblingLink(function (link) {
79 percentKey = toAgent === link.getToAgent() ? 'toFillPercent' : 'fromFillPercent';
80 percent = link.getClient(percentKey);
81 maxPercent = percent > maxPercent ? percent : maxPercent;
82 });
83 return maxPercent;
84 } else {
85 return this.getClient('toFillPercent');
86 }
87 },
88 // 重载获取网元名称方法,判断如果是链路捆绑代理,就返回起始和结束代理节点的名称
89 getName: function () {
90 if (this.getClient('shortened')) {
91 return null;
92 } else if (this.isBundleAgent()) {
93 return this.getFromAgent().getName() + '-' + this.getToAgent().getName();
94 } else {
95 return demo.ScaleLink.superClass.getName.call(this);
96 }
97 }
98 });
99
100 // 自定义LinkUI构造函数
101 demo.ScaleLinkUI = function(network, element){
102 // 调用基类构造函数
103 demo.ScaleLinkUI.superClass.constructor.call(this, network, element);
104 };
105 // 设置自定义Link继承twaver.canvas.LinkUI
106 twaver.Util.ext('demo.ScaleLinkUI', twaver.canvas.LinkUI, {
107 // 获取Link角度
108 getAngle: function () {
109 return getAngle(this.getFromPoint(), this.getToPoint());
110 },
111 // 获取Link中间点
112 getMiddlePoint: function (from, to, percent) {
113 return {
114 x: from.x + (to.x - from.x) * percent,
115 y: from.y + (to.y - from.y) * percent
116 };
117 },
118 // 画刻度线
119 drawScaleLine: function (from, to, angle, length, ctx, percent, lineWidth, lineColor) {
120 var point = this.getMiddlePoint(from, to, percent);
121 var y = length/2 * Math.sin(angle),
122 x = length/2 * Math.cos(angle);
123 ctx.beginPath();
124 ctx.lineWidth = lineWidth;
125 ctx.strokeStyle = lineColor;
126 ctx.moveTo(point.x + x, point.y + y);
127 ctx.lineTo(point.x - x, point.y -y);
128 ctx.stroke();
129 },
130 // 获取是否将链路变短
131 isShorten: function () {
132 var link = this.getElement();
133 return link.getClient('shortened') && this.getLineLength() > link.getClient('shortenLength') * 2;
134 },
135 // 重载画链路函数,用自定义逻辑画链路
136 paintBody: function (ctx) {
137 var points = this.getLinkPoints(),
138 link = this.getElement();
139 if (!points || points.size() < 2) {
140 return;
141 }
142
143 var lineLength = this.getLineLength(),
144 shortenLength = link.getClient('shortenLength'),
145 percent = shortenLength / lineLength,
146 from = points.get(0),
147 to = points.get(1),
148 angle = this.getAngle() + Math.PI/2;
149 if (this.isShorten()) {
150 fromPoints = new twaver.List([from, this.getMiddlePoint(from, to, percent)]);
151 toPoints = new twaver.List([this.getMiddlePoint(from, to, 1 - percent), to]);
152 this._paintBody(ctx, fromPoints, angle);
153 this._paintBody(ctx, toPoints, angle);
154
155 // 画文字
156 ctx.textAlign = 'center';
157 ctx.textBaseline = 'middle';
158 ctx.fillStyle = 'black';
159 var textCenter = {x: (fromPoints.get(0).x + fromPoints.get(1).x)/2, y: (fromPoints.get(0).y + fromPoints.get(1).y)/2};
160 ctx.fillText(link.getName(), textCenter.x, textCenter.y);
161
162 textCenter = {x: (toPoints.get(0).x + toPoints.get(1).x)/2, y: (toPoints.get(0).y + toPoints.get(1).y)/2};
163 ctx.fillText(link.getName(), textCenter.x, textCenter.y);
164
165 ctx.fillText(link.getToNode().getName(), fromPoints.get(1).x, fromPoints.get(1).y);
166 ctx.fillText(link.getFromNode().getName(), toPoints.get(0).x, toPoints.get(0).y);
167 } else {
168 this._paintBody(ctx, points, angle);
169 }
170
171 // 画起始箭头
172 if (link.getClient('arrow.from')) {
173 twaver.Util.drawArrow(ctx, 12, 9, points, true, 'arrow.standard', true, 'gray', 0, 0, 1, 'black');
174 }
175 // 画结束箭头
176 if (link.getClient('arrow.to')) {
177 twaver.Util.drawArrow(ctx, 12, 9, points, false, 'arrow.standard', true, 'gray', 0, 0, 1, 'black');
178 }
179 },
180 _paintBody: function (ctx, points, angle) {
181 var link = this.getElement(),
182 width = link.getStyle('link.width'),
183 grow = width,
184 outerColor = this.getOuterColor();
185 if (outerColor) {
186 var outerWidth = link.getStyle('outer.width');
187 grow += outerWidth * 2;
188 }
189 var selectBorder = !this.getEditAttachment() && link.getStyle('select.style') === 'border' && this.getNetwork().isSelected(link);
190 if (selectBorder) {
191 var selectWidth = link.getStyle('select.width');
192 grow += selectWidth * 2;
193 }
194 ctx.lineCap = link.getStyle('link.cap');
195 ctx.lineJoin = link.getStyle('link.join');
196 // 画选中边框
197 if (selectBorder) {
198 this.drawLinePoints(ctx, points, grow, link.getStyle('select.color'));
199 }
200 // 画边框
201 if (outerColor) {
202 this.drawLinePoints(ctx, points, width + outerWidth * 2, outerColor);
203 }
204 // 画Link
205 this.drawLinePoints(ctx, points, width, this.getInnerColor() || link.getStyle('link.color'));
206
207 var fromFillPercent = link.getFromFillPercent(),
208 toFillPercent = link.getToFillPercent(),
209 fromFillColor = link.getFromFillColor(),
210 toFillColor = link.getToFillColor(),
211 from = points.get(0),
212 to = points.get(1);
213
214 var x = from.x + (to.x - from.x) / 2 * fromFillPercent,
215 y = from.y + (to.y - from.y) / 2 * fromFillPercent;
216 var middle = {x: x, y: y};
217 var fromPoints = new twaver.List([from, middle]);
218 // 画起始填充色
219 this.drawLinePoints(ctx, fromPoints, width, fromFillColor);
220
221 from = points.get(1);
222 to = points.get(0);
223 x = from.x + (to.x - from.x) / 2 * toFillPercent;
224 y = from.y + (to.y - from.y) / 2 * toFillPercent;
225 middle = {x: x, y: y};
226 var toPoints = new twaver.List([from, middle]);
227 // 画结束填充色
228 this.drawLinePoints(ctx, toPoints, width, toFillColor);
229
230 from = points.get(0);
231 to = points.get(1);
232 var scaleWidth = link.getClient('scaleWidth'),
233 scaleColor = link.getClient('scaleColor');
234 // 画刻度
235 for (var i = 1, n = link.getClient('scaleNumbers') * 2; i < n; i++) {
236 this.drawScaleLine(from, to, angle, width/2, ctx, i/n, scaleWidth, scaleColor);
237 }
238 // 画分隔线
239 this.drawScaleLine(from, to, angle, width, ctx, 0.5, 3, link.getClient('splitterColor'));
240 }
241 });
最后一个需求可以通过定制Node和NodeUI达到目的:
1 // 自定义Node构造函数
2 demo.ScaleNode = function(id) {
3 // 调用基类构造函数
4 demo.ScaleNode.superClass.constructor.call(this, id);
5 };
6 // 设置自定义Node继承twaver.Node
7 twaver.Util.ext('demo.ScaleNode', twaver.Node, {
8 getCanvasUIClass: function () {
9 return demo.ScaleNodeUI;
10 }
11 });
12
13 // 自定义NodeUI构造函数
14 demo.ScaleNodeUI = function(network, element){
15 // 调用基类构造函数
16 demo.ScaleNodeUI.superClass.constructor.call(this, network, element);
17 };
18 // 设置自定义NodeUI继承twaver.canvas.NodeUI
19 twaver.Util.ext('demo.ScaleNodeUI', twaver.canvas.NodeUI, {
20 // 重载画网元方法,画上层链路
21 paintBody: function (ctx) {
22 demo.ScaleNodeUI.superClass.paintBody.call(this, ctx);
23 var result = this.getAttachedLinks();
24 if (!result) {
25 return;
26 }
27 for (var position in result) {
28 this.paintLink(ctx, result[position], position);
29 }
30 },
31 // 画链路
32 paintLink: function (ctx, links, position) {
33 var center = this.getElement().getCenterLocation(),
34 count = links.length,
35 half = count / 2,
36 network = this.getNetwork(),
37 gap = (count - 1) * -10,
38 terminal, link, i, offset, shortenLength, angle, tempCenter, textWidth, textHeight = 20, textCenter;
39 for (i=0; i<count; i++) {
40 link = links[i];
41 offset = link.getStyle('link.bundle.offset');
42 shortenLength = link.getClient('shortenLength');
43 textWidth = ctx.measureText(link.getName()).width;
44 if (position === 'left') {
45 terminal = {x: center.x - offset - shortenLength, y: center.y + gap};
46 tempCenter = {x: center.x - offset, y: center.y + gap};
47 textCenter = {x: terminal.x - textWidth/2 - 10, y: terminal.y};
48 angle = Math.PI/2;
49 } else if (position === 'right') {
50 terminal = {x: center.x + offset + shortenLength, y: center.y + gap};
51 tempCenter = {x: center.x + offset, y: center.y + gap};
52 textCenter = {x: terminal.x + textWidth/2 + 10, y: terminal.y};
53 angle = Math.PI/2;
54 } else if (position === 'top') {
55 terminal = {x: center.x + gap, y: center.y - offset - shortenLength};
56 tempCenter = {x: center.x + gap, y: center.y - offset};
57 textCenter = {x: terminal.x, y: terminal.y - 10};
58 angle = 0;
59 } else {
60 terminal = {x: center.x + gap, y: center.y + offset + shortenLength};
61 tempCenter = {x: center.x + gap, y: center.y + offset};
62 textCenter = {x: terminal.x, y: terminal.y + 10};
63 angle = 0;
64 }
65 gap += 20;
66 var isFrom = link.getFromNode() === this.getElement(),
67 points;
68 if (isFrom) {
69 points = new twaver.List([tempCenter, terminal]);
70 } else {
71 points = new twaver.List([terminal, tempCenter]);
72 }
73 network.getElementUI(link)._paintBody(ctx, points, angle);
74
75 ctx.textAlign = 'center';
76 ctx.textBaseline = 'middle';
77 ctx.fillStyle = 'black';
78 // 另一端节点标签
79 var name = isFrom ? link.getToNode().getName() : link.getFromNode().getName();
80 ctx.fillText(name, textCenter.x, textCenter.y);
81 textCenter = {x: (tempCenter.x + terminal.x)/2, y: (tempCenter.y + terminal.y)/2};
82 // Link标签
83 ctx.fillText(link.getName(), textCenter.x, textCenter.y);
84
85 // 画起始箭头
86 if (link.getClient('arrow.from')) {
87 twaver.Util.drawArrow(ctx, 12, 9, points, true, 'arrow.standard', true, 'gray', 0, 0, 1, 'black');
88 }
89 // 画结束箭头
90 if (link.getClient('arrow.to')) {
91 twaver.Util.drawArrow(ctx, 12, 9, points, false, 'arrow.standard', true, 'gray', 0, 0, 1, 'black');
92 }
93 }
94 },
95 // 获取不同方位的上层链路集合
96 getAttachedLinks: function () {
97 var currentSubNetwork = this.getNetwork().getCurrentSubNetwork();
98 if (!currentSubNetwork || !this.getElement().getLinks()) {
99 return null;
100 }
101 var result;
102 this.getElement().getLinks().forEach(function (link) {
103 var fromSubNetwork = twaver.Util.getSubNetwork(link.getFromNode()),
104 toSubNetwork = twaver.Util.getSubNetwork(link.getToNode());
105 if (fromSubNetwork !== toSubNetwork) {
106 if (!result) {
107 result = {};
108 }
109 var fromCenter = link.getFromNode().getCenterLocation(),
110 toCenter = link.getToNode().getCenterLocation(),
111 angle = getAngle(fromCenter, toCenter),
112 isOut = currentSubNetwork === fromSubNetwork,
113 position;
114 if (isOut) {
115 if (fromCenter.x <= toCenter.x) {
116 if (angle >= -Math.PI/4 && angle <= Math.PI/4) {
117 position = 'right';
118 } else if (angle > Math.PI/4) {
119 position = 'bottom';
120 } else {
121 position = 'top';
122 }
123 } else {
124 if (angle >= -Math.PI/4 && angle <= Math.PI/4) {
125 position = 'left';
126 } else if (angle > Math.PI/4) {
127 position = 'top';
128 } else {
129 position = 'bottom';
130 }
131 }
132 } else {
133 if (fromCenter.x <= toCenter.x) {
134 if (angle >= -Math.PI/4 && angle <= Math.PI/4) {
135 position = 'left';
136 } else if (angle > Math.PI/4) {
137 position = 'top';
138 } else {
139 position = 'bottom';
140 }
141 } else {
142 if (angle >= -Math.PI/4 && angle <= Math.PI/4) {
143 position = 'right';
144 } else if (angle > Math.PI/4) {
145 position = 'bottom';
146 } else {
147 position = 'top';
148 }
149 }
150 }
151 if (!result[position]) {
152 result[position] = [];
153 }
154 result[position].push(link);
155 }
156 });
157 return result;
158 }
159 });
本文完整代码见附件:见原文最下方