blob: 92be07315253ea98d45d3cda01c816b9741906cf [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;
26
27 // Some configurations
28 this.maxArc = 200; // maximum height of the bezier control point
29 this.overlapDiff = 40; // Difference on overlaps and minimum height for self-refernces
30 this.arcDiff = 15;
31 this.anchorDiff = 8;
32 this.anchorStart = 15;
33 this.tokenSep = 30;
34 this.xPadding = 10;
35 this.yPadding = 5;
36
37 // No snippet to process
38 if (snippet == undefined || snippet == null)
39 return this;
40
41 // Parse the snippet
42 var html = document.createElement("div");
43 html.innerHTML = snippet;
44
45 // Establish temporary parsing memory
46 this.temp = {
47 target : {}, // Remember the map id => pos
48 edges : [], // Remember edge definitions
49 pos : 0 // Keep track of the current token position
50 };
51
52 // Start parsing from root
53 this._parse(0, html.childNodes, undefined);
54
55 // Establish edge list
56 var targetMap = this.temp['target'];
57 var edges = this.temp['edges'];
58
59 // Iterate over edge lists
60 // TODO:
61 // Support spans for anchors!
62 for (var i in edges) {
63 var edge = edges[i];
64
65 // Check the target identifier
66 var targetID = edge.targetID;
67 var target = targetMap[targetID];
68
69 if (target != undefined) {
70
71 // Check if the source is a span anchor
72 /*
73 var start = edge.srcStart;
74 if (start !== edge.srcEnd) {
75 start = [start, edge.srcEnd];
76 };
77 */
78
79 // Add relation
80 var relation = {
81 start : [edge.srcStart, edge.srcEnd],
82 end : target,
83 direction : 'uni',
84 label : edge.label
85 };
86 // console.log(relation);
87 this.addRel(relation);
88 };
89 };
90
91 // Reset parsing memory
92 this.temp = {};
93
94 return this;
95 },
96
97 // Parse a node of the tree snippet
98 _parse : function (parent, children, mark) {
99
100 // Iterate over all child nodes
101 for (var i in children) {
102 var c = children[i];
103
104 // Element node
105 if (c.nodeType == 1) {
106
107 var xmlid, target;
108
109 // Node is an identifier
110 if (c.hasAttribute('xml:id')) {
111
112 // Remember that pos has this identifier
113 xmlid = c.getAttribute('xml:id');
114 this.temp['target'][xmlid] = [this.temp['pos'], this.temp['pos']];
115 }
116
117 // Node is a relation
118 else if (c.hasAttribute('xlink:href')) {
119 var label;
120
121 // Get target id
122 target = c.getAttribute('xlink:href').replace(/^#/, "");
123
124 if (c.hasAttribute('xlink:title')) {
125 label = this._clean(c.getAttribute('xlink:title'));
126 };
127
128 // Remember the defined edge
129 var edge = {
130 label : label,
131 srcStart : this.temp['pos'],
132 targetID : target
133 };
134 this.temp['edges'].push(edge);
135 };
136
137 // Go on with child nodes
138 if (c.hasChildNodes()) {
139 this._parse(0, c.childNodes, mark);
140 };
141
142 if (xmlid !== undefined) {
143 this.temp['target'][xmlid][1] = this.temp['pos'] -1;
144
145 /*
146 console.log('Target ' + xmlid + ' spans from ' +
147 this.temp['target'][xmlid][0] +
148 ' to ' +
149 this.temp['target'][xmlid][1]
150 );
151 */
152 xmlid = undefined;
153 }
154 else if (target !== undefined) {
155 edge["srcEnd"] = this.temp['pos'] -1;
156
157 /*
158 console.log('Source spans from ' +
159 edge["srcStart"] +
160 ' to ' +
161 edge["srcEnd"]
162 );
163 */
164 target = undefined;
165 };
166 }
167
168 // Text node
169 else if (c.nodeType == 3) {
170
171 // Check, if there is a non-whitespace token
172 if (c.nodeValue !== undefined) {
173 var str = c.nodeValue.trim();
174 if (str !== undefined && str.length > 0) {
175
176 // Add token to token list
177 this.addToken(str);
178
179 // Move token position
180 this.temp['pos']++;
181 };
182 };
183 }
184 };
185 },
186
187
188 // Remove foundry and layer for labels
189 _clean : function (title) {
190 return title.replace(_TermRE, "$3");
191 },
192
193
194 // Return the number of leaf nodes
195 // (not necessarily part of a relation).
196 // Consecutive nodes that are not part of any
197 // relation are summarized in one node.
198 size : function () {
199 return this._tokens.length;
200 },
201
202
203 // This is a shorthand for SVG element creation
204 _c : function (tag) {
205 return document.createElementNS(svgNS, tag);
206 },
207
208 // Get bounding box - with workaround for text nodes
209 _rect : function (node) {
210 if (node.tagName == "tspan" && !navigator.userAgent.match(/Edge/)) {
211 var range = document.createRange();
212 range.selectNode(node);
213 var rect = range.getBoundingClientRect();
214 range.detach();
215 return rect;
216 };
217 return node.getBoundingClientRect();
218 },
219
220 // Returns the center point of the requesting token
221 _tokenPoint : function (node) {
222 var box = this._rect(node);
223 return box.left + (box.width / 2);
224 },
225
226
227 // Draws an anchor
228 _drawAnchor : function (anchor) {
229
230 // Calculate the span of the first and last token, the anchor spans
231 var firstBox = this._rect(this._tokenElements[anchor.first]);
232 var lastBox = this._rect(this._tokenElements[anchor.last]);
233
234 var startPos = firstBox.left - this.offsetLeft;
235 var endPos = lastBox.right - this.offsetLeft;
236
237 var y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;
238
239 var l = this._c('path');
240 this._arcsElement.appendChild(l);
241 var pathStr = "M " + startPos + "," + y + " L " + endPos + "," + y;
242 l.setAttribute("d", pathStr);
243 l.setAttribute("class", "anchor");
244 anchor.element = l;
245 anchor.y = y;
246 return l;
247 },
248
249
250 // Create an arc with an optional label
251 // Potentially needs a height parameter for stacks
252 _drawArc : function (arc) {
253
254 var startPos, endPos;
255 var startY = this._y;
256 var endY = this._y;
257
258 if (arc.startAnchor !== undefined) {
259 startPos = this._tokenPoint(arc.startAnchor.element);
260 startY = arc.startAnchor.y;
261 }
262 else {
263 startPos = this._tokenPoint(this._tokenElements[arc.first]);
264 };
265
266 if (arc.endAnchor !== undefined) {
267 endPos = this._tokenPoint(arc.endAnchor.element)
268 endY = arc.endAnchor.y;
269 }
270 else {
271 endPos = this._tokenPoint(this._tokenElements[arc.last]);
272 };
273
274
275 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
336 g = this._c("g");
337 g.setAttribute("class", "label");
338 this._labelsElement.appendChild(g);
339
340 var that = this;
341 g.addEventListener('mouseenter', function () {
342 that._labelsElement.appendChild(this);
343 });
344
345 var labelE = g.appendChild(this._c("text"));
346 labelE.setAttribute("x", x + middle);
347 labelE.setAttribute("y", middleY + 3);
348 labelE.setAttribute("text-anchor", "middle");
349 var textNode = document.createTextNode(arc.label);
350 labelE.appendChild(textNode);
351
352 var labelBox = labelE.getBBox();
353 var textWidth = labelBox.width; // labelE.getComputedTextLength();
354 var textHeight = labelBox.height; // labelE.getComputedTextLength();
355
356 // Add box with padding to left and right
357 var labelR = g.insertBefore(this._c("rect"), labelE);
358 var boxWidth = textWidth + 2 * this.xPadding;
359 labelR.setAttribute("x", x + middle - (boxWidth / 2));
360 labelR.setAttribute("ry", 5);
361 labelR.setAttribute("y", labelBox.y - this.yPadding);
362 labelR.setAttribute("width", boxWidth);
363 labelR.setAttribute("height", textHeight + 2 * this.yPadding);
364 },
365
366 // Get the svg element
367 element : function () {
368 if (this._element !== undefined)
369 return this._element;
370
371 // Create svg
372 var svg = this._c("svg");
373
374 window.addEventListener("resize", function () {
375 // TODO:
376 // Only if text-size changed!
377 // TODO:
378 // This is currently untested
379 this.show();
380 }.bind(this));
381
382 // Define marker arrows
383 var defs = svg.appendChild(this._c("defs"));
384 var marker = defs.appendChild(this._c("marker"));
385 marker.setAttribute("refX", 9);
386 marker.setAttribute("id", "arr");
387 marker.setAttribute("orient", "auto-start-reverse");
388 marker.setAttribute("markerUnits","userSpaceOnUse");
389 var arrow = this._c("path");
390 arrow.setAttribute("transform", "scale(0.8)");
391 arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
392 marker.appendChild(arrow);
393
394 this._element = svg;
395 return this._element;
396 },
397
398 // Add a relation with a start, an end,
399 // a direction value and an optional label text
400 addRel : function (rel) {
401 this._arcs.push(rel);
402 return this;
403 },
404
405
406 // Add a token to the list (this will mostly be a word)
407 addToken : function(token) {
408 this._tokens.push(token);
409 return this;
410 },
411
412 /*
413 * All arcs need to be sorted before shown,
414 * to avoid nesting.
415 */
416 _sortArcs : function () {
417
418 // TODO:
419 // Keep in mind that the arcs may have long anchors!
420 // 1. Iterate over all arcs
421 // 2. Sort all multi
422 var anchors = {};
423
424 // 1. Sort by length
425 // 2. Tag all spans with the number of overlaps before
426 // a) Iterate over all spans
427 // b) check the latest preceeding overlapping span (lpos)
428 // -> not found: tag with 0
429 // -> found: Add +1 to the level of the (lpos)
430 // c) If the new tag is smaller than the previous element,
431 // reorder
432
433 // Normalize start and end
434 var sortedArcs = this._arcs.map(function (v) {
435
436 // Check for long anchors
437 if (v.start instanceof Array) {
438
439 if (v.start[0] == v.start[1]) {
440 v.start = v.start[0];
441 }
442
443 else {
444
445 var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
446
447 // Calculate signature to avoid multiple anchors
448 var anchorSig = "#" + v.start[0] + "_" + v.start[1];
449 if (v.start[0] > v.start[1]) {
450 anchorSig = "#" + v.start[1] + "_" + v.start[0];
451 };
452
453 // Check if the anchor already exist
454 var anchor = anchors[anchorSig];
455 if (anchor === undefined) {
456 anchor = {
457 "first": v.start[0],
458 "last" : v.start[1],
459 "length" : v.start[1] - v.start[0]
460 };
461 anchors[anchorSig] = anchor;
462 // anchors.push(v.startAnchor);
463 };
464
465 v.startAnchor = anchor;
466
467 // Add to anchors list
468 v.start = middle;
469 };
470 };
471
472 if (v.end instanceof Array) {
473
474 if (v.end[0] == v.end[1]) {
475 v.end = v.end[0];
476 }
477
478 else {
479
480 var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
481
482 // Calculate signature to avoid multiple anchors
483 var anchorSig = "#" + v.end[0] + "_" + v.end[1];
484 if (v.end[0] > v.end[1]) {
485 anchorSig = "#" + v.end[1] + "_" + v.end[0];
486 };
487
488 // Check if the anchor already exist
489 var anchor = anchors[anchorSig];
490 if (anchor === undefined) {
491 anchor = {
492 "first": v.end[0],
493 "last" : v.end[1],
494 "length" : v.end[1] - v.end[0]
495 };
496 anchors[anchorSig] = anchor;
497 // anchors.push(v.startAnchor);
498 };
499
500 v.endAnchor = anchor;
501
502 // Add to anchors list
503 // anchors.push(v.endAnchor);
504 v.end = middle;
505 };
506 };
507
508 v.first = v.start;
509 v.last = v.end;
510
511 // calculate the arch length
512 if (v.start < v.end) {
513 v.length = v.end - v.start;
514 }
515 else {
516 // v.first = v.end;
517 // v.last = v.start;
518 v.length = v.start - v.end;
519 };
520
521 return v;
522 });
523
524 // Sort based on length
525 sortedArcs.sort(function (a, b) {
526 if (a.length < b.length)
527 return -1;
528 else
529 return 1;
530 });
531
532 // Add sorted arcs and anchors
533 this._sortedArcs = lengthSort(sortedArcs, false);
534
535 // Translate map to array (there is probably a better JS method)
536 var sortedAnchors = [];
537 for (var i in anchors) {
538 sortedAnchors.push(anchors[i]);
539 };
540 this._sortedAnchors = lengthSort(sortedAnchors, true);
541 },
542
543 /**
544 * Center the viewport of the canvas
545 * TODO:
546 * This is identical to tree
547 */
548 center : function () {
549 if (this._element === undefined)
550 return;
551
552 var treeDiv = this._element.parentNode;
553
554 var cWidth = parseFloat(window.getComputedStyle(this._element).width);
555 var treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
556 // Reposition:
557 if (cWidth > treeWidth) {
558 var scrollValue = (cWidth - treeWidth) / 2;
559 treeDiv.scrollLeft = scrollValue;
560 };
561 },
562
563
564 // Show the element
565 show : function () {
566 var svg = this._element;
567 var height = this.maxArc;
568
569 // Delete old group
570 if (svg.getElementsByTagName("g")[0] !== undefined) {
571 var group = svg.getElementsByTagName("g")[0];
572 svg.removeChild(group);
573 this._tokenElements = [];
574 };
575
576 var g = svg.appendChild(this._c("g"));
577
578 // Draw token list
579 var text = g.appendChild(this._c("text"));
580 text.setAttribute('class', 'leaf');
581 text.setAttribute("text-anchor", "start");
582 text.setAttribute("y", height);
583
584 // Calculate the start position
585 this._y = height - (this.anchorStart);
586
587 // Introduce some prepending whitespace (yeah - I know ...)
588 var ws = text.appendChild(this._c("tspan"));
589 ws.appendChild(document.createTextNode('\u00A0'));
590 ws.style.textAnchor = "start";
591
592 var lastRight = 0;
593 for (var node_i in this._tokens) {
594 // Append svg
595 // var x = text.appendChild(this._c("text"));
596 var tspan = text.appendChild(this._c("tspan"));
597 tspan.appendChild(document.createTextNode(this._tokens[node_i]));
598 tspan.setAttribute("text-anchor", "middle");
599
600 this._tokenElements.push(tspan);
601
602 // Add whitespace!
603 tspan.setAttribute("dx", this.tokenSep);
604 };
605
606 // Get some global position data that may change on resize
607 var globalBoundingBox = this._rect(g);
608 this.offsetLeft = globalBoundingBox.left;
609
610 // The group of arcs
611 var arcs = g.appendChild(this._c("g"));
612 this._arcsElement = arcs;
613 arcs.classList.add("arcs");
614
615 var labels = g.appendChild(this._c("g"));
616 this._labelsElement = labels;
617 labels.classList.add("labels");
618
619 // Sort arcs if not sorted yet
620 if (this._sortedArcs === undefined)
621 this._sortArcs();
622
623 // 1. Draw all anchors
624 var i;
625 for (i in this._sortedAnchors) {
626 this._drawAnchor(this._sortedAnchors[i]);
627 };
628
629 // 2. Draw all arcs
630 for (i in this._sortedArcs) {
631 this._drawArc(this._sortedArcs[i]);
632 };
633
634 // Resize the svg with some reasonable margins
635 var width = this._rect(text).width;
636 svg.setAttribute("width", width + 20);
637 svg.setAttribute("height", height + 20);
638 svg.setAttribute("class", "relTree");
639 }
640 };
641
642 // Sort relations regarding their span
643 function lengthSort (list, inclusive) {
644
645 /*
646 * The "inclusive" flag allows to
647 * modify the behaviour for inclusivity check,
648 * e.g. if identical start or endpoints mean overlap or not.
649 */
650
651 var stack = [];
652
653 // Iterate over all definitions
654 for (var i = 0; i < list.length; i++) {
655 var current = list[i];
656
657 // Check the stack order
658 var overlaps = 0;
659 for (var j = (stack.length - 1); j >= 0; j--) {
660 var check = stack[j];
661
662 // (a..(b..b)..a)
663 if (current.first <= check.first && current.last >= check.last) {
664 overlaps = check.overlaps + 1;
665 break;
666 }
667
668 // (a..(b..a)..b)
669 else if (current.first <= check.first && current.last >= check.first) {
670
671 if (inclusive || (current.first != check.first && current.last != check.first)) {
672 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
673 };
674 }
675
676 // (b..(a..b)..a)
677 else if (current.first <= check.last && current.last >= check.last) {
678
679 if (inclusive || (current.first != check.last && current.last != check.last)) {
680 overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
681 };
682 };
683 };
684
685 // Set overlaps
686 current.overlaps = overlaps;
687
688 stack.push(current);
689
690 // Although it is already sorted,
691 // the new item has to be put at the correct place
692 // TODO:
693 // Use something like splice() instead
694 stack.sort(function (a,b) {
695 b.overlaps - a.overlaps
696 });
697 };
698
699 return stack;
700 };
701});