blob: 921c699cc419eea86287463bedf82b9388108afe [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
108 var xmlid, target;
109
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');
115 this.temp['target'][xmlid] = [this.temp['pos'], this.temp['pos']];
116 }
117
118 // Node is a relation
119 else if (c.hasAttribute('xlink:href')) {
120 var label;
121
122 // Get target id
123 target = c.getAttribute('xlink:href').replace(/^#/, "");
124
125 if (c.hasAttribute('xlink:title')) {
126 label = this._clean(c.getAttribute('xlink:title'));
127 };
128
129 // Remember the defined edge
130 var edge = {
131 label : label,
132 srcStart : this.temp['pos'],
133 targetID : target
134 };
135 this.temp['edges'].push(edge);
136 };
137
138 // Go on with child nodes
139 if (c.hasChildNodes()) {
140 this._parse(0, c.childNodes, mark);
141 };
142
143 if (xmlid !== undefined) {
144 this.temp['target'][xmlid][1] = this.temp['pos'] -1;
145
146 /*
147 console.log('Target ' + xmlid + ' spans from ' +
148 this.temp['target'][xmlid][0] +
149 ' to ' +
150 this.temp['target'][xmlid][1]
151 );
152 */
153 xmlid = undefined;
154 }
155 else if (target !== undefined) {
156 edge["srcEnd"] = this.temp['pos'] -1;
157
158 /*
159 console.log('Source spans from ' +
160 edge["srcStart"] +
161 ' to ' +
162 edge["srcEnd"]
163 );
164 */
165 target = undefined;
166 };
167 }
168
169 // Text node
170 else if (c.nodeType == 3) {
171
172 // Check, if there is a non-whitespace token
173 if (c.nodeValue !== undefined) {
174 var str = c.nodeValue.trim();
175 if (str !== undefined && str.length > 0) {
176
177 // Add token to token list
178 this.addToken(str);
179
180 // Move token position
181 this.temp['pos']++;
182 };
183 };
184 }
185 };
186 },
187
188
189 // Remove foundry and layer for labels
190 _clean : function (title) {
191 return title.replace(_TermRE, "$3");
192 },
193
194
195 // Return the number of leaf nodes
196 // (not necessarily part of a relation).
197 // Consecutive nodes that are not part of any
198 // relation are summarized in one node.
199 size : function () {
200 return this._tokens.length;
201 },
202
203
204 // This is a shorthand for SVG element creation
205 _c : function (tag) {
206 return document.createElementNS(svgNS, tag);
207 },
208
209 // Get bounding box - with workaround for text nodes
210 _rect : function (node) {
211 if (node.tagName == "tspan" && !navigator.userAgent.match(/Edge/)) {
212 var range = document.createRange();
213 range.selectNode(node);
214 var rect = range.getBoundingClientRect();
215 range.detach();
216 return rect;
217 };
218 return node.getBoundingClientRect();
219 },
220
221 // Returns the center point of the requesting token
222 _tokenPoint : function (node) {
223 var box = this._rect(node);
224 return box.left + (box.width / 2);
225 },
226
227
228 // Draws an anchor
229 _drawAnchor : function (anchor) {
230
231 // Calculate the span of the first and last token, the anchor spans
232 var firstBox = this._rect(this._tokenElements[anchor.first]);
233 var lastBox = this._rect(this._tokenElements[anchor.last]);
234
235 var startPos = firstBox.left - this.offsetLeft;
236 var endPos = lastBox.right - this.offsetLeft;
237
238 var y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;
239
240 var l = this._c('path');
241 this._arcsElement.appendChild(l);
242 var pathStr = "M " + startPos + "," + y + " L " + endPos + "," + y;
243 l.setAttribute("d", pathStr);
244 l.setAttribute("class", "anchor");
245 anchor.element = l;
246 anchor.y = y;
247 return l;
248 },
249
250
251 // Create an arc with an optional label
252 // Potentially needs a height parameter for stacks
253 _drawArc : function (arc) {
254
255 var startPos, endPos;
256 var startY = this._y;
257 var endY = this._y;
258
259 if (arc.startAnchor !== undefined) {
260 startPos = this._tokenPoint(arc.startAnchor.element);
261 startY = arc.startAnchor.y;
262 }
263 else {
264 startPos = this._tokenPoint(this._tokenElements[arc.first]);
265 };
266
267 if (arc.endAnchor !== undefined) {
268 endPos = this._tokenPoint(arc.endAnchor.element)
269 endY = arc.endAnchor.y;
270 }
271 else {
272 endPos = this._tokenPoint(this._tokenElements[arc.last]);
273 };
274
Akron3ebfd4e2017-11-13 17:56:49 +0100275 startPos -= this.offsetLeft;
276 endPos -= this.offsetLeft;
277
278 // Special treatment for self-references
279 var overlaps = arc.overlaps;
280 if (startPos == endPos) {
281 startPos -= this.overlapDiff / 3;
282 endPos += this.overlapDiff / 3;
283 overlaps += .5;
284 };
285
286 var g = this._c("g");
287 g.setAttribute("class", "arc");
288 var p = g.appendChild(this._c("path"));
289 p.setAttribute('class', 'edge');
290
291 // Attach the new arc before drawing, so computed values are available
292 this._arcsElement.appendChild(g);
293
294 // Create arc
295 var middle = Math.abs(endPos - startPos) / 2;
296
297 // TODO:
298 // take the number of tokens into account!
299 var cHeight = this.arcDiff + (overlaps * this.overlapDiff) + (middle / 2);
300
301 // Respect the maximum height
302 cHeight = cHeight < this.maxArc ? cHeight : this.maxArc;
303
304 var x = Math.min(startPos, endPos);
305
306 //var controlY = (startY + endY - cHeight);
307 var controlY = (endY - cHeight);
308
309 var arcE = "M "+ startPos + "," + startY +
310 " C " + startPos + "," + controlY +
311 " " + endPos + "," + controlY +
312 " " + endPos + "," + endY;
313
314 p.setAttribute("d", arcE);
315
316 if (arc.direction !== undefined) {
317 p.setAttribute("marker-end", "url(#arr)");
318 if (arc.direction === 'bi') {
319 p.setAttribute("marker-start", "url(#arr)");
320 };
321 };
322
323 if (arc.label === undefined)
324 return g;
325
326 /*
327 * Calculate the top point of the arc for labeling using
328 * de Casteljau's algorithm, see e.g.
329 * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/
330 * of course simplified to symmetric arcs ...
331 */
332 // Interpolate one side of the control polygon
333 var middleY = (((startY + controlY) / 2) + controlY) / 2;
334
335 // Create a boxed label
Akron430332b2017-11-20 15:36:51 +0100336 var label = this._c("g");
337 label.setAttribute("class", "label");
338 this._labelsElement.appendChild(label);
339
340 // Set arc reference
341 label.arcRef = g;
Akron3ebfd4e2017-11-13 17:56:49 +0100342
343 var that = this;
Akron430332b2017-11-20 15:36:51 +0100344 label.addEventListener('mouseenter', function () {
345 that.inFocus(this);
Akron3ebfd4e2017-11-13 17:56:49 +0100346 });
347
Akron430332b2017-11-20 15:36:51 +0100348 var labelE = label.appendChild(this._c("text"));
Akron3ebfd4e2017-11-13 17:56:49 +0100349 labelE.setAttribute("x", x + middle);
350 labelE.setAttribute("y", middleY + 3);
351 labelE.setAttribute("text-anchor", "middle");
352 var textNode = document.createTextNode(arc.label);
353 labelE.appendChild(textNode);
354
355 var labelBox = labelE.getBBox();
356 var textWidth = labelBox.width; // labelE.getComputedTextLength();
357 var textHeight = labelBox.height; // labelE.getComputedTextLength();
358
359 // Add box with padding to left and right
Akron430332b2017-11-20 15:36:51 +0100360 var labelR = label.insertBefore(this._c("rect"), labelE);
Akron3ebfd4e2017-11-13 17:56:49 +0100361 var boxWidth = textWidth + 2 * this.xPadding;
362 labelR.setAttribute("x", x + middle - (boxWidth / 2));
363 labelR.setAttribute("ry", 5);
364 labelR.setAttribute("y", labelBox.y - this.yPadding);
365 labelR.setAttribute("width", boxWidth);
366 labelR.setAttribute("height", textHeight + 2 * this.yPadding);
367 },
368
369 // Get the svg element
370 element : function () {
371 if (this._element !== undefined)
372 return this._element;
373
374 // Create svg
375 var svg = this._c("svg");
376
377 window.addEventListener("resize", function () {
378 // TODO:
379 // Only if text-size changed!
380 // TODO:
381 // This is currently untested
382 this.show();
383 }.bind(this));
384
385 // Define marker arrows
386 var defs = svg.appendChild(this._c("defs"));
387 var marker = defs.appendChild(this._c("marker"));
388 marker.setAttribute("refX", 9);
389 marker.setAttribute("id", "arr");
390 marker.setAttribute("orient", "auto-start-reverse");
391 marker.setAttribute("markerUnits","userSpaceOnUse");
392 var arrow = this._c("path");
393 arrow.setAttribute("transform", "scale(0.8)");
394 arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
395 marker.appendChild(arrow);
396
397 this._element = svg;
398 return this._element;
399 },
400
401 // Add a relation with a start, an end,
402 // a direction value and an optional label text
403 addRel : function (rel) {
404 this._arcs.push(rel);
405 return this;
406 },
407
408
409 // Add a token to the list (this will mostly be a word)
410 addToken : function(token) {
411 this._tokens.push(token);
412 return this;
413 },
Akron430332b2017-11-20 15:36:51 +0100414
415
416 // Move label and arc in focus
417 inFocus : function (element) {
418 var cif;
419
420 if (this._currentInFocus) {
421
422 // Already in focus
423 if (this._currentInFocus === element)
424 return;
425
426 cif = this._currentInFocus;
427 cif.classList.remove('infocus');
428 cif.arcRef.classList.remove('infocus');
429 };
430
431 cif = this._currentInFocus = element;
432 this._labelsElement.appendChild(cif);
433 this._arcsElement.appendChild(cif.arcRef);
434 cif.classList.add('infocus');
435 cif.arcRef.classList.add('infocus');
436 },
Akron3ebfd4e2017-11-13 17:56:49 +0100437
438 /*
439 * All arcs need to be sorted before shown,
440 * to avoid nesting.
441 */
442 _sortArcs : function () {
443
444 // TODO:
445 // Keep in mind that the arcs may have long anchors!
446 // 1. Iterate over all arcs
447 // 2. Sort all multi
448 var anchors = {};
449
450 // 1. Sort by length
451 // 2. Tag all spans with the number of overlaps before
452 // a) Iterate over all spans
453 // b) check the latest preceeding overlapping span (lpos)
454 // -> not found: tag with 0
455 // -> found: Add +1 to the level of the (lpos)
456 // c) If the new tag is smaller than the previous element,
457 // reorder
458
459 // Normalize start and end
460 var sortedArcs = this._arcs.map(function (v) {
461
462 // Check for long anchors
463 if (v.start instanceof Array) {
464
465 if (v.start[0] == v.start[1]) {
466 v.start = v.start[0];
467 }
468
469 else {
470
471 var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
472
473 // Calculate signature to avoid multiple anchors
474 var anchorSig = "#" + v.start[0] + "_" + v.start[1];
475 if (v.start[0] > v.start[1]) {
476 anchorSig = "#" + v.start[1] + "_" + v.start[0];
477 };
478
479 // Check if the anchor already exist
480 var anchor = anchors[anchorSig];
481 if (anchor === undefined) {
482 anchor = {
483 "first": v.start[0],
484 "last" : v.start[1],
485 "length" : v.start[1] - v.start[0]
486 };
487 anchors[anchorSig] = anchor;
488 // anchors.push(v.startAnchor);
489 };
490
491 v.startAnchor = anchor;
492
493 // Add to anchors list
494 v.start = middle;
495 };
496 };
497
498 if (v.end instanceof Array) {
499
500 if (v.end[0] == v.end[1]) {
501 v.end = v.end[0];
502 }
503
504 else {
505
506 var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
507
508 // Calculate signature to avoid multiple anchors
509 var anchorSig = "#" + v.end[0] + "_" + v.end[1];
510 if (v.end[0] > v.end[1]) {
511 anchorSig = "#" + v.end[1] + "_" + v.end[0];
512 };
513
514 // Check if the anchor already exist
515 var anchor = anchors[anchorSig];
516 if (anchor === undefined) {
517 anchor = {
518 "first": v.end[0],
519 "last" : v.end[1],
520 "length" : v.end[1] - v.end[0]
521 };
522 anchors[anchorSig] = anchor;
523 // anchors.push(v.startAnchor);
524 };
525
526 v.endAnchor = anchor;
527
528 // Add to anchors list
529 // anchors.push(v.endAnchor);
530 v.end = middle;
531 };
532 };
533
534 v.first = v.start;
535 v.last = v.end;
536
537 // calculate the arch length
538 if (v.start < v.end) {
539 v.length = v.end - v.start;
540 }
541 else {
542 // v.first = v.end;
543 // v.last = v.start;
544 v.length = v.start - v.end;
545 };
546
547 return v;
548 });
549
550 // Sort based on length
551 sortedArcs.sort(function (a, b) {
552 if (a.length < b.length)
553 return -1;
554 else
555 return 1;
556 });
557
558 // Add sorted arcs and anchors
559 this._sortedArcs = lengthSort(sortedArcs, false);
560
561 // Translate map to array (there is probably a better JS method)
562 var sortedAnchors = [];
563 for (var i in anchors) {
564 sortedAnchors.push(anchors[i]);
565 };
566 this._sortedAnchors = lengthSort(sortedAnchors, true);
567 },
568
569 /**
570 * Center the viewport of the canvas
571 * TODO:
572 * This is identical to tree
573 */
574 center : function () {
575 if (this._element === undefined)
576 return;
577
578 var treeDiv = this._element.parentNode;
579
580 var cWidth = parseFloat(window.getComputedStyle(this._element).width);
581 var treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
582 // Reposition:
583 if (cWidth > treeWidth) {
584 var scrollValue = (cWidth - treeWidth) / 2;
585 treeDiv.scrollLeft = scrollValue;
586 };
587 },
588
589
590 // Show the element
591 show : function () {
592 var svg = this._element;
593 var height = this.maxArc;
594
595 // Delete old group
596 if (svg.getElementsByTagName("g")[0] !== undefined) {
597 var group = svg.getElementsByTagName("g")[0];
598 svg.removeChild(group);
599 this._tokenElements = [];
600 };
601
602 var g = svg.appendChild(this._c("g"));
603
604 // Draw token list
605 var text = g.appendChild(this._c("text"));
606 text.setAttribute('class', 'leaf');
607 text.setAttribute("text-anchor", "start");
608 text.setAttribute("y", height);
609
610 // Calculate the start position
611 this._y = height - (this.anchorStart);
612
613 // Introduce some prepending whitespace (yeah - I know ...)
614 var ws = text.appendChild(this._c("tspan"));
615 ws.appendChild(document.createTextNode('\u00A0'));
616 ws.style.textAnchor = "start";
617
618 var lastRight = 0;
619 for (var node_i in this._tokens) {
620 // Append svg
621 // var x = text.appendChild(this._c("text"));
622 var tspan = text.appendChild(this._c("tspan"));
623 tspan.appendChild(document.createTextNode(this._tokens[node_i]));
624 tspan.setAttribute("text-anchor", "middle");
625
626 this._tokenElements.push(tspan);
627
628 // Add whitespace!
629 tspan.setAttribute("dx", this.tokenSep);
630 };
631
632 // Get some global position data that may change on resize
633 var globalBoundingBox = this._rect(g);
634 this.offsetLeft = globalBoundingBox.left;
635
636 // The group of arcs
637 var arcs = g.appendChild(this._c("g"));
638 this._arcsElement = arcs;
639 arcs.classList.add("arcs");
640
641 var labels = g.appendChild(this._c("g"));
642 this._labelsElement = labels;
643 labels.classList.add("labels");
644
645 // Sort arcs if not sorted yet
646 if (this._sortedArcs === undefined)
647 this._sortArcs();
648
649 // 1. Draw all anchors
650 var i;
651 for (i in this._sortedAnchors) {
652 this._drawAnchor(this._sortedAnchors[i]);
653 };
654
655 // 2. Draw all arcs
656 for (i in this._sortedArcs) {
657 this._drawArc(this._sortedArcs[i]);
658 };
659
660 // Resize the svg with some reasonable margins
661 var width = this._rect(text).width;
662 svg.setAttribute("width", width + 20);
663 svg.setAttribute("height", height + 20);
664 svg.setAttribute("class", "relTree");
665 }
666 };
667
668 // Sort relations regarding their span
669 function lengthSort (list, inclusive) {
670
671 /*
672 * The "inclusive" flag allows to
673 * modify the behaviour for inclusivity check,
674 * e.g. if identical start or endpoints mean overlap or not.
675 */
676
677 var stack = [];
678
679 // Iterate over all definitions
680 for (var i = 0; i < list.length; i++) {
681 var current = list[i];
682
683 // Check the stack order
684 var overlaps = 0;
685 for (var j = (stack.length - 1); j >= 0; j--) {
686 var check = stack[j];
687
688 // (a..(b..b)..a)
689 if (current.first <= check.first && current.last >= check.last) {
690 overlaps = check.overlaps + 1;
691 break;
692 }
693
694 // (a..(b..a)..b)
695 else if (current.first <= check.first && current.last >= check.first) {
696
697 if (inclusive || (current.first != check.first && current.last != check.first)) {
698 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
699 };
700 }
701
702 // (b..(a..b)..a)
703 else if (current.first <= check.last && current.last >= check.last) {
704
705 if (inclusive || (current.first != check.last && current.last != check.last)) {
706 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
707 };
708 };
709 };
710
711 // Set overlaps
712 current.overlaps = overlaps;
713
714 stack.push(current);
715
716 // Although it is already sorted,
717 // the new item has to be put at the correct place
718 // TODO:
719 // Use something like splice() instead
720 stack.sort(function (a,b) {
721 b.overlaps - a.overlaps
722 });
723 };
724
725 return stack;
726 };
727});