Goby3 3.7.0
2026.11.12
Loading...
Searching...
No Matches
render_d2.h
Go to the documentation of this file.
1// Copyright 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_D2_H
25#define GOBY_MIDDLEWARE_TOOL_INTERFACE_RENDER_D2_H
26
27#include <cctype>
28#include <map>
29#include <ostream>
30#include <set>
31#include <string>
32#include <tuple>
33#include <vector>
34
36
37namespace goby
38{
39namespace middleware
40{
41namespace tool
42{
43namespace interface
44{
46{
48 bool omit_disconnected{false};
49 // elk lays out connections between nested containers better than d2's default, dagre
50 std::string layout_engine{"elk"};
51};
52
53namespace detail
54{
55inline std::string d2_quoted(const std::string& s)
56{
57 std::string out = "\"";
58 for (char c : s)
59 {
60 if (c == '\n')
61 {
62 out += "\\n";
63 continue;
64 }
65 if (c == '"' || c == '\\')
66 out += '\\';
67 out += c;
68 }
69 return out + "\"";
70}
71
72// d2 renders a tooltip as Markdown and parses the HTML, so a template argument list such as
73// <class Foo> would be read as a tag
74inline std::string d2_tooltip(const std::string& s)
75{
76 std::string escaped;
77 for (char c : s)
78 {
79 if (c == '<')
80 escaped += "&lt;";
81 else if (c == '>')
82 escaped += "&gt;";
83 else
84 escaped += c;
85 }
86 return d2_quoted(escaped);
87}
88
92{
93 public:
94 D2Writer(std::ostream& os, const Deployment& deployment, const Resolution& resolution,
95 const D2Params& params)
96 : os_(os),
97 deployment_(deployment),
98 resolution_(resolution),
99 params_(params),
100 filter_(params.filter),
101 view_(deployment, resolution, filter_, params.omit_disconnected),
102 indexer_(deployment.indexer)
103 {
104 }
105
106 void write()
107 {
108 collect();
109 write_header();
110 write_containers();
111 write_edges();
112 }
113
114 private:
115 struct Edge
116 {
117 std::string from;
118 std::string to;
119 PubSubEntry pub;
120 std::vector<std::string> classes;
121 };
122
123 // D2 keys: letters, digits and underscores, unique among their siblings, never a keyword
124 std::string id(const std::string& parent, const std::string& name)
125 {
126 auto key = parent + "\n" + name;
127 auto it = ids_.find(key);
128 if (it != ids_.end())
129 return it->second;
130
131 static const std::set<std::string> keywords{
132 "label", "style", "class", "classes", "shape",
133 "icon", "near", "width", "height", "tooltip",
134 "link", "direction", "vars", "constraint", "grid-rows",
135 "grid-columns", "top", "left", "source-arrowhead", "target-arrowhead",
136 "layers", "scenarios", "steps", "filled", "_"};
137 // keyed by the unqualified name without template arguments; the label keeps the rest
138 auto shortened = name.substr(0, name.find('<'));
139 auto scope = shortened.rfind("::");
140 if (scope != std::string::npos)
141 shortened = shortened.substr(scope + 2);
142
143 std::string base;
144 for (unsigned char c : shortened)
145 {
146 if (std::isalnum(c) || c == '_')
147 base += c;
148 else if (base.empty() || base.back() != '_')
149 base += '_';
150 }
151 while (!base.empty() && base.back() == '_') base.pop_back();
152 while (!base.empty() && base.front() == '_') base.erase(0, 1);
153 if (base.empty() || keywords.count(base))
154 base = "n_" + base;
155
156 std::string unique = base;
157 for (int n = 2; used_[parent].count(unique); ++n) unique = base + "_" + std::to_string(n);
158 used_[parent].insert(unique);
159 return ids_[key] = unique;
160 }
161
162 // a collapsed application is one shape, whichever of its threads an entry names
163 std::string path(const Endpoint& at, const std::string& thread)
164 {
165 auto app = app_path(at);
166 if (view_.collapsed(*at.application))
167 return app;
168 threads_[app].insert(thread);
169 return app + "." + id(app, thread);
170 }
171
172 std::string app_path(const Endpoint& at)
173 {
174 std::string p = id("", at.platform->name);
175 if (at.platform->has_modules())
176 p += "." + id(p, at.module->name);
177 return p + "." + id(p, at.application->name);
178 }
179
180 // the "no publisher" / "no subscriber" node beside an application's threads
181 std::string placeholder_id(const std::string& app, bool no_publisher)
182 {
183 // the leading control character keeps these apart from a thread of the same name
184 auto key = no_publisher ? "\x01no publisher" : "\x01no subscriber";
185 auto placeholder = id(app, key);
186 placeholders_[app][placeholder] = no_publisher ? "no publisher" : "no subscriber";
187 return placeholder;
188 }
189
190 std::vector<std::string> edge_classes(const PubSubEntry& sub, bool dynamic)
191 {
192 std::vector<std::string> classes{layer_name(sub.layer)};
193 if (sub.necessity == goby::middleware::Necessity::REQUIRED)
194 classes.push_back("required");
195 else if (sub.necessity == goby::middleware::Necessity::RECOMMENDED)
196 classes.push_back("recommended");
197 if (sub.is_regex)
198 classes.push_back("regex");
199 else if (dynamic)
200 classes.push_back("dynamic");
201 return classes;
202 }
203
204 void collect()
205 {
206 for (const auto& c : resolution_.connections)
207 {
208 if (!view_.drawn(c))
209 continue;
210 edges_.push_back({path(c.pub_at, c.pub.thread), path(c.sub_at, c.sub.thread), c.pub,
211 edge_classes(c.sub, c.pub.dynamic || c.sub.dynamic || c.loose)});
212 }
213
214 for (const auto& u : resolution_.unsubscribed)
215 {
216 if (!view_.drawn_unsubscribed(u))
217 continue;
218 auto app = app_path(u.at);
219 auto placeholder = placeholder_id(app, false);
220 std::vector<std::string> classes{layer_name(u.entry.layer)};
221 if (u.entry.dynamic)
222 classes.push_back("dynamic");
223 edges_.push_back(
224 {path(u.at, u.entry.thread), app + "." + placeholder, u.entry, classes});
225 }
226
227 for (const auto& u : resolution_.unpublished)
228 {
229 if (!view_.drawn_unpublished(u))
230 continue;
231 bool required = u.entry.necessity == goby::middleware::Necessity::REQUIRED;
232 auto app = app_path(u.at);
233 auto placeholder = placeholder_id(app, true);
234 PubSubEntry placeholder_pub = u.entry;
235 placeholder_pub.publish_index = indexer_.next(u.entry.layer);
236 auto classes = edge_classes(u.entry, u.entry.dynamic);
237 if (required)
238 classes.push_back("unmet");
239 edges_.push_back(
240 {app + "." + placeholder, path(u.at, u.entry.thread), placeholder_pub, classes});
241 }
242
243 for (const auto& e : edges_)
244 {
245 if (e.pub.direction == PubSubEntry::Direction::PUBLISH)
246 pubs_by_thread_[e.from].emplace(e.pub.layer, e.pub.publish_index, e.pub);
247 }
248 }
249
250 void write_header()
251 {
252 if (!params_.layout_engine.empty())
253 os_ << "vars: { d2-config: { layout-engine: " << params_.layout_engine << " } }\n";
254 os_ << "direction: right\n";
255 os_ << "classes: {\n";
256 auto container = [&](const std::string& name, const std::string& color)
257 {
258 os_ << " " << name << ": { style.stroke: \"" << color << "\"; style.font-color: \""
259 << color << "\"; style.stroke-width: 2; style.fill: transparent }\n";
260 };
261 container("platform", style::vehicle_color);
262 container("module", style::module_color);
263 container("application", style::process_color);
264 os_ << " thread: { style.stroke: \"" << style::thread_color << "\"; style.font-color: \""
265 << style::thread_color << "\" }\n";
266 os_ << " placeholder: { shape: text; style.font-color: \"#888888\" }\n";
267 for (auto layer :
269 os_ << " " << layer_name(layer) << ": { style.stroke: \"" << style::layer_color(layer)
270 << "\"; style.font-size: 10 }\n";
271 os_ << " required: { style.stroke-width: 3 }\n";
272 os_ << " recommended: { style.stroke-dash: 6 }\n";
273 os_ << " regex: { style.stroke-dash: 2 }\n";
274 os_ << " dynamic: { style.stroke-dash: 4 }\n";
275 os_ << " unmet: { style.stroke: red; style.stroke-width: 3 }\n";
276 os_ << "}\n";
277 }
278
279 void label(const std::string& indent, const std::string& key, const std::string& name)
280 {
281 if (key != name)
282 os_ << indent << "label: " << d2_quoted(name) << "\n";
283 }
284
285 void write_containers()
286 {
287 for (const auto& platform : deployment_.platforms)
288 {
289 if (!filter_.is_node_included(platform.name))
290 continue;
291 auto platform_id = id("", platform.name);
292 os_ << platform_id << ": {\n class: platform\n";
293 label(" ", platform_id,
294 platform.has_modules()
295 ? platform.name
296 : view_.container_label(platform.name, platform.modules.front()));
297
298 for (const auto& module : platform.modules)
299 {
300 std::string indent = " ";
301 std::string parent = platform_id;
302 if (platform.has_modules())
303 {
304 if (!filter_.is_node_included(module.name))
305 continue;
306 auto module_id = id(platform_id, module.name);
307 os_ << indent << module_id << ": {\n" << indent << " class: module\n";
308 label(indent + " ", module_id, view_.container_label(module.name, module));
309 parent += "." + module_id;
310 indent += " ";
311 }
312
313 for (const auto& app_p : module.applications)
314 {
315 const auto& app = app_p.second;
316 if (!filter_.is_node_included(app.name) || view_.folded(app))
317 continue;
318 auto app_id = id(parent, app.name);
319 auto app_path = parent + "." + app_id;
320 os_ << indent << app_id << ": {\n" << indent << " class: application\n";
321 label(indent + " ", app_id, app.name);
322 if (view_.collapsed(app))
323 {
324 auto tooltip = pubs_tooltip(app_path);
325 if (!tooltip.empty())
326 os_ << indent << " tooltip: " << d2_tooltip(tooltip) << "\n";
327 }
328
329 // drawn threads (a base class is drawn as its most derived class), and any
330 // thread an edge names
331 std::map<std::string, bool> threads;
332 for (const auto& thread_p : app.threads)
333 {
334 const auto& thread = *thread_p.second;
335 if (!view_.collapsed(app) && view_.thread_drawn(app, thread))
336 threads[thread.most_derived_name()] = thread.known;
337 }
338 for (const auto& thread : threads_[app_path]) threads.emplace(thread, true);
339
340 for (const auto& thread_p : threads)
341 write_thread(indent + " ", app_path, thread_p.first, thread_p.second);
342 for (const auto& placeholder : placeholders_[app_path])
343 os_ << indent << " " << placeholder.first
344 << ": { class: placeholder; label: " << d2_quoted(placeholder.second)
345 << " }\n";
346 os_ << indent << "}\n";
347 }
348
349 if (platform.has_modules())
350 os_ << " }\n";
351 }
352 os_ << "}\n";
353 }
354 }
355
356 void write_thread(const std::string& indent, const std::string& app_path,
357 const std::string& thread, bool known)
358 {
359 auto thread_id = id(app_path, thread);
360 auto display = display_thread_name(thread);
361 os_ << indent << thread_id << ": { class: thread";
362 if (thread_id != display)
363 os_ << "; label: " << d2_quoted(display);
364 if (!known)
365 os_ << "; style.stroke-dash: 3";
366
367 auto tooltip = pubs_tooltip(app_path + "." + thread_id);
368 if (display != thread)
369 tooltip = thread + (tooltip.empty() ? "" : "\n") + tooltip;
370 if (!tooltip.empty())
371 os_ << "; tooltip: " << d2_tooltip(tooltip);
372 os_ << " }\n";
373 }
374
375 // the publications drawn from a shape, one per line
376 std::string pubs_tooltip(const std::string& path)
377 {
378 std::string tooltip;
379 for (const auto& p : pubs_by_thread_[path])
380 {
381 const auto& pub = std::get<2>(p);
382 tooltip += (tooltip.empty() ? "" : "\n") + pub.publish_index_str() + ": " +
383 pub.display_group() + " | " + pub.scheme + " | " + pub.type;
384 }
385 return tooltip;
386 }
387
388 void write_edges()
389 {
390 for (const auto& e : edges_)
391 {
392 auto pub_label = e.pub.publish_index_str();
393 os_ << e.from << " -> " << e.to << ": "
394 << d2_quoted(pub_label + " [" + short_group(e.pub.display_group()) + "]")
395 << " { class: ";
396 if (e.classes.size() == 1)
397 os_ << e.classes.front();
398 else
399 {
400 os_ << "[";
401 for (std::size_t i = 0; i < e.classes.size(); ++i)
402 os_ << (i ? "; " : "") << e.classes[i];
403 os_ << "]";
404 }
405 os_ << "; tooltip: "
406 << d2_tooltip(pub_label + ": " + e.pub.display_group() + " | " + e.pub.scheme +
407 " | " + e.pub.type)
408 << " }\n";
409 }
410 }
411
412 private:
413 std::ostream& os_;
414 const Deployment& deployment_;
415 const Resolution& resolution_;
416 const D2Params& params_;
417 ViewFilter filter_;
418 DiagramView view_;
419 PublishIndexer indexer_;
420
421 std::map<std::string, std::string> ids_;
422 std::map<std::string, std::set<std::string>> used_;
423 std::vector<Edge> edges_;
424 // application path -> threads named by edges
425 std::map<std::string, std::set<std::string>> threads_;
426 // application path -> placeholder key -> label
427 std::map<std::string, std::map<std::string, std::string>> placeholders_;
428 // thread path -> (layer, index, publication), for the thread's tooltip
429 std::map<std::string, std::set<std::tuple<Layer, int, PubSubEntry>>> pubs_by_thread_;
430};
431} // namespace detail
432
434inline void write_d2(std::ostream& os, const Deployment& deployment, const Resolution& resolution,
435 const D2Params& params)
436{
437 detail::D2Writer(os, deployment, resolution, params).write();
438}
439
440} // namespace interface
441} // namespace tool
442} // namespace middleware
443} // namespace goby
444
445#endif
bool drawn_unsubscribed(const Unmatched &u) const
Definition resolve.h:389
std::string container_label(const std::string &name, const Module &module) const
The container's name, with any gobyd folded into it.
Definition resolve.h:372
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 thread_drawn(const Application &app, const Thread &thread) const
Goby's own threads within a project's application (such as its portal's forwarder) are drawn only whe...
Definition resolve.h:409
bool folded(const Application &app) const
Definition resolve.h:369
bool collapsed(const Application &app) const
Definition resolve.h:364
bool drawn(const Connection &c) const
Definition resolve.h:382
bool is_node_included(const std::string &node) const
Definition resolve.h:222
Writes a deployment as D2 (https://d2lang.com): the same containers, colors and edge conventions as t...
Definition render_d2.h:92
D2Writer(std::ostream &os, const Deployment &deployment, const Resolution &resolution, const D2Params &params)
Definition render_d2.h:94
detail namespace with internal helper functions
Definition json.hpp:263
std::string short_group(const std::string &group)
Definition render_dot.h:133
std::string d2_tooltip(const std::string &s)
Definition render_d2.h:74
std::string d2_quoted(const std::string &s)
Definition render_d2.h:55
const std::string & layer_color(Layer layer)
Definition render_dot.h:56
std::string layer_name(Layer layer)
std::string display_thread_name(const std::string &name)
A thread's name as drawn: a lambda by its file and line, a template without its arguments.
Definition resolve.h:296
void write_d2(std::ostream &os, const Deployment &deployment, const Resolution &resolution, const D2Params &params)
Write a D2 diagram of the deployment's publish/subscribe connections.
Definition render_d2.h:434
std::string thread_id(std::thread::id i=std::this_thread::get_id())
Definition common.h:53
constexpr T e
Definition constants.h:35
The global namespace for the Goby project.
One publication or subscription as written to (and read from) an interface file.