目录
Preperation
接上章讲,该算法实现过程中只有两次递归,并且名称就叫FIRSTWALK和SECONDWALK,但是,像我一样的凡人还需要一些准备工作。
该算法计算节点时使用了两种定位方式:根据左兄弟定位(左兄弟位置加上节点宽度和间距)和根据子节点定位(计算所有子节点的中心位置),进行定位运算前,我们需要先得到“左兄弟”、“子节点”等一系列关系表示字段,根据page7中的函数介绍,定义好单个节点的结构体:
struct Node
{
//节点标识
QString name;
//根据page7定义的,逻辑运算相关字段
QString parent;
QString firstChild;
QString leftSibling;
QString rightSibling;
QString leftNeighbor;
float x;
float y;
float prelim;
float modifier;
//译者用于计算左邻居的辅助参数
int column;
int row;
};
其中x,y,prelim,modifier是我们需要计算的属性,而parent等一系列字段需要我们从原有的树形逻辑中获取,这里译者使用的Qt写的一个递归函数,从根节点开始转换为结构体列表,代码如下:
//保存节点信息的map
QMap<QString, Node> nodeList;
//节点信息转换函数
void convertNode(int level, QDomNode node)
{
if(node.isNull())
return;
Node temp;
temp.name = node.toElement().tagName();
temp.parent = node.parentNode().toElement().tagName();
temp.firstChild = node.firstChild().toElement().tagName();
temp.rightSibling = node.nextSibling().toElement().tagName();
temp.leftSibling = node.previousSibling().toElement().tagName();
temp.x = 0;
temp.y = 0;
temp.prelim = 0;
temp.modifier = 0;
temp.row = level;
//检查是第几行,遍历得到当前行排序
int column = 0;
for(int n = 0; n < nodeList.count(); n++)
{
if(nodeList.values().at(n).row == level)
column++;
}
temp.column = column;
//根据行列取得左节点
temp.leftNeighbor = getLeftNeighbor(temp.row, temp.column);
//qmap不能修改内容,直接替换节点
nodeList.insert(temp.name, temp);
//递归查询
QDomNode child = node.firstChild();
while (!child.isNull()) {
convertNode(level + 1, child);
child = child.nextSibling();
}
}
FirstWalk
由于原文代码是这样子的,译者使用更现代的语言来解释FirstWalk中发生了什么,有异常可以直接留言。
在FirstWalk中作者需要计算每个节点的初始x坐标和修正字段,即结构体中的两个属性prelim和modifier,计算逻辑如下:
1.当前节点是叶节点无左兄弟,prelim记0
2.当前节点是叶节点有左兄弟,prelim是左兄弟prelim加上宽度,即根据左兄弟定位;
3.当前节点非叶节点无左兄弟,prelim是第一个子节点prelim加上最后一个子节点除以二,即根据子节点定位
4.当前节点非叶节点有左兄弟,prelim记左兄弟相对位置,modifier记prelim相对子节点定位修正值,使用apportion修正子孙节点定位
prelim是初始的x坐标值,modifier是一个辅助标记位,用于修正根据左兄弟定位和根据子节点定位两种不同定位方式产生的偏移,具体实现将留到下章。
该逻辑中判读叶节点、左邻居、左兄弟等关系是依靠convertNode中解析出来的关系字段,具体实现代码如下:
QDomElement ele = doc.documentElement();
firstWalk(0, ele.tagName());
void firstWalk(int level, QString name)
{
Node curNode = nodeList.value(name);
//当前节点为叶节点
if(curNode.firstChild.isEmpty())
{
//有左兄弟prelim计算相对位置
if(!curNode.leftSibling.isEmpty())
{
Node leftSibling = nodeList.value(curNode.leftSibling);
curNode.prelim = leftSibling.prelim + space + nodeSize;
}else//没有左兄弟prelim记0
curNode.prelim = 0;
//多次刷新列表中节点信息,防止函数跳转后节点信息更新延迟
nodeList.insert(curNode.name, curNode);
}
else //非叶节点计算:
{
//先计算所有子节点位置
int leftPrelim = 0;
int rightPrelim = 0;
Node child = nodeList.value(curNode.firstChild);
firstWalk(level + 1, child.name);
child = nodeList.value(child.name);
leftPrelim = child.prelim;
while (!child.rightSibling.isEmpty()) {
child = nodeList.value(child.rightSibling);
firstWalk(level + 1, child.name);
}
child = nodeList.value(child.name);
rightPrelim = child.prelim;
int midPoint = (leftPrelim + rightPrelim) / 2; //这是子节点集合的中心位置
//如果当前节点有左兄弟,prelim计算相对位置, 计算modifier
if(!curNode.leftSibling.isEmpty())
{
Node leftSibling = nodeList.value(curNode.leftSibling);
curNode.prelim = leftSibling.prelim + space + nodeSize;
curNode.modifier = curNode.prelim - midPoint;
//替换更改后信息
nodeList.insert(curNode.name, curNode);
//计算子树移动距离,当前深度节点分摊偏移
apportion(curNode.name);
}
else//没有左兄弟,prelim记子节点集合中间位置
{
curNode.prelim = midPoint;
nodeList.insert(curNode.name, curNode);
}
}
}
函数中所有节点宽度使用nodeSize来表示,视为所有节点宽度相等,若不等则取当前节点和左兄弟的平均宽度。
SecondWalk
相对而言secondwalk中逻辑简单得多,只需要初始x值加上所有祖宗节点的modifier修正值,即可算得最终x坐标,y坐标可以根据深度直接算出。具体代码如下:
void secondWalk(int level, int modSom, QString name)
{
if(name.isEmpty())
return;
//根据prelim和偏移计算x,根据深度计算y
Node curNode = nodeList.value(name);
curNode.x = curNode.prelim + modSom;
curNode.y = level * space;
nodeList.insert(curNode.name, curNode);
//遍历子节点, 子节偏移累加父节点prelim
Node child = nodeList.value(curNode.firstChild);
secondWalk(level + 1, modSom + curNode.modifier, child.name);
while (!child.rightSibling.isEmpty()) {
child = nodeList.value(child.rightSibling);
secondWalk(level + 1, modSom + curNode.modifier, child.name);
}
}