Added span anchors to relation visualization

Change-Id: Id106c4659869a7162e1c0b3f1838e69fad688886
diff --git a/dev/js/src/match/info.js b/dev/js/src/match/info.js
index 7a659ba..c2154a5 100644
--- a/dev/js/src/match/info.js
+++ b/dev/js/src/match/info.js
@@ -57,17 +57,17 @@
       var elem = this._match.element();
 
       if (this.opened == true) {        
-	      elem.removeChild(
-	        this.element()
-	      );
-	      this.opened = false;
+        elem.removeChild(
+          this.element()
+        );
+        this.opened = false;
       }
       else {
-	      // Append element to match
+        // Append element to match
         elem.appendChild(
-	        this.element()
-	      );
-	      this.opened = true;
+          this.element()
+        );
+        this.opened = true;
       };
       
       return this.opened;
@@ -83,48 +83,48 @@
 
       // Get all tokens
       if (tokens === undefined) {
-	      focus = this._match.getTokens();
+        focus = this._match.getTokens();
       } 
 
       // Get only some tokens
       else {
-
-	      // Push newly to focus array
-	      for (var i = 0; i < tokens.length; i++) {
-	        var term = tokens[i];
-	        try {
-	          // Create info layer objects
-	          var layer = infoLayerClass.create(term);
-	          layer.type = "tokens";
-	          focus.push(layer);
-	        }
-	        catch (e) {
-	          continue;
-	        };
-	      };
+        
+        // Push newly to focus array
+        for (var i = 0; i < tokens.length; i++) {
+          var term = tokens[i];
+          try {
+            // Create info layer objects
+            var layer = infoLayerClass.create(term);
+            layer.type = "tokens";
+            focus.push(layer);
+          }
+          catch (e) {
+            continue;
+          };
+        };
       };
       
       // No tokens chosen
       if (focus.length == 0)
