• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CyberZHG / SVGDiagram / 20093014786

10 Dec 2025 09:01AM UTC coverage: 95.382% (-1.6%) from 96.996%
20093014786

Pull #4

github

web-flow
Merge e0dfefada into 68adacba8
Pull Request #4: Add subgraph for default group attributes

183 of 207 new or added lines in 2 files covered. (88.41%)

10 existing lines in 2 files now uncovered.

1487 of 1559 relevant lines covered (95.38%)

1072.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.7
/src/svg_nodes.cpp
1
#include "svg_nodes.h"
2

3
#include <format>
4
#include <cmath>
5

6
#include "attribute_utils.h"
7
#include "svg_text_size.h"
8
#include "geometry_utils.h"
9
using namespace std;
10
using namespace svg_diagram;
11

12
const unordered_map<string_view, string>& SVGItem::attributes() const {
19,408✔
13
    return _attributes;
19,408✔
14
}
15

16
void SVGItem::setAttribute(const string_view& key, const string& value) {
2,362✔
17
    _attributes[key] = value;
2,362✔
18
}
2,362✔
19

UNCOV
20
void SVGItem::setAttributeIfNotExist(const string_view& key, const string& value) {
×
UNCOV
21
    if (!_attributes.contains(key)) {
×
UNCOV
22
        _attributes[key] = value;
×
23
    }
UNCOV
24
}
×
25

UNCOV
26
const string& SVGItem::getAttribute(const string_view& key) const {
×
UNCOV
27
    static const string EMPTY_STRING;
×
UNCOV
28
    if (const auto it = _attributes.find(key); it != _attributes.end()) {
×
UNCOV
29
        return it->second;
×
30
    }
UNCOV
31
    return EMPTY_STRING;
×
32
}
33

34
void SVGItem::setPrecomputedTextSize(const double width, const double height) {
52✔
35
    _precomputedTextWidth = width;
52✔
36
    _precomputedTextHeight = height;
52✔
37
}
52✔
38

39
pair<double, double> SVGItem::precomputedTextSize() const {
300✔
40
    return {_precomputedTextWidth, _precomputedTextHeight};
300✔
41
}
42

43
const string& SVGItem::id() const {
495✔
44
    const auto it = _attributes.find(DOT_ATTR_KEY_ID);
495✔
45
    if (it == _attributes.end()) {
495✔
NEW
46
        throw runtime_error("Attribute 'ID' not found");
×
47
    }
48
    return it->second;
990✔
49
}
50

51
void SVGItem::setID(const string& id) {
261✔
52
    setAttribute(DOT_ATTR_KEY_ID, id);
261✔
53
}
261✔
54

55
void SVGItem::setLabel(const string& label) {
202✔
56
    setAttribute(DOT_ATTR_KEY_LABEL, label);
202✔
57
}
202✔
58

59
void SVGItem::setMargin(const string& value) {
59✔
60
    setAttribute(DOT_ATTR_KEY_MARGIN, value);
59✔
61
}
59✔
62

63
void SVGItem::setMargin(const double margin) {
8✔
64
    setMargin(format("{}", margin / POINTS_PER_INCH));
8✔
65
}
8✔
66

67
void SVGItem::setMargin(const double marginX, const double marginY) {
51✔
68
    setMargin(format("{},{}", marginX / POINTS_PER_INCH, marginY / POINTS_PER_INCH));
51✔
69
}
51✔
70

71
void SVGItem::setColor(const string& color) {
43✔
72
    setAttribute(DOT_ATTR_KEY_COLOR, color);
43✔
73
}
43✔
74

75
void SVGItem::setFillColor(const string& color) {
18✔
76
    setAttribute(DOT_ATTR_KEY_FILL_COLOR, color);
18✔
77
}
18✔
78

79
void SVGItem::setFontColor(const string& color) {
45✔
80
    setAttribute(DOT_ATTR_KEY_FONT_COLOR, color);
45✔
81
}
45✔
82

83
void SVGItem::setPenWidth(const double width) {
13✔
84
    setAttribute(DOT_ATTR_KEY_PEN_WIDTH, format("{}", width));
13✔
85
}
13✔
86

87
double SVGItem::penWidth() const {
947✔
88
    const auto value = getAttribute(DOT_ATTR_KEY_PEN_WIDTH);
947✔
89
    if (!value.empty()) {
947✔
90
        const auto width = stod(value);
48✔
91
        if (fabs(width - 1.0) < GeometryUtils::EPSILON) {
48✔
92
            return 1.0;
5✔
93
        }
94
        return width;
43✔
95
    }
96
    return 1.0;
899✔
97
}
947✔
98

99
void SVGItem::appendSVGDrawsLabelWithCenter(vector<unique_ptr<SVGDraw>>& svgDraws, const double cx, const double cy) {
256✔
100
    if (enabledDebug()) {
256✔
101
        const auto [textWidth, textHeight] = computeTextSize();
34✔
102
        const auto [marginX, marginY] = computeMargin();
34✔
103
        auto textRect = make_unique<SVGDrawRect>(cx, cy, textWidth, textHeight);
34✔
104
        textRect->setFill("none");
68✔
105
        textRect->setStroke("blue");
68✔
106
        svgDraws.emplace_back(std::move(textRect));
34✔
107
        auto marginRect = make_unique<SVGDrawRect>(cx, cy, textWidth + marginX * 2, textHeight + marginY * 2);
34✔
108
        marginRect->setFill("none");
68✔
109
        marginRect->setStroke("red");
68✔
110
        svgDraws.emplace_back(std::move(marginRect));
34✔
111
    }
34✔
112
    if (const auto label = getAttribute(DOT_ATTR_KEY_LABEL); !label.empty()) {
256✔
113
        auto draw = make_unique<SVGDrawText>(cx, cy, label);
254✔
114
        if (const auto& fontColor = getAttribute(DOT_ATTR_KEY_FONT_COLOR); fontColor != "black") {
254✔
115
            draw->setFill(fontColor);
119✔
116
        }
117
        svgDraws.emplace_back(std::move(draw));
254✔
118
    }
510✔
119
}
256✔
120

