-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
base: master
Are you sure you want to change the base?
Changes from 8 commits
832a3cd
51d1e12
8169191
85e9c6b
5772d40
a1127b8
9cb04c7
f7199d9
d64d749
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
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_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; | ||
|
@@ -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); | ||
}; | ||
|
@@ -108,6 +176,12 @@ | |
OVER : 2 | ||
}; | ||
|
||
Diagram.EXECUTION_LVL_CHANGE = { | ||
UNCHANGED : 0, | ||
INCREASE_LEVEL : 1, | ||
DECREASE_LEVEL : -1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
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,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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is level allowed to be <0? Should that be stopped elsewhere? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
******************/ | ||
|
@@ -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(); | ||
|
@@ -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); | ||
}); | ||
|
||
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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); | ||
}, | ||
|
@@ -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; | ||
|
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 () { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() { | ||
|
There was a problem hiding this comment.
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"