yuml-diagram
Version:
UML diagramming package based on the yUML syntax
304 lines (240 loc) • 8.8 kB
JavaScript
var svg_builder = require('../utils/svg-builder.js');
/*
Rendering algorithms based on:
https://github.com/sharvil/node-sequence-diagram
*/
module.exports = function(actors, signals, uids, isDark)
{
var DIAGRAM_MARGIN = 10;
var ACTOR_MARGIN = 10; // Margin around a actor
var ACTOR_PADDING = 10; // Padding inside a actor
var SIGNAL_MARGIN = 5; // Margin around a signal
var SIGNAL_PADDING = 5; // Padding inside a signal
var NOTE_MARGIN = 10; // Margin around a note
var NOTE_PADDING = 5; // Padding inside a note
var SELF_SIGNAL_WIDTH = 20; // How far out a self signal goes
this.svg_ = new svg_builder(isDark);
this._actors_height = 0;
this._signals_height = 0;
this.actors = actors;
this.signals = signals;
this.uids = uids;
this.width = 0;
this.height = 0;
this.draw = function(container)
{
this.layout();
this.svg_.setDocumentSize(this.width, this.height);
var y = DIAGRAM_MARGIN;
this.draw_actors(y);
this.draw_signals(y + this._actors_height);
};
this.layout = function()
{
this.width = 0; // min width
this.height = 0; // min width
this.actors.forEach(function(a)
{
var text = a.label;
var bb = this.svg_.getTextSize(text);
a.text_bb = bb;
a.x = 0; a.y = 0;
a.width = bb.width + (ACTOR_PADDING + ACTOR_MARGIN) * 2;
a.height = bb.height + (ACTOR_PADDING + ACTOR_MARGIN) * 2;
a.distances = [];
a.padding_right = 0;
this._actors_height = Math.max(a.height, this._actors_height);
}, this);
function actor_ensure_distance(a, b, d)
{
console.assert(a < b, "a must be less than or equal to b");
if (a < 0) {
// Ensure b has left margin
b = actors[b];
b.x = Math.max(d - b.width / 2, b.x);
} else if (b >= actors.length) {
// Ensure a has right margin
a = actors[a];
a.padding_right = Math.max(d, a.padding_right);
} else {
a = actors[a];
a.distances[b] = Math.max(d, a.distances[b] ? a.distances[b] : 0);
}
}
signals.forEach(function(s) {
var a, b; // Indexes of the left and right actors involved
var text = s.message;
var bb = this.svg_.getTextSize(text);
s.text_bb = bb;
s.width = bb.width;
s.height = bb.height;
var extra_width = 0;
if (s.type == "signal")
{
s.width += (SIGNAL_MARGIN + SIGNAL_PADDING) * 2;
s.height += (SIGNAL_MARGIN + SIGNAL_PADDING) * 2;
if (s.actorA == s.actorB) {
a = s.actorA.index;
b = a + 1;
s.width += SELF_SIGNAL_WIDTH;
} else {
a = Math.min(s.actorA.index, s.actorB.index);
b = Math.max(s.actorA.index, s.actorB.index);
}
}
else if (s.type == "note")
{
s.width += (NOTE_MARGIN + NOTE_PADDING) * 2;
s.height += (NOTE_MARGIN + NOTE_PADDING) * 2;
extra_width = 2 * ACTOR_MARGIN;
a = s.actor.index;
b = a + 1;
}
actor_ensure_distance(a, b, s.width + extra_width);
this._signals_height += s.height;
}, this);
// Re-jig the positions
var actors_x = 0;
this.actors.forEach(function(a) {
a.x = Math.max(actors_x, a.x);
// TODO This only works if we loop in sequence, 0, 1, 2, etc
a.distances.forEach(function(distance, b) {
b = actors[b];
distance = Math.max(distance, a.width / 2, b.width / 2);
b.x = Math.max(b.x, a.x + a.width/2 + distance - b.width/2);
});
actors_x = a.x + a.width + a.padding_right;
}, this);
this.width = 2 * DIAGRAM_MARGIN + Math.max(actors_x, this.width);
this.height = 2 * DIAGRAM_MARGIN + 2 * this._actors_height + this._signals_height;
return this;
};
this.draw_actors = function(offsetY)
{
var y = offsetY;
this.actors.forEach(function(a) {
// Top box
this.draw_actor(a, y, this._actors_height);
// Bottom box
this.draw_actor(a, y + this._actors_height + this._signals_height, this._actors_height);
// Vertical line
var aX = getCenterX(a);
var line = this.svg_.createPath('M{0},{1} v{2}', 'solid', 'none',
aX,
y + this._actors_height - ACTOR_MARGIN,
2 * ACTOR_MARGIN + this._signals_height);
this.svg_.appendChild(line);
}, this);
};
this.draw_actor = function (actor, offsetY, height)
{
actor.y = offsetY;
actor.height = height;
this.draw_text_box(actor, actor.label, ACTOR_MARGIN, ACTOR_PADDING);
};
this.draw_signals = function (offsetY)
{
var y = offsetY;
this.signals.forEach(function(s)
{
if (s.type == "signal")
{
if (s.actorA == s.actorB)
this.draw_self_signal(s, y);
else
this.draw_signal(s, y);
}
else if (s.type == "note")
this.draw_note(s, y);
y += s.height;
}, this);
};
this.draw_self_signal = function(signal, offsetY)
{
var text_bb = signal.text_bb;
var aX = getCenterX(signal.actorA);
var x = aX + SELF_SIGNAL_WIDTH + SIGNAL_PADDING + text_bb.width / 2;
var y = offsetY + signal.height / 2;
this.draw_text(x, y, signal.message, true);
var line = this.svg_.createPath("M{0},{1} C{2},{1} {2},{3} {0},{3}", signal.linetype, 'none',
aX,
offsetY + SIGNAL_MARGIN,
aX + SELF_SIGNAL_WIDTH * 2,
offsetY + signal.height);
line = this.svg_.setAttribute(line, "marker-end", "url(#" + signal.arrowtype + ")");
this.svg_.appendChild(line);
};
this.draw_signal = function (signal, offsetY)
{
var aX = getCenterX( signal.actorA );
var bX = getCenterX( signal.actorB );
// Mid point between actors
var x = (bX - aX) / 2 + aX;
var y = offsetY + SIGNAL_MARGIN + 2*SIGNAL_PADDING;
// Draw the text in the middle of the signal
this.draw_text(x, y, signal.message, true);
// Draw the line along the bottom of the signal
y = offsetY + signal.height - SIGNAL_MARGIN - SIGNAL_PADDING;
var line = this.svg_.createPath('M{0},{1} h{2}', signal.linetype, 'none', aX, y, (bX - aX));
line = this.svg_.setAttribute(line, "marker-end", "url(#" + signal.arrowtype + ")");
this.svg_.appendChild(line);
};
this.draw_note = function (note, offsetY)
{
var aX = getCenterX( note.actor );
var margin = NOTE_MARGIN;
var fill = note.bgcolor || 'none';
note.x = aX + ACTOR_MARGIN;
note.y = offsetY;
var noteShape = this.svg_.createPath("M{0},{1} L{0},{2} L{3},{2} L{0},{1} L{4},{1} L{4},{5} L{3},{5} L{3},{2} Z",
'solid', fill,
note.x - margin + note.width - 7,
note.y + margin,
note.y + margin + 7,
note.x - margin + note.width,
note.x + margin,
note.y - margin + note.height);
this.svg_.appendChild(noteShape);
// Draw text (in the center)
x = getCenterX(note);
y = getCenterY(note);
if (note.hasOwnProperty("fontcolor"))
this.draw_text(x, y, note.message, true, note.fontcolor);
else
this.draw_text(x, y, note.message, true);
};
this.draw_text = function (x, y, text, dontDrawBox, color)
{
var t = this.svg_.createText(text, x, y, color);
if (!dontDrawBox)
{
var size = this.svg_.getTextSize(text);
var rect = this.svg_.createRect(x, y, size.width, size.height);
this.svg_.appendChild(rect);
}
this.svg_.appendChild(t);
};
this.draw_text_box = function (box, text, margin)
{
var x = box.x + margin;
var y = box.y + margin;
var w = box.width - 2 * margin;
var h = box.height - 2 * margin;
// Draw inner box
var rect = this.svg_.createRect(x, y, w, h);
//rect.classList.add(Renderer.RECT_CLASS_);
this.svg_.appendChild(rect);
// Draw text (in the center)
x = getCenterX(box);
y = getCenterY(box);
this.draw_text(x, y, text, true);
};
function getCenterX(box)
{
return box.x + box.width / 2;
}
function getCenterY(box) {
return box.y + box.height / 2;
}
this.draw();
}