/* * Copyright 2015 * 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.monitoring.page; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.wicket.ajax.AjaxEventBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.behavior.Behavior; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.panel.Fragment; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.markup.repeater.RefreshingView; import org.apache.wicket.model.AbstractReadOnlyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.util.resource.AbstractResourceStream; import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.resource.ResourceStreamNotFoundException; import de.tudarmstadt.ukp.clarin.webanno.curation.agreement.AgreementUtils; import de.tudarmstadt.ukp.clarin.webanno.curation.agreement.PairwiseAnnotationResult; import de.tudarmstadt.ukp.clarin.webanno.curation.agreement.AgreementUtils.AgreementResult; import de.tudarmstadt.ukp.clarin.webanno.support.AJAXDownload; import de.tudarmstadt.ukp.clarin.webanno.support.DefaultRefreshingView; import de.tudarmstadt.ukp.clarin.webanno.support.DescriptionTooltipBehavior; import de.tudarmstadt.ukp.clarin.webanno.ui.monitoring.page.MonitoringPage.AgreementFormModel; public class AgreementTable extends Panel { private final static Logger LOG = LoggerFactory.getLogger(AgreementTable.class); private static final long serialVersionUID = 571396822546125376L; private RefreshingView<String> rows; private IModel<AgreementFormModel> settings; public AgreementTable(String aId) { super(aId); } public AgreementTable(String aId, IModel<AgreementFormModel> aSettings, IModel<PairwiseAnnotationResult> aModel) { super(aId, aModel); settings = aSettings; setOutputMarkupId(true); // This model makes sure we add a "null" dummy rater which accounts for the header columns // of the table. final IModel<List<String>> ratersAdapter = new AbstractReadOnlyModel<List<String>>() { private static final long serialVersionUID = 1L; @Override public List<String> getObject() { List<String> raters = new ArrayList<>(); if (getModelObject() != null) { raters.add(null); raters.addAll(getModelObject().getRaters()); } return raters; } }; rows = new DefaultRefreshingView<String>("rows", ratersAdapter) { private static final long serialVersionUID = 1L; @Override protected void populateItem(final Item<String> aRowItem) { // Render regular row aRowItem.add(new DefaultRefreshingView<String>("cells", ratersAdapter) { private static final long serialVersionUID = 1L; @Override protected void populateItem(Item<String> aCellItem) { aCellItem.setRenderBodyOnly(true); Fragment cell; // Content cell if (aRowItem.getIndex() == 0 || aCellItem.getIndex() == 0) { cell = new Fragment("cell", "th", AgreementTable.this); } // Header cell else { cell = new Fragment("cell", "td", AgreementTable.this); } // Top-left cell if (aRowItem.getIndex() == 0 && aCellItem.getIndex() == 0) { cell.add(new Label("label", Model.of(""))); } // Raters header horizontally else if (aRowItem.getIndex() == 0 && aCellItem.getIndex() != 0) { cell.add(new Label("label", Model.of(aCellItem.getModelObject()))); } // Raters header vertically else if (aRowItem.getIndex() != 0 && aCellItem.getIndex() == 0) { cell.add(new Label("label", Model.of(aRowItem.getModelObject()))); } // Upper diagonal else if (aCellItem.getIndex() > aRowItem.getIndex()) { AgreementResult result = AgreementTable.this.getModelObject().getStudy( aRowItem.getModelObject(), aCellItem.getModelObject()); boolean noDataRater0 = result.isAllNull(result.getCasGroupIds().get(0)); boolean noDataRater1 = result.isAllNull(result.getCasGroupIds().get(1)); int incPos = result.getIncompleteSetsByPosition().size(); int incLabel = result.getIncompleteSetsByLabel().size(); String label; if (result.getStudy().getItemCount() == 0) { label = "no positions"; } else if (noDataRater0 && noDataRater1) { label = "no labels"; } else if (noDataRater0) { label = "no labels from " + result.getCasGroupIds().get(0); } else if (noDataRater1) { label = "no labels from " + result.getCasGroupIds().get(1); } else if (incPos == result.getRelevantSetCount()) { label = "positions disjunct"; } else if (incLabel == result.getRelevantSetCount()) { label = "labels disjunct"; } else if ((incLabel + incPos) == result.getRelevantSetCount()) { label = "labels/positions disjunct"; } else { label = String.format("%.2f", result.getAgreement()); } StringBuilder tooltipTitle = new StringBuilder(); tooltipTitle.append(result.getCasGroupIds().get(0)); tooltipTitle.append('/'); tooltipTitle.append(result.getCasGroupIds().get(1)); StringBuilder tooltipContent = new StringBuilder(); tooltipContent.append("Positions annotated:\n"); tooltipContent.append(String.format("- %s: %d/%d%n", result.getCasGroupIds().get(0), result.getNonNullCount(result.getCasGroupIds().get(0)), result.getStudy().getItemCount())); tooltipContent.append(String.format("- %s: %d/%d%n", result.getCasGroupIds().get(1), result.getNonNullCount(result.getCasGroupIds().get(1)), result.getStudy().getItemCount())); tooltipContent.append(String.format("Distinct labels used: %d%n", result.getStudy().getCategoryCount())); Label l = new Label("label", Model.of(label)); cell.add(l); l.add(makeDownloadBehavior(aRowItem.getModelObject(), aCellItem.getModelObject())); DescriptionTooltipBehavior tooltip = new DescriptionTooltipBehavior( tooltipTitle.toString(), tooltipContent.toString()); tooltip.setOption("position", (Object) null); l.add(tooltip); l.add(new AttributeAppender("style", "cursor: pointer", ";")); } // Lower diagonal else if (aCellItem.getIndex() < aRowItem.getIndex()) { AgreementResult result = AgreementTable.this.getModelObject().getStudy( aRowItem.getModelObject(), aCellItem.getModelObject()); String label = String.format("%d/%d", result.getCompleteSetCount(), result.getRelevantSetCount()); String tooltipTitle = "Details about annotations excluded from " + "agreement calculation"; StringBuilder tooltipContent = new StringBuilder(); if (result.isExcludeIncomplete()) { tooltipContent.append(String.format("- Incomplete (missing): %d%n", result.getIncompleteSetsByPosition().size())); tooltipContent.append(String.format( "- Incomplete (not labeled): %d%n", result .getIncompleteSetsByLabel().size())); } tooltipContent.append(String.format("- Plurality: %d", result .getPluralitySets().size())); Label l = new Label("label", Model.of(label)); DescriptionTooltipBehavior tooltip = new DescriptionTooltipBehavior( tooltipTitle.toString(), tooltipContent.toString()); tooltip.setOption("position", (Object) null); l.add(tooltip); l.add(new AttributeAppender("style", "cursor: help", ";")); cell.add(l); } // Rest else { cell.add(new Label("label", Model.of("-"))); } aCellItem.add(cell); } }); // Odd/even coloring is reversed here to account for the header row at index 0 aRowItem.add(new AttributeAppender("class", (aRowItem.getIndex() % 2 == 0) ? "odd" : "even")); } }; add(rows); } private Behavior makeDownloadBehavior(final String aKey1, final String aKey2) { return new AjaxEventBehavior("click") { private static final long serialVersionUID = 1L; @Override protected void onEvent(AjaxRequestTarget aTarget) { AJAXDownload download = new AJAXDownload() { private static final long serialVersionUID = 1L; @Override protected IResourceStream getResourceStream() { return new AbstractResourceStream() { private static final long serialVersionUID = 1L; @Override public InputStream getInputStream() throws ResourceStreamNotFoundException { try { AgreementResult result = AgreementTable.this.getModelObject() .getStudy(aKey1, aKey2); switch (settings.getObject().exportFormat) { case CSV: return AgreementUtils.generateCsvReport(result); case DEBUG: return generateDebugReport(result); default: throw new IllegalStateException("Unknown export format [" + settings.getObject().exportFormat + "]"); } } catch (Exception e) { // FIXME Is there some better error handling here? LOG.error("Unable to generate agreement report", e); throw new ResourceStreamNotFoundException(e); } } @Override public void close() throws IOException { // Nothing to do } }; } }; getComponent().add(download); download.initiate(aTarget, "agreement" + settings.getObject().exportFormat.getExtension()); } }; } private InputStream generateDebugReport(AgreementResult aResult) { ByteArrayOutputStream buf = new ByteArrayOutputStream(); AgreementUtils.dumpAgreementStudy(new PrintStream(buf), aResult); return new ByteArrayInputStream(buf.toByteArray()); } @Override protected void onComponentTag(ComponentTag tag) { checkComponentTag(tag, "table"); super.onComponentTag(tag); } public PairwiseAnnotationResult getModelObject() { return (PairwiseAnnotationResult) getDefaultModelObject(); } public void setModelObject(PairwiseAnnotationResult aAgreements2) { setDefaultModelObject(aAgreements2); } }