UNPKG

malgo-brat-frontend-editor

Version:
1,181 lines (1,091 loc) 179 kB
/* * ## brat ## * Copyright (C) 2010-2012 The brat contributors, all rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ // -*- 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 fontLoadTimeout = 5000; // 5 seconds // WEBANNO EXTENSION BEGIN - RTL support - DEV mode switch var rtlmode = false; // WEBANNO EXTENSION END 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; // WEBANNO EXTENSION BEGIN - #820 - Allow setting label/color individually // this.labelText = undefined; // this.color = undefined // WEBANNO EXTENSION END }; 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] = []; } }); // BEGIN WEBANNO EXTENSION - #1074 - Fix potential NPE // Avoid exception due to undefined text in tokenise and sentenceSplit if (sourceData.text === undefined) { sourceData.text = ""; } // END WEBANNO EXTENSION // 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. // BEGIN WEBANNO EXTENSION - #361 - Sentence numbers are cropped /* var sentNumMargin = 20; */ var sentNumMargin = 40; // END WEBANNO EXTENSION 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 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 // due to silly Chrome bug, I have to make it pay attention var forceRedraw = function() { // WEBANNO EXTENSION BEGIN - #1074 - $.browser is no longer supported in jQuery /* if (!$.browser.chrome) return; // not needed */ // WEBANNO EXTENSION END $svg.css('margin-bottom', 1); setTimeout(function() { $svg.css('margin-bottom', 0); }, 0); } 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'); // WEBANNO EXTENSION BEGIN - #820 - Allow setting label/color individually if (entity[3]) { span.labelText = entity[3]; } if (entity[4]) { span.color = entity[4]; } // WEBANNO EXTENSION END 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'); // WEBANNO EXTENSION BEGIN - #820 - Allow setting label/color individually if (rel[3]) { data.eventDescs[rel[0]].labelText = rel[3]; } if (rel[4]) { data.eventDescs[rel[0]].color = rel[4]; } // WEBANNO EXTENSION END }); // 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[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++; } } // WEBANNO EXTENSION BEGIN - #820 - Allow setting label/color individually if (fragment.span.labelText) { fragment.labelText = fragment.span.labelText; } // WEBANNO EXTENSION END 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[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 =