Skip to content

Added support for UML ExecutionSpecifications #157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions build/sequence-diagram-min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/sequence-diagram-min.js.map

Large diffs are not rendered by default.

76 changes: 75 additions & 1 deletion src/diagram.js
Original file line number Diff line number Diff line change
@@ -50,28 +50,62 @@
};

Diagram.prototype.addSignal = function(signal) {
// Set the numerical index of the signal.
signal.index = this.signals.length;
this.signals.push( signal );
};

Diagram.Actor = function(alias, name, index) {
this.alias = alias;
this.name = name;
this.index = index;
this.executionStack = [];
this.executions = [];
this.maxExecutionsLevel = -1;
};

Diagram.Signal = function(actorA, signaltype, actorB, message) {
Diagram.Signal = function(actorA, signaltype, actorB, message,
executionLevelChangeA, executionLevelChangeB) {
this.type = "Signal";
this.actorA = actorA;
this.actorB = actorB;
this.linetype = signaltype & 3;
this.arrowtype = (signaltype >> 2) & 3;
this.message = message;
this.index = null;
// If this is a self-signal and an Execution level modifier was only applied to the
// left-hand side of the signal, move it to the right-hand side to prevent rendering issues.
if (actorA === actorB && executionLevelChangeB === Diagram.EXECUTION_CHANGE.NONE) {
executionLevelChangeB = executionLevelChangeA;
executionLevelChangeA = Diagram.EXECUTION_CHANGE.NONE;
}

if (actorA === actorB && executionLevelChangeA === executionLevelChangeB &&
executionLevelChangeA !== Diagram.EXECUTION_CHANGE.NONE) {
throw new Error("You cannot move the Execution nesting level in the same " +
"direction twice on a single self-signal.");
}
this.actorA.changeExecutionLevel(executionLevelChangeA, this);
this.startLevel = this.actorA.executionStack.length - 1;
this.actorB.changeExecutionLevel(executionLevelChangeB, this);
this.endLevel = this.actorB.executionStack.length - 1;
};

Diagram.Signal.prototype.isSelf = function() {
return this.actorA.index == this.actorB.index;
};

/*
* If the signal is a self signal, this method returns the higher Execution nesting level
* between the start and end of the signal.
*/
Diagram.Signal.prototype.maxExecutionLevel = function () {
if (!this.isSelf()) {
throw new Error("maxExecutionLevel() was called on a non-self signal.");
}
return Math.max(this.startLevel, this.endLevel);
};

Diagram.Note = function(actor, placement, message) {
this.type = "Note";
this.actor = actor;
@@ -83,6 +117,40 @@
}
};

Diagram.Execution = function(actor, startSignal, level) {
this.actor = actor;
this.startSignal = startSignal;
this.endSignal = null;
this.level = level;
};

Diagram.Actor.prototype.changeExecutionLevel = function(change, signal) {
switch (change) {
case Diagram.EXECUTION_CHANGE.NONE:
break;
case Diagram.EXECUTION_CHANGE.INCREASE:
var newLevel = this.executionStack.length;
this.maxExecutionsLevel =
Math.max(this.maxExecutionsLevel, newLevel);
var execution = new Diagram.Execution(this, signal, newLevel);
this.executionStack.push(execution);
this.executions.push(execution);
break;
case Diagram.EXECUTION_CHANGE.DECREASE:
if (this.executionStack.length > 0) {
this.executionStack.pop().setEndSignal(signal);
} else {
throw new Error("The execution level for actor " + this.name +
" was dropped below 0.");
}
break;
}
};

Diagram.Execution.prototype.setEndSignal = function (signal) {
this.endSignal = signal;
};

Diagram.Note.prototype.hasManyActors = function() {
return _.isArray(this.actor);
};
@@ -108,6 +176,12 @@
OVER : 2
};

Diagram.EXECUTION_CHANGE = {
NONE : 0,
INCREASE : 1,
DECREASE : 2
};


