/*
* Copyright 2011 Corpuslinguistic working group Humboldt University Berlin.
*
* 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 annis.gui.resultview;
import annis.CommonHelper;
import annis.gui.AnnisUI;
import annis.gui.ShareSingleMatchGenerator;
import annis.gui.MetaDataPanel;
import annis.gui.QueryController;
import annis.gui.objects.DisplayedResultQuery;
import annis.gui.objects.PagedResultQuery;
import annis.libgui.Helper;
import static annis.libgui.Helper.calculateMarkedAndCoveredIDs;
import annis.libgui.IDGenerator;
import annis.libgui.InstanceConfig;
import annis.libgui.PluginSystem;
import annis.libgui.ResolverProvider;
import static annis.model.AnnisConstants.ANNIS_NS;
import static annis.model.AnnisConstants.FEAT_RELANNIS_NODE;
import annis.model.RelannisNodeFeature;
import annis.resolver.ResolverEntry;
import annis.service.objects.Match;
import com.vaadin.data.Property;
import com.vaadin.data.util.IndexedContainer;
import com.vaadin.server.FontAwesome;
import com.vaadin.server.Page;
import com.vaadin.server.Resource;
import com.vaadin.ui.AbstractSelect;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.ComboBox;
import com.vaadin.ui.CssLayout;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.Notification;
import com.vaadin.ui.ProgressBar;
import com.vaadin.ui.UI;
import com.vaadin.ui.Window;
import com.vaadin.ui.themes.ValoTheme;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.SortedSet;
import org.apache.commons.lang3.StringUtils;
import org.corpus_tools.salt.common.SDocument;
import org.corpus_tools.salt.common.SDocumentGraph;
import org.corpus_tools.salt.common.SToken;
import org.corpus_tools.salt.common.SaltProject;
import org.corpus_tools.salt.core.SFeature;
import org.corpus_tools.salt.core.SNode;
import org.slf4j.LoggerFactory;
/**
*
* @author thomas
*/
public class SingleResultPanel extends CssLayout implements
Button.ClickListener, VisualizerContextChanger
{
private static final long serialVersionUID = 2L;
private static final String HIDE_KWIC = "hide_kwic";
private static final String INITIAL_OPEN = "initial_open";
private static final Resource ICON_RESOURCE = FontAwesome.INFO_CIRCLE;
private SDocument result;
private Map<String, String> markedCoveredMap;
private Map<String, String> markedExactMap;
private final PluginSystem ps;
private final AnnisUI ui;
private List<VisualizerPanel> visualizers;
private List<ResolverEntry> resolverEntries;
private final Button btInfo;
private final Button btLink;
private final List<String> path;
private String segmentationName;
private final HorizontalLayout infoBar;
private final String corpusName;
private final String documentName;
private final QueryController queryController;
private final long resultNumber;
private final ResolverProvider resolverProvider;
private final Set<String> visibleTokenAnnos;
private ProgressBar reloadVisualizer;
private final ComboBox lftCtxCombo;
private final ComboBox rghtCtxCombo;
private final Map<Long, Boolean> visualizerState;
private static final org.slf4j.Logger log = LoggerFactory.getLogger(
SingleResultPanel.class);
private final InstanceConfig instanceConfig;
private PagedResultQuery query;
private final Match match;
public SingleResultPanel(final SDocument result,
Match match,
long resultNumber,
ResolverProvider resolverProvider, PluginSystem ps,
AnnisUI ui,
Set<String> visibleTokenAnnos, String segmentationName,
QueryController controller, InstanceConfig instanceConfig,
DisplayedResultQuery query)
{
this.ps = ps;
this.ui = ui;
this.result = result;
this.segmentationName = segmentationName;
this.queryController = controller;
this.resultNumber = resultNumber;
this.resolverProvider = resolverProvider;
this.visibleTokenAnnos = visibleTokenAnnos;
this.instanceConfig = instanceConfig;
this.query = query;
this.match = match;
calculateHelperVariables();
setWidth("100%");
setHeight("-1px");
if(query != null
&& query.getSelectedMatches().contains(resultNumber))
{
addStyleName("selected-match");
}
infoBar = new HorizontalLayout();
infoBar.addStyleName("info-bar");
infoBar.setWidth("100%");
infoBar.setHeight("-1px");
Label lblNumber = new Label("" + (resultNumber + 1));
infoBar.addComponent(lblNumber);
lblNumber.setSizeUndefined();
btInfo = new Button();
btInfo.setStyleName(ValoTheme.BUTTON_BORDERLESS);
btInfo.setIcon(ICON_RESOURCE);
btInfo.setDescription("Show metadata");
btInfo.addClickListener((Button.ClickListener) this);
infoBar.addComponent(btInfo);
btLink = new Button();
btLink.setStyleName(ValoTheme.BUTTON_BORDERLESS);
btLink.setIcon(FontAwesome.SHARE_ALT);
btLink.setDescription("Share match reference");
btLink.setDisableOnClick(true);
btLink.addClickListener(new Button.ClickListener()
{
@Override
public void buttonClick(ClickEvent event)
{
showShareSingleMatchGenerator();
}
});
infoBar.addComponent(btLink);
/**
* Extract the top level corpus name and the document name of this single
* result.
*/
path = CommonHelper.getCorpusPath(result.getGraph(), result);
Collections.reverse(path);
corpusName = path.get(0);
documentName = path.get(path.size() - 1);
MinMax minMax = getIds(result.getDocumentGraph());
// build label
StringBuilder sb = new StringBuilder("Path: ");
sb.append(StringUtils.join(path, " > "));
sb.append(" (" + minMax.segName + " ").append(minMax.min);
sb.append(" - ").append(minMax.max).append(")");
Label lblPath = new Label(sb.toString());
lblPath.addStyleName("path-label");
lblPath.setWidth("100%");
lblPath.setHeight("-1px");
infoBar.addComponent(lblPath);
infoBar.setExpandRatio(lblPath, 1.0f);
infoBar.setSpacing(false);
this.visualizerState = new HashMap<>();
// init context combox
lftCtxCombo = new ComboBox();
rghtCtxCombo = new ComboBox();
lftCtxCombo.setWidth(50, Unit.PIXELS);
rghtCtxCombo.setWidth(50, Unit.PIXELS);
lftCtxCombo.setNullSelectionAllowed(false);
rghtCtxCombo.setNullSelectionAllowed(false);
lftCtxCombo.addStyleName(ValoTheme.COMBOBOX_SMALL);
rghtCtxCombo.addStyleName(ValoTheme.COMBOBOX_SMALL);
IndexedContainer lftCtxContainer = new IndexedContainer();
IndexedContainer rghtCtxContainer = new IndexedContainer();
// and a property for sorting
lftCtxContainer.addContainerProperty("number", Integer.class, 0);
rghtCtxContainer.addContainerProperty("number", Integer.class, 0);
for (int i = 0; i < 30; i += 5)
{
lftCtxContainer.addItem(i).getItemProperty("number").setValue(i);
rghtCtxContainer.addItem(i).getItemProperty("number").setValue(i);
}
int lftContextIdx = query == null ? 0 : query.getLeftContext();
lftCtxContainer.addItemAt(lftContextIdx, lftContextIdx);
lftCtxContainer.sort(new Object[]
{
"number"
}, new boolean[]
{
true
});
int rghtCtxIdx = query == null ? 0 : query.getRightContext();
rghtCtxContainer.addItem(rghtCtxIdx);
rghtCtxContainer.sort(new Object[]
{
"number"
}, new boolean[]
{
true
});
lftCtxCombo.setContainerDataSource(lftCtxContainer);
rghtCtxCombo.setContainerDataSource(rghtCtxContainer);
lftCtxCombo.select(lftContextIdx);
rghtCtxCombo.select(rghtCtxIdx);
lftCtxCombo.setNewItemsAllowed(true);
rghtCtxCombo.setNewItemsAllowed(true);
lftCtxCombo.setImmediate(true);
rghtCtxCombo.setImmediate(true);
lftCtxCombo.setNewItemHandler(new AddNewItemHandler(lftCtxCombo));
rghtCtxCombo.setNewItemHandler(new AddNewItemHandler(rghtCtxCombo));
lftCtxCombo.addValueChangeListener(
new ContextChangeListener(resultNumber, true));
rghtCtxCombo.addValueChangeListener(
new ContextChangeListener(resultNumber, false));
Label leftCtxLabel = new Label("left context: ");
Label rightCtxLabel = new Label("right context: ");
leftCtxLabel.setWidth("-1px");
rightCtxLabel.setWidth("-1px");
HorizontalLayout ctxLayout = new HorizontalLayout();
ctxLayout.setSpacing(true);
ctxLayout.addComponents(leftCtxLabel, lftCtxCombo, rightCtxLabel,
rghtCtxCombo);
infoBar.addComponent(ctxLayout);
addComponent(infoBar);
initVisualizer();
}
private void showShareSingleMatchGenerator()
{
// select the current match
if(ui != null)
{
ui.getQueryState().getSelectedMatches().getValue().clear();
ui.getQueryState().getSelectedMatches().getValue().add(resultNumber);
ui.getSearchView().updateFragment(ui.getQueryController().getSearchQuery());
}
Window window = new ShareSingleMatchGenerator(resolverEntries, match, query, segmentationName, ps);
window.setWidth(790, Unit.PIXELS);
window.setHeight(580, Unit.PIXELS);
window.setResizable(true);
window.setModal(true);
window.addCloseListener(new Window.CloseListener()
{
@Override
public void windowClose(Window.CloseEvent e)
{
btLink.setEnabled(true);
}
});
window.setCaption("Match reference link");
UI.getCurrent().addWindow(window);
}
public void setSegmentationLayer(String segmentationName)
{
this.segmentationName = segmentationName;
if (result != null)
{
List<SNode> segNodes = CommonHelper.getSortedSegmentationNodes(
segmentationName,
result.getDocumentGraph());
Map<String, Long> markedAndCovered = calculateMarkedAndCoveredIDs(result, segNodes, segmentationName);
for (VisualizerPanel p : visualizers)
{
p.setSegmentationLayer(segmentationName, markedAndCovered);
}
}
}
public void setVisibleTokenAnnosVisible(SortedSet<String> annos)
{
for (VisualizerPanel p : visualizers)
{
p.setVisibleTokenAnnosVisible(annos);
}
}
private void calculateHelperVariables()
{
markedCoveredMap = new HashMap<>();
markedExactMap = Helper.calculateColorsForMarkedExact(result);
}
@Override
public void buttonClick(ClickEvent event)
{
if (event.getButton() == btInfo && result != null)
{
Window infoWindow = new Window("Info for " + result.getId());
infoWindow.setModal(false);
MetaDataPanel meta = new MetaDataPanel(path.get(0), path.get(path.size()
- 1));
infoWindow.setContent(meta);
infoWindow.setWidth("400px");
infoWindow.setHeight("400px");
UI.getCurrent().addWindow(infoWindow);
}
}
private void showReloadingProgress()
{
//remove the old visualizer
for (VisualizerPanel v : visualizers)
{
this.removeComponent(v);
}
// first set loading indicator
reloadVisualizer = new ProgressBar(1.0f);
reloadVisualizer.setIndeterminate(true);
reloadVisualizer.setSizeFull();
reloadVisualizer.setHeight(150, Unit.PIXELS);
addComponent(reloadVisualizer);
}
private void initVisualizer()
{
try
{
ResolverEntry[] entries
= resolverProvider == null ? new ResolverEntry[0]
: resolverProvider.getResolverEntries(result);
visualizers = new LinkedList<>();
resolverEntries = new LinkedList<>();
List<VisualizerPanel> openVisualizers = new LinkedList<>();
List<SNode> segNodes = CommonHelper.getSortedSegmentationNodes(
segmentationName,
result.getDocumentGraph());
Map<String, Long> markedAndCovered =
Helper.calculateMarkedAndCoveredIDs(result, segNodes, segmentationName);
Helper.calulcateColorsForMarkedAndCovered(result, markedAndCovered, markedCoveredMap);
String resultID = "" + new Random().nextInt(Integer.MAX_VALUE);
for (int i = 0; i < entries.length; i++)
{
String htmlID = "resolver-" + resultNumber + "_" + i;
VisualizerPanel p = new VisualizerPanel(
entries[i], result, corpusName, documentName,
visibleTokenAnnos, markedAndCovered,
markedCoveredMap, markedExactMap,
htmlID, resultID, this, segmentationName, ps, instanceConfig);
visualizers.add(p);
resolverEntries.add(entries[i]);
Properties mappings = entries[i].getMappings();
// check if there is the visibility of a visualizer changed
// since it the whole result panel was loaded. If not the entry of the
// resovler entry is used, for determine the visibility status
if (visualizerState.containsKey(entries[i].getId()))
{
if (visualizerState.get(entries[i].getId()))
{
openVisualizers.add(p);
}
}
else
{
if (Boolean.parseBoolean(mappings.getProperty(INITIAL_OPEN, "false")))
{
openVisualizers.add(p);
}
}
} // for each resolver entry
// attach visualizer
for (VisualizerPanel p : visualizers)
{
addComponent(p);
}
for (VisualizerPanel p : openVisualizers)
{
p.toggleVisualizer(true, null);
}
}
catch (RuntimeException ex)
{
log.error("problems with initializing Visualizer Panel", ex);
}
catch (Exception ex)
{
log.error("problems with initializing Visualizer Panel", ex);
}
}
@Override
public void attach()
{
super.attach();
if (Helper.isKickstarter(getSession()))
{
btLink.setVisible(false);
}
IDGenerator.assignIDForFields(SingleResultPanel.this, infoBar, btInfo);
}
@Override
public void registerVisibilityStatus(long entryId, boolean status)
{
visualizerState.put(entryId, status);
}
@Override
public void changeContext(long resultNumber, int context,
boolean left)
{
//delegates the task to the query controller.
queryController.changeContext(query, match, resultNumber, context, this, left);
}
private static class AddNewItemHandler implements AbstractSelect.NewItemHandler
{
final private ComboBox combobox;
public AddNewItemHandler(ComboBox comboBox)
{
this.combobox = comboBox;
}
@Override
public void addNewItem(String newValue)
{
String ERROR_MESSAGE_HEADER = "Illegal value";
try
{
int i = Integer.parseInt(newValue);
if (i < 0)
{
new Notification(ERROR_MESSAGE_HEADER,
"<div><p>context < 0 makes no sense</p></div>",
Notification.Type.WARNING_MESSAGE, true).show(Page.getCurrent());
}
else
{
combobox.getContainerDataSource().addItem(i).
getItemProperty("number").setValue(i);
if (combobox.getContainerDataSource() instanceof IndexedContainer)
{
((IndexedContainer) combobox.getContainerDataSource()).sort(
new Object[]
{
"number"
}, new boolean[]
{
true
});
}
combobox.select(i);
}
}
catch (NumberFormatException ex)
{
new Notification(ERROR_MESSAGE_HEADER,
"<div><p>Only numbers are allowed.</p></div>",
Notification.Type.WARNING_MESSAGE, true).show(Page.getCurrent());
}
}
}
private class ContextChangeListener implements
Property.ValueChangeListener
{
long resultNumber;
boolean left;
public ContextChangeListener(long resultNumber, boolean left)
{
this.resultNumber = resultNumber;
this.left = left;
}
@Override
public void valueChange(Property.ValueChangeEvent event)
{
showReloadingProgress();
lftCtxCombo.setEnabled(false);
rghtCtxCombo.setEnabled(false);
int ctx = Integer.parseInt(event.getProperty().getValue().toString());
changeContext(resultNumber, ctx, left);
}
}
private static class MinMax
{
String segName = "tokens";
long min;
long max;
}
private MinMax getIds(SDocumentGraph graph)
{
List<SToken> sTokens = graph.getTokens();
MinMax minMax = new MinMax();
minMax.min = Long.MAX_VALUE;
minMax.max = Long.MIN_VALUE;
if (segmentationName == null)
{
minMax.segName = "tokens";
if (sTokens != null)
{
for (SToken t : sTokens)
{
SFeature feature = t.getFeature(ANNIS_NS,
FEAT_RELANNIS_NODE);
if(feature != null && feature.getValue() instanceof RelannisNodeFeature)
{
RelannisNodeFeature f = (RelannisNodeFeature) feature.getValue();
if (minMax.min > f.getTokenIndex())
{
minMax.min = f.getTokenIndex();
}
if (minMax.max < f.getTokenIndex())
{
minMax.max = f.getTokenIndex();
}
}
}
}
}
else
{
minMax.segName = segmentationName;
List<SNode> nodes = CommonHelper.getSortedSegmentationNodes(
segmentationName, graph);
for (SNode n : nodes)
{
RelannisNodeFeature f = RelannisNodeFeature.extract(n);
if (minMax.min > f.getSegIndex())
{
minMax.min = f.getSegIndex();
}
if (minMax.max < f.getSegIndex())
{
minMax.max = f.getSegIndex();
}
}
}
minMax.min++;
minMax.max++;
return minMax;
}
@Override
public void updateResult(SaltProject p, PagedResultQuery query)
{
this.query = query;
if (p != null
&& p.getCorpusGraphs() != null
&& !p.getCorpusGraphs().isEmpty()
&& p.getCorpusGraphs().get(0) != null
&& p.getCorpusGraphs().get(0).getDocuments() != null
&& !p.getCorpusGraphs().get(0).getDocuments().isEmpty())
{
this.result = p.getCorpusGraphs().get(0).getDocuments().get(0);
}
removeComponent(reloadVisualizer);
initVisualizer();
lftCtxCombo.setEnabled(true);
rghtCtxCombo.setEnabled(true);
}
protected SDocument getResult()
{
return result;
}
}