blob: 519363569938fe7b0aee24c109860fff41629d54 [file] [log] [blame]
Akronf5dc5102017-05-16 20:32:57 +02001define(function () {
2 "use strict";
3
4 var svgNS = "http://www.w3.org/2000/svg";
5
6 return {
7 create : function (snippet) {
8 var obj = Object.create(this)._init(snippet);
Akronc5b5f742017-05-23 16:04:35 +02009 obj._tokens = [];
Akronc5b5f742017-05-23 16:04:35 +020010 obj._arcs = []
Akron3a4a08e2017-05-23 22:34:18 +020011 obj._tokenElements = [];
12 obj._y = 0;
Akrond67d45b2017-05-18 21:47:38 +020013
Akronc5b5f742017-05-23 16:04:35 +020014 // Some configurations
Akronf5dc5102017-05-16 20:32:57 +020015 obj.maxArc = 200; // maximum height of the bezier control point
Akronc5b5f742017-05-23 16:04:35 +020016 obj.overlapDiff = 20;
17 obj.arcDiff = 15;
18 obj.anchorDiff = 6;
Akron3a4a08e2017-05-23 22:34:18 +020019 obj.anchorStart = 15;
Akronc5b5f742017-05-23 16:04:35 +020020 obj.tokenSep = 30;
Akronf5dc5102017-05-16 20:32:57 +020021 return obj;
22 },
23
24 _init : function (snippet) {
25 /*
26 var html = document.createElement("div");
27 html.innerHTML = snippet;
28 */
29 return this;
30 },
31
Akron15175132017-09-07 18:12:55 +020032
Akronf5dc5102017-05-16 20:32:57 +020033 // This is a shorthand for SVG element creation
34 _c : function (tag) {
35 return document.createElementNS(svgNS, tag);
36 },
37
Akron15175132017-09-07 18:12:55 +020038
Akronf5dc5102017-05-16 20:32:57 +020039 // Returns the center point of the requesting token
40 _tokenPoint : function (node) {
41 var box = node.getBoundingClientRect();
42 return box.x + (box.width / 2);
43 },
44
Akron15175132017-09-07 18:12:55 +020045
46 // Draws an anchor
Akrond67d45b2017-05-18 21:47:38 +020047 _drawAnchor : function (anchor) {
Akron15175132017-09-07 18:12:55 +020048
49 // Calculate the span of the first and last token, the anchor spans
Akron3a4a08e2017-05-23 22:34:18 +020050 var firstBox = this._tokenElements[anchor.first].getBoundingClientRect();
51 var lastBox = this._tokenElements[anchor.last].getBoundingClientRect();
Akrond67d45b2017-05-18 21:47:38 +020052
Akron15175132017-09-07 18:12:55 +020053 var startPos = firstBox.left - this.offsetLeft;
54 var endPos = lastBox.right - this.offsetLeft;
55
Akron3a4a08e2017-05-23 22:34:18 +020056 var y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;
57
Akrond67d45b2017-05-18 21:47:38 +020058 var l = this._c('path');
Akron3a4a08e2017-05-23 22:34:18 +020059 l.setAttribute("d", "M " + startPos + "," + y + " L " + endPos + "," + y);
Akrond67d45b2017-05-18 21:47:38 +020060 l.setAttribute("class", "anchor");
61 anchor.element = l;
62 anchor.y = y;
63 return l;
64 },
65
Akronf5dc5102017-05-16 20:32:57 +020066 // Create an arc with a label
67 // Potentially needs a height parameter for stacks
Akron63ae00b2017-05-16 22:03:36 +020068 _drawArc : function (arc) {
Akronf5dc5102017-05-16 20:32:57 +020069
Akrond67d45b2017-05-18 21:47:38 +020070 var startPos, endPos;
Akron3a4a08e2017-05-23 22:34:18 +020071 var startY = this._y, endY = this._y;
Akronf5dc5102017-05-16 20:32:57 +020072
Akrond67d45b2017-05-18 21:47:38 +020073 if (arc.startAnchor !== undefined) {
Akron3a4a08e2017-05-23 22:34:18 +020074 startPos = this._tokenPoint(arc.startAnchor.element);
Akrond67d45b2017-05-18 21:47:38 +020075 startY = arc.startAnchor.y;
76 }
77 else {
78 startPos = this._tokenPoint(this._tokenElements[arc.first]);
79 };
80
81 if (arc.endAnchor !== undefined) {
82 endPos = this._tokenPoint(arc.endAnchor.element)
83 endY = arc.endAnchor.y;
84 }
85 else {
86 endPos = this._tokenPoint(this._tokenElements[arc.last]);
87 };
88
Akron15175132017-09-07 18:12:55 +020089 startPos -= this.offsetLeft;
90 endPos -= this.offsetLeft;
91
Akronf5dc5102017-05-16 20:32:57 +020092 var g = this._c("g");
93 var p = g.appendChild(this._c("path"));
Akron15175132017-09-07 18:12:55 +020094 p.setAttribute('class', 'edge');
Akronf5dc5102017-05-16 20:32:57 +020095
96 // Create arc
97 var middle = Math.abs(endPos - startPos) / 2;
98
Akron63ae00b2017-05-16 22:03:36 +020099 // TODO: take the number of tokens into account!
Akronc5b5f742017-05-23 16:04:35 +0200100 var cHeight = this.arcDiff + arc.overlaps * this.overlapDiff + (middle / 2);
101
102 // Respect the maximum height
103 cHeight = cHeight < this.maxArc ? cHeight : this.maxArc;
Akronf5dc5102017-05-16 20:32:57 +0200104
105 var x = Math.min(startPos, endPos);
Akronc5b5f742017-05-23 16:04:35 +0200106
Akron3a4a08e2017-05-23 22:34:18 +0200107 //var controlY = (startY + endY - cHeight);
108 var controlY = (endY - cHeight);
Akronf5dc5102017-05-16 20:32:57 +0200109
Akron3a4a08e2017-05-23 22:34:18 +0200110 var arcE = "M "+ startPos + "," + startY +
111 " C " + startPos + "," + controlY +
112 " " + endPos + "," + controlY +
113 " " + endPos + "," + endY;
Akrond67d45b2017-05-18 21:47:38 +0200114
Akron63ae00b2017-05-16 22:03:36 +0200115 p.setAttribute("d", arcE);
Akronf5dc5102017-05-16 20:32:57 +0200116
Akron3a4a08e2017-05-23 22:34:18 +0200117 if (arc.direction !== undefined) {
118 p.setAttribute("marker-end", "url(#arr)");
119 if (arc.direction === 'bi') {
120 p.setAttribute("marker-start", "url(#arr)");
121 };
122 };
123
Akronc5b5f742017-05-23 16:04:35 +0200124 /*
125 * Calculate the top point of the arc for labeling using
126 * de Casteljau's algorithm, see e.g.
127 * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/
128 * of course simplified to symmetric arcs ...
129 */
130 // Interpolate one side of the control polygon
131 // var controlInterpY1 = (startY + controlY) / 2;
132 // var controlInterpY2 = (controlInterpY1 + controlY) / 2;
133 var middleY = (((startY + controlY) / 2) + controlY) / 2;
134
135 // WARNING!
136 // This won't respect span anchors, adjusting startY and endY!
137
Akron63ae00b2017-05-16 22:03:36 +0200138 if (arc.label !== undefined) {
Akronf5dc5102017-05-16 20:32:57 +0200139 var labelE = g.appendChild(this._c("text"));
140 labelE.setAttribute("x", x + middle);
Akronc5b5f742017-05-23 16:04:35 +0200141 labelE.setAttribute("y", middleY + 3);
Akronf5dc5102017-05-16 20:32:57 +0200142 labelE.setAttribute("text-anchor", "middle");
Akron63ae00b2017-05-16 22:03:36 +0200143 labelE.appendChild(document.createTextNode(arc.label));
Akron1dc87902017-05-29 16:04:56 +0200144
145 /*
146 var textWidth = labelE.getComputedTextLength();
147
148 var labelR = g.appendChild(this._c("rect"));
149 labelR.setAttribute("x", x + middle);
150 labelR.setAttribute("y", middleY + 3);
151 labelR.setAttribute("width", textWidth +30);
152 labelR.setAttribute("height", 20);
153 */
Akronf5dc5102017-05-16 20:32:57 +0200154 };
Akronf5dc5102017-05-16 20:32:57 +0200155 return g;
156 },
157
158 element : function () {
159 if (this._element !== undefined)
160 return this._element;
161
162 // Create svg
163 var svg = this._c("svg");
Akron3a4a08e2017-05-23 22:34:18 +0200164
165 window.addEventListener("resize", function () {
166 // TODO: Only if text-size changed!
167 this.show();
168 }.bind(this));
169
170 var defs = svg.appendChild(this._c("defs"));
171 var marker = defs.appendChild(this._c("marker"));
172 marker.setAttribute("refX", 9);
173 marker.setAttribute("id", "arr");
174 marker.setAttribute("orient", "auto-start-reverse");
175 marker.setAttribute("markerUnits","userSpaceOnUse");
176
177 var arrow = this._c("path");
178 arrow.setAttribute("transform", "scale(0.8)");
179 arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
180 marker.appendChild(arrow);
181
Akronf5dc5102017-05-16 20:32:57 +0200182 this._element = svg;
183 return this._element;
184 },
185
186 // Add a relation with a start, an end,
187 // a direction value and a label text
Akronc5b5f742017-05-23 16:04:35 +0200188 addRel : function (rel) {
189 this._arcs.push(rel);
190 return this;
Akronf5dc5102017-05-16 20:32:57 +0200191 },
192
Akronc5b5f742017-05-23 16:04:35 +0200193
194 addToken : function(token) {
195 this._tokens.push(token);
196 return this;
197 },
198
Akronf5dc5102017-05-16 20:32:57 +0200199 /*
200 * All arcs need to be sorted before shown,
201 * to avoid nesting.
202 */
203 _sortArcs : function () {
204
Akrond67d45b2017-05-18 21:47:38 +0200205
206 // TODO:
207 // Keep in mind that the arcs may have long anchors!
208 // 1. Iterate over all arcs
209 // 2. Sort all multi
210 var anchors = [];
211
Akronf5dc5102017-05-16 20:32:57 +0200212 // 1. Sort by length
213 // 2. Tag all spans with the number of overlaps before
214 // a) Iterate over all spans
215 // b) check the latest preceeding overlapping span (lpos)
216 // -> not found: tag with 0
217 // -> found: Add +1 to the level of the (lpos)
218 // c) If the new tag is smaller than the previous element,
219 // reorder
Akron63ae00b2017-05-16 22:03:36 +0200220
221 // Normalize start and end
222 var sortedArcs = this._arcs.map(function (v) {
Akrond67d45b2017-05-18 21:47:38 +0200223
224 // Check for long anchors
225 if (v.start instanceof Array) {
226 var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
227
228 v.startAnchor = {
Akron3a4a08e2017-05-23 22:34:18 +0200229 "first": v.start[0],
230 "last" : v.start[1],
Akrond67d45b2017-05-18 21:47:38 +0200231 "length" : v.start[1] - v.start[0]
232 };
233
234 // Add to anchors list
235 anchors.push(v.startAnchor);
236 v.start = middle;
237 };
238
239 if (v.end instanceof Array) {
240 var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
241 v.endAnchor = {
Akron3a4a08e2017-05-23 22:34:18 +0200242 "first": v.end[0],
243 "last" : v.end[1],
Akrond67d45b2017-05-18 21:47:38 +0200244 "length" : v.end[1] - v.end[0]
245 };
246
247 // Add to anchors list
248 anchors.push(v.endAnchor);
249 v.end = middle;
250 };
251
252 // calculate the arch length
Akron63ae00b2017-05-16 22:03:36 +0200253 if (v.start < v.end) {
254 v.first = v.start;
255 v.last = v.end;
256 v.length = v.end - v.start;
257 }
258 else {
259 v.first = v.end;
260 v.last = v.start;
261 v.length = v.start - v.end;
262 };
263 return v;
264 });
265
266 // Sort based on length
267 sortedArcs.sort(function (a, b) {
268 if (a.length < b.length)
269 return -1;
270 else
271 return 1;
272 });
273
Akron3a4a08e2017-05-23 22:34:18 +0200274 this._sortedArcs = lengthSort(sortedArcs, false);
Akrond67d45b2017-05-18 21:47:38 +0200275 this._sortedAnchors = lengthSort(anchors, true);
Akronf5dc5102017-05-16 20:32:57 +0200276 },
277
278 show : function () {
279 var svg = this._element;
Akron3a4a08e2017-05-23 22:34:18 +0200280 var height = this.maxArc;
281
Akron3a4a08e2017-05-23 22:34:18 +0200282 // Delete old group
283 if (svg.getElementsByTagName("g")[0] !== undefined) {
284 var group = svg.getElementsByTagName("g")[0];
285 svg.removeChild(group);
286 this._tokenElements = [];
287 };
288
289 var g = svg.appendChild(this._c("g"));
Akronf5dc5102017-05-16 20:32:57 +0200290
291 /*
Akron15175132017-09-07 18:12:55 +0200292 * Create token list
Akronf5dc5102017-05-16 20:32:57 +0200293 */
Akron3a4a08e2017-05-23 22:34:18 +0200294 var text = g.appendChild(this._c("text"));
295 text.setAttribute("text-anchor", "start");
296 text.setAttribute("y", height);
Akronf5dc5102017-05-16 20:32:57 +0200297
Akron3a4a08e2017-05-23 22:34:18 +0200298 this._y = height - (this.anchorStart);
299
300 var ws = text.appendChild(this._c("tspan"));
301 ws.appendChild(document.createTextNode('\u00A0'));
302 ws.style.textAnchor = "start";
303
Akronf5dc5102017-05-16 20:32:57 +0200304 var lastRight = 0;
305 for (var node_i in this._tokens) {
306 // Append svg
307 var tspan = text.appendChild(this._c("tspan"));
308 tspan.appendChild(document.createTextNode(this._tokens[node_i]));
Akron3a4a08e2017-05-23 22:34:18 +0200309 tspan.setAttribute("text-anchor", "middle");
310
Akronf5dc5102017-05-16 20:32:57 +0200311 this._tokenElements.push(tspan);
312
313 // Add whitespace!
Akron3a4a08e2017-05-23 22:34:18 +0200314 //var ws = text.appendChild(this._c("tspan"));
315 //ws.appendChild(document.createTextNode(" "));
316 // ws.setAttribute("class", "rel-ws");
317 tspan.setAttribute("dx", this.tokenSep);
Akronf5dc5102017-05-16 20:32:57 +0200318 };
319
Akron15175132017-09-07 18:12:55 +0200320 var globalBoundingBox = g.getBoundingClientRect();
321 this.offsetLeft = globalBoundingBox.left;
322
Akron3a4a08e2017-05-23 22:34:18 +0200323 var arcs = g.appendChild(this._c("g"));
324 arcs.classList.add("arcs");
Akron15175132017-09-07 18:12:55 +0200325
326 // Sort arcs if not sorted yet
Akron3a4a08e2017-05-23 22:34:18 +0200327 if (this._sortedArcs === undefined) {
328 this._sortArcs();
329 };
Akrond67d45b2017-05-18 21:47:38 +0200330
331 var i;
Akron15175132017-09-07 18:12:55 +0200332
333 // Draw all anchors
Akrond67d45b2017-05-18 21:47:38 +0200334 for (i in this._sortedAnchors) {
Akron3a4a08e2017-05-23 22:34:18 +0200335 arcs.appendChild(this._drawAnchor(this._sortedAnchors[i]));
Akrond67d45b2017-05-18 21:47:38 +0200336 };
Akron15175132017-09-07 18:12:55 +0200337
338
339 // draw all arcs
Akrond67d45b2017-05-18 21:47:38 +0200340 for (i in this._sortedArcs) {
Akron3a4a08e2017-05-23 22:34:18 +0200341 arcs.appendChild(this._drawArc(this._sortedArcs[i]));
Akronf5dc5102017-05-16 20:32:57 +0200342 };
Akron3a4a08e2017-05-23 22:34:18 +0200343
344 var width = text.getBoundingClientRect().width;
Akronf3d7d8e2017-05-23 22:52:54 +0200345 svg.setAttribute("width", width + 20);
346 svg.setAttribute("height", height + 20);
Akron3a4a08e2017-05-23 22:34:18 +0200347 svg.setAttribute("class", "relTree");
Akronf5dc5102017-05-16 20:32:57 +0200348 }
Akrond67d45b2017-05-18 21:47:38 +0200349 };
350
351 function lengthSort (list, inclusive) {
352
353 /*
354 * The "inclusive" flag allows to
355 * modify the behaviour for inclusivity check,
356 * e.g. if identical start or endpoints mean overlap or not.
357 */
358
359 var stack = [];
360
361 // Iterate over all definitions
362 for (var i = 0; i < list.length; i++) {
363 var current = list[i];
364
365 // Check the stack order
366 var overlaps = 0;
367
368 for (var j = (stack.length - 1); j >= 0; j--) {
369 var check = stack[j];
370
371 // (a..(b..b)..a)
372 if (current.first <= check.first && current.last >= check.last) {
373 overlaps = check.overlaps + 1;
374 break;
375 }
376
377 // (a..(b..a)..b)
378 else if (current.first <= check.first && current.last >= check.first) {
379
380 if (inclusive || (current.first != check.first && current.last != check.first)) {
381 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
382 };
383 }
384
385 // (b..(a..b)..a)
386 else if (current.first <= check.last && current.last >= check.last) {
387
388 if (inclusive || (current.first != check.last && current.last != check.last)) {
389 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
390 };
391 };
392 };
393
394 // Set overlaps
395 current.overlaps = overlaps;
396
397 stack.push(current);
398
399 // Although it is already sorted,
400 // the new item has to be put at the correct place
401 // TODO: Use something like splice() instead
402 stack.sort(function (a,b) {
403 b.overlaps - a.overlaps
404 });
405 };
406
407 return stack;
408 };
Akronf5dc5102017-05-16 20:32:57 +0200409});