// Some older browsers don't have getPrototypeOf, thus we polyfill it
// https://github.com/bramp/js-sequence-diagrams/issues/57
4 changes: 2 additions & 2 deletions src/grammar.ebnf
Original file line number Diff line number Diff line change
@@ -15,11 +15,11 @@ statement ::=
( 'left of' | 'right of') actor
| 'over' (actor | actor ',' actor)
) ':' message
| actor ( '-' | '--' ) ( '>' | '>>' )? actor ':' message
| ( '-' | '+' )? actor ( '-' | '--' ) ( '>' | '>>' )? ( '-' | '+' )? actor ':' message
)

/*
message ::= [^\n]+

actor ::= [^\->:\n,]+
actor ::= [^\->:\n,+]+
*/
13 changes: 10 additions & 3 deletions src/grammar.jison
Original file line number Diff line number Diff line change
@@ -23,10 +23,11 @@
"note" return 'note';
"title" return 'title';
"," return ',';
[^\->:,\r\n"]+ return 'ACTOR';
[^\->:,\r\n"+]+ return 'ACTOR';
\"[^"]+\" return 'ACTOR';
"--" return 'DOTLINE';
"-" return 'LINE';
"+" return 'PLUS';
">>" return 'OPENARROW';
">" return 'ARROW';
:[^\r\n]+ return 'MESSAGE';
@@ -76,8 +77,14 @@ placement
;

signal
: actor signaltype actor message
{ $$ = new Diagram.Signal($1, $2, $3, $4); }
: execution_modifier actor signaltype execution_modifier actor message
{ $$ = new Diagram.Signal($2, $3, $5, $6, $1, $4); }
;

execution_modifier
: /* empty */ { $$ = Diagram.EXECUTION_CHANGE.NONE }
| LINE { $$ = Diagram.EXECUTION_CHANGE.DECREASE }
| PLUS { $$ = Diagram.EXECUTION_CHANGE.INCREASE }
;

actor
95 changes: 93 additions & 2 deletions src/sequence-diagram.js
Original file line number Diff line number Diff line change
@@ -34,6 +34,9 @@

var SELF_SIGNAL_WIDTH = 20; // How far out a self signal goes

var EXECUTION_WIDTH = 10;
var OVERLAPPING_EXECUTION_OFFSET = EXECUTION_WIDTH * 0.5;

var PLACEMENT = Diagram.PLACEMENT;
var LINETYPE = Diagram.LINETYPE;
var ARROWTYPE = Diagram.ARROWTYPE;
@@ -47,6 +50,12 @@
'fill': "#fff"
};

var EXECUTION_RECT = {
'stroke': '#000',
'stroke-width': 2,
'fill': '#e6e6e6' // Color taken from the UML examples
};

function AssertException(message) { this.message = message; }
AssertException.prototype.toString = function () {
return 'AssertException: ' + this.message;
@@ -76,6 +85,25 @@
return box.y + box.height / 2;
}

/******************
* Drawing-related extra diagram methods.
******************/

// These functions return the x-offset from the lifeline centre given the current Execution nesting-level.
function executionMarginLeft(level) {
if (level < 0) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is level allowed to be <0? Should that be stopped elsewhere?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A level < 0 indicates that there are no executions (just the actor/lifeline) It is impossible to go lower than -1, the parser will throw an error.

return 0;
}
return -EXECUTION_WIDTH * 0.5 + level * OVERLAPPING_EXECUTION_OFFSET;
}

function executionMarginRight(level) {
if (level < 0) {
return 0;
}
return EXECUTION_WIDTH * 0.5 + level * OVERLAPPING_EXECUTION_OFFSET;
}

/******************
* Raphaël extras
******************/
@@ -230,6 +258,7 @@

this.draw_title();
this.draw_actors(y);
this.draw_executions(y + this._actors_height);
this.draw_signals(y + this._actors_height);