-	      cb(null);
+        cb(null);
 
       // Get info (may be cached)
       KorAP.API.getMatchInfo(
-	      this._match,
-	      { 'spans' : false, 'layer' : focus },
-
-	      // Callback for retrieval
-	      function (matchResponse) {
+        this._match,
+        { 'spans' : false, 'layer' : focus },
+        
+        // Callback for retrieval
+        function (matchResponse) {
 
           if (matchResponse === undefined)
             cb(null);
 
-	        // Get snippet from match info
-	        if (matchResponse["snippet"] !== undefined) {
-	          this._table = matchTableClass.create(matchResponse["snippet"]);
-	          cb(this._table);
-	        };
-	      }.bind(this)
+          // Get snippet from match info
+          if (matchResponse["snippet"] !== undefined) {
+            this._table = matchTableClass.create(matchResponse["snippet"]);
+            cb(this._table);
+          };
+        }.bind(this)
       );
 
       /*
@@ -142,22 +142,22 @@
       
       // TODO: Support and cache multiple trees
       KorAP.API.getMatchInfo(
-	      this._match, {
-	        'spans' : true,
-	        'foundry' : foundry,
-	        'layer' : layer
-	      },
-	      function (matchResponse) {
-	        // Get snippet from match info
-	        if (matchResponse["snippet"] !== undefined) {
-	          // Todo: This should be cached somehow
+        this._match, {
+          'spans' : true,
+          'foundry' : foundry,
+          'layer' : layer
+        },
+        function (matchResponse) {
+          // Get snippet from match info
+          if (matchResponse["snippet"] !== undefined) {
+            // Todo: This should be cached somehow
 
-	          cb(matchTreeClass.create(matchResponse["snippet"]));
-	        }
-	        else {
-	          cb(null);
-	        };
-	      }.bind(this)
+            cb(matchTreeClass.create(matchResponse["snippet"]));
+          }
+          else {
+            cb(null);
+          };
+        }.bind(this)
       );
     },
 
@@ -188,10 +188,10 @@
       h6.appendChild(document.createElement('span'))
 	      .appendChild(document.createTextNode(foundry));
       h6.appendChild(document.createElement('span'))
-	      .appendChild(document.createTextNode(layer));
-      
+	      .appendChild(document.createTextNode(layer));      
+
       var tree = matchtree.appendChild(
-	      document.createElement('div')
+        document.createElement('div')
       );
       
       this._element.insertBefore(matchtree, this._element.lastChild);
@@ -202,10 +202,10 @@
       close.className = 'close';
       close.appendChild(document.createElement('span'));
       close.addEventListener(
-	      'click', function (e) {
-	        matchtree.parentNode.removeChild(matchtree);
-	        e.halt();
-	      }
+        'click', function (e) {
+          matchtree.parentNode.removeChild(matchtree);
+          e.halt();
+        }
       );
 
       tree.classList.add('loading');
@@ -213,24 +213,24 @@
       // Get tree data async
       this.getTree(foundry, layer, function (treeObj) {
 
-	      tree.classList.remove('loading');
+        tree.classList.remove('loading');
 
-	      // Something went wrong - probably log!!!
+        // Something went wrong - probably log!!!
 
-	      if (treeObj === null) {
-	        tree.appendChild(document.createTextNode('No data available.'));
-	      }
-	      else {
-	        tree.appendChild(treeObj.element());
-	        // Reposition the view to the center
-	        // (This may in a future release be a reposition
-	        // to move the root into the center or the actual
-	        // match)
+        if (treeObj === null) {
+          tree.appendChild(document.createTextNode('No data available.'));
+        }
+        else {
+          tree.appendChild(treeObj.element());
+          // Reposition the view to the center
+          // (This may in a future release be a reposition
+          // to move the root into the center or the actual
+          // match)
 
           var dl = document.createElement('li');
           dl.className = 'download';
           dl.addEventListener(
-	          'click', function (e) {
+            'click', function (e) {
 
               var a = document.createElement('a');
               a.setAttribute('href-lang', 'image/svg+xml');
@@ -243,14 +243,14 @@
               document.body.removeChild(a)
 
               e.halt();
-	          }
+            }
           );
           actions.appendChild(dl);
           treeObj.center();
-	      };
-	
-	      if (cb !== undefined)
-	        cb(treeObj);
+        };
+  
+        if (cb !== undefined)
+          cb(treeObj);
       });
     },
     
@@ -260,7 +260,7 @@
     element : function () {
       
       if (this._element !== undefined)
-	      return this._element;
+        return this._element;
       
       // Create info table
       var info = document.createElement('div');
@@ -274,7 +274,7 @@
       // Create the table asynchronous
       this.getTable(undefined, function (table) {
 
-	      if (table !== null) {
+        if (table !== null) {
           matchtable.appendChild(table.element());
 	      };
 	      matchtable.classList.remove('loading');
@@ -283,36 +283,48 @@
         this._matchCreator = matchQueryCreator.create(info);
       });
 
+      // Join spans and relations
+      var treeLayers = []
+      var spans = this._match.getSpans();
+      var rels = this._match.getRels();
+      var i;
+      for (i in spans) {
+        treeLayers.push(spans[i]);
+      };
+      for (i in rels) {
+        treeLayers.push(rels[i]);
+      };
+
       // Get spans
-      var spanLayers = this._match.getSpans().sort(
-	      function (a, b) {
-	        if (a.foundry < b.foundry) {
-	          return -1;
-	        }
-	        else if (a.foundry > b.foundry) {
-	          return 1;
-	        }
-	        else if (a.layer < b.layer) {
-	          return -1;
-	        }
-	        else if (a.layer > b.layer) {
-	          return 1;
-	        };
-	        return 0;
-	      });
+      treeLayers = treeLayers.sort(
+        function (a, b) {
+          if (a.foundry < b.foundry) {
+            return -1;
+          }
+          else if (a.foundry > b.foundry) {
+            return 1;
+          }
+          else if (a.layer < b.layer) {
+            return -1;
+          }
+          else if (a.layer > b.layer) {
+            return 1;
+          };
+          return 0;
+        });
       
       var menuList = [];
       
       // Show tree views
-      for (var i = 0; i < spanLayers.length; i++) {
-	      var span = spanLayers[i];
-	      
-	      // Add foundry/layer to menu list
-	      menuList.push([
-	        span.foundry + '/' + span.layer,
-	        span.foundry,
-	        span.layer
-	      ]);
+      for (var i = 0; i < treeLayers.length; i++) {
+        var span = treeLayers[i];
+        
+        // Add foundry/layer to menu list
+        menuList.push([
+          span.foundry + '/' + span.layer,
+          span.foundry,
+          span.layer
+        ]);
       };
 
       // Create tree menu
@@ -325,8 +337,8 @@
       span.appendChild(treeElement);
 
       span.addEventListener('click', function (e) {
-	      treemenu.show();
-	      treemenu.focus();
+        treemenu.show();
+        treemenu.focus();
       });
       
       this._element = info;
@@ -342,7 +354,7 @@
      */
     treeMenu : function (list) {
       if (this._treeMenu !== undefined)
-	      return this._treeMenu;
+        return this._treeMenu;
       
       return this._treeMenu = matchTreeMenuClass.create(this, list);
     }
