Goby3 3.7.0
2026.11.12
Loading...
Searching...
No Matches
render_dot.h
Go to the documentation of this file.
1// Copyright 2020-2026:
2// GobySoft, LLC (2013-)
3// Community contributors (see AUTHORS file)
4// File authors:
5// Toby Schneider <toby@gobysoft.org>
6//
7//
8// This file is part of the Goby Underwater Autonomy Project Libraries
9// ("The Goby Libraries").
10//
11// The Goby Libraries are free software: you can redistribute them and/or modify
12// them under the terms of the GNU Lesser General Public License as published by
13// the Free Software Foundation, either version 2.1 of the License, or
14// (at your option) any later version.
15//
16// The Goby Libraries are distributed in the hope that they will be useful,
17// but WITHOUT ANY WARRANTY; without even the implied warranty of
18// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19// GNU Lesser General Public License for more details.
20//
21// You should have received a copy of the GNU Lesser General Public License
22// along with Goby. If not, see <http://www.gnu.org/licenses/>.
23
24#ifndef GOBY_MIDDLEWARE_TOOL_INTERFACE_RENDER_DOT_H
25#define GOBY_MIDDLEWARE_TOOL_INTERFACE_RENDER_DOT_H
26
27#include <algorithm>
28#include <cctype>
29#include <functional>
30#include <map>
31#include <memory>
32#include <ostream>
33#include <string>
34#include <vector>
35
36#include <boost/algorithm/string.hpp>
37
39
40namespace goby
41{
42namespace middleware
43{
44namespace tool
45{
46namespace interface
47{
48namespace style
49{
50// Graphviz X11 names: darkgreen, orange3, dodgerblue4, purple4
51const std::string vehicle_color = "#006400";
52const std::string module_color = "#cd8500";
53const std::string process_color = "#104e8b";
54const std::string thread_color = "#551a8b";
55
56inline const std::string& layer_color(Layer layer)
57{
58 switch (layer)
59 {
63 case Layer::UNKNOWN:
64 case Layer::INTERPROCESS: break;
65 }
66 return process_color;
67}
68} // namespace style
69
71{
73 bool omit_disconnected{false};
74 std::string splines{"ortho"};
75};
76
77namespace detail
78{
79inline void html_escape(std::string& s, bool do_break_before_lt = true,
80 bool do_break_after_commas = true)
81{
82 using boost::algorithm::replace_all;
83 replace_all(s, "&", "&amp;");
84 replace_all(s, "\"", "&quot;");
85 replace_all(s, "\'", "&apos;");
86 replace_all(s, "<", "&lt;");
87 replace_all(s, ">", "&gt;");
88
89 if (do_break_before_lt)
90 {
91 boost::algorithm::replace_first(s, "&lt;", "<br/>&lt;");
92 replace_all(s, "&lt;", "<font point-size=\"8\">&lt;");
93 replace_all(s, "&gt;", "&gt;</font>");
94 }
95
96 if (do_break_after_commas)
97 replace_all(s, ", ", ",<br/>");
98}
99
100inline std::string html_escaped(std::string s, bool break_before_lt = true,
101 bool break_after_commas = true)
102{
103 html_escape(s, break_before_lt, break_after_commas);
104 return s;
105}
106
107// unquoted DOT IDs may only contain letters, digits and underscores
108inline std::string dot_id(const std::string& s)
109{
110 std::string escaped;
111 for (unsigned char c : s)
112 {
113 if (std::isalnum(c) || c == '_')
114 escaped += c;
115 else
116 escaped += "_" + std::to_string(static_cast<int>(c)) + "_";
117 }
118 return escaped;
119}
120
121inline std::string dot_quoted(const std::string& s)
122{
123 std::string out;
124 for (char c : s)
125 {
126 if (c == '"' || c == '\\')
127 out += '\\';
128 out += c;
129 }
130 return out;
131}
132
133inline std::string short_group(const std::string& group)
134{
135 auto last_colon = group.find_last_of(":");
136 return (last_colon == std::string::npos) ? group : group.substr(last_colon + 1);
137}
138
140{
141 public:
142 DotWriter(std::ostream& os, const Deployment& deployment, const Resolution& resolution,
143 const DotParams& params)
144 : os_(os),
145 deployment_(deployment),
146 resolution_(resolution),
147 params_(params),
148 filter_(params.filter),
149 view_(deployment, resolution, filter_, params.omit_disconnected),
150 indexer_(deployment.indexer)
151 {
152 }
153
154 void write();
155
156 private:
157 // a collapsed application is one node, whichever of its threads an entry names
158 std::string node_name(const Endpoint& at, const std::string& thread) const
159 {
160 auto app = dot_id(at.platform->name) + "_" + dot_id(at.module->name) + "_" +
162 return view_.collapsed(*at.application) ? app : app + "_" + dot_id(thread);
163 }
164
165 void open_cluster(const std::string& indent, const std::string& name, const std::string& color)
166 {
167 os_ << indent.substr(1) << "subgraph cluster_" << cluster_++ << " {\n";
168 os_ << indent << "label=<<b>" << html_escaped(name) << "</b>>\n";
169 os_ << indent << "fontcolor=\"" << color << "\"\n";
170 os_ << indent << "penwidth=2\n";
171 os_ << indent << "pencolor=\"" << color << box_transparency_ << "\"\n";
172 }
173
174 std::string edge(const PubSubEntry& pub, const std::string& pub_node,
175 const std::string& sub_node, const std::string& color,
176 goby::middleware::Necessity necessity, bool is_regex, bool dynamic)
177 {
178 pubs_in_use_[pub_node][pub.layer].emplace(pub.publish_index, pub);
179
180 std::string edge_style;
181 switch (necessity)
182 {
184 edge_style = dynamic ? "\"bold,dashed\"" : "bold";
185 break;
187 edge_style = dynamic ? "dashed" : "tapered";
188 break;
190 edge_style = dynamic ? "dashed" : "solid";
191 break;
192 }
193 if (is_regex)
194 edge_style = "dotted";
195
196 auto label = pub.publish_index_str();
197 auto tooltip = dot_quoted(label + ": " + pub.display_group() + " | " + pub.scheme + " | " +
198 pub.type + (dynamic ? " [dynamic]" : ""));
199 return pub_node + "->" + sub_node + "[fontsize=7,headlabel=\"" + label + "\",taillabel=\"" +
200 label + "\",xlabel=<" + label + "[" +
201 html_escaped(short_group(pub.display_group()), false, false) + "]>,color=\"" +
202 color + "\",style=" + edge_style + ",tooltip=\"" + tooltip + "\",labeltooltip=\"" +
203 tooltip + "\",headtooltip=\"" + tooltip +
204 "\",penwidth=0.5,arrowhead=vee,arrowsize=0.3]\n";
205 }
206
207 void connection(const std::string& indent, const Connection& c)
208 {
209 os_ << indent
210 << edge(c.pub, node_name(c.pub_at, c.pub.thread), node_name(c.sub_at, c.sub.thread),
211 style::layer_color(c.pub.layer), c.sub.necessity, c.sub.is_regex,
212 c.pub.dynamic || c.sub.dynamic || c.loose);
213 }
214
215 void unsubscribed(const std::string& indent, const Unmatched& u)
216 {
217 const auto& color = style::layer_color(u.entry.layer);
218 auto node = node_name(u.at, u.entry.thread);
219 auto placeholder = node + "_no_subscribers_" + dot_id(color);
220 os_ << indent << placeholder << " [label=\"\",style=invis] \n"
221 << edge(u.entry, node, placeholder, color, goby::middleware::Necessity::OPTIONAL, false,
222 u.entry.dynamic);
223 }
224
225 void unpublished(const std::string& indent, const Unmatched& u)
226 {
227 bool required = u.entry.necessity == goby::middleware::Necessity::REQUIRED;
228 std::string color = required ? "red" : style::layer_color(u.entry.layer);
229 auto node = node_name(u.at, u.entry.thread);
230 auto placeholder = node + "_no_publishers_" + dot_id(color);
231
232 node_threads_[placeholder] = nullptr;
233 PubSubEntry fake_pub = u.entry;
234 fake_pub.direction = PubSubEntry::Direction::PUBLISH;
235 fake_pub.publish_index = indexer_.next(fake_pub.layer);
236
237 os_ << indent << placeholder << " \n"
238 << edge(fake_pub, placeholder, node, color, u.entry.necessity, u.entry.is_regex,
239 u.entry.dynamic);
240 }
241
242 void connections_from(const std::string& indent, Layer layer,
243 const std::function<bool(const Endpoint&)>& at)
244 {
245 for (const auto& c : resolution_.connections)
246 {
247 if (c.pub.layer == layer && at(c.pub_at) && view_.drawn(c))
248 connection(indent, c);
249 }
250 for (const auto& u : resolution_.unsubscribed)
251 {
252 if (u.entry.layer == layer && at(u.at) && view_.drawn_unsubscribed(u))
253 unsubscribed(indent, u);
254 }
255 }
256
257 void unpublished_in(const std::string& indent, Layer layer,
258 const std::function<bool(const Endpoint&)>& at)
259 {
260 std::vector<const Unmatched*> subs;
261 for (const auto& u : resolution_.unpublished)
262 {
263 if (u.entry.layer == layer && at(u.at) && view_.drawn_unpublished(u))
264 subs.push_back(&u);
265 }
266 std::stable_sort(subs.begin(), subs.end(),
267 [](const Unmatched* a, const Unmatched* b)
268 {
269 auto key = [](const Unmatched* u) {
270 return std::make_tuple(u->at.platform->name, u->at.module->name,
271 u->at.application->name);
272 };
273 if (key(a) != key(b))
274 return key(a) < key(b);
275 return a->entry < b->entry;
276 });
277 for (const auto* u : subs) unpublished(indent, *u);
278 }
279
280 void write_node_labels();
281
282 // the publications drawn from a node, listed in its label
283 std::string pub_key(const std::string& node)
284 {
285 std::string key;
286 for (const auto& layer_p : pubs_in_use_[node])
287 {
288 for (const auto& index_pub_p : layer_p.second)
289 {
290 const auto& pub = index_pub_p.second;
291 key += "<font color=\"" + style::layer_color(layer_p.first) +
292 "\" point-size=\"10\">" + pub.publish_index_str() +
293 ": </font><b><font point-size=\"10\">" + html_escaped(pub.display_group()) +
294 "</font></b><br/><font point-size=\"6\">" + html_escaped(pub.scheme) +
295 "</font><br/><font point-size=\"8\">" +
296 html_escaped(pub.type, false, false) + "</font><br/>";
297 }
298 }
299 return key;
300 }
301
302 private:
303 std::ostream& os_;
304 const Deployment& deployment_;
305 const Resolution& resolution_;
306 const DotParams& params_;
307 ViewFilter filter_;
308 DiagramView view_;
309 PublishIndexer indexer_;
310 int cluster_{0};
311 const std::string box_transparency_{"20"};
312
313 // node -> thread drawn there (nullptr for an unpublished subscription's placeholder)
314 std::map<std::string, const Thread*> node_threads_;
315 // node -> collapsed application drawn there
316 std::map<std::string, const Application*> node_apps_;
317 // node -> layer -> publish index -> publication
318 std::map<std::string, std::map<Layer, std::map<int, PubSubEntry>>> pubs_in_use_;
319};
320
321inline void DotWriter::write()
322{
323 os_ << "digraph " << dot_id(deployment_.name) << " { \n";
324 os_ << "\tsplines=" << params_.splines << "\n";
325 os_ << "\tnewrank=true\n";
326
327 for (const auto& platform : deployment_.platforms)
328 {
329 if (!filter_.is_node_included(platform.name))
330 continue;
331 open_cluster("\t\t",
332 platform.has_modules()
333 ? platform.name
334 : view_.container_label(platform.name, platform.modules.front()),
336
337 for (const auto& module : platform.modules)
338 {
339 if (platform.has_modules())
340 {
341 if (!filter_.is_node_included(module.name))
342 continue;
343 open_cluster("\t\t", view_.container_label(module.name, module),
345 }
346
347 for (const auto& app_p : module.applications)
348 {
349 const auto& app = app_p.second;
350 if (!filter_.is_node_included(app.name) || view_.folded(app))
351 continue;
352 if (view_.collapsed(app))
353 {
354 auto node = node_name({&platform, &module, &app, nullptr}, "");
355 node_apps_[node] = &app;
356 os_ << "\t\t\t" << node << "\n";
357 connections_from("\t\t", Layer::INTERPROCESS,
358 [&](const Endpoint& at) { return at.application == &app; });
359 continue;
360 }
361 open_cluster("\t\t\t", app.name, style::process_color);
362
363 for (const auto& thread_p : app.threads)
364 {
365 const auto& thread = *thread_p.second;
366 if (!view_.thread_drawn(app, thread))
367 continue;
368
369 connections_from("\t\t\t", Layer::INTERTHREAD,
370 [&](const Endpoint& at) { return at.owner == &thread; });
371
372 Endpoint at{&platform, &module, &app, &thread};
373 auto node = node_name(at, thread.most_derived_name());
374 node_threads_[node] = &thread;
375 os_ << "\t\t\t" << node << "[penwidth=2,color=\"" << style::thread_color
376 << box_transparency_ << "\"]\n";
377 }
378
379 unpublished_in("\t\t\t", Layer::INTERTHREAD,
380 [&](const Endpoint& at) { return at.application == &app; });
381 os_ << "\t\t}\n";
382
383 connections_from("\t\t", Layer::INTERPROCESS,
384 [&](const Endpoint& at) { return at.application == &app; });
385 }
386
387 unpublished_in("\t\t", Layer::INTERPROCESS,
388 [&](const Endpoint& at) { return at.module == &module; });
389
390 if (platform.has_modules())
391 os_ << "\t}\n";
392
393 connections_from("\t", Layer::INTERMODULE,
394 [&](const Endpoint& at) { return at.module == &module; });
395 }
396
397 unpublished_in("\t\t", Layer::INTERMODULE,
398 [&](const Endpoint& at) { return at.platform == &platform; });
399 os_ << "\t}\n";
400
401 connections_from("\t", Layer::INTERVEHICLE,
402 [&](const Endpoint& at) { return at.platform == &platform; });
403 }
404
405 unpublished_in("\t", Layer::INTERVEHICLE, [](const Endpoint&) { return true; });
406
407 write_node_labels();
408 os_ << "}\n";
409}
410
411inline void DotWriter::write_node_labels()
412{
413 for (const auto& node_app_p : node_apps_)
414 {
415 os_ << "\t" << node_app_p.first << "\n"
416 << " [label=<<b><font color=\"" << style::process_color << "\">"
417 << html_escaped(node_app_p.second->name) << "</font></b><br/>"
418 << pub_key(node_app_p.first) << ">,shape=box,penwidth=2,color=\""
419 << style::process_color << box_transparency_ << "\"]\n";
420 }
421
422 for (const auto& node_thread_p : node_threads_)
423 {
424 const auto& node = node_thread_p.first;
425 const auto* thread = node_thread_p.second;
426 auto pub_key = this->pub_key(node);
427
428 if (thread)
429 {
430 auto name = thread->most_derived_name();
431 os_ << "\t" << node << "\n"
432 << " [label=<<font color=\"" << style::thread_color << "\">"
433 << html_escaped(display_thread_name(name), false, false) << "</font><br/>"
434 << pub_key << ">,shape=box,style=" << (thread->known ? "solid" : "dashed")
435 << (display_thread_name(name) == name ? ""
436 : ",tooltip=\"" + dot_quoted(name) + "\"")
437 << "]\n";
438 }
439 else
440 {
441 os_ << "\t" << node << "\n"
442 << " [label=<" << pub_key << ">,penwidth=0]\n";
443 }
444 }
445}
446} // namespace detail
447
449inline void write_dot(std::ostream& os, const Deployment& deployment, const Resolution& resolution,
450 const DotParams& params)
451{
452 detail::DotWriter(os, deployment, resolution, params).write();
453}
454
455} // namespace interface
456} // namespace tool
457} // namespace middleware
458} // namespace goby
459
460#endif
bool drawn_unsubscribed(const Unmatched &u) const
Definition resolve.h:389
bool drawn_unpublished(const Unmatched &u) const
An unmet required subscription is drawn even when the rest are left out.
Definition resolve.h:396
bool collapsed(const Application &app) const
Definition resolve.h:364
bool drawn(const Connection &c) const
Definition resolve.h:382
DotWriter(std::ostream &os, const Deployment &deployment, const Resolution &resolution, const DotParams &params)
Definition render_dot.h:142
goby::util::logger::GroupSetter group(std::string n)
detail namespace with internal helper functions
Definition json.hpp:263
std::string replace_all(std::string text, const std::string &from, const std::string &to)
Definition launch.h:70
std::string dot_id(const std::string &s)
Definition render_dot.h:108
std::string short_group(const std::string &group)
Definition render_dot.h:133
std::string html_escaped(std::string s, bool break_before_lt=true, bool break_after_commas=true)
Definition render_dot.h:100
std::string dot_quoted(const std::string &s)
Definition render_dot.h:121
void html_escape(std::string &s, bool do_break_before_lt=true, bool do_break_after_commas=true)
Definition render_dot.h:79
const std::string & layer_color(Layer layer)
Definition render_dot.h:56
void write_dot(std::ostream &os, const Deployment &deployment, const Resolution &resolution, const DotParams &params)
Write a Graphviz digraph of the deployment's publish/subscribe connections.
Definition render_dot.h:449
Necessity
Used to tag subscriptions based on their necessity (e.g. required for correct functioning,...
Definition interface.h:222
The global namespace for the Goby project.
Where in a deployment a publication or subscription is made.
Definition resolve.h:45