121
pair<double, double> SVGItem::computeTextSize() {
300✔
122
    if (const auto [precomputedTextWidth, precomputedTextHeight] = precomputedTextSize();
300✔
123
        precomputedTextWidth > 0 && precomputedTextHeight > 0) {
300✔
124
        return {precomputedTextWidth, precomputedTextHeight};
136✔
125
        }
126
    const SVGTextSize textSize;
164✔
127
    const auto label = getAttribute(DOT_ATTR_KEY_LABEL);
164✔
128
    setAttributeIfNotExist(DOT_ATTR_KEY_FONT_NAME, "Times,serif");
328✔
129
    setAttributeIfNotExist(DOT_ATTR_KEY_FONT_SIZE, "14");
328✔
130
    const double fontSize = stod(getAttribute(DOT_ATTR_KEY_FONT_SIZE));
164✔
131
    const string fontFamily = getAttribute(DOT_ATTR_KEY_FONT_NAME);
164✔
132
    auto [width, height] = textSize.computeTextSize(label, fontSize, fontFamily);
164✔
133
    if (width == 0.0) {
164✔
134
        width = fontSize * SVGTextSize::DEFAULT_APPROXIMATION_WIDTH_SCALE;
4✔
135
    }
136
    if (height == 0.0) {
164✔
137
        height = fontSize * SVGTextSize::DEFAULT_APPROXIMATION_HEIGHT_SCALE;
4✔
138
    }
139
    return {width, height};
164✔
140
}
164✔
141

142
pair<double, double> SVGItem::computeMargin() {
300✔
143
    setAttributeIfNotExist(DOT_ATTR_KEY_MARGIN, "0.1111111111111111,0.05555555555555555");
600✔
144
    const auto margin = getAttribute(DOT_ATTR_KEY_MARGIN);
300✔
145
    return AttributeUtils::parseMargin(margin);
600✔
146
}
300✔
147

148
std::pair<double, double> SVGItem::computeTextSizeWithMargin() {
238✔
149
    const auto [width, height] = computeTextSize();
238✔
150
    const auto [marginX, marginY] = computeMargin();
238✔
151
    return {width + marginX * 2, height + marginY * 2};
238✔
152
}
153

154
SVGItem::SVGItem(const string& id) {
2✔
155
    setID(id);
2✔
156
}
2✔
157

158
void SVGItem::enableDebug() {
52✔
159
    _enabledDebug = true;
52✔
160
}
52✔
161

162
bool SVGItem::enabledDebug() const {
586✔
163
    return _enabledDebug;
586✔
164
}
165

166
void SVGItem::setParent(SVGGraph* parent) {
261✔
167
    _parent = parent;
261✔
168
}
261✔
169

170
SVGGraph * SVGItem::parent() const {
8,265✔
171
    return _parent;
8,265✔
172
}
173

174
SVGNode::SVGNode(const double cx, const double cy) {
50✔
175
    _cx = cx;
50✔
176
    _cy = cy;
50✔
177
}
50✔
178

179
void SVGNode::setAttributeIfNotExist(const std::string_view &key, const std::string &value) {
1,492✔
180
    if (attributes().contains(key)) {
1,492✔
181
        return;
952✔
182
    }
183
    if (parent() != nullptr) {
540✔
184
        if (const auto ret = parent()->defaultNodeAttribute(key); ret.has_value()) {
540✔
185
            return;
100✔
186
        }
187
    }
188
    setAttribute(key, value);
440✔
189
}
190

191
const string& SVGNode::getAttribute(const std::string_view& key) const {
3,283✔
192
    static const string EMPTY_STRING;
3,283✔
193
    if (const auto it = attributes().find(key); it != attributes().end()) {
3,283✔
194
        return it->second;
2,524✔
195
    }
196
    if (parent() != nullptr) {
759✔
197
        if (const auto ret = parent()->defaultNodeAttribute(key); ret.has_value()) {
759✔
198
            return ret.value();
108✔
199
        }
200
    }
201
    return EMPTY_STRING;
651✔
202
}
203

204
void SVGNode::setShape(const string& shape) {
93✔
205
    setAttribute(DOT_ATTR_KEY_SHAPE, shape);
93✔
206
}
93✔
207

208
void SVGNode::setShape(const string_view& shape) {
93✔
209
    setShape(string(shape));
93✔
210
}
93✔
211

212
void SVGNode::setCenter(const double cx, const double cy) {
54✔
213
    _cx = cx;
54✔
214
    _cy = cy;
54✔
215
}
54✔
216

217
pair<double, double> SVGNode::center() const {
140✔
218
    return {_cx, _cy};
140✔
219
}
220

221
void SVGNode::adjustNodeSize() {
169✔
222
    setAttributeIfNotExist(DOT_ATTR_KEY_SHAPE, string(NODE_SHAPE_DEFAULT));
169✔
223
    const auto shape = getAttribute(DOT_ATTR_KEY_SHAPE);
169✔
224
    if (shape == NODE_SHAPE_CIRCLE) {
169✔
225
        adjustNodeSizeCircle();
104✔
226
    } else if (shape == NODE_SHAPE_NONE || shape == NODE_SHAPE_RECT) {
65✔
227
        adjustNodeSizeRect();
37✔
228
    } else if (shape == NODE_SHAPE_ELLIPSE) {
28✔
229
        adjustNodeSizeEllipse();
28✔
230
    }
231
}
169✔
232

233
vector<unique_ptr<SVGDraw>> SVGNode::produceSVGDraws() {
159✔
234
    setAttributeIfNotExist(DOT_ATTR_KEY_SHAPE, string(NODE_SHAPE_DEFAULT));
318✔
235
    setAttributeIfNotExist(DOT_ATTR_KEY_COLOR, "black");
318✔
236
    setAttributeIfNotExist(DOT_ATTR_KEY_FILL_COLOR, "none");
318✔
237
    setAttributeIfNotExist(DOT_ATTR_KEY_FONT_COLOR, "black");
159✔
238
    const auto shape = getAttribute(DOT_ATTR_KEY_SHAPE);
159✔
239
    if (shape == NODE_SHAPE_NONE) {
159✔
240
        return produceSVGDrawsNone();
1✔
241
    }
242
    if (shape == NODE_SHAPE_CIRCLE) {
158✔
243
        return produceSVGDrawsCircle();
104✔
244
    }
245
    if (shape == NODE_SHAPE_RECT) {
54✔
246
        return produceSVGDrawsRect();
28✔
247
    }
248
    if (shape == NODE_SHAPE_ELLIPSE) {
26✔
249
        return produceSVGDrawsEllipse();
26✔
250
    }
251
    return {};
×
252
}
159✔
253

