-
Notifications
You must be signed in to change notification settings - Fork 10
/
sequence_diagram.js
351 lines (316 loc) · 12.1 KB
/
sequence_diagram.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
let myDiagram;
function init(graph) {
// Since 2.2 you can also author concise templates with method chaining instead of GraphObject.make
// For details, see https://gojs.net/latest/intro/buildingObjects.html
const $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "sequenceDiagramDiv", // must be the ID or reference to an HTML DIV
{
allowCopy: false,
linkingTool: $(MessagingTool), // defined below
"resizingTool.isGridSnapEnabled": true,
draggingTool: $(MessageDraggingTool), // defined below
"draggingTool.gridSnapCellSize": new go.Size(1, MessageSpacing / 4),
"draggingTool.isGridSnapEnabled": true,
// automatically extend Lifelines as Activities are moved or resized
"SelectionMoved": ensureLifelineHeights,
"PartResized": ensureLifelineHeights,
"undoManager.isEnabled": true
});
// define the Lifeline Node template.
myDiagram.groupTemplate =
$(go.Group, "Vertical",
{
locationSpot: go.Spot.Bottom,
locationObjectName: "HEADER",
minLocation: new go.Point(0, 0),
maxLocation: new go.Point(9999, 0),
selectionObjectName: "HEADER"
},
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
$(go.Panel, "Auto",
{ name: "HEADER" },
$(go.Shape, "Rectangle",
{
fill: $(go.Brush, "Linear", { 0: "#bbdefb", 1: go.Brush.darkenBy("#bbdefb", 0.1) }),
stroke: null
}),
$(go.TextBlock,
{
margin: 5,
font: "400 10pt Source Sans Pro, sans-serif"
},
new go.Binding("text", "text"))
),
$(go.Shape,
{
figure: "LineV",
fill: null,
stroke: "gray",
strokeDashArray: [3, 3],
width: 1,
alignment: go.Spot.Center,
portId: "",
fromLinkable: true,
fromLinkableDuplicates: true,
toLinkable: true,
toLinkableDuplicates: true,
cursor: "pointer"
},
new go.Binding("height", "duration", computeLifelineHeight))
);
// define the Activity Node template
myDiagram.nodeTemplate =
$(go.Node,
{
locationSpot: go.Spot.Top,
locationObjectName: "SHAPE",
minLocation: new go.Point(NaN, LinePrefix - ActivityStart),
maxLocation: new go.Point(NaN, 19999),
selectionObjectName: "SHAPE",
resizable: true,
resizeObjectName: "SHAPE",
resizeAdornmentTemplate:
$(go.Adornment, "Spot",
$(go.Placeholder),
$(go.Shape, // only a bottom resize handle
{
alignment: go.Spot.Bottom, cursor: "col-resize",
desiredSize: new go.Size(6, 6), fill: "yellow"
})
)
},
new go.Binding("location", "", computeActivityLocation).makeTwoWay(backComputeActivityLocation),
$(go.Shape, "Rectangle",
{
name: "SHAPE",
fill: "white", stroke: "black",
width: ActivityWidth,
// allow Activities to be resized down to 1/4 of a time unit
minSize: new go.Size(ActivityWidth, computeActivityHeight(0.25))
},
new go.Binding("height", "duration", computeActivityHeight).makeTwoWay(backComputeActivityHeight))
);
// define the Message Link template.
myDiagram.linkTemplate =
$(MessageLink, // defined below
{ selectionAdorned: true, curviness: 0 },
$(go.Shape, "Rectangle",
{ stroke: "black" }),
$(go.Shape,
{ toArrow: "OpenTriangle", stroke: "black" }),
$(go.TextBlock,
{
font: "400 9pt Source Sans Pro, sans-serif",
segmentIndex: 0,
segmentOffset: new go.Point(NaN, NaN),
isMultiline: false,
editable: true
},
new go.Binding("text", "text").makeTwoWay())
);
// create the graph by reading the JSON data saved in "mySavedModel" textarea element
myDiagram.model = go.Model.fromJson(graph)
return myDiagram
}
function ensureLifelineHeights(e) {
// iterate over all Activities (ignore Groups)
const arr = myDiagram.model.nodeDataArray;
let max = -1;
for (let i = 0; i < arr.length; i++) {
const act = arr[i];
if (act.isGroup) continue;
max = Math.max(max, act.start + act.duration);
}
if (max > 0) {
// now iterate over only Groups
for (let i = 0; i < arr.length; i++) {
const gr = arr[i];
if (!gr.isGroup) continue;
if (max > gr.duration) { // this only extends, never shrinks
myDiagram.model.setDataProperty(gr, "duration", max);
}
}
}
}
// some parameters
const LinePrefix = 20; // vertical starting point in document for all Messages and Activations
const LineSuffix = 30; // vertical length beyond the last message time
const MessageSpacing = 20; // vertical distance between Messages at different steps
const ActivityWidth = 10; // width of each vertical activity bar
const ActivityStart = 5; // height before start message time
const ActivityEnd = 5; // height beyond end message time
function computeLifelineHeight(duration) {
return LinePrefix + duration * MessageSpacing + LineSuffix;
}
function computeActivityLocation(act) {
const groupdata = myDiagram.model.findNodeDataForKey(act.group);
if (groupdata === null) return new go.Point();
// get location of Lifeline's starting point
const grouploc = go.Point.parse(groupdata.loc);
return new go.Point(grouploc.x, convertTimeToY(act.start) - ActivityStart);
}
function backComputeActivityLocation(loc, act) {
myDiagram.model.setDataProperty(act, "start", convertYToTime(loc.y + ActivityStart));
}
function computeActivityHeight(duration) {
return ActivityStart + duration * MessageSpacing + ActivityEnd;
}
function backComputeActivityHeight(height) {
return (height - ActivityStart - ActivityEnd) / MessageSpacing;
}
// time is just an abstract small non-negative integer
// here we map between an abstract time and a vertical position
function convertTimeToY(t) {
return t * MessageSpacing + LinePrefix;
}
function convertYToTime(y) {
return (y - LinePrefix) / MessageSpacing;
}
// a custom routed Link
class MessageLink extends go.Link {
constructor() {
super();
this.time = 0; // use this "time" value when this is the temporaryLink
}
getLinkPoint(node, port, spot, from, ortho, othernode, otherport) {
const p = port.getDocumentPoint(go.Spot.Center);
const r = port.getDocumentBounds();
const op = otherport.getDocumentPoint(go.Spot.Center);
const data = this.data;
const time = data !== null ? data.time : this.time; // if not bound, assume this has its own "time" property
const aw = this.findActivityWidth(node, time);
const x = (op.x > p.x ? p.x + aw / 2 : p.x - aw / 2);
const y = convertTimeToY(time);
return new go.Point(x, y);
}
findActivityWidth(node, time) {
let aw = ActivityWidth;
if (node instanceof go.Group) {
// see if there is an Activity Node at this point -- if not, connect the link directly with the Group's lifeline
if (!node.memberParts.any(mem => {
const act = mem.data;
return (act !== null && act.start <= time && time <= act.start + act.duration);
})) {
aw = 0;
}
}
return aw;
}
getLinkDirection(node, port, linkpoint, spot, from, ortho, othernode, otherport) {
const p = port.getDocumentPoint(go.Spot.Center);
const op = otherport.getDocumentPoint(go.Spot.Center);
const right = op.x > p.x;
return right ? 0 : 180;
}
computePoints() {
if (this.fromNode === this.toNode) { // also handle a reflexive link as a simple orthogonal loop
const data = this.data;
const time = data !== null ? data.time : this.time; // if not bound, assume this has its own "time" property
const p = this.fromNode.port.getDocumentPoint(go.Spot.Center);
const aw = this.findActivityWidth(this.fromNode, time);
const x = p.x + aw / 2;
const y = convertTimeToY(time);
this.clearPoints();
this.addPoint(new go.Point(x, y));
this.addPoint(new go.Point(x + 50, y));
this.addPoint(new go.Point(x + 50, y + 5));
this.addPoint(new go.Point(x, y + 5));
return true;
} else {
return super.computePoints();
}
}
}
// end MessageLink
// A custom LinkingTool that fixes the "time" (i.e. the Y coordinate)
// for both the temporaryLink and the actual newly created Link
class MessagingTool extends go.LinkingTool {
constructor() {
super();
// Since 2.2 you can also author concise templates with method chaining instead of GraphObject.make
// For details, see https://gojs.net/latest/intro/buildingObjects.html
const $ = go.GraphObject.make;
this.temporaryLink =
$(MessageLink,
$(go.Shape, "Rectangle",
{ stroke: "magenta", strokeWidth: 2 }),
$(go.Shape,
{ toArrow: "OpenTriangle", stroke: "magenta" }));
}
doActivate() {
super.doActivate();
const time = convertYToTime(this.diagram.firstInput.documentPoint.y);
this.temporaryLink.time = Math.ceil(time); // round up to an integer value
}
insertLink(fromnode, fromport, tonode, toport) {
const newlink = super.insertLink(fromnode, fromport, tonode, toport);
if (newlink !== null) {
const model = this.diagram.model;
// specify the time of the message
const start = this.temporaryLink.time;
const duration = 1;
newlink.data.time = start;
model.setDataProperty(newlink.data, "text", "msg");
// and create a new Activity node data in the "to" group data
const newact = {
group: newlink.data.to,
start: start,
duration: duration
};
model.addNodeData(newact);
// now make sure all Lifelines are long enough
ensureLifelineHeights();
}
return newlink;
}
}
// end MessagingTool
// A custom DraggingTool that supports dragging any number of MessageLinks up and down --
// changing their data.time value.
class MessageDraggingTool extends go.DraggingTool {
// override the standard behavior to include all selected Links,
// even if not connected with any selected Nodes
computeEffectiveCollection(parts, options) {
const result = super.computeEffectiveCollection(parts, options);
// add a dummy Node so that the user can select only Links and move them all
result.add(new go.Node(), new go.DraggingInfo(new go.Point()));
// normally this method removes any links not connected to selected nodes;
// we have to add them back so that they are included in the "parts" argument to moveParts
parts.each(part => {
if (part instanceof go.Link) {
result.add(part, new go.DraggingInfo(part.getPoint(0).copy()));
}
})
return result;
}
// override to allow dragging when the selection only includes Links
mayMove() {
return !this.diagram.isReadOnly && this.diagram.allowMove;
}
// override to move Links (which are all assumed to be MessageLinks) by
// updating their Link.data.time property so that their link routes will
// have the correct vertical position
moveParts(parts, offset, check) {
super.moveParts(parts, offset, check);
const it = parts.iterator;
while (it.next()) {
if (it.key instanceof go.Link) {
const link = it.key;
const startY = it.value.point.y; // DraggingInfo.point.y
let y = startY + offset.y; // determine new Y coordinate value for this link
const cellY = this.gridSnapCellSize.height;
y = Math.round(y / cellY) * cellY; // snap to multiple of gridSnapCellSize.height
const t = Math.max(0, convertYToTime(y));
link.diagram.model.set(link.data, "time", t);
link.invalidateRoute();
}
}
}
}
function load(graph) {
const $ = go.GraphObject.make
myDiagram.model = go.Model.fromJson(graph)
return myDiagram
}
export { init, load }