blob: 4a1ddc2b5671b9a87feec62654f1496343dc0e1b [file] [log] [blame]
Akron3ebfd4e2017-11-13 17:56:49 +01001/**
2 * Parse a relational tree and visualize using arcs.
3 *
4 * @author Nils Diewald
5 */
6
7define([], function () {
8 "use strict";
9
10 var svgNS = "http://www.w3.org/2000/svg";
11 var _TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
12
13 return {
14 create : function (snippet) {
15 return Object.create(this)._init(snippet);
16 },
17
18 // Initialize the state of the object
19 _init : function (snippet) {
20
21 // Predefine some values
22 this._tokens = [];
23 this._arcs = [];
24 this._tokenElements = [];
25 this._y = 0;
Akron430332b2017-11-20 15:36:51 +010026 this._currentInFocus = undefined;
Akron3ebfd4e2017-11-13 17:56:49 +010027
28 // Some configurations
29 this.maxArc = 200; // maximum height of the bezier control point
30 this.overlapDiff = 40; // Difference on overlaps and minimum height for self-refernces
31 this.arcDiff = 15;
32 this.anchorDiff = 8;
33 this.anchorStart = 15;
34 this.tokenSep = 30;
35 this.xPadding = 10;
36 this.yPadding = 5;
37
38 // No snippet to process
39 if (snippet == undefined || snippet == null)
40 return this;
41
42 // Parse the snippet
43 var html = document.createElement("div");
44 html.innerHTML = snippet;
45
46 // Establish temporary parsing memory
47 this.temp = {
48 target : {}, // Remember the map id => pos
49 edges : [], // Remember edge definitions
50 pos : 0 // Keep track of the current token position
51 };
52
53 // Start parsing from root
54 this._parse(0, html.childNodes, undefined);
55
56 // Establish edge list
57 var targetMap = this.temp['target'];
58 var edges = this.temp['edges'];
59
60 // Iterate over edge lists
61 // TODO:
62 // Support spans for anchors!
63 for (var i in edges) {
64 var edge = edges[i];
65
66 // Check the target identifier
67 var targetID = edge.targetID;
68 var target = targetMap[targetID];
69
70 if (target != undefined) {
71
72 // Check if the source is a span anchor
73 /*
74 var start = edge.srcStart;
75 if (start !== edge.srcEnd) {
76 start = [start, edge.srcEnd];
77 };
78 */
79
80 // Add relation
81 var relation = {
82 start : [edge.srcStart, edge.srcEnd],
83 end : target,
84 direction : 'uni',
85 label : edge.label
86 };
87 // console.log(relation);
88 this.addRel(relation);
89 };
90 };
91
92 // Reset parsing memory
93 this.temp = {};
94
95 return this;
96 },
97
98 // Parse a node of the tree snippet
99 _parse : function (parent, children, mark) {
100
101 // Iterate over all child nodes
102 for (var i in children) {
103 var c = children[i];
104
105 // Element node
106 if (c.nodeType == 1) {
107
Akron63581052018-01-31 17:50:59 +0100108 var xmlid, target, start, end;
Akron3ebfd4e2017-11-13 17:56:49 +0100109
110 // Node is an identifier
111 if (c.hasAttribute('xml:id')) {
112
113 // Remember that pos has this identifier
114 xmlid = c.getAttribute('xml:id');
Akron63581052018-01-31 17:50:59 +0100115 // this.temp['target'][xmlid] =
116 start = this.temp['pos'];
117 end = this.temp['pos'];
Akron3ebfd4e2017-11-13 17:56:49 +0100118 }
119
Akron63581052018-01-31 17:50:59 +0100120 // Node is a link
121 // Stricter: && c.hasAttribute('xlink:show')
Akron3ebfd4e2017-11-13 17:56:49 +0100122 else if (c.hasAttribute('xlink:href')) {
Akron3ebfd4e2017-11-13 17:56:49 +0100123
Akron63581052018-01-31 17:50:59 +0100124 // Node is a continuation
125 if (c.getAttribute('xlink:show') == "other" &&
126 c.hasAttribute('data-action') &&
127 c.getAttribute('data-action') == "join"
128 ) {
129 xmlid = c.getAttribute('xlink:href').replace(/^#/, "");
130 start = this.temp['pos'];
131 end = this.temp['pos'];
Akron3ebfd4e2017-11-13 17:56:49 +0100132
Akron63581052018-01-31 17:50:59 +0100133 // this.temp['target'][xmlid][1] = this.temp['pos'] -1;
134 // console.log("Here");
135 }
136
137 // Node is a relation
138 // Stricter: c.getAttribute('xlink:show') == "none"
139 else {
140 var label;
141
142 // Get target id
143 target = c.getAttribute('xlink:href').replace(/^#/, "");
144
145 if (c.hasAttribute('xlink:title')) {
146 label = this._clean(c.getAttribute('xlink:title'));
147 };
148
149 // Remember the defined edge
150 var edge = {
151 label : label,
152 srcStart : this.temp['pos'],
153 targetID : target
154 };
155
156 // TEMP: Hot-fix for root relations
157 if (!label.match(/--$/) && !label.match(/ROOT$/)) {
158 this.temp['edges'].push(edge);
159 };
160
Akron3ebfd4e2017-11-13 17:56:49 +0100161 };
Akron3ebfd4e2017-11-13 17:56:49 +0100162 };
163
164 // Go on with child nodes
165 if (c.hasChildNodes()) {
166 this._parse(0, c.childNodes, mark);
167 };
168
Akron63581052018-01-31 17:50:59 +0100169
170 // The element is a defined anchor
Akron3ebfd4e2017-11-13 17:56:49 +0100171 if (xmlid !== undefined) {
Akron63581052018-01-31 17:50:59 +0100172
173 // this.temp['target'][xmlid][1] = this.temp['pos'] -1;
174
175 // Element already defined
176 if (this.temp['target'][xmlid] !== undefined) {
177 var newtarget = this.temp['target'][xmlid];
178 end = this.temp['pos'] - 1;
179 newtarget[0] = start < newtarget[0] ? start : newtarget[0];
180 newtarget[1] = end > newtarget[1] ? end : newtarget[1];
181 }
182
183 // Element not yet defined
184 else {
185 end = this.temp['pos'] - 1;
186 this.temp['target'][xmlid] = [start, end];
187 };
Akron3ebfd4e2017-11-13 17:56:49 +0100188
189 /*
190 console.log('Target ' + xmlid + ' spans from ' +
191 this.temp['target'][xmlid][0] +
192 ' to ' +
193 this.temp['target'][xmlid][1]
194 );
195 */
196 xmlid = undefined;
197 }
Akron63581052018-01-31 17:50:59 +0100198
199 // Current element describes an arc
Akron3ebfd4e2017-11-13 17:56:49 +0100200 else if (target !== undefined) {
Akron63581052018-01-31 17:50:59 +0100201
202 // TODO: This does not work yet!
Akron3ebfd4e2017-11-13 17:56:49 +0100203 edge["srcEnd"] = this.temp['pos'] -1;
Akron63581052018-01-31 17:50:59 +0100204 // console.log('Here');
Akron3ebfd4e2017-11-13 17:56:49 +0100205
206 /*
207 console.log('Source spans from ' +
208 edge["srcStart"] +
209 ' to ' +
210 edge["srcEnd"]
211 );
212 */
213 target = undefined;
214 };
215 }
216
217 // Text node
218 else if (c.nodeType == 3) {
219
220 // Check, if there is a non-whitespace token
221 if (c.nodeValue !== undefined) {
222 var str = c.nodeValue.trim();
223 if (str !== undefined && str.length > 0) {
224
225 // Add token to token list
226 this.addToken(str);
227
228 // Move token position
229 this.temp['pos']++;
230 };
231 };
232 }
233 };
Akron63581052018-01-31 17:50:59 +0100234
235 // Todo: define edges here!
Akron3ebfd4e2017-11-13 17:56:49 +0100236 },
237
238
239 // Remove foundry and layer for labels
240 _clean : function (title) {
241 return title.replace(_TermRE, "$3");
242 },
243
244
245 // Return the number of leaf nodes
246 // (not necessarily part of a relation).
247 // Consecutive nodes that are not part of any
248 // relation are summarized in one node.
249 size : function () {
250 return this._tokens.length;
251 },
252
253
254 // This is a shorthand for SVG element creation
255 _c : function (tag) {
256 return document.createElementNS(svgNS, tag);
257 },
258
259 // Get bounding box - with workaround for text nodes
260 _rect : function (node) {
261 if (node.tagName == "tspan" && !navigator.userAgent.match(/Edge/)) {
262 var range = document.createRange();
263 range.selectNode(node);
264 var rect = range.getBoundingClientRect();
265 range.detach();
266 return rect;
267 };
268 return node.getBoundingClientRect();
269 },
270
271 // Returns the center point of the requesting token
272 _tokenPoint : function (node) {
273 var box = this._rect(node);
274 return box.left + (box.width / 2);
275 },
276
277
278 // Draws an anchor
279 _drawAnchor : function (anchor) {
280
281 // Calculate the span of the first and last token, the anchor spans
282 var firstBox = this._rect(this._tokenElements[anchor.first]);
283 var lastBox = this._rect(this._tokenElements[anchor.last]);
284
285 var startPos = firstBox.left - this.offsetLeft;
286 var endPos = lastBox.right - this.offsetLeft;
287
288 var y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;
289
290 var l = this._c('path');
291 this._arcsElement.appendChild(l);
292 var pathStr = "M " + startPos + "," + y + " L " + endPos + "," + y;
293 l.setAttribute("d", pathStr);
294 l.setAttribute("class", "anchor");
295 anchor.element = l;
296 anchor.y = y;
297 return l;
298 },
299
300
301 // Create an arc with an optional label
302 // Potentially needs a height parameter for stacks
303 _drawArc : function (arc) {
304
305 var startPos, endPos;
306 var startY = this._y;
307 var endY = this._y;
308
309 if (arc.startAnchor !== undefined) {
310 startPos = this._tokenPoint(arc.startAnchor.element);
311 startY = arc.startAnchor.y;
312 }
313 else {
314 startPos = this._tokenPoint(this._tokenElements[arc.first]);
315 };
316
317 if (arc.endAnchor !== undefined) {
318 endPos = this._tokenPoint(arc.endAnchor.element)
319 endY = arc.endAnchor.y;
320 }
321 else {
322 endPos = this._tokenPoint(this._tokenElements[arc.last]);
323 };
324
Akron3ebfd4e2017-11-13 17:56:49 +0100325 startPos -= this.offsetLeft;
326 endPos -= this.offsetLeft;
327
328 // Special treatment for self-references
329 var overlaps = arc.overlaps;
330 if (startPos == endPos) {
331 startPos -= this.overlapDiff / 3;
332 endPos += this.overlapDiff / 3;
333 overlaps += .5;
334 };
335
336 var g = this._c("g");
337 g.setAttribute("class", "arc");
338 var p = g.appendChild(this._c("path"));
339 p.setAttribute('class', 'edge');
340
341 // Attach the new arc before drawing, so computed values are available
342 this._arcsElement.appendChild(g);
343
344 // Create arc
345 var middle = Math.abs(endPos - startPos) / 2;
346
347 // TODO:
348 // take the number of tokens into account!
349 var cHeight = this.arcDiff + (overlaps * this.overlapDiff) + (middle / 2);
350
351 // Respect the maximum height
352 cHeight = cHeight < this.maxArc ? cHeight : this.maxArc;
353
354 var x = Math.min(startPos, endPos);
355
356 //var controlY = (startY + endY - cHeight);
357 var controlY = (endY - cHeight);
358
359 var arcE = "M "+ startPos + "," + startY +
360 " C " + startPos + "," + controlY +
361 " " + endPos + "," + controlY +
362 " " + endPos + "," + endY;
363
364 p.setAttribute("d", arcE);
365
366 if (arc.direction !== undefined) {
367 p.setAttribute("marker-end", "url(#arr)");
368 if (arc.direction === 'bi') {
369 p.setAttribute("marker-start", "url(#arr)");
370 };
371 };
372
373 if (arc.label === undefined)
374 return g;
375
376 /*
377 * Calculate the top point of the arc for labeling using
378 * de Casteljau's algorithm, see e.g.
379 * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/
380 * of course simplified to symmetric arcs ...
381 */
382 // Interpolate one side of the control polygon
383 var middleY = (((startY + controlY) / 2) + controlY) / 2;
384
385 // Create a boxed label
Akron430332b2017-11-20 15:36:51 +0100386 var label = this._c("g");
387 label.setAttribute("class", "label");
388 this._labelsElement.appendChild(label);
389
390 // Set arc reference
391 label.arcRef = g;
Akron3ebfd4e2017-11-13 17:56:49 +0100392
393 var that = this;
Akron430332b2017-11-20 15:36:51 +0100394 label.addEventListener('mouseenter', function () {
395 that.inFocus(this);
Akron3ebfd4e2017-11-13 17:56:49 +0100396 });
397
Akron430332b2017-11-20 15:36:51 +0100398 var labelE = label.appendChild(this._c("text"));
Akron3ebfd4e2017-11-13 17:56:49 +0100399 labelE.setAttribute("x", x + middle);
400 labelE.setAttribute("y", middleY + 3);
401 labelE.setAttribute("text-anchor", "middle");
402 var textNode = document.createTextNode(arc.label);
403 labelE.appendChild(textNode);
404
405 var labelBox = labelE.getBBox();
406 var textWidth = labelBox.width; // labelE.getComputedTextLength();
407 var textHeight = labelBox.height; // labelE.getComputedTextLength();
408
409 // Add box with padding to left and right
Akron430332b2017-11-20 15:36:51 +0100410 var labelR = label.insertBefore(this._c("rect"), labelE);
Akron3ebfd4e2017-11-13 17:56:49 +0100411 var boxWidth = textWidth + 2 * this.xPadding;
412 labelR.setAttribute("x", x + middle - (boxWidth / 2));
413 labelR.setAttribute("ry", 5);
414 labelR.setAttribute("y", labelBox.y - this.yPadding);
415 labelR.setAttribute("width", boxWidth);
416 labelR.setAttribute("height", textHeight + 2 * this.yPadding);
417 },
418
419 // Get the svg element
420 element : function () {
421 if (this._element !== undefined)
422 return this._element;
423
424 // Create svg
425 var svg = this._c("svg");
426
427 window.addEventListener("resize", function () {
428 // TODO:
429 // Only if text-size changed!
430 // TODO:
431 // This is currently untested
432 this.show();
433 }.bind(this));
434
435 // Define marker arrows
436 var defs = svg.appendChild(this._c("defs"));
437 var marker = defs.appendChild(this._c("marker"));
438 marker.setAttribute("refX", 9);
439 marker.setAttribute("id", "arr");
440 marker.setAttribute("orient", "auto-start-reverse");
441 marker.setAttribute("markerUnits","userSpaceOnUse");
442 var arrow = this._c("path");
443 arrow.setAttribute("transform", "scale(0.8)");
444 arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
445 marker.appendChild(arrow);
446
447 this._element = svg;
448 return this._element;
449 },
450
451 // Add a relation with a start, an end,
452 // a direction value and an optional label text
453 addRel : function (rel) {
454 this._arcs.push(rel);
455 return this;
456 },
457
458
459 // Add a token to the list (this will mostly be a word)
460 addToken : function(token) {
461 this._tokens.push(token);
462 return this;
463 },
Akron430332b2017-11-20 15:36:51 +0100464
465
466 // Move label and arc in focus
467 inFocus : function (element) {
468 var cif;
469
470 if (this._currentInFocus) {
471
472 // Already in focus
473 if (this._currentInFocus === element)
474 return;
475
476 cif = this._currentInFocus;
477 cif.classList.remove('infocus');
478 cif.arcRef.classList.remove('infocus');
479 };
480
481 cif = this._currentInFocus = element;
482 this._labelsElement.appendChild(cif);
483 this._arcsElement.appendChild(cif.arcRef);
484 cif.classList.add('infocus');
485 cif.arcRef.classList.add('infocus');
486 },
Akron3ebfd4e2017-11-13 17:56:49 +0100487
488 /*
489 * All arcs need to be sorted before shown,
490 * to avoid nesting.
491 */
492 _sortArcs : function () {
493
494 // TODO:
495 // Keep in mind that the arcs may have long anchors!
496 // 1. Iterate over all arcs
497 // 2. Sort all multi
498 var anchors = {};
499
500 // 1. Sort by length
501 // 2. Tag all spans with the number of overlaps before
502 // a) Iterate over all spans
503 // b) check the latest preceeding overlapping span (lpos)
504 // -> not found: tag with 0
505 // -> found: Add +1 to the level of the (lpos)
506 // c) If the new tag is smaller than the previous element,
507 // reorder
508
509 // Normalize start and end
510 var sortedArcs = this._arcs.map(function (v) {
511
512 // Check for long anchors
513 if (v.start instanceof Array) {
514
515 if (v.start[0] == v.start[1]) {
516 v.start = v.start[0];
517 }
518
519 else {
520
521 var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
522
523 // Calculate signature to avoid multiple anchors
524 var anchorSig = "#" + v.start[0] + "_" + v.start[1];
525 if (v.start[0] > v.start[1]) {
526 anchorSig = "#" + v.start[1] + "_" + v.start[0];
527 };
528
529 // Check if the anchor already exist
530 var anchor = anchors[anchorSig];
531 if (anchor === undefined) {
532 anchor = {
533 "first": v.start[0],
534 "last" : v.start[1],
535 "length" : v.start[1] - v.start[0]
536 };
537 anchors[anchorSig] = anchor;
538 // anchors.push(v.startAnchor);
539 };
540
541 v.startAnchor = anchor;
542
543 // Add to anchors list
544 v.start = middle;
545 };
546 };
547
548 if (v.end instanceof Array) {
549
550 if (v.end[0] == v.end[1]) {
551 v.end = v.end[0];
552 }
553
554 else {
555
556 var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
557
558 // Calculate signature to avoid multiple anchors
559 var anchorSig = "#" + v.end[0] + "_" + v.end[1];
560 if (v.end[0] > v.end[1]) {
561 anchorSig = "#" + v.end[1] + "_" + v.end[0];
562 };
563
564 // Check if the anchor already exist
565 var anchor = anchors[anchorSig];
566 if (anchor === undefined) {
567 anchor = {
568 "first": v.end[0],
569 "last" : v.end[1],
570 "length" : v.end[1] - v.end[0]
571 };
572 anchors[anchorSig] = anchor;
573 // anchors.push(v.startAnchor);
574 };
575
576 v.endAnchor = anchor;
577
578 // Add to anchors list
579 // anchors.push(v.endAnchor);
580 v.end = middle;
581 };
582 };
583
584 v.first = v.start;
585 v.last = v.end;
586
587 // calculate the arch length
588 if (v.start < v.end) {
589 v.length = v.end - v.start;
590 }
591 else {
592 // v.first = v.end;
593 // v.last = v.start;
594 v.length = v.start - v.end;
595 };
596
597 return v;
598 });
599
600 // Sort based on length
601 sortedArcs.sort(function (a, b) {
602 if (a.length < b.length)
603 return -1;
604 else
605 return 1;
606 });
607
608 // Add sorted arcs and anchors
609 this._sortedArcs = lengthSort(sortedArcs, false);
610
611 // Translate map to array (there is probably a better JS method)
612 var sortedAnchors = [];
613 for (var i in anchors) {
614 sortedAnchors.push(anchors[i]);
615 };
616 this._sortedAnchors = lengthSort(sortedAnchors, true);
617 },
618
619 /**
620 * Center the viewport of the canvas
621 * TODO:
622 * This is identical to tree
623 */
624 center : function () {
625 if (this._element === undefined)
626 return;
627
628 var treeDiv = this._element.parentNode;
629
630 var cWidth = parseFloat(window.getComputedStyle(this._element).width);
631 var treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
632 // Reposition:
633 if (cWidth > treeWidth) {
634 var scrollValue = (cWidth - treeWidth) / 2;
635 treeDiv.scrollLeft = scrollValue;
636 };
637 },
638
639
640 // Show the element
641 show : function () {
642 var svg = this._element;
643 var height = this.maxArc;
644
645 // Delete old group
646 if (svg.getElementsByTagName("g")[0] !== undefined) {
647 var group = svg.getElementsByTagName("g")[0];
648 svg.removeChild(group);
649 this._tokenElements = [];
650 };
651
652 var g = svg.appendChild(this._c("g"));
653
654 // Draw token list
655 var text = g.appendChild(this._c("text"));
656 text.setAttribute('class', 'leaf');
657 text.setAttribute("text-anchor", "start");
658 text.setAttribute("y", height);
659
660 // Calculate the start position
661 this._y = height - (this.anchorStart);
662
663 // Introduce some prepending whitespace (yeah - I know ...)
664 var ws = text.appendChild(this._c("tspan"));
665 ws.appendChild(document.createTextNode('\u00A0'));
666 ws.style.textAnchor = "start";
667
668 var lastRight = 0;
669 for (var node_i in this._tokens) {
670 // Append svg
671 // var x = text.appendChild(this._c("text"));
672 var tspan = text.appendChild(this._c("tspan"));
673 tspan.appendChild(document.createTextNode(this._tokens[node_i]));
674 tspan.setAttribute("text-anchor", "middle");
675
676 this._tokenElements.push(tspan);
677
678 // Add whitespace!
679 tspan.setAttribute("dx", this.tokenSep);
680 };
681
682 // Get some global position data that may change on resize
683 var globalBoundingBox = this._rect(g);
684 this.offsetLeft = globalBoundingBox.left;
685
686 // The group of arcs
687 var arcs = g.appendChild(this._c("g"));
688 this._arcsElement = arcs;
689 arcs.classList.add("arcs");
690
691 var labels = g.appendChild(this._c("g"));
692 this._labelsElement = labels;
693 labels.classList.add("labels");
694
695 // Sort arcs if not sorted yet
696 if (this._sortedArcs === undefined)
697 this._sortArcs();
698
699 // 1. Draw all anchors
700 var i;
701 for (i in this._sortedAnchors) {
702 this._drawAnchor(this._sortedAnchors[i]);
703 };
704
705 // 2. Draw all arcs
706 for (i in this._sortedArcs) {
707 this._drawArc(this._sortedArcs[i]);
708 };
709
710 // Resize the svg with some reasonable margins
711 var width = this._rect(text).width;
712 svg.setAttribute("width", width + 20);
713 svg.setAttribute("height", height + 20);
714 svg.setAttribute("class", "relTree");
715 }
716 };
717
718 // Sort relations regarding their span
719 function lengthSort (list, inclusive) {
720
721 /*
722 * The "inclusive" flag allows to
723 * modify the behaviour for inclusivity check,
724 * e.g. if identical start or endpoints mean overlap or not.
725 */
726
727 var stack = [];
728
729 // Iterate over all definitions
730 for (var i = 0; i < list.length; i++) {
731 var current = list[i];
732
733 // Check the stack order
734 var overlaps = 0;
735 for (var j = (stack.length - 1); j >= 0; j--) {
736 var check = stack[j];
737
738 // (a..(b..b)..a)
739 if (current.first <= check.first && current.last >= check.last) {
740 overlaps = check.overlaps + 1;
741 break;
742 }
743
744 // (a..(b..a)..b)
745 else if (current.first <= check.first && current.last >= check.first) {
746
747 if (inclusive || (current.first != check.first && current.last != check.first)) {
748 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
749 };
750 }
751
752 // (b..(a..b)..a)
753 else if (current.first <= check.last && current.last >= check.last) {
754
755 if (inclusive || (current.first != check.last && current.last != check.last)) {
756 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
757 };
758 };
759 };
760
761 // Set overlaps
762 current.overlaps = overlaps;
763
764 stack.push(current);
765
766 // Although it is already sorted,
767 // the new item has to be put at the correct place
768 // TODO:
769 // Use something like splice() instead
770 stack.sort(function (a,b) {
771 b.overlaps - a.overlaps
772 });
773 };
774
775 return stack;
776 };
777});