使用 jsPlumb 绘制拓扑图 —— 异步加载与绘制的实现


        本文实现的方法可以边异步加载数据边绘制拓扑图。 有若干点需要说明一下:

        1.  一次性获取所有数据并绘制拓扑图, 请参见文章: <使用 JsPlumb 绘制拓扑图的通用方法> ; 本文实现的最终显示效果与之类似, 所使用的基本方法与之类似。

        2.  在此次实现中, 可以一边异步加载数据一边绘制拓扑图, 是动态可扩展的; 

        3.  所有影响节点位置、布局的配置均放置在最前面, 便于修改, 避免在代码中穿梭, 浪费时间;

        4.  布局算法比之前的实现更加完善;

        5.  此实现由于与业务逻辑绑得比较紧, 可复用的部分不多, 但是可以作为一个模板, 用在读者自己的场景中, 自行修改相应的节点类型、URL等。

        6.  添加了附着点的点击事件处理, 可以刷新显示关联实体;

        7.  主流程很简单:  发送 AJAX 请求获取数据 ---> 创建节点(实际上就是DIV) ---> 计算节点位置、布局 ---> 添加节点附着点 ---> 缓存节点连接 ---> 连接所有现有的缓存节点连接。  多个 AJAX 请求的处理是异步的, 顺序没有控制。

        8.  代码:   

  1 /**
  2  * 使用 jsPlumb 根据指定的拓扑数据结构绘制拓扑图
  3  * 使用 drawTopo_asyn(vmName, regionNo, parentDivId) 方法
  4  */
  5 /**
  6  * 初始化拓扑图实例及外观设置
  7  */
  8 (function() {
  9     
 10     jsPlumb.importDefaults({
 11         
 12         DragOptions : { cursor: 'pointer', zIndex:2000 },
 13     
 14         EndpointStyles : [{ fillStyle:'#225588' }, { fillStyle:'#558822' }],
 15     
 16         Endpoints : [ [ "Dot", { radius:2 } ], [ "Dot", { radius: 2 } ]],
 17     
 18         ConnectionOverlays : [
 19             [ "Label", { location:1 } ],
 20             [ "Label", { 
 21                 location:0.1,
 22                 id:"label",
 23                 cssClass:"aLabel"
 24             }]
 25         ]
 26     });
 27     
 28     var connectorPaintStyle = {
 29         lineWidth: 1,
 30         strokeStyle: "#096EBB",
 31         joinstyle:"round",
 32         outlineColor: "#096EBB",
 33         outlineWidth: 1
 34     };
 35     
 36     var connectorHoverStyle = {
 37         lineWidth: 2,
 38         strokeStyle: "#5C96BC",
 39         outlineWidth: 2,
 40         outlineColor:"white"
 41     };
 42     
 43     var endpointHoverStyle = {
 44         fillStyle:"#5C96BC"
 45     };
 46     
 47     window.topoDrawUtil = {
 48             
 49         sourceEndpoint: {
 50             endpoint:"Dot",
 51             paintStyle:{ 
 52                 strokeStyle:"#1e8151",
 53                 fillStyle:"transparent",
 54                 radius: 4,
 55                 lineWidth:2 
 56             },                
 57             isSource:true,
 58             maxConnections:-1,
 59             connector:[ "Flowchart", { stub:[40, 60], gap:1, cornerRadius:5, alwaysRespectStubs:true } ],                                                
 60             connectorStyle: connectorPaintStyle,
 61             hoverPaintStyle: endpointHoverStyle,
 62             connectorHoverStyle: connectorHoverStyle,
 63             dragOptions:{},
 64             overlays:[
 65                 [ "Label", { 
 66                     location:[0.5, 1.5], 
 67                     label:"",
 68                     cssClass:"endpointSourceLabel" 
 69                 } ]
 70             ]
 71         },
 72         
 73         targetEndpoint: {
 74             endpoint: "Dot",                    
 75             paintStyle: { fillStyle:"#1e8151",radius: 2 },
 76             hoverPaintStyle: endpointHoverStyle,
 77             maxConnections:-1,
 78             dropOptions:{ hoverClass:"hover", activeClass:"active" },
 79             isTarget:true,
 80             overlays:[
 81                 [ "Label", { location:[0.5, -0.5], label:"", cssClass:"endpointTargetLabel" } ]
 82             ]
 83         },
 84         
 85         initConnection: function(connection) {
 86             connection.getOverlay("label").setLabel(connection.sourceId + "-" + connection.targetId);
 87             connection.bind("editCompleted", function(o) {
 88                 if (typeof console != "undefined")
 89                     console.log("connection edited. path is now ", o.path);
 90             });
 91         },    
 92         
 93         removeAllEndPoints: function(nodeDivId) {
 94             jsPlumb.removeAllEndpoints($('#'+nodeDivId)); 
 95         },
 96         addEndpoints: function(toId, sourceAnchors, targetAnchors) {
 97             for (var i = 0; i < sourceAnchors.length; i++) {
 98                 var sourceUUID = toId + sourceAnchors[i];
 99                 var endPoint = jsPlumb.addEndpoint(toId, this.sourceEndpoint, { anchor:sourceAnchors[i], uuid:sourceUUID });    
100                 endPoint.bind("click", function(endpoint) {
101                     var anchorType = endpoint.anchor.type;
102                     var nodeType = toId.split('_')[0];
103                     var content = toId.split('_')[1];
104                     if (nodeType == VM_TYPE) {
105                         switch (anchorType) {
106                             case 'Right':
107                                 cacheKey = 'VM-DEVICE-'+ vmNodeData.key;
108                                 cacheConnectionData[cacheKey] = null;
109                                 linkDevices(vmNodeData, vmNodeData.key);
110                                 break;
111                             case 'Top':
112                                 cacheKey = 'VM-ACCOUNT-'+ vmNodeData.key;
113                                 cacheConnectionData[cacheKey] = null;
114                                 vmName = vmNodeData.key;
115                                 regionNo = vmNodeData.data.region_no;
116                                 linkAccount(vmNodeData, vmName, regionNo);
117                                 break;
118                             case 'Bottom':
119                                 cacheKey = 'VM-NC-'+ vmNodeData.key;
120                                 cacheConnectionData[cacheKey] = null;
121                                 ncId = vmNodeData.data.nc_id;
122                                 regionNo = vmNodeData.data.region_no;
123                                 linkNc(vmNodeData, ncId, regionNo);
124                                 break;
125                             case 'Left':
126                                 cacheKey = 'VM-VIP-'+ vmNodeData.key;
127                                 cacheConnectionData[cacheKey] = null;
128                                 vmInnerIp = vmNodeData.data.vm_inner_ip;
129                                 linkVips(vmNodeData, vmInnerIp);
130                                 break;
131                             default:
132                                 break;
133                         }
134                     }
135                     else if (nodeType == DEVICE_TYPE) {
136                         if (anchorType == 'Bottom') {
137                             cacheKey = 'DEVICE-SNAPSHOT-'+ content;
138                             cacheConnectionData[cacheKey] = null;
139                             deviceNodeData = deviceNodeDataMapping[content];
140                             linkSnapshot(deviceNodeData.data.aliUid, content, deviceNodeData);
141                         }
142                     }
143                 });
144             }
145             for (var j = 0; j < targetAnchors.length; j++) {
146                 var targetUUID = toId + targetAnchors[j];
147                 jsPlumb.addEndpoint(toId, this.targetEndpoint, { anchor:targetAnchors[j], uuid:targetUUID });                        
148             }
149         }
150     };
151 })();
152 //////////////////////////////////////////////////////////////////////////////
153 // 这里将所有用到的数据结构汇聚在这里, 避免修改时需要在代码中穿行, 浪费时间
154 /**
155  * 重新刷新VM关联实体时需要使用到VM的信息,这里进行全局缓存,避免重复查询
156  * 重新刷新VM关联实体时VM必定存在, vmNodeData 也必定是最近一次查询的结果
157  */
158 var vmNodeData = {};
159 /**
160  * 重新刷新磁盘关联快照实体时需要使用到磁盘的信息,这里进行全局缓存,避免重复查询
161  * 重新刷新磁盘关联快照实体时磁盘必定存在,且必定是最近一次查询的结果
162  * eg. {'instanceId': { "ecsInstanceId": "vmName", "houyiDiskId": "102-80012003",
163                         "aliUid": aliUidNum,  "instanceId": "d-28ilj8rsf", ... }}
164  */
165 var deviceNodeDataMapping = {};
166 /**
167  * 拓扑图中的节点类型
168  */
169 var nodeTypeArray = ['VM', 'DEVICE', 'NC', 'VIP', 'SNAPSHOT', 'CLUSTER', 'AVZ', 'ACCOUNT'];
170 var VM_TYPE = nodeTypeArray[0];
171 var DEVICE_TYPE = nodeTypeArray[1];
172 var NC_TYPE = nodeTypeArray[2];
173 var VIP_TYPE = nodeTypeArray[3];
174 var SNAPSHOT_TYPE= nodeTypeArray[4];
175 var CLUSTER_TYPE= nodeTypeArray[5];
176 var AVZ_TYPE= nodeTypeArray[6];
177 var ACCOUNT_TYPE= nodeTypeArray[7];
178 /**
179  * cacheConnectionData 节点之间的已有连接数目缓存, 在计算节点位置及布局方法 computeLayout 中用到
180  * eg. {
181  *    'VM-DEVICE-vmkey': 2,  'VM-NC-vmkey':1,  'VM-VIP-vmkey':2 , 'VM-ACCOUNT-vmkey': 1,
182  * } 
183  * 表示已经有2磁盘/1NC/2VIP/1ACCOUNT 与VM(key 为 vmkey)连接, 这些信息用于计算与VM相连接的同类型的下一个实体的相对位置
184  */
185 var cacheConnectionData = {};
186 /**
187  * 连接关系的存储, 在 cacheConnections , reconnectAll 方法中用到
188  * 由于重复节点会移动到新的位置,原有连接会出现断连现象, 因此采用"每次刷新拉取新实体时重连所有连线" 的策略, 可以保证实时性, 只要连接数不多重复连接的开销是可以接受的.
189  * connections = [[startPoint1, endPoint1], [startPoint2, endPoint2], ..., [startPointN, endPointN]];
190  */
191 var connections = [];
192 /**
193  * 实体与实体上附着点方向的设置
194  * DEVICE_TYPE: [['Right', 'Bottom'], ['Left']] 的含义是:
195  * 对于DEVICE实体: 作为起始节点时, 附着点可以在右方中点(连接CLUSTER), 下方中点(连接快照); 作为终止节点时, 附着点仅在左方中点(连接VM) 
196  */
197 var entityEndPointsMapping = {
198     "VM": [['Top', 'Bottom', 'Right', 'Left'], []],
199     "DEVICE": [['Right', 'Bottom'], ['Left']],
200     "NC": [['Bottom'], ['Top']],
201     "VIP": [[], ['Right']],
202     "SNAPSHOT": [[], ['Top']],
203     "CLUSTER": [[], ['Left', 'Top']],
204     "AVZ": [['Bottom'], ['Top']],
205     "ACCOUNT": [[], ['Bottom']]
206 };
207 /**
208  * 连接线附着点方向设置
209  * "VM-ACCOUNT": ['Top', 'Bottom'] 的含义是:
210  * VM 的上方附着点 与 ACCOUNT 的下方附着点的连接
211  */
212 var connectionDirectionMapping = {
213     "VM-ACCOUNT": ['Top', 'Bottom'],
214     "VM-NC": ['Bottom', 'Top'],
215     "NC-CLUSTER": ['Bottom', 'Top'],
216     "VM-DEVICE": ['Right', 'Left'],
217     "DEVICE-CLUSTER": ['Right', 'Left'],
218     "VM-VIP": ['Left', 'Right'],
219     "DEVICE-SNAPSHOT": ['Bottom', 'Top']
220 }
221 /**
222  * 节点之间的水平与垂直相对位置
223  */
224 var largeVerticalDistance = 270;
225 var verticalDistance = 220;
226 var horizontalDistance = 300;
227 var shortVerticalDistance = 50;
228 var shortHorizontalDistance = 220;
229 /**
230  * 节点之间的水平或垂直相对位置和距离的设置
231  * "VM-DEVICE": [largeVerticalDistance, horizontalDistance] 
232  */
233 var connectionDistanceMapping = {
234     "VM-ACCOUNT": [-verticalDistance, 0],
235     "VM-NC": [shortVerticalDistance, 0],
236     "NC-CLUSTER": [shortVerticalDistance, 0],
237     "VM-DEVICE": [largeVerticalDistance, horizontalDistance],
238     "DEVICE-CLUSTER": [-108, horizontalDistance],
239     "VM-VIP": [verticalDistance, -horizontalDistance],
240     "DEVICE-SNAPSHOT": [shortVerticalDistance, shortHorizontalDistance]
241 }
242 /**
243  * 根节点位置
244  */
245 rootPosition = [220, 360];
246 rootTop = rootPosition[0];
247 rootLeft = rootPosition[1];
248 var parentDiv = null;
249 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
250 function drawtopo_asyn(vmName, regionNo, parentDivId) {
251     
252     parentDiv = $('#'+parentDivId);
253     
254     var vmInfoReq = {
255         'url':     httpPrefix + '/controllers/vm/obtainVmData',
256         'params' : {
257             'vm_name' : vmName,
258             'region_no': regionNo
259         }
260     };
261     var vmjq = doAjaxRequest(vmInfoReq);
262     var vmInfoLoadedAfter = function(resultJson) {
263         
264         vmNodeData = resultJson.result.data;
265         if (vmNodeData == null) {
266             alert('没有找到VM的相关信息!');
267             return ;
268         }
269         vmNode = createDiv(vmNodeData);
270         
271         vmDivId = obtainNodeDivId(vmNodeData);
272         $('#'+vmDivId).css('top', rootTop + 'px');
273         $('#'+vmDivId).css('left', rootLeft + 'px');
274         
275         linkAccount(vmNodeData, vmName, regionNo);
276         
277         ncId = vmNodeData.data.nc_id;
278         linkNc(vmNodeData, ncId, regionNo);
279         
280         // vmName = 'ATX-28n2dhdq8';
281         linkDevices(vmNodeData, vmName);
282         
283         vmInnerIp = vmNodeData.data.vm_inner_ip;
284         linkVips(vmNodeData, vmInnerIp);
285         
286     };
287     vmjq.done(vmInfoLoadedAfter);
288 }
289 function linkAccount(vmNodeData, vmName, regionNo) {
290     var accountInfoReq = {
291         'url': httpPrefix + '/controllers/vm/obtainAliyunAccountInfo',
292         'params': {
293             'vm_name' : vmName,
294             'region_no': regionNo
295         }
296     };
297     var accountjq = doAjaxRequest(accountInfoReq);
298     accountjq.done(function(resultJson) {
299         
300         // for test
301         // resultJson = {"result":{"msg":"successful","code":200,"data":{"errorCode":null,"errorMsg":null,"aliyunID":"it-cloudpc@alibaba-inc.com","kp":null},"success":true}};
302         
303         if (resultJson.result.success) {
304             accountData = resultJson.result.data;
305             accountNodeData = createAccountData(accountData);
306             accountNode = createDiv(accountNodeData);
307             cacheConnections(vmNodeData, accountNodeData, obtainConnectionDirections(vmNodeData, accountNodeData));
308             reconnectAll(connections);
309         }
310         else {
311             $('#error').append('获取关联云账号信息失败!');
312         }
313     }).fail(function() {
314         $('#error').append('获取关联云账号信息失败! ');
315     });
316     
317 }
318 function linkNc(vmNodeData, ncId, regionNo) {
319     var ncInfoReq = {
320         'url':     httpPrefix + '/controllers/nc/listNc',
321         'params': {
322             'region_no': regionNo,
323             'nc_id': ncId,
324             'start': 0,
325             'page': 1,
326             'limit': 1
327         }
328     };
329     var ncjq = doAjaxRequest(ncInfoReq);
330     ncjq.done(function(resultJson) {
331         ncDataList = resultJson.data;
332         if (ncDataList.length > 0) {
333             ncData = ncDataList[0];
334             ncNodeData = createNcData(ncData);
335             ncNode = createDiv(ncNodeData);
336             cacheConnections(vmNodeData, ncNodeData, obtainConnectionDirections(vmNodeData, ncNodeData));
337             
338             ncClusterNodeData = createNcClusterData(ncData);
339             ncClusterNode = createDiv(ncClusterNodeData);
340             cacheConnections(ncNodeData, ncClusterNodeData, obtainConnectionDirections(ncNodeData, ncClusterNodeData));
341             reconnectAll(connections);
342         }
343         else {
344             $('#error').append('获取关联NC实体失败!');
345         }
346     }).fail(function() {
347         $('#error').append('获取关联NC实体失败!');
348     });
349 }
350 function linkDevices(vmNodeData, vmName) {
351     var deviceInfoReq = {
352         'url' :  httpPrefix + '/controllers/disk/search',
353         'params': {
354             'vmName': vmName
355         }
356     }
357     var regionPeNickName = vmNodeData.data.region_pe_nickname;
358     var devicejq = doAjaxRequest(deviceInfoReq);
359     devicejq.done(function(resultJson) {
360         
361         total = resultJson.data.total;
362         if (total > 0) {
363             devices = resultJson.data.list;
364             
365             for (var i=0; i<total; i++) {
366                 
367                 deviceData = devices[i];
368                 deviceData['regionPeNickName'] = regionPeNickName;
369                 deviceNodeData = createDeviceData(deviceData);
370                 deviceNodeDataMapping[deviceData.instanceId] = deviceNodeData;
371                 deviceNode = createDiv(deviceNodeData);
372                 cacheConnections(vmNodeData, deviceNodeData, obtainConnectionDirections(vmNodeData, deviceNodeData));
373                 
374                 deviceClusterNodeData = createDeviceClusterData(deviceData);
375                 deviceClusterNode = createDiv(deviceClusterNodeData);
376                 cacheConnections(deviceNodeData, deviceClusterNodeData, obtainConnectionDirections(deviceNodeData,deviceClusterNodeData));
377                 linkSnapshot(devices[i].aliUid, devices[i].instanceId, deviceNodeData);
378             }
379             reconnectAll(connections);
380         }
381         else {
382             $('#error').append('该VM没有关联的磁盘实体!');
383         }
384     }).fail(function() {
385         $('#error').append('获取关联磁盘实体失败!');
386     });
387 }
388 function linkVips(vmNodeData, vmInnerIp) {
389     
390     var vipInfoReq = {
391         'url': httpPrefix + '/controllers/slbvip/listVip',
392         'params': {
393             'realserver_param': vmInnerIp,
394             'start': 0,
395             'page': 1,
396             'limit': 100
397         }
398     };
399     var vipjq = doAjaxRequest(vipInfoReq);
400     vipjq.done(function(resultJson) {
401         
402         total = resultJson.total;
403         vips = resultJson.data;
404         if (total > 0) {
405             for (j=0; j<total; j++) {
406                 var vipInfo = vips[j];
407                 vipNodeData = createVipData(vipInfo);
408                 vipNode = createDiv(vipNodeData);
409                 cacheConnections(vmNodeData, vipNodeData, obtainConnectionDirections(vmNodeData,vipNodeData));
410             }
411             reconnectAll(connections);
412         }
413         else {
414             $('#error').append('该VM没有关联的VIP实体!');
415         }
416     }).fail(function() {
417         $('#error').append('获取关联VIP实体失败!');
418     });
419     
420 }
421 function linkSnapshot(aliUid, diskId, deviceNodeData) {
422     
423     var snapshotInfoReq = {
424         'url': httpPrefix + '/controllers/snapshot/search',
425         'params': {
426             'aliUid': aliUid,
427             'diskId': diskId
428         }
429     };
430     var snapshotjq = doAjaxRequest(snapshotInfoReq);
431     snapshotjq.done(function(resultJson) {
432         
433         total = resultJson.total;
434         if (total > 0) {
435             snapshotDataList = resultJson.list;
436             for (k=0; k<total; k++) {
437                 snapshotData = snapshotDataList[k];
438                 snapshotNodeData = createSnapshotData(snapshotData);
439                 snapshotNode = createDiv(snapshotNodeData);
440                 cacheConnections(deviceNodeData, snapshotNodeData, obtainConnectionDirections(deviceNodeData, snapshotNodeData));
441             }
442             reconnectAll(connections);
443         }
444         else {
445             $('#error').append('磁盘 ' + diskId + ' 没有关联的快照实体!');
446         }
447     }).fail(function() {
448         $('#error').append('磁盘' + diskId + ' 获取关联快照实体失败!');
449     });
450 }
451 /**
452  * createXXXData
453  * 创建拓扑图所使用的节点数据  
454  */
455 function createVmData(vmData) {
456     return {
457         'type': 'VM',
458         'key': vmData.vm_name,
459         'data': vmData
460     }
461 }
462 function createNcData(ncData) {
463     return {
464         'type': 'NC',
465         'key': ncData.ip,
466         'data': ncData
467     }
468 }
469 function createNcClusterData(ncData) {
470     return {
471         'type': 'CLUSTER',
472         'key': ncData.regionPeNickName,
473         'data': {
474             'regionNo': ncData.regionNo,
475             'regionNickName': ncData.regionNickName,
476             'regionPeNickName': ncData.regionPeNickName
477         }
478     }
479 }
480 function createDeviceData(deviceData) {
481     return {
482         'type': 'DEVICE',
483         'key': deviceData.instanceId,
484         'data': deviceData
485     }
486 }
487 function createDeviceClusterData(deviceData) {
488     return {
489         'type': 'CLUSTER',
490         'key': deviceData.regionNo,
491         'data': {
492             'regionNo': deviceData.regionNo
493         }
494     }
495 }
496 function createSnapshotData(snapshotData) {
497     return {
498         'type': 'SNAPSHOT',
499         'key': snapshotData.snapshotId,
500         'data': snapshotData 
501     }
502 }
503 function createSnapshotClusterData(snapshotData) {
504     return {
505         'type': 'CLUSTER',
506         'key': snapshotData.regionNo,
507         'data': {
508             'regionNo': snapshotData.regionNo
509         }
510     }
511 }
512 function createVipData(vipData) {
513     return {
514         'type': 'VIP',
515         'key': vipData.vipAddress,
516         'data': vipData
517     }
518 }
519 function createAccountData(accountData) {
520     return {
521         'type': 'ACCOUNT',
522         'key': accountData.aliyunID,
523         'data': accountData
524     }
525 }
526 /**
527  * 缓存起始节点 beginNode 和终止节点 endNode 的连接关系 
528  */
529 function cacheConnections(beginNode, endNode, directions) {
530     
531     computeLayout(beginNode, endNode);
532     
533     var startPoint = obtainNodeDivId(beginNode) + directions[0];
534     var endPoint = obtainNodeDivId(endNode) + directions[1];
535     connections.push([startPoint, endPoint]);
536 }
537 /**
538  * 计算节点位置及布局
539  */
540 function computeLayout(beginNode, endNode) {
541     
542     var beginDivId = obtainNodeDivId(beginNode);
543     var endDivId = obtainNodeDivId(endNode);
544     var beginNodeType = beginNode.type;
545     var endNodeType = endNode.type;
546     
547     beginNodeTop = $('#'+beginDivId).offset().top;
548     beginNodeLeft = $('#'+beginDivId).offset().left;
549     
550     var key = beginNodeType + '-' + endNodeType + '-' + beginNode.key;
551     if (cacheConnectionData[key] == null) {
552         cacheConnectionData[key] = -1;
553     }
554     else {
555         cacheConnectionData[key] = cacheConnectionData[key]+1;
556     }
557     connNum = cacheConnectionData[key];
558     
559     var typeKey = beginNodeType + '-' + endNodeType;
560     var relDistance = connectionDistanceMapping[typeKey];
561     var relVertiDistance = relDistance[0];
562     var relHoriDistance = relDistance[1];
563     
564     switch (beginNodeType) {
565         case VM_TYPE:
566             if (endNodeType == VIP_TYPE) {
567                 endNodePosition = [beginNodeTop+connNum*relVertiDistance, beginNodeLeft+relHoriDistance];
568             }
569             else if (endNodeType == DEVICE_TYPE) {
570                 endNodePosition = [beginNodeTop+connNum*relVertiDistance, beginNodeLeft+relHoriDistance];
571             }
572             else if (endNodeType == NC_TYPE) {
573                 endNodePosition = [beginNodeTop+relVertiDistance, beginNodeLeft+relHoriDistance];
574             }
575             else if (endNodeType == ACCOUNT_TYPE) {
576                 endNodePosition = [beginNodeTop+relVertiDistance, beginNodeLeft+relHoriDistance];
577             }
578             break;
579         case DEVICE_TYPE:
580             if (endNodeType == CLUSTER_TYPE) {
581                 endNodePosition = [beginNodeTop+relVertiDistance, beginNodeLeft+relHoriDistance];
582             }
583             else if (endNodeType == SNAPSHOT_TYPE) {
584                 endNodePosition = [beginNodeTop+relVertiDistance, beginNodeLeft+(connNum+1)*relHoriDistance];
585             }
586             break;
587         case VIP_TYPE:
588             break;
589         case NC_TYPE:
590             if (endNodeType == CLUSTER_TYPE) {
591                 endNodePosition = [beginNodeTop+relVertiDistance, beginNodeLeft+relHoriDistance];
592             }
593             break;
594         case SNAPSHOT_TYPE:
595         default: 
596             break;
597     }
598     
599     $('#'+endDivId).css('top', endNodePosition[0] + 'px');
600     $('#'+endDivId).css('left', endNodePosition[1] + 'px');
601     
602     addEndPoints(beginDivId, beginNodeType);
603     addEndPoints(endDivId, endNodeType);
604 }
605 /**
606  * 为节点添加用于连线的附着点
607  * @param nodeDivId  节点的 DIV ID
608  * @param type  节点类型
609  */
610 function addEndPoints(nodeDivId, type) {
611     var startAttachedPoints = entityEndPointsMapping[type][0];
612     var endAttachedPoints = entityEndPointsMapping[type][1];
613     topoDrawUtil.addEndpoints(nodeDivId, startAttachedPoints, endAttachedPoints);
614 }
615 /**
616  * 连接所有连线
617  */
618 function reconnectAll(connections) {
619     
620     var i=0;
621     for (i=0; i<connections.length; i++) {
622         jsPlumb.connect({uuids:connections[i], editable: false}); 
623     }
624     // 使所有拓扑节点均为可拉拽的                    
625     jsPlumb.draggable(jsPlumb.getSelector(".node"), { grid: [5, 5] });
626 }
627 /**
628  * div id cache , avoid duplicated div.
629  * {'divId': 'divStr'}
630  */
631 divIdCache = {};
632 /**
633  * 为节点数据创建节点附着点并返回节点的DIV
634  */
635 function createDiv(metaNode) {
636     var clickUrl = '';
637     var display = '';
638     var type = metaNode.type;
639     var regionPeNickname = '';
640     if (metaNode.data != null) {
641         regionPeNickname = metaNode.data.regionPeNickName;
642     }
643     
644     nodeDivId = obtainNodeDivId(metaNode);
645     
646     if (divIdCache[nodeDivId] != null) {
647         // 该节点要移动到新的位置, 因此原来的附着点要去掉
648         topoDrawUtil.removeAllEndPoints(nodeDivId);
649         return divIdCache[nodeDivId];
650     }
651     
652     switch(type.toUpperCase()) {
653         case VM_TYPE:
654             clickUrl = httpPrefix + '/framehtml/vm_monitor.html?vm_name=' + metaNode.key + '&data='+JSON.stringify(metaNode.data).replace(/"/g,"'");
655             display = metaNode.key;
656             break;
657         case DEVICE_TYPE:
658             displayDevice1 = metaNode.data.instanceId;
659             clickDeviceUrl2 = httpPrefix + '/framehtml/device_monitor.html?device_id=' + metaNode.data.houyiDiskId + '®ion_pe_nickname='+regionPeNickname;
660             displayDevice2 = metaNode.data.houyiDiskId;
661             break;
662         case NC_TYPE:
663             var regionNo = metaNode.data.regionNo;
664             clickUrl = httpPrefix + '/framehtml/nc_monitor.html?nc_ip=' + metaNode.key + '®ion_pe_nickname='+regionPeNickname + '®ion_no='+regionNo;
665             display = metaNode.key;
666             break;
667         case VIP_TYPE:
668             display = metaNode.key + ':' + metaNode.data.port;
669             clickUrl = httpPrefix + '/framehtml/vip_monitor.html?vip=' + display + '®ion_pe_nickname='+regionPeNickname + '&slbdb_configId='+metaNode.data.slbdb_configId;
670             break;
671         case SNAPSHOT_TYPE:
672             display = metaNode.key + '<br/>' + metaNode.data.houyiSnapshotId + '<br/><span style="color:green">'+ metaNode.data.regionNo + '</span>';
673             break;
674         case CLUSTER_TYPE:
675         case AVZ_TYPE:
676         case ACCOUNT_TYPE:
677             display = metaNode.key;
678             break;
679         default:
680             break;
681     } 
682     
683     if (type == VM_TYPE || type == NC_TYPE || type == VIP_TYPE || type == ACCOUNT_TYPE ) {
684         divStr =  '<div class="node biggerNode" id="' + nodeDivId + '"><strong>' 
685                 + metaNode.type + '<br/><a href="' + clickUrl + '" target="_blank">' + display + '</a><br/></strong></div>';
686     }
687     else if (type == DEVICE_TYPE){
688         divStr =  '<div class="node biggerNode" id="' + nodeDivId + '"><strong>' 
689         + metaNode.type + '<br/>' + displayDevice1 + '<br/><a href="' + clickDeviceUrl2 + '" target="_blank">' + displayDevice2 + '</a><br/></strong></div>';
690     }
691     else {
692         divStr = '<div class="node biggerNode" id="' + nodeDivId + '"><strong>' 
693                 + metaNode.type + '<br/>' + display + '<br/></strong></div>';
694     }
695     parentDiv.append(divStr);
696     
697     divIdCache[nodeDivId] = divStr;
698     return divStr;
699 }
700 function obtainConnectionDirections(srcNodeData, destNodeData) {
701     var key = srcNodeData.type + '-' + destNodeData.type;
702     var startDirection = connectionDirectionMapping[key][0];
703     var endDirection = connectionDirectionMapping[key][1];
704     return [startDirection, endDirection];
705 }
706 /**
707  * 生成节点的 DIV id
708  * divId = nodeType.toUpperCase + "_" + key
709  * key 可能为 IP , 其中的 . 将被替换成 ZZZ , 因为 jquery id 选择器中 . 属于转义字符.
710  * eg. {type: 'VM', key: '1.1.1.1' }, divId = 'VM_1ZZZ1ZZZ1ZZZ1'
711  */
712 function obtainNodeDivId(metaNode) {
713     if (metaNode.type == VIP_TYPE) {
714         return metaNode.type.toUpperCase() + '_' + transferKey(metaNode.key) + '_' + metaNode.data.port;
715     }
716     return metaNode.type.toUpperCase() + '_' + transferKey(metaNode.key);
717 }
718 function transferKey(key) {
719     return key.replace(/./g, 'ZZZ').replace(/@/g,'YYY');
720 }
721 function revTransferKey(value) {
722     return value.replace(/ZZZ/g, '.').replace('/YYY/g','@');
723 }  
原文地址:https://www.cnblogs.com/lovesqcc/p/4037689.html