blob: 137eb635ee82b1313964ed79c11dbdff6815cd9b [file] [log] [blame]
Akron671fdb92017-09-12 18:09:46 +02001define([], function () {
Akronf5dc5102017-05-16 20:32:57 +02002 "use strict";
3
4 var svgNS = "http://www.w3.org/2000/svg";
Akron671fdb92017-09-12 18:09:46 +02005 var _TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
Akronf5dc5102017-05-16 20:32:57 +02006
7 return {
8 create : function (snippet) {
Akron671fdb92017-09-12 18:09:46 +02009 var obj = Object.create(this);
Akronc5b5f742017-05-23 16:04:35 +020010 obj._tokens = [];
Akronc5b5f742017-05-23 16:04:35 +020011 obj._arcs = []
Akron3a4a08e2017-05-23 22:34:18 +020012 obj._tokenElements = [];
13 obj._y = 0;
Akrond67d45b2017-05-18 21:47:38 +020014
Akronc5b5f742017-05-23 16:04:35 +020015 // Some configurations
Akron671fdb92017-09-12 18:09:46 +020016 obj.maxArc = 200; // maximum height of the bezier control point
Akron65d31082017-09-08 16:23:40 +020017 obj.overlapDiff = 40;
Akron671fdb92017-09-12 18:09:46 +020018 obj.arcDiff = 15;
19 obj.anchorDiff = 8;
Akron3a4a08e2017-05-23 22:34:18 +020020 obj.anchorStart = 15;
Akron671fdb92017-09-12 18:09:46 +020021 obj.tokenSep = 30;
22 obj.xPadding = 10;
23 obj.yPadding = 5;
24 return obj._init(snippet);
Akronf5dc5102017-05-16 20:32:57 +020025 },
26
27 _init : function (snippet) {
Akron671fdb92017-09-12 18:09:46 +020028
29 if (snippet != undefined && snippet != null) {
30 var html = document.createElement("div");
31 html.innerHTML = snippet;
32
33 // Establish temporary parsing memory
34 this.temp = {
35
36 // Remember the map id => pos
37 target : {},
38
39 // Remember edge definitions
40 edges : [],
41
42 // Keep track of the current token position
43 pos : 0
44 };
45 this._parse(0, html.childNodes, undefined);
46
47 // Establish edge list
48 var targetMap = this.temp['target'];
49 var edges = this.temp['edges'];
50 for (var i in edges) {
51 var edge = edges[i];
52
53 // Check the target identifier
54 var targetID = edge.targetID;
55 var target = targetMap[targetID];
56
57 if (target != undefined) {
58
59 // Add relation
60 this.addRel({
61 start : edge.src,
62 end : target,
63 direction : 'uni',
64 label : edge.label
65 });
66 };
67 };
68
69 // Reset parsing memory
70 this.temp = {};
71 };
72
Akronf5dc5102017-05-16 20:32:57 +020073 return this;
74 },
75
Akron671fdb92017-09-12 18:09:46 +020076 _parse : function (parent, children, mark) {
77 for (var i in children) {
78 var c = children[i];
79
80 // Element node
81 if (c.nodeType == 1) {
82
83 // Node is an identifier
84 if (c.hasAttribute('xml:id')) {
85
86 // Remember that pos has this identifier
87 this.temp['target'][c.getAttribute('xml:id')] = this.temp['pos'];
88 }
89
90 // Node is a relation
91 else if (c.hasAttribute('xlink:href')) {
92 var label, target;
93
94 target = c.getAttribute('xlink:href');
95 target = target.replace(/^#/, "");
96
97 if (c.hasAttribute('xlink:title')) {
98 label = this._clean(c.getAttribute('xlink:title'));
99 };
100
101 // Remember the defined edge
102 this.temp['edges'].push({
103 label : label,
104 src : this.temp['pos'],
105 targetID : target
106 });
107 };
108
109 if (c.hasChildNodes()) {
110 this._parse(0, c.childNodes, mark);
111 };
112 }
113
114 // Text node
115 else if (c.nodeType == 3) {
116
117 // Check, if there is a non-whitespace token
118 if (c.nodeValue !== undefined) {
119 var str = c.nodeValue.trim();
120 if (str !== undefined && str.length > 0) {
121
122 // Add token to token list
123 this.addToken(str);
124
125 // Move token position
126 this.temp['pos']++;
127 };
128 };
129 }
130 };
131 },
132
133 // Remove foundry and layer for labels
134 _clean : function (title) {
135 return title.replace(_TermRE, "$3");
136 },
137
138 size : function () {
139 return this._tokens.length;
140 },
Akron15175132017-09-07 18:12:55 +0200141
Akronf5dc5102017-05-16 20:32:57 +0200142 // This is a shorthand for SVG element creation
143 _c : function (tag) {
144 return document.createElementNS(svgNS, tag);
145 },
146
Akron15175132017-09-07 18:12:55 +0200147
Akronf5dc5102017-05-16 20:32:57 +0200148 // Returns the center point of the requesting token
149 _tokenPoint : function (node) {
150 var box = node.getBoundingClientRect();
151 return box.x + (box.width / 2);
152 },
153
Akron15175132017-09-07 18:12:55 +0200154
155 // Draws an anchor
Akrond67d45b2017-05-18 21:47:38 +0200156 _drawAnchor : function (anchor) {
Akron15175132017-09-07 18:12:55 +0200157
158 // Calculate the span of the first and last token, the anchor spans
Akron3a4a08e2017-05-23 22:34:18 +0200159 var firstBox = this._tokenElements[anchor.first].getBoundingClientRect();
160 var lastBox = this._tokenElements[anchor.last].getBoundingClientRect();
Akrond67d45b2017-05-18 21:47:38 +0200161
Akron15175132017-09-07 18:12:55 +0200162 var startPos = firstBox.left - this.offsetLeft;
163 var endPos = lastBox.right - this.offsetLeft;
164
Akron3a4a08e2017-05-23 22:34:18 +0200165 var y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;
166
Akrond67d45b2017-05-18 21:47:38 +0200167 var l = this._c('path');
Akron65d31082017-09-08 16:23:40 +0200168 this._arcsElement.appendChild(l);
Akron3a4a08e2017-05-23 22:34:18 +0200169 l.setAttribute("d", "M " + startPos + "," + y + " L " + endPos + "," + y);
Akrond67d45b2017-05-18 21:47:38 +0200170 l.setAttribute("class", "anchor");
171 anchor.element = l;
172 anchor.y = y;
173 return l;
174 },
175
Akronf5dc5102017-05-16 20:32:57 +0200176 // Create an arc with a label
177 // Potentially needs a height parameter for stacks
Akron63ae00b2017-05-16 22:03:36 +0200178 _drawArc : function (arc) {
Akronf5dc5102017-05-16 20:32:57 +0200179
Akrond67d45b2017-05-18 21:47:38 +0200180 var startPos, endPos;
Akron3a4a08e2017-05-23 22:34:18 +0200181 var startY = this._y, endY = this._y;
Akronf5dc5102017-05-16 20:32:57 +0200182
Akrond67d45b2017-05-18 21:47:38 +0200183 if (arc.startAnchor !== undefined) {
Akron3a4a08e2017-05-23 22:34:18 +0200184 startPos = this._tokenPoint(arc.startAnchor.element);
Akrond67d45b2017-05-18 21:47:38 +0200185 startY = arc.startAnchor.y;
186 }
187 else {
188 startPos = this._tokenPoint(this._tokenElements[arc.first]);
189 };
190
191 if (arc.endAnchor !== undefined) {
192 endPos = this._tokenPoint(arc.endAnchor.element)
193 endY = arc.endAnchor.y;
194 }
195 else {
196 endPos = this._tokenPoint(this._tokenElements[arc.last]);
197 };
198
Akron15175132017-09-07 18:12:55 +0200199 startPos -= this.offsetLeft;
200 endPos -= this.offsetLeft;
201
Akronf5dc5102017-05-16 20:32:57 +0200202 var g = this._c("g");
Akron65d31082017-09-08 16:23:40 +0200203 g.setAttribute("class", "arc");
Akronf5dc5102017-05-16 20:32:57 +0200204 var p = g.appendChild(this._c("path"));
Akron15175132017-09-07 18:12:55 +0200205 p.setAttribute('class', 'edge');
Akron65d31082017-09-08 16:23:40 +0200206
207 // Attach the new arc before drawing, so computed values are available
208 this._arcsElement.appendChild(g);
Akronf5dc5102017-05-16 20:32:57 +0200209
210 // Create arc
211 var middle = Math.abs(endPos - startPos) / 2;
212
Akron63ae00b2017-05-16 22:03:36 +0200213 // TODO: take the number of tokens into account!
Akronc5b5f742017-05-23 16:04:35 +0200214 var cHeight = this.arcDiff + arc.overlaps * this.overlapDiff + (middle / 2);
215
216 // Respect the maximum height
217 cHeight = cHeight < this.maxArc ? cHeight : this.maxArc;
Akronf5dc5102017-05-16 20:32:57 +0200218
219 var x = Math.min(startPos, endPos);
Akronc5b5f742017-05-23 16:04:35 +0200220
Akron3a4a08e2017-05-23 22:34:18 +0200221 //var controlY = (startY + endY - cHeight);
222 var controlY = (endY - cHeight);
Akronf5dc5102017-05-16 20:32:57 +0200223
Akron3a4a08e2017-05-23 22:34:18 +0200224 var arcE = "M "+ startPos + "," + startY +
225 " C " + startPos + "," + controlY +
226 " " + endPos + "," + controlY +
227 " " + endPos + "," + endY;
Akrond67d45b2017-05-18 21:47:38 +0200228
Akron63ae00b2017-05-16 22:03:36 +0200229 p.setAttribute("d", arcE);
Akronf5dc5102017-05-16 20:32:57 +0200230
Akron3a4a08e2017-05-23 22:34:18 +0200231 if (arc.direction !== undefined) {
232 p.setAttribute("marker-end", "url(#arr)");
233 if (arc.direction === 'bi') {
234 p.setAttribute("marker-start", "url(#arr)");
235 };
236 };
237
Akronc5b5f742017-05-23 16:04:35 +0200238 /*
239 * Calculate the top point of the arc for labeling using
240 * de Casteljau's algorithm, see e.g.
241 * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/
242 * of course simplified to symmetric arcs ...
243 */
244 // Interpolate one side of the control polygon
245 // var controlInterpY1 = (startY + controlY) / 2;
246 // var controlInterpY2 = (controlInterpY1 + controlY) / 2;
247 var middleY = (((startY + controlY) / 2) + controlY) / 2;
248
249 // WARNING!
250 // This won't respect span anchors, adjusting startY and endY!
251
Akron63ae00b2017-05-16 22:03:36 +0200252 if (arc.label !== undefined) {
Akronf5dc5102017-05-16 20:32:57 +0200253 var labelE = g.appendChild(this._c("text"));
254 labelE.setAttribute("x", x + middle);
Akronc5b5f742017-05-23 16:04:35 +0200255 labelE.setAttribute("y", middleY + 3);
Akronf5dc5102017-05-16 20:32:57 +0200256 labelE.setAttribute("text-anchor", "middle");
Akron65d31082017-09-08 16:23:40 +0200257 var textNode = document.createTextNode(arc.label);
258 labelE.appendChild(textNode);
Akron1dc87902017-05-29 16:04:56 +0200259
Akron65d31082017-09-08 16:23:40 +0200260 var labelBox = labelE.getBBox();
261 var textWidth = labelBox.width; // labelE.getComputedTextLength();
262 var textHeight = labelBox.height; // labelE.getComputedTextLength();
Akron1dc87902017-05-29 16:04:56 +0200263
Akron65d31082017-09-08 16:23:40 +0200264 // Add padding to left and right
265
266 // var labelR = g.appendChild(this._c("rect"));
267 var labelR = g.insertBefore(this._c("rect"), labelE);
268 var boxWidth = textWidth + 2 * this.xPadding;
269 labelR.setAttribute("x", x + middle - (boxWidth / 2));
270 labelR.setAttribute("ry", 5);
271 labelR.setAttribute("y", labelBox.y - this.yPadding);
272 labelR.setAttribute("width", boxWidth);
273 labelR.setAttribute("height", textHeight + 2*this.yPadding);
Akronf5dc5102017-05-16 20:32:57 +0200274 };
Akron65d31082017-09-08 16:23:40 +0200275
276 // return g;
Akronf5dc5102017-05-16 20:32:57 +0200277 },
278
279 element : function () {
280 if (this._element !== undefined)
281 return this._element;
282
283 // Create svg
284 var svg = this._c("svg");
Akron3a4a08e2017-05-23 22:34:18 +0200285
286 window.addEventListener("resize", function () {
287 // TODO: Only if text-size changed!
288 this.show();
289 }.bind(this));
290
291 var defs = svg.appendChild(this._c("defs"));
292 var marker = defs.appendChild(this._c("marker"));
293 marker.setAttribute("refX", 9);
294 marker.setAttribute("id", "arr");
295 marker.setAttribute("orient", "auto-start-reverse");
296 marker.setAttribute("markerUnits","userSpaceOnUse");
297
298 var arrow = this._c("path");
299 arrow.setAttribute("transform", "scale(0.8)");
300 arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
301 marker.appendChild(arrow);
302
Akronf5dc5102017-05-16 20:32:57 +0200303 this._element = svg;
304 return this._element;
305 },
306
307 // Add a relation with a start, an end,
308 // a direction value and a label text
Akronc5b5f742017-05-23 16:04:35 +0200309 addRel : function (rel) {
310 this._arcs.push(rel);
311 return this;
Akronf5dc5102017-05-16 20:32:57 +0200312 },
313
Akronc5b5f742017-05-23 16:04:35 +0200314
315 addToken : function(token) {
316 this._tokens.push(token);
317 return this;
318 },
319
Akronf5dc5102017-05-16 20:32:57 +0200320 /*
321 * All arcs need to be sorted before shown,
322 * to avoid nesting.
323 */
324 _sortArcs : function () {
325
Akrond67d45b2017-05-18 21:47:38 +0200326
327 // TODO:
328 // Keep in mind that the arcs may have long anchors!
329 // 1. Iterate over all arcs
330 // 2. Sort all multi
331 var anchors = [];
332
Akronf5dc5102017-05-16 20:32:57 +0200333 // 1. Sort by length
334 // 2. Tag all spans with the number of overlaps before
335 // a) Iterate over all spans
336 // b) check the latest preceeding overlapping span (lpos)
337 // -> not found: tag with 0
338 // -> found: Add +1 to the level of the (lpos)
339 // c) If the new tag is smaller than the previous element,
340 // reorder
Akron63ae00b2017-05-16 22:03:36 +0200341
342 // Normalize start and end
343 var sortedArcs = this._arcs.map(function (v) {
Akrond67d45b2017-05-18 21:47:38 +0200344
345 // Check for long anchors
346 if (v.start instanceof Array) {
347 var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
348
349 v.startAnchor = {
Akron3a4a08e2017-05-23 22:34:18 +0200350 "first": v.start[0],
351 "last" : v.start[1],
Akrond67d45b2017-05-18 21:47:38 +0200352 "length" : v.start[1] - v.start[0]
353 };
354
355 // Add to anchors list
356 anchors.push(v.startAnchor);
357 v.start = middle;
358 };
359
360 if (v.end instanceof Array) {
361 var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
362 v.endAnchor = {
Akron3a4a08e2017-05-23 22:34:18 +0200363 "first": v.end[0],
364 "last" : v.end[1],
Akrond67d45b2017-05-18 21:47:38 +0200365 "length" : v.end[1] - v.end[0]
366 };
367
368 // Add to anchors list
369 anchors.push(v.endAnchor);
370 v.end = middle;
371 };
372
373 // calculate the arch length
Akron63ae00b2017-05-16 22:03:36 +0200374 if (v.start < v.end) {
375 v.first = v.start;
376 v.last = v.end;
377 v.length = v.end - v.start;
378 }
379 else {
380 v.first = v.end;
381 v.last = v.start;
382 v.length = v.start - v.end;
383 };
384 return v;
385 });
386
387 // Sort based on length
388 sortedArcs.sort(function (a, b) {
389 if (a.length < b.length)
390 return -1;
391 else
392 return 1;
393 });
394
Akron3a4a08e2017-05-23 22:34:18 +0200395 this._sortedArcs = lengthSort(sortedArcs, false);
Akrond67d45b2017-05-18 21:47:38 +0200396 this._sortedAnchors = lengthSort(anchors, true);
Akronf5dc5102017-05-16 20:32:57 +0200397 },
398
399 show : function () {
400 var svg = this._element;
Akron3a4a08e2017-05-23 22:34:18 +0200401 var height = this.maxArc;
402
Akron3a4a08e2017-05-23 22:34:18 +0200403 // Delete old group
404 if (svg.getElementsByTagName("g")[0] !== undefined) {
405 var group = svg.getElementsByTagName("g")[0];
406 svg.removeChild(group);
407 this._tokenElements = [];
408 };
409
410 var g = svg.appendChild(this._c("g"));
Akronf5dc5102017-05-16 20:32:57 +0200411
412 /*
Akron15175132017-09-07 18:12:55 +0200413 * Create token list
Akronf5dc5102017-05-16 20:32:57 +0200414 */
Akron3a4a08e2017-05-23 22:34:18 +0200415 var text = g.appendChild(this._c("text"));
Akron3d204282017-09-07 18:24:18 +0200416 text.setAttribute('class', 'leaf');
Akron3a4a08e2017-05-23 22:34:18 +0200417 text.setAttribute("text-anchor", "start");
418 text.setAttribute("y", height);
Akronf5dc5102017-05-16 20:32:57 +0200419
Akron3a4a08e2017-05-23 22:34:18 +0200420 this._y = height - (this.anchorStart);
421
422 var ws = text.appendChild(this._c("tspan"));
423 ws.appendChild(document.createTextNode('\u00A0'));
424 ws.style.textAnchor = "start";
425
Akronf5dc5102017-05-16 20:32:57 +0200426 var lastRight = 0;
427 for (var node_i in this._tokens) {
428 // Append svg
429 var tspan = text.appendChild(this._c("tspan"));
430 tspan.appendChild(document.createTextNode(this._tokens[node_i]));
Akron3a4a08e2017-05-23 22:34:18 +0200431 tspan.setAttribute("text-anchor", "middle");
432
Akronf5dc5102017-05-16 20:32:57 +0200433 this._tokenElements.push(tspan);
434
435 // Add whitespace!
Akron3a4a08e2017-05-23 22:34:18 +0200436 //var ws = text.appendChild(this._c("tspan"));
437 //ws.appendChild(document.createTextNode(" "));
438 // ws.setAttribute("class", "rel-ws");
439 tspan.setAttribute("dx", this.tokenSep);
Akronf5dc5102017-05-16 20:32:57 +0200440 };
441
Akron15175132017-09-07 18:12:55 +0200442 var globalBoundingBox = g.getBoundingClientRect();
443 this.offsetLeft = globalBoundingBox.left;
444
Akron3a4a08e2017-05-23 22:34:18 +0200445 var arcs = g.appendChild(this._c("g"));
Akron65d31082017-09-08 16:23:40 +0200446 this._arcsElement = arcs;
447
Akron3a4a08e2017-05-23 22:34:18 +0200448 arcs.classList.add("arcs");
Akron15175132017-09-07 18:12:55 +0200449
450 // Sort arcs if not sorted yet
Akron3a4a08e2017-05-23 22:34:18 +0200451 if (this._sortedArcs === undefined) {
452 this._sortArcs();
453 };
Akrond67d45b2017-05-18 21:47:38 +0200454
455 var i;
Akron15175132017-09-07 18:12:55 +0200456
457 // Draw all anchors
Akrond67d45b2017-05-18 21:47:38 +0200458 for (i in this._sortedAnchors) {
Akron65d31082017-09-08 16:23:40 +0200459 this._drawAnchor(this._sortedAnchors[i]);
Akrond67d45b2017-05-18 21:47:38 +0200460 };
Akron15175132017-09-07 18:12:55 +0200461
462
463 // draw all arcs
Akrond67d45b2017-05-18 21:47:38 +0200464 for (i in this._sortedArcs) {
Akron65d31082017-09-08 16:23:40 +0200465 this._drawArc(this._sortedArcs[i]);
Akronf5dc5102017-05-16 20:32:57 +0200466 };
Akron3a4a08e2017-05-23 22:34:18 +0200467
468 var width = text.getBoundingClientRect().width;
Akronf3d7d8e2017-05-23 22:52:54 +0200469 svg.setAttribute("width", width + 20);
470 svg.setAttribute("height", height + 20);
Akron3a4a08e2017-05-23 22:34:18 +0200471 svg.setAttribute("class", "relTree");
Akronf5dc5102017-05-16 20:32:57 +0200472 }
Akrond67d45b2017-05-18 21:47:38 +0200473 };
474
475 function lengthSort (list, inclusive) {
476
477 /*
478 * The "inclusive" flag allows to
479 * modify the behaviour for inclusivity check,
480 * e.g. if identical start or endpoints mean overlap or not.
481 */
482
483 var stack = [];
484
485 // Iterate over all definitions
486 for (var i = 0; i < list.length; i++) {
487 var current = list[i];
488
489 // Check the stack order
490 var overlaps = 0;
491
492 for (var j = (stack.length - 1); j >= 0; j--) {
493 var check = stack[j];
494
495 // (a..(b..b)..a)
496 if (current.first <= check.first && current.last >= check.last) {
497 overlaps = check.overlaps + 1;
498 break;
499 }
500
501 // (a..(b..a)..b)
502 else if (current.first <= check.first && current.last >= check.first) {
503
504 if (inclusive || (current.first != check.first && current.last != check.first)) {
505 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
506 };
507 }
508
509 // (b..(a..b)..a)
510 else if (current.first <= check.last && current.last >= check.last) {
511
512 if (inclusive || (current.first != check.last && current.last != check.last)) {
513 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
514 };
515 };
516 };
517
518 // Set overlaps
519 current.overlaps = overlaps;
520
521 stack.push(current);
522
523 // Although it is already sorted,
524 // the new item has to be put at the correct place
525 // TODO: Use something like splice() instead
526 stack.sort(function (a,b) {
527 b.overlaps - a.overlaps
528 });
529 };
530
531 return stack;
532 };
Akronf5dc5102017-05-16 20:32:57 +0200533});