254
pair<double, double> SVGNode::computeConnectionPoint(const double angle) {
342✔
255
    setAttributeIfNotExist(DOT_ATTR_KEY_SHAPE, string(NODE_SHAPE_DEFAULT));
342✔
256
    const auto shape = getAttribute(DOT_ATTR_KEY_SHAPE);
342✔
257
    if (shape == NODE_SHAPE_CIRCLE) {
342✔
258
        return computeConnectionPointCircle(angle);
108✔
259
    }
260
    if (shape == NODE_SHAPE_RECT) {
234✔
261
        return computeConnectionPointRect(angle);
116✔
262
    }
263
    if (shape == NODE_SHAPE_ELLIPSE) {
118✔
264
        return computeConnectionPointEllipse(angle);
118✔
265
    }
266
    return {0.0, 0.0};
×
267
}
342✔
268

269
/*
270
pair<double, double> SVGNode::computeConnectionPoint(const double x1, const double y1, const double x2, const double y2) {
271
    return computeConnectionPoint(atan2(y2 - y1, x2 - x1));
272
}
273

274
pair<double, double> SVGNode::computeConnectionPoint(const double x, const double y) {
275
    return computeConnectionPoint(_cx, _cy, x, y);
276
}
277

278
pair<double, double> SVGNode::computeConnectionPoint(const pair<double, double>& p) {
279
    return computeConnectionPoint(p.first, p.second);
280
}
281
*/
282

283
double SVGNode::computeAngle(const double x, const double y) const {
342✔
284
    return atan2(y - _cy, x - _cx);
342✔
285
}
286

287
double SVGNode::computeAngle(const pair<double, double>& p) const {
342✔
288
    return computeAngle(p.first, p.second);
342✔
289
}
290

291
bool SVGNode::isFixedSize() const {
169✔
292
    return AttributeUtils::parseBool(getAttribute(DOT_ATTR_KEY_FIXED_SIZE));
169✔
293
}
294

295
void SVGNode::updateNodeSize(const double width, const double height) {
169✔
296
    const auto widthString = format("{}", width);
169✔
297
    const auto heightString = format("{}", height);
169✔
298
    if (isFixedSize()) {
169✔
299
        setAttributeIfNotExist(DOT_ATTR_KEY_WIDTH, widthString);
×
300
        setAttributeIfNotExist(DOT_ATTR_KEY_HEIGHT, heightString);
×
301
    } else {
302
        setAttribute(DOT_ATTR_KEY_WIDTH, widthString);
169✔
303
        setAttribute(DOT_ATTR_KEY_HEIGHT, heightString);
169✔
304
    }
305
}
169✔
306

307
void SVGNode::updateNodeSize(const pair<double, double>& size) {
37✔
308
    updateNodeSize(size.first, size.second);
37✔
309
}
37✔
310

311
void SVGNode::appendSVGDrawsLabel(vector<unique_ptr<SVGDraw>>& svgDraws) {
159✔
312
    appendSVGDrawsLabelWithCenter(svgDraws, _cx, _cy);
159✔
313
}
159✔
314

315
vector<unique_ptr<SVGDraw>> SVGNode::produceSVGDrawsNone() {
1✔
316
    vector<unique_ptr<SVGDraw>> svgDraws;
1✔
317
    appendSVGDrawsLabel(svgDraws);
1✔
318
    return svgDraws;
1✔
319
}
×
320

321
void SVGNode::adjustNodeSizeCircle() {
104✔
322
    const auto diameter = GeometryUtils::distance(computeTextSizeWithMargin());
104✔
323
    updateNodeSize(diameter, diameter);
104✔
324
}
104✔
325

326
vector<unique_ptr<SVGDraw>> SVGNode::produceSVGDrawsCircle() {
104✔
327
    const double width = stod(getAttribute(DOT_ATTR_KEY_WIDTH));
104✔
328
    const double height = stod(getAttribute(DOT_ATTR_KEY_WIDTH));
104✔
329
    vector<unique_ptr<SVGDraw>> svgDraws;
104✔
330
    auto circle = make_unique<SVGDrawCircle>(_cx, _cy, max(width, height) / 2.0);
104✔
331
    circle->setStroke(getAttribute(DOT_ATTR_KEY_COLOR));
104✔
332
    circle->setFill(getAttribute(DOT_ATTR_KEY_FILL_COLOR));
104✔
333
    if (const auto strokeWidth = penWidth(); strokeWidth != 1.0) {
104✔
334
        circle->setStrokeWidth(strokeWidth);
×
335
    }
336
    svgDraws.emplace_back(std::move(circle));
104✔
337
    appendSVGDrawsLabel(svgDraws);
104✔
338
    return svgDraws;
208✔
339
}
104✔
340

341
pair<double, double> SVGNode::computeConnectionPointCircle(const double angle) const {
108✔
342
    const double width = stod(getAttribute(DOT_ATTR_KEY_WIDTH));
108✔
343
    const double radius = (width + penWidth()) / 2.0;
108✔
344
    return {_cx + radius * cos(angle), _cy + radius * sin(angle)};
108✔
345
}
346

347
void SVGNode::adjustNodeSizeRect() {
37✔
348
    updateNodeSize(computeTextSizeWithMargin());
37✔
349
}
37✔
350

351
vector<unique_ptr<SVGDraw>> SVGNode::produceSVGDrawsRect() {
28✔
352
    const double width = stod(getAttribute(DOT_ATTR_KEY_WIDTH));
28✔
353
    const double height = stod(getAttribute(DOT_ATTR_KEY_HEIGHT));
28✔
354
    vector<unique_ptr<SVGDraw>> svgDraws;
28✔
355
    auto rect = make_unique<SVGDrawRect>(_cx, _cy, width, height);
28✔
356
    rect->setStroke(getAttribute(DOT_ATTR_KEY_COLOR));
28✔
357
    rect->setFill(getAttribute(DOT_ATTR_KEY_FILL_COLOR));
28✔
358
    if (const auto strokeWidth = penWidth(); strokeWidth != 1.0) {
28✔
359
        rect->setStrokeWidth(strokeWidth);
2✔
360
    }
361
    svgDraws.emplace_back(std::move(rect));
28✔
362
    appendSVGDrawsLabel(svgDraws);
28✔
363
    return svgDraws;
56✔
364
}
28✔
365

