/*
* This file is part of ELKI:
* Environment for Developing KDD-Applications Supported by Index-Structures
*
* Copyright (C) 2017
* ELKI Development Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.lmu.ifi.dbs.elki.visualization;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import de.lmu.ifi.dbs.elki.algorithm.clustering.trivial.ByLabelHierarchicalClustering;
import de.lmu.ifi.dbs.elki.algorithm.clustering.trivial.TrivialAllInOne;
import de.lmu.ifi.dbs.elki.data.Clustering;
import de.lmu.ifi.dbs.elki.data.NumberVector;
import de.lmu.ifi.dbs.elki.data.model.Model;
import de.lmu.ifi.dbs.elki.data.type.NoSupportedDataTypeException;
import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
import de.lmu.ifi.dbs.elki.database.Database;
import de.lmu.ifi.dbs.elki.database.datastore.DataStoreEvent;
import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
import de.lmu.ifi.dbs.elki.database.relation.Relation;
import de.lmu.ifi.dbs.elki.evaluation.AutomaticEvaluation;
import de.lmu.ifi.dbs.elki.logging.Logging;
import de.lmu.ifi.dbs.elki.result.*;
import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
/**
* Map to store context information for the visualizer. This can be any data
* that should to be shared among plots, such as line colors, styles etc.
*
* @author Erich Schubert
* @since 0.3
*
* @apiviz.landmark
* @apiviz.composedOf StyleLibrary
* @apiviz.composedOf StylingPolicy
* @apiviz.composedOf SelectionResult
* @apiviz.composedOf ResultHierarchy
* @apiviz.composedOf VisualizationTree
* @apiviz.composedOf DataStoreListener
* @apiviz.composedOf VisualizationProcessor
*/
public class VisualizerContext implements DataStoreListener, Result {
/**
* Logger.
*/
private static final Logging LOG = Logging.getLogger(VisualizerContext.class);
/**
* Tree of visualizations.
*/
private VisualizationTree vistree = new VisualizationTree();
/**
* The full result object
*/
private ResultHierarchy hier;
/**
* The event listeners for this context.
*/
private ArrayList<DataStoreListener> listenerList = new ArrayList<>();
/**
* Factories to use
*/
private Collection<VisualizationProcessor> factories;
/**
* Selection result
*/
private SelectionResult selection;
/**
* Styling policy
*/
StylingPolicy stylepolicy;
/**
* Style library
*/
StyleLibrary stylelibrary;
/**
* Starting point of the result tree, may be {@code null}.
*/
private Result baseResult;
/**
* Constructor. We currently require a Database and a Result.
*
* @param hier Result hierarchy
* @param start Starting result
* @param stylelib Style library
* @param factories Visualizer Factories to use
*/
public VisualizerContext(ResultHierarchy hier, Result start, StyleLibrary stylelib, Collection<VisualizationProcessor> factories) {
super();
this.hier = hier;
this.baseResult = start;
this.factories = factories;
// Ensure that various common results needed by visualizers are
// automatically created
final Database db = ResultUtil.findDatabase(hier);
if(db == null) {
LOG.warning("No database reachable from " + hier);
return;
}
AutomaticEvaluation.ensureClusteringResult(db, db);
this.selection = SelectionResult.ensureSelectionResult(db);
for(Relation<?> rel : ResultUtil.getRelations(db)) {
SamplingResult.getSamplingResult(rel);
// FIXME: this is a really ugly workaround. :-(
if(TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
@SuppressWarnings("unchecked")
Relation<? extends NumberVector> vrel = (Relation<? extends NumberVector>) rel;
ScalesResult.getScalesResult(vrel);
}
}
makeStyleResult(stylelib);
// Add visualizers.
notifyFactories(db);
// For proxying events.
db.addDataStoreListener(this);
// Add a result listener.
// Don't expose these methods to avoid inappropriate use.
addResultListener(new ResultListener() {
@Override
public void resultAdded(Result child, Result parent) {
notifyFactories(child);
}
@Override
public void resultChanged(Result current) {
// FIXME: need to do anything?
}
@Override
public void resultRemoved(Result child, Result parent) {
// FIXME: implement
}
});
}
/**
* Generate a new style result for the given style library.
*
* @param stylelib Style library
*/
protected void makeStyleResult(StyleLibrary stylelib) {
final Database db = ResultUtil.findDatabase(hier);
stylelibrary = stylelib;
List<Clustering<? extends Model>> clusterings = Clustering.getClusteringResults(db);
if(!clusterings.isEmpty()) {
stylepolicy = new ClusterStylingPolicy(clusterings.get(0), stylelib);
}
else {
Clustering<Model> c = generateDefaultClustering();
stylepolicy = new ClusterStylingPolicy(c, stylelib);
}
}
/**
* Get the hierarchy object
*
* @return hierarchy object
*/
public ResultHierarchy getHierarchy() {
return hier;
}
/**
* Get the active styling policy
*
* @return Styling policy
*/
public StylingPolicy getStylingPolicy() {
return stylepolicy;
}
/**
* Set the active styling policy
*
* @param policy new Styling policy
*/
public void setStylingPolicy(StylingPolicy policy) {
this.stylepolicy = policy;
visChanged(policy);
}
/**
* Get the style library
*
* @return Style library
*/
public StyleLibrary getStyleLibrary() {
return stylelibrary;
}
/**
* Get the style library
*
* @param library Style library
*/
public void setStyleLibrary(StyleLibrary library) {
this.stylelibrary = library;
}
/**
* Generate a default (fallback) clustering.
*
* @return generated clustering
*/
private Clustering<Model> generateDefaultClustering() {
final Database db = ResultUtil.findDatabase(hier);
Clustering<Model> c = null;
try {
// Try to cluster by labels
ByLabelHierarchicalClustering split = new ByLabelHierarchicalClustering();
c = split.run(db);
}
catch(NoSupportedDataTypeException e) {
// Put everything into one
c = new TrivialAllInOne().run(db);
}
return c;
}
// TODO: add ShowVisualizer,HideVisualizer with tool semantics.
// TODO: add ShowVisualizer,HideVisualizer with tool semantics.
/**
* Get the current selection result.
*
* @return selection result
*/
public SelectionResult getSelectionResult() {
return selection;
}
/**
* Get the current selection.
*
* @return selection
*/
public DBIDSelection getSelection() {
return selection.getSelection();
}
/**
* Set a new selection.
*
* @param sel Selection
*/
public void setSelection(DBIDSelection sel) {
selection.setSelection(sel);
getHierarchy().resultChanged(selection);
}
/**
* Adds a listener for the <code>DataStoreEvent</code> posted after the
* content changes.
*
* @param l the listener to add
* @see #removeDataStoreListener
*/
public void addDataStoreListener(DataStoreListener l) {
for(int i = 0; i < listenerList.size(); i++) {
if(listenerList.get(i) == l) {
return;
}
}
listenerList.add(l);
}
/**
* Removes a listener previously added with <code>addDataStoreListener</code>.
*
* @param l the listener to remove
* @see #addDataStoreListener
*/
public void removeDataStoreListener(DataStoreListener l) {
listenerList.remove(l);
}
/**
* Proxy datastore event to child listeners.
*/
@Override
public void contentChanged(DataStoreEvent e) {
for(int i = 0; i < listenerList.size(); i++) {
listenerList.get(i).contentChanged(e);
}
}
/**
* Register a result listener.
*
* @param listener Result listener.
*/
public void addResultListener(ResultListener listener) {
getHierarchy().addResultListener(listener);
}
/**
* Remove a result listener.
*
* @param listener Result listener.
*/
public void removeResultListener(ResultListener listener) {
getHierarchy().removeResultListener(listener);
}
/**
* Add a listener.
*
* @param listener Listener to add
*/
public void addVisualizationListener(VisualizationListener listener) {
vistree.addVisualizationListener(listener);
}
/**
* Add a listener.
*
* @param listener Listener to remove
*/
public void removeVisualizationListener(VisualizationListener listener) {
vistree.removeVisualizationListener(listener);
}
@Override
public String getLongName() {
return "Visualizer context";
}
@Override
public String getShortName() {
return "vis-context";
}
/**
* Starting point for visualization, may be {@code null}.
*
* @return Starting point in the result tree, may be {@code null}.
*/
public Result getBaseResult() {
return baseResult;
}
/**
* Add (register) a visualization.
*
* @param parent Parent object
* @param vis Visualization
*/
public void addVis(Object parent, VisualizationItem vis) {
vistree.add(parent, vis);
notifyFactories(vis);
visChanged(vis);
}
/**
* A visualization item has changed.
*
* @param item Item that has changed
*/
public void visChanged(VisualizationItem item) {
vistree.visChanged(item);
}
/**
* Notify factories of a change.
*
* @param item Item that has changed.
*/
private void notifyFactories(Object item) {
for(VisualizationProcessor f : factories) {
try {
f.processNewResult(this, item);
}
catch(Throwable e) {
LOG.warning("VisFactory " + f.getClass().getCanonicalName() + " failed:", e);
}
}
}
public List<VisualizationTask> getVisTasks(VisualizationItem item) {
List<VisualizationTask> out = new ArrayList<>();
vistree.iterDescendants(item).filter(VisualizationTask.class).forEach(out::add);
return out;
}
public VisualizationTree getVisHierarchy() {
return vistree;
}
}