1 // Copyright (C) 2020 by Yuri Victorovich. All rights reserved.
2 
3 #include "graphviz-cgraph.h"
4 #include "misc.h"
5 #include "util.h"
6 
7 #include <gvc.h>
8 #include <gvplugin.h>
9 #include <stdio.h>
10 #include <assert.h>
11 
12 #include <array>
13 #include <chrono>
14 #include <vector>
15 #include <string>
16 
17 #define S(str) ((char*)str)
18 #define GRAPH            ((Agraph_t*)this->_g)
19 #define NODE(opaqueNode) ((Agnode_t*)opaqueNode)
20 #define EDGE(opaqueEdge) ((Agedge_t*)opaqueEdge)
21 #define SYM(opaqueSym)   ((Agsym_t*)opaqueSym)
22 
23 extern gvplugin_library_t gvplugin_dot_layout_LTX_library;
24 
25 lt_symlist_t lt_preloaded_symbols[] = {
26 	{"gvplugin_dot_layout_LTX_library", (void*)(&gvplugin_dot_layout_LTX_library)},
27 	{0, 0}
28 };
29 
30 /// static initializer
31 
32 static GVC_t *gvc = gvContextPlugins(lt_preloaded_symbols, 1/*demand_loading*/); // XXX CAVEAT no way to destroy this object during static destructors
33 
34 /// static helpers
35 
parseTwoFloats(const char * str)36 static std::array<float,2> parseTwoFloats(const char *str) {
37 	assert(str);
38 	float p[2];
39 	auto n = ::sscanf(str, "%f,%f", &p[0], &p[1]);
40 	assert(n == 2);
41 	UNUSED(n)
42 
43 	return {p[0],p[1]};
44 }
45 
46 
47 /// Graphviz_CGraph
48 
Graphviz_CGraph(const char * graphName,float userDPI_)49 Graphviz_CGraph::Graphviz_CGraph(const char *graphName, float userDPI_)
50 : symNodeShape(nullptr)
51 , symNodeWidth(nullptr)
52 , symNodeHeight(nullptr)
53 , symEdgeLabel(nullptr)
54 , userDPI(userDPI_)
55 {
56 	_g = agopen(S(graphName), Agdirected, NULL);
57 
58 	// set the dpi value of 72 because that's what GraphViz assumes anyway
59 	agattr(GRAPH, AGRAPH, S("dpi"), S(CSTR(assumedDPI())));
60 }
61 
~Graphviz_CGraph()62 Graphviz_CGraph::~Graphviz_CGraph() {
63 	gvFreeLayout(gvc, GRAPH);
64 	agclose(GRAPH); // the memory is lost by GraphViz, see https://gitlab.com/graphviz/graphviz/issues/1651
65 }
66 
67 /// interface implementation
68 
addNode(const char * name)69 Graphviz_CGraph::Node Graphviz_CGraph::addNode(const char *name) {
70 	return (Node)agnode(GRAPH, S(name), TRUE/*create*/);
71 }
72 
addEdge(Node node1,Node node2,const char * name)73 Graphviz_CGraph::Edge Graphviz_CGraph::addEdge(Node node1, Node node2, const char *name) {
74 	return (Edge)agedge(GRAPH, NODE(node1), NODE(node2), S(name), TRUE/*create*/);
75 }
76 
setDefaultNodeShape(const char * shapeValue)77 void Graphviz_CGraph::setDefaultNodeShape(const char *shapeValue) {
78 	assert(!symNodeShape);
79 	symNodeShape = agattr(GRAPH, AGNODE, S("shape"), S(shapeValue));
80 }
81 
setDefaultNodeSize(float width,float height)82 void Graphviz_CGraph::setDefaultNodeSize(float width, float height) {
83 	assert(!symNodeWidth && !symNodeHeight);
84 	symNodeWidth = agattr(GRAPH, AGNODE, S("width"), S(CSTR(userInchesToInches(width))));
85 	symNodeHeight = agattr(GRAPH, AGNODE, S("height"), S(CSTR(userInchesToInches(height))));
86 }
87 
setGraphOrdering(bool orderingIn,bool orderingOut)88 void Graphviz_CGraph::setGraphOrdering(bool orderingIn, bool orderingOut) {
89 	if (orderingIn || orderingOut) {
90 		assert(!(orderingIn && orderingOut)); // graphviz doesn't support this yet: see https://gitlab.com/graphviz/graphviz/issues/1645
91 		agattr(GRAPH, AGRAPH, S("ordering"), orderingIn ? S("in") : S("out"));
92 	}
93 }
94 
setGraphPad(float padX,float padY)95 void Graphviz_CGraph::setGraphPad(float padX, float padY) {
96 	agattr(GRAPH, AGRAPH, S("pad"), S(CSTR(padX << "," << padY)));
97 }
98 
setGraphMargin(float marginX,float marginY)99 void Graphviz_CGraph::setGraphMargin(float marginX, float marginY) {
100 	agattr(GRAPH, AGRAPH, S("margin"), S(CSTR(marginX << "," << marginY)));
101 }
102 
setNodeShape(Node node,const char * shapeValue)103 void Graphviz_CGraph::setNodeShape(Node node, const char *shapeValue) {
104 	assert(symNodeShape); // need to call setDefaultNodeShape() first
105 	agxset(NODE(node), SYM(symNodeShape), S("box"));
106 }
107 
setNodeSize(Node node,float width,float height)108 void Graphviz_CGraph::setNodeSize(Node node, float width, float height) {
109 	assert(symNodeWidth && symNodeHeight); // need to call setDefaultNodeSize() first
110 	agxset(NODE(node), SYM(symNodeWidth), S(CSTR(userInchesToInches(width))));
111 	agxset(NODE(node), SYM(symNodeHeight), S(CSTR(userInchesToInches(height))));
112 }
113 
setEdgeLabel(Edge edge,const char * label)114 void Graphviz_CGraph::setEdgeLabel(Edge edge, const char *label) {
115 	if (!symEdgeLabel)
116 		symEdgeLabel = agattr(GRAPH, AGEDGE, S("label"), S(""));
117 	agxset(EDGE(edge), SYM(symEdgeLabel), S(label));
118 }
119 
render()120 void Graphviz_CGraph::render() {
121 	// time begin
122 	auto tmStart = std::chrono::high_resolution_clock::now();
123 
124 	int err = gvLayout(gvc, GRAPH, S("dot"));
125 	if (err)
126 		FAIL("GraphViz failed to render the graph")
127 
128 	attach_attrs(GRAPH);
129 
130 	{ // time end
131 		auto tmStop = std::chrono::high_resolution_clock::now();
132 		PRINT("graph was rendered in " << std::chrono::duration_cast<std::chrono::milliseconds>(tmStop - tmStart).count() << " milliseconds")
133 	}
134 }
135 
136 // getting information
137 
getBBox() const138 std::array<std::array<float, 2>, 2> Graphviz_CGraph::getBBox() const {
139 	auto str = agget(GRAPH, S("bb"));
140 	assert(str);
141 
142 	float p[4];
143 	auto n = ::sscanf(str, "%f,%f,%f,%f", &p[0], &p[1], &p[2], &p[3]);
144 	assert(n == 4);
145 	UNUSED(n)
146 
147 	return {{
148 		{{pixelsToUserInches(p[0]), pixelsToUserInches(p[1])}},
149 		{{pixelsToUserInches(p[2]), pixelsToUserInches(p[3])}}
150 	}};
151 }
152 
getNodePos(Graphviz_CGraph::Node node) const153 std::array<float,2> Graphviz_CGraph::getNodePos(Graphviz_CGraph::Node node) const {
154 	return pixelsToUserInches(parseTwoFloats(agget(NODE(node), S("pos"))));
155 }
156 
getNodeSize(Graphviz_CGraph::Node node) const157 std::array<float,2> Graphviz_CGraph::getNodeSize(Graphviz_CGraph::Node node) const {
158 	auto strW = agget(NODE(node), S("width"));
159 	auto strH = agget(NODE(node), S("height"));
160 	return {inchesToUserInches(std::stof(strW)), inchesToUserInches(std::stof(strH))};
161 }
162 
getEdgeSplines(Edge edge) const163 std::vector<std::array<float,2>> Graphviz_CGraph::getEdgeSplines(Edge edge) const {
164 	auto splines = agget(EDGE(edge), S("pos"));
165 	assert(splines);
166 
167 	std::vector<std::string> segments;
168 	Util::splitString(splines, segments, ' ');
169 
170 	std::vector<std::array<float,2>> pts;
171 	pts.push_back({-1,-1}); // startp
172 	pts.push_back({-1,-1}); // endp
173 
174 	for (auto &s : segments)
175 		switch (s[0]) {
176 		case 's': // startp
177 			assert(s.size()>=5 && s[1]==',');
178 			pts[0] = pixelsToUserInches(parseTwoFloats(s.c_str()+2));
179 			break;
180 		case 'e': // endp
181 			assert(s.size()>=5 && s[1]==',');
182 			pts[1] = pixelsToUserInches(parseTwoFloats(s.c_str()+2));
183 			break;
184 		default: // a spline point
185 			pts.push_back(pixelsToUserInches(parseTwoFloats(s.c_str())));
186 		}
187 	assert((pts.size()-2)%3==1); // n = 1 (mod 3) for splines
188 
189 	return pts; // {startp,endp, 1+2*n points defining the spline}, startp,endp are optional, {-1,-1} means it isn't set
190 }
191 
getEdgeLabelPosition(Edge edge) const192 std::array<float,2> Graphviz_CGraph::getEdgeLabelPosition(Edge edge) const { // CAVEAT there's no way to specify boxes for edge labels, we just have to use positions
193 	return pixelsToUserInches(parseTwoFloats(agget(EDGE(edge), S("lp"))));
194 }
195 
196 // DEBUG
197 
writeDotToStdio() const198 void Graphviz_CGraph::writeDotToStdio() const {
199 	agwrite(GRAPH, stdout);
200 }
201 
202 /// internals
203 
assumedDPI()204 float Graphviz_CGraph::assumedDPI() {
205 	return 72; // GraphViz always uses 72dpi internally, see https://gitlab.com/graphviz/graphviz/issues/1649
206 }
207 
userInchesToInches(float inches) const208 float Graphviz_CGraph::userInchesToInches(float inches) const {
209 	return inches*userDPI/assumedDPI();
210 }
211 
inchesToUserInches(float inches) const212 float Graphviz_CGraph::inchesToUserInches(float inches) const {
213 	return inches*assumedDPI()/userDPI;
214 }
215 
pixelsToUserInches(float pixels) const216 float Graphviz_CGraph::pixelsToUserInches(float pixels) const {
217 	return pixels/userDPI;
218 }
219 
pixelsToUserInches(const std::array<float,2> pixels) const220 std::array<float,2> Graphviz_CGraph::pixelsToUserInches(const std::array<float,2> pixels) const {
221 	return {{
222 		pixelsToUserInches(pixels[0]),
223 		pixelsToUserInches(pixels[1])
224 	}};
225 }
226