连线自动路由算法:在GEF中实现连线的自动直角路由,智能避障并绕开模型,选择最佳路径进行布线,仿Visio效果
2011-09-21 20:10
411 查看
在使用GEF(图形编辑框架)开发建模工具时,比如利用GEF实现程序流程图建模功能,有时对连线的路由方式会有比较高的要求,比如连线自动采用直角布局,要能够智能地避障并绕开模型,选择最佳路径进行布线。在建模类工具中,Microsoft Visio基本流程图中的连线的智能效果做的是同类工具中最好的,起码作者感觉如此。这篇博客就介绍如何在GEF中为连线实现类似Visio中的智能效果。当然,本文以GEF为背景和实例进行介绍,文中的路由算法和思想同样可以应用于其他有类似需求的应用,在此不做赘述。
首先对GEF框架提供的连线路由进行简单介绍。除了最基本的两点之间的直线布局以外,GEF提供的连线路由主要有3种:
1. 拐点路由(BendpointConnectionRouter)
2. 最短路路由(ShortestPathConnectionRouter)
3. 曼哈顿路由(ManhattanConnectionRouter)
以上3种路由方式均不能满足本文第一段提出的一些连线路由的要求。要实现仿Visio连线的效果,我们需要首先对Visio中连线的路由的特点进行总结,其特点包括:
1. 一旦用户对某条连线的路由方式进行了调整,就不再为其提供智能路由布线的功能。
虽然Visio连线的路由已经做的足够智能,但依旧不可能满足用户的所有的布线要求,故Visio中的连线依旧提供着用户对连线的路由进行调整的功能。当用户对一条连线的路由进行调整后,工具将不再为该连线提供路由支持,而是采用用户调整以后的路由方式来布线,即便发生了连线与模型的布局冲突依旧如此。
2. 用户是否对连线的路径进行了调整的信息是需要持久存储的。
对一幅图中的每一条连线,用户对哪些进行了路由调整需要进行记录,当这幅图关闭下次重新打开时,需要知道应该为哪些连线提供路由支持,不为哪些连线提供路由支持。
3. 用户在建模时,只有当发生了连线和模型的布局冲突时,才对存在布局冲突的连线进行路由调整,对其他连线不做更改。
本文的连线路由算法以GEF提供的拐点路由(BendpointConnectionRouter)为基础,对拐点路由做进一步改进,以实现智能路由的效果。以拐点路由(BendpointConnectionRouter)为基础的好处是,可以很好的利用该路由支持用户对连线路径进行调整的功能。首先,我们来了解一下拐点路由(BendpointConnectionRouter)连线的一些基本知识。
采用拐点路由的连线上保存着一个一个的拐点(BendpointModel),BendpointModel是Bendpoint的子类,只有2个属性d1和d2,类型均为Dimension。d1保存拐点相对于连线起点的偏移量,d2保存拐点相对于连线终点的偏移量。d1的类型为Dimension,width对应x坐标偏移,height对应y坐标偏移,d2与之相同。(一点题外话,当连线的起点、终点和拐点相对于起点的偏移量d1已知时,拐点相对于连线终点的偏移量d2时唯一确定的,本人认为BendpiointModel中同时记录着d1和d2有些冗余)这里还需要说明一点,本文要实现路由算法的连线不是GEF默认提供的连线,而是对默认连线改造后两端能够悬空存在的连线,点此查看相关背景知识。
1. 实现连线和模型布局的冲突检测。
什么样的布局可以被认为是冲突的呢?当连线发生紧贴模型,或连线横穿模型时,就可以认为连线和模型的布局发生了布局冲突,见下图,此时就需要对连线的路由方式进行调整。需要说明一点,布局冲突检测只负责检测直角连线(如果一条连线被拐点分割开的所有的线段均处于水平或垂直方向,没有斜线,我们称这样的连线为直角连线)的布局冲突,后面会解释为什么只要进行这种检测就已经足够了。
算法1:连线与箭头箭尾模型布局冲突检测算法:
对该算法的解释:当箭头或箭尾处发生了连线和模型的布局冲突时,就认为存在布局冲突。下面以箭尾处为例来介绍如何判断。对于一个矩形模型而言,它允许连线的地方有4个,即上下左右边的中点,这里我们以东西南北来称呼这4个位置。同样箭尾处连线的方向也可以用东西南北来标识。如下图:
当连线的方向和它与模型的连接位置相同时,则认为不存在布局冲突;否则,认为存在冲突。算法中,getConnectedPosition()获取连线锚点与模型的连接位置,返回值为东、西、南或北(东西南北为定义的整形常量);getConnecterDirectionInDetail()获取箭头或箭尾处连线的方向,返回值为东、西、南或北。
算法2: 连线与横穿模型的情况的布局冲突检测算法
该算法用于检测一条连线是否和一个矩形发生了布局冲突。实现思路为获取连线上线段的集合,然后对线段集合进行遍历,判断每一个线段是否与给定矩形发生布局冲突。只要存在任何一个线段与矩形发生了布局冲突,则认为连线与矩形见存在布局冲突。类Segment表示一个线段,其中保存了线段的起点和终点,类的定义如下:
算法2中segmentHitTest()的作用为检测一个线段是否和图元(矩形)发生了布局冲突。如果存在布局冲突,返回存在布局冲突的矩形集合;否则,返回null。该方法的定义如下:
在该方法中,第一个参数rectangleList表示进行冲突检测的所有矩形;第二个参数segment表示进行冲突检测的线段;第三个参数表示cornerConflictContained表示冲突检测时是否考虑拐角冲突的情况(如果线段线段没有横穿矩形,而是线段的一个端点位于矩形内部或2个端点均位于矩形内部,称这种情况为拐角冲突)。如下图中的2种情况为拐角冲突的情况,如果cornerConflictContained为true,则认为存在布局冲突,如果cornerConflictContained为false,则认为不存在布局冲突。
这个算法的实现思路比较简单,首先判断线段是水平还是垂直方向,对于垂直方向的线段,首先找到图中所有跨过线段x坐标的矩形,再对该集合进行遍历,判断矩形的上沿和下沿的坐标是否位于线段的范围内;水平方向的线段的处理方法与之类似。实现过程相见上面的代码,从而最终判断处线段是否与矩形发生了布局冲突。在该方法中需要说明的方法有,getDirection()用于判断一个坐标位于另一个坐标的什么方向,如果2个点坐标相同,则返回NONE,否则,返回东、南、西、北、东北、东南、西北或西南。getRectangleCrossingX()用于获取所有横跨X坐标的矩形(矩形的某个边紧贴坐标x时同样认为该矩形横跨了指定坐标);getRectangleCrossingY()获取所有横跨y坐标的矩形(矩形的某个边紧贴线段时同样认为横跨了指定坐标);
算法3:拐点归并算法
算法3是整个连线路由算法的核心,该算法对连线上的所有拐点进行检查,将距离小于误差容忍度的拐点进行归并,并处理存在斜线的拐点。(需要说明的一点是,该算法虽然名称叫做拐点归并算法,但该算法中不只是对拐点进行归并删除,有点情况也会在连线上增加必要的拐点,以使连线成为直角连线。)什么是误差容忍度呢?误差容忍度是定义的一个整数常量,这里定义为10个像素。水平或垂直方向上坐标差小于误差容忍度的2个拐点可以认为是出于同一条直线上的拐点,应该对这种拐点进行归并。注,下文中会提到定长这一个称呼,定长制的就是此处的误差容忍度。如下图所示,如果点线段BC的长度小于误差容忍度,则需要将直角连线ABCD归并为直线EF:
拐点归并算法的接口为:
返回值类型为void,参数为程序流程图连线。整个算法的整体结构伪代码如下:
下面,对该算法进行解释。
首先介绍连线上有1个拐点的情况。这种情况下,只需要删除连线上的那个拐点,然后调用拐点归并算法即可,如下:
当连线上有2个拐点时,采用如下伪代码进行处理:
下面介绍连线上拐点个数大于等于3时的处理方法。
下面介绍当连线上有0个拐点时的处理方式。当连线上有0个拐点时,需要根据具体情况,为连线添加合适的拐点,以使连线成为直角连线,且自动绕开模型;为了思路清晰,在介绍该算法时分为2个步骤来介绍:第一步,该连线添加拐点时不考虑连线是否与其他模型发生布局冲突,只要保证连线是直角连线且箭头箭尾垂直于连接着的模型(如果有的话)即可;第二步,对第一步中形成的直角连线进行布局冲突检测,如果发生了布局冲突,则对连线进行路由算法,搜索一条可以绕过模型的路由进行调整。需要说明的是,该算法是一种比较保守的算法,即算法只进行有限次数的搜索尝试(大概400到500次),如果在这有限次的尝试中未能找到可以绕开模型的路由(找不到路由的情况极少出现),则放弃尝试,采用第一步中的路由作为连线的最终路由(尽管可能存在布局冲突,但毕竟我们的连线还提供着用户手动对其路由进行调整的功能,剩下的路由工作就由用户手动完成吧)。
首先介绍第一步。第一步的实现算法的整体结构如下所示:
从上述算法可以看出,在第一步的实现过程中,根据箭头,箭尾是否连接着模型或悬空分成了4种情况,第一种情况(箭头箭尾都悬空)的实现方式比较简单,另外3种情况实现方式类似,算法中只给出了箭尾不悬空,箭头悬空情况下的处理方法。可以看出,算法采取的是穷举的方式,根据箭尾位于模型的东南西北可以分为4种情况,在每种情况中,又根据箭头位于箭尾的什么方向分为了8种情况。当然这是一种比较笨的方法,在具体实现时,有很多种情况是可以归结为同一种方法来处理的,至于如何归结,则需要我们对各种情况下的布线方式和拐点坐标的计算方式进行统计和总结,最终将可以归结的情况进行归并,以简化算法。下面举例说明各种情况下如何布线和如何将不同的情况进行归结。
在上图中,列出的是箭尾不悬空,箭头悬空,且箭尾位于模型的北部,箭头位于箭尾的东北,东南,西北,西南方向时4种情况下的布线方式。那么图中哪些情况可以归并呢?情况1和情况3是可以归并成一种情况的。原因在于,情况1和情况3的连线上都只有一个拐点,且计算拐点的坐标偏移时采取的公式是相同的,即拐点x坐标偏移 = 0;拐点的y坐标偏移 = 箭头.y - 箭尾.y;所以在算法中的2个if - else 语句就可以合成为一个if( 箭头在箭尾的东北 || 箭头在箭尾的西北 ){坐标计算公式...}了,这就是进行情况归并的方法。
下面介绍第二步。第二步的目的是对第一步中产生的直角连线进行冲突检测(前文中介绍冲突检测算法时曾经提到,只需要对直角连线进行冲突检测就足够,这里就是其原因),如果存在布局冲突则对连线的路由方式进行调整,搜索一条可以绕开模型的路由,从而最终实现连线的避障功能。实现第二步的主要穷举和思想是分而治之。所谓穷举,穷举的是在不考虑布局冲突时所有情况下连线的走向,每一种走向作为一种情况;算法对每一种情况的连线提供一个方法,以实现该种连线的冲突检测和自动路由探索。这里还需要详细解释一下什么叫连线的走向。如下图所示,
情况1和情况2中的连线是走向相同的连线,走向可以解释为从箭尾到箭头依次需要朝什么方向前进,在情况1中,从箭尾到箭头需要先往北,再往西,再往南,情况2中的连线相同,这种连线可以成为“北西南连线”。
所谓分而治之,指的是,在实现路由搜索时,只需要实现几种最基本走向的连线的路由搜索算法就可了;走向复杂的连线可以被分解为几种基本走向的连线,然后调用基本走向连线的路由搜索算法,最后根据每个被分解开的子连线搜索到的拐点,将上述拐点合并到整条连线,即可实现整条连线的路由索搜算法,这就是分而治之思想的应用。
下面给出各种走向的连线以及它们之间分而治之的调用关系图(点此下载清晰原始图片):
该调用关系图看似复杂,其实不然,下面分步骤对该图进行解释。
1. 图中每个矩形框均代表一个函数,每个函数实现某种走向的连线的冲突检测及自动路由功能。
2. 图中的箭头代表了函数间的调用关系,且由父函数指向子函数,代表的意思是,父函数表示的连线要被分解为子函数表示的连线来进行处理(分而治之)。
3. 为了便于表达,所有函数按颜色分成了5类,每种颜色的函数连线上拐点个数相同的连线,例如***的函数处理的是只有一个拐点的连线,绿色的函数处理的是有2个拐点的连线等等等等。
4. 函数的命名规则及其意义。所有函数的命名方式为hitTestFor...Connecter,意思是为...的连线进行冲突检测和路由。每个函数名的中间部分由大写字母N(北), S(南), W(西), E(东)的组合来指明该函数处理哪种走向的连线。例如hitTestFor_SWNE_Connecter()表明该函数处理南西北东走向的连线。有些函数同时处理2种走向的连线,例如:hitTestFor_NEN_NWN_Connecter()处理北东北走向和北西北走向的连线。
由于整个算法采用分而治之的思想来实现,则我们的讲解也从最底层的函数开始讲起。
1. hitTestFor0BendpointConnecter()函数的实现。
该函数用于处理有0个拐点的连线,上图中紫色的函数。该函数的函数签名为private static void hitTestFor0BendpointConnecter( connecter );函数实现算法如下所示:
所以,进行路由搜索的思路为确定线段1->2的x坐标,以及拐点0和拐点3的y坐标。具体实现详见代码,此处不再详述。另外3个算法与之类似。
2. 下面介绍对存在1个拐点的连线进行路由的算法,即图中***部分的函数的实现。
有1个拐点的连线共有8种情况:如下图(线的2端可能是悬空的 ):
这里以hitTestFor_WS_Connecter()为例介绍这8个函数的实现方法。
对于“函数调用关系图”中给出的所有函数(hitTestFor0BendpointConnecter()除外),都采用了基本上统一的处理思路进行实现。那就是,在对各种走向的连线进行路由时,首先进行默认路由方式(可能存在2种或3种默认路由方式)的冲突检测,如果默认路由方式不存在布局冲突,则函数结束;否则才进行下一步处理;而在下一步处理中,同样采用了相同的思路,即,对于只有1个拐点的的底层处理函数(在分而治之中处于较低的层次,故称之为底层处理函数),一般采用搜索的方向寻找可以布线的路由,而对于有多个拐点的连线的处理函数,一般是将其拆分为几段简单的连线,然后对各个子连线就可以调用底层的处理函数,待所有的子连线的路由都找到,即可通过计算得到整条连线的路由。以hitTestFor_WS_Connecter()函数为例来说,它的默认路由方式有2种,如下所示:
在hitTestFor_WS_Connecter()函数实现时,首先检测这2种路由方式是否存在冲突,如果不存在冲突,则函数结束;如果存在冲突,则进行下一步的搜索,首先给出示意图:
图中右下角所示的是我们进行路由搜索的目标。进行搜索时,首先找到箭尾向西无障碍的区域,图中的A点;在找到箭头向北无障碍的区域,图中的B点;然后,我们需要在灰色矩形区域内搜索到一条可以布线的X坐标,搜索时以西侧为优先,目的是为了为搜索到目标路由中的横线提供最大的可能性,假设搜索到可以布线的位置为DE;然后在***矩形区域内搜索到可以布线的横线,假设搜索到的位置为IJ,则最终的路由为箭尾->H->J->I->箭头;如果这样的尝试失败,则可以先搜索目标路由中的横线,再搜索目标路由中的竖线,思路类似。具体代码如下:
对只有1个拐点的连线进行处理的另外7个函数的实现与之类似,此处不再赘述。
3. 下面介绍对存在2个拐点的连线的处理函数,即“函数调用关系图”中红色的函数。
函数处理的整体思路在上文已有介绍,这里对如何对连线进行分而治之进行解释和说明。以SES走向的连线为例进行说明。首先给出示意图
在上图中,首先求出箭尾处向南的无障碍区域,图中A点;然后求出箭头处向北的无障碍区域,图中B点;然后将箭尾到A点,箭头到B点的线段以搜索步长(可以设置为10个像素或12个像素等)为单位进行分割,采用2个嵌套的for循环对2个线段的搜索步长分割点进行遍历,假设某一时刻,箭尾端取分割点C,箭头端取fengedianB,则整个连线被分割为3段,我们需要对中间的一段即CB调用子函数来处理,可以假想CB的原始路由为途中粉红色的折线,则它就应该调用ES走向的连线的处理函数来处理。最终将3段连线的拐点进行计算即可求得整条连线的路由。对连线进行分割和调用哪个子函数进行处理的选择可能是多样的,“函数调用关系图”中列出的只是作者做出的选择。下面是具体代码:
上面介绍的是连线路由算法的实现,但仅有这些是不够的,下面列出的问题同样需要进行处理。
当移动连线锚点时,需要进行怎样的处理?
当移动连线锚点时,由于连线上的拐点记录的相对于箭头和箭尾的坐标偏移,所以连线上所有的拐点需要进行偏移坐标的调整;移动箭尾锚点时,需要调整地一个拐点的坐标,以使移动后的连线依旧是一条直角连线;同理,移动箭头锚点时,需要调整最后一个拐点的坐标,以使连线依旧是一条直角连线。以上所述处理完成后,调用布局冲突检测算法进行一下检验,如果发生了布局冲突,则调用路由算法进行路由处理。
移动 连线的拐点时,需要进行怎样的处理?
移动连线的拐点时,同样需要保证移动后连线是一条直角连线。这就要求移动某个连线拐点时,需要同时移动它的前一个拐点和后一个拐点,以满足这一要求;更进一步,如果当前移动的拐点时连线上的第一个或最后一个拐点,则需要在它前面或后面添加一个新的拐点,以保证连线是直角连线;完成上述操作后,调用布局冲突检测算法进行一下检验,如果发生了布局冲突,则调用路由算法进行路由处理。
创建连线拐点时,需要进行怎样的处理?
创建连线拐点时,需要考虑新创建连线拐点的前一个和后一个拐点,根据新创建的连线拐点的坐标,调整前一个和后一个拐点的坐标,以使连线是一条直角连线;如果新创建的连线拐点是连线上的第一个或最后一个拐点,则可能需要再添加一些拐点才能满足直角连线的要求。完成上述操作后,调用布局冲突检测算法进行一下检验,如果发生了布局冲突,则调用路由算法进行路由处理。
删除连线拐点,需要进行怎样的处理?
不需要多余的处理,只要调用布局冲突检测算法进行一下检验,如果发生了布局冲突,则调用路由算法进行路由处理。
点此下载所有相关代码。
结束。
首先对GEF框架提供的连线路由进行简单介绍。除了最基本的两点之间的直线布局以外,GEF提供的连线路由主要有3种:
1. 拐点路由(BendpointConnectionRouter)
2. 最短路路由(ShortestPathConnectionRouter)
3. 曼哈顿路由(ManhattanConnectionRouter)
以上3种路由方式均不能满足本文第一段提出的一些连线路由的要求。要实现仿Visio连线的效果,我们需要首先对Visio中连线的路由的特点进行总结,其特点包括:
1. 一旦用户对某条连线的路由方式进行了调整,就不再为其提供智能路由布线的功能。
虽然Visio连线的路由已经做的足够智能,但依旧不可能满足用户的所有的布线要求,故Visio中的连线依旧提供着用户对连线的路由进行调整的功能。当用户对一条连线的路由进行调整后,工具将不再为该连线提供路由支持,而是采用用户调整以后的路由方式来布线,即便发生了连线与模型的布局冲突依旧如此。
2. 用户是否对连线的路径进行了调整的信息是需要持久存储的。
对一幅图中的每一条连线,用户对哪些进行了路由调整需要进行记录,当这幅图关闭下次重新打开时,需要知道应该为哪些连线提供路由支持,不为哪些连线提供路由支持。
3. 用户在建模时,只有当发生了连线和模型的布局冲突时,才对存在布局冲突的连线进行路由调整,对其他连线不做更改。
本文的连线路由算法以GEF提供的拐点路由(BendpointConnectionRouter)为基础,对拐点路由做进一步改进,以实现智能路由的效果。以拐点路由(BendpointConnectionRouter)为基础的好处是,可以很好的利用该路由支持用户对连线路径进行调整的功能。首先,我们来了解一下拐点路由(BendpointConnectionRouter)连线的一些基本知识。
采用拐点路由的连线上保存着一个一个的拐点(BendpointModel),BendpointModel是Bendpoint的子类,只有2个属性d1和d2,类型均为Dimension。d1保存拐点相对于连线起点的偏移量,d2保存拐点相对于连线终点的偏移量。d1的类型为Dimension,width对应x坐标偏移,height对应y坐标偏移,d2与之相同。(一点题外话,当连线的起点、终点和拐点相对于起点的偏移量d1已知时,拐点相对于连线终点的偏移量d2时唯一确定的,本人认为BendpiointModel中同时记录着d1和d2有些冗余)这里还需要说明一点,本文要实现路由算法的连线不是GEF默认提供的连线,而是对默认连线改造后两端能够悬空存在的连线,点此查看相关背景知识。
1. 实现连线和模型布局的冲突检测。
什么样的布局可以被认为是冲突的呢?当连线发生紧贴模型,或连线横穿模型时,就可以认为连线和模型的布局发生了布局冲突,见下图,此时就需要对连线的路由方式进行调整。需要说明一点,布局冲突检测只负责检测直角连线(如果一条连线被拐点分割开的所有的线段均处于水平或垂直方向,没有斜线,我们称这样的连线为直角连线)的布局冲突,后面会解释为什么只要进行这种检测就已经足够了。
算法1:连线与箭头箭尾模型布局冲突检测算法:
/** * 检测连线是否与箭尾和箭头模型存在布局上的冲突。即判断一条连线是否发生了横穿或紧贴模型的情况。 * * @param flowChartConnecter * @return 如果存在连线横穿模型的情况,则返回true;否则,返回false。 */ public static boolean hitTestWithTailAndHeadModel( FlowChartConnecterModel flowChartConnecter) { if (flowChartConnecter == null) return false; FlowChartModel tailModel = flowChartConnecter.getTailModel(); FlowChartModel headModel = flowChartConnecter.getHeadModel(); // 箭尾模型处是否发生了冲突 boolean tailConflict = false; // 箭头模型处是否发生了冲突 boolean headConflict = false; // 判断箭尾模型是否与连线发生了冲突 if (tailModel != null) { ConnecterAnchorModel tailAnchor = flowChartConnecter.getTail(); int tailAnchorPosition = getConnectedPosition(tailAnchor); int tailConnecterDirection = getConnecterDirectionInDetail(tailAnchor); if (tailAnchorPosition != tailConnecterDirection) { tailConflict = true; } } // 判断箭头模型是否与连线发生了冲突 if (headModel != null) { ConnecterAnchorModel headAnchor = flowChartConnecter.getHead(); int headAnchorPosition = getConnectedPosition(headAnchor); int headConnecterDirection = getConnecterDirectionInDetail(headAnchor); if (headAnchorPosition != headConnecterDirection) { headConflict = true; } } if (tailConflict || headConflict) { return true; } return false; }
对该算法的解释:当箭头或箭尾处发生了连线和模型的布局冲突时,就认为存在布局冲突。下面以箭尾处为例来介绍如何判断。对于一个矩形模型而言,它允许连线的地方有4个,即上下左右边的中点,这里我们以东西南北来称呼这4个位置。同样箭尾处连线的方向也可以用东西南北来标识。如下图:
当连线的方向和它与模型的连接位置相同时,则认为不存在布局冲突;否则,认为存在冲突。算法中,getConnectedPosition()获取连线锚点与模型的连接位置,返回值为东、西、南或北(东西南北为定义的整形常量);getConnecterDirectionInDetail()获取箭头或箭尾处连线的方向,返回值为东、西、南或北。
算法2: 连线与横穿模型的情况的布局冲突检测算法
/** * 检查某条连线是否与某个矩形发生了布局冲突 * * @param connecter * @param rectangle * @return */ private static boolean hitTestWithSingleRectangle( FlowChartConnecterModel connecter, Rectangle rectangle) { if (connecter == null || rectangle == null) { return false; } ArrayList<Segment> segmentList = getSegmentList(connecter); ArrayList<Rectangle> rectangleList = new ArrayList<Rectangle>(); rectangleList.add(rectangle); for (Segment segment : segmentList) { Object result = segmentHitTest(rectangleList, segment, true); if (result != null) {// 如果发生冲突 // 就没有必要再对剩下的线段进行测试了 return true; } } return false; }
该算法用于检测一条连线是否和一个矩形发生了布局冲突。实现思路为获取连线上线段的集合,然后对线段集合进行遍历,判断每一个线段是否与给定矩形发生布局冲突。只要存在任何一个线段与矩形发生了布局冲突,则认为连线与矩形见存在布局冲突。类Segment表示一个线段,其中保存了线段的起点和终点,类的定义如下:
算法2中segmentHitTest()的作用为检测一个线段是否和图元(矩形)发生了布局冲突。如果存在布局冲突,返回存在布局冲突的矩形集合;否则,返回null。该方法的定义如下:
/** * 检查一个线段是否和图元发生了布局冲突 * * @param rectangleList * @param location1 * 绝对坐标 * @param location2 * 绝对坐标 * @param segment * 线段的2端不分先后顺序 * @param cornerConflictContained * 是否包含拐角冲突的情况 * @return 存在布局冲突时,返回存在布局冲突的矩形的集合; 不存在布局冲突时返回null。 */ private static ArrayList<Rectangle> segmentHitTest( ArrayList<Rectangle> rectangleList, Segment segment, boolean cornerConflictContained) { if (rectangleList == null || segment == null) { return null; } // 起点 Point startingPoint = segment.getStartingPoint(); // 终点 Point finishingPoint = segment.getFinishingPoint(); int direction = getDirection(startingPoint, finishingPoint); if (direction != PositionConstants.NORTH && direction != PositionConstants.SOUTH && direction != PositionConstants.WEST && direction != PositionConstants.EAST) { // 允许有6个像素的误差 if (Math.abs(startingPoint.x - finishingPoint.x) <= 6) { // 垂直 if (startingPoint.y >= finishingPoint.y) { direction = PositionConstants.NORTH; } else { direction = PositionConstants.SOUTH; } } if (Math.abs(startingPoint.y - finishingPoint.y) <= 6) { // 水平 if (startingPoint.x >= finishingPoint.y) { direction = PositionConstants.WEST; } else { direction = PositionConstants.EAST; } } } // 垂直方向 if (direction == PositionConstants.NORTH || direction == PositionConstants.SOUTH) { // 垂直线段的x坐标 int x = startingPoint.x; // 所有横跨指定x坐标的矩形 ArrayList<Rectangle> rectanglesCrossingX = getRectangleCrossingX( rectangleList, x); // 与线段存在布局冲突的矩形 ArrayList<Rectangle> conflictedRectangles = new ArrayList<Rectangle>(); int smallY = 0;// 线段北端点的y坐标 int bigY = 0;// 线段南端点的y坐标 if (startingPoint.y > finishingPoint.y) { smallY = finishingPoint.y; bigY = startingPoint.y; } else { smallY = startingPoint.y; bigY = finishingPoint.y; } // 对矩形进行遍历 for (Rectangle rect : rectanglesCrossingX) { if (cornerConflictContained) {// 包含拐角冲突的情况 if (rect.y > bigY || rect.y + rect.height < smallY) { // 不冲突 } else { conflictedRectangles.add(rect); } } else {// 不包含拐角冲突的情况 if (rect.y > smallY && rect.y + rect.height < bigY) { conflictedRectangles.add(rect); } } } if (conflictedRectangles.size() == 0) { return null; } return conflictedRectangles; } // 水平方向 else if (direction == PositionConstants.EAST || direction == PositionConstants.WEST) { // 垂直线段的x坐标 int y = startingPoint.y; // 所有横跨指定x坐标的矩形 ArrayList<Rectangle> rectanglesCrossingY = getRectangleCrossingY( rectangleList, y); // 与线段存在布局冲突的矩形 ArrayList<Rectangle> conflictedRectangles = new ArrayList<Rectangle>(); int smallX = 0;// 线段西侧端点的x坐标 int bigX = 0;// 线段东侧端点的x坐标 if (startingPoint.x > finishingPoint.x) { smallX = finishingPoint.x; bigX = startingPoint.x; } else { smallX = startingPoint.x; bigX = finishingPoint.x; } // 对矩形进行遍历 for (Rectangle rect : rectanglesCrossingY) { if (cornerConflictContained) {// 包含拐角冲突的情况 if (rect.x > bigX || rect.x + rect.width < smallX) { // 不冲突 } else { conflictedRectangles.add(rect); } } else {// 不包含拐角冲突的情况 if (rect.x > smallX && rect.x + rect.width < bigX) { conflictedRectangles.add(rect); } } } if (conflictedRectangles.size() == 0) { return null; } return conflictedRectangles; } return null; }
在该方法中,第一个参数rectangleList表示进行冲突检测的所有矩形;第二个参数segment表示进行冲突检测的线段;第三个参数表示cornerConflictContained表示冲突检测时是否考虑拐角冲突的情况(如果线段线段没有横穿矩形,而是线段的一个端点位于矩形内部或2个端点均位于矩形内部,称这种情况为拐角冲突)。如下图中的2种情况为拐角冲突的情况,如果cornerConflictContained为true,则认为存在布局冲突,如果cornerConflictContained为false,则认为不存在布局冲突。
这个算法的实现思路比较简单,首先判断线段是水平还是垂直方向,对于垂直方向的线段,首先找到图中所有跨过线段x坐标的矩形,再对该集合进行遍历,判断矩形的上沿和下沿的坐标是否位于线段的范围内;水平方向的线段的处理方法与之类似。实现过程相见上面的代码,从而最终判断处线段是否与矩形发生了布局冲突。在该方法中需要说明的方法有,getDirection()用于判断一个坐标位于另一个坐标的什么方向,如果2个点坐标相同,则返回NONE,否则,返回东、南、西、北、东北、东南、西北或西南。getRectangleCrossingX()用于获取所有横跨X坐标的矩形(矩形的某个边紧贴坐标x时同样认为该矩形横跨了指定坐标);getRectangleCrossingY()获取所有横跨y坐标的矩形(矩形的某个边紧贴线段时同样认为横跨了指定坐标);
算法3:拐点归并算法
算法3是整个连线路由算法的核心,该算法对连线上的所有拐点进行检查,将距离小于误差容忍度的拐点进行归并,并处理存在斜线的拐点。(需要说明的一点是,该算法虽然名称叫做拐点归并算法,但该算法中不只是对拐点进行归并删除,有点情况也会在连线上增加必要的拐点,以使连线成为直角连线。)什么是误差容忍度呢?误差容忍度是定义的一个整数常量,这里定义为10个像素。水平或垂直方向上坐标差小于误差容忍度的2个拐点可以认为是出于同一条直线上的拐点,应该对这种拐点进行归并。注,下文中会提到定长这一个称呼,定长制的就是此处的误差容忍度。如下图所示,如果点线段BC的长度小于误差容忍度,则需要将直角连线ABCD归并为直线EF:
拐点归并算法的接口为:
public static void mergingBendpoints(FlowChartConnecterModel flowChartConnecter)
返回值类型为void,参数为程序流程图连线。整个算法的整体结构伪代码如下:
mergingBendpoints(连线){ if(连线上有0个拐点){ } if(连线上有1个拐点){ } if(连线上有2个拐点){ } if(连线上有3个及以上个拐点){ } }
下面,对该算法进行解释。
首先介绍连线上有1个拐点的情况。这种情况下,只需要删除连线上的那个拐点,然后调用拐点归并算法即可,如下:
if(连线上有1个拐点){ 删除拐点; mergingBendpoints(连线); }
当连线上有2个拐点时,采用如下伪代码进行处理:
if(箭头,箭尾,和2个拐点位于一条直线上){ 删除2个拐点; return; } if (2个拐点位于垂直方向) { if(拐点1和拐点2垂直方向的距离小于定长){ 删除连线上的2个拐点,将连线归并为一条直线即可。 由于箭头箭尾的坐标存在一定的偏移,以谁为基准需要考虑箭头箭尾是否连接着模型。 以连接着模型的那一端的坐标为基准,将另一端的坐标进行调整,以使连线成为一条直线。 return; } if (拐点1和箭尾的水平方向的距离小于定长) {//进行归并 删除拐点; mergingBendpoints(连线); return; } if (拐点2和箭头的水平方向的距离小于定长) {//进行归并 删除拐点; mergingBendpoints(连线); return; } } if (2个拐点位于水平方向) { //处理方式与2个拐点位于垂直方向的情况类似 if(拐点1和拐点2水平方向的距离小于定长){ 删除连线上的2个拐点,将连线归并为一条直线即可。 由于箭头箭尾的坐标存在一定的偏移,以谁为基准需要考虑箭头箭尾是否连接着模型。 以连接着模型的那一端的坐标为基准,将另一端的坐标进行调整,以使连线成为一条直线。 return; } if (拐点1和箭尾的垂直方向的距离小于定长) {//进行归并 删除拐点; mergingBendpoints(连线); return; } if (拐点2和箭头的垂直方向的距离小于定长) {//进行归并 删除拐点; mergingBendpoints(连线); return; } } /* * 代码执行到这里时,只存在4中情况:1、 正常的连线情况。2、两个拐点间连成斜线。3、 第一个拐点和箭尾连成了一条斜线、 4、 * 第二个拐点和箭头连成了一条斜线 * 。(注意:第一个拐点和箭尾连成斜线,并且第二个拐点和箭头连成斜线,并且两个拐点间连成斜线的情况是不存在的。) */ direction = 第一个拐点到第二个拐点的方向; if (direction == 水平) { if (箭尾和第一个拐点之间形成斜线) { 第一个拐点的x坐标偏移 = 0; 第一个拐点的y坐标偏移 = 第二个拐点的y坐标偏移; mergingBendpoints(连线); }else if (箭头和第二个拐点之间形成斜线) { 第二个拐点的x坐标偏移 = 箭头.x - 箭尾.x; 第二个拐点的y坐标偏移 = 第一个拐点的y坐标偏移; mergingBendpoints(连线); } }else if (direction == 垂直) { //处理方式与direction == 水平的情况类似 分箭尾和第一个拐点间形成斜线与箭头和第二个拐点间形成斜线2种情况来处理, 处理方法为调整形成斜线的拐点的坐标,以使其成为直线,然后调用 mergingBendpoints(连线); }else{//2个拐点间形成了斜线 /* * 这种情况下,拐点归并后的结果一定是仅有一个拐点。所以,问题的关键是确定这个仅有的拐点的位置。 * 通过对箭头在箭尾的什么方位(东北,西北,东南,西南)进行穷举,每种方位下又存在两种情况,通过分析, * 得出如下结论:如果第一个拐点的d1的width为0,则拐点的d1 = (0, head.y - tail.y); * 如果第一个拐点的d1的width不为0,则拐点的d1 = ( head.x - tail.x, 0). */ }
下面介绍连线上拐点个数大于等于3时的处理方法。
定义局部变量bendpoint1, bendpoint2; bendpoint1 = 连线上的第一个拐点; bendpoint2 = 连线上的第二个拐点; while (bendpoint2不为空) { 定义局部变量nextPoint = NULL; nextPoint = bendpoint2的下一个拐点(有可能为空); if (nextPoint为空) { nextPoint = 箭头锚点; } if (bendpoint1是连线上的第一个拐点) { if (箭尾,bendpoint1, bendpoint2位于一条直线上) { 删除bendpoint1; mergingBendpoints(连线); } } if (bendpoint1, bendpoint2, nextPoint位于一条直线上) { 删除bendpoint2; mergingBendpoints(连线); return; }else if (bendpoint1,bendpoint2 位于水平方向或垂直方向) { if (bendpoint1, bendpoint2, nextPoint间形成一个直角) { if (bendpoint1, bendpoint2位于垂直方向) { if (bendpoint1, bendpoint2之间的y距离小于定长) { // 如果有前一个拐点则删除bendpint1 and bendpoint2, // 并调整前一个拐点的位置;否则删除bendpoint2 and bendpoint2,并调整后一个拐点的位置 formerBendpoint = bendpoint1的前一个拐点; if (formerBendpoint 为空) { //前一个拐点不存在时,后一个拐点一定存在。 laterBendpoint = bendpoint2的下一个拐点。 laterBendpoint的y坐标偏移 = bendpoint1的y坐标偏移; 删除bendpoint1 and benpoint2; mergingBendpoints(连线); return }else{//前一个拐点存在 formerBendpoint的y坐标偏移 = bendpoint2的y坐标偏移; 删除bendpoint1 and bendpoint2; mergingBendpoints(连线); return; } }else{//正常情况 bendpoint1 = bendpoint2; benpoint2 = 获取下一个拐点; } }else if (bendpoint1, bendpoint2 位于水平方向) { //处理方式与bendpoint1, bendpoint2位于垂直方向类似 if (bendpoint1, bendpoint2之间的x距离小于定长) { // 如果有前一个拐点则删除bendpint1 and bendpoint2, // 并调整前一个拐点的位置;否则删除bendpoint2 and bendpoint2,并调整后一个拐点的位置 formerBendpoint = bendpoint1的前一个拐点; if (formerBendpoint 为空) { //前一个拐点不存在时,后一个拐点一定存在。 laterBendpoint = bendpoint2的下一个拐点。 laterBendpoint的x坐标偏移 = bendpoint1的x坐标偏移; 删除bendpoint1 and benpoint2; mergingBendpoints(连线); return }else{//前一个拐点存在 formerBendpoint的x坐标偏移 = bendpoint2的x坐标偏移; 删除bendpoint1 and bendpoint2; mergingBendpoints(连线); return; } }else{//正常情况 bendpoint1 = bendpoint2; benpoint2 = 获取下一个拐点; } } }else { //bendpoint2和下一个点形成斜线,在下一次循环进行处理 bendpoint1 = bendpoint2; benpoint2 = 获取下一个拐点; } }else{//bendpoint1, bendpoint2之间形成斜线 //拐点归并后的结果一定是将2个拐点合并为一个。 定义局部变量newBp;//合并后的新拐点 newBp的x坐标偏移 = bendpoint1的x坐标偏移; newBp的y坐标偏移 = bendpoint2的y坐标偏移; 删除bendpoint2; 将bendpoint1替换为newBp; mergingBendpoints(连线); return; } } /* * 循环结束后,需要处理第二个拐点为null的情况,因为该情况下,还存在几种特殊的情况,1、 * 最后一个拐点与箭头并非处于水平或垂直方向(即斜线情况)。 2、 * 最后一个拐点和箭头锚点间的距离小于容忍度,应该将其归并。这两种情况对于箭尾处同样存在。 */ // 首先处理箭头处的拐点 if (bendpoint1和箭头形成斜线) {//需要注意,bendpoint1是最后一个拐点 //判断箭头是悬空还是连接着模型,如果箭头悬空,则随意采取一种直角布线方式即可。 //若箭头连接着模型,则需要根据箭头在模型上的方位来确定布线的方式。 }else if (bendpoint1与箭头位于垂直方向) { 判断最后一个拐点和箭头y方向上的坐标差是否小于定长;如果是,则删除最后一个拐点, 然后调整上一个拐点的位置,以使直线成为直角连线。 }else if (bendpoint1与箭头位于水平方向) { 处理方式与bendpoint1与箭头位于水平方向的情况类似。判断最后一个拐点和箭头x方向上的 坐标差是否小于定长;如果是,则删除最后一个拐点,并调整上一个拐点的位置。 } // 下面处理箭尾处的拐点,处理方式参考箭头处拐点的处理方式即可,此处不再赘述。 firstBendpoint = 第一个拐点; if (firstBendpoint和箭尾形成斜线) { }else if (firstBendpoint与箭尾位于垂直方向) { }else if (firstBendpoint与箭尾位于水平方向) { }
下面介绍当连线上有0个拐点时的处理方式。当连线上有0个拐点时,需要根据具体情况,为连线添加合适的拐点,以使连线成为直角连线,且自动绕开模型;为了思路清晰,在介绍该算法时分为2个步骤来介绍:第一步,该连线添加拐点时不考虑连线是否与其他模型发生布局冲突,只要保证连线是直角连线且箭头箭尾垂直于连接着的模型(如果有的话)即可;第二步,对第一步中形成的直角连线进行布局冲突检测,如果发生了布局冲突,则对连线进行路由算法,搜索一条可以绕过模型的路由进行调整。需要说明的是,该算法是一种比较保守的算法,即算法只进行有限次数的搜索尝试(大概400到500次),如果在这有限次的尝试中未能找到可以绕开模型的路由(找不到路由的情况极少出现),则放弃尝试,采用第一步中的路由作为连线的最终路由(尽管可能存在布局冲突,但毕竟我们的连线还提供着用户手动对其路由进行调整的功能,剩下的路由工作就由用户手动完成吧)。
首先介绍第一步。第一步的实现算法的整体结构如下所示:
if (箭头,箭尾都悬空) { if (箭头箭尾x或y方向上的距离小于定长) { 则调整一段的坐标,将连线设置为水平的或垂直的。 }else{ 任意拐一个直角就可以了。 } }else if (箭尾不悬空,箭头悬空) { tailPosition = 箭尾位于模型的什么方位(东,南,西,北); headDirection = 箭头位于箭尾的什么方向(东,南,西,北,东南,东北,西南,西北); if (tailPosition == 北 ) { if (headDirection == 北) { ... }else if (headDirection == 南) { ... }else if (headDirection == 西) { ... }else if (headDirection == 东) { ... }else if (headDirection == 东北) { ... }else if (headDirection == 西北) { ... }else if (headDirection == 东南) { ... }else if (headDirection == 西南) { ... } }else if (tailPosition == 南) { ... }else if (tailPosition == 西) { ... }else if (tailPosition == 东) { ... } }else if (箭尾悬空,箭头不悬空) { ... }else if (箭头,箭尾都不悬空) { ... }
从上述算法可以看出,在第一步的实现过程中,根据箭头,箭尾是否连接着模型或悬空分成了4种情况,第一种情况(箭头箭尾都悬空)的实现方式比较简单,另外3种情况实现方式类似,算法中只给出了箭尾不悬空,箭头悬空情况下的处理方法。可以看出,算法采取的是穷举的方式,根据箭尾位于模型的东南西北可以分为4种情况,在每种情况中,又根据箭头位于箭尾的什么方向分为了8种情况。当然这是一种比较笨的方法,在具体实现时,有很多种情况是可以归结为同一种方法来处理的,至于如何归结,则需要我们对各种情况下的布线方式和拐点坐标的计算方式进行统计和总结,最终将可以归结的情况进行归并,以简化算法。下面举例说明各种情况下如何布线和如何将不同的情况进行归结。
在上图中,列出的是箭尾不悬空,箭头悬空,且箭尾位于模型的北部,箭头位于箭尾的东北,东南,西北,西南方向时4种情况下的布线方式。那么图中哪些情况可以归并呢?情况1和情况3是可以归并成一种情况的。原因在于,情况1和情况3的连线上都只有一个拐点,且计算拐点的坐标偏移时采取的公式是相同的,即拐点x坐标偏移 = 0;拐点的y坐标偏移 = 箭头.y - 箭尾.y;所以在算法中的2个if - else 语句就可以合成为一个if( 箭头在箭尾的东北 || 箭头在箭尾的西北 ){坐标计算公式...}了,这就是进行情况归并的方法。
下面介绍第二步。第二步的目的是对第一步中产生的直角连线进行冲突检测(前文中介绍冲突检测算法时曾经提到,只需要对直角连线进行冲突检测就足够,这里就是其原因),如果存在布局冲突则对连线的路由方式进行调整,搜索一条可以绕开模型的路由,从而最终实现连线的避障功能。实现第二步的主要穷举和思想是分而治之。所谓穷举,穷举的是在不考虑布局冲突时所有情况下连线的走向,每一种走向作为一种情况;算法对每一种情况的连线提供一个方法,以实现该种连线的冲突检测和自动路由探索。这里还需要详细解释一下什么叫连线的走向。如下图所示,
情况1和情况2中的连线是走向相同的连线,走向可以解释为从箭尾到箭头依次需要朝什么方向前进,在情况1中,从箭尾到箭头需要先往北,再往西,再往南,情况2中的连线相同,这种连线可以成为“北西南连线”。
所谓分而治之,指的是,在实现路由搜索时,只需要实现几种最基本走向的连线的路由搜索算法就可了;走向复杂的连线可以被分解为几种基本走向的连线,然后调用基本走向连线的路由搜索算法,最后根据每个被分解开的子连线搜索到的拐点,将上述拐点合并到整条连线,即可实现整条连线的路由索搜算法,这就是分而治之思想的应用。
下面给出各种走向的连线以及它们之间分而治之的调用关系图(点此下载清晰原始图片):
该调用关系图看似复杂,其实不然,下面分步骤对该图进行解释。
1. 图中每个矩形框均代表一个函数,每个函数实现某种走向的连线的冲突检测及自动路由功能。
2. 图中的箭头代表了函数间的调用关系,且由父函数指向子函数,代表的意思是,父函数表示的连线要被分解为子函数表示的连线来进行处理(分而治之)。
3. 为了便于表达,所有函数按颜色分成了5类,每种颜色的函数连线上拐点个数相同的连线,例如***的函数处理的是只有一个拐点的连线,绿色的函数处理的是有2个拐点的连线等等等等。
4. 函数的命名规则及其意义。所有函数的命名方式为hitTestFor...Connecter,意思是为...的连线进行冲突检测和路由。每个函数名的中间部分由大写字母N(北), S(南), W(西), E(东)的组合来指明该函数处理哪种走向的连线。例如hitTestFor_SWNE_Connecter()表明该函数处理南西北东走向的连线。有些函数同时处理2种走向的连线,例如:hitTestFor_NEN_NWN_Connecter()处理北东北走向和北西北走向的连线。
由于整个算法采用分而治之的思想来实现,则我们的讲解也从最底层的函数开始讲起。
1. hitTestFor0BendpointConnecter()函数的实现。
该函数用于处理有0个拐点的连线,上图中紫色的函数。该函数的函数签名为private static void hitTestFor0BendpointConnecter( connecter );函数实现算法如下所示:
private static void hitTestFor0BendpointConnecter(connecter ){ segment = 箭尾到箭头的线段; rectangleList = 图中所有图元的矩形框; 调用segmentHitTest(),求得与线段发生布局冲突的所有矩形; if(不存在布局冲突){ return; }else{ direction = 箭尾到箭头的方向; if (direction == 北) { setPathForNorthGoingConnecter(参数); }else if (direction == 南) { setPathForSouthGoingConnecter(参数); }else if (direction == 西) { setPathForWestGoingConnecter(参数); }else if (direction == 东) { setPathForEastGoingConnecter(参数); } } }在上述算法中,调用了4个字函数;下面对朝北走向的连线的路由算法setPathForNorthGoingConnecter()进行介绍:
/** * 为朝北走向的连线设置路径 * * @param rectangleList * 流程图内所有的矩形 * @param conflectedRectangleList * 与连线发生布局从图的矩形的集合,包括连线端点在矩形内部的情况 * @param connecterX * 线的x坐标 */ private static void setPathForNorthGoingConnecter( ArrayList<Rectangle> rectangleList, ArrayList<Rectangle> conflectedRectangleList, int connecterX, FlowChartConnecterModel connecter) { if (conflectedRectangleList == null || connecter == null) return; Point tailLocation = connecter.getTail().getLocation(); Point headLocation = connecter.getHead().getLocation(); // 对与连线发生了布局冲突的矩形进行检查,在线的东西两侧,障碍较窄的一边为线的绕线方向 int westEdge = Integer.MAX_VALUE; int eastEdge = 0; int northEdge = Integer.MAX_VALUE; int southEdge = 0; for (Rectangle rect : conflectedRectangleList) { if (rect.x < westEdge) {// 修正西侧边缘的值 westEdge = rect.x; } if (rect.x + rect.width > eastEdge) {// 修正东侧边缘的值 eastEdge = rect.x + rect.width; } if (rect.y < northEdge) {// 修正北侧边缘的值 northEdge = rect.y; } if (rect.y + rect.height > southEdge) {// 修正南侧边缘的值 southEdge = rect.y + rect.height; } } // 在这种情况下,连线上应该会产生4个拐点 // 首先选定第一个拐点的坐标 Point firstLocation = new Point(); if (southEdge > tailLocation.y || tailLocation.y - southEdge > 2 * Length.smallestMarginBetweenConnecterAndModel) { firstLocation = new Point(connecterX, tailLocation.y - Length.smallestMarginBetweenConnecterAndModel); } else { firstLocation = new Point(connecterX, (southEdge + tailLocation.y) / 2); } // 选定第四个拐点的坐标 Point fourthLocation = new Point(); if (northEdge < headLocation.y || headLocation.y - northEdge > 2 * Length.smallestMarginBetweenConnecterAndModel) { fourthLocation = new Point(connecterX, headLocation.y + Length.smallestMarginBetweenConnecterAndModel); } else { fourthLocation = new Point(connecterX, (headLocation.y + northEdge) / 2); } // 下面选定第二个和第三个拐点的坐标 // 如果从西侧布线,则其x坐标为 int westX = getWestPathPosition(rectangleList, westEdge, firstLocation, fourthLocation, connecterX); // 如果从东侧布线,则其x坐标为 int eastX = getEastPathPosition(rectangleList, eastEdge, firstLocation, fourthLocation, connecterX); // 比较westX和eastX,取他们与连线的偏移较小的一个为最终的布线的位置 int chosenX = (connecterX - westX) < (eastX - connecterX) ? westX : eastX; // 对chosenX进行非负修正 if (chosenX <= 0) { chosenX = eastX; } // 第二个拐点的坐标 Point secondLocation = new Point(chosenX, firstLocation.y); // 第三个拐点的位置 Point thirdLocation = new Point(chosenX, fourthLocation.y); // 将4个拐点的坐标转换为相对坐标,并设置连线的4个拐点 Dimension bp1_d1 = transferToDimension(tailLocation, firstLocation); Dimension bp1_d2 = getTheOtherRelativeDimension(tailLocation, headLocation, bp1_d1); BendpointModel bp1 = new BendpointModel(bp1_d1, bp1_d2); Dimension bp2_d1 = transferToDimension(tailLocation, secondLocation); Dimension bp2_d2 = getTheOtherRelativeDimension(tailLocation, headLocation, bp2_d1); BendpointModel bp2 = new BendpointModel(bp2_d1, bp2_d2); Dimension bp3_d1 = transferToDimension(tailLocation, thirdLocation); Dimension bp3_d2 = getTheOtherRelativeDimension(tailLocation, headLocation, bp3_d1); BendpointModel bp3 = new BendpointModel(bp3_d1, bp3_d2); Dimension bp4_d1 = transferToDimension(tailLocation, fourthLocation); Dimension bp4_d2 = getTheOtherRelativeDimension(tailLocation, headLocation, bp4_d1); BendpointModel bp4 = new BendpointModel(bp4_d1, bp4_d2); FlowChartConnecterGraphicalProperty property = connecter .getGraphicalProperty(); ArrayList<BendpointModel> bendpointList = new ArrayList<BendpointModel>(); bendpointList.add(bp1); bendpointList.add(bp2); bendpointList.add(bp3); bendpointList.add(bp4); property.setBendPoints(bendpointList); }对该函数的解释:当向北走向的连线的连线与模型发生布局冲突时,应该从模型的东侧或西侧进行绕线,如下图所示:
所以,进行路由搜索的思路为确定线段1->2的x坐标,以及拐点0和拐点3的y坐标。具体实现详见代码,此处不再详述。另外3个算法与之类似。
2. 下面介绍对存在1个拐点的连线进行路由的算法,即图中***部分的函数的实现。
有1个拐点的连线共有8种情况:如下图(线的2端可能是悬空的 ):
这里以hitTestFor_WS_Connecter()为例介绍这8个函数的实现方法。
对于“函数调用关系图”中给出的所有函数(hitTestFor0BendpointConnecter()除外),都采用了基本上统一的处理思路进行实现。那就是,在对各种走向的连线进行路由时,首先进行默认路由方式(可能存在2种或3种默认路由方式)的冲突检测,如果默认路由方式不存在布局冲突,则函数结束;否则才进行下一步处理;而在下一步处理中,同样采用了相同的思路,即,对于只有1个拐点的的底层处理函数(在分而治之中处于较低的层次,故称之为底层处理函数),一般采用搜索的方向寻找可以布线的路由,而对于有多个拐点的连线的处理函数,一般是将其拆分为几段简单的连线,然后对各个子连线就可以调用底层的处理函数,待所有的子连线的路由都找到,即可通过计算得到整条连线的路由。以hitTestFor_WS_Connecter()函数为例来说,它的默认路由方式有2种,如下所示:
在hitTestFor_WS_Connecter()函数实现时,首先检测这2种路由方式是否存在冲突,如果不存在冲突,则函数结束;如果存在冲突,则进行下一步的搜索,首先给出示意图:
图中右下角所示的是我们进行路由搜索的目标。进行搜索时,首先找到箭尾向西无障碍的区域,图中的A点;在找到箭头向北无障碍的区域,图中的B点;然后,我们需要在灰色矩形区域内搜索到一条可以布线的X坐标,搜索时以西侧为优先,目的是为了为搜索到目标路由中的横线提供最大的可能性,假设搜索到可以布线的位置为DE;然后在***矩形区域内搜索到可以布线的横线,假设搜索到的位置为IJ,则最终的路由为箭尾->H->J->I->箭头;如果这样的尝试失败,则可以先搜索目标路由中的横线,再搜索目标路由中的竖线,思路类似。具体代码如下:
/** * 对西-南走向连线进行冲突检查和重新布局;调用该函数时要求连线上已经有了一个拐点。 * * @param 连线的2端均可以悬空或连接着模型 */ private static boolean hitTestFor_WS_Connecter( FlowChartConnecterModel connecter) { if (connecter == null) return true; FlowChartConnecterGraphicalProperty flowChartConnecterGraphicalProperty = connecter .getGraphicalProperty(); if (flowChartConnecterGraphicalProperty.getBendPoints().size() != 1) { return true; } // 首先查看连线的箭头箭尾是否位于某个矩形内部,如果是,则采用默认路由方式,不管是否发生布局冲突 ArrayList<Rectangle> rectangleList = getLayoutInfo(connecter); Point tailLocation = connecter.getTail().getLocation(); Point headLocation = connecter.getHead().getLocation(); // 首先默认方式1的路由是否存在布局冲突 boolean conflict1 = hitTestForConnecter(rectangleList, connecter); boolean isTailContained = isContainedInRectangle(rectangleList, connecter.getTail());// 箭尾是否位于某个矩形内部 boolean isHeadContained = isContainedInRectangle(rectangleList, connecter.getHead());// 箭头是否位于某个矩形内部 if (isTailContained || isHeadContained) { return conflict1; } if (!conflict1) {// 如果默认路由方式1不存在冲突,直接返回 return false; } // 检查默认方式2的路由是否存在布局冲突 Point p1 = new Point(tailLocation.x - Length.smallestMarginBetweenConnecterAndModel, 0); Point p2 = new Point(p1.x, headLocation.y - Length.smallestMarginBetweenConnecterAndModel); Point p3 = new Point(headLocation.x, p2.y); ArrayList<Point> pointList = new ArrayList<Point>(); pointList.add(tailLocation); pointList.add(p1); pointList.add(p2); pointList.add(p3); pointList.add(headLocation); boolean conflict2 = hitTestForPointList(rectangleList, pointList); if (!conflict2) {// 如果默认方式2没有冲突则采用之,并返回。 Dimension bp1_d1 = new Dimension( -Length.smallestMarginBetweenConnecterAndModel, 0); Dimension bp1_d2 = getTheOtherRelativeDimension(tailLocation, headLocation, bp1_d1); BendpointModel bp1 = new BendpointModel(bp1_d1, bp1_d2); Dimension bp2_d1 = new Dimension(bp1_d1.width, headLocation.y - tailLocation.y - Length.smallestMarginBetweenConnecterAndModel); Dimension bp2_d2 = getTheOtherRelativeDimension(tailLocation, headLocation, bp2_d1); BendpointModel bp2 = new BendpointModel(bp2_d1, bp2_d2); Dimension bp3_d1 = new Dimension(headLocation.x - tailLocation.x, bp2_d1.height); Dimension bp3_d2 = getTheOtherRelativeDimension(tailLocation, headLocation, bp3_d1); BendpointModel bp3 = new BendpointModel(bp3_d1, bp3_d2); // 给连线设置拐点 flowChartConnecterGraphicalProperty.clearAllBendPoints(); flowChartConnecterGraphicalProperty.addBendPoint(0, bp1); flowChartConnecterGraphicalProperty.addBendPoint(1, bp2); flowChartConnecterGraphicalProperty.addBendPoint(2, bp3); return false; } // 如果前面2种默认的布局的方式都不行,则继续其他探索 // 计算箭尾处能向左无障碍的延伸到什么位置 ArrayList<Rectangle> rectanglesCrossingTailLine = getRectanglesCrossingHorizontalSegment( new Segment(tailLocation, new Point(headLocation.x, tailLocation.y)), rectangleList); int endXAtTail = getBiggestX(rectanglesCrossingTailLine);// 箭尾处能向左无障碍得延伸到哪 if (endXAtTail < headLocation.x) { endXAtTail = headLocation.x; } ArrayList<Rectangle> rectanglesCrossingHeadLine = getRectanglesCrossingVerticalSegment( new Segment(new Point(headLocation.x, tailLocation.y), headLocation), rectangleList); int endYAtHead = getBiggestY(rectanglesCrossingHeadLine);// 箭头处能向上无障碍得延伸到哪 if (endYAtHead < tailLocation.y) { endYAtHead = tailLocation.y; } { boolean firstTryOK = true; int lastX = findXWithinRectangleScope(new Point(endXAtTail, tailLocation.y), new Point(tailLocation.x, headLocation.y), rectangleList, PositionConstants.WEST); if (lastX == -1) { firstTryOK = false; } if (firstTryOK) { if (lastX < headLocation.x) { lastX = headLocation.x + Length.smallestMarginBetweenConnecterAndModel; } int lastY = findYWithinRectangleScope(new Point(headLocation.x, endYAtHead), new Point(lastX, headLocation.y), rectangleList, PositionConstants.SOUTH); if (lastY == -1) { firstTryOK = false; } if (firstTryOK) { if (lastY < tailLocation.y) { lastY = tailLocation.y; } // 检查经过lastX和lastY的布线是否存在布局冲突 ArrayList<Point> list = new ArrayList<Point>(); list.add(tailLocation); list.add(new Point(lastX, tailLocation.y)); list.add(new Point(lastX, lastY)); list.add(new Point(headLocation.x, lastY)); list.add(headLocation); boolean hitted = hitTestForPointList(rectangleList, list); if (!hitted) {// 如果没发生冲突,则就采用这种方式布线,并返回 Dimension bp1_d1 = new Dimension( lastX - tailLocation.x, 0); Dimension bp1_d2 = getTheOtherRelativeDimension( tailLocation, headLocation, bp1_d1); BendpointModel bp1 = new BendpointModel(bp1_d1, bp1_d2); Dimension bp2_d1 = new Dimension(bp1_d1.width, lastY - tailLocation.y); Dimension bp2_d2 = getTheOtherRelativeDimension( tailLocation, headLocation, bp2_d1); BendpointModel bp2 = new BendpointModel(bp2_d1, bp2_d2); Dimension bp3_d1 = new Dimension(headLocation.x - tailLocation.x, bp2_d1.height); Dimension bp3_d2 = getTheOtherRelativeDimension( tailLocation, headLocation, bp3_d1); BendpointModel bp3 = new BendpointModel(bp3_d1, bp3_d2); flowChartConnecterGraphicalProperty .clearAllBendPoints(); flowChartConnecterGraphicalProperty .addBendPoint(0, bp1); flowChartConnecterGraphicalProperty .addBendPoint(1, bp2); flowChartConnecterGraphicalProperty .addBendPoint(2, bp3); return false; } } } } // 探索另外一种方式 int finalY = findYWithinRectangleScope(new Point(headLocation.x, endYAtHead), new Point(tailLocation.x, headLocation.y), rectangleList, PositionConstants.NORTH); if (finalY == -1) { return true; } if (finalY < tailLocation.y) { finalY = tailLocation.y; } int finalX = findXWithinRectangleScope(new Point(endXAtTail, tailLocation.y), new Point(tailLocation.x, finalY), rectangleList, PositionConstants.EAST); if (finalX == -1) { return true; } if (finalX < headLocation.x) { finalX = headLocation.x; } // 检查经过finalY和finalX的布线是否存在布局冲突 ArrayList<Point> list = new ArrayList<Point>(); list.add(tailLocation); list.add(new Point(finalX, tailLocation.y)); list.add(new Point(finalX, finalY)); list.add(new Point(headLocation.x, finalY)); list.add(headLocation); boolean hitted = hitTestForPointList(rectangleList, list); if (!hitted) {// 如果没发生冲突,则就采用这种方式布线,并返回 Dimension bp1_d1 = new Dimension(finalX - tailLocation.x, 0); Dimension bp1_d2 = getTheOtherRelativeDimension(tailLocation, headLocation, bp1_d1); BendpointModel bp1 = new BendpointModel(bp1_d1, bp1_d2); Dimension bp2_d1 = new Dimension(bp1_d1.width, finalY - tailLocation.y); Dimension bp2_d2 = getTheOtherRelativeDimension(tailLocation, headLocation, bp2_d1); BendpointModel bp2 = new BendpointModel(bp2_d1, bp2_d2); Dimension bp3_d1 = new Dimension(headLocation.x - tailLocation.x, bp2_d1.height); Dimension bp3_d2 = getTheOtherRelativeDimension(tailLocation, headLocation, bp3_d1); BendpointModel bp3 = new BendpointModel(bp3_d1, bp3_d2); flowChartConnecterGraphicalProperty.clearAllBendPoints(); flowChartConnecterGraphicalProperty.addBendPoint(0, bp1); flowChartConnecterGraphicalProperty.addBendPoint(1, bp2); flowChartConnecterGraphicalProperty.addBendPoint(2, bp3); return false; } return true; }
对只有1个拐点的连线进行处理的另外7个函数的实现与之类似,此处不再赘述。
3. 下面介绍对存在2个拐点的连线的处理函数,即“函数调用关系图”中红色的函数。
函数处理的整体思路在上文已有介绍,这里对如何对连线进行分而治之进行解释和说明。以SES走向的连线为例进行说明。首先给出示意图
在上图中,首先求出箭尾处向南的无障碍区域,图中A点;然后求出箭头处向北的无障碍区域,图中B点;然后将箭尾到A点,箭头到B点的线段以搜索步长(可以设置为10个像素或12个像素等)为单位进行分割,采用2个嵌套的for循环对2个线段的搜索步长分割点进行遍历,假设某一时刻,箭尾端取分割点C,箭头端取fengedianB,则整个连线被分割为3段,我们需要对中间的一段即CB调用子函数来处理,可以假想CB的原始路由为途中粉红色的折线,则它就应该调用ES走向的连线的处理函数来处理。最终将3段连线的拐点进行计算即可求得整条连线的路由。对连线进行分割和调用哪个子函数进行处理的选择可能是多样的,“函数调用关系图”中列出的只是作者做出的选择。下面是具体代码:
/** * 为南东南或南西南走向的连线设置路由。 (箭头箭尾处得模型不在冲突检测的范围之内。) * * @param connecter * 要求连线上已经有了2个拐点,且连线的箭头箭尾悬空和连接着模型均可以。 * @return 重新路由后连线是否还存在布局冲突;存在布局冲突时返回true;不存在布局冲突时返回false。 */ private static boolean hitTestFor_SES_SWS_Connecter( FlowChartConnecterModel connecter) { if (connecter == null) return true; FlowChartConnecterGraphicalProperty property = connecter .getGraphicalProperty(); if (property.getBendPoints().size() != 2) { return true; } // 首先查看连线的箭头箭尾是否位于某个矩形内部,如果是,则采用默认路由方式,不管是否发生布局冲突 ArrayList<Rectangle> rectangleList = getLayoutInfo(connecter); Point tailLocation = connecter.getTail().getLocation(); Point headLocation = connecter.getHead().getLocation(); // 首先默认方式1的路由是否存在布局冲突 boolean conflict1 = hitTestForConnecter(rectangleList, connecter); boolean isTailContained = isContainedInRectangle(rectangleList, connecter.getTail());// 箭尾是否位于某个矩形内部 boolean isHeadContained = isContainedInRectangle(rectangleList, connecter.getHead());// 箭头是否位于某个矩形内部 if (isTailContained || isHeadContained) { return conflict1; } if (!conflict1) {// 如果默认路由方式1不存在冲突,直接返回 return false; } // 如果默认路由方式1发生了冲突,则对默认路由方式1的拐点位置进行微调,并检查微调后是否依旧存在冲突 // 计算箭尾处能无障碍的向下延伸到什么位置 int endYAtTail = getYWithoutConflictSpreadToSouth(rectangleList, tailLocation, new Point(tailLocation.x, headLocation.y)); if (endYAtTail > headLocation.y) { endYAtTail = headLocation.y; } if (endYAtTail < tailLocation.y + Length.smallestMarginBetweenConnecterAndModel) { int defaultY1 = (tailLocation.y + endYAtTail) / 2; property.clearAllBendPoints(); add2BendpointToFuture_NEN_NWN_SES_SWS_Connecter(defaultY1, tailLocation, headLocation, property); conflict1 = hitTestForConnecter(rectangleList, connecter); if (!conflict1) { return false; } } // 检查defaultY2的情况是否发生了布局冲突 // 首先计算箭头处能无障碍的向上延伸到什么位置 int endYAtHead = getYWithoutConflictSpreadToNorth(rectangleList, headLocation, new Point(headLocation.x, 0)); if (endYAtHead < tailLocation.y) { endYAtHead = tailLocation.y; } int defaultY2 = headLocation.y - Length.smallestMarginBetweenConnecterAndModel; if (headLocation.y - endYAtHead < 2 * Length.smallestMarginBetweenConnecterAndModel) { defaultY2 = (headLocation.y + endYAtHead) / 2; } Point p1 = new Point(tailLocation.x, defaultY2); Point p2 = new Point(headLocation.x, defaultY2); ArrayList<Point> pointList = new ArrayList<Point>(); pointList.add(tailLocation); pointList.add(p1); pointList.add(p2); pointList.add(headLocation); boolean defaultY2Conflict = hitTestForPointList(rectangleList, pointList); // 如果defaultY2没有冲突则采用默认路由方式 if (!defaultY2Conflict) { property.clearAllBendPoints(); add2Bendpoints(tailLocation, headLocation, property, defaultY2); return false; } // 尝试在中间布置条横线 // 箭头处向上延伸,箭尾处向下延伸,看看延伸部分在水平方向上是否存在重叠 if (endYAtHead < endYAtTail) {// 有重叠部分 // Rectangle crossingRect = new Rectangle() int chosenY = -1; if (headLocation.x < tailLocation.x) { chosenY = findYWithinRectangleScope(new Point(headLocation.x, endYAtHead), new Point(tailLocation.x, endYAtTail), rectangleList, PositionConstants.SOUTH); } else if (headLocation.x > tailLocation.x) { chosenY = findYWithinRectangleScope(new Point(tailLocation.x, endYAtHead), new Point(headLocation.x, endYAtTail), rectangleList, PositionConstants.SOUTH); } if (chosenY != -1) { property.clearAllBendPoints(); add2Bendpoints(tailLocation, headLocation, property, chosenY); return false; } } // 将连线分解为2部分,第一部分为向南的线段,第二部分为东南走向的连线,采用虚拟连线测试法,对其路由进行探索。 if (endYAtTail > headLocation.y) { endYAtTail = headLocation.y; } // 在可以无障碍延伸的区间里,对连线进行分解,然后分别进行路由。 ArrayList<Point> stepPoints = getStepPoints(tailLocation, new Point( tailLocation.x, endYAtTail)); for (Point point : stepPoints) { // 创建虚拟连线 Point virtualTailLocation = new Point(point); Point virtualHeadLocation = new Point(headLocation); FlowChartConnecterModel virtualConnecter = createVirtualConnecter( virtualTailLocation, virtualHeadLocation, null, connecter .getHeadModel(), connecter.getTail().getParent()); // 给虚拟连线加上应有的拐点 Dimension d1 = new Dimension(virtualHeadLocation.x - virtualTailLocation.x, 0); Dimension d2 = getTheOtherRelativeDimension(virtualTailLocation, virtualHeadLocation, d1); BendpointModel bp = new BendpointModel(d1, d2); virtualConnecter.getGraphicalProperty().addBendPoint(0, bp); // 对虚拟连线进行路由尝试 int headDirection = getDirection(tailLocation, headLocation); boolean virtualConnecterConflict = true;// 虚拟连线的布局是否发生了冲突 if (headDirection == PositionConstants.SOUTH_EAST) { virtualConnecterConflict = hitTestFor_ES_Connecter(virtualConnecter); } else if (headDirection == PositionConstants.SOUTH_WEST) { virtualConnecterConflict = hitTestFor_WS_Connecter(virtualConnecter); } // 如果 路由调整成功 if (!virtualConnecterConflict) { ArrayList<BendpointModel> bendpointList = virtualConnecter .getGraphicalProperty().getBendPoints(); // 清除旧有拐点 property.clearAllBendPoints(); // 首先加入第一个拐点 Dimension firstD1 = new Dimension(0, point.y - tailLocation.y); Dimension firstD2 = getTheOtherRelativeDimension(tailLocation, headLocation, firstD1); BendpointModel firstBendpoint = new BendpointModel(firstD1, firstD2); property.addBendPoint(0, firstBendpoint); // 加入虚拟连线上的拐点,注意需要对拐点们的偏移量进行调整 // x偏移量不变,y偏移量需要加上ajustment int ajustment = point.y - tailLocation.y; for (int i = 0; i < bendpointList.size(); i++) { BendpointModel virtualBendpoint = bendpointList.get(i); Dimension virtualD1 = virtualBendpoint .getFirstRelativeDimension(); Dimension newD1 = new Dimension(virtualD1.width, virtualD1.height + ajustment); Dimension newD2 = getTheOtherRelativeDimension( tailLocation, headLocation, newD1); BendpointModel newBendpoint = new BendpointModel(newD1, newD2); property.addBendPoint(i + 1, newBendpoint); } return false; } } return true; }4. 拐点个数更多的连线的处理函数的实现与以上相似,此处不再冲突,本文最后会给出所有函数的实际代码。
上面介绍的是连线路由算法的实现,但仅有这些是不够的,下面列出的问题同样需要进行处理。
当移动连线锚点时,需要进行怎样的处理?
当移动连线锚点时,由于连线上的拐点记录的相对于箭头和箭尾的坐标偏移,所以连线上所有的拐点需要进行偏移坐标的调整;移动箭尾锚点时,需要调整地一个拐点的坐标,以使移动后的连线依旧是一条直角连线;同理,移动箭头锚点时,需要调整最后一个拐点的坐标,以使连线依旧是一条直角连线。以上所述处理完成后,调用布局冲突检测算法进行一下检验,如果发生了布局冲突,则调用路由算法进行路由处理。
移动 连线的拐点时,需要进行怎样的处理?
移动连线的拐点时,同样需要保证移动后连线是一条直角连线。这就要求移动某个连线拐点时,需要同时移动它的前一个拐点和后一个拐点,以满足这一要求;更进一步,如果当前移动的拐点时连线上的第一个或最后一个拐点,则需要在它前面或后面添加一个新的拐点,以保证连线是直角连线;完成上述操作后,调用布局冲突检测算法进行一下检验,如果发生了布局冲突,则调用路由算法进行路由处理。
创建连线拐点时,需要进行怎样的处理?
创建连线拐点时,需要考虑新创建连线拐点的前一个和后一个拐点,根据新创建的连线拐点的坐标,调整前一个和后一个拐点的坐标,以使连线是一条直角连线;如果新创建的连线拐点是连线上的第一个或最后一个拐点,则可能需要再添加一些拐点才能满足直角连线的要求。完成上述操作后,调用布局冲突检测算法进行一下检验,如果发生了布局冲突,则调用路由算法进行路由处理。
删除连线拐点,需要进行怎样的处理?
不需要多余的处理,只要调用布局冲突检测算法进行一下检验,如果发生了布局冲突,则调用路由算法进行路由处理。
点此下载所有相关代码。
结束。
相关文章推荐
- 连线自动路由算法:在GEF中实现连线的自动直角路由,智能避障并绕开模型,选择最佳路径进行布线,仿Visio效果
- 元旦快乐,阖家团圆,幸福安康.C#重载示例(有问重载该如何选择?在C#中可很方便地在智能感知弹出中选择不同参数列表进行使用;不像C/C++那样,要记住编译器自动选择最佳匹配参数列表的概念)
- 转:zTree树控件扩展篇:巧用zTree控件实现文本框输入关键词自动模糊查找zTree树节点实现模糊匹配下拉选择效果
- 一个简单的敌人自动寻找玩家进行攻击及受到伤害死亡效果实现
- 让程序更智能-自动选择功能的实现
- Swift仿选择电影票的效果并实现无限/自动轮播的方法
- 第六课 自己实现路由改进,针对不同请求的路径进行响应
- 重分布中多路由协议中选择最佳路径
- ViewFlipper实现带索引效果的自动播放也可手动滑动的广告栏
- JS实现智能识别金钱数字输入(不是金钱数字则自动清空)
- [Material Design] MaterialButton 效果进阶 动画自动移动进行对齐效果
- Android 控件自动“移入、暂停、移出”效果的实现
- Android:实现一种浮动选择菜单的效果
- Vue2路由动画效果实现
- jquery插件ui中tabs实现选择面板效果
- 三层交换机实现和单臂路由同样的效果
- Android 文字自动滚动(跑马灯)效果的两种实现方法
- javascript suggest效果 自动完成实现代码分享
- Android 仿京东商城底部布局的选择效果(Selector 选择器的实现)
- 源生控件实现自动补全效果