diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRenderer.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRenderer.java index 991bb0f9655..af3aa2ca1b0 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRenderer.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRenderer.java @@ -20,6 +20,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Comparator.comparingInt; +import static org.apache.commons.lang3.StringUtils.abbreviate; import static org.apache.uima.fit.util.CasUtil.selectCovered; import java.util.ArrayList; @@ -39,6 +40,7 @@ import org.apache.uima.cas.Type; import org.apache.uima.cas.TypeSystem; import org.apache.uima.cas.text.AnnotationFS; +import org.apache.uima.jcas.tcas.Annotation; import org.apache.wicket.Page; import org.apache.wicket.core.request.handler.IPageRequestHandler; import org.apache.wicket.request.cycle.PageRequestHandlerTracker; @@ -157,27 +159,24 @@ public void render(final CAS aCas, List aFeatures, VDocument } } - private Optional renderYield(AnnotationFS fs, Map> relationLinks, - List yieldDeps) + private Optional renderYield(AnnotationFS fs) { FeatureStructure dependentFs = getDependentFs(fs); - if (relationLinks.keySet().contains(ICasUtil.getAddr(dependentFs)) - && !yieldDeps.contains(ICasUtil.getAddr(dependentFs))) { - yieldDeps.add(ICasUtil.getAddr(dependentFs)); + var relationLinks = getRelationLinks(fs.getCAS()); - // sort the annotations (begin, end) - List sortedDepFs = new ArrayList<>( - relationLinks.get(ICasUtil.getAddr(dependentFs))); - sortedDepFs.sort(comparingInt( - arg0 -> ICasUtil.selectAnnotationByAddr(fs.getCAS(), arg0).getBegin())); + if (!relationLinks.keySet().contains(ICasUtil.getAddr(dependentFs))) { + return Optional.empty(); + } - String cm = getYieldMessage(fs.getCAS(), sortedDepFs); + // sort the annotations (begin, end) + var sortedDepFs = new ArrayList<>(relationLinks.get(ICasUtil.getAddr(dependentFs))); + sortedDepFs.sort(comparingInt( + arg0 -> ICasUtil.selectAnnotationByAddr(fs.getCAS(), arg0).getBegin())); - return Optional.of(cm); - } + var cm = getYieldMessage(fs.getCAS(), sortedDepFs); - return Optional.empty(); + return Optional.of(cm); } @Override @@ -188,9 +187,9 @@ public List render(VDocument aVDocument, AnnotationFS aFS, return Collections.emptyList(); } - RelationAdapter typeAdapter = getTypeAdapter(); - FeatureStructure dependentFs = getDependentFs(aFS); - FeatureStructure governorFs = getGovernorFs(aFS); + var typeAdapter = getTypeAdapter(); + var dependentFs = getDependentFs(aFS); + var governorFs = getGovernorFs(aFS); if (dependentFs == null || governorFs == null) { StringBuilder message = new StringBuilder(); @@ -259,23 +258,29 @@ public List lookupLazyDetails(CAS aCas, VID aVid, int aWindowB return Collections.emptyList(); } - // FIXME Should also handle relations that are only partially visible using - // selectAnnotationsInWindow() - var relationLinks = getRelationLinks(aCas, aWindowBeginOffset, aWindowEndOffset); - - // if this is a governor for more than one dependent, avoid duplicate yield - var yieldDeps = new ArrayList(); - var fs = ICasUtil.selectByAddr(aCas, AnnotationFS.class, aVid.getId()); - var yield = renderYield(fs, relationLinks, yieldDeps); + var group = new VLazyDetailGroup(); - var details = super.lookupLazyDetails(aCas, aVid, aWindowBeginOffset, aWindowEndOffset); + var dependentFs = getDependentFs(fs); + if (dependentFs instanceof AnnotationFS) { + group.addDetail(new VLazyDetail("Target", + abbreviate(((AnnotationFS) dependentFs).getCoveredText(), 300))); + } - if (yield.isPresent()) { - details.add(new VLazyDetailGroup(new VLazyDetail("Yield", yield.get()))); + var governorFs = getGovernorFs(fs); + if (governorFs instanceof AnnotationFS) { + group.addDetail(new VLazyDetail("Origin", + abbreviate(((AnnotationFS) governorFs).getCoveredText(), 300))); } + renderYield(fs).ifPresent( + yield -> group.addDetail(new VLazyDetail("Yield", abbreviate(yield, "...", 300)))); + + var details = super.lookupLazyDetails(aCas, aVid, aWindowBeginOffset, aWindowEndOffset); + if (!group.getDetails().isEmpty()) { + details.add(0, group); + } return details; } @@ -314,46 +319,48 @@ else if (end + 1 != ICasUtil.selectAnnotationByAddr(aCas, depFs).getBegin()) { /** * Get relation links to display in relation yield */ - private Map> getRelationLinks(CAS aCas, int aWindowBegin, int aWindowEnd) + private Map> getRelationLinks(CAS aCas) { - RelationAdapter typeAdapter = getTypeAdapter(); - Map> relations = new ConcurrentHashMap<>(); + var typeAdapter = getTypeAdapter(); + var relations = new ConcurrentHashMap>(); - for (AnnotationFS fs : selectCovered(aCas, type, aWindowBegin, aWindowEnd)) { - FeatureStructure dependentFs = getGovernorFs(fs); - FeatureStructure governorFs = getDependentFs(fs); + for (var fs : aCas. select(type)) { + var govFs = getGovernorFs(fs); + var depFs = getDependentFs(fs); - if (dependentFs == null || governorFs == null) { + if (govFs == null || depFs == null) { log.warn("Relation [" + typeAdapter.getLayer().getName() + "] with id [" - + ICasUtil.getAddr(fs) + "] has loose ends - cannot render."); + + VID.of(fs) + "] has loose ends - cannot render."); continue; } - Set links = relations.get(ICasUtil.getAddr(governorFs)); + var links = relations.get(ICasUtil.getAddr(depFs)); if (links == null) { links = new ConcurrentSkipListSet<>(); } - links.add(ICasUtil.getAddr(dependentFs)); - relations.put(ICasUtil.getAddr(governorFs), links); + links.add(ICasUtil.getAddr(govFs)); + relations.put(ICasUtil.getAddr(depFs), links); } // Update other subsequent links for (int i = 0; i < relations.keySet().size(); i++) { - for (Integer fs : relations.keySet()) { + for (var fs : relations.keySet()) { updateLinks(relations, fs); } } + // to start displaying the text from the governor, include it - for (Integer fs : relations.keySet()) { + for (var fs : relations.keySet()) { relations.get(fs).add(fs); } + return relations; } private void updateLinks(Map> aRelLinks, Integer aGov) { - for (Integer dep : aRelLinks.get(aGov)) { + for (var dep : aRelLinks.get(aGov)) { if (aRelLinks.containsKey(dep) && !aRelLinks.get(aGov).containsAll(aRelLinks.get(dep))) { aRelLinks.get(aGov).addAll(aRelLinks.get(dep)); diff --git a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRendererTest.java b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRendererTest.java index cefb415ec3d..133d4685199 100644 --- a/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRendererTest.java +++ b/inception/inception-api-annotation/src/test/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationRendererTest.java @@ -27,7 +27,6 @@ import static de.tudarmstadt.ukp.clarin.webanno.support.WebAnnoConst.RELATION_TYPE; import static de.tudarmstadt.ukp.clarin.webanno.support.WebAnnoConst.SPAN_TYPE; import static de.tudarmstadt.ukp.inception.rendering.vmodel.VCommentType.ERROR; -import static de.tudarmstadt.ukp.inception.rendering.vmodel.VCommentType.YIELD; import static java.util.Arrays.asList; import static org.apache.uima.fit.util.JCasUtil.select; import static org.assertj.core.api.Assertions.assertThat; @@ -201,8 +200,7 @@ public void thatRelationOverlapBehaviorOnRenderGeneratesErrors() throws Exceptio VDocument vdoc = new VDocument(); sut.render(jcas.getCas(), asList(), vdoc, 0, jcas.getDocumentText().length()); - assertThat(vdoc.comments()).filteredOn(c -> !YIELD.equals(c.getCommentType())) - .isEmpty(); + assertThat(vdoc.comments()).filteredOn(c -> ERROR.equals(c.getCommentType())).isEmpty(); } { @@ -210,8 +208,7 @@ public void thatRelationOverlapBehaviorOnRenderGeneratesErrors() throws Exceptio VDocument vdoc = new VDocument(); sut.render(jcas.getCas(), asList(), vdoc, 0, jcas.getDocumentText().length()); - assertThat(vdoc.comments()).filteredOn(c -> !YIELD.equals(c.getCommentType())) - .isEmpty(); + assertThat(vdoc.comments()).filteredOn(c -> ERROR.equals(c.getCommentType())).isEmpty(); } @@ -221,7 +218,7 @@ public void thatRelationOverlapBehaviorOnRenderGeneratesErrors() throws Exceptio sut.render(jcas.getCas(), asList(), vdoc, 0, jcas.getDocumentText().length()); assertThat(vdoc.comments()) // - .filteredOn(c -> !YIELD.equals(c.getCommentType())) + .filteredOn(c -> ERROR.equals(c.getCommentType())) .usingRecursiveFieldByFieldElementComparator().contains( // new VComment(dep1, ERROR, "Stacking is not permitted."), new VComment(dep2, ERROR, "Stacking is not permitted.")); @@ -233,7 +230,7 @@ public void thatRelationOverlapBehaviorOnRenderGeneratesErrors() throws Exceptio sut.render(jcas.getCas(), asList(), vdoc, 0, jcas.getDocumentText().length()); assertThat(vdoc.comments()) // - .filteredOn(c -> !YIELD.equals(c.getCommentType())) + .filteredOn(c -> ERROR.equals(c.getCommentType())) .usingRecursiveFieldByFieldElementComparator().contains( // new VComment(dep1, ERROR, "Stacking is not permitted."), new VComment(dep2, ERROR, "Stacking is not permitted.")); @@ -251,7 +248,7 @@ public void thatRelationOverlapBehaviorOnRenderGeneratesErrors() throws Exceptio sut.render(jcas.getCas(), asList(), vdoc, 0, jcas.getDocumentText().length()); assertThat(vdoc.comments()) // - .filteredOn(c -> !YIELD.equals(c.getCommentType())) + .filteredOn(c -> ERROR.equals(c.getCommentType())) .usingRecursiveFieldByFieldElementComparator().contains( // new VComment(dep1, ERROR, "Overlap is not permitted."), new VComment(dep3, ERROR, "Overlap is not permitted.")); diff --git a/inception/inception-api-render/src/main/java/de/tudarmstadt/ukp/inception/rendering/vmodel/VCommentType.java b/inception/inception-api-render/src/main/java/de/tudarmstadt/ukp/inception/rendering/vmodel/VCommentType.java index 9214f9bbc90..3a88f3aa989 100644 --- a/inception/inception-api-render/src/main/java/de/tudarmstadt/ukp/inception/rendering/vmodel/VCommentType.java +++ b/inception/inception-api-render/src/main/java/de/tudarmstadt/ukp/inception/rendering/vmodel/VCommentType.java @@ -19,5 +19,5 @@ public enum VCommentType { - INFO, ERROR, YIELD + INFO, ERROR } diff --git a/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImpl.java b/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImpl.java index 5fcc44c337e..3b0ba0aa434 100644 --- a/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImpl.java +++ b/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImpl.java @@ -193,9 +193,6 @@ private void renderComments(GetDocumentResponse aResponse, VDocument aVDoc, case INFO: type = AnnotationComment.ANNOTATOR_NOTES; break; - case YIELD: - type = "Yield"; - break; default: type = AnnotationComment.ANNOTATOR_NOTES; break; diff --git a/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/model/AnnotationComment.java b/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/model/AnnotationComment.java index 56d7fb215fb..9398def687a 100644 --- a/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/model/AnnotationComment.java +++ b/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/model/AnnotationComment.java @@ -23,9 +23,6 @@ import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; import de.tudarmstadt.ukp.inception.support.json.BeanAsArraySerializer; -/** - * Use this "comments" to highlight "yield" of relation nodes - */ @JsonSerialize(using = BeanAsArraySerializer.class) @JsonPropertyOrder(value = { "vid", "commentType", "comment" }) public class AnnotationComment diff --git a/inception/inception-brat-editor/src/main/ts/src/protocol/Protocol.ts b/inception/inception-brat-editor/src/main/ts/src/protocol/Protocol.ts index 178a5b18349..2ecc8b7a965 100644 --- a/inception/inception-brat-editor/src/main/ts/src/protocol/Protocol.ts +++ b/inception/inception-brat-editor/src/main/ts/src/protocol/Protocol.ts @@ -40,9 +40,6 @@ export type EntityAttributesDto = { cl: ClippedState; } -/** - * Use this "comments" to highlight "yield" of relation nodes - */ export type AnnotationCommentDto = [ id: VID, commentType: CommentType, diff --git a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/model/compactv2/CompactSerializerV2Impl.java b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/model/compactv2/CompactSerializerV2Impl.java index 4d1bd13009d..c560ba3553e 100644 --- a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/model/compactv2/CompactSerializerV2Impl.java +++ b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/model/compactv2/CompactSerializerV2Impl.java @@ -195,9 +195,6 @@ private void renderComments(VDocument aVDoc, HashMap vid case INFO: code = CompactComment.INFO; break; - case YIELD: - code = CompactComment.INFO; - break; default: throw new IllegalStateException( "Unsupported comment type [" + comment.getCommentType() + "]"); diff --git a/inception/inception-js-api/src/main/ts/src/diam/DiamAjax.ts b/inception/inception-js-api/src/main/ts/src/diam/DiamAjax.ts index 46cc916c91a..6cc5fa5197b 100644 --- a/inception/inception-js-api/src/main/ts/src/diam/DiamAjax.ts +++ b/inception/inception-js-api/src/main/ts/src/diam/DiamAjax.ts @@ -15,7 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { LazyDetail } from '@inception-project/inception-js-api/src/model/LazyDetail' import { Annotation, LazyDetailGroup, Offsets, VID } from '../model' export type DiamLoadAnnotationsOptions = { @@ -30,6 +29,14 @@ export type DiamSelectAnnotationOptions = { } export interface DiamAjax { + /** + * Select the given annotation. + * + * This will generally trigger a re-rendering of the annotation detail sidebar by the server. + * + * @param id the annotation ID + * @param options options like whether to scroll the annotation into view + */ selectAnnotation(id: VID, options?: DiamSelectAnnotationOptions): void; /** @@ -39,47 +46,98 @@ export interface DiamAjax { scrollTo(args: { id?: VID, offset?: Offsets }): void; /** - * Delete the annotation with the given VID. + * Delete the given annotation. + * + * This will generally trigger a re-rendering of the document triggered by the server. * - * @param id the VID of the annotation to delete. + * @param id the annotation ID */ deleteAnnotation(id: VID): void; /** - * Create a new span annotation at the given location. + * Create a new span annotation at the given loction. * - * @param offsets the offsets of the annotation. + * This will generally trigger a re-rendering of the document triggered by the server. * - * NOTE: Currently only a single element is supported in the offsets array. + * @param offsets the position of the new annotation. Note that currently only a single offset is + * supported. + * @param spanText the text of the new annotation. This is deprecated and will be removed in the + * future. */ createSpanAnnotation(offsets: Array, spanText?: string): void; /** - * Move a new span annotation to a new location. + * Move the given span annotation to a new location. * - * @param offsets the offsets of the annotation. + * This will generally trigger a re-rendering of the document triggered by the server. * - * NOTE: Currently only a single element is supported in the offsets array. + * @param id the annotation ID + * @param offsets the position of the new annotation. Note that currently only a single offset is supported. */ moveSpanAnnotation(id: VID, offsets: Array): void; + /** + * Create a new relation annotation between the two given spans. + * + * @param originSpanId the ID of the origin span + * @param targetSpanId the ID of the target span + */ createRelationAnnotation(originSpanId: VID, targetSpanId: VID): void; + /** + * Load annotations from the server. In the options, you can specify the format of the annotations. + * The controls the kind of data that is provided to the promise. + * + * This method can be used by the editor e.g. to load an entire document or only the currently + * visible annotations when scrolling through a document. + * + * @param options options controlling e.g. the format of the annotations + * @returns a promise that resolves to the loaded annotations in the specified format + */ loadAnnotations(options?: DiamLoadAnnotationsOptions): Promise; /** - * Loads the lazy details for the given annotation + * Load the lazy details of the given annotation. * * @param ann either the VID or the annotation itself * @param layerId the layer ID of the annnotation if the annotation is specified as a VID */ loadLazyDetails(ann: VID | Annotation, layerId?: number): Promise; - loadPreferences (key: string): Promise; + /** + * Load the preferences stored under the given key. This must be a key assigned to the editor + * by the server during initialization. + * + * @param key the key of the preferences + * @see {@link ../editor/AnnotationEditorProperties.ts} + * @returns a promise that resolves to a JSON-fiable object with the preferences + */ + loadPreferences(key: string): Promise; - savePreferences (key: string, data: Record): Promise; + /** + * Store preferences under the given key. This must be a key assigned to the editor by the + * server during initialization. + * + * @param key the key of the preferences + * @param data a JSON-fiable object with the preferences + */ + savePreferences(key: string, data: Record): Promise; + /** + * Trigger an extension action with the given ID. This is typically bound to left-double-click + * events on an annotation. + * + * @param id the ID of the extension action + */ triggerExtensionAction(id: VID): void; + /** + * Open the context menu for the given annotation. The implementation of this context menu is + * on the server side. + * + * @param id the ID of the annotation + * @param evt the mouse event that triggered the context menu. The mouse position from the event + * is used by the server to determine where to display the context menu. + */ openContextMenu(id: VID, evt: MouseEvent): void; }