366
pair<double, double> SVGNode::computeConnectionPointRect(const double angle) const {
116✔
367
    const double strokeWidth = penWidth();
116✔
368
    const double width = stod(getAttribute(DOT_ATTR_KEY_WIDTH)) + strokeWidth;
116✔
369
    const double height = stod(getAttribute(DOT_ATTR_KEY_HEIGHT)) + strokeWidth;
116✔
370
    double x1 = -width / 2, y1 = -height / 2;
116✔
371
    double x2 = width / 2, y2 = height / 2;
116✔
372
    const auto vertices = vector<pair<double, double>>{{x1, y1}, {x2, y1}, {x2, y2}, {x1, y2}};
232✔
373
    for (const auto& [x, y] : vertices) {
580✔
374
        if (GeometryUtils::isSameAngle(angle, x, y)) {
464✔
375
            return {_cx + x, _cy + y};
×
376
        }
377
    }
378
    for (int i = 0; i < static_cast<int>(vertices.size()); ++i) {
266✔
379
        x1 = vertices[i].first;
266✔
380
        y1 = vertices[i].second;
266✔
381
        x2 = vertices[(i + 1) % vertices.size()].first;
266✔
382
        y2 = vertices[(i + 1) % vertices.size()].second;
266✔
383
        if (const auto intersect = GeometryUtils::intersect(angle, x1, y1, x2, y2); intersect != nullopt) {
266✔
384
            return {_cx + intersect.value().first, _cy + intersect.value().second};
116✔
385
        }
386
    }
387
    return {_cx, _cy};
×
388
}
116✔
389

390
void SVGNode::adjustNodeSizeEllipse() {
28✔
391
    const auto [textWidth, textHeight] = computeTextSize();
28✔
392
    const auto [marginX, marginY] = computeMargin();
28✔
393
    updateNodeSize((textWidth + marginX * 2) * sqrt(2.0), (textHeight + marginY * 2) * sqrt(2.0));
28✔
394
}
28✔
395

396
vector<unique_ptr<SVGDraw>> SVGNode::produceSVGDrawsEllipse() {
26✔
397
    const double width = stod(getAttribute(DOT_ATTR_KEY_WIDTH));
26✔
398
    const double height = stod(getAttribute(DOT_ATTR_KEY_HEIGHT));
26✔
399
    vector<unique_ptr<SVGDraw>> svgDraws;
26✔
400
    auto ellipse = make_unique<SVGDrawEllipse>(_cx, _cy, width, height);
26✔
401
    ellipse->setStroke(getAttribute(DOT_ATTR_KEY_COLOR));
26✔
402
    ellipse->setFill(getAttribute(DOT_ATTR_KEY_FILL_COLOR));
26✔
403
    if (const auto strokeWidth = penWidth(); strokeWidth != 1.0) {
26✔
404
        ellipse->setStrokeWidth(strokeWidth);
6✔
405
    }
406
    svgDraws.emplace_back(std::move(ellipse));
26✔
407
    appendSVGDrawsLabel(svgDraws);
26✔
408
    return svgDraws;
52✔
409
}
26✔
410

411
pair<double, double> SVGNode::computeConnectionPointEllipse(const double angle) const {
118✔
412
    const double strokeWidth = penWidth();
118✔
413
    const double width = stod(getAttribute(DOT_ATTR_KEY_WIDTH)) + strokeWidth;
118✔
414
    const double height = stod(getAttribute(DOT_ATTR_KEY_HEIGHT)) + strokeWidth;
118✔
415
    const double rx = width / 2, ry = height / 2;
118✔
416
    const double base = sqrt(ry * ry * cos(angle) * cos(angle) + rx * rx * sin(angle) * sin(angle));
118✔
417
    const double x = rx * ry * cos(angle) / base;
118✔
418
    const double y = rx * ry * sin(angle) / base;
118✔
419
    return {_cx + x, _cy + y};
118✔
420
}
421

422
SVGEdge::SVGEdge(const string& idFrom, const string& idTo) {
23✔
423
    _nodeFrom = idFrom;
23✔
424
    _nodeTo = idTo;
23✔
425
}
23✔
426

427
void SVGEdge::setAttributeIfNotExist(const std::string_view &key, const std::string &value) {
1,138✔
428
    if (attributes().contains(key)) {
1,138✔
429
        return;
354✔
430
    }
431
    if (parent() != nullptr) {
784✔
432
        if (const auto ret = parent()->defaultEdgeAttribute(key); ret.has_value()) {
784✔
433
            return;
63✔
434
        }
435
    }
436
    setAttribute(key, value);
721✔
437
}
438

439
const string& SVGEdge::getAttribute(const string_view& key) const {
2,166✔
440
    static const string EMPTY_STRING;
2,166✔
441
    if (const auto it = attributes().find(key); it != attributes().end()) {
2,166✔
442
        return it->second;
1,542✔
443
    }
444
    if (parent() != nullptr) {
624✔
445
        if (const auto ret = parent()->defaultEdgeAttribute(key); ret.has_value()) {
624✔
446
            return ret.value();
51✔
447
        }
448
    }
449
    return EMPTY_STRING;
573✔
450
}
451

452
void SVGEdge::setNodeFrom(const string& id) {
7✔
453
    _nodeFrom = id;
7✔
454
}
7✔
455

456
const string& SVGEdge::nodeFrom() const {
342✔
457
    return _nodeFrom;
342✔
458
}
459

460
void SVGEdge::setNodeTo(const string& id) {
7✔
461
    _nodeTo = id;
7✔
462
}
7✔
463

464
const string& SVGEdge::nodeTo() const {
342✔
465
    return _nodeTo;
342✔
466
}
467

468
void SVGEdge::setConnection(const string& idFrom, const string& idTo) {
117✔
469
    _nodeFrom = idFrom;
117✔
470
    _nodeTo = idTo;
117✔
471
}
117✔
472