this._paper.setFinish();
@@ -274,6 +303,11 @@

a.distances = [];
a.padding_right = 0;
if (a.maxExecutionsLevel >= 0) {
a.padding_right = (EXECUTION_WIDTH / 2.0) +
(a.maxExecutionsLevel *
OVERLAPPING_EXECUTION_OFFSET);
}
self._actors_height = Math.max(a.height, self._actors_height);
});

@@ -418,6 +452,52 @@
this.draw_text_box(actor, actor.name, ACTOR_MARGIN, ACTOR_PADDING, this._font);
},

draw_executions : function (offsetY) {
var y = offsetY;
var self = this;

// Calculate the y-positions of each signal before we attempt to draw the executions.
_.each(this.diagram.signals, function(s) {
if (s.type == "Signal") {
if (s.isSelf()) {
s.startY = y + SIGNAL_MARGIN;
s.endY = s.startY + s.height - SIGNAL_MARGIN;
} else {
s.startY = s.endY = y + s.height - SIGNAL_MARGIN - SIGNAL_PADDING;
}
}

y += s.height;
});

_.each(this.diagram.actors, function(a) {
self.draw_actors_executions(a);
});
},

draw_actors_executions : function (actor) {
var self = this;
_.each(actor.executions, function (e) {
var aX = getCenterX(actor);
aX += e.level * OVERLAPPING_EXECUTION_OFFSET;
var x = aX - EXECUTION_WIDTH / 2.0;
var y;
var w = EXECUTION_WIDTH;
var h;
if (e.startSignal === e.endSignal) {
y = e.startSignal.startY;
h = e.endSignal ? e.endSignal.endY - y : (actor.y - y);
} else {
y = e.startSignal.endY;
h = e.endSignal ? e.endSignal.startY - y : (actor.y - y);
}

// Draw actual execution.
var rect = self.draw_rect(x, y, w, h);
rect.attr(EXECUTION_RECT);
});
},

draw_signals : function (offsetY) {
var y = offsetY;
var self = this;
@@ -442,6 +522,7 @@

var text_bb = signal.text_bb;
var aX = getCenterX(signal.actorA);
aX += executionMarginRight(signal.maxExecutionLevel());

var x = aX + SELF_SIGNAL_WIDTH + SIGNAL_PADDING - text_bb.x;
var y = offsetY + signal.height / 2;
@@ -452,18 +533,20 @@
'stroke-dasharray': this.line_types[signal.linetype]
});

var x1 = getCenterX(signal.actorA) + executionMarginRight(signal.startLevel);
var x2 = getCenterX(signal.actorA) + executionMarginRight(signal.endLevel);
var y1 = offsetY + SIGNAL_MARGIN;
var y2 = y1 + signal.height - SIGNAL_MARGIN;

// Draw three lines, the last one with a arrow
var line;
line = this.draw_line(aX, y1, aX + SELF_SIGNAL_WIDTH, y1);
line = this.draw_line(x1, y1, aX + SELF_SIGNAL_WIDTH, y1);
line.attr(attr);

line = this.draw_line(aX + SELF_SIGNAL_WIDTH, y1, aX + SELF_SIGNAL_WIDTH, y2);
line.attr(attr);

line = this.draw_line(aX + SELF_SIGNAL_WIDTH, y2, aX, y2);
line = this.draw_line(aX + SELF_SIGNAL_WIDTH, y2, x2, y2);
attr['arrow-end'] = this.arrow_types[signal.arrowtype] + '-wide-long';
line.attr(attr);
},
@@ -472,6 +555,14 @@
var aX = getCenterX( signal.actorA );
var bX = getCenterX( signal.actorB );

if (bX > aX) {
aX += executionMarginRight(signal.startLevel);
bX += executionMarginLeft(signal.endLevel);
} else {
aX += executionMarginLeft(signal.startLevel);
bX += executionMarginRight(signal.endLevel);
}

