编写布局插件
要创建一个名为xxx的新布局插件,首先需要提供两个函数:xxx_layout和xxx_cleanup。这些函数的语义如下所述。
布局
void xxx_layout(Agraph_t * g)
初始化图形。
-
如果算法将使用通用的边路由代码,它应该调用
setEdgeType (g, ...);。 -
对于每个节点,调用
common_init_node和gv_nodesize。 -
如果算法将使用
spline_edges()来路由边,那么节点坐标需要存储在ND_pos中,因此应该在此处分配。这一点以及上述提到的两个调用,都可以通过调用neato_init_node()来处理。 -
对于每条边,调用
common_init_edge。 -
算法应该分配它所需的任何其他数据结构。这可以涉及
A*info_t字段中的字段。此外,每个这些字段都包含一个void* alg;子字段,算法可以使用该子字段存储额外数据。一旦我们迁移到cgraph,所有这些都将被替换为算法特定的记录。 -
对图形进行布局。完成后,每个节点的坐标应存储在
ND_coord_i(n)的点中,每条边的布局应在ED_spl(e)中描述。 (注意:从2.21版本开始,ND_coord_i已被ND_coord取代,后者现在是浮点坐标。)
要添加边,有以下3个可用函数:
spline_edges1 (Agraph_t*, int edgeType)假设节点坐标存储在ND_coord_i中,并且GD_bb已设置。对于每条边,该函数会构造相应的 数据并将其存储在ED_spl中。spline_edges0 (Agraph_t*)假设节点坐标存储在ND_pos中,并且GD_bb已设置。如果设置了ratio属性,此函数会使用该属性, 将ND_pos中的值复制到ND_coord_i(从英寸转换为点); 然后根据setEdgeType()指定的边类型调用spline_edges1。spline_edges (Agraph_t*)假设节点坐标存储在ND_pos中。该函数计算g的边界框并将其存储在GD_bb中,然后调用spline_edges0()。
如果算法仅适用于连通分量,代码可以利用pack库获取各分量,单独布局,并根据用户规格将它们打包在一起。下面给出一个典型方案。如需更详细的示例,可参考twopi、circo、neato或fdp的代码实现。
int ncc;
Agraph_t **ccs = ccomps(g, &ncc, 0);
if (ncc == 1) {
/* layout nodes of g */
adjustNodes(g); /* if you need to remove overlaps */
spline_edges(g); /* generic edge routing code */
} else {
pack_info pinfo;
pack_mode pmode = getPackMode(g, l_node);
for (int i = 0; i < ncc; i++) {
Agraph_t *const sg = ccs[i];
/* layout sg */
adjustNodes(sg); /* if you need to remove overlaps */
}
spline_edges(g); /* generic edge routing */
/* initialize packing info, e.g. */
pinfo.margin = getPack(g, CL_OFFSET, CL_OFFSET);
pinfo.doSplines = 1;
pinfo.mode = pmode;
pinfo.fixed = 0;
packSubgraphs(ncc, ccs, g, &pinfo);
}
for (int i = 0; i < ncc; i++) {
agdelete(g, ccs[i]);
}
free(ccs);
如果您依赖仅在根图中设置的属性,在布局子图时需要小心。对于连通分量,可以在打包之前(如上所述)或在分量打包之后(参见circo)为每个分量添加边。
最好检查一下图的简单情况,比如有0个或1个节点,或者没有边的情况。
在 xxx_layout 结束时,调用
dotneato_postprocess(g);
以下模板在大多数情况下都适用,忽略处理断开连接的图和移除节点重叠的问题:
static void
xxx_init_node(node_t * n)
{
neato_init_node(n);
/* add algorithm-specific data, if desired */
}
static void
xxx_init_edge(edge_t * e)
{
common_init_edge(e);
/* add algorithm-specific data, if desired */
}
static void
xxx_init_node_edge(graph_t * g)
{
for (node_t *n = agfstnode(g); n; n = agnxtnode(g, n)) {
xxx_init_node(n);
}
for (node_t *n = agfstnode(g); n; n = agnxtnode(g, n)) {
for (edge_t *e = agfstout(g, n); e; e = agnxtout(g, e)){
xxx_init_edge(e);
}
}
}
void
xxx_layout (Agraph_t* g)
{
xxx_init_node_edge(g);
/* Set ND_pos(n) for each node n */
spline_edges(g);
dotneato_postprocess(g);
}
清理
void xxx_cleanup(Agraph_t * g)
释放布局中分配的所有资源。
最后对每个节点和边调用gv_cleanup_node和gv_cleanup_edge。这会清理样条线标签、ND_pos、形状并将A*info_t置零,因此这些操作必须最后执行,但也可以根据需要作为显式xxx_cleanup_node和xxx_cleanup_edge的一部分。
最后,您应该执行:
if (g != g->root) memset(&g->u, 0, sizeof(Agraphinfo_t));
这对于重新布局图形是必要的,因为布局代码假定此结构是干净的。
libgvc 会对根图执行最终清理操作,释放所有绘图资源,释放其标签,并将根图的 Agraphinfo_t 置零。
以下模板在大多数情况下都适用:
static void xxx_cleanup_graph(Agraph_t * g)
{
/* Free any algorithm-specific data attached to the graph */
if (g != g->root) memset(&g->u, 0, sizeof(Agraphinfo_t));
}
static void xxx_cleanup_edge (Agedge_t* e)
{
/* Free any algorithm-specific data attached to the edge */
gv_cleanup_edge(e);
}
static void xxx_cleanup_node (Agnode_t* n)
{
/* Free any algorithm-specific data attached to the node */
gv_cleanup_node(e);
}
void xxx_cleanup(Agraph_t * g)
{
for (Agnode_t *n = agfstnode(g); n; n = agnxtnode(g, n)) {
for (Agedge_t *e = agfstout(g, n); e; e = agnxtout(g, e)) {
xxx_cleanup_edge(e);
}
xxx_cleanup_node(n);
}
xxx_cleanup_graph(g);
}
大多数布局使用类似于neato的辅助例程,因此可以在plugin/neato_layout中添加入口点。
添加到 gvlayout_neato_layout.c:
gvlayout_engine_t xxxgen_engine = {
xxx_layout,
xxx_cleanup,
};
以及该行
{LAYOUT_XXX, "xxx", 0, &xxxgen_engine, &neatogen_features},
将gvlayout_neato_types和新的枚举LAYOUT_XXX添加到该文件中的layout_type。
上述方法允许新布局直接利用neato插件的基础,但需要重新构建该插件。通常来说,用户可以(而且很可能应该)完全独立地构建一个布局插件。
要实现这一点,在编写完xxx_layout和xxx_cleanup后,需要:
-
添加类型和数据结构:
typedef enum { LAYOUT_XXX } layout_type; static gvlayout_features_t xxxgen_features = { 0 }; gvlayout_engine_t xxxgen_engine = { xxx_layout, xxx_cleanup, }; static gvplugin_installed_t gvlayout_xxx_types[] = { {LAYOUT_XXX, "xxx", 0, &xxxgen_engine, &xxxgen_features}, {0} }; static gvplugin_api_t apis[] = { {API_layout, &gvlayout_xxx_types}, {0}, }; gvplugin_library_t gvplugin_xxx_layout_LTX_library = { "xxx_layout", apis }; -
将所有内容整合到一个动态库中,该库名称包含字符串
gvplugin_,并将该库安装在与其它Graphviz插件相同的目录下。例如,在Linux系统上,dot布局插件位于库文件libgvplugin_dot_layout.so中。 -
运行
dot -c以重新生成配置文件。
注意事项:
- 额外的布局可以作为额外的行添加到
gvlayout_xxx_types中。 - 显然,大多数名称和字符串可以是任意的。一个约束条件是
gvplugin_library_t类型的外部标识符必须以_LTX_library结尾。此外,gvlayout_xxx_types每个条目中的字符串xxx是用来标识布局算法的名称,因此需要与其他布局名称区分开来。 - 布局算法的特性目前仅限于一组位标志,唯一支持的标志是
LAYOUT_USES_RANKDIR,该标志使布局能够响应rankdir属性。
需要对所有静态了解布局算法的应用程序进行更改。
Automake 配置
如果您想将代码集成到Graphviz软件并使用其构建系统,请按照以下说明操作。当然,您也可以使用自己的构建软件来构建和安装插件。
- 将您的软件放在
lib/xxxgen中,并将上述描述的钩子添加到gvlayout_neato_layout.c中 - 在
lib/xxxgen目录中,提供一个Makefile.am文件(基于简单示例如lib/fdpgen/Makefile.am) - 在
lib/Makefile.am文件中,将xxxgen添加到SUBDIRS中 - 在
configure.ac中,将lib/xxxgen/Makefile添加到AC_CONFIG_FILES。 - 在
lib/plugin/neato_layout/Makefile.am文件中,将$(top_builddir)/lib/xxxgen/libxxxgen_C.la插入到libgvplugin_neato_layout_C_la_LIBADD中。 - 记得运行
autogen.sh,因为单独运行configure可能会猜错配置。
这也假设您的系统上安装了各种automake工具的良好版本。