1 /*
2 This file is a part of
3 QVGE - Qt Visual Graph Editor
4 
5 (c) 2016-2021 Ars L. Masiuk (ars.masiuk@gmail.com)
6 
7 It can be used freely, maintaining the information above.
8 */
9 
10 #include "CFormatPlainDOT.h"
11 
12 #include <QFile>
13 #include <QDebug>
14 #include <QTextStream>
15 #include <QFont>
16 
17 
18 // helpers
19 
fromDotNodeShape(const QString & shape)20 static QString fromDotNodeShape(const QString& shape)
21 {
22 	// rename to conform dot
23 	if (shape == "ellipse")		return "disc";
24 	if (shape == "rect" || shape == "box" ) return "square";
25 	if (shape == "invtriangle")	return "triangle2";
26 
27 	// else take original
28 	return shape;
29 }
30 
31 
fromDotNodeStyle(const QString & style,GraphAttributes & nodeAttr)32 static void fromDotNodeStyle(const QString& style, GraphAttributes& nodeAttr)
33 {
34 	if (style.contains("dashed"))
35 		nodeAttr["stroke.style"] = "dashed";
36 	else
37 	if (style.contains("dotted"))
38 		nodeAttr["stroke.style"] = "dotted";
39 
40 	if (style.contains("invis"))
41 		nodeAttr["stroke.size"] = 0;
42 	else
43 	if (style.contains("solid"))
44 		nodeAttr["stroke.size"] = 1;
45 	else
46 	if (style.contains("bold"))
47 		nodeAttr["stroke.size"] = 3;
48 }
49 
50 
51 class QStringRefsConstIterator
52 {
53 public:
QStringRefsConstIterator(const QStringList & refs)54 	QStringRefsConstIterator(const QStringList& refs): m_refs(refs)
55 	{
56 		m_pos = (m_refs.size()) ? 0 : -1;
57 	}
58 
pos() const59 	int pos() const
60 	{
61 		return m_pos;
62 	}
63 
restCount() const64 	int restCount() const
65 	{
66 		return canNext() ? m_refs.count() - m_pos : 0;
67 	}
68 
canNext() const69 	bool canNext() const
70 	{
71 		return (m_pos >= 0 && m_pos < m_refs.count());
72 	}
73 
next()74 	bool next()
75 	{
76 		if (canNext())
77 		{
78 			m_pos++;
79 			return true;
80 		}
81 
82 		return false;
83 	}
84 
next(float & f)85 	bool next(float &f)
86 	{
87 		if (canNext())
88 		{
89 			bool ok = false;
90 			float tf = m_refs.at(m_pos++).toFloat(&ok);
91 			if (ok)
92 			{
93 				f = tf;
94 				return true;
95 			}
96 		}
97 
98 		return false;
99 	}
100 
next(int & i)101 	bool next(int &i)
102 	{
103 		if (canNext())
104 		{
105 			bool ok = false;
106 			int ti = m_refs.at(m_pos++).toInt(&ok);
107 			if (ok)
108 			{
109 				i = ti;
110 				return true;
111 			}
112 		}
113 
114 		return false;
115 	}
116 
next(QString & s)117 	bool next(QString &s)
118 	{
119 		if (canNext())
120 		{
121 			s = m_refs.at(m_pos++);
122 			return true;
123 		}
124 
125 		return false;
126 	}
127 
next(QByteArray & s)128 	bool next(QByteArray &s)
129 	{
130 		if (canNext())
131 		{
132 			s = m_refs.at(m_pos++).toUtf8();
133 			return true;
134 		}
135 
136 		return false;
137 	}
138 
139 private:
140 	const QStringList& m_refs;
141 	int m_pos = -1;
142 };
143 
144 
145 // parsing plain dot
146 
parseLine(QTextStream & ts)147 static QStringList parseLine(QTextStream &ts)
148 {
149 	QStringList tokens;
150 
151 	// normal mode
152 	while (!ts.atEnd())
153 	{
154 		QString line = ts.readLine().trimmed();
155 		if (line.isEmpty())
156 			continue;
157 
158 		// add EOL
159 		line += '\0';
160 
161 		int i = 0;
162 
163 	_loop:
164 		// skip to next token
165 		while (line[i].isSpace())
166 			i++;
167 		if (line[i] == '\0')
168 			break;
169 
170 		// check for "
171 		if (line[i] == '"')
172 		{
173 			i++;
174 			QString token;
175 			while (line[i] != '"' && line[i] != '\0')
176 				token += line[i++];
177 			tokens << token;
178 
179 			goto _loop;
180 		}
181 
182 		// check for < + eol
183 		if (line[i] == '<' && line[i+1] == '\0')
184 		{
185 			QString token;
186 			while (!ts.atEnd())
187 			{
188 				QString l = ts.readLine();
189 				QString lt = l.trimmed() + '\0';
190 				if (lt[0] == '>' && (lt[1].isSpace() || lt[1] == '\0'))
191 				{
192 					tokens << token;
193 					line = lt;
194 					i = 1;
195 					goto _loop;
196 				}
197 				else
198 				{
199 					token += l + "\n";
200 				}
201 			}
202 		}
203 
204 		// read normal tokens
205 		QString token;
206 		while (line[i] != '\0' && !line[i].isSpace())
207 			token += line[i++];
208 
209 		tokens << token;
210 		goto _loop;
211 	}
212 
213 	return tokens;
214 }
215 
216 
splitEdgePortIds(QByteArray & nodeId,QByteArray & portId)217 static void splitEdgePortIds(QByteArray& nodeId, QByteArray& portId)
218 {
219 	int index = nodeId.indexOf(':');
220 	if (index <= 0)
221 	{
222 		portId.clear();
223 		return;
224 	}
225 
226 	portId = nodeId.mid(index + 1);
227 	nodeId = nodeId.left(index);
228 }
229 
230 
231 // reimp
232 
load(const QString & fileName,Graph & g,QString * lastError) const233 bool CFormatPlainDOT::load(const QString& fileName, Graph& g, QString* lastError) const
234 {
235 	GraphInternal gi;
236 	gi.g = &g;
237 
238 	QFile f(fileName);
239 	if (!f.open(QFile::ReadOnly))
240 	{
241 		if (lastError)
242 			*lastError = QObject::tr("Cannot open file");
243 
244 		return false;
245 	}
246 
247 	QTextStream ts(&f);
248 
249 	while (!ts.atEnd())
250 	{
251 		auto refs = parseLine(ts);
252 		if (refs.first() == "stop")
253 			break;
254 
255 		if (refs.first() == "graph")
256 		{
257 			parseGraph(refs, gi);
258 			continue;
259 		}
260 
261 		if (refs.first() == "node")
262 		{
263 			parseNode(refs, gi);
264 			continue;
265 		}
266 
267 		if (refs.first() == "edge")
268 		{
269 			parseEdge(refs, gi);
270 			continue;
271 		}
272 	}
273 
274 	// done
275 	f.close();
276 
277     return true;
278 }
279 
280 
save(const QString & fileName,Graph & g,QString * lastError) const281 bool CFormatPlainDOT::save(const QString& fileName, Graph& g, QString* lastError) const
282 {
283 	return false;
284 }
285 
286 
287 // privates
288 
parseGraph(const QStringList & refs,GraphInternal & gi) const289 bool CFormatPlainDOT::parseGraph(const QStringList &refs, GraphInternal &gi) const
290 {
291 	QStringRefsConstIterator rit(refs);
292 	rit.next();	// skip header
293 	rit.next(gi.g_scale);
294 	rit.next(gi.g_x);
295 	rit.next(gi.g_y);
296 
297 	return true;
298 }
299 
300 
parseNode(const QStringList & refs,GraphInternal & gi) const301 bool CFormatPlainDOT::parseNode(const QStringList &refs, GraphInternal &gi) const
302 {
303 	QStringRefsConstIterator rit(refs);
304 	rit.next();	// skip header
305 
306 	Node node;
307 	rit.next(node.id);
308 
309 	QString label, style, shape, color, fillcolor;
310 	float x, y, width, height;
311 
312 	rit.next(x);
313 	rit.next(y);
314 	rit.next(width);
315 	rit.next(height);
316 
317 	rit.next(label);
318 	rit.next(style);
319 	rit.next(shape);
320 	rit.next(color);
321 	rit.next(fillcolor);
322 
323 	node.attrs["x"] = x * 72.0 * gi.g_scale;
324 	node.attrs["y"] = y * 72.0 * gi.g_scale;
325 	node.attrs["width"] = width * 72.0 * gi.g_scale;
326 	node.attrs["height"] = height * 72.0 * gi.g_scale;
327 
328 	label = label.replace("\\n", "\n");
329 	node.attrs["label"] = label;
330 	node.attrs["shape"] = fromDotNodeShape(shape);
331 	fromDotNodeStyle(style, node.attrs);
332 	node.attrs["color"] = fillcolor;
333 	node.attrs["stroke.color"] = color;
334 
335 	gi.g->nodes << node;
336 
337 	return true;
338 }
339 
340 
parseEdge(const QStringList & refs,GraphInternal & gi) const341 bool CFormatPlainDOT::parseEdge(const QStringList &refs, GraphInternal &gi) const
342 {
343 	QStringRefsConstIterator rit(refs);
344 	rit.next();	// skip header
345 
346 	Edge edge;
347 	rit.next(edge.startNodeId);
348 	rit.next(edge.endNodeId);
349 
350 	int jointCount = 0;
351 	float x, y;
352 	rit.next(jointCount);
353 	if (jointCount > 0)
354 	{
355 		QString points;
356 		for (int i = 0; i < jointCount; ++i)
357 		{
358 			rit.next(x);
359 			rit.next(y);
360 			x = x * 72.0 * gi.g_scale;
361 			y = y * 72.0 * gi.g_scale;
362 			points += QString("%1 %2 ").arg(x).arg(y);
363 		}
364 		//edge.attrs["points"] = points;
365 	}
366 
367 
368 	if (rit.restCount() > 2)
369 	{
370 		QString label;
371 		rit.next(label);
372 		label = label.replace("\\n", "\n");
373 
374 		rit.next(x);
375 		rit.next(y);
376 		edge.attrs["label"] = label;
377 		edge.attrs["label.x"] = x * 72.0 * gi.g_scale;
378 		edge.attrs["label.y"] = y * 72.0 * gi.g_scale;
379 
380 		edge.id = label.toUtf8();
381 	}
382 
383 	if (rit.canNext())
384 	{
385 		QString style; rit.next(style);
386 		edge.attrs["style"] = style;
387 	}
388 
389 	if (rit.canNext())
390 	{
391 		QString color; rit.next(color);
392 		edge.attrs["color"] = color;
393 	}
394 
395 
396 	// check id
397 	if (edge.id.isEmpty())
398 	{
399 		edge.id = edge.startNodeId + "-" + edge.endNodeId;
400 	}
401 
402 
403 	// split ports if any
404 	splitEdgePortIds(edge.startNodeId, edge.startPortId);
405 	splitEdgePortIds(edge.endNodeId, edge.endPortId);
406 
407 
408 	gi.g->edges << edge;
409 
410 	return true;
411 }
412 
413