473
void SVGEdge::setSplines(const string& value) {
24✔
474
    setAttribute(DOT_ATTR_KEY_SPLINES, value);
24✔
475
}
24✔
476

477
void SVGEdge::setSplines(const string_view& value) {
24✔
478
    setSplines(string(value));
24✔
479
}
24✔
480

481
void SVGEdge::addConnectionPoint(const pair<double, double>& point) {
129✔
482
    _connectionPoints.emplace_back(point);
129✔
483
}
129✔
484

485
void SVGEdge::addConnectionPoint(const double x, const double y) {
129✔
486
    addConnectionPoint({x, y});
129✔
487
}
129✔
488

489
vector<unique_ptr<SVGDraw>> SVGEdge::produceSVGDraws(const NodesMapping& nodes) {
171✔
490
    setAttributeIfNotExist(DOT_ATTR_KEY_SPLINES, string(EDGE_SPLINES_DEFAULT));
342✔
491
    setAttributeIfNotExist(DOT_ATTR_KEY_COLOR, "black");
342✔
492
    setAttributeIfNotExist(DOT_ATTR_KEY_ARROW_HEAD, "none");
342✔
493
    setAttributeIfNotExist(DOT_ATTR_KEY_ARROW_TAIL, "none");
342✔
494
    setAttributeIfNotExist(DOT_ATTR_KEY_MARGIN, "0,0");
171✔
495
    const auto splines = getAttribute(DOT_ATTR_KEY_SPLINES);
171✔
496
    if (splines == EDGE_SPLINES_LINE) {
171✔
497
        return produceSVGDrawsLine(nodes);
24✔
498
    }
499
    if (splines == EDGE_SPLINES_SPLINE) {
147✔
500
        return produceSVGDrawsSpline(nodes);
147✔
501
    }
502
    return {};
×
503
}
171✔
504

505
void SVGEdge::setArrowHead() {
72✔
506
    setArrowHead(ARROW_SHAPE_DEFAULT);
72✔
507
}
72✔
508

509
void SVGEdge::setArrowHead(const string_view& shape) {
80✔
510
    setAttribute(DOT_ATTR_KEY_ARROW_HEAD, string(shape));
160✔
511
}
80✔
512

513
void SVGEdge::setArrowTail() {
17✔
514
    setArrowTail(ARROW_SHAPE_DEFAULT);
17✔
515
}
17✔
516

517
void SVGEdge::setArrowTail(const string_view& shape) {
25✔
518
    setAttribute(DOT_ATTR_KEY_ARROW_TAIL, string(shape));
50✔
519
}
25✔
520

521
std::pair<double, double> SVGEdge::computeTextCenter(const double cx, const double cy, double dx, double dy) {
97✔
522
    const auto [width, height] = computeTextSizeWithMargin();
97✔
523
    const auto points = vector<pair<double, double>>{
524
        {cx - width / 2, cy - height / 2},
×
525
        {cx - width / 2, cy + height / 2},
×
526
        {cx + width / 2, cy + height / 2},
×
527
        {cx + width / 2, cy - height / 2},
×
528
    };
194✔
529
    const auto d = GeometryUtils::normalize(dx, dy);
97✔
530
    dx = d.first, dy = d.second;
97✔
531
    const double ux = -dy, uy = dx;
97✔
532
    double maxShift = 0.0;
97✔
533
    for (const auto& [x, y] : points) {
485✔
534
        const double totalArea = GeometryUtils::cross(x - cx, y - cy, dx, dy);
388✔
535
        const double unitArea = GeometryUtils::cross(dx, dy, ux, uy);
388✔
536
        const double shift = totalArea / unitArea;
388✔
537
        maxShift = max(maxShift, shift);
388✔
538
    }
539
    return {cx + ux * maxShift, cy + uy * maxShift};
194✔
540
}
97✔
541

542
vector<unique_ptr<SVGDraw>> SVGEdge::produceSVGDrawsLine(const NodesMapping& nodes) {
84✔
543
    const auto& nodeFrom = nodes.at(_nodeFrom);
84✔
544
    const auto& nodeTo = nodes.at(_nodeTo);
84✔
545
    const auto arrowHeadShape = getAttribute(DOT_ATTR_KEY_ARROW_HEAD);
84✔
546
    const auto arrowTailShape = getAttribute(DOT_ATTR_KEY_ARROW_TAIL);
84✔
547
    vector<unique_ptr<SVGDraw>> svgDraws;
84✔
548
    vector<unique_ptr<SVGDraw>> svgDrawArrows;
84✔
549
    vector<pair<double, double>> points;
84✔
550
    if (_connectionPoints.empty()) {
84✔
551
        const double angleFrom = nodeFrom->computeAngle(nodeTo->center());
70✔
552
        const double angleTo = nodeTo->computeAngle(nodeFrom->center());
70✔
553
        points.emplace_back(addArrow(arrowTailShape, svgDrawArrows, nodeFrom->computeConnectionPoint(angleFrom), angleFrom));
70✔
554
        points.emplace_back(addArrow(arrowHeadShape, svgDrawArrows, nodeTo->computeConnectionPoint(angleTo), angleTo));
70✔
555
    } else {
556
        const double angleFrom = nodeFrom->computeAngle(_connectionPoints[0]);
14✔
557
        points.emplace_back(addArrow(arrowTailShape, svgDrawArrows, nodeFrom->computeConnectionPoint(angleFrom), angleFrom));
14✔
558
        for (const auto& [x, y] : _connectionPoints) {
36✔
559
            points.emplace_back(x, y);
22✔
560
        }
561
        const size_t n = _connectionPoints.size();
14✔
562
        const double angleTo = nodeTo->computeAngle(_connectionPoints[n - 1]);
14✔
563
        points.emplace_back(addArrow(arrowHeadShape, svgDrawArrows, nodeTo->computeConnectionPoint(angleTo), angleTo));
14✔
564
    }
565
    for (size_t i = 0; i + 1 < points.size(); ++i) {
190✔
566
        const auto& [x1, y1] = points[i];
106✔
567
        const auto& [x2, y2] = points[i + 1];
106✔
568
        svgDraws.emplace_back(make_unique<SVGDrawLine>(x1, y1, x2, y2));
106✔
569
    }
570
    const auto strokeWidth = penWidth();
84✔
571
    for (const auto& line : svgDraws) {
190✔
572
        const auto& draw = dynamic_cast<SVGDrawLine*>(line.get());
106✔
573
        draw->setStroke(getAttribute(DOT_ATTR_KEY_COLOR));
106✔
574
        if (strokeWidth != 1.0) {
106✔
575
            draw->setStrokeWidth(strokeWidth);
3✔
576
        }
577
    }
578
    for (auto& arrow : svgDrawArrows) {
124✔
579
        svgDraws.emplace_back(std::move(arrow));
40✔
580
    }
581
    if (const auto label = getAttribute(DOT_ATTR_KEY_LABEL); !label.empty()) {
84✔
582
        double totalLength = 0.0;
22✔
583
        for (size_t i = 0; i + 1 < points.size(); ++i) {
46✔
584
            const auto& [x1, y1] = points[i];
24✔
585
            const auto& [x2, y2] = points[i + 1];
24✔
586
            totalLength += GeometryUtils::distance(x1, y1, x2, y2);
24✔
587
        }
588
        const double halfLength = totalLength / 2.0;
22✔
589
        double sumLength = 0.0;
22✔
590
        size_t index = 0;
22✔
591
        double lineX = 0.0, lineY = 0.0;
22✔
592
        for (size_t i = 0; i + 1 < points.size(); ++i) {
24✔
593
            const auto& [x1, y1] = points[i];
24✔
594
            const auto& [x2, y2] = points[i + 1];
24✔
595
            const double length = GeometryUtils::distance(x1, y1, x2, y2);
24✔
596
            const double nextSum = sumLength + length;
24✔
597
            if (nextSum > halfLength) {
24✔
598
                index = i;
22✔
599
                const double ratio = (halfLength - sumLength) / length;
22✔
600
                lineX = x1 + ratio * (x2 - x1);
22✔
601
                lineY = y1 + ratio * (y2 - y1);
22✔
602
                break;
22✔
603
            }
604
            sumLength = nextSum;
2✔
605
        }
606
        const double dx = points[index + 1].first - points[index].first;
22✔
607
        const double dy = points[index + 1].second - points[index].second;
22✔
608
        const auto [cx, cy] = computeTextCenter(lineX, lineY, dx, dy);
22✔
609
        appendSVGDrawsLabelWithCenter(svgDraws, cx, cy);
22✔
610
    }
84✔
611
    return svgDraws;
168✔
612
}
84✔
613