diff --git a/dev/js/src/match/infolayer.js b/dev/js/src/match/infolayer.js
index 0812f71..82b9bd3 100644
--- a/dev/js/src/match/infolayer.js
+++ b/dev/js/src/match/infolayer.js
@@ -23,26 +23,26 @@
     // Initialize Layer 
     _init : function (foundry, layer, type) {
       if (foundry === undefined)
-	throw new Error("Missing parameters");
+	      throw new Error("Missing parameters");
       
       if (layer === undefined) {
-	if (_AvailableRE.exec(foundry)) {
-	  this.foundry = RegExp.$1;
-	  this.layer = RegExp.$2;
-	  this.type = RegExp.$3;
-	}
-	else {
-	  throw new Error("Missing parameters");
-	};
+	      if (_AvailableRE.exec(foundry)) {
+	        this.foundry = RegExp.$1;
+	        this.layer = RegExp.$2;
+	        this.type = RegExp.$3;
+	      }
+	      else {
+	        throw new Error("Missing parameters");
+	      };
       }
       else {
-	this.foundry = foundry;
-	this.layer = layer;
-	this.type = type;
+	      this.foundry = foundry;
+	      this.layer = layer;
+	      this.type = type;
       };
       
       if (this.type === undefined)
-	this.type = 'tokens';
+	      this.type = 'tokens';
 
       return this;
     }
diff --git a/dev/js/src/match/relations.js b/dev/js/src/match/relations.js
index 040a674..b1809a0 100644
--- a/dev/js/src/match/relations.js
+++ b/dev/js/src/match/relations.js
@@ -6,16 +6,21 @@
   return {
     create : function (snippet) {
       var obj = Object.create(this)._init(snippet);
-      obj._tokens = ["0", "1", "2", "3", "4", "5", "6", "7", "8"];
+      obj._tokens = ["Der", "alte", "Mann", "ging", "über", "die", "breite", "nasse", "Straße"];
       obj._tokenElements = [];
       obj._arcs = [
+
+
+        /*
+         * Start and end may be spans, i.e. arrays
+         */
         { start: 0, end: 1, label: "a" },
         { start: 0, end: 1, label: "b" },
         { start: 1, end: 2, label: "c" },
         { start: 0, end: 2, label: "d" },
-        { start: 1, end: 5, label: "e" },
-        { start: 4, end: 8, label: "f" },
-        { start: 6, end: 7, label: "g" },
+        { start: [2,4], end: 5, label: "e" },
+        { start: 4, end: [6,8], label: "f" },
+        { start: [5,6], end: 7, label: "g" },
       ]
       obj.maxArc = 200; // maximum height of the bezier control point
       return obj;
@@ -40,14 +45,42 @@
       return box.x + (box.width / 2);
     },
 
+    _drawAnchor : function (anchor) {
+      var startPos = this._tokenElements[anchor.first].getBoundingClientRect().left;
+      var endPos = this._tokenElements[anchor.last].getBoundingClientRect().right;
+
+      var y = (anchor.overlaps * -5) - 10;
+      var l = this._c('path');
+      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 a label
     // Potentially needs a height parameter for stacks
     _drawArc : function (arc) {
 
-      var startPos = this._tokenPoint(this._tokenElements[arc.first]);
-      var endPos = this._tokenPoint(this._tokenElements[arc.last]);
+      var startPos, endPos;
+      var startY = 0, endY = 0;
 
-      var y = 0;
+      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]);
+      };
+
       var g = this._c("g");
       var p = g.appendChild(this._c("path"));
 
@@ -60,10 +93,11 @@
 
       var x = Math.min(startPos, endPos);
       
