示例工程见: http://download.csdn.net/detail/shuqin1984/6488513
一、 实现目标
绘制拓扑图, 实际上是个数据结构和算法的问题。 需要设计一个合适的数据结构来表达拓扑结构,设计一个算法来计算拓扑节点的位置及连接。
二、 实现思想
1. 数据结构
首先, 从节点开始。 显然, 需要一个字段 type 表示节点类型, 一个字段 data 表示节点数据(详情), 对于连接, 则采用一个 rel 字段, 表示有哪些节点与之关联, 相当于C 里面的指针。 为了唯一标识该节点, 还需要一个字段 key 。 通过 type-key 组合来唯一标识该节点。结合JavaScript 标准的数据格式 JSON, 定下数据结构如下:
a. 节点数据结构: node = { type: 'nodeType', key: 'nodeKey', rel: [], data: {'More Info'}}
b. rel, data 可选 , type-key 唯一标识该节点, rel 不存在表示该节点为叶子节点
c. 关联关系: rel: [node1, node2, ..., nodeN]
d. 节点详情: 关于节点的更多信息可放置于 data 字段中
2. 算法
在算法上, 要预先规划好各个节点类型如何布局以及如何连接。 连接方向很容易定: 根据起始节点及终止节点的类型组合, 可以规定不同的连接方向, 比如 的连线方向为, 的连线方向为 。 节点位置的确定是一个关键问题, 该算法的实现难易取决于拓扑数据结构的设计。 这里采用的方法是: 采用深度优先遍历, 下一个的节点位置通过<上一个节点位置, 上一个节点类型, 下一个节点类型> 确定, 如果上一个节点有多个相同类型的后继节点, 那么这多个后继节点的位置是重合的, 需要在后面进行调整。实际上, 这个节点位置的算法是比较笨拙的, 如果有更好的算法, 请告知。
3. JsPlumb
jsPlumb 有几个基本概念。 首先, 拓扑节点实际上是 DIV 区域,每个DIV 都必须有一个ID,用于唯一标识该节点。 连接拓扑节点的一个重要概念是EndPoint . EndPoint 是附着于节点上的连接线的端点, 简称“附着点”。 将附着点 attach 到指定拓扑节点上的方法如下:
jsPlumb.addEndpoint(toId, this.sourceEndpoint, { anchor: sourceAnchor, uuid:sourceUUID });
toId 是 拓扑节点的 DIV 区域的 ID 值, sourceEndpoint 是附着点的样式设置, 可以复用 , sourceAnchor 是附着点位置, 共有八种,也就是四方形的八个边缘:
- Top
(also aliased as TopCenter
) - TopRight
- Right
(also aliased as RightMiddle
) -
BottomRight
- Bottom
(also aliased as BottomCenter
) -BottomLeft
- Left
(also aliased as LeftMiddle
) - TopLeft
sourceUUID 是拓扑节点与附着位置的结合, 也就是说, 要将一个 附着点附着到拓扑节点为 toId 的 sourceAnchor 指定的位置上。 每个拓扑节点都可以定义多个源附着点和目标附着点。 源附着点是连接线的起始端, 目标附着点是连接线的终止端。
两个 uuid 即可定义一条连接线:
jsPlumb.connect({uuids:[startPoint, endPoint], editable: false});
startPoint 和 endPoint 分别是连接线的起始端 Endpoint uuid 和 终止段 Endpoint uuid. 它定义了从起始拓扑节点的指定附着点连接到终止拓扑节点的指定附着点。
三、 实现代码
drawTopo.js 提供绘制拓扑图的基本方法, 只要按照指定格式将数据结构扔进去, 就可以自动绘制出拓扑图来。
- "font-family:Microsoft YaHei; font-size:14px">
-
-
-
-
-
-
-
-
- (function() {
-
- jsPlumb.importDefaults({
-
- DragOptions : { cursor: 'pointer', zIndex:2000 },
-
- EndpointStyles : [{ fillStyle:'#225588' }, { fillStyle:'#558822' }],
-
- Endpoints : [ [ "Dot", { radius:2 } ], [ "Dot", { radius: 2 } ]],
-
- ConnectionOverlays : [
- [ "Arrow", { location:1 } ],
- [ "Label", {
- location:0.1,
- id:"label",
- cssClass:"aLabel"
- }]
- ]
- });
-
- var connectorPaintStyle = {
- lineWidth: 1,
- strokeStyle: "#096EBB",
- joinstyle:"round",
- outlineColor: "#096EBB",
- outlineWidth: 1
- };
-
- var connectorHoverStyle = {
- lineWidth: 2,
- strokeStyle: "#5C96BC",
- outlineWidth: 2,
- outlineColor:"white"
- };
-
- var endpointHoverStyle = {
- fillStyle:"#5C96BC"
- };
-
- window.topoDrawUtil = {
-
- sourceEndpoint: {
- endpoint:"Dot",
- paintStyle:{
- strokeStyle:"#1e8151",
- fillStyle:"transparent",
- radius: 2,
- lineWidth:2
- },
- isSource:true,
- maxConnections:-1,
- connector:[ "Flowchart", { stub:[40, 60], gap:10, cornerRadius:5, alwaysRespectStubs:true } ],
- connectorStyle: connectorPaintStyle,
- hoverPaintStyle: endpointHoverStyle,
- connectorHoverStyle: connectorHoverStyle,
- dragOptions:{},
- overlays:[
- [ "Label", {
- location:[0.5, 1.5],
- label:"",
- cssClass:"endpointSourceLabel"
- } ]
- ]
- },
-
- targetEndpoint: {
- endpoint: "Dot",
- paintStyle: { fillStyle:"#1e8151",radius: 2 },
- hoverPaintStyle: endpointHoverStyle,
- maxConnections:-1,
- dropOptions:{ hoverClass:"hover", activeClass:"active" },
- isTarget:true,
- overlays:[
- [ "Label", { location:[0.5, -0.5], label:"", cssClass:"endpointTargetLabel" } ]
- ]
- },
-
- initConnection: function(connection) {
- connection.getOverlay("label").setLabel(connection.sourceId + "-" + connection.targetId);
- connection.bind("editCompleted", function(o) {
- if (typeof console != "undefined")
- console.log("connection edited. path is now ", o.path);
- });
- },
-
- addEndpoints: function(toId, sourceAnchors, targetAnchors) {
- for (var i = 0; i < sourceAnchors.length; i++) {
- var sourceUUID = toId + sourceAnchors[i];
- jsPlumb.addEndpoint(toId, this.sourceEndpoint, { anchor:sourceAnchors[i], uuid:sourceUUID });
- }
- for (var j = 0; j < targetAnchors.length; j++) {
- var targetUUID = toId + targetAnchors[j];
- jsPlumb.addEndpoint(toId, this.targetEndpoint, { anchor:targetAnchors[j], uuid:targetUUID });
- }
- }
- };
-
-
- })();
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- function drawTopo(topoData, rootPosition, nodeTypeArray) {
-
-
- createNodes(topoData, rootPosition, nodeTypeArray);
-
-
- adjust(topoData, nodeTypeArray);
-
-
- jsPlumb.draggable(jsPlumb.getSelector(".node"), { grid: [5, 5] });
-
-
- createConnections(topoData, nodeTypeArray);
-
- }
-
-
-
-
-
-
-
- function createNodes(rootData, rootPosition, nodeTypeArray) {
-
- if (rootData == null) {
- return ;
- }
-
- var topoRegion = $('#topoRegion');
- var relData = rootData.rel;
- var i=0, relLen = relLength(relData);;
- var VM_TYPE = nodeTypeArray[0];
- var DEVICE_TYPE = nodeTypeArray[1];
- var NC_TYPE = nodeTypeArray[2];
- var VIP_TYPE = nodeTypeArray[3];
-
-
- var rootTop = rootPosition[0];
- var rootLeft = rootPosition[1];
-
- var nextRootData = {};
- var nextRootPosition = [];
-
-
- var divStr = createDiv(rootData);
- var nodeDivId = obtainNodeDivId(rootData);
- topoRegion.append(divStr);
-
-
-
- $('#'+nodeDivId).css('top', rootTop + 'px');
- $('#'+nodeDivId).css('left', rootLeft + 'px');
-
- for (i=0; i < relLen; i++) {
- nextRootData = relData[i];
- nextRootPosition = obtainNextRootPosition(rootData, nextRootData, rootPosition, nodeTypeArray);
- createNodes(nextRootData, nextRootPosition, nodeTypeArray);
- }
-
- }
-
-
-
-
- function adjust(topoData, nodeTypeArray) {
-
- var vm_deviceOffset = 0;
- var vm_vipOffset = 0;
- var vm_ncOffset = 0;
- var vip_vmOffset = 0;
- var nc_vmOffset = 0;
- var verticalDistance = 120;
- var horizontalDistance = 150;
-
- var VM_TYPE = nodeTypeArray[0];
- var DEVICE_TYPE = nodeTypeArray[1];
- var NC_TYPE = nodeTypeArray[2];
- var VIP_TYPE = nodeTypeArray[3];
-
- $('.node').each(function(index, element) {
- var nodeDivId = $(element).attr('id');
- var nodeType = nodeDivId.split('-')[0];
- var offset = $(element).offset();
- var originalTop = offset.top;
- var originalLeft = offset.left;
- var parentNode = $(element).parent();
- var parentNodeType = parentNode.attr('id').split('-')[0];
- switch (nodeType) {
- case VM_TYPE:
-
- $(element).css('left', (originalLeft + vip_vmOffset*horizontalDistance) + 'px');
- vip_vmOffset++;
- topoDrawUtil.addEndpoints(nodeDivId, ['Top', 'Bottom', 'Right'], []);
- break;
- case DEVICE_TYPE:
-
- $(element).css('top', (originalTop + (vm_deviceOffset-1)*verticalDistance) + 'px');
- vm_deviceOffset++;
- topoDrawUtil.addEndpoints(nodeDivId, [], ['Left']);
- break;
- case VIP_TYPE:
-
- $(element).css('left', (originalLeft + vm_vipOffset*horizontalDistance) + 'px');
- vm_vipOffset++;
- topoDrawUtil.addEndpoints(nodeDivId, ['Top'], ['Bottom']);
- break;
- case NC_TYPE:
-
- $(element).css('left', (originalLeft + vm_ncOffset*verticalDistance) + 'px');
- vm_ncOffset++;
- topoDrawUtil.addEndpoints(nodeDivId, ['Bottom'], ['Top']);
- break;
- default:
- break;
- }
- });
- }
-
-
-
-
-
-
-
-
- function obtainNextRootPosition(root, nextRoot, rootPosition, nodeTypeArray) {
-
- var VM_TYPE = nodeTypeArray[0];
- var DEVICE_TYPE = nodeTypeArray[1];
- var NC_TYPE = nodeTypeArray[2];
- var VIP_TYPE = nodeTypeArray[3];
-
- var startNodeType = root.type;
- var endNodeType = nextRoot.type;
- var nextRootPosition = [];
- var rootTop = rootPosition[0];
- var rootLeft = rootPosition[1];
-
- var verticalDistance = 120;
- var horizontalDistance = 250;
- var shortVerticalDistance = 80;
-
- switch (startNodeType) {
- case VM_TYPE:
- if (endNodeType == VIP_TYPE) {
- nextRootPosition = [rootTop-verticalDistance, rootLeft];
- }
- else if (endNodeType == DEVICE_TYPE) {
- nextRootPosition = [rootTop, rootLeft+horizontalDistance];
- }
- else if (endNodeType == NC_TYPE) {
- nextRootPosition = [rootTop+verticalDistance, rootLeft];
- }
- break;
- case VIP_TYPE:
- if (endNodeType == VM_TYPE) {
- nextRootPosition = [rootTop-shortVerticalDistance, rootLeft];
- }
- break;
- case NC_TYPE:
- if (endNodeType == VM_TYPE) {
- nextRootPosition = [rootTop+shortVerticalDistance, rootLeft];
- }
- break;
- default:
- break;
- }
- return nextRootPosition;
- }
-
-
-
-
-
-
- function createConnections(topoData, nodeTypeArray) {
-
- if (topoData == null) {
- return ;
- }
- var rootData = topoData;
- var relData = topoData.rel;
- var i=0, len = relLength(relData);;
- for (i=0; i < len; i++) {
- connectionNodes(rootData, relData[i], nodeTypeArray);
- createConnections(relData[i], nodeTypeArray);
- }
- }
-
-
-
-
-
-
-
- function connectionNodes(beginNode, endNode, nodeTypeArray)
- {
- var startNodeType = beginNode.type;
- var endNodeType = endNode.type;
- var startDirection = '';
- var endDirection = '';
-
- var VM_TYPE = nodeTypeArray[0];
- var DEVICE_TYPE = nodeTypeArray[1];
- var NC_TYPE = nodeTypeArray[2];
- var VIP_TYPE = nodeTypeArray[3];
-
- switch (startNodeType) {
- case VM_TYPE:
- if (endNodeType == VIP_TYPE) {
-
- startDirection = 'Top';
- endDirection = 'Bottom';
- }
- else if (endNodeType == DEVICE_TYPE) {
-
- startDirection = 'Right';
- endDirection = 'Left';
- }
- else if (endNodeType == NC_TYPE) {
-
- startDirection = 'Bottom';
- endDirection = 'Top';
- }
- break;
- case VIP_TYPE:
- if (endNodeType == VM_TYPE) {
-
- startDirection = 'Top';
- endDirection = 'Top';
- }
- break;
- case NC_TYPE:
- if (endNodeType == VM_TYPE) {
-
- startDirection = 'Bottom';
- endDirection = 'Bottom';
- }
- break;
- default:
- break;
- }
- var startPoint = obtainNodeDivId(beginNode) + startDirection;
- var endPoint = obtainNodeDivId(endNode) + endDirection;
- jsPlumb.connect({uuids:[startPoint, endPoint], editable: false});
- }
-
- function createDiv(metaNode) {
- return '
+ obtainNodeDivId(metaNode) + '">'
- + metaNode.type + '
' + metaNode.key + '
'
- }
-
-
-
-
-
-
-
- function obtainNodeDivId(metaNode) {
- return metaNode.type.toUpperCase() + '-' + transferKey(metaNode.key);
- }
-
- function transferKey(key) {
- return key.replace(/\./g, 'ZZZ');
- }
-
- function revTransferKey(value) {
- return value.replace(/ZZZ/g, '.');
- }
-
-
-
-
-
-
-
- function mergeNewTopo(srcTopoData, newTopoData) {
-
- var srcTopoData = shallowCopyTopo(srcTopoData);
-
- if (srcTopoData == null || newTopoData == null) {
- return srcTopoData || newTopoData;
- }
-
- var srcRoot = srcTopoData;
- var newRoot = newTopoData;
-
- var newRelData = newTopoData.rel;
- var i=0, newRelLen = relLength(newRelData);
-
- var matched = findMatched(srcRoot, newRoot);
- if (matched == null) {
-
- return srcTopoData;
- }
- matched.rel = matched.rel.concat(newRelData);
- return srcTopoData;
- }
-
-
-
-
-
-
-
- function findMatched(srcRootData, newRootData) {
- var srcRelData = srcRootData.rel;
- var i=0, srcRelLen = relLength(srcRelData);
- var matched = null;
- if ((srcRootData.type == newRootData.type) && (srcRootData.key == newRootData.key)) {
- return srcRootData;
- }
- for (i=0; i
- matched = findMatched(srcRelData[i], newRootData);
- if (matched != null) {
- return matched;
- }
- }
- return matched;
- }
-
- function relLength(relData) {
- if (isArray(relData)) {
- return relData.length;
- }
- return 0;
- }
-
- function isArray(value) {
- return value && (typeof value === 'object') && (typeof value.length === 'number');
- }
-
-
-
-
- function shallowCopyTopo(srcTopoData) {
- return srcTopoData;
- }
-
-
-
-
- function deepCopyTopo(srcTopoData) {
-
- }
-
topodemo.html 绘制拓扑图的客户端接口。 只要引进相应的依赖 JS,预置一个
- <span style="font-family:Microsoft YaHei; font-size:14px">>
- <html>
- <head>
- <title>jsPlumb 1.5.3 - flowchart connectors demonstration - jQuerytitle>
- <link rel="stylesheet" href="topo-all.css">
- <link rel="stylesheet" href="topo.css">
-
-
- <script src="../jsPlumb/jquery-1.9.0-min.js">script>
- <script src="../jsPlumb/jquery-ui-1.9.2-min.js">script>
-
-
-
-
-
- <script src="../jsPlumb/jsBezier-0.6-min.js">script>
-
- <script src="../jsPlumb/jsplumb-geom-0.1.js">script>
-
- <script src="../jsPlumb/util.js">script>
-
- <script src="../jsPlumb/dom-adapter.js">script>
-
- <script src="../jsPlumb/jsPlumb.js">script>
-
- <script src="../jsPlumb/endpoint.js">script>
-
- <script src="../jsPlumb/connection.js">script>
-
- <script src="../jsPlumb/anchors.js">script>
-
- <script src="../jsPlumb/defaults.js">script>
-
- <script src="../jsPlumb/connector-editors.js">script>
-
- <script src="../jsPlumb/connectors-bezier.js">script>
-
- <script src="../jsPlumb/connectors-statemachine.js">script>
-
- <script src="../jsPlumb/connectors-flowchart.js">script>
-
- <script src="../jsPlumb/renderers-svg.js">script>
-
- <script src="../jsPlumb/renderers-canvas.js">script>
-
- <script src="../jsPlumb/renderers-vml.js">script>
-
-
- <script src="../jsPlumb/jquery.jsPlumb.js">script>
-
-
-
- <script src="drawtopo.js">script>
-
- <script type="text/javascript">
- jsPlumb.bind("ready", function() {
-
- // 拓扑数据结构根节点位置设置
- var rootPosition = [270, 300];
- var nodeTypeArray = ['VM', 'DEVICE', 'NC', 'VIP'];
- var topoData = {
- type: 'VM', key: '110.75.188.35',
- rel: [
- {
- type: 'DEVICE',
- key: '3-120343'
- },
-
- {
- type: 'DEVICE',
- key: '3-120344'
- },
-
- {
- type: 'VIP',
- key: '223.6.250.2',
- rel: [
- { type: 'VM', key: '110.75.189.12' },
- { type: 'VM', key: '110.75.189.13' }
- ]
- },
-
- {
- type: 'NC',
- key: '10.242.192.2',
- rel: [
- { type: 'VM', key: '110.75.188.132' },
- { type: 'VM', key: '110.75.188.135' }
- ]
-
- }
- ]
- };
-
- drawTopo(topoData, rootPosition, nodeTypeArray);
-
- var newTopoData = {
- type: 'NC',
- key: '10.242.192.2',
- rel: [
- { type: 'VM', key: '110.75.188.140' }
- ]
- };
-
- var mergedTopoData = mergeNewTopo(topoData, newTopoData);
- $('#topoRegion').empty();
- drawTopo(mergedTopoData, rootPosition, nodeTypeArray);
-
-
- });
-
-
- script>
-
- head>
- <body>
-
- <div id="topoRegion">
- div>
-
- body>
- html>
- span>
样式文件及依赖JS 见工程示例。 里面已经包含绘制拓扑图的最小依赖。
四、 最终效果图
五、 小结
这里看到, 数据结构/算法与实际应用的一个很好的结合。 所以说, 绝不要忽视数据结构和算法的作用, 实际上, 只要仔细去发现, 去挖掘, 就会发现其大有用武之地。