/* * Copyright 2012 * Ubiquitous Knowledge Processing (UKP) Lab and FG Language Technology * Technische Universität Darmstadt * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.tudarmstadt.ukp.clarin.webanno.ui.curation.component; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.getAddr; import static de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil.selectSentenceAt; import static org.apache.uima.fit.util.JCasUtil.selectFollowing; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.uima.UIMAException; import org.apache.uima.jcas.JCas; import org.apache.wicket.AttributeModifier; import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.behavior.AbstractAjaxBehavior; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.panel.FeedbackPanel; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.Model; import org.apache.wicket.spring.injection.annot.SpringBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.tudarmstadt.ukp.clarin.webanno.api.AnnotationSchemaService; import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.AnnotationEditorBase; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.exception.AnnotationException; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.model.AnnotatorState; import de.tudarmstadt.ukp.clarin.webanno.api.annotation.util.WebAnnoCasUtil; import de.tudarmstadt.ukp.clarin.webanno.brat.annotation.BratAnnotationEditor; import de.tudarmstadt.ukp.clarin.webanno.curation.storage.CurationDocumentService; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocumentState; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.detail.AnnotationDetailEditorPanel; import de.tudarmstadt.ukp.clarin.webanno.ui.curation.component.model.AnnotationSelection; import de.tudarmstadt.ukp.clarin.webanno.ui.curation.component.model.CurationContainer; import de.tudarmstadt.ukp.clarin.webanno.ui.curation.component.model.CurationUserSegmentForAnnotationDocument; import de.tudarmstadt.ukp.clarin.webanno.ui.curation.component.model.SourceListView; import de.tudarmstadt.ukp.clarin.webanno.ui.curation.component.model.SuggestionBuilder; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; /** * Main Panel for the curation page. It displays a box with the complete text on the left side and a * box for a selected sentence on the right side. */ public class CurationPanel extends Panel { private static final long serialVersionUID = -5128648754044819314L; private static final Logger LOG = LoggerFactory.getLogger(CurationPanel.class); private @SpringBean DocumentService documentService; private @SpringBean CurationDocumentService curationDocumentService; private @SpringBean AnnotationSchemaService annotationService; private @SpringBean UserDao userRepository; public SuggestionViewPanel suggestionViewPanel; private AnnotationEditorBase annotationEditor; public AnnotationDetailEditorPanel editor; private final WebMarkupContainer sentencesListView; private final WebMarkupContainer corssSentAnnoView; private AnnotatorState bModel; private int fSn = 0; private int lSn = 0; private boolean firstLoad = true; private boolean annotate = false; /** * Map for tracking curated spans. Key contains the address of the span, the value contains the * username from which the span has been selected */ private Map<String, Map<Integer, AnnotationSelection>> annotationSelectionByUsernameAndAddress = new HashMap<String, Map<Integer, AnnotationSelection>>(); public SourceListView curationView; ListView<SourceListView> sentenceList; ListView<String> crossSentAnnoList; List<SourceListView> sourceListModel; // CurationContainer curationContainer; public CurationPanel(String id, final IModel<CurationContainer> cCModel) { super(id, cCModel); setOutputMarkupId(true); WebMarkupContainer sidebarCell = new WebMarkupContainer("sidebarCell") { private static final long serialVersionUID = 1L; @Override protected void onComponentTag(ComponentTag aTag) { super.onComponentTag(aTag); aTag.put("width", bModel.getPreferences().getSidebarSize()+"%"); } }; add(sidebarCell); WebMarkupContainer annotationViewCell = new WebMarkupContainer("annotationViewCell") { private static final long serialVersionUID = 1L; @Override protected void onComponentTag(ComponentTag aTag) { super.onComponentTag(aTag); aTag.put("width", (100-bModel.getPreferences().getSidebarSize())+"%"); } }; add(annotationViewCell); // add container for list of sentences panel sentencesListView = new WebMarkupContainer("sentencesListView"); sentencesListView.setOutputMarkupId(true); add(sentencesListView); // add container for the list of sentences where annotations exists crossing multiple // sentences // outside of the current page corssSentAnnoView = new WebMarkupContainer("corssSentAnnoView"); corssSentAnnoView.setOutputMarkupId(true); annotationViewCell.add(corssSentAnnoView); bModel = getModelObject().getBratAnnotatorModel(); LinkedList<CurationUserSegmentForAnnotationDocument> sentences = new LinkedList<CurationUserSegmentForAnnotationDocument>(); CurationUserSegmentForAnnotationDocument curationUserSegmentForAnnotationDocument = new CurationUserSegmentForAnnotationDocument(); if (bModel != null) { curationUserSegmentForAnnotationDocument .setAnnotationSelectionByUsernameAndAddress(annotationSelectionByUsernameAndAddress); curationUserSegmentForAnnotationDocument.setBratAnnotatorModel(bModel); sentences.add(curationUserSegmentForAnnotationDocument); } // update source list model only first time. sourceListModel = sourceListModel == null ? getModelObject().getCurationViews() : sourceListModel; suggestionViewPanel = new SuggestionViewPanel("suggestionViewPanel", new Model<LinkedList<CurationUserSegmentForAnnotationDocument>>(sentences)) { private static final long serialVersionUID = 2583509126979792202L; CurationContainer curationContainer = cCModel.getObject(); @Override public void onChange(AjaxRequestTarget aTarget) { try { // update begin/end of the curationsegment based on bratAnnotatorModel changes // (like sentence change in auto-scroll mode,.... aTarget.addChildren(getPage(), FeedbackPanel.class); CurationPanel.this.updatePanel(aTarget, curationContainer); } catch (UIMAException e) { error(ExceptionUtils.getRootCause(e)); } catch (ClassNotFoundException e) { error(e.getMessage()); } catch (IOException e) { error(e.getMessage()); } catch (AnnotationException e) { error(e.getMessage()); } } }; suggestionViewPanel.setOutputMarkupId(true); annotationViewCell.add(suggestionViewPanel); editor = new AnnotationDetailEditorPanel( "annotationDetailEditorPanel", new Model<AnnotatorState>(bModel)) { private static final long serialVersionUID = 2857345299480098279L; @Override protected void onChange(AjaxRequestTarget aTarget) { aTarget.addChildren(getPage(), FeedbackPanel.class); annotate = true; try { updatePanel(aTarget, cCModel.getObject()); } catch (UIMAException e) { LOG.error("Error: " + e.getMessage(), e); error(ExceptionUtils.getRootCause(e)); } catch (Exception e) { LOG.error("Error: " + e.getMessage(), e); error(e.getMessage()); } } @Override protected void onAutoForward(AjaxRequestTarget aTarget) { try { annotationEditor.render(aTarget, getEditorCas()); } catch (Exception e) { LOG.info("Error reading CAS " + e.getMessage(), e); error("Error reading CAS " + e.getMessage()); return; } } @Override protected void onConfigure() { super.onConfigure(); setEnabled(bModel.getDocument()!=null && !documentService .getSourceDocument(bModel.getDocument().getProject(), bModel.getDocument().getName()).getState() .equals(SourceDocumentState.CURATION_FINISHED)); } }; sidebarCell.add(editor); annotationEditor = new BratAnnotationEditor("mergeView", new Model<AnnotatorState>(bModel), editor, () -> { return getEditorCas(); }); // reset sentenceAddress and lastSentenceAddress to the orginal once annotationViewCell.add(annotationEditor); LoadableDetachableModel sentenceDiffModel = new LoadableDetachableModel() { @Override protected Object load() { int fSN = bModel.getFirstVisibleSentenceNumber(); int lSN = bModel.getLastVisibleSentenceNumber(); List<String> crossSentAnnos = new ArrayList<>(); if (SuggestionBuilder.crossSentenceLists != null) { for (int sn : SuggestionBuilder.crossSentenceLists.keySet()) { if (sn >= fSN && sn <= lSN) { List<Integer> cr = new ArrayList<>(); for (int c : SuggestionBuilder.crossSentenceLists.get(sn)) { if (c < fSN || c > lSN) { cr.add(c); } } if (!cr.isEmpty()) { crossSentAnnos.add(sn + "-->" + cr); } } } } return crossSentAnnos; } }; crossSentAnnoList = new ListView<String>("crossSentAnnoList", sentenceDiffModel) { private static final long serialVersionUID = 8539162089561432091L; @Override protected void populateItem(ListItem<String> item) { String crossSentAnno = item.getModelObject(); // ajax call when clicking on a sentence on the left side final AbstractDefaultAjaxBehavior click = new AbstractDefaultAjaxBehavior() { private static final long serialVersionUID = 5803814168152098822L; @Override protected void respond(AjaxRequestTarget aTarget) { // Expand curation view } }; // add subcomponents to the component item.add(click); Label crossSentAnnoItem = new AjaxLabel("crossAnnoSent", crossSentAnno, click); item.add(crossSentAnnoItem); } }; crossSentAnnoList.setOutputMarkupId(true); corssSentAnnoView.add(crossSentAnnoList); LoadableDetachableModel sentencesListModel = new LoadableDetachableModel() { @Override protected Object load() { return getModelObject().getCurationViews(); } }; sentenceList = new ListView<SourceListView>("sentencesList", sentencesListModel) { private static final long serialVersionUID = 8539162089561432091L; @Override protected void populateItem(ListItem<SourceListView> item) { final SourceListView curationViewItem = item.getModelObject(); // ajax call when clicking on a sentence on the left side final AbstractDefaultAjaxBehavior click = new AbstractDefaultAjaxBehavior() { private static final long serialVersionUID = 5803814168152098822L; @Override protected void respond(AjaxRequestTarget aTarget) { curationView = curationViewItem; fSn = 0; try { JCas jCas = curationDocumentService .readCurationCas(bModel.getDocument()); updateCurationView(cCModel.getObject(), curationViewItem, aTarget, jCas); updatePanel(aTarget, cCModel.getObject()); bModel.setFocusSentenceNumber(curationViewItem.getSentenceNumber()); } catch (UIMAException e) { error(ExceptionUtils.getRootCause(e)); } catch (ClassNotFoundException e) { error(e.getMessage()); } catch (IOException e) { error(e.getMessage()); } catch (AnnotationException e) { error(e.getMessage()); } } }; // add subcomponents to the component item.add(click); // Is in focus? if (curationViewItem.getSentenceNumber() == bModel.getFocusSentenceNumber()) { item.add(AttributeModifier.append("class", "current")); } // Agree or disagree? String cC = curationViewItem.getSentenceState().getValue(); if (cC != null) { item.add(AttributeModifier.append("class", "disagree")); } else { item.add(AttributeModifier.append("class", "agree")); } // In range or not? if (curationViewItem.getSentenceNumber() >= fSn && curationViewItem.getSentenceNumber() <= lSn) { item.add(AttributeModifier.append("class", "in-range")); } else { item.add(AttributeModifier.append("class", "out-range")); } Label sentenceNumber = new AjaxLabel("sentenceNumber", curationViewItem .getSentenceNumber().toString(), click); item.add(sentenceNumber); } }; // add subcomponents to the component sentenceList.setOutputMarkupId(true); sentencesListView.add(sentenceList); } public void setModel(IModel<CurationContainer> aModel) { setDefaultModel(aModel); } public void setModelObject(CurationContainer aModel) { setDefaultModelObject(aModel); } @SuppressWarnings("unchecked") public IModel<CurationContainer> getModel() { return (IModel<CurationContainer>) getDefaultModel(); } public CurationContainer getModelObject() { return (CurationContainer) getDefaultModelObject(); } private void updateCurationView(final CurationContainer curationContainer, final SourceListView curationViewItem, AjaxRequestTarget aTarget, JCas jCas) { Sentence currentSent = WebAnnoCasUtil.getCurrentSentence(jCas, curationViewItem.getBegin(), curationViewItem.getEnd()); bModel.setFirstVisibleSentence(WebAnnoCasUtil.findWindowStartCenteringOnSelection(jCas, currentSent, curationViewItem.getBegin(), bModel.getProject(), bModel.getDocument(), bModel.getPreferences().getWindowSize())); curationContainer.setBratAnnotatorModel(bModel); onChange(aTarget); } protected void onChange(AjaxRequestTarget aTarget) { } protected JCas getEditorCas() throws IOException { if (bModel.getDocument() == null) { throw new IllegalStateException("Please open a document first!"); } return curationDocumentService.readCurationCas(bModel.getDocument()); } @Override public void renderHead(IHeaderResponse response) { super.renderHead(response); if (firstLoad) { firstLoad = false; } } public void updatePanel(AjaxRequestTarget aTarget, CurationContainer aCC) throws UIMAException, ClassNotFoundException, IOException, AnnotationException { JCas jCas = curationDocumentService.readCurationCas(bModel.getDocument()); final Sentence sentence = selectSentenceAt(jCas, bModel.getFirstVisibleSentenceBegin(), bModel.getFirstVisibleSentenceEnd()); bModel.setFirstVisibleSentence(sentence); List<Sentence> followingSentences = selectFollowing(jCas, Sentence.class, sentence, bModel .getPreferences().getWindowSize()); // Check also, when getting the last sentence address in the display window, if this is the // last sentence or the ONLY sentence in the document Sentence lastSentenceAddressInDisplayWindow = followingSentences.size() == 0 ? sentence : followingSentences.get(followingSentences.size() - 1); if (curationView == null) { curationView = new SourceListView(); } curationView.setCurationBegin(sentence.getBegin()); curationView.setCurationEnd(lastSentenceAddressInDisplayWindow.getEnd()); int ws = bModel.getPreferences().getWindowSize(); Sentence fs = WebAnnoCasUtil.selectSentenceAt(jCas, bModel.getFirstVisibleSentenceBegin(), bModel.getFirstVisibleSentenceEnd()); Sentence ls = WebAnnoCasUtil.getLastSentenceInDisplayWindow(jCas, getAddr(fs), ws); fSn = WebAnnoCasUtil.getSentenceNumber(jCas, fs.getBegin()); lSn = WebAnnoCasUtil.getSentenceNumber(jCas, ls.getBegin()); sentencesListView.addOrReplace(sentenceList); aTarget.add(sentencesListView); /* * corssSentAnnoView.addOrReplace(crossSentAnnoList); aTarget.add(corssSentAnnoView); */ aTarget.add(suggestionViewPanel); if (annotate) { annotationEditor.render(aTarget, editor.getEditorCas()); annotationEditor.setHighlight(aTarget, bModel.getSelection().getAnnotation()); } else { annotationEditor.renderLater(aTarget); } annotate = false; suggestionViewPanel.updatePanel(aTarget, aCC, annotationEditor, annotationSelectionByUsernameAndAddress, curationView); } // CurationContainer curationContainer; /** * Class for combining an on click ajax call and a label */ class AjaxLabel extends Label { private static final long serialVersionUID = -4528869530409522295L; private AbstractAjaxBehavior click; public AjaxLabel(String id, String label, AbstractAjaxBehavior click) { super(id, label); this.click = click; } @Override public void onComponentTag(ComponentTag tag) { // add onclick handler to the browser // if clicked in the browser, the function // click.response(AjaxRequestTarget target) is called on the server side tag.put("ondblclick", "Wicket.Ajax.get({'u':'" + click.getCallbackUrl() + "'})"); tag.put("onclick", "Wicket.Ajax.get({'u':'" + click.getCallbackUrl() + "'})"); } } }