Improved MS Edge support for relation visualizations slightly

Change-Id: I9ac2a3b364fac782cdb0e2306ab6f92ec42599a2
diff --git a/dev/js/src/match/relations.js b/dev/js/src/match/relations.js
index 6f69cbb..92be073 100644
--- a/dev/js/src/match/relations.js
+++ b/dev/js/src/match/relations.js
@@ -1,699 +1,701 @@
-/**
- * Parse a relational tree and visualize using arcs.
- *
- * @author Nils Diewald
- */
-
-define([], function () {
-  "use strict";
-
-  var svgNS = "http://www.w3.org/2000/svg";
-  var _TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
-
-  return {
-    create : function (snippet) {
-      return Object.create(this)._init(snippet);
-    },
-
-    // Initialize the state of the object
-    _init : function (snippet) {
-
-      // Predefine some values
-      this._tokens  = [];
-      this._arcs    = [];
-      this._tokenElements = [];
-      this._y = 0;
-
-      // Some configurations
-      this.maxArc      = 200; // maximum height of the bezier control point
-      this.overlapDiff = 40;  // Difference on overlaps and minimum height for self-refernces
-      this.arcDiff     = 15;
-      this.anchorDiff  = 8;
-      this.anchorStart = 15;
-      this.tokenSep    = 30;
-      this.xPadding    = 10;
-      this.yPadding    = 5;
-
-      // No snippet to process
-      if (snippet == undefined || snippet == null)
-        return this;
-
-      // Parse the snippet
-      var html = document.createElement("div");
-      html.innerHTML = snippet;
-
-      // Establish temporary parsing memory
-      this.temp = {
-        target : {}, // Remember the map id => pos        
-        edges  : [], // Remember edge definitions
-        pos    : 0   // Keep track of the current token position
-      };
-
-      // Start parsing from root
-      this._parse(0, html.childNodes, undefined);
-
-      // Establish edge list
-      var targetMap = this.temp['target'];
-      var edges = this.temp['edges'];
-
-      // Iterate over edge lists
-      // TODO:
-      //   Support spans for anchors!
-      for (var i in edges) {
-        var edge = edges[i];
-
-        // Check the target identifier
-        var targetID = edge.targetID;
-        var target = targetMap[targetID];
-
-        if (target != undefined) {
-
-          // Check if the source is a span anchor
-          /*
-          var start = edge.srcStart;
-          if (start !== edge.srcEnd) {
-            start = [start, edge.srcEnd];
-          };
-          */
-          
-          // Add relation
-          var relation = {
-            start : [edge.srcStart, edge.srcEnd],
-            end : target,
-            direction : 'uni',
-            label : edge.label
-          };
-          // console.log(relation);
-          this.addRel(relation);
-        };
-      };
-
-      // Reset parsing memory
-      this.temp = {};
-
-      return this;
-    },
-
-    // Parse a node of the tree snippet
-    _parse : function (parent, children, mark) {
-
-      // Iterate over all child nodes
-      for (var i in children) {
-        var c = children[i];
-
-        // Element node
-        if (c.nodeType == 1) {
-
-          var xmlid, target;
-
-          // Node is an identifier
-          if (c.hasAttribute('xml:id')) {
-
-            // Remember that pos has this identifier
-            xmlid = c.getAttribute('xml:id');
-            this.temp['target'][xmlid] = [this.temp['pos'], this.temp['pos']];
-          }
-
-          // Node is a relation
-          else if (c.hasAttribute('xlink:href')) {
-            var label;
-
-            // Get target id
-            target = c.getAttribute('xlink:href').replace(/^#/, "");
-
-            if (c.hasAttribute('xlink:title')) {
-              label = this._clean(c.getAttribute('xlink:title'));
-            };
-
-            // Remember the defined edge
-            var edge = {
-              label    : label,
-              srcStart : this.temp['pos'],
-              targetID : target
-            };
-            this.temp['edges'].push(edge);
-          };
-
-          // Go on with child nodes
-          if (c.hasChildNodes()) {
-            this._parse(0, c.childNodes, mark);
-          };
-
-          if (xmlid !== undefined) {
-            this.temp['target'][xmlid][1] = this.temp['pos'] -1;
-
-            /*
-            console.log('Target ' + xmlid + ' spans from ' +
-                        this.temp['target'][xmlid][0] +
-                        ' to ' +
-                        this.temp['target'][xmlid][1]
-                       );
-            */
-            xmlid = undefined;
-          }
-          else if (target !== undefined) {
-            edge["srcEnd"] = this.temp['pos'] -1;
-
-            /*
-            console.log('Source spans from ' +
-                        edge["srcStart"] +
-                        ' to ' +
-                        edge["srcEnd"]
-                       );
-            */
-            target = undefined;
-          };
-        }
-
-        // Text node
-        else if (c.nodeType == 3) {
-
-          // Check, if there is a non-whitespace token
-          if (c.nodeValue !== undefined) {
-            var str = c.nodeValue.trim();
-            if (str !== undefined && str.length > 0) {
-
-              // Add token to token list
-              this.addToken(str);
-
-              // Move token position
-              this.temp['pos']++;
-            };
-          };
-        }
-      };
-    },
-
-    
-    // Remove foundry and layer for labels
-    _clean : function (title) {
-      return title.replace(_TermRE, "$3");
-    },
-
-    
-    // Return the number of leaf nodes
-    // (not necessarily part of a relation).
-    // Consecutive nodes that are not part of any
-    // relation are summarized in one node.
-    size : function () {
-      return this._tokens.length;
-    },
-
-    
-    // This is a shorthand for SVG element creation
-    _c : function (tag) {
-      return document.createElementNS(svgNS, tag);
-    },
-
-    // Get bounding box - with workaround for text nodes
-    _rect : function (node) {
-      if (node.tagName == "tspan") {
-        var range = document.createRange();
-        range.selectNode(node);
-        var rect = range.getBoundingClientRect();
-        range.detach();
-        return rect;
-      };
-      return node.getBoundingClientRect();
-    },
-
-    // Returns the center point of the requesting token
-    _tokenPoint : function (node) {
-      var box = this._rect(node);
-      return box.x + (box.width / 2);
-    },
-
-
-    // Draws an anchor
-    _drawAnchor : function (anchor) {
-
-      // Calculate the span of the first and last token, the anchor spans
-      var firstBox = this._rect(this._tokenElements[anchor.first]);
-      var lastBox  = this._rect(this._tokenElements[anchor.last]);
-
-      var startPos = firstBox.left - this.offsetLeft;
-      var endPos   = lastBox.right - this.offsetLeft;
-      
-      var y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;
-
-      var l = this._c('path');
-      this._arcsElement.appendChild(l);
-      l.setAttribute("d", "M " + startPos + "," + y + " L " + endPos + "," + y);
-      l.setAttribute("class", "anchor");
-      anchor.element = l;
-      anchor.y = y;
-      return l;
-    },
-    
-
-    // Create an arc with an optional label
-    // Potentially needs a height parameter for stacks
-    _drawArc : function (arc) {
-
-      var startPos, endPos;
-      var startY = this._y;
-      var endY = this._y;
-
-      if (arc.startAnchor !== undefined) {
-        startPos = this._tokenPoint(arc.startAnchor.element);
-        startY = arc.startAnchor.y;
-      }
-      else {
-        startPos = this._tokenPoint(this._tokenElements[arc.first]);
-      };
-
-      if (arc.endAnchor !== undefined) {
-        endPos = this._tokenPoint(arc.endAnchor.element)
-        endY = arc.endAnchor.y;
-      }
-      else {
-        endPos = this._tokenPoint(this._tokenElements[arc.last]);
-      };
-
-      startPos -= this.offsetLeft;
-      endPos -= this.offsetLeft;
-
-      // Special treatment for self-references
-      var overlaps = arc.overlaps;
-      if (startPos == endPos) {
-        startPos -= this.overlapDiff / 3;
-        endPos   += this.overlapDiff / 3;
-        overlaps += .5;
-      };
-
-      var g = this._c("g");
-      g.setAttribute("class", "arc");
-      var p = g.appendChild(this._c("path"));
-      p.setAttribute('class', 'edge');
-      
-      // Attach the new arc before drawing, so computed values are available
-      this._arcsElement.appendChild(g);
-
-      // Create arc
-      var middle = Math.abs(endPos - startPos) / 2;
-
-      // TODO:
-      //   take the number of tokens into account!
-      var cHeight = this.arcDiff + (overlaps * this.overlapDiff) + (middle / 2);
-
-      // Respect the maximum height
-      cHeight = cHeight < this.maxArc ? cHeight : this.maxArc;
-
-      var x = Math.min(startPos, endPos);
-
-      //var controlY = (startY + endY - cHeight);
-      var controlY = (endY - cHeight);
-      
-      var arcE = "M "+ startPos + "," + startY +
-          " C " + startPos + "," + controlY +
-          " " + endPos + "," + controlY +
-          " " + endPos + "," + endY;
-
-      p.setAttribute("d", arcE);
-
-      if (arc.direction !== undefined) {
-        p.setAttribute("marker-end", "url(#arr)");
-        if (arc.direction === 'bi') {
-          p.setAttribute("marker-start", "url(#arr)");
-        };
-      };
-
-      if (arc.label === undefined)
-        return g;
-      
-      /*
-       * Calculate the top point of the arc for labeling using
-       * de Casteljau's algorithm, see e.g.
-       * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/
-       * of course simplified to symmetric arcs ...
-       */
-      // Interpolate one side of the control polygon
-      var middleY = (((startY + controlY) / 2) + controlY) / 2;
-
-      // Create a boxed label
-      g = this._c("g");
-      g.setAttribute("class", "label");
-      this._labelsElement.appendChild(g);
-
-      var that = this;
-      g.addEventListener('mouseenter', function () {
-        that._labelsElement.appendChild(this);
-      });
-
-      var labelE = g.appendChild(this._c("text"));
-      labelE.setAttribute("x", x + middle);
-      labelE.setAttribute("y", middleY + 3);
-      labelE.setAttribute("text-anchor", "middle");
-      var textNode = document.createTextNode(arc.label);
-      labelE.appendChild(textNode);
-
-      var labelBox   = labelE.getBBox();
-      var textWidth  = labelBox.width; // labelE.getComputedTextLength();
-      var textHeight = labelBox.height; // labelE.getComputedTextLength();
-
-      // Add box with padding to left and right
-      var labelR = g.insertBefore(this._c("rect"), labelE);
-      var boxWidth = textWidth + 2 * this.xPadding;
-      labelR.setAttribute("x", x + middle - (boxWidth / 2));
-      labelR.setAttribute("ry", 5);
-      labelR.setAttribute("y", labelBox.y - this.yPadding);
-      labelR.setAttribute("width", boxWidth);
-      labelR.setAttribute("height", textHeight + 2 * this.yPadding);
-    },
-
-    // Get the svg element
-    element : function () {
-      if (this._element !== undefined)
-        return this._element;
-
-      // Create svg
-      var svg = this._c("svg");
-
-      window.addEventListener("resize", function () {
-        // TODO:
-        //   Only if text-size changed!
-        // TODO:
-        //   This is currently untested
-        this.show();
-      }.bind(this));
-
-      // Define marker arrows
-      var defs = svg.appendChild(this._c("defs"));
-      var marker = defs.appendChild(this._c("marker"));
-      marker.setAttribute("refX", 9);
-      marker.setAttribute("id", "arr");
-      marker.setAttribute("orient", "auto-start-reverse");
-      marker.setAttribute("markerUnits","userSpaceOnUse");
-      var arrow = this._c("path");
-      arrow.setAttribute("transform", "scale(0.8)");
-      arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");
-      marker.appendChild(arrow);
-
-      this._element = svg;
-      return this._element;
-    },
-
-    // Add a relation with a start, an end,
-    // a direction value and an optional label text
-    addRel : function (rel) {
-      this._arcs.push(rel);
-      return this;
-    },
-
-
-    // Add a token to the list (this will mostly be a word)
-    addToken : function(token) {
-      this._tokens.push(token);
-      return this;
-    },
-    
-    /*
-     * All arcs need to be sorted before shown,
-     * to avoid nesting.
-     */
-    _sortArcs : function () {
-
-      // TODO:
-      //   Keep in mind that the arcs may have long anchors!
-      //   1. Iterate over all arcs
-      //   2. Sort all multi
-      var anchors = {};
-      
-      // 1. Sort by length
-      // 2. Tag all spans with the number of overlaps before
-      //    a) Iterate over all spans
-      //    b) check the latest preceeding overlapping span (lpos)
-      //       -> not found: tag with 0
-      //       -> found: Add +1 to the level of the (lpos)
-      //    c) If the new tag is smaller than the previous element,
-      //       reorder
-
-      // Normalize start and end
-      var sortedArcs = this._arcs.map(function (v) {
-
-        // Check for long anchors
-        if (v.start instanceof Array) {
-
-          if (v.start[0] == v.start[1]) {
-            v.start = v.start[0];
-          }
-
-          else {
-          
-            var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
-
-            // Calculate signature to avoid multiple anchors
-            var anchorSig = "#" + v.start[0] + "_" + v.start[1];
-            if (v.start[0] > v.start[1]) {
-              anchorSig = "#" + v.start[1] + "_" + v.start[0];
-            };
-            
-            // Check if the anchor already exist
-            var anchor = anchors[anchorSig];
-            if (anchor === undefined) {
-              anchor = {
-                "first":   v.start[0],
-                "last" :   v.start[1],
-                "length" : v.start[1] - v.start[0]
-              };
-              anchors[anchorSig] = anchor;
-              // anchors.push(v.startAnchor);
-            };
-
-            v.startAnchor = anchor;
-
-            // Add to anchors list
-            v.start = middle;
-          };
-        };
-
-        if (v.end instanceof Array) {
-
-          if (v.end[0] == v.end[1]) {
-            v.end = v.end[0];
-          }
-
-          else {
-
-            var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
-
-            // Calculate signature to avoid multiple anchors
-            var anchorSig = "#" + v.end[0] + "_" + v.end[1];
-            if (v.end[0] > v.end[1]) {
-              anchorSig = "#" + v.end[1] + "_" + v.end[0];
-            };
-
-            // Check if the anchor already exist
-            var anchor = anchors[anchorSig];
-            if (anchor === undefined) {
-              anchor = {
-                "first":   v.end[0],
-                "last" :   v.end[1],
-                "length" : v.end[1] - v.end[0]
-              };
-              anchors[anchorSig] = anchor;
-              // anchors.push(v.startAnchor);
-            };
-            
-            v.endAnchor = anchor;
-
-            // Add to anchors list
-            // anchors.push(v.endAnchor);
-            v.end = middle;
-          };
-        };
-
-        v.first = v.start;
-        v.last = v.end;
-
-        // calculate the arch length
-        if (v.start < v.end) {
-          v.length = v.end - v.start;
-        }
-        else {
-          // v.first = v.end;
-          // v.last = v.start;
-          v.length = v.start - v.end;
-        };
-
-        return v;
-      });
-
-      // Sort based on length
-      sortedArcs.sort(function (a, b) {
-        if (a.length < b.length)
-          return -1;
-        else
-          return 1;
-      });
-
-      // Add sorted arcs and anchors
-      this._sortedArcs    = lengthSort(sortedArcs, false);
-
-      // Translate map to array (there is probably a better JS method)
-      var sortedAnchors = [];
-      for (var i in anchors) {
-        sortedAnchors.push(anchors[i]);
-      };
-      this._sortedAnchors = lengthSort(sortedAnchors, true);
-    },
-
-    /**
-     * Center the viewport of the canvas
-     * TODO:
-     *   This is identical to tree
-     */
-    center : function () {
-      if (this._element === undefined)
-       return;
-
-      var treeDiv = this._element.parentNode;
-
-      var cWidth = parseFloat(window.getComputedStyle(this._element).width);
-      var treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
-      // Reposition:
-      if (cWidth > treeWidth) {
-       var scrollValue = (cWidth - treeWidth) / 2;
-       treeDiv.scrollLeft = scrollValue;
-      };
-    },
-
-
-    // Show the element
-    show : function () {
-      var svg = this._element;
-      var height = this.maxArc;
-
-      // Delete old group
-      if (svg.getElementsByTagName("g")[0] !== undefined) {
-        var group = svg.getElementsByTagName("g")[0];
-        svg.removeChild(group);
-        this._tokenElements = [];
-      };
-
-      var g = svg.appendChild(this._c("g"));
-
-      // Draw token list
-      var text = g.appendChild(this._c("text"));
-      text.setAttribute('class', 'leaf');
-      text.setAttribute("text-anchor", "start");
-      text.setAttribute("y", height);
-
-      // Calculate the start position
-      this._y = height - (this.anchorStart);
-
-      // Introduce some prepending whitespace (yeah - I know ...)
-      var ws = text.appendChild(this._c("tspan"));
-      ws.appendChild(document.createTextNode('\u00A0'));
-      ws.style.textAnchor = "start";
-      
-      var lastRight = 0;
-      for (var node_i in this._tokens) {
-        // Append svg
-        // var x = text.appendChild(this._c("text"));
-        var tspan = text.appendChild(this._c("tspan"));
-        tspan.appendChild(document.createTextNode(this._tokens[node_i]));
-        tspan.setAttribute("text-anchor", "middle");
-        
-        this._tokenElements.push(tspan);
-
-        // Add whitespace!
-        tspan.setAttribute("dx", this.tokenSep);
-      };
-
-      // Get some global position data that may change on resize
-      var globalBoundingBox = this._rect(g);
-      this.offsetLeft = globalBoundingBox.left;
-
-      // The group of arcs
-      var arcs = g.appendChild(this._c("g"));
-      this._arcsElement = arcs;
-      arcs.classList.add("arcs");
-
-      var labels = g.appendChild(this._c("g"));
-      this._labelsElement = labels;
-      labels.classList.add("labels");
-
-      // Sort arcs if not sorted yet
-      if (this._sortedArcs === undefined)
-        this._sortArcs();
-
-      // 1. Draw all anchors
-      var i;
-      for (i in this._sortedAnchors) {
-        this._drawAnchor(this._sortedAnchors[i]);
-      };
-
-      // 2. Draw all arcs
-      for (i in this._sortedArcs) {
-        this._drawArc(this._sortedArcs[i]);
-      };
-
-      // Resize the svg with some reasonable margins
-      var width = this._rect(text).width;
-      svg.setAttribute("width", width + 20);
-      svg.setAttribute("height", height + 20);
-      svg.setAttribute("class", "relTree");
-    }
-  };
-
-  // Sort relations regarding their span
-  function lengthSort (list, inclusive) {
-
-    /*
-     * The "inclusive" flag allows to
-     * modify the behaviour for inclusivity check,
-     * e.g. if identical start or endpoints mean overlap or not.
-     */
-    
-    var stack = [];
-
-    // Iterate over all definitions
-    for (var i = 0; i < list.length; i++) {
-      var current = list[i];
-
-      // Check the stack order
-      var overlaps = 0;
-      for (var j = (stack.length - 1); j >= 0; j--) {
-        var check = stack[j];
-
-        // (a..(b..b)..a)
-        if (current.first <= check.first && current.last >= check.last) {
-          overlaps = check.overlaps + 1;
-          break;
-        }
-
-        // (a..(b..a)..b)
-        else if (current.first <= check.first && current.last >= check.first) {
-
-          if (inclusive || (current.first != check.first && current.last != check.first)) {
-            overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
-          };
-        }
-        
-        // (b..(a..b)..a)
-        else if (current.first <= check.last && current.last >= check.last) {
-
-          if (inclusive || (current.first != check.last && current.last != check.last)) {
-            overlaps = check.overlaps + (current.length == check.length ? 0 : 1);
-          };
-        };
-      };
-
-      // Set overlaps
-      current.overlaps = overlaps;
-
-      stack.push(current);
-
-      // Although it is already sorted,
-      // the new item has to be put at the correct place
-      // TODO:
-      //   Use something like splice() instead
-      stack.sort(function (a,b) {
-        b.overlaps - a.overlaps
-      });
-    };
-
-    return stack;
-  };
-});
+/**

+ * Parse a relational tree and visualize using arcs.

+ *

+ * @author Nils Diewald

+ */

+

+define([], function () {

+  "use strict";

+

+  var svgNS = "http://www.w3.org/2000/svg";

+  var _TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");

+

+  return {

+    create : function (snippet) {

+      return Object.create(this)._init(snippet);

+    },

+

+    // Initialize the state of the object

+    _init : function (snippet) {

+

+      // Predefine some values

+      this._tokens  = [];

+      this._arcs    = [];

+      this._tokenElements = [];

+      this._y = 0;

+

+      // Some configurations

+      this.maxArc      = 200; // maximum height of the bezier control point

+      this.overlapDiff = 40;  // Difference on overlaps and minimum height for self-refernces

+      this.arcDiff     = 15;

+      this.anchorDiff  = 8;

+      this.anchorStart = 15;

+      this.tokenSep    = 30;

+      this.xPadding    = 10;

+      this.yPadding    = 5;

+

+      // No snippet to process

+      if (snippet == undefined || snippet == null)

+        return this;

+

+      // Parse the snippet

+      var html = document.createElement("div");

+      html.innerHTML = snippet;

+

+      // Establish temporary parsing memory

+      this.temp = {

+        target : {}, // Remember the map id => pos        

+        edges  : [], // Remember edge definitions

+        pos    : 0   // Keep track of the current token position

+      };

+

+      // Start parsing from root

+      this._parse(0, html.childNodes, undefined);

+

+      // Establish edge list

+      var targetMap = this.temp['target'];

+      var edges = this.temp['edges'];

+

+      // Iterate over edge lists

+      // TODO:

+      //   Support spans for anchors!

+      for (var i in edges) {

+        var edge = edges[i];

+

+        // Check the target identifier

+        var targetID = edge.targetID;

+        var target = targetMap[targetID];

+

+        if (target != undefined) {

+

+          // Check if the source is a span anchor

+          /*

+          var start = edge.srcStart;

+          if (start !== edge.srcEnd) {

+            start = [start, edge.srcEnd];

+          };

+          */

+          

+          // Add relation

+          var relation = {

+            start : [edge.srcStart, edge.srcEnd],

+            end : target,

+            direction : 'uni',

+            label : edge.label

+          };

+          // console.log(relation);

+          this.addRel(relation);

+        };

+      };

+

+      // Reset parsing memory

+      this.temp = {};

+

+      return this;

+    },

+

+    // Parse a node of the tree snippet

+    _parse : function (parent, children, mark) {

+

+      // Iterate over all child nodes

+      for (var i in children) {

+        var c = children[i];

+

+        // Element node

+        if (c.nodeType == 1) {

+

+          var xmlid, target;

+

+          // Node is an identifier

+          if (c.hasAttribute('xml:id')) {

+

+            // Remember that pos has this identifier

+            xmlid = c.getAttribute('xml:id');

+            this.temp['target'][xmlid] = [this.temp['pos'], this.temp['pos']];

+          }

+

+          // Node is a relation

+          else if (c.hasAttribute('xlink:href')) {

+            var label;

+

+            // Get target id

+            target = c.getAttribute('xlink:href').replace(/^#/, "");

+

+            if (c.hasAttribute('xlink:title')) {

+              label = this._clean(c.getAttribute('xlink:title'));

+            };

+

+            // Remember the defined edge

+            var edge = {

+              label    : label,

+              srcStart : this.temp['pos'],

+              targetID : target

+            };

+            this.temp['edges'].push(edge);

+          };

+

+          // Go on with child nodes

+          if (c.hasChildNodes()) {

+            this._parse(0, c.childNodes, mark);

+          };

+

+          if (xmlid !== undefined) {

+            this.temp['target'][xmlid][1] = this.temp['pos'] -1;

+

+            /*

+            console.log('Target ' + xmlid + ' spans from ' +

+                        this.temp['target'][xmlid][0] +

+                        ' to ' +

+                        this.temp['target'][xmlid][1]

+                       );

+            */

+            xmlid = undefined;

+          }

+          else if (target !== undefined) {

+            edge["srcEnd"] = this.temp['pos'] -1;

+

+            /*

+            console.log('Source spans from ' +

+                        edge["srcStart"] +

+                        ' to ' +

+                        edge["srcEnd"]

+                       );

+            */

+            target = undefined;

+          };

+        }

+

+        // Text node

+        else if (c.nodeType == 3) {

+

+          // Check, if there is a non-whitespace token

+          if (c.nodeValue !== undefined) {

+            var str = c.nodeValue.trim();

+            if (str !== undefined && str.length > 0) {

+

+              // Add token to token list

+              this.addToken(str);

+

+              // Move token position

+              this.temp['pos']++;

+            };

+          };

+        }

+      };

+    },

+

+    

+    // Remove foundry and layer for labels

+    _clean : function (title) {

+      return title.replace(_TermRE, "$3");

+    },

+

+    

+    // Return the number of leaf nodes

+    // (not necessarily part of a relation).

+    // Consecutive nodes that are not part of any

+    // relation are summarized in one node.

+    size : function () {

+      return this._tokens.length;

+    },

+

+    

+    // This is a shorthand for SVG element creation

+    _c : function (tag) {

+      return document.createElementNS(svgNS, tag);

+    },

+

+    // Get bounding box - with workaround for text nodes

+    _rect : function (node) {

+      if (node.tagName == "tspan" && !navigator.userAgent.match(/Edge/)) {

+        var range = document.createRange();

+        range.selectNode(node);

+        var rect = range.getBoundingClientRect();

+        range.detach();

+        return rect;

+      };

+      return node.getBoundingClientRect();

+    },

+

+    // Returns the center point of the requesting token

+    _tokenPoint : function (node) {

+	    var box = this._rect(node);

+	    return box.left + (box.width / 2);

+    },

+

+

+    // Draws an anchor

+    _drawAnchor : function (anchor) {

+

+      // Calculate the span of the first and last token, the anchor spans

+      var firstBox = this._rect(this._tokenElements[anchor.first]);

+      var lastBox  = this._rect(this._tokenElements[anchor.last]);

+	

+      var startPos = firstBox.left - this.offsetLeft;

+      var endPos   = lastBox.right - this.offsetLeft;

+      

+      var y = this._y + (anchor.overlaps * this.anchorDiff) - this.anchorStart;

+

+      var l = this._c('path');

+	    this._arcsElement.appendChild(l);

+	    var pathStr = "M " + startPos + "," + y + " L " + endPos + "," + y;

+      l.setAttribute("d", pathStr);

+      l.setAttribute("class", "anchor");

+      anchor.element = l;

+      anchor.y = y;

+      return l;

+    },

+    

+

+    // Create an arc with an optional label

+    // Potentially needs a height parameter for stacks

+    _drawArc : function (arc) {

+

+      var startPos, endPos;

+      var startY = this._y;

+      var endY = this._y;

+	

+      if (arc.startAnchor !== undefined) {

+        startPos = this._tokenPoint(arc.startAnchor.element);

+        startY = arc.startAnchor.y;

+      }

+      else {

+        startPos = this._tokenPoint(this._tokenElements[arc.first]);

+      };

+

+      if (arc.endAnchor !== undefined) {

+        endPos = this._tokenPoint(arc.endAnchor.element)

+        endY = arc.endAnchor.y;

+      }

+      else {

+        endPos = this._tokenPoint(this._tokenElements[arc.last]);

+      };

+

+

+      startPos -= this.offsetLeft;

+      endPos -= this.offsetLeft;

+

+	    // Special treatment for self-references

+      var overlaps = arc.overlaps;

+      if (startPos == endPos) {

+        startPos -= this.overlapDiff / 3;

+        endPos   += this.overlapDiff / 3;

+        overlaps += .5;

+      };

+

+      var g = this._c("g");

+      g.setAttribute("class", "arc");

+      var p = g.appendChild(this._c("path"));

+      p.setAttribute('class', 'edge');

+      

+      // Attach the new arc before drawing, so computed values are available

+      this._arcsElement.appendChild(g);

+

+      // Create arc

+      var middle = Math.abs(endPos - startPos) / 2;

+

+      // TODO:

+      //   take the number of tokens into account!

+      var cHeight = this.arcDiff + (overlaps * this.overlapDiff) + (middle / 2);

+

+      // Respect the maximum height

+      cHeight = cHeight < this.maxArc ? cHeight : this.maxArc;

+

+      var x = Math.min(startPos, endPos);

+

+      //var controlY = (startY + endY - cHeight);

+      var controlY = (endY - cHeight);

+      

+      var arcE = "M "+ startPos + "," + startY +

+          " C " + startPos + "," + controlY +

+          " " + endPos + "," + controlY +

+          " " + endPos + "," + endY;

+

+      p.setAttribute("d", arcE);

+

+      if (arc.direction !== undefined) {

+        p.setAttribute("marker-end", "url(#arr)");

+        if (arc.direction === 'bi') {

+          p.setAttribute("marker-start", "url(#arr)");

+        };

+      };

+

+      if (arc.label === undefined)

+        return g;

+      

+      /*

+       * Calculate the top point of the arc for labeling using

+       * de Casteljau's algorithm, see e.g.

+       * http://blog.sklambert.com/finding-the-control-points-of-a-bezier-curve/

+       * of course simplified to symmetric arcs ...

+       */

+      // Interpolate one side of the control polygon

+      var middleY = (((startY + controlY) / 2) + controlY) / 2;

+

+      // Create a boxed label

+      g = this._c("g");

+      g.setAttribute("class", "label");

+      this._labelsElement.appendChild(g);

+

+      var that = this;

+      g.addEventListener('mouseenter', function () {

+        that._labelsElement.appendChild(this);

+      });

+

+      var labelE = g.appendChild(this._c("text"));

+      labelE.setAttribute("x", x + middle);

+      labelE.setAttribute("y", middleY + 3);

+      labelE.setAttribute("text-anchor", "middle");

+      var textNode = document.createTextNode(arc.label);

+      labelE.appendChild(textNode);

+

+      var labelBox   = labelE.getBBox();

+      var textWidth  = labelBox.width; // labelE.getComputedTextLength();

+      var textHeight = labelBox.height; // labelE.getComputedTextLength();

+

+      // Add box with padding to left and right

+      var labelR = g.insertBefore(this._c("rect"), labelE);

+      var boxWidth = textWidth + 2 * this.xPadding;

+      labelR.setAttribute("x", x + middle - (boxWidth / 2));

+      labelR.setAttribute("ry", 5);

+      labelR.setAttribute("y", labelBox.y - this.yPadding);

+      labelR.setAttribute("width", boxWidth);

+      labelR.setAttribute("height", textHeight + 2 * this.yPadding);

+    },

+

+    // Get the svg element

+    element : function () {

+      if (this._element !== undefined)

+        return this._element;

+

+      // Create svg

+      var svg = this._c("svg");

+

+      window.addEventListener("resize", function () {

+        // TODO:

+        //   Only if text-size changed!

+        // TODO:

+        //   This is currently untested

+        this.show();

+      }.bind(this));

+

+      // Define marker arrows

+      var defs = svg.appendChild(this._c("defs"));

+      var marker = defs.appendChild(this._c("marker"));

+      marker.setAttribute("refX", 9);

+      marker.setAttribute("id", "arr");

+      marker.setAttribute("orient", "auto-start-reverse");

+      marker.setAttribute("markerUnits","userSpaceOnUse");

+      var arrow = this._c("path");

+      arrow.setAttribute("transform", "scale(0.8)");

+      arrow.setAttribute("d", "M 0,-5 0,5 10,0 Z");

+      marker.appendChild(arrow);

+

+      this._element = svg;

+      return this._element;

+    },

+

+    // Add a relation with a start, an end,

+    // a direction value and an optional label text

+    addRel : function (rel) {

+      this._arcs.push(rel);

+      return this;

+    },

+

+

+    // Add a token to the list (this will mostly be a word)

+    addToken : function(token) {

+      this._tokens.push(token);

+      return this;

+    },

+    

+    /*

+     * All arcs need to be sorted before shown,

+     * to avoid nesting.

+     */

+    _sortArcs : function () {

+

+      // TODO:

+      //   Keep in mind that the arcs may have long anchors!

+      //   1. Iterate over all arcs

+      //   2. Sort all multi

+      var anchors = {};

+      

+      // 1. Sort by length

+      // 2. Tag all spans with the number of overlaps before

+      //    a) Iterate over all spans

+      //    b) check the latest preceeding overlapping span (lpos)

+      //       -> not found: tag with 0

+      //       -> found: Add +1 to the level of the (lpos)

+      //    c) If the new tag is smaller than the previous element,

+      //       reorder

+

+      // Normalize start and end

+      var sortedArcs = this._arcs.map(function (v) {

+

+        // Check for long anchors

+        if (v.start instanceof Array) {

+

+          if (v.start[0] == v.start[1]) {

+            v.start = v.start[0];

+          }

+

+          else {

+          

+            var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];

+

+            // Calculate signature to avoid multiple anchors

+            var anchorSig = "#" + v.start[0] + "_" + v.start[1];

+            if (v.start[0] > v.start[1]) {

+              anchorSig = "#" + v.start[1] + "_" + v.start[0];

+            };

+            

+            // Check if the anchor already exist

+            var anchor = anchors[anchorSig];

+            if (anchor === undefined) {

+              anchor = {

+                "first":   v.start[0],

+                "last" :   v.start[1],

+                "length" : v.start[1] - v.start[0]

+              };

+              anchors[anchorSig] = anchor;

+              // anchors.push(v.startAnchor);

+            };

+

+            v.startAnchor = anchor;

+

+            // Add to anchors list

+            v.start = middle;

+          };

+        };

+

+        if (v.end instanceof Array) {

+

+          if (v.end[0] == v.end[1]) {

+            v.end = v.end[0];

+          }

+

+          else {

+

+            var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];

+

+            // Calculate signature to avoid multiple anchors

+            var anchorSig = "#" + v.end[0] + "_" + v.end[1];

+            if (v.end[0] > v.end[1]) {

+              anchorSig = "#" + v.end[1] + "_" + v.end[0];

+            };

+

+            // Check if the anchor already exist

+            var anchor = anchors[anchorSig];

+            if (anchor === undefined) {

+              anchor = {

+                "first":   v.end[0],

+                "last" :   v.end[1],

+                "length" : v.end[1] - v.end[0]

+              };

+              anchors[anchorSig] = anchor;

+              // anchors.push(v.startAnchor);

+            };

+            

+            v.endAnchor = anchor;

+

+            // Add to anchors list

+            // anchors.push(v.endAnchor);

+            v.end = middle;

+          };

+        };

+

+        v.first = v.start;

+        v.last = v.end;

+

+        // calculate the arch length

+        if (v.start < v.end) {

+          v.length = v.end - v.start;

+        }

+        else {

+          // v.first = v.end;

+          // v.last = v.start;

+          v.length = v.start - v.end;

+        };

+

+        return v;

+      });

+

+      // Sort based on length

+      sortedArcs.sort(function (a, b) {

+        if (a.length < b.length)

+          return -1;

+        else

+          return 1;

+      });

+

+      // Add sorted arcs and anchors

+      this._sortedArcs    = lengthSort(sortedArcs, false);

+

+      // Translate map to array (there is probably a better JS method)

+      var sortedAnchors = [];

+      for (var i in anchors) {

+        sortedAnchors.push(anchors[i]);

+      };

+      this._sortedAnchors = lengthSort(sortedAnchors, true);

+    },

+

+    /**

+     * Center the viewport of the canvas

+     * TODO:

+     *   This is identical to tree

+     */

+    center : function () {

+      if (this._element === undefined)

+       return;

+

+      var treeDiv = this._element.parentNode;

+

+      var cWidth = parseFloat(window.getComputedStyle(this._element).width);

+      var treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);

+      // Reposition:

+      if (cWidth > treeWidth) {

+       var scrollValue = (cWidth - treeWidth) / 2;

+       treeDiv.scrollLeft = scrollValue;

+      };

+    },

+

+

+    // Show the element

+    show : function () {

+      var svg = this._element;

+      var height = this.maxArc;

+

+      // Delete old group

+      if (svg.getElementsByTagName("g")[0] !== undefined) {

+        var group = svg.getElementsByTagName("g")[0];

+        svg.removeChild(group);

+        this._tokenElements = [];

+      };

+

+      var g = svg.appendChild(this._c("g"));

+

+      // Draw token list

+      var text = g.appendChild(this._c("text"));

+      text.setAttribute('class', 'leaf');

+      text.setAttribute("text-anchor", "start");

+      text.setAttribute("y", height);

+

+      // Calculate the start position

+      this._y = height - (this.anchorStart);

+

+      // Introduce some prepending whitespace (yeah - I know ...)

+      var ws = text.appendChild(this._c("tspan"));

+      ws.appendChild(document.createTextNode('\u00A0'));

+      ws.style.textAnchor = "start";

+      

+      var lastRight = 0;

+      for (var node_i in this._tokens) {

+        // Append svg

+        // var x = text.appendChild(this._c("text"));

+        var tspan = text.appendChild(this._c("tspan"));

+        tspan.appendChild(document.createTextNode(this._tokens[node_i]));

+        tspan.setAttribute("text-anchor", "middle");

+        

+        this._tokenElements.push(tspan);

+

+        // Add whitespace!

+        tspan.setAttribute("dx", this.tokenSep);

+      };

+

+      // Get some global position data that may change on resize

+      var globalBoundingBox = this._rect(g);

+      this.offsetLeft = globalBoundingBox.left;

+

+      // The group of arcs

+      var arcs = g.appendChild(this._c("g"));

+      this._arcsElement = arcs;

+      arcs.classList.add("arcs");

+

+      var labels = g.appendChild(this._c("g"));

+      this._labelsElement = labels;

+      labels.classList.add("labels");

+

+      // Sort arcs if not sorted yet

+      if (this._sortedArcs === undefined)

+        this._sortArcs();

+

+      // 1. Draw all anchors

+      var i;

+      for (i in this._sortedAnchors) {

+        this._drawAnchor(this._sortedAnchors[i]);

+      };

+

+      // 2. Draw all arcs

+      for (i in this._sortedArcs) {

+        this._drawArc(this._sortedArcs[i]);

+      };

+

+      // Resize the svg with some reasonable margins

+      var width = this._rect(text).width;

+      svg.setAttribute("width", width + 20);

+      svg.setAttribute("height", height + 20);

+      svg.setAttribute("class", "relTree");

+    }

+  };

+

+  // Sort relations regarding their span

+  function lengthSort (list, inclusive) {

+

+    /*

+     * The "inclusive" flag allows to

+     * modify the behaviour for inclusivity check,

+     * e.g. if identical start or endpoints mean overlap or not.

+     */

+    

+    var stack = [];

+

+    // Iterate over all definitions

+    for (var i = 0; i < list.length; i++) {

+      var current = list[i];

+

+      // Check the stack order

+      var overlaps = 0;

+      for (var j = (stack.length - 1); j >= 0; j--) {

+        var check = stack[j];

+

+        // (a..(b..b)..a)

+        if (current.first <= check.first && current.last >= check.last) {

+          overlaps = check.overlaps + 1;

+          break;

+        }

+

+        // (a..(b..a)..b)

+        else if (current.first <= check.first && current.last >= check.first) {

+

+          if (inclusive || (current.first != check.first && current.last != check.first)) {

+            overlaps = check.overlaps + (current.length == check.length ? 0 : 1);

+          };

+        }

+        

+        // (b..(a..b)..a)

+        else if (current.first <= check.last && current.last >= check.last) {

+

+          if (inclusive || (current.first != check.last && current.last != check.last)) {

+            overlaps = check.overlaps + (current.length == check.length ? 0 : 1);

+          };

+        };

+      };

+

+      // Set overlaps

+      current.overlaps = overlaps;

+

+      stack.push(current);

+

+      // Although it is already sorted,

+      // the new item has to be put at the correct place

+      // TODO:

+      //   Use something like splice() instead

+      stack.sort(function (a,b) {

+        b.overlaps - a.overlaps

+      });

+    };

+

+    return stack;

+  };

+});