// Mid point between actors
var x = (bX - aX) / 2 + aX;
var y = offsetY + SIGNAL_MARGIN + 2*SIGNAL_PADDING;
46 changes: 46 additions & 0 deletions test/grammar-tests.js
Original file line number Diff line number Diff line change
@@ -67,6 +67,13 @@ function assertEmptyDocument(d) {
equal(d.signals.length, 0, "Zero signals");
}

function testExecutions(execution, affectedActorName, startSignal, endSignal, level) {
equal(execution.actor.name, affectedActorName, "Correct actor");
equal(execution.startSignal, startSignal, "Start signal of Execution");
equal(execution.endSignal, endSignal, "End signal of Execution");
equal(execution.level, level, "Nesting level of Execution");
}


var LINETYPE = Diagram.LINETYPE;
var ARROWTYPE = Diagram.ARROWTYPE;
@@ -185,6 +192,45 @@ test( "Quoted names", function() {
assertSingleArrow(Diagram.parse("\"->:\"->B: M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "->:", "B", "M");
assertSingleArrow(Diagram.parse("A->\"->:\": M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "A", "->:", "M");
assertSingleActor(Diagram.parse("Participant \"->:\""), "->:");
assertSingleArrow(Diagram.parse("A->\"+B\": M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "A", "+B", "M");
assertSingleArrow(Diagram.parse("\"+A\"->B: M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "+A", "B", "M");
assertSingleArrow(Diagram.parse("\"+A\"->\"+B\": M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "+A", "+B", "M");
});

test( "Executions", function () {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does
++A->B
do anything?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to check what happens, I know -- will throw an error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine ++ will throw an error too. I've only allowed single changes to the execution level.

assertSingleArrow(Diagram.parse("A->+B: M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "A", "B", "M");
assertSingleArrow(Diagram.parse("+A->B: M"), ARROWTYPE.FILLED, LINETYPE.SOLID, "A", "B", "M");
assertSingleArrow(Diagram.parse("+A-->+B: M"), ARROWTYPE.FILLED, LINETYPE.DOTTED, "A", "B", "M");
assertSingleArrow(Diagram.parse("+\"+A\"-->+B: M"), ARROWTYPE.FILLED, LINETYPE.DOTTED, "+A", "B", "M");

var d = Diagram.parse("A->+B: M1\n+B-->-B: M2\n-B-->>+A: M3");
equal(d.actors.length, 2, "Correct actors count");

var a = d.actors[0];
var b = d.actors[1];
equal(a.name, "A", "Actors A name");
equal(b.name, "B", "Actors B name");
var execsA = a.executions;
var execsB = b.executions;

equal(d.signals.length, 3, "Correct signals count");
equal(execsA.length, 1, "Correct actor A Execution count");
equal(execsB.length, 2, "Correct actor B Execution count");

// More or less normal Execution
testExecutions(execsB[0], "B", d.signals[0], d.signals[2], 0);
// Self-signalled Execution
testExecutions(execsB[1], "B", d.signals[1], d.signals[1], 1);
// Endless Execution
testExecutions(execsA[0], "A", d.signals[2], null, 0);

// Make sure we haven't broken the different arrow types.
equal(d.signals[0].arrowtype, ARROWTYPE.FILLED, "Signal 1 Arrow Type");
equal(d.signals[0].linetype, LINETYPE.SOLID, "Signal 1 Line Type");
equal(d.signals[1].arrowtype, ARROWTYPE.FILLED, "Signal 2 Arrow Type");
equal(d.signals[1].linetype, LINETYPE.DOTTED, "Signal 2 Line Type");
equal(d.signals[2].arrowtype, ARROWTYPE.OPEN, "Signal 3 Arrow Type");
equal(d.signals[2].linetype, LINETYPE.DOTTED, "Signal 3 Line Type");
});

test( "API", function() {