电信网络拓扑图自动布局之总线

在前面《电信网络拓扑图自动布局》一文中,我们大体介绍了 HT for Web 电信网络拓扑图自动布局的相关知识,但是都没有深入地描述各种自动布局的用法,我们今天在这边就重点介绍总线的具体实现方案。

HT for Web 的连线手册中,有说明可以自定义连线类型,通过 ht.Default.setEdgeType(type, func, mutual) 函数定义,我们今天要描述的总线也是通过这样的方法来实现的。

我们来简单地描述下这个方法,虽然在文档(http://www.hightopo.com/guide/guide/plugin/edgetype/ht-edgetype-guide.html)中已经描述得很详细了,为了下面的工作能够更好的开展,我这边还是再强调下。

这个函数名是 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

那这第三个参数呢,是决定连线是否影响起始或结束节点上的所有连线,这个参数解释起来比较复杂,后续有机会的话,我们再详细说明。

先来看看一个简单的例子,http://www.hightopo.com/guide/guide/plugin/edgetype/examples/example_custom.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

前面的废话太多了,下面就是我们今天的主要内容了,看看如何通过自定义 EdgeType 来实现总线的效果,http://www.hightopo.com/demo/EdgeType/BusEdgeType.html

上图就是一个总线的简单例子,所有的节点都通过线条链接黑色的总线,连线的走向都是节点到总线的最短距离。

来讲讲整体的设计思路吧,其实总的来说,就是点的线的垂直点计算问题。那么问题来了,碰到曲线怎么办?其实曲线也是可以微分成线条来处理的,至于这个线段的划分精细度就需要用户来自定义了。

EdgeType 结合 ShapeLayout 实现均匀自动布局:http://www.hightopo.com/demo/EdgeType/ShapeLayout-Oval.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));

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Bingo的深度学习杂货店

HTML5新特性

本章的主要内容有: ---- [1] 用于媒体回放的 video 和audio 元素 [2] HTML5拖放 [3] canvas简单应用 [4] Web存储:...

3315
来自专栏lonelydawn的前端猿区

requireJs的使用,以canvas绘制星空为例

RequireJS是符合AMD规范(Asynchronous module definition异步模块加载)一种js加载方式,目的是为了防止加载js的时候阻塞...

2369
来自专栏新工科课程建设探讨——以能源与动力工程专业为例

9.2 时域分析与频域分析

时域和频域分析师自动控制原理里非常重要的内容,也是《热工过程自动控制》里的基础内容。首先分析一个常见的RC网络电路:

1090
来自专栏天天P图攻城狮

iOS GPUImage源码解读(一)

最近在不断学习、使用的过程中,有了更深刻的理解,特来写一篇源码解读的文章详细介绍下核心代码的具体实现。

2K9
来自专栏Flutter入门

Android OpenGL ES(六) - 将输入源换成视频

上文中,我们是将相机提供的预览画面输入到Surface当中,然后进行滤镜处理和录制。 那我们可以切换输入源为视频吗?

1704
来自专栏wOw的Android小站

[Android] 使用MediaProjection截屏

Android5.0以上提供了MediaProjection,方便截屏录屏等功能。

1901
来自专栏极乐技术社区

『组件』大转盘、刮刮乐、老虎机……

组件 框架为开发者提供了一系列基础组件,开发者可以通过组合这些基础组件进行快速开发。 什么是组件: 组件是视图层的基本组成单元。 组件自带一些功能与微信风格的样...

2956
来自专栏WebDeveloper

大转盘的实现

1115
来自专栏菩提树下的杨过

Flash/Flex学习笔记(39):弹性运动

动画中的弹性运动 从视觉效果上接近 物理经典力学中的单摆运动或弹簧(胡克定律F=Kx)振动 先看下面的模拟演示: 规律: 小球先从出发点(初始为最左侧)向目标点...

1865
来自专栏儿童编程

儿童编程Scratch之“画笔”基础功能学习总结

Scratch中“画笔”功能能够让使用者模拟画笔在舞台上创作,合理运用能够给作品带来极大的趣味性。

1432

扫码关注云+社区