Major redesign of JS and Sass assets
diff --git a/public/js/src/match.js b/public/js/src/match.js
new file mode 100644
index 0000000..f8fdd91
--- /dev/null
+++ b/public/js/src/match.js
@@ -0,0 +1,1021 @@
+/**
+ * Get information on matches,
+ * generate annotation tables and trees.
+ *
+ * @author Nils Diewald
+ */
+// require menu.js, dagre
+/*
+ * - Highlight (at least mark as bold) the match
+ * - Scroll to match vertically per default
+ */
+var KorAP = KorAP || {};
+
+(function (KorAP) {
+ "use strict";
+
+ var svgXmlns = "http://www.w3.org/2000/svg";
+
+ // Default log message
+ KorAP.log = KorAP.log || function (type, msg) {
+ console.log(type + ": " + msg);
+ };
+
+ // Localization values
+ var loc = (KorAP.Locale = KorAP.Locale || {} );
+ loc.ADDTREE = loc.ADDTREE || 'Add tree view';
+ loc.SHOWINFO = loc.SHOWINFO || 'Show information';
+ loc.CLOSE = loc.CLOSE || 'Close';
+
+ KorAP._AvailableRE = new RegExp("^([^\/]+?)\/([^=]+?)(?:=(spans|rels|tokens))?$");
+ KorAP._TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
+ KorAP._matchTerms = ['corpusID', 'docID', 'textID', 'matchID', 'available'];
+
+ // API requests
+ KorAP.API = KorAP.API || {};
+
+ // TODO: Make this async
+ KorAP.API.getMatchInfo = KorAP.API.getMatchInfo || function () { return {} };
+
+
+ /**
+ * Match object
+ */
+ KorAP.Match = {
+
+ /**
+ * Create a new annotation object.
+ * Expects an array of available foundry/layer=type terms.
+ * Supported types are 'spans', 'tokens' and 'rels'.
+ */
+ create : function (match) {
+ return Object.create(KorAP.Match)._init(match);
+ },
+
+ /**
+ * Initialize match.
+ */
+ _init : function (match) {
+ this._element = null;
+
+ // No match defined
+ if (arguments.length < 1 ||
+ match === null ||
+ match === undefined) {
+ throw new Error('Missing parameters');
+ }
+
+ // Match defined as a node
+ else if (match instanceof Node) {
+ this._element = match;
+
+ // Circular reference !!
+ match["_match"] = this;
+
+ this.corpusID = match.getAttribute('data-corpus-id'),
+ this.docID = match.getAttribute('data-doc-id'),
+ this.textID = match.getAttribute('data-text-id'),
+ this.matchID = match.getAttribute('data-match-id')
+
+ // List of available annotations
+ this.available = match.getAttribute('data-available-info').split(' ');
+ }
+
+ // Match as an object
+ else {
+
+ // Iterate over allowed match terms
+ for (var i in KorAP._matchTerms) {
+ var term = KorAP._matchTerms[i];
+ if (match[term] !== undefined) {
+ this[term] = match[term];
+ }
+ else {
+ this[term] = undefined;
+ }
+ };
+ };
+
+ this._available = {
+ tokens : [],
+ spans : [],
+ rels : []
+ };
+
+ // Iterate over info layers
+ for (var i = 0; i < this.available.length; i++) {
+ var term = this.available[i];
+
+ // Create info layer objects
+ try {
+ var layer = KorAP.InfoLayer.create(term);
+ this._available[layer.type].push(layer);
+ }
+ catch (e) {
+ continue;
+ };
+ };
+
+ return this;
+ },
+
+
+ /**
+ * Return a list of parseable tree annotations.
+ */
+ getSpans : function () {
+ return this._available.spans;
+ },
+
+
+ /**
+ * Return a list of parseable token annotations.
+ */
+ getTokens : function () {
+ return this._available.tokens;
+ },
+
+
+ /**
+ * Return a list of parseable relation annotations.
+ */
+ getRels : function () {
+ return this._available.rels;
+ },
+
+ /**
+ * Open match
+ */
+ open : function () {
+
+ // Add actions unless it's already activated
+ var element = this._element;
+
+ // There is an element to open
+ if (this._element === undefined || this._element === null)
+ return false;
+
+ // The element is already opened
+ if (element.classList.contains('active'))
+ return false;
+
+ // Add active class to element
+ element.classList.add('active');
+
+ // Create action buttons
+ var ul = document.createElement('ul');
+ ul.classList.add('action', 'right');
+ element.appendChild(ul);
+
+ // Use localization
+ var loc = KorAP.Locale;
+
+ // Add close button
+ var close = document.createElement('li');
+ close.appendChild(document.createElement('span'))
+ .appendChild(document.createTextNode(loc.CLOSE));
+ close.classList.add('close');
+ close.setAttribute('title', loc.CLOSE);
+
+ // Add info button
+ var info = document.createElement('li');
+ info.appendChild(document.createElement('span'))
+ .appendChild(document.createTextNode(loc.SHOWINFO));
+ info.classList.add('info');
+ info.setAttribute('title', loc.SHOWINFO);
+
+ var that = this;
+
+ // Close match
+ close.addEventListener('click', function (e) {
+ e.halt();
+ that.close()
+ });
+
+ // Add information, unless it already exists
+ info.addEventListener('click', function (e) {
+ e.halt();
+ that.info();
+ });
+
+ ul.appendChild(close);
+ ul.appendChild(info);
+
+ return true;
+ },
+
+
+ /**
+ * Close info view
+ */
+ close : function () {
+ this._element.classList.remove('active');
+
+/*
+ if (this._info !== undefined) {
+ this._info.destroy();
+ };
+*/
+ },
+
+
+
+ /**
+ * Get and open associated match info.
+ */
+ info : function () {
+
+ // Create match info
+ if (this._info === undefined)
+ this._info = KorAP.MatchInfo.create(this);
+
+ // There is an element to append
+ if (this._element === undefined ||
+ this._element === null)
+ return this._info;
+
+ // Info is already activated
+ if (this._info._elemet !== undefined)
+ return this._info;
+
+ // Append element to match
+ this._element.children[0].appendChild(
+ this._info.element()
+ );
+
+ return this._info;
+ },
+
+
+ /**
+ * Get match element.
+ */
+ element : function () {
+
+ // May be null
+ return this._element;
+ }
+ };
+
+
+
+ /**
+ * Information about a match.
+ */
+ KorAP.MatchInfo = {
+
+ /**
+ * Create new object
+ */
+ create : function (match) {
+ return Object.create(KorAP.MatchInfo)._init(match);
+ },
+
+ /**
+ * Initialize object
+ */
+ _init : function (match) {
+ this._match = match;
+ return this;
+ },
+
+
+ /**
+ * Get match object
+ */
+ match : function () {
+ return this._match;
+ },
+
+
+ /**
+ * Retrieve and parse snippet for table representation
+ */
+ getTable : function (tokens) {
+ var focus = [];
+
+ // Get all tokens
+ if (tokens === undefined) {
+ 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 = KorAP.InfoLayer.create(term);
+ layer.type = "tokens";
+ focus.push(layer);
+ }
+ catch (e) {
+ continue;
+ };
+ };
+ };
+
+ // No tokens chosen
+ if (focus.length == 0)
+ return;
+
+ // Get info (may be cached)
+ // TODO: Async
+ var matchResponse = KorAP.API.getMatchInfo(
+ this._match,
+ { 'spans' : false, 'layer' : focus }
+ );
+
+ // Get snippet from match info
+ if (matchResponse["snippet"] !== undefined) {
+ this._table = KorAP.MatchTable.create(matchResponse["snippet"]);
+ return this._table;
+ };
+
+ // Todo: Store the table as a hash of the focus
+
+ return null;
+ },
+
+
+ /**
+ * Retrieve and parse snippet for tree representation
+ */
+ getTree : function (foundry, layer) {
+ var focus = [];
+
+ // TODO: Async
+ var matchResponse = KorAP.API.getMatchInfo(
+ this._match, {
+ 'spans' : true,
+ 'foundry' : foundry,
+ 'layer' : layer
+ }
+ );
+
+ // TODO: Support and cache multiple trees
+
+ // Get snippet from match info
+ if (matchResponse["snippet"] !== undefined) {
+ // Todo: This should be cached somehow
+ return KorAP.MatchTree.create(matchResponse["snippet"]);
+ };
+
+ return null;
+ },
+
+ /**
+ * Destroy this match information view.
+ */
+ destroy : function () {
+
+ // Remove circular reference
+ if (this._treeMenu !== undefined)
+ delete this._treeMenu["info"];
+
+ this._treeMenu.destroy();
+ this._treeMenu = undefined;
+ this._match = undefined;
+
+ // Element destroy
+ },
+
+
+ /**
+ * Add a new tree view to the list
+ */
+ addTree : function (foundry, layer) {
+ var treeObj = this.getTree(foundry, layer);
+
+ // Something went wrong - probably log!!!
+ if (treeObj === null)
+ return;
+
+ var matchtree = document.createElement('div');
+ matchtree.classList.add('matchtree');
+
+ var h6 = matchtree.appendChild(document.createElement('h6'));
+ h6.appendChild(document.createElement('span'))
+ .appendChild(document.createTextNode(foundry));
+ h6.appendChild(document.createElement('span'))
+ .appendChild(document.createTextNode(layer));
+
+ var tree = matchtree.appendChild(
+ document.createElement('div')
+ );
+ tree.appendChild(treeObj.element());
+ this._element.insertBefore(matchtree, this._element.lastChild);
+
+ var close = tree.appendChild(document.createElement('em'));
+ close.addEventListener(
+ 'click', function (e) {
+ matchtree.parentNode.removeChild(matchtree);
+ e.halt();
+ }
+ );
+
+ // 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)
+ treeObj.center();
+ },
+
+ /**
+ * Create match information view.
+ */
+ element : function () {
+
+ if (this._element !== undefined)
+ return this._element;
+
+ // Create info table
+ var info = document.createElement('div');
+ info.classList.add('matchinfo');
+
+ // Append default table
+ var matchtable = document.createElement('div');
+ matchtable.classList.add('matchtable');
+ matchtable.appendChild(this.getTable().element());
+ info.appendChild(matchtable);
+
+ // 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;
+ });
+
+ 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
+ ]);
+ };
+
+ // Create tree menu
+ var treemenu = this.treeMenu(menuList);
+ var span = info.appendChild(document.createElement('p'));
+ span.classList.add('addtree');
+ span.appendChild(document.createTextNode(loc.ADDTREE));
+
+ var treeElement = treemenu.element();
+ span.appendChild(treeElement);
+
+ span.addEventListener('click', function (e) {
+ treemenu.show('');
+ treemenu.focus();
+ });
+
+ this._element = info;
+
+ return info;
+
+ },
+
+
+ /**
+ * Get tree menu.
+ * There is only one menu rendered
+ * - no matter how many trees exist
+ */
+ treeMenu : function (list) {
+ if (this._treeMenu !== undefined)
+ return this._treeMenu;
+
+ return this._treeMenu = KorAP.MatchTreeMenu.create(this, list);
+ }
+ };
+
+
+
+ /**
+ *
+ * Alternatively pass a string as <tt>base/s=span</tt>
+ *
+ * @param foundry
+ */
+ KorAP.InfoLayer = {
+ create : function (foundry, layer, type) {
+ return Object.create(KorAP.InfoLayer)._init(foundry, layer, type);
+ },
+ _init : function (foundry, layer, type) {
+ if (foundry === undefined)
+ throw new Error("Missing parameters");
+
+ if (layer === undefined) {
+ if (KorAP._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;
+ };
+
+ if (this.type === undefined)
+ this.type = 'tokens';
+
+ return this;
+ }
+ };
+
+
+ KorAP.MatchTable = {
+ create : function (snippet) {
+ return Object.create(KorAP.MatchTable)._init(snippet);
+ },
+ _init : function (snippet) {
+ // Create html for traversal
+ var html = document.createElement("div");
+ html.innerHTML = snippet;
+
+ this._pos = 0;
+ this._token = [];
+ this._info = [];
+ this._foundry = {};
+ this._layer = {};
+
+ // Parse the snippet
+ this._parse(html.childNodes);
+
+ html.innerHTML = '';
+ return this;
+ },
+
+ length : function () {
+ return this._pos;
+ },
+
+ getToken : function (pos) {
+ if (pos === undefined)
+ return this._token;
+ return this._token[pos];
+ },
+
+ getValue : function (pos, foundry, layer) {
+ return this._info[pos][foundry + '/' + layer]
+ },
+
+ getLayerPerFoundry : function (foundry) {
+ return this._foundry[foundry]
+ },
+
+ getFoundryPerLayer : function (layer) {
+ return this._layer[layer];
+ },
+
+ // Parse the snippet
+ _parse : function (children) {
+
+ // Get all children
+ for (var i in children) {
+ var c = children[i];
+
+ // Create object on position unless it exists
+ if (this._info[this._pos] === undefined)
+ this._info[this._pos] = {};
+
+ // Store at position in foundry/layer as array
+ var found = this._info[this._pos];
+
+ // Element with title
+ if (c.nodeType === 1) {
+ if (c.getAttribute("title") &&
+ KorAP._TermRE.exec(c.getAttribute("title"))) {
+
+ // Fill position with info
+ var foundry, layer, value;
+ if (RegExp.$2) {
+ foundry = RegExp.$1;
+ layer = RegExp.$2;
+ }
+ else {
+ foundry = "base";
+ layer = RegExp.$1
+ };
+
+ value = RegExp.$3;
+
+ if (found[foundry + "/" + layer] === undefined)
+ found[foundry + "/" + layer] = [];
+
+ // Push value to foundry/layer at correct position
+ found[foundry + "/" + layer].push(RegExp.$3);
+
+ // Set foundry
+ if (this._foundry[foundry] === undefined)
+ this._foundry[foundry] = {};
+ this._foundry[foundry][layer] = 1;
+
+ // Set layer
+ if (this._layer[layer] === undefined)
+ this._layer[layer] = {};
+ this._layer[layer][foundry] = 1;
+ };
+
+ // depth search
+ if (c.hasChildNodes())
+ this._parse(c.childNodes);
+ }
+
+ // Leaf node
+ // store string on position and go to next string
+ else if (c.nodeType === 3) {
+ if (c.nodeValue.match(/[a-z0-9]/i))
+ this._token[this._pos++] = c.nodeValue;
+ };
+ };
+
+ delete this._info[this._pos];
+ },
+
+
+ /**
+ * Get HTML table view of annotations.
+ */
+ element : function () {
+ if (this._element !== undefined)
+ return this._element;
+
+ // First the legend table
+ var d = document;
+ var table = d.createElement('table');
+
+ // Single row in head
+ var tr = table.appendChild(d.createElement('thead'))
+ .appendChild(d.createElement('tr'));
+
+ // Add cell to row
+ var addCell = function (type, name) {
+ var c = this.appendChild(d.createElement(type))
+ if (name === undefined)
+ return c;
+
+ if (name instanceof Array) {
+ for (var n = 0; n < name.length; n++) {
+ c.appendChild(d.createTextNode(name[n]));
+ if (n !== name.length - 1) {
+ c.appendChild(d.createElement('br'));
+ };
+ };
+ }
+ else {
+ c.appendChild(d.createTextNode(name));
+ };
+ };
+
+ tr.addCell = addCell;
+
+ // Add header information
+ tr.addCell('th', 'Foundry');
+ tr.addCell('th', 'Layer');
+
+ // Add tokens
+ for (var i in this._token) {
+ tr.addCell('th', this.getToken(i));
+ };
+
+ var tbody = table.appendChild(
+ d.createElement('tbody')
+ );
+
+ var foundryList = Object.keys(this._foundry).sort();
+
+ for (var f = 0; f < foundryList.length; f++) {
+ var foundry = foundryList[f];
+ var layerList =
+ Object.keys(this._foundry[foundry]).sort();
+
+ for (var l = 0; l < layerList.length; l++) {
+ var layer = layerList[l];
+ tr = tbody.appendChild(
+ d.createElement('tr')
+ );
+ tr.setAttribute('tabindex', 0);
+ tr.addCell = addCell;
+
+ tr.addCell('th', foundry);
+ tr.addCell('th', layer);
+
+ for (var v = 0; v < this.length(); v++) {
+ tr.addCell(
+ 'td',
+ this.getValue(v, foundry, layer)
+ );
+ };
+ };
+ };
+
+ return this._element = table;
+ }
+ };
+
+
+ /**
+ * Visualize span annotations as a tree using Dagre.
+ */
+ KorAP.MatchTree = {
+
+ create : function (snippet) {
+ return Object.create(KorAP.MatchTree)._init(snippet);
+ },
+
+ nodes : function () {
+ return this._next;
+ },
+
+ _addNode : function (id, obj) {
+ obj["width"] = 55;
+ obj["height"] = 20;
+ this._graph.setNode(id, obj)
+ },
+
+ _addEdge : function (src, target) {
+ this._graph.setEdge(src, target);
+ },
+
+ _init : function (snippet) {
+ this._next = new Number(0);
+
+ // Create html for traversal
+ var html = document.createElement("div");
+ html.innerHTML = snippet;
+ var g = new dagre.graphlib.Graph({
+ "directed" : true
+ });
+ g.setGraph({
+ "nodesep" : 35,
+ "ranksep" : 15,
+ "marginx" : 40,
+ "marginy" : 10
+ });
+ g.setDefaultEdgeLabel({});
+
+ this._graph = g;
+
+ // This is a new root
+ this._addNode(
+ this._next++,
+ { "class" : "root" }
+ );
+
+ // Parse nodes from root
+ this._parse(0, html.childNodes);
+
+ // Root node has only one child - remove
+ if (g.outEdges(0).length === 1)
+ g.removeNode(0);
+
+ html = undefined;
+ return this;
+ },
+
+ // Remove foundry and layer for labels
+ _clean : function (title) {
+ return title.replace(KorAP._TermRE, "$3");
+ },
+
+ // Parse the snippet
+ _parse : function (parent, children) {
+ for (var i in children) {
+ var c = children[i];
+
+ // Element node
+ if (c.nodeType == 1) {
+
+ // Get title from html
+ if (c.getAttribute("title")) {
+ var title = this._clean(c.getAttribute("title"));
+
+ // Add child node
+ var id = this._next++;
+
+ this._addNode(id, {
+ "class" : "middle",
+ "label" : title
+ });
+ this._addEdge(parent, id);
+
+ // Check for next level
+ if (c.hasChildNodes())
+ this._parse(id, c.childNodes);
+ }
+
+ // Step further
+ else if (c.hasChildNodes())
+ this._parse(parent, c.childNodes);
+ }
+
+ // Text node
+ else if (c.nodeType == 3)
+
+ if (c.nodeValue.match(/[-a-z0-9]/i)) {
+
+ // Add child node
+ var id = this._next++;
+ this._addNode(id, {
+ "class" : "leaf",
+ "label" : c.nodeValue
+ });
+
+ this._addEdge(parent, id);
+ };
+ };
+ return this;
+ },
+
+ /**
+ * Center the viewport of the canvas
+ */
+ 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;
+ };
+ },
+
+ // Get element
+ element : function () {
+ if (this._element !== undefined)
+ return this._element;
+
+ var g = this._graph;
+
+ dagre.layout(g);
+
+ var canvas = document.createElementNS(svgXmlns, 'svg');
+ this._element = canvas;
+
+ canvas.setAttribute('height', g.graph().height);
+ canvas.setAttribute('width', g.graph().width);
+
+ // Create edges
+ g.edges().forEach(
+ function (e) {
+ var src = g.node(e.v);
+ var target = g.node(e.w);
+ var p = document.createElementNS(svgXmlns, 'path');
+ p.setAttributeNS(null, "d", _line(src, target));
+ p.classList.add('edge');
+ canvas.appendChild(p);
+ });
+
+ // Create nodes
+ g.nodes().forEach(
+ function (v) {
+ v = g.node(v);
+ var group = document.createElementNS(svgXmlns, 'g');
+ group.classList.add(v.class);
+
+ // Add node box
+ var rect = group.appendChild(document.createElementNS(svgXmlns, 'rect'));
+ rect.setAttributeNS(null, 'x', v.x - v.width / 2);
+ rect.setAttributeNS(null, 'y', v.y - v.height / 2);
+ rect.setAttributeNS(null, 'width', v.width);
+ rect.setAttributeNS(null, 'height', v.height);
+ rect.setAttributeNS(null, 'rx', 5);
+ rect.setAttributeNS(null, 'ry', 5);
+
+ // Add label
+ var text = group.appendChild(document.createElementNS(svgXmlns, 'text'));
+ text.setAttributeNS(null, 'x', v.x - v.width / 2);
+ text.setAttributeNS(null, 'y', v.y - v.height / 2);
+ text.setAttributeNS(
+ null,
+ 'transform',
+ 'translate(' + v.width/2 + ',' + ((v.height / 2) + 5) + ')'
+ );
+ var tspan = document.createElementNS(svgXmlns, 'tspan');
+ tspan.appendChild(document.createTextNode(v.label));
+ text.appendChild(tspan);
+ canvas.appendChild(group);
+ }
+ );
+
+ return this._element;
+ }
+ };
+
+ /**
+ * Menu item for tree view choice.
+ */
+ KorAP.MatchTreeItem = {
+ create : function (params) {
+ return Object.create(KorAP.MenuItem)
+ .upgradeTo(KorAP.MatchTreeItem)._init(params);
+ },
+ content : function (content) {
+ if (arguments.length === 1) {
+ this._content = content;
+ };
+ return this._content;
+ },
+
+ // The foundry attribute
+ foundry : function () {
+ return this._foundry;
+ },
+
+ // The layer attribute
+ layer : function () {
+ return this._layer;
+ },
+
+ // enter or click
+ onclick : function (e) {
+ var menu = this.menu();
+ menu.hide();
+ e.halt();
+ if (menu.info() !== undefined)
+ menu.info().addTree(this._foundry, this._layer);
+ },
+
+ _init : function (params) {
+ if (params[0] === undefined)
+ throw new Error("Missing parameters");
+
+ this._name = params[0];
+ this._foundry = params[1];
+ this._layer = params[2];
+ this._content = document.createTextNode(this._name);
+ this._lcField = ' ' + this.content().textContent.toLowerCase();
+ return this;
+ }
+ };
+
+
+ /**
+ * Menu to choose from for tree views.
+ */
+ KorAP.MatchTreeMenu = {
+ create : function (info, params) {
+ var obj = Object.create(KorAP.Menu)
+ .upgradeTo(KorAP.MatchTreeMenu)
+ ._init(KorAP.MatchTreeItem, undefined, params);
+ obj.limit(6);
+
+ obj._info = info;
+
+ // This is only domspecific
+ obj.element().addEventListener('blur', function (e) {
+ this.menu.hide();
+ });
+
+ return obj;
+ },
+ info :function () {
+ return this._info;
+ }
+ };
+
+
+ // Create path for node connections
+ function _line (src, target) {
+ var x1 = src.x,
+ y1 = src.y,
+ x2 = target.x,
+ y2 = target.y - target.height / 2;
+
+ // c 0,0 -10,0
+ return 'M ' + x1 + ',' + y1 + ' ' +
+ 'C ' + x1 + ',' + y1 + ' ' +
+ x2 + ',' + (y2 - (y2 - y1) / 2) + ' ' +
+ x2 + ',' + y2;
+ };
+
+}(this.KorAP));