-      var arcE = "M "+ startPos + " " + y +
-          " C " + startPos + " " + (y-cHeight) +
-          " " + endPos + " " + (y-cHeight) +
-          " " + endPos + " " + y;
+      var arcE = "M "+ startPos + " " + startY +
+          " C " + startPos + " " + (startY + endY - cHeight) +
+          " " + endPos + " " + (startY + endY - cHeight) +
+          " " + endPos + " " + endY;
+
       p.setAttribute("d", arcE);
 
       if (arc.label !== undefined) {
@@ -100,6 +134,13 @@
      */
     _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
@@ -111,6 +152,36 @@
 
       // Normalize start and end
       var sortedArcs = this._arcs.map(function (v) {
+
+        // Check for long anchors
+        if (v.start instanceof Array) {
+          var middle = Math.ceil(Math.abs(v.start[1] - v.start[0]) / 2) + v.start[0];
+
+          v.startAnchor = {
+            "first": v.start[0],
+            "last" : v.start[1],
+            "length" : v.start[1] - v.start[0]
+          };
+
+          // Add to anchors list
+          anchors.push(v.startAnchor);
+          v.start = middle;
+        };
+
+        if (v.end instanceof Array) {
+          var middle = Math.abs(v.end[0] - v.end[1]) + v.end[0];
+          v.endAnchor = {
+            "first": v.end[0],
+            "last" : v.end[1],
+            "length" : v.end[1] - v.end[0]
+          };
+
+          // Add to anchors list
+          anchors.push(v.endAnchor);
+          v.end = middle;
+        };
+
+        // calculate the arch length
         if (v.start < v.end) {
           v.first = v.start;
           v.last = v.end;
@@ -132,49 +203,9 @@
           return 1;
       });
 
-      var arcStack = [];
+      this._sortedArcs = lengthSort(sortedArcs, false);
 
-      // Iterate over all arc definitions
-      for (var i = 0; i < sortedArcs.length; i++) {
-        var currentArc = sortedArcs[i];
-
-        // Check the stack order
-        var overlaps = 0;
-
-        for (var j = (arcStack.length - 1); j >= 0; j--) {
-          var checkArc = arcStack[j];
-
-          // (a..(b..b)..a)
-          if (currentArc.first <= checkArc.first && currentArc.last >= checkArc.last) {
-            overlaps = checkArc.overlaps + 1;
-            break;
-          }
-
-          // (a..(b..a)..b)
-          else if (currentArc.first < checkArc.first && currentArc.last > checkArc.first) {
-            overlaps = checkArc.overlaps + (currentArc.length == checkArc.length ? 0 : 1);
-          }
-
-          // (b..(a..b)..a)
-          else if (currentArc.first < checkArc.last && currentArc.last > checkArc.last) {
-            overlaps = checkArc.overlaps + (currentArc.length == checkArc.length ? 0 : 1);
-          };
-        };
-
-        // Set overlaps
-        currentArc.overlaps = overlaps;
-
-        arcStack.push(currentArc);
-
-        // Although it is already sorted,
-        // the new item has to be put at the correct place
-        // TODO: Use something like splice() instead
-        arcStack.sort(function (a,b) {
-          b.overlaps - a.overlaps
-        });
-      };
-
-      return arcStack;
+      this._sortedAnchors = lengthSort(anchors, true);
     },
     
     show : function () {
@@ -215,11 +246,78 @@
        * needs to be calculated to make it possible to "stack" arcs.
        * That means, the arcs need to be presorted, so massively
        * overlapping arcs are taken first.
+       * On the other hand, anchors need to be sorted as well
+       * in the same way.
        */
-      var sortedArcs = this._sortArcs();
-      for (var i in sortedArcs) {
-        this.arcs.appendChild(this._drawArc(sortedArcs[i]));
+      this._sortArcs();
+
+      var i;
+      for (i in this._sortedAnchors) {
+        this.arcs.appendChild(this._drawAnchor(this._sortedAnchors[i]));
+      };
+      
+      for (i in this._sortedArcs) {
+        this.arcs.appendChild(this._drawArc(this._sortedArcs[i]));
       };
     }
-  }
+  };
+
+  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;
+  };
 });
diff --git a/dev/js/src/match/treeitem.js b/dev/js/src/match/treeitem.js
index 9a4b952..9ddb1fe 100644
--- a/dev/js/src/match/treeitem.js
+++ b/dev/js/src/match/treeitem.js
@@ -11,7 +11,7 @@
      */
     create : function (params) {
       return Object.create(itemClass)
-	.upgradeTo(this)._init(params);
+	      .upgradeTo(this)._init(params);
     },
 
     /**
@@ -20,7 +20,7 @@
      */
     content : function (content) {
       if (arguments.length === 1) {
-	this._content = content;
+	      this._content = content;
       };
       return this._content;
     },
@@ -47,13 +47,13 @@
       menu.hide();
       e.halt();
       if (menu.info() !== undefined)
-	menu.info().addTree(this._foundry, this._layer);
+	      menu.info().addTree(this._foundry, this._layer);
     },
 
     // Initialize tree menu item.
     _init : function (params) {
       if (params[0] === undefined)
-	throw new Error("Missing parameters");
+	      throw new Error("Missing parameters");
 
       this._name    = params[0];
       this._foundry = params[1];
diff --git a/dev/js/src/match/treemenu.js b/dev/js/src/match/treemenu.js
index 5af4619..2ca0731 100644
--- a/dev/js/src/match/treemenu.js
+++ b/dev/js/src/match/treemenu.js
@@ -17,14 +17,14 @@
      */
     create : function (info, params) {
       var obj = Object.create(menuClass)
-	.upgradeTo(this)
-	._init(params, {itemClass : itemClass});
+	        .upgradeTo(this)
+	        ._init(params, {itemClass : itemClass});
       obj.limit(6);
       obj._info = info;
 
       // This is only domspecific
       obj.element().addEventListener('blur', function (e) {
-	this.menu.hide();
+	      this.menu.hide();
       });
       
       return obj;