614
vector<unique_ptr<SVGDraw>> SVGEdge::produceSVGDrawsSpline(const NodesMapping& nodes) {
147✔
615
    static constexpr double NUM_SPLINE_LENGTH_APPROXIMATION_SEGMENTS = 100;
616
    constexpr double SPLINE_LENGTH_APPROXIMATION_STEP = 1.0 / NUM_SPLINE_LENGTH_APPROXIMATION_SEGMENTS;
147✔
617
    if (_connectionPoints.empty()) {
147✔
618
        return produceSVGDrawsLine(nodes);
60✔
619
    }
620
    const auto& nodeFrom = nodes.at(_nodeFrom);
87✔
621
    const auto& nodeTo = nodes.at(_nodeTo);
87✔
622
    const auto arrowHeadShape = getAttribute(DOT_ATTR_KEY_ARROW_HEAD);
87✔
623
    const auto arrowTailShape = getAttribute(DOT_ATTR_KEY_ARROW_TAIL);
87✔
624
    vector<unique_ptr<SVGDraw>> svgDraws;
87✔
625
    vector<unique_ptr<SVGDraw>> svgDrawArrows;
87✔
626
    const auto strokeWidth = penWidth();
87✔
627
    vector<pair<double, double>> points;
87✔
628
    const double angleFrom = nodeFrom->computeAngle(_connectionPoints[0]);
87✔
629
    auto [sx, sy] = addArrow(arrowTailShape, svgDrawArrows, nodeFrom->computeConnectionPoint(angleFrom), angleFrom);
87✔
630
    points.emplace_back(sx, sy);
87✔
631
    points.emplace_back(sx, sy);
87✔
632
    for (const auto& [x, y] : _connectionPoints) {
216✔
633
        points.emplace_back(x, y);
129✔
634
    }
635
    const double angleTo = nodeTo->computeAngle(_connectionPoints[_connectionPoints.size() - 1]);
87✔
636
    auto [ex, ey] = addArrow(arrowHeadShape, svgDrawArrows, nodeTo->computeConnectionPoint(angleTo), angleTo);
87✔
637
    points.emplace_back(ex, ey);
87✔
638
    points.emplace_back(ex, ey);
87✔
639
    auto d = format("M {} {}", points[1].first, points[1].second);
87✔
640
    vector<vector<pair<double, double>>> splines;
87✔
641
    for (int i = 1; i + 2 < static_cast<int>(points.size()); ++i) {
303✔
642
        const auto [x0, y0] = points[i - 1];
216✔
643
        const auto [x1, y1] = points[i];
216✔
644
        const auto [x2, y2] = points[i + 1];
216✔
645
        const auto [x3, y3] = points[i + 2];
216✔
646
        const double c1x = x1 + (x2 - x0) / 6.0;
216✔
647
        const double c1y = y1 + (y2 - y0) / 6.0;
216✔
648
        const double c2x = x2 - (x3 - x1) / 6.0;
216✔
649
        const double c2y = y2 - (y3 - y1) / 6.0;
216✔
650
        d += format(" C {} {} {} {} {} {}", c1x, c1y, c2x, c2y, x2, y2);
216✔
651
        splines.emplace_back(vector{points[i], {c1x, c1y}, {c2x, c2y}, points[i + 1]});
648✔
652
    }
653
    auto path = make_unique<SVGDrawPath>(d);
87✔
654
    path->setStroke(getAttribute(DOT_ATTR_KEY_COLOR));
87✔
655
    path->setFill("none");
174✔
656
    if (strokeWidth != 1.0) {
87✔
657
        path->setStrokeWidth(strokeWidth);
4✔
658
    }
659
    svgDraws.emplace_back(std::move(path));
87✔
660
    for (auto& arrow : svgDrawArrows) {
185✔
661
        svgDraws.emplace_back(std::move(arrow));
98✔
662
    }
663
    if (const auto label = getAttribute(DOT_ATTR_KEY_LABEL); !label.empty()) {
87✔
664
        double totalLength = 0.0;
75✔
665
        vector<double> lengths(splines.size());
75✔
666
        for (size_t i = 0; i < splines.size(); ++i) {
259✔
667
            lengths[i] = GeometryUtils::computeBezierLength(splines[i][0], splines[i][1], splines[i][2], splines[i][3]);
184✔
668
            totalLength += lengths[i];
184✔
669
        }
670
        const double halfLength = totalLength / 2.0;
75✔
671
        double sumLength = 0.0;
75✔
672
        double splineX = 0.0, splineY = 0.0;
75✔
673
        double dx = 0.0, dy = 0.0;
75✔
674
        for (size_t i = 0; i < splines.size(); ++i) {
138✔
675
            const double nextSum = sumLength + lengths[i];
138✔
676
            if (nextSum > halfLength) {
138✔
677
                const double targetLength = halfLength - sumLength;
75✔
678
                auto [x1, y1] = splines[i][0];
75✔
679
                double totalSegmentLength = 0.0;
75✔
680
                for (int j = 1; j < NUM_SPLINE_LENGTH_APPROXIMATION_SEGMENTS; ++j) {
3,034✔
681
                    const double t = j * SPLINE_LENGTH_APPROXIMATION_STEP;
3,034✔
682
                    const auto [x2, y2] = GeometryUtils::computeBezierAt(splines[i], t);
3,034✔
683
                    totalSegmentLength += GeometryUtils::distance(x1, y1, x2, y2);
3,034✔
684
                    if (j + 1 == NUM_SPLINE_LENGTH_APPROXIMATION_SEGMENTS || totalSegmentLength > targetLength - GeometryUtils::EPSILON) {
3,034✔
685
                        const double tMid = t - SPLINE_LENGTH_APPROXIMATION_STEP * 0.5;
75✔
686
                        auto point = GeometryUtils::computeBezierAt(splines[i], tMid);
75✔
687
                        splineX = point.first, splineY = point.second;
75✔
688
                        point = GeometryUtils::computeBezierDerivative(splines[i], tMid);
75✔
689
                        dx = point.first, dy = point.second;
75✔
690
                        break;
75✔
691
                    }
692
                    x1 = x2;
2,959✔
693
                    y1 = y2;
2,959✔
694
                }
695
                break;
75✔
696
            }
697
            sumLength = nextSum;
63✔
698
        }
699
        const auto [cx, cy] = computeTextCenter(splineX, splineY, dx, dy);
75✔
700
        appendSVGDrawsLabelWithCenter(svgDraws, cx, cy);
75✔
701
    }
162✔
702
    return svgDraws;
87✔
703
}
87✔
704

