/*
* 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.QueryController;
import annis.gui.components.OnLoadCallbackExtension;
import annis.gui.controlpanel.QueryPanel;
import annis.gui.objects.DisplayedResultQuery;
import annis.gui.objects.PagedResultQuery;
import annis.gui.paging.PagingComponent;
import annis.libgui.Helper;
import annis.libgui.IDGenerator;
import annis.libgui.InstanceConfig;
import annis.libgui.PluginSystem;
import annis.libgui.ResolverProviderImpl;
import annis.model.AnnisConstants;
import annis.resolver.ResolverEntry;
import annis.resolver.SingleResolverRequest;
import annis.service.objects.CorpusConfig;
import annis.service.objects.Match;
import com.google.common.base.Preconditions;
import com.vaadin.server.AbstractClientConnector;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.CssLayout;
import com.vaadin.ui.JavaScript;
import com.vaadin.ui.Label;
import com.vaadin.ui.MenuBar;
import com.vaadin.ui.MenuBar.MenuItem;
import com.vaadin.ui.Notification;
import com.vaadin.ui.Panel;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.themes.ValoTheme;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.BlockingQueue;
import org.apache.commons.lang3.StringUtils;
import org.corpus_tools.salt.common.SCorpusGraph;
import org.corpus_tools.salt.common.SDocument;
import org.corpus_tools.salt.common.SDocumentGraph;
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 ResultViewPanel extends VerticalLayout implements
OnLoadCallbackExtension.Callback
{
private static final org.slf4j.Logger log = LoggerFactory.getLogger(
ResultViewPanel.class);
public static final String NULL_SEGMENTATION_VALUE = "tokens (default)";
private final Map<HashSet<SingleResolverRequest>, List<ResolverEntry>> cacheResolver;
public static final String FILESYSTEM_CACHE_RESULT
= "ResultSetPanel_FILESYSTEM_CACHE_RESULT";
public static final String MAPPING_HIDDEN_ANNOS = "hidden_annos";
private final PagingComponent paging;
private final PluginSystem ps;
private final MenuItem miTokAnnos;
private final MenuItem miSegmentation;
private final TreeMap<String, Boolean> tokenAnnoVisible;
private final QueryController controller;
private final Set<String> segmentationLayerSet
= Collections.synchronizedSet(new TreeSet<String>());
private final Set<String> tokenAnnotationLevelSet
= Collections.synchronizedSet(new TreeSet<String>());
private final InstanceConfig instanceConfig;
private final CssLayout resultLayout;
private final List<SingleResultPanel> resultPanelList;
private String segmentationName;
private int currentResults;
private int numberOfResults;
private ArrayList<Match> allMatches;
private transient BlockingQueue<SaltProject> projectQueue;
private PagedResultQuery currentQuery;
private final DisplayedResultQuery initialQuery;
private final AnnisUI sui;
public ResultViewPanel(AnnisUI ui,
PluginSystem ps, InstanceConfig instanceConfig, DisplayedResultQuery initialQuery)
{
this.sui = ui;
this.tokenAnnoVisible = new TreeMap<>();
this.ps = ps;
this.controller = ui.getQueryController();
this.initialQuery = initialQuery;
cacheResolver
= Collections.synchronizedMap(
new HashMap<HashSet<SingleResolverRequest>, List<ResolverEntry>>());
resultPanelList
= Collections.synchronizedList(new LinkedList<SingleResultPanel>());
resultLayout = new CssLayout();
resultLayout.addStyleName("result-view-css");
Panel resultPanel = new Panel(resultLayout);
resultPanel.setSizeFull();
resultPanel.addStyleName(ValoTheme.PANEL_BORDERLESS);
resultPanel.addStyleName("result-view-panel");
this.instanceConfig = instanceConfig;
setSizeFull();
setMargin(false);
MenuBar mbResult = new MenuBar();
mbResult.setWidth("100%");
mbResult.addStyleName("menu-hover");
addComponent(mbResult);
miSegmentation = mbResult.addItem("Base text", null);
miTokAnnos = mbResult.addItem("Token Annotations", null);
addComponent(resultPanel);
setExpandRatio(mbResult, 0.0f);
setExpandRatio(resultPanel, 1.0f);
paging = new PagingComponent();
addComponent(paging, 1);
setComponentAlignment(paging, Alignment.TOP_CENTER);
setExpandRatio(paging, 0.0f);
}
@Override
public void attach()
{
super.attach();
IDGenerator.assignIDForFields(ResultViewPanel.this, resultLayout);
IDGenerator.assignIDForEachField(paging);
}
/**
* Informs the user about the searching process.
*
* @param query Represents a limited query
*/
public void showMatchSearchInProgress(PagedResultQuery query)
{
resultLayout.removeAllComponents();
segmentationName = query.getSegmentation();
}
public void showNoResult()
{
resultLayout.removeAllComponents();
currentResults = 0;
// nothing to show since we have an empty result
Label lblNoResult = new Label("No matches found.");
lblNoResult.setWidth("100%");
lblNoResult.addStyleName("result-view-no-content");
resultLayout.addComponent(lblNoResult);
showFinishedSubgraphSearch();
}
public void showSubgraphSearchInProgress(PagedResultQuery q, float percent)
{
if (percent == 0.0f)
{
resultLayout.removeAllComponents();
currentResults = 0;
}
}
/**
* Set a new querys in result panel.
*
* @param queue holds the salt graph
* @param q holds the ordinary query
* @param allMatches All matches.
*/
public void setQueryResultQueue(BlockingQueue<SaltProject> queue,
PagedResultQuery q, ArrayList<Match> allMatches)
{
this.projectQueue = queue;
this.currentQuery = q;
this.numberOfResults = allMatches.size();
this.allMatches = allMatches;
paging.setPageSize(q.getLimit(), false);
paging.setInfo(q.getQuery());
resultLayout.removeAllComponents();
resultPanelList.clear();
// get the first query result
SaltProject first = queue.poll();
Preconditions.checkState(first != null,
"There must be already an element in the queue");
addQueryResult(q, Arrays.asList(first));
}
private void resetQueryResultQueue()
{
this.projectQueue = null;
this.currentQuery = null;
this.currentResults = 0;
this.numberOfResults = 0;
}
private void addQueryResult(PagedResultQuery q, List<SaltProject> subgraphList)
{
if (q == null)
{
return;
}
List<SingleResultPanel> newPanels = new LinkedList<>();
try
{
if (subgraphList == null || subgraphList.isEmpty())
{
Notification.show("Could not get subgraphs",
Notification.Type.TRAY_NOTIFICATION);
}
else
{
for (SaltProject p : subgraphList)
{
updateVariables(p);
newPanels = createPanels(p, currentResults, q.getOffset() + currentResults);
currentResults += newPanels.size();
String strResults = numberOfResults > 1 ? "results" : "result";
sui.getSearchView().getControlPanel().getQueryPanel().setStatus(sui.getSearchView().getControlPanel().
getQueryPanel().getLastPublicStatus(),
" (showing " + currentResults + "/" + numberOfResults + " " + strResults + ")");
if (currentResults == numberOfResults)
{
resetQueryResultQueue();
}
for (SingleResultPanel panel : newPanels)
{
resultPanelList.add(panel);
resultLayout.addComponent(panel);
panel.setSegmentationLayer(sui.getQueryState().getVisibleBaseText().getValue());
}
}
if (currentResults == numberOfResults)
{
showFinishedSubgraphSearch();
if(!initialQuery.getSelectedMatches().isEmpty())
{
// scroll to the first selected match
JavaScript.eval(
"$(\".v-panel-content-result-view-panel\").animate({scrollTop: $(\".selected-match\").offset().top - $(\".result-view-panel\").offset().top}, 1000);");
}
}
if (projectQueue != null && !newPanels.isEmpty() && currentResults < numberOfResults)
{
log.debug("adding callback for result " + currentResults);
// add a callback so we can load the next single result
OnLoadCallbackExtension ext = new OnLoadCallbackExtension(this, 250);
ext.extend(newPanels.get(newPanels.size() - 1));
}
}
}
catch (Throwable ex)
{
log.error(null, ex);
}
}
public void showFinishedSubgraphSearch()
{
//Search complete, stop progress bar control
if (sui.getSearchView().getControlPanel().getQueryPanel().getPiCount() != null)
{
if (sui.getSearchView().getControlPanel().getQueryPanel().getPiCount().isVisible())
{
sui.getSearchView().getControlPanel().getQueryPanel().getPiCount().setVisible(false);
sui.getSearchView().getControlPanel().getQueryPanel().getPiCount().setEnabled(false);
}
}
// also remove the info how many results have been fetched
QueryPanel qp = sui.getSearchView().getControlPanel().getQueryPanel();
qp.setStatus(qp.getLastPublicStatus());
}
private List<SingleResultPanel> createPanels(SaltProject p, int localMatchIndex, long globalOffset)
{
List<SingleResultPanel> result = new LinkedList<>();
int i = 0;
for (SCorpusGraph corpusGraph : p.getCorpusGraphs())
{
SDocument doc = corpusGraph.getDocuments().get(0);
Match m = new Match();
if(allMatches != null && localMatchIndex >= 0 && localMatchIndex < allMatches.size())
{
m = allMatches.get(localMatchIndex);
}
SingleResultPanel panel = new SingleResultPanel(doc, m,
i + globalOffset, new ResolverProviderImpl(cacheResolver), ps, sui,
getVisibleTokenAnnos(), segmentationName, controller,
instanceConfig, initialQuery);
i++;
panel.setWidth("100%");
panel.setHeight("-1px");
result.add(panel);
}
return result;
}
private void updateVariables(SaltProject p)
{
segmentationLayerSet.addAll(getSegmentationNames(p));
tokenAnnotationLevelSet.addAll(CommonHelper.getTokenAnnotationLevelSet(p));
Set<String> hiddenTokenAnnos = null;
Set<String> corpusNames = CommonHelper.getToplevelCorpusNames(p);
for (String corpusName : corpusNames)
{
CorpusConfig corpusConfig = Helper.getCorpusConfig(corpusName);
if (corpusConfig != null && corpusConfig.containsKey(MAPPING_HIDDEN_ANNOS))
{
hiddenTokenAnnos = new HashSet<>(
Arrays.asList(
StringUtils.split(
corpusConfig.getConfig(MAPPING_HIDDEN_ANNOS), ",")
)
);
}
}
if (hiddenTokenAnnos != null)
{
for (String tokenLevel : hiddenTokenAnnos)
{
if (tokenAnnotationLevelSet.contains(tokenLevel))
{
tokenAnnotationLevelSet.remove(tokenLevel);
}
}
}
updateSegmentationLayer(segmentationLayerSet);
updateVisibleToken(tokenAnnotationLevelSet);
}
private Set<String> getSegmentationNames(SaltProject p)
{
Set<String> result = new TreeSet<>();
for (SCorpusGraph corpusGraphs : p.getCorpusGraphs())
{
for (SDocument doc : corpusGraphs.getDocuments())
{
SDocumentGraph g = doc.getDocumentGraph();
if (g != null)
{
// collect the start nodes of a segmentation chain of length 1
for (SNode n : g.getNodes())
{
SFeature feat
= n.getFeature(AnnisConstants.ANNIS_NS,
AnnisConstants.FEAT_FIRST_NODE_SEGMENTATION_CHAIN);
if (feat != null && feat.getValue_STEXT() != null)
{
result.add(feat.getValue_STEXT());
}
}
} // end if graph not null
}
}
return result;
}
public void setCount(int count)
{
paging.setCount(count, false);
paging.setStartNumber(initialQuery.getOffset());
}
public SortedSet<String> getVisibleTokenAnnos()
{
TreeSet<String> result = new TreeSet<>();
for (Entry<String, Boolean> e : tokenAnnoVisible.entrySet())
{
if (e.getValue().booleanValue() == true)
{
result.add(e.getKey());
}
}
return result;
}
/**
* Listens to events on the base text menu and updates the segmentation layer.
*/
private class MenuBaseTextCommand implements MenuBar.Command
{
@Override
public void menuSelected(MenuItem selectedItem)
{
// remember old value
String oldSegmentationLayer = sui.getQueryState().getVisibleBaseText().getValue();
// set the new selected item
String newSegmentationLayer = selectedItem.getText();
if (NULL_SEGMENTATION_VALUE.equals(newSegmentationLayer))
{
newSegmentationLayer = null;
}
for (MenuItem mi : miSegmentation.getChildren())
{
mi.setChecked(mi == selectedItem);
}
if (oldSegmentationLayer != null)
{
if (!oldSegmentationLayer.equals(newSegmentationLayer))
{
setSegmentationLayer(newSegmentationLayer);
}
}
else if (newSegmentationLayer != null)
{
// oldSegmentation is null, but selected is not
setSegmentationLayer(newSegmentationLayer);
}
//update URL with newly selected segmentation layer
sui.getQueryState().getVisibleBaseText().setValue(newSegmentationLayer);
sui.getSearchView().updateFragment(sui.getQueryController().getSearchQuery());
}
}
private void updateSegmentationLayer(Set<String> segLayers)
{
// clear the menu base text
miSegmentation.removeChildren();
// add the default token layer
segLayers.add("");
// iterate of all segmentation layers and add them to the menu
for (String s : segLayers)
{
// the new menu entry
MenuItem miSingleSegLayer;
/**
* TODO maybe it would be better, to mark the default text level
* corresponding to the corpus.properties.
*
* There exists always a default text level.
*/
if (s == null || "".equals(s))
{
miSingleSegLayer = miSegmentation.addItem(
NULL_SEGMENTATION_VALUE, new MenuBaseTextCommand());
}
else
{
miSingleSegLayer = miSegmentation.addItem(s, new MenuBaseTextCommand());
}
// mark as selectable
miSingleSegLayer.setCheckable(true);
/**
* Check if a segmentation item must set checked. If no segmentation layer
* is selected, set the default layer as selected.
*/
final String selectedSegmentationLayer = sui.getQueryState().getVisibleBaseText().getValue();
if ((selectedSegmentationLayer == null && "".equals(s))
|| s.equals(selectedSegmentationLayer))
{
miSingleSegLayer.setChecked(true);
}
else
{
miSingleSegLayer.setChecked(false);
}
} // end iterate for segmentation layer
}
public void updateVisibleToken(Set<String> tokenAnnotationLevelSet)
{
// if no token annotations are there, do not show this mneu
if (tokenAnnotationLevelSet == null
|| tokenAnnotationLevelSet.isEmpty())
{
miTokAnnos.setVisible(false);
}
else
{
miTokAnnos.setVisible(true);
}
// add new annotations
if (tokenAnnotationLevelSet != null)
{
for (String s : tokenAnnotationLevelSet)
{
if (!tokenAnnoVisible.containsKey(s))
{
tokenAnnoVisible.put(s, Boolean.TRUE);
}
}
}
miTokAnnos.removeChildren();
if (tokenAnnotationLevelSet != null)
{
for (final String a : tokenAnnotationLevelSet)
{
MenuItem miSingleTokAnno = miTokAnnos.addItem(a.replaceFirst("::", ":"),
new MenuBar.Command()
{
@Override
public void menuSelected(MenuItem selectedItem)
{
if (selectedItem.isChecked())
{
tokenAnnoVisible.put(a, Boolean.TRUE);
}
else
{
tokenAnnoVisible.put(a, Boolean.FALSE);
}
setVisibleTokenAnnosVisible(getVisibleTokenAnnos());
}
});
miSingleTokAnno.setCheckable(true);
miSingleTokAnno.setChecked(tokenAnnoVisible.get(a).booleanValue());
}
}
}
@Override
public boolean onCompononentLoaded(AbstractClientConnector source)
{
if (source != null)
{
if (projectQueue != null && currentQuery != null)
{
LinkedList<SaltProject> subgraphs = new LinkedList<>();
SaltProject p;
while ((p = projectQueue.poll()) != null)
{
log.debug("Polling queue for SaltProject graph");
subgraphs.add(p);
}
if (subgraphs.isEmpty())
{
log.debug("no SaltProject graph in queue");
return false;
}
log.debug("taken {} SaltProject graph(s) from queue", subgraphs.size());
addQueryResult(currentQuery, subgraphs);
return true;
}
}
return true;
}
private void setVisibleTokenAnnosVisible(SortedSet<String> annos)
{
for (SingleResultPanel p : resultPanelList)
{
p.setVisibleTokenAnnosVisible(annos);
}
}
private void setSegmentationLayer(String segmentationLayer)
{
for (SingleResultPanel p : resultPanelList)
{
p.setSegmentationLayer(segmentationLayer);
}
}
public PagingComponent getPaging()
{
return paging;
}
}