Skip to content
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

Added support for UML ExecutionSpecifications #157

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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
Expand Up @@ -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_LVL_CHANGE.UNCHANGED) {
executionLevelChangeB = executionLevelChangeA;
executionLevelChangeA = Diagram.EXECUTION_LVL_CHANGE.UNCHANGED;
}

if (actorA === actorB && executionLevelChangeA === executionLevelChangeB &&
executionLevelChangeA !== Diagram.EXECUTION_LVL_CHANGE.UNCHANGED) {
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;
Expand All @@ -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_LVL_CHANGE.UNCHANGED:
break;
case Diagram.EXECUTION_LVL_CHANGE.INCREASE_LEVEL:
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_LVL_CHANGE.DECREASE_LEVEL:
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);
};
Expand All @@ -108,6 +176,12 @@
OVER : 2
};

Diagram.EXECUTION_LVL_CHANGE = {
Copy link
Owner

Choose a reason for hiding this comment

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

Maybe just "Diagram.EXECUTION_CHANGE"

UNCHANGED : 0,
INCREASE_LEVEL : 1,
DECREASE_LEVEL : -1
Copy link
Owner

Choose a reason for hiding this comment

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

Then, None:0, INCREASE:1, DECREASE:2.

Does the LVL, and LEVEL help explain?

Copy link
Author

Choose a reason for hiding this comment

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

Not really. I should be able to get to this tonight.

};


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

/*
Expand Down
13 changes: 10 additions & 3 deletions src/grammar.jison
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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_LVL_CHANGE.UNCHANGED }
| LINE { $$ = Diagram.EXECUTION_LVL_CHANGE.DECREASE_LEVEL }
| PLUS { $$ = Diagram.EXECUTION_LVL_CHANGE.INCREASE_LEVEL }
;

actor
Expand Down
97 changes: 95 additions & 2 deletions src/sequence-diagram.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -76,6 +85,27 @@
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;
} else {
Copy link
Owner

Choose a reason for hiding this comment

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

No need for the else, so you return above.

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

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

/******************
* Raphaël extras
******************/
Expand Down Expand Up @@ -230,6 +260,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();
Expand Down Expand Up @@ -274,6 +305,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);
});

Expand Down Expand Up @@ -418,6 +454,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;
Expand All @@ -442,6 +524,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;
Expand All @@ -452,18 +535,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);
},
Expand All @@ -472,6 +557,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;
Expand Down
46 changes: 46 additions & 0 deletions test/grammar-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down