brat-client
Version:
Client from brat rapid annotation tool
1,316 lines (1,208 loc) • 140 kB
JavaScript
// -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; -*-
// vim:set ft=javascript ts=2 sw=2 sts=2 cindent:
var Visualizer = (function($, window, undefined) {
var DocumentData = function(text) {
this.text = text;
this.chunks = [];
this.spans = {};
this.eventDescs = {};
this.sentComment = {};
this.arcs = [];
this.arcById = {};
this.markedSent = {};
this.spanAnnTexts = {};
this.towers = {};
// this.sizes = {};
};
var Fragment = function(id, span, from, to) {
this.id = id;
this.span = span;
this.from = from;
this.to = to;
// this.towerId = undefined;
// this.drawOrder = undefined;
};
var Span = function(id, type, offsets, generalType) {
this.id = id;
this.type = type;
this.totalDist = 0;
this.numArcs = 0;
this.generalType = generalType;
this.headFragment = null;
this.unsegmentedOffsets = offsets;
this.offsets = [];
this.segmentedOffsetsMap = {};
// this.unsegmentedOffsets = undefined;
// this.from = undefined;
// this.to = undefined;
// this.wholeFrom = undefined;
// this.wholeTo = undefined;
// this.headFragment = undefined;
// this.chunk = undefined;
// this.marked = undefined;
// this.avgDist = undefined;
// this.curly = undefined;
// this.comment = undefined; // { type: undefined, text: undefined };
// this.annotatorNotes = undefined;
// this.drawCurly = undefined;
// this.glyphedLabelText = undefined;
// this.group = undefined;
// this.height = undefined;
// this.highlightPos = undefined;
// this.indexNumber = undefined;
// this.labelText = undefined;
// this.nestingDepth = undefined;
// this.nestingDepthLR = undefined;
// this.nestingDepthRL = undefined;
// this.nestingHeight = undefined;
// this.nestingHeightLR = undefined;
// this.nestingHeightRL = undefined;
// this.rect = undefined;
// this.rectBox = undefined;
// this.refedIndexSum = undefined;
// this.right = undefined;
// this.totaldist = undefined;
// this.width = undefined;
this.initContainers();
};
Span.prototype.initContainers = function(offsets) {
this.incoming = [];
this.outgoing = [];
this.attributes = {};
this.attributeText = [];
this.attributeCues = {};
this.attributeCueFor = {};
this.attributeMerge = {}; // for box, cross, etc. that are span-global
this.fragments = [];
this.normalizations = [];
};
Span.prototype.splitMultilineOffsets = function(text) {
this.segmentedOffsetsMap = {};
for (var fi = 0, nfi = 0; fi < this.unsegmentedOffsets.length; fi++) {
var begin = this.unsegmentedOffsets[fi][0];
var end = this.unsegmentedOffsets[fi][1];
for (var ti = begin; ti < end; ti++) {
var c = text.charAt(ti);
if (c == '\n' || c == '\r') {
if (begin !== null) {
this.offsets.push([begin, ti])
this.segmentedOffsetsMap[nfi++] = fi;
begin = null;
}
}
else if (begin === null) {
begin = ti;
}
}
if (begin !== null) {
this.offsets.push([begin, end]);
this.segmentedOffsetsMap[nfi++] = fi;
}
}
};
Span.prototype.copy = function(id) {
var span = $.extend(new Span(), this); // clone
span.id = id;
// protect from shallow copy
span.initContainers();
span.unsegmentedOffsets = this.unsegmentedOffsets.slice();
// read-only; shallow copy is fine
span.offsets = this.offsets;
span.segmentedOffsetsMap = this.segmentedOffsetsMap;
return span;
};
var EventDesc = function(id, triggerId, roles, klass) {
this.id = id;
this.triggerId = triggerId;
var roleList = this.roles = [];
$.each(roles, function(roleNo, role) {
roleList.push({ type: role[0], targetId: role[1] });
});
if (klass == "equiv") {
this.equiv = true;
} else if (klass == "relation") {
this.relation = true;
}
// this.leftSpans = undefined;
// this.rightSpans = undefined;
// this.annotatorNotes = undefined;
};
var Chunk = function(index, text, from, to, space, spans) {
this.index = index;
this.text = text;
this.from = from;
this.to = to;
this.space = space;
this.fragments = [];
// this.sentence = undefined;
// this.group = undefined;
// this.highlightGroup = undefined;
// this.markedTextStart = undefined;
// this.markedTextEnd = undefined;
// this.nextSpace = undefined;
// this.right = undefined;
// this.row = undefined;
// this.textX = undefined;
// this.translation = undefined;
}
var Arc = function(eventDesc, role, dist, eventNo) {
this.origin = eventDesc.id;
this.target = role.targetId;
this.dist = dist;
this.type = role.type;
this.shadowClass = eventDesc.shadowClass;
this.jumpHeight = 0;
if (eventDesc.equiv) {
this.equiv = true;
this.eventDescId = eventNo;
eventDesc.equivArc = this;
} else if (eventDesc.relation) {
this.relation = true;
this.eventDescId = eventNo;
}
// this.marked = undefined;
};
var Row = function(svg) {
this.group = svg.group();
this.background = svg.group(this.group);
this.chunks = [];
this.hasAnnotations = false;
this.maxArcHeight = 0;
this.maxSpanHeight = 0;
};
var Measurements = function(widths, height, y) {
this.widths = widths;
this.height = height;
this.y = y;
};
// A naive whitespace tokeniser
var tokenise = function(text) {
var tokenOffsets = [];
var tokenStart = null;
var lastCharPos = null;
for (var i = 0; i < text.length; i++) {
var c = text[i];
// Have we found the start of a token?
if (tokenStart == null && !/\s/.test(c)) {
tokenStart = i;
lastCharPos = i;
// Have we found the end of a token?
} else if (/\s/.test(c) && tokenStart != null) {
tokenOffsets.push([tokenStart, i]);
tokenStart = null;
// Is it a non-whitespace character?
} else if (!/\s/.test(c)) {
lastCharPos = i;
}
}
// Do we have a trailing token?
if (tokenStart != null) {
tokenOffsets.push([tokenStart, lastCharPos + 1]);
}
return tokenOffsets;
};
// A naive newline sentence splitter
var sentenceSplit = function(text) {
var sentenceOffsets = [];
var sentStart = null;
var lastCharPos = null;
for (var i = 0; i < text.length; i++) {
var c = text[i];
// Have we found the start of a sentence?
if (sentStart == null && !/\s/.test(c)) {
sentStart = i;
lastCharPos = i;
// Have we found the end of a sentence?
} else if (c == '\n' && sentStart != null) {
sentenceOffsets.push([sentStart, i]);
sentStart = null;
// Is it a non-whitespace character?
} else if (!/\s/.test(c)) {
lastCharPos = i;
}
}
// Do we have a trailing sentence without a closing newline?
if (sentStart != null) {
sentenceOffsets.push([sentStart, lastCharPos + 1]);
}
return sentenceOffsets;
};
// Sets default values for a wide range of optional attributes
var setSourceDataDefaults = function(sourceData) {
// The following are empty lists if not set
$.each([
'attributes',
'comments',
'entities',
'equivs',
'events',
'modifications',
'normalizations',
'relations',
'triggers',
], function(attrNo, attr) {
if (sourceData[attr] === undefined) {
sourceData[attr] = [];
}
});
// If we lack sentence offsets we fall back on naive sentence splitting
if (sourceData.sentence_offsets === undefined) {
sourceData.sentence_offsets = sentenceSplit(sourceData.text);
}
// Similarily we fall back on whitespace tokenisation
if (sourceData.token_offsets === undefined) {
sourceData.token_offsets = tokenise(sourceData.text);
}
};
// Set default values for a variety of collection attributes
var setCollectionDefaults = function(collectionData) {
// The following are empty lists if not set
$.each([
'entity_attribute_types',
'entity_types',
'event_attribute_types',
'event_types',
'relation_attribute_types',
'relation_types',
'unconfigured_types',
], function(attrNo, attr) {
if (collectionData[attr] === undefined) {
collectionData[attr] = [];
}
});
};
var Visualizer = function(dispatcher, svgId, webFontURLs) {
var $svgDiv = $('#' + svgId);
if (!$svgDiv.length) {
throw Error('Could not find container with id="' + svgId + '"');
}
var that = this;
// OPTIONS
var roundCoordinates = true; // try to have exact pixel offsets
var boxTextMargin = { x: 0, y: 1.5 }; // effect is inverse of "margin" for some reason
var highlightRounding = { x: 3, y:3 }; // rx, ry for highlight boxes
var spaceWidths = {
' ': 4,
'\u00a0': 4,
'\u200b': 0,
'\u3000': 8,
'\t': 12,
'\n': 4
};
var coloredCurlies = true; // color curlies by box BG
var arcSlant = 15; //10;
var minArcSlant = 8;
var arcHorizontalSpacing = 10; // min space boxes with connecting arc
var rowSpacing = -5; // for some funny reason approx. -10 gives "tight" packing.
var sentNumMargin = 31;
var smoothArcCurves = true; // whether to use curves (vs lines) in arcs
var smoothArcSteepness = 0.5; // steepness of smooth curves (control point)
var reverseArcControlx = 5; // control point distance for "UFO catchers"
// "shadow" effect settings (note, error, incompelete)
var rectShadowSize = 3;
var rectShadowRounding = 2.5;
var arcLabelShadowSize = 1;
var arcLabelShadowRounding = 5;
var shadowStroke = 2.5; // TODO XXX: this doesn't affect anything..?
// "marked" effect settings (edited, focus, match)
var markedSpanSize = 6;
var markedArcSize = 2;
var markedArcStroke = 7; // TODO XXX: this doesn't seem to do anything..?
var rowPadding = 2;
var nestingAdjustYStepSize = 2; // size of height adjust for nested/nesting spans
var nestingAdjustXStepSize = 1; // size of height adjust for nested/nesting spans
var highlightSequence = '#FF9632;#FFCC00;#FF9632'; // yellow - deep orange
//var highlightSequence = '#FFFC69;#FFCC00;#FFFC69'; // a bit toned town
var highlightSpanSequence = highlightSequence;
var highlightArcSequence = highlightSequence;
var highlightTextSequence = highlightSequence;
var highlightDuration = '2s';
// different sequence for "mere" matches (as opposed to "focus" and
// "edited" highlights)
var highlightMatchSequence = '#FFFF00'; // plain yellow
var fragmentConnectorDashArray = '1,3,3,3';
var fragmentConnectorColor = '#000000';
// END OPTIONS
var svg;
var $svg;
var data = null;
var sourceData = null;
var requestedData = null;
var coll, doc, args;
var relationTypesHash;
var isRenderRequested;
var isCollectionLoaded = false;
var entityAttributeTypes = null;
var eventAttributeTypes = null;
var spanTypes = null;
var highlightGroup;
var collapseArcs = false;
var rtlmode = false;
var collapseArcSpace = false;
// var commentPrioLevels = ['Unconfirmed', 'Incomplete', 'Warning', 'Error', 'AnnotatorNotes'];
// XXX Might need to be tweaked - inserted diff levels
var commentPrioLevels = [
'Unconfirmed', 'Incomplete', 'Warning', 'Error', 'AnnotatorNotes',
'AddedAnnotation', 'MissingAnnotation', 'ChangedAnnotation'];
this.arcDragOrigin = null; // TODO
var rowBBox = function(span) {
var box = $.extend({}, span.rectBox); // clone
var chunkTranslation = span.chunk.translation;
box.x += chunkTranslation.x;
box.y += chunkTranslation.y;
return box;
};
var commentPriority = function(commentClass) {
if (commentClass === undefined) return -1;
var len = commentPrioLevels.length;
for (var i = 0; i < len; i++) {
if (commentClass.indexOf(commentPrioLevels[i]) != -1) return i;
}
return 0;
};
var clearSVG = function() {
data = null;
sourceData = null;
svg.clear();
$svgDiv.hide();
};
var setMarked = function(markedType) {
$.each(args[markedType] || [], function(markedNo, marked) {
if (marked[0] == 'sent') {
data.markedSent[marked[1]] = true;
} else if (marked[0] == 'equiv') { // [equiv, Equiv, T1]
$.each(sourceData.equivs, function(equivNo, equiv) {
if (equiv[1] == marked[1]) {
var len = equiv.length;
for (var i = 2; i < len; i++) {
if (equiv[i] == marked[2]) {
// found it
len -= 3;
for (var i = 1; i <= len; i++) {
var arc = data.eventDescs[equiv[0] + "*" + i].equivArc;
arc.marked = markedType;
}
return; // next equiv
}
}
}
});
} else if (marked.length == 2) {
markedText.push([parseInt(marked[0], 10), parseInt(marked[1], 10), markedType]);
} else {
var span = data.spans[marked[0]];
if (span) {
if (marked.length == 3) { // arc
$.each(span.outgoing, function(arcNo, arc) {
if (arc.target == marked[2] && arc.type == marked[1]) {
arc.marked = markedType;
}
});
} else { // span
span.marked = markedType;
}
} else {
var eventDesc = data.eventDescs[marked[0]];
if (eventDesc) { // relation
var relArc = eventDesc.roles[0];
$.each(data.spans[eventDesc.triggerId].outgoing, function(arcNo, arc) {
if (arc.target == relArc.targetId && arc.type == relArc.type) {
arc.marked = markedType;
}
});
} else { // try for trigger
$.each(data.eventDescs, function(eventDescNo, eventDesc) {
if (eventDesc.triggerId == marked[0]) {
data.spans[eventDesc.id].marked = markedType;
}
});
}
}
}
});
};
var findArcHeight = function(fromIndex, toIndex, fragmentHeights) {
var height = 0;
for (var i = fromIndex; i <= toIndex; i++) {
if (fragmentHeights[i] > height) height = fragmentHeights[i];
}
height += Configuration.visual.arcSpacing;
return height;
}
var adjustFragmentHeights = function(fromIndex, toIndex, fragmentHeights, height) {
for (var i = fromIndex; i <= toIndex; i++) {
if (fragmentHeights[i] < height) fragmentHeights[i] = height;
}
}
var fragmentComparator = function(a, b) {
var tmp;
var aSpan = a.span;
var bSpan = b.span;
// spans with more fragments go first
tmp = aSpan.fragments.length - bSpan.fragments.length;
if (tmp) {
return tmp < 0 ? 1 : -1;
}
// longer arc distances go last
tmp = aSpan.avgDist - bSpan.avgDist;
if (tmp) {
return tmp < 0 ? -1 : 1;
}
// spans with more arcs go last
tmp = aSpan.numArcs - bSpan.numArcs;
if (tmp) {
return tmp < 0 ? -1 : 1;
}
// compare the span widths,
// put wider on bottom so they don't mess with arcs, or shorter
// on bottom if there are no arcs.
var ad = a.to - a.from;
var bd = b.to - b.from;
tmp = ad - bd;
if (aSpan.numArcs == 0 && bSpan.numArcs == 0) {
tmp = -tmp;
}
if (tmp) {
return tmp < 0 ? 1 : -1;
}
tmp = aSpan.refedIndexSum - bSpan.refedIndexSum;
if (tmp) {
return tmp < 0 ? -1 : 1;
}
// if no other criterion is found, sort by type to maintain
// consistency
// TODO: isn't there a cmp() in JS?
if (aSpan.type < bSpan.type) {
return -1;
} else if (aSpan.type > bSpan.type) {
return 1;
}
return 0;
};
var setData = function(_sourceData) {
if (!args) args = {};
sourceData = _sourceData;
dispatcher.post('newSourceData', [sourceData]);
data = new DocumentData(sourceData.text);
// collect annotation data
$.each(sourceData.entities, function(entityNo, entity) {
// offsets given as array of (start, end) pairs
var span =
// (id, type, offsets, generalType)
new Span(entity[0], entity[1], entity[2], 'entity');
span.splitMultilineOffsets(data.text);
data.spans[entity[0]] = span;
});
var triggerHash = {};
$.each(sourceData.triggers, function(triggerNo, trigger) {
// (id, type, offsets, generalType)
var triggerSpan = new Span(trigger[0], trigger[1], trigger[2], 'trigger');
triggerSpan.splitMultilineOffsets(data.text);
triggerHash[trigger[0]] = [triggerSpan, []]; // triggerSpan, eventlist
});
$.each(sourceData.events, function(eventNo, eventRow) {
var eventDesc = data.eventDescs[eventRow[0]] =
// (id, triggerId, roles, klass)
new EventDesc(eventRow[0], eventRow[1], eventRow[2]);
var trigger = triggerHash[eventDesc.triggerId];
var span = trigger[0].copy(eventDesc.id);
trigger[1].push(span);
data.spans[eventDesc.id] = span;
});
// XXX modifications: delete later
$.each(sourceData.modifications, function(modNo, mod) {
// mod: [id, spanId, modification]
if (!data.spans[mod[2]]) {
dispatcher.post('messages', [[['<strong>ERROR</strong><br/>Event ' + mod[2] + ' (referenced from modification ' + mod[0] + ') does not occur in document ' + data.document + '<br/>(please correct the source data)', 'error', 5]]]);
return;
}
data.spans[mod[2]][mod[1]] = true;
});
var midpointComparator = function(a, b) {
var tmp = a.from + a.to - b.from - b.to;
if (!tmp) return 0;
return tmp < 0 ? -1 : 1;
};
// split spans into span fragments (for discontinuous spans)
$.each(data.spans, function(spanNo, span) {
$.each(span.offsets, function(offsetsNo, offsets) {
var from = parseInt(offsets[0], 10);
var to = parseInt(offsets[1], 10);
var fragment = new Fragment(offsetsNo, span, from, to);
span.fragments.push(fragment);
});
// ensure ascending order
span.fragments.sort(midpointComparator);
span.wholeFrom = span.fragments[0].from;
span.wholeTo = span.fragments[span.fragments.length - 1].to;
span.headFragment = span.fragments[(true) ? span.fragments.length - 1 : 0]; // TODO configurable!
});
var spanComparator = function(a, b) {
var aSpan = data.spans[a];
var bSpan = data.spans[b];
var tmp = aSpan.headFragment.from + aSpan.headFragment.to - bSpan.headFragment.from - bSpan.headFragment.to;
if (tmp) {
return tmp < 0 ? -1 : 1;
}
return 0;
};
$.each(sourceData.equivs, function(equivNo, equiv) {
// equiv: ['*', 'Equiv', spanId...]
equiv[0] = "*" + equivNo;
var equivSpans = equiv.slice(2);
var okEquivSpans = [];
// collect the equiv spans in an array
$.each(equivSpans, function(equivSpanNo, equivSpan) {
if (data.spans[equivSpan]) okEquivSpans.push(equivSpan);
// TODO: #404, inform the user with a message?
});
// sort spans in the equiv by their midpoint
okEquivSpans.sort(spanComparator);
// generate the arcs
var len = okEquivSpans.length;
for (var i = 1; i < len; i++) {
var eventDesc = data.eventDescs[equiv[0] + '*' + i] =
// (id, triggerId, roles, klass)
new EventDesc(okEquivSpans[i - 1], okEquivSpans[i - 1], [[equiv[1], okEquivSpans[i]]], 'equiv');
eventDesc.leftSpans = okEquivSpans.slice(0, i);
eventDesc.rightSpans = okEquivSpans.slice(i);
}
});
$.each(sourceData.relations, function(relNo, rel) {
// rel[2] is args, rel[2][a][0] is role and rel[2][a][1] is value for a in (0,1)
var argsDesc = relationTypesHash[rel[1]];
argsDesc = argsDesc && argsDesc.args;
var t1, t2;
if (argsDesc) {
// sort the arguments according to the config
var args = {}
args[rel[2][0][0]] = rel[2][0][1];
args[rel[2][1][0]] = rel[2][1][1];
t1 = args[argsDesc[0].role];
t2 = args[argsDesc[1].role];
} else {
// (or leave as-is in its absence)
t1 = rel[2][0][1];
t2 = rel[2][1][1];
}
data.eventDescs[rel[0]] =
// (id, triggerId, roles, klass)
new EventDesc(t1, t1, [[rel[1], t2]], 'relation');
});
// attributes
$.each(sourceData.attributes, function(attrNo, attr) {
// attr: [id, name, spanId, value, cueSpanId
// TODO: might wish to check what's appropriate for the type
// instead of using the first attribute def found
var attrType = (eventAttributeTypes[attr[1]] ||
entityAttributeTypes[attr[1]]);
var attrValue = attrType && attrType.values.byName[attrType.bool || attr[3]];
var span = data.spans[attr[2]];
if (!span) {
dispatcher.post('messages', [[['Annotation ' + attr[2] + ', referenced from attribute ' + attr[0] + ', does not exist.', 'error']]]);
return;
}
var valText = (attrValue && attrValue.name) || attr[3];
var attrText = attrType
? (attrType.bool ? attrType.name : (attrType.name + ': ' + valText))
: (attr[3] == true ? attr[1] : attr[1] + ': ' + attr[3]);
span.attributeText.push(attrText);
span.attributes[attr[1]] = attr[3];
if (attr[4]) { // cue
span.attributeCues[attr[1]] = attr[4];
var cueSpan = data.spans[attr[4]];
cueSpan.attributeCueFor[data.spans[1]] = attr[2];
cueSpan.cue = 'CUE'; // special css type
}
$.extend(span.attributeMerge, attrValue);
});
// comments
$.each(sourceData.comments, function(commentNo, comment) {
// comment: [entityId, type, text]
// TODO error handling
// sentence id: ['sent', sentId]
if (comment[0] instanceof Array && comment[0][0] == 'sent') {
// sentence comment
var sent = comment[0][1];
var text = comment[2];
if (data.sentComment[sent]) {
text = data.sentComment[sent].text + '<br/>' + text;
}
data.sentComment[sent] = { type: comment[1], text: text };
} else {
var id = comment[0];
var trigger = triggerHash[id];
var eventDesc = data.eventDescs[id];
var commentEntities =
trigger
? trigger[1] // trigger: [span, ...]
: id in data.spans
? [data.spans[id]] // span: [span]
: id in data.eventDescs
? [data.eventDescs[id]] // arc: [eventDesc]
: [];
$.each(commentEntities, function(entityId, entity) {
// if duplicate comment for entity:
// overwrite type, concatenate comment with a newline
if (!entity.comment) {
entity.comment = { type: comment[1], text: comment[2] };
} else {
entity.comment.type = comment[1];
entity.comment.text += "\n" + comment[2];
}
// partially duplicate marking of annotator note comments
if (comment[1] == "AnnotatorNotes") {
entity.annotatorNotes = comment[2];
}
// prioritize type setting when multiple comments are present
if (commentPriority(comment[1]) > commentPriority(entity.shadowClass)) {
entity.shadowClass = comment[1];
}
});
}
});
// normalizations
$.each(sourceData.normalizations, function(normNo, norm) {
var id = norm[0];
var normType = norm[1];
var target = norm[2];
var refdb = norm[3];
var refid = norm[4];
var reftext = norm[5];
// grab entity / event the normalization applies to
var span = data.spans[target];
if (!span) {
dispatcher.post('messages', [[['Annotation ' + target + ', referenced from normalization ' + id + ', does not exist.', 'error']]]);
return;
}
// TODO: do we have any possible use for the normType?
span.normalizations.push([refdb, refid, reftext]);
// quick hack for span box visual style
span.normalized = 'Normalized';
});
// prepare span boundaries for token containment testing
var sortedFragments = [];
$.each(data.spans, function(spanNo, span) {
$.each(span.fragments, function(fragmentNo, fragment) {
sortedFragments.push(fragment);
});
});
// sort fragments by beginning, then by end
sortedFragments.sort(function(a, b) {
var x = a.from;
var y = b.from;
if (x == y) {
x = a.to;
y = b.to;
}
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
});
var currentFragmentId = 0;
var startFragmentId = 0;
var numFragments = sortedFragments.length;
var lastTo = 0;
var firstFrom = null;
var chunkNo = 0;
var space;
var chunk = null;
// token containment testing (chunk recognition)
$.each(sourceData.token_offsets, function() {
var from = this[0];
var to = this[1];
if (firstFrom === null) firstFrom = from;
// Replaced for speedup; TODO check correctness
// inSpan = false;
// $.each(data.spans, function(spanNo, span) {
// if (span.from < to && to < span.to) {
// // it does; no word break
// inSpan = true;
// return false;
// }
// });
// Is the token end inside a span?
if (startFragmentId && to > sortedFragments[startFragmentId - 1].to) {
while (startFragmentId < numFragments && to > sortedFragments[startFragmentId].from) {
startFragmentId++;
}
}
currentFragmentId = startFragmentId;
while (currentFragmentId < numFragments && to >= sortedFragments[currentFragmentId].to) {
currentFragmentId++;
}
// if yes, the next token is in the same chunk
if (currentFragmentId < numFragments && to > sortedFragments[currentFragmentId].from) {
return;
}
// otherwise, create the chunk found so far
space = data.text.substring(lastTo, firstFrom);
var text = data.text.substring(firstFrom, to);
if (chunk) chunk.nextSpace = space;
// (index, text, from, to, space) {
chunk = new Chunk(chunkNo++, text, firstFrom, to, space);
chunk.lastSpace = space;
data.chunks.push(chunk);
lastTo = to;
firstFrom = null;
});
var numChunks = chunkNo;
// find sentence boundaries in relation to chunks
chunkNo = 0;
var sentenceNo = 0;
var pastFirst = false;
$.each(sourceData.sentence_offsets, function() {
var from = this[0];
if (chunkNo >= numChunks) return false;
if (data.chunks[chunkNo].from > from) return;
var chunk;
while (chunkNo < numChunks && (chunk = data.chunks[chunkNo]).from < from) {
chunkNo++;
}
chunkNo++;
if (pastFirst && from <= chunk.from) {
var numNL = chunk.space.split("\n").length - 1;
if (!numNL) numNL = 1;
sentenceNo += numNL;
chunk.sentence = sentenceNo;
} else {
pastFirst = true;
}
});
// assign fragments to appropriate chunks
var currentChunkId = 0;
var chunk;
$.each(sortedFragments, function(fragmentId, fragment) {
while (fragment.to > (chunk = data.chunks[currentChunkId]).to) currentChunkId++;
chunk.fragments.push(fragment);
fragment.text = chunk.text.substring(fragment.from - chunk.from, fragment.to - chunk.from);
fragment.chunk = chunk;
});
// assign arcs to spans; calculate arc distances
$.each(data.eventDescs, function(eventNo, eventDesc) {
var dist = 0;
var origin = data.spans[eventDesc.id];
if (!origin) {
// TODO: include missing trigger ID in error message
dispatcher.post('messages', [[['<strong>ERROR</strong><br/>Trigger for event "' + eventDesc.id + '" not found in ' + data.document + '<br/>(please correct the source data)', 'error', 5]]]);
return;
}
var here = origin.headFragment.from + origin.headFragment.to;
$.each(eventDesc.roles, function(roleNo, role) {
var target = data.spans[role.targetId];
if (!target) {
dispatcher.post('messages', [[['<strong>ERROR</strong><br/>"' + role.targetId + '" (referenced from "' + eventDesc.id + '") not found in ' + data.document + '<br/>(please correct the source data)', 'error', 5]]]);
return;
}
var there = target.headFragment.from + target.headFragment.to;
var dist = Math.abs(here - there);
var arc = new Arc(eventDesc, role, dist, eventNo);
origin.totalDist += dist;
origin.numArcs++;
target.totalDist += dist;
target.numArcs++;
data.arcs.push(arc);
target.incoming.push(arc);
origin.outgoing.push(arc);
// ID dict for easy access. TODO: have a function defining the
// (origin,type,target)->id mapping (see also annotator_ui.js)
var arcId = origin.id + '--' + role.type + '--' + target.id;
data.arcById[arcId] = arc;
}); // roles
}); // eventDescs
// highlighting
markedText = [];
setMarked('edited'); // set by editing process
setMarked('focus'); // set by URL
setMarked('matchfocus'); // set by search process, focused match
setMarked('match'); // set by search process, other (non-focused) match
$.each(data.spans, function(spanId, span) {
// calculate average arc distances
// average distance of arcs (0 for no arcs)
span.avgDist = span.numArcs ? span.totalDist / span.numArcs : 0;
lastSpan = span;
// collect fragment texts into span texts
var fragmentTexts = [];
$.each(span.fragments, function(fragmentNo, fragment) {
// TODO heuristics
fragmentTexts.push(fragment.text);
});
span.text = fragmentTexts.join('');
}); // data.spans
for (var i = 0; i < 2; i++) {
// preliminary sort to assign heights for basic cases
// (first round) and cases resolved in the previous
// round(s).
$.each(data.chunks, function(chunkNo, chunk) {
// sort
chunk.fragments.sort(fragmentComparator);
// renumber
$.each(chunk.fragments, function(fragmentNo, fragment) {
fragment.indexNumber = fragmentNo;
});
});
// nix the sums, so we can sum again
$.each(data.spans, function(spanNo, span) {
span.refedIndexSum = 0;
});
// resolved cases will now have indexNumber set
// to indicate their relative order. Sum those for referencing cases
// for use in iterative resorting
$.each(data.arcs, function(arcNo, arc) {
data.spans[arc.origin].refedIndexSum += data.spans[arc.target].headFragment.indexNumber;
});
}
// Final sort of fragments in chunks for drawing purposes
// Also identify the marked text boundaries regarding chunks
$.each(data.chunks, function(chunkNo, chunk) {
// and make the next sort take this into account. Note that this will
// now resolve first-order dependencies between sort orders but not
// second-order or higher.
chunk.fragments.sort(fragmentComparator);
$.each(chunk.fragments, function(fragmentNo, fragment) {
fragment.drawOrder = fragmentNo;
});
});
data.spanDrawOrderPermutation = Object.keys(data.spans);
data.spanDrawOrderPermutation.sort(function(a, b) {
var spanA = data.spans[a];
var spanB = data.spans[b];
// We're jumping all over the chunks, but it's enough that
// we're doing everything inside each chunk in the right
// order. should it become necessary to actually do these in
// linear order, put in a similar condition for
// spanX.headFragment.chunk.index; but it should not be
// needed.
var tmp = spanA.headFragment.drawOrder - spanB.headFragment.drawOrder;
if (tmp) return tmp < 0 ? -1 : 1;
return 0;
});
// resort the spans for linear order by center
sortedFragments.sort(midpointComparator);
// sort fragments into towers, calculate average arc distances
var lastFragment = null;
var towerId = -1;
$.each(sortedFragments, function(i, fragment) {
if (!lastFragment || (lastFragment.from != fragment.from || lastFragment.to != fragment.to)) {
towerId++;
}
fragment.towerId = towerId;
lastFragment = fragment;
}); // sortedFragments
// find curlies (only the first fragment drawn in a tower)
$.each(data.spanDrawOrderPermutation, function(spanIdNo, spanId) {
var span = data.spans[spanId];
$.each(span.fragments, function(fragmentNo, fragment) {
if (!data.towers[fragment.towerId]) {
data.towers[fragment.towerId] = [];
fragment.drawCurly = true;
fragment.span.drawCurly = true;
}
data.towers[fragment.towerId].push(fragment);
});
});
var spanAnnTexts = {};
$.each(data.chunks, function(chunkNo, chunk) {
chunk.markedTextStart = [];
chunk.markedTextEnd = [];
$.each(chunk.fragments, function(fragmentNo, fragment) {
if (chunk.firstFragmentIndex == undefined) {
chunk.firstFragmentIndex = fragment.towerId;
}
chunk.lastFragmentIndex = fragment.towerId;
var spanLabels = Util.getSpanLabels(spanTypes, fragment.span.type);
fragment.labelText = Util.spanDisplayForm(spanTypes, fragment.span.type);
// Find the most appropriate label according to text width
if (Configuration.abbrevsOn && spanLabels) {
var labelIdx = 1; // first abbrev
var maxLength = (fragment.to - fragment.from) / 0.8;
while (fragment.labelText.length > maxLength &&
spanLabels[labelIdx]) {
fragment.labelText = spanLabels[labelIdx];
labelIdx++;
}
}
var svgtext = svg.createText(); // one "text" element per row
var postfixArray = [];
var prefix = '';
var postfix = '';
var warning = false;
$.each(fragment.span.attributes, function(attrType, valType) {
// TODO: might wish to check what's appropriate for the type
// instead of using the first attribute def found
var attr = (eventAttributeTypes[attrType] ||
entityAttributeTypes[attrType]);
if (!attr) {
// non-existent type
warning = true;
return;
}
var val = attr.values.byName[attr.bool || valType];
if (!val) {
// non-existent value
warning = true;
return;
}
if ($.isEmptyObject(val)) {
// defined, but lacks any visual presentation
warning = true;
return;
}
if (val.glyph) {
if (val.position == "left") {
prefix = val.glyph + prefix;
var tspan_attrs = { 'class' : 'glyph' };
if (val.glyphColor) {
tspan_attrs.fill = val.glyphColor;
}
svgtext.span(val.glyph, tspan_attrs);
} else { // XXX right is implied - maybe change
postfixArray.push([attr, val]);
postfix += val.glyph;
}
}
});
var text = fragment.labelText;
if (prefix !== '') {
text = prefix + ' ' + text;
svgtext.string(' ');
}
svgtext.string(fragment.labelText);
if (postfixArray.length) {
text += ' ' + postfix;
svgtext.string(' ');
$.each(postfixArray, function(elNo, el) {
var tspan_attrs = { 'class' : 'glyph' };
if (el[1].glyphColor) {
tspan_attrs.fill = el[1].glyphColor;
}
svgtext.span(el[1].glyph, tspan_attrs);
});
}
if (warning) {
svgtext.span("#", { 'class': 'glyph attribute_warning' });
text += ' #';
}
fragment.glyphedLabelText = text;
if (!spanAnnTexts[text]) {
spanAnnTexts[text] = true;
data.spanAnnTexts[text] = svgtext;
}
}); // chunk.fragments
}); // chunks
var numChunks = data.chunks.length;
// note the location of marked text with respect to chunks
var startChunk = 0;
var currentChunk;
// sort by "from"; we don't need to sort by "to" as well,
// because unlike spans, chunks are disjunct
markedText.sort(function(a, b) {
return Util.cmp(a[0], b[0]);
});
$.each(markedText, function(textNo, textPos) {
var from = textPos[0];
var to = textPos[1];
var markedType = textPos[2];
if (from < 0) from = 0;
if (to < 0) to = 0;
if (to >= data.text.length) to = data.text.length - 1;
if (from > to) from = to;
while (startChunk < numChunks) {
var chunk = data.chunks[startChunk];
if (from <= chunk.to) {
chunk.markedTextStart.push([textNo, true, from - chunk.from, null, markedType]);
break;
}
startChunk++;
}
if (startChunk == numChunks) {
dispatcher.post('messages', [[['Wrong text offset', 'error']]]);
return;
}
currentChunk = startChunk;
while (currentChunk < numChunks) {
var chunk = data.chunks[currentChunk];
if (to <= chunk.to) {
chunk.markedTextEnd.push([textNo, false, to - chunk.from]);
break
}
currentChunk++;
}
if (currentChunk == numChunks) {
dispatcher.post('messages', [[['Wrong text offset', 'error']]]);
var chunk = data.chunks[data.chunks.length - 1];
chunk.markedTextEnd.push([textNo, false, chunk.text.length]);
return;
}
}); // markedText
dispatcher.post('dataReady', [data]);
};
var resetData = function() {
setData(sourceData);
renderData();
}
var translate = function(element, x, y) {
$(element.group).attr('transform', 'translate(' + x + ', ' + y + ')');
element.translation = { x: x, y: y };
};
var showMtime = function() {
if (data.mtime) {
// we're getting seconds and need milliseconds
//$('#document_ctime').text("Created: " + Annotator.formatTime(1000 * data.ctime)).css("display", "inline");
$('#document_mtime').text("Last modified: " + Util.formatTimeAgo(1000 * data.mtime)).css("display", "inline");
} else {
//$('#document_ctime').css("display", "none");
$('#document_mtime').css("display", "none");
}
};
var addHeaderAndDefs = function() {
var commentName = (coll + '/' + doc).replace('--', '-\\-');
$svg.append('<!-- document: ' + commentName + ' -->');
var defs = svg.defs();
var $blurFilter = $('<filter id="Gaussian_Blur"><feGaussianBlur in="SourceGraphic" stdDeviation="2" /></filter>');
svg.add(defs, $blurFilter);
return defs;
}
var getTextMeasurements = function(textsHash, options, callback) {
// make some text elements, find out the dimensions
var textMeasureGroup = svg.group(options);
// changed from $.each because of #264 ('length' can appear)
for (var text in textsHash) {
if (textsHash.hasOwnProperty(text)) {
svg.text(textMeasureGroup, 0, 0, text);
}
}
// measuring goes on here
var widths = {};
$(textMeasureGroup).find('text').each(function(svgTextNo, svgText) {
var text = $(svgText).text();
widths[text] = this.getComputedTextLength();
if (callback) {
$.each(textsHash[text], function(text, object) {
callback(object, svgText);
});
}
});
var bbox = textMeasureGroup.getBBox();
svg.remove(textMeasureGroup);
return new Measurements(widths, bbox.height, bbox.y);
};
var getTextAndSpanTextMeasurements = function() {
// get the span text sizes
var chunkTexts = {}; // set of span texts
$.each(data.chunks, function(chunkNo, chunk) {
chunk.row = undefined; // reset
if (!chunkTexts.hasOwnProperty(chunk.text)) chunkTexts[chunk.text] = []
var chunkText = chunkTexts[chunk.text];
// here we also need all the spans that are contained in
// chunks with this text, because we need to know the position
// of the span text within the respective chunk text
chunkText.push.apply(chunkText, chunk.fragments);
// and also the markedText boundaries
chunkText.push.apply(chunkText, chunk.markedTextStart);
chunkText.push.apply(chunkText, chunk.markedTextEnd);
});
var textSizes = getTextMeasurements(
chunkTexts,
undefined,
function(fragment, text) {
if (fragment instanceof Fragment) { // it's a fragment!
// measure the fragment text position in pixels
var firstChar = fragment.from - fragment.chunk.from;
if (firstChar < 0) {
firstChar = 0;
dispatcher.post('messages', [[['<strong>WARNING</strong>' +
'<br/> ' +
'The fragment [' + fragment.from + ', ' + fragment.to + '] (' + fragment.text + ') is not ' +
'contained in its designated chunk [' +
fragment.chunk.from + ', ' + fragment.chunk.to + '] most likely ' +
'due to the fragment starting or ending with a space, please ' +
'verify the sanity of your data since we are unable to ' +
'visualise this fragment correctly and will drop leading ' +
'space characters'
, 'warning', 15]]]);
}
var lastChar = fragment.to - fragment.chunk.from - 1;
// Adjust for XML whitespace (#832, #1009)
var textUpToFirstChar = fragment.chunk.text.substring(0, firstChar);
var textUpToLastChar = fragment.chunk.text.substring(0, lastChar);
var textUpToFirstCharUnspaced = textUpToFirstChar.replace(/\s\s+/g, ' ');
var textUpToLastCharUnspaced = textUpToLastChar.replace(/\s\s+/g, ' ');
firstChar -= textUpToFirstChar.length - textUpToFirstCharUnspaced.length;
lastChar -= textUpToLastChar.length - textUpToLastCharUnspaced.length;
var startPos, endPos;
if (firstChar < fragment.chunk.text.length) {
startPos = text.getStartPositionOfChar(firstChar).x;
} else {
startPos = text.getComputedTextLength();
}
endPos = (lastChar < firstChar)
? startPos
: text.getEndPositionOfChar(lastChar).x;
// In RTL mode, positions are negative (left to right)
if (rtlmode) {
startPos = -startPos;
endPos = -endPos;
}
fragment.curly = {
from: Math.min(startPos, endPos),
to: Math.max(startPos, endPos)
};
} else { // it's markedText [id, start?, char#, offset]
if (fragment[2] < 0) fragment[2] = 0;
if (!fragment[2]) { // start
fragment[3] = text.getStartPositionOfChar(fragment[2]).x;
} else {
fragment[3] = text.getEndPositionOfChar(fragment[2] - 1).x + 1;
}
}
});
// get the fragment annotation text sizes
var fragmentTexts = {};
var noSpans = true;
$.each(data.spans, function(spanNo, span) {
$.each(span.fragments, function(fragmentNo, fragment) {
fragmentTexts[fragment.glyphedLabelText] = true;
noSpans = false;
});
});
if (noSpans) fragmentTexts.$ = true; // dummy so we can at least get the height
var fragmentSizes = getTextMeasurements(fragmentTexts, {'class': 'span'});
return {
texts: textSizes,
fragments: fragmentSizes
};
};
var addArcTextMeasurements = function(sizes) {
// get the arc annotation text sizes (for all labels)
var arcTexts = {};
$.each(data.arcs, function(arcNo, arc) {
var labels = Util.getArcLabels(spanTypes, data.spans[arc.origin].type, arc.type, relationTypesHash);
if (!labels.length) labels = [arc.type];
$.each(labels, function(labelNo, label) {
arcTexts[label] = true;
});
});
var arcSizes = getTextMeasurements(arcTexts, {'class': 'arcs'});
sizes.arcs = arcSizes;
};
var adjustTowerAnnotationSizes = function() {
// find biggest annotation in each tower
$.each(data.towers, function(towerNo, tower) {
var maxWidth = 0;
$.each(tower, function(fragmentNo, fragment) {
var width = data.sizes.fragments.widths[fragment.glyphedLabelText];
if (width > maxWidth) maxWidth = width;
}); // tower
$.each(tower, function(fragmentNo, fragment) {
fragment.width = maxWidth;
}); // tower
}); // data.towers
};
var makeArrow = function(defs, spec) {
var parsedSpec = spec.split(',');
var type = parsedSpec[0];
if (type == 'none') return;
var width = 5;
var height = 5;
var color = "black";
if ($.isNumeric(parsedSpec[1]) && parsedSpec[2]) {
if ($.