705
double SVGEdge::computeArrowTipMargin(const string_view& shape) const {
342✔
706
    if (shape == ARROW_SHAPE_NORMAL) {
342✔
707
        return computeArrowTipMarginNormal();
138✔
708
    }
709
    return 0.0;
204✔
710
}
711

712
double SVGEdge::computeArrowTipMarginNormal() const {
138✔
713
    const double angle = atan(ARROW_HALF_HEIGHT / ARROW_WIDTH);
138✔
714
    const double strokeWidth = penWidth();
138✔
715
    const double margin = strokeWidth / 2.0 / sin(angle);
138✔
716
    return margin;
138✔
717
}
718

719
pair<double, double> SVGEdge::addArrow(const string_view& shape, vector<unique_ptr<SVGDraw>>& svgDraws, const pair<double, double>& connectionPoint, const double angle) const {
342✔
720
    const double arrowTipMargin = computeArrowTipMargin(shape);
342✔
721
    const pair arrowTip = {connectionPoint.first + arrowTipMargin * cos(angle), connectionPoint.second + arrowTipMargin * sin(angle)};
342✔
722
    if (shape == ARROW_SHAPE_NORMAL) {
342✔
723
        return addArrowNormal(svgDraws, arrowTip, angle);
138✔
724
    }
725
    return {connectionPoint.first - 0.2 * cos(angle), connectionPoint.second - 0.2 * sin(angle)};
204✔
726
}
727

728
pair<double, double> SVGEdge::addArrowNormal(vector<unique_ptr<SVGDraw>>& svgDraws, const pair<double, double>& connectionPoint, const double angle) const {
138✔
729
    const double x0 = connectionPoint.first;
138✔
730
    const double y0 = connectionPoint.second;
138✔
731
    const double sideLen = GeometryUtils::distance(ARROW_WIDTH, ARROW_HALF_HEIGHT);
138✔
732
    const double halfAngle = atan(ARROW_HALF_HEIGHT / ARROW_WIDTH);
138✔
733
    const double x1 = x0 + sideLen * cos(angle - halfAngle);
138✔
734
    const double y1 = y0 + sideLen * sin(angle - halfAngle);
138✔
735
    const double x2 = x0 + sideLen * cos(angle + halfAngle);
138✔
736
    const double y2 = y0 + sideLen * sin(angle + halfAngle);
138✔
737
    auto polygon = make_unique<SVGDrawPolygon>(vector<pair<double, double>>{{x0, y0}, {x1, y1}, {x2, y2}, {x0, y0}});
276✔
738
    polygon->setStroke(getAttribute(DOT_ATTR_KEY_COLOR));
138✔
739
    polygon->setFill(getAttribute(DOT_ATTR_KEY_COLOR));
138✔
740
    if (const double strokeWidth = penWidth(); strokeWidth != 1.0) {
138✔
741
        polygon->setStrokeWidth(strokeWidth);
7✔
742
    }
743
    svgDraws.emplace_back(std::move(polygon));
138✔
744
    return {x0 + ARROW_WIDTH * cos(angle), y0 + ARROW_WIDTH * sin(angle)};
276✔
745
}
138✔
746

747
void SVGGraph::addNode(shared_ptr<SVGNode>& node) {
110✔
748
    node->setParent(this);
110✔
749
    _nodes.emplace_back(node);
110✔
750
}
110✔
751

752
void SVGGraph::addEdge(shared_ptr<SVGEdge>& edge) {
147✔
753
    edge->setParent(this);
147✔
754
    _edges.emplace_back(edge);
147✔
755
}
147✔
756

