blob: 4e3786ba00e133fbe8e2eb0a502a7f8767817866 [file] [log] [blame]
/**
* 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 () {
KorAP.log(0, 'KorAP.API.getMatchInfo() not implemented')
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().toggle();
});
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._element !== undefined)
return this._info;
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;
this.opened = false;
return this;
},
/**
* Get match object
*/
match : function () {
return this._match;
},
toggle : function () {
if (this.opened == true) {
this._match.element().children[0].removeChild(
this.element()
);
this.opened = false;
}
else {
// Append element to match
this._match.element().children[0].appendChild(
this.element()
);
this.opened = true;
};
return this.opened;
},
/**
* Retrieve and parse snippet for table representation
*/
getTable : function (tokens, cb) {
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)
cb(null);
// Get info (may be cached)
// TODO: Async
KorAP.API.getMatchInfo(
this._match,
{ 'spans' : false, 'layer' : focus },
// Callback for retrieval
function (matchResponse) {
// Get snippet from match info
if (matchResponse["snippet"] !== undefined) {
this._table = KorAP.MatchTable.create(matchResponse["snippet"]);
cb(this._table);
};
}.bind(this)
);
/*
// Todo: Store the table as a hash of the focus
return null;
*/
},
/**
* Retrieve and parse snippet for tree representation
*/
getTree : function (foundry, layer, cb) {
var focus = [];
// 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
cb(KorAP.MatchTree.create(matchResponse["snippet"]));
}
else {
cb(null);
};
}.bind(this)
);
},
/**
* 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, cb) {
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')
);
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();
}
);
// Get tree data async
this.getTree(foundry, layer, function (treeObj) {
// 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)
treeObj.center();
}
if (cb !== undefined)
cb(treeObj);
});
},
/**
* 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');
info.appendChild(matchtable);
// Create the table asynchronous
this.getTable(undefined, function (table) {
if (table !== null) {
matchtable.appendChild(table.element());
};
});
// 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, 'rx', 5);
rect.setAttributeNS(null, 'ry', 5);
rect.setAttributeNS(null, 'width', v.width);
rect.setAttributeNS(null, 'height', v.height);
if (v.class === 'root' && v.label === undefined) {
rect.setAttributeNS(null, 'width', v.height);
rect.setAttributeNS(null, 'x', v.x - v.height / 2);
rect.setAttributeNS(null, 'class', 'empty');
};
// Add label
if (v.label !== undefined) {
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));