blob: 540a60518026ea50688164d884800fbb904c36ac [file] [log] [blame]
Nils Diewalde8518f82015-03-18 22:41:49 +00001/**
Nils Diewald4f6521a2015-03-20 21:30:13 +00002 * Visualize annotations.
Nils Diewalde8518f82015-03-18 22:41:49 +00003 *
4 * @author Nils Diewald
5 */
6/*
7 - Scroll with a static left legend.
8 - Highlight (at least mark as bold) the match
9 - Scroll to match vertically per default
10 */
11var KorAP = KorAP || {};
12
13(function (KorAP) {
14 "use strict";
15
Nils Diewald4f6521a2015-03-20 21:30:13 +000016 // Default log message
17 KorAP.log = KorAP.log || function (type, msg) {
18 console.log(type + ": " + msg);
19 };
20
Nils Diewalde8518f82015-03-18 22:41:49 +000021 KorAP._AvailableRE = new RegExp("^([^\/]+?)\/([^=]+?)(?:=(spans|rels|tokens))?$");
Nils Diewald4f6521a2015-03-20 21:30:13 +000022 KorAP._TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
Nils Diewalde8518f82015-03-18 22:41:49 +000023 KorAP._matchTerms = ["corpusID", "docID", "textID"];
24
25 // API requests
26 KorAP.API = KorAP.API || {};
Nils Diewald8bc7e412015-03-19 22:08:27 +000027 // TODO: Make this async
Nils Diewalde8518f82015-03-18 22:41:49 +000028 KorAP.API.getMatchInfo = KorAP.API.getMatchInfo || function () { return {} };
29
30 KorAP.MatchInfo = {
31
32 /**
33 * Create a new annotation object.
34 * Expects an array of available foundry/layer=type terms.
35 * Supported types are 'spans', 'tokens' and 'rels'.
36 */
37 create : function (match, available) {
38 if (arguments.length < 2)
39 throw new Error("Missing parameters");
40
41 return Object.create(KorAP.MatchInfo)._init(match, available);
42 },
43
44 _init : function (match, available) {
45 this._match = KorAP.Match.create(match);
46 this._available = {
47 tokens : [],
48 spans : [],
49 rels : []
50 };
51 for (var i = 0; i < available.length; i++) {
52 var term = available[i];
53 // Create info layer objects
54 try {
55 var layer = KorAP.InfoLayer.create(term);
56 this._available[layer.type].push(layer);
57 }
58 catch (e) {
59 continue;
60 };
61 };
62 return this;
63 },
64
65
66 /**
67 * Return a list of parseable tree annotations.
68 */
69 getSpans : function () {
70 return this._available.spans;
71 },
72
73
74 /**
75 * Return a list of parseable token annotations.
76 */
77 getTokens : function () {
78 return this._available.tokens;
79 },
80
81
82 /**
83 * Return a list of parseable relation annotations.
84 */
85 getRels : function () {
86 return this._available.rels;
87 },
88
89
Nils Diewald8bc7e412015-03-19 22:08:27 +000090 /**
91 * Get table object.
92 */
Nils Diewalde8518f82015-03-18 22:41:49 +000093 getTable : function (tokens) {
94 var focus = [];
95
96 // Get all tokens
97 if (tokens === undefined) {
98 focus = this.getTokens();
99 }
100
101 // Get only some tokens
102 else {
103
104 // Push newly to focus array
105 for (var i = 0; i < tokens.length; i++) {
106 var term = tokens[i];
107 try {
108 // Create info layer objects
109 var layer = KorAP.InfoLayer.create(term);
110 layer.type = "tokens";
111 focus.push(layer);
112 }
113 catch (e) {
114 continue;
115 };
116 };
117 };
118
119 // No tokens chosen
120 if (focus.length == 0)
121 return;
122
123 // Get info (may be cached)
Nils Diewald4f6521a2015-03-20 21:30:13 +0000124 // TODO: Async
Nils Diewalde8518f82015-03-18 22:41:49 +0000125 var matchResponse = KorAP.API.getMatchInfo(
126 this._match,
Nils Diewald4f6521a2015-03-20 21:30:13 +0000127 { 'spans' : false, 'layer' : focus }
Nils Diewalde8518f82015-03-18 22:41:49 +0000128 );
129
130 // Get snippet from match info
131 if (matchResponse["snippet"] !== undefined) {
Nils Diewald8bc7e412015-03-19 22:08:27 +0000132 this._table = KorAP.MatchTable.create(matchResponse["snippet"]);
Nils Diewalde8518f82015-03-18 22:41:49 +0000133 return this._table;
134 };
135
136 return null;
Nils Diewald4f6521a2015-03-20 21:30:13 +0000137 },
Nils Diewalde8518f82015-03-18 22:41:49 +0000138
Nils Diewalde8518f82015-03-18 22:41:49 +0000139 // Parse snippet for table visualization
140 getTree : function (foundry, layer) {
Nils Diewald4f6521a2015-03-20 21:30:13 +0000141 var focus = [];
142
143 // TODO: Async
144 var matchResponse = KorAP.API.getMatchInfo(
145 this._match, {
146 'spans' : true,
147 'foundry' : foundry,
148 'layer' : layer
149 }
150 );
151
152 // TODO: Support and cache multiple trees
153
154 // Get snippet from match info
155 if (matchResponse["snippet"] !== undefined) {
156 this._tree = KorAP.MatchTree.create(matchResponse["snippet"]);
157 return this._tree;
158 };
159
160 return null;
161
Nils Diewald8bc7e412015-03-19 22:08:27 +0000162 }
Nils Diewalde8518f82015-03-18 22:41:49 +0000163 };
164
165 KorAP.Match = {
166 create : function (match) {
167 return Object.create(KorAP.Match)._init(match);
168 },
169 _init : function (match) {
170 for (var i in KorAP._matchTerms) {
171 var term = KorAP._matchTerms[i];
172 if (match[term] !== undefined) {
173 this[term] = match[term];
174 }
175 else {
176 this[term] = undefined;
177 }
178 };
179 return this;
180 },
181 };
182
183 /**
184 *
185 * Alternatively pass a string as <tt>base/s=span</tt>
186 *
187 * @param foundry
188 */
189 KorAP.InfoLayer = {
190 create : function (foundry, layer, type) {
191 return Object.create(KorAP.InfoLayer)._init(foundry, layer, type);
192 },
193 _init : function (foundry, layer, type) {
194 if (foundry === undefined)
195 throw new Error("Missing parameters");
196
197 if (layer === undefined) {
198 if (KorAP._AvailableRE.exec(foundry)) {
199 this.foundry = RegExp.$1;
200 this.layer = RegExp.$2;
201 this.type = RegExp.$3;
202 }
203 else {
204 throw new Error("Missing parameters");
205 };
206 }
207 else {
208 this.foundry = foundry;
209 this.layer = layer;
210 this.type = type;
211 };
212
213 if (this.type === undefined)
214 this.type = 'tokens';
215
216 return this;
217 }
218 };
219
220
Nils Diewald8bc7e412015-03-19 22:08:27 +0000221 KorAP.MatchTable = {
Nils Diewalde8518f82015-03-18 22:41:49 +0000222 create : function (snippet) {
Nils Diewald8bc7e412015-03-19 22:08:27 +0000223 return Object.create(KorAP.MatchTable)._init(snippet);
Nils Diewalde8518f82015-03-18 22:41:49 +0000224 },
225 _init : function (snippet) {
226 // Create html for traversal
227 var html = document.createElement("div");
228 html.innerHTML = snippet;
229
230 this._pos = 0;
231 this._token = [];
232 this._info = [];
Nils Diewald8bc7e412015-03-19 22:08:27 +0000233 this._foundry = {};
234 this._layer = {};
Nils Diewalde8518f82015-03-18 22:41:49 +0000235
236 // Parse the snippet
237 this._parse(html.childNodes);
238
Nils Diewalde8518f82015-03-18 22:41:49 +0000239 html.innerHTML = '';
240 return this;
241 },
242
243 length : function () {
244 return this._pos;
245 },
246
247 getToken : function (pos) {
248 if (pos === undefined)
249 return this._token;
250 return this._token[pos];
251 },
252
253 getValue : function (pos, foundry, layer) {
254 return this._info[pos][foundry + '/' + layer]
255 },
256
257 getLayerPerFoundry : function (foundry) {
258 return this._foundry[foundry]
259 },
260
261 getFoundryPerLayer : function (layer) {
262 return this._layer[layer];
263 },
264
265 // Parse the snippet
266 _parse : function (children) {
267
268 // Get all children
269 for (var i in children) {
270 var c = children[i];
271
272 // Create object on position unless it exists
273 if (this._info[this._pos] === undefined)
274 this._info[this._pos] = {};
275
276 // Store at position in foundry/layer as array
277 var found = this._info[this._pos];
278
279 // Element with title
280 if (c.nodeType === 1) {
281 if (c.getAttribute("title") &&
282 KorAP._TermRE.exec(c.getAttribute("title"))) {
283
284 // Fill position with info
285 var foundry, layer, value;
286 if (RegExp.$2) {
287 foundry = RegExp.$1;
288 layer = RegExp.$2;
289 }
290 else {
291 foundry = "base";
292 layer = RegExp.$1
293 };
294
295 value = RegExp.$3;
296
297 if (found[foundry + "/" + layer] === undefined)
298 found[foundry + "/" + layer] = [];
299
300 // Push value to foundry/layer at correct position
301 found[foundry + "/" + layer].push(RegExp.$3);
302
303 // Set foundry
Nils Diewald8bc7e412015-03-19 22:08:27 +0000304 if (this._foundry[foundry] === undefined)
Nils Diewalde8518f82015-03-18 22:41:49 +0000305 this._foundry[foundry] = {};
306 this._foundry[foundry][layer] = 1;
307
308 // Set layer
Nils Diewald8bc7e412015-03-19 22:08:27 +0000309 if (this._layer[layer] === undefined)
Nils Diewalde8518f82015-03-18 22:41:49 +0000310 this._layer[layer] = {};
311 this._layer[layer][foundry] = 1;
312 };
313
314 // depth search
315 if (c.hasChildNodes())
316 this._parse(c.childNodes);
317 }
318
Nils Diewald8bc7e412015-03-19 22:08:27 +0000319 // Leaf node
320 // store string on position and go to next string
Nils Diewalde8518f82015-03-18 22:41:49 +0000321 else if (c.nodeType === 3) {
322 if (c.nodeValue.match(/[a-z0-9]/i))
323 this._token[this._pos++] = c.nodeValue;
324 };
325 };
326
327 delete this._info[this._pos];
328 },
Nils Diewald4f6521a2015-03-20 21:30:13 +0000329
330
331 /**
332 * Get HTML table view of annotations.
333 */
Nils Diewalde8518f82015-03-18 22:41:49 +0000334 element : function () {
Nils Diewalde8518f82015-03-18 22:41:49 +0000335 // First the legend table
Nils Diewald8bc7e412015-03-19 22:08:27 +0000336 var d = document;
337 var table = d.createElement('table');
Nils Diewald8bc7e412015-03-19 22:08:27 +0000338
Nils Diewald4f6521a2015-03-20 21:30:13 +0000339 // Single row in head
340 var tr = table.appendChild(d.createElement('thead'))
341 .appendChild(d.createElement('tr'));
342
343 // Add cell to row
Nils Diewald8bc7e412015-03-19 22:08:27 +0000344 var addCell = function (type, name) {
345 var c = this.appendChild(d.createElement(type))
346 if (name === undefined)
347 return c;
348
349 if (name instanceof Array) {
350 for (var n = 0; n < name.length; n++) {
351 c.appendChild(d.createTextNode(name[n]));
352 if (n !== name.length - 1) {
353 c.appendChild(d.createElement('br'));
354 };
355 };
356 }
357 else {
358 c.appendChild(d.createTextNode(name));
359 };
360 };
361
362 tr.addCell = addCell;
363
364 // Add header information
365 tr.addCell('th', 'Foundry');
366 tr.addCell('th', 'Layer');
367
368 // Add tokens
369 for (var i in this._token) {
370 tr.addCell('th', this.getToken(i));
371 };
372
373 var tbody = table.appendChild(
374 d.createElement('tbody')
375 );
376
377 var foundryList = Object.keys(this._foundry).sort();
378
379 for (var f = 0; f < foundryList.length; f++) {
380 var foundry = foundryList[f];
381 var layerList =
382 Object.keys(this._foundry[foundry]).sort();
383
384 for (var l = 0; l < layerList.length; l++) {
385 var layer = layerList[l];
386 tr = tbody.appendChild(
387 d.createElement('tr')
388 );
389 tr.setAttribute('tabindex', 0);
390 tr.addCell = addCell;
391
392 tr.addCell('th', foundry);
393 tr.addCell('th', layer);
394
395 for (var v = 0; v < this.length(); v++) {
396 tr.addCell(
397 'td',
398 this.getValue(v, foundry, layer)
399 );
400 };
401 };
402 };
403
404 return table;
Nils Diewalde8518f82015-03-18 22:41:49 +0000405 }
406 };
407
Nils Diewald4f6521a2015-03-20 21:30:13 +0000408 /**
409 * Visualize span annotations as a tree.
410 */
411 // http://java-hackers.com/p/paralin/meteor-dagre-d3
412 KorAP.MatchTree = {
413
414 create : function (snippet) {
415 return Object.create(KorAP.MatchTree)._init(snippet);
416 },
417
418 nodes : function () {
419 return this._next;
420 },
421
422 _init : function (snippet) {
423 this._next = new Number(0);
424
425 // Create html for traversal
426 var html = document.createElement("div");
427 html.innerHTML = snippet;
428 this._graph = new dagreD3.Digraph();
429
430 // This is a new root
431 this._graph.addNode(
432 this._next++,
433 { "nodeclass" : "root" }
434 );
435
436 // Parse nodes from root
437 this._parse(0, html.childNodes);
438
439 // Root node has only one child - remove
440 if (Object.keys(this._graph._outEdges[0]).length === 1)
441 this._graph.delNode(0);
442
443 // Initialize d3 renderer for dagre
444 this._renderer = new dagreD3.Renderer();
445 /*
446 var oldDrawNodes = this._renderer.drawNodes();
447 this._renderer.drawNodes(
448 function (graph, root) {
449 var svgNodes = oldDrawNodes(graph, root);
450 svgNodes.each(
451 function (u) {
452 d3.select(this).classed(graph.node(u).nodeClass, true);
453 }
454 );
455 }
456 );
457*/
458 // Disable pan and zoom
459 this._renderer.zoom(false);
460
461 html = undefined;
462 return this;
463 },
464
465 // Remove foundry and layer for labels
466 _clean : function (title) {
467 return title.replace(KorAP._TermRE, RegExp.$1);
468 },
469
470 // Parse the snippet
471 _parse : function (parent, children) {
472 for (var i in children) {
473 var c = children[i];
474
475 // Element node
476 if (c.nodeType == 1) {
477
478 // Get title from html
479 if (c.getAttribute("title")) {
480 var title = this._clean(c.getAttribute("title"));
481
482 // Add child node
483 var id = this._next++;
484
485 this._graph.addNode(id, {
486 "nodeclass" : "middle",
487 "label" : title
488 });
489 this._graph.addEdge(null, parent, id);
490
491 // Check for next level
492 if (c.hasChildNodes())
493 this._parse(id, c.childNodes);
494 }
495
496 // Step further
497 else if (c.hasChildNodes())
498 this._parse(parent, c.childNodes);
499 }
500
501 // Text node
502 else if (c.nodeType == 3)
503
504 if (c.nodeValue.match(/[-a-z0-9]/i)) {
505
506 // Add child node
507 var id = this._next++;
508 this._graph.addNode(id, {
509 "nodeclass" : "leaf",
510 "label" : c.nodeValue
511 });
512
513 this._graph.addEdge(null, parent, id);
514 };
515 };
516 return this;
517 },
518
519 element : function () {
520 this._element = document.createElement('div');
521 var svg = document.createElement('svg');
522 this._element.appendChild(svg);
523 var svgGroup = svg.appendChild(document.createElement('svg:g'));
524
525 svgGroup = d3.select(svgGroup);
526
527 console.log(svgGroup);
528 var layout = this._renderer.run(this._graph, svgGroup);
529/*
530 var w = layout.graph().width;
531 var h = layout.graph().height;
532 this._element.setAttribute("width", w + 10);
533 this._element.setAttribute("height", h + 10);
534 svgGroup.attr("transform", "translate(5, 5)");
535*/
536 return this._element;
537 }
538 };
539
Nils Diewalde8518f82015-03-18 22:41:49 +0000540}(this.KorAP));