757
void SVGGraph::addSubgraph(shared_ptr<SVGGraph>& subgraph) {
4✔
758
    subgraph->setParent(this);
4✔
759
    _graphs.emplace_back(subgraph);
4✔
760
}
4✔
761

762
SVGNode& SVGGraph::defaultNodeAttributes() {
8✔
763
    return _defaultNode;
8✔
764
}
765

766
SVGEdge& SVGGraph::defaultEdgeAttributes() {
9✔
767
    return _defaultEdge;
9✔
768
}
769

770
optional<reference_wrapper<const string>> SVGGraph::defaultNodeAttribute(const string_view& key) const {
1,421✔
771
    if (const auto it = _defaultNode.attributes().find(key); it != _defaultNode.attributes().end()) {
1,421✔
772
        return std::ref(it->second);
208✔
773
    }
774
    if (parent() != nullptr) {
1,213✔
775
        return parent()->defaultNodeAttribute(key);
122✔
776
    }
777
    return {};
1,091✔
778
}
779

780
optional<reference_wrapper<const string>> SVGGraph::defaultEdgeAttribute(const string_view& key) const {
1,519✔
781
    if (const auto it = _defaultEdge.attributes().find(key); it != _defaultEdge.attributes().end()) {
1,519✔
782
        return it->second;
114✔
783
    }
784
    if (parent() != nullptr) {
1,405✔
785
        return parent()->defaultEdgeAttribute(key);
111✔
786
    }
787
    return {};
1,294✔
788
}
789

790
void SVGGraph::adjustNodeSizes() const {
94✔
791
    for (auto& node : _nodes) {
263✔
792
        node->adjustNodeSize();
169✔
793
    }
794
    for (auto& graph : _graphs) {
100✔
795
        graph->adjustNodeSizes();
6✔
796
    }
797
}
94✔
798

799
vector<unique_ptr<SVGDraw>> SVGGraph::produceSVGDraws(const NodesMapping &nodes) const {
84✔
800
    vector<unique_ptr<SVGDraw>> svgDraws;
84✔
801
    for (auto& draw : produceNodeSVGDraws()) {
402✔
802
        svgDraws.emplace_back(std::move(draw));
318✔
803
    }
84✔
804
    for (auto& draw : produceEdgeSVGDraws(nodes)) {
426✔
805
        svgDraws.emplace_back(std::move(draw));
342✔
806
    }
84✔
807
    return svgDraws;
84✔
NEW
808
}
×
809

810
vector<shared_ptr<SVGNode>> SVGGraph::findNodes() const {
88✔
811
    vector<shared_ptr<SVGNode>> nodes = _nodes;
88✔
812
    for (auto& graph : _graphs) {
92✔
813
        for (auto& node : graph->findNodes()) {
14✔
814
            nodes.emplace_back(std::move(node));
10✔
815
        }
4✔
816
    }
817
    return nodes;
88✔
NEW
818
}
×
819

NEW
820
vector<shared_ptr<SVGEdge>> SVGGraph::findEdges() const {
×
NEW
821
    vector<shared_ptr<SVGEdge>> edges = _edges;
×
NEW
822
    for (auto& graph : _graphs) {
×
NEW
823
        for (auto& edge : graph->findEdges()) {
×
NEW
824
            edges.emplace_back(std::move(edge));
×
NEW
825
        }
×
826
    }
NEW
827
    return edges;
×
NEW
828
}
×
829

830
vector<unique_ptr<SVGDraw>> SVGGraph::produceNodeSVGDraws() const {
88✔
831
    adjustNodeSizes();
88✔
832
    vector<unique_ptr<SVGDraw>> svgDraws;
88✔
833
    for (auto& node : _nodes) {
247✔
834
        if (enabledDebug()) {
159✔
835
            node->enableDebug();
26✔
836
        }
837
        const auto& id = node->id();
159✔
838
        svgDraws.emplace_back(make_unique<SVGDrawComment>(format("Node: {}", id)));
159✔
839
        auto group = make_unique<SVGDrawGroup>();
159✔
840
        group->setAttribute("id", id);
159✔
841
        group->setAttribute("class", "node");
318✔
842
        group->addChild(make_unique<SVGDrawTitle>(id));
159✔
843
        auto subDraws = node->produceSVGDraws();
159✔
844
        group->addChildren(subDraws);
159✔
845
        svgDraws.emplace_back(std::move(group));
159✔
846
    }
159✔
847
    for (auto& graph : _graphs) {
92✔
848
        for (auto subDraws = graph->produceNodeSVGDraws(); auto& subDraw : subDraws) {
24✔
849
            svgDraws.emplace_back(std::move(subDraw));
20✔
850
        }
4✔
851
    }
852
    return svgDraws;
88✔
NEW
853
}
×
854

855
vector<unique_ptr<SVGDraw>> SVGGraph::produceEdgeSVGDraws(const NodesMapping& nodes) const {
88✔
856
    vector<unique_ptr<SVGDraw>> svgDraws;
88✔
857
    for (auto& edge : _edges) {
259✔
858
        if (enabledDebug()) {
171✔
859
            edge->enableDebug();
8✔
860
        }
861
        const auto& id = edge->id();
171✔
862
        svgDraws.emplace_back(make_unique<SVGDrawComment>(format("Edge: {} ({} -> {})", id, edge->nodeFrom(), edge->nodeTo())));
171✔
863
        auto group = make_unique<SVGDrawGroup>();
171✔
864
        group->setAttribute("id", id);
171✔
865
        group->setAttribute("class", "edge");
342✔
866
        group->addChild(make_unique<SVGDrawTitle>(format("{}->{}", edge->nodeFrom(), edge->nodeTo())));
171✔
867
        auto subDraws = edge->produceSVGDraws(nodes);
171✔
868
        group->addChildren(subDraws);
171✔
869
        svgDraws.emplace_back(std::move(group));
171✔
870
    }
171✔
871
    for (auto& graph : _graphs) {
92✔
872
        for (auto subDraws = graph->produceEdgeSVGDraws(nodes); auto& subDraw : subDraws) {
24✔
873
            svgDraws.emplace_back(std::move(subDraw));
20✔
874
        }
4✔
875
    }
876
    return svgDraws;
88✔
NEW
877
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc