package de.lmu.ifi.dbs.elki.visualization;
/*
This file is part of ELKI:
Environment for Developing KDD-Applications Supported by Index-Structures
Copyright (C) 2015
Ludwig-Maximilians-Universität München
Lehr- und Forschungseinheit für Datenbanksysteme
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 .
*/
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import de.lmu.ifi.dbs.elki.algorithm.DistanceBasedAlgorithm;
import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
import de.lmu.ifi.dbs.elki.database.Database;
import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
import de.lmu.ifi.dbs.elki.database.relation.Relation;
import de.lmu.ifi.dbs.elki.datasource.FileBasedDatabaseConnection;
import de.lmu.ifi.dbs.elki.logging.Logging;
import de.lmu.ifi.dbs.elki.math.random.RandomFactory;
import de.lmu.ifi.dbs.elki.result.Result;
import de.lmu.ifi.dbs.elki.result.ResultHierarchy;
import de.lmu.ifi.dbs.elki.result.ResultUtil;
import de.lmu.ifi.dbs.elki.result.SamplingResult;
import de.lmu.ifi.dbs.elki.result.SettingsResult;
import de.lmu.ifi.dbs.elki.utilities.ClassGenericsUtil;
import de.lmu.ifi.dbs.elki.utilities.ELKIServiceRegistry;
import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.WrongParameterValueException;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.constraints.CommonConstraints;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.MergedParameterization;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.TrackedParameter;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.IntParameter;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Parameter;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.PatternParameter;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.StringParameter;
import de.lmu.ifi.dbs.elki.visualization.style.PropertiesBasedStyleLibrary;
import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
import de.lmu.ifi.dbs.elki.workflow.AlgorithmStep;
/**
* Utility class to determine the visualizers for a result class.
*
* You really should use the parameterization API to configure this
* class. Manually populating the factory collection is cumbersome, and the
* parameterization API takes care of this.
*
* @author Erich Schubert
* @author Remigius Wojdanowski
* @since 0.3
*
* @apiviz.landmark
* @apiviz.has VisualizerContext oneway - - «create»
* @apiviz.uses VisualizationProcessor oneway - n «configure»
*/
public class VisualizerParameterizer {
/**
* Get a logger for this class.
*/
private static final Logging LOG = Logging.getLogger(VisualizerParameterizer.class);
/**
* Default sample size to visualize.
*/
public static final int DEFAULT_SAMPLE_SIZE = 10000;
/**
* Style library to use.
*/
private StyleLibrary stylelib;
/**
* Projections and visualization factories.
*/
private Collection factories;
/**
* Sample size
*/
private int samplesize = -1;
/**
* Random seed for sampling.
*
* FIXME: make parameterizable.
*/
private RandomFactory rnd = RandomFactory.DEFAULT;
/**
* Constructor.
*
* @param samplesize
* @param stylelib Style library
* @param factories Factories to use
*/
public VisualizerParameterizer(int samplesize, StyleLibrary stylelib, Collection factories) {
super();
this.samplesize = samplesize;
this.stylelib = stylelib;
this.factories = factories;
}
/**
* Make a new visualization context
*
* @param hier Result hierarchy
* @param start Starting result
* @return New context
*/
public VisualizerContext newContext(ResultHierarchy hier, Result start) {
Relation> relation = null;
Collection> rels = ResultUtil.filterResults(hier, Relation.class);
for(Relation> rel : rels) {
if(!TypeUtil.DBID.isAssignableFrom(rel.getDataTypeInformation()) && relation == null) {
relation = rel;
}
if(samplesize == 0) {
continue;
}
if(!ResultUtil.filterResults(hier, rel, SamplingResult.class).isEmpty()) {
continue;
}
if(rel.size() > samplesize) {
SamplingResult sample = new SamplingResult(rel);
sample.setSample(DBIDUtil.randomSample(sample.getSample(), samplesize, rnd));
ResultUtil.addChildResult(rel, sample);
}
}
return new VisualizerContext(hier, start, relation, stylelib, factories);
}
/**
* Try to automatically generate a title for this.
*
* @param db Database
* @param result Result object
* @return generated title
*/
public static String getTitle(Database db, Result result) {
List settings = new ArrayList<>();
for(SettingsResult sr : ResultUtil.getSettingsResults(result)) {
settings.addAll(sr.getSettings());
}
String algorithm = null;
String distance = null;
String dataset = null;
for(TrackedParameter setting : settings) {
Parameter> param = setting.getParameter();
OptionID option = param.getOptionID();
String value = param.isDefined() ? param.getValueAsString() : null;
if(option.equals(AlgorithmStep.Parameterizer.ALGORITHM_ID)) {
algorithm = value;
}
if(option.equals(DistanceBasedAlgorithm.DISTANCE_FUNCTION_ID)) {
distance = value;
}
if(option.equals(FileBasedDatabaseConnection.Parameterizer.INPUT_ID)) {
dataset = value;
}
}
StringBuilder buf = new StringBuilder();
if(algorithm != null) {
buf.append(shortenClassname(algorithm.split(",")[0], '.'));
}
if(distance != null) {
if(buf.length() > 0) {
buf.append(" using ");
}
buf.append(shortenClassname(distance, '.'));
}
if(dataset != null) {
if(buf.length() > 0) {
buf.append(" on ");
}
buf.append(shortenClassname(dataset, File.separatorChar));
}
if(buf.length() > 0) {
return buf.toString();
}
return null;
}
/**
* Shorten the class name.
*
* @param nam Class name
* @param c Splitting character
* @return Shortened name
*/
protected static String shortenClassname(String nam, char c) {
final int lastdot = nam.lastIndexOf(c);
if(lastdot >= 0) {
nam = nam.substring(lastdot + 1);
}
return nam;
}
/**
* Parameterization class.
*
* @author Erich Schubert
*
* @apiviz.exclude
*/
public static class Parameterizer extends AbstractParameterizer {
/**
* Parameter to get the style properties file.
*
*
* Key: -visualizer.stylesheet
*
* Default: default properties file
* included stylesheets:
*
* - classic
* - default
* - greyscale
* - neon
* - presentation
* - print
*
* These are {@code *.properties} files in the package
* {@link de.lmu.ifi.dbs.elki.visualization.style}.
*
*/
public static final OptionID STYLELIB_ID = new OptionID("visualizer.stylesheet", "Style properties file to use, included properties: classic, default, greyscale, neon, presentation, print");
/**
* Parameter to enable visualizers
*
*
* Key: -vis.enable
*
* Default: ELKI core
*
*/
public static final OptionID ENABLEVIS_ID = new OptionID("vis.enable", "Visualizers to enable by default.");
/**
* Parameter to set the sampling level
*
*
* Key: -vis.sampling
*
*/
public static final OptionID SAMPLING_ID = new OptionID("vis.sampling", "Maximum number of objects to visualize by default (for performance reasons).");
/**
* Style library
*/
protected StyleLibrary stylelib = null;
/**
* Pattern to enable visualizers
*/
protected Pattern enableVisualizers = null;
/**
* Visualizer factories
*/
protected Collection factories = null;
/**
* Sampling size
*/
protected int samplesize = -1;
@Override
protected void makeOptions(Parameterization config) {
super.makeOptions(config);
IntParameter samplingP = new IntParameter(SAMPLING_ID, DEFAULT_SAMPLE_SIZE) //
.addConstraint(CommonConstraints.GREATER_EQUAL_MINUSONE_INT);
if(config.grab(samplingP)) {
samplesize = samplingP.intValue();
}
StringParameter stylelibP = new StringParameter(STYLELIB_ID, PropertiesBasedStyleLibrary.DEFAULT_SCHEME_FILENAME);
if(config.grab(stylelibP)) {
String filename = stylelibP.getValue();
try {
stylelib = new PropertiesBasedStyleLibrary(filename, filename);
}
catch(AbortException e) {
config.reportError(new WrongParameterValueException(stylelibP, filename, e));
}
}
PatternParameter enablevisP = new PatternParameter(ENABLEVIS_ID) //
.setOptional(true);
if(config.grab(enablevisP)) {
if(!"all".equals(enablevisP.getValueAsString())) {
enableVisualizers = enablevisP.getValue();
}
}
MergedParameterization merged = new MergedParameterization(config);
factories = collectFactorys(merged, enableVisualizers);
}
/**
* Collect and instantiate all visualizer factories.
*
* @param config Parameterization
* @param filter Filter
* @return List of all adapters found.
*/
private static Collection collectFactorys(MergedParameterization config, Pattern filter) {
ArrayList factories = new ArrayList<>();
for(Class> c : ELKIServiceRegistry.findAllImplementations(VisualizationProcessor.class)) {
if(filter != null && !filter.matcher(c.getCanonicalName()).find()) {
continue;
}
try {
config.rewind();
VisualizationProcessor a = ClassGenericsUtil.tryInstantiate(VisualizationProcessor.class, c, config);
factories.add(a);
}
catch(Throwable e) {
if(LOG.isDebugging()) {
LOG.exception("Error instantiating visualization processor " + c.getName(), e.getCause());
}
else {
LOG.warning("Error instantiating visualization processor " + c.getName() + ": " + e.getMessage());
}
}
}
return factories;
}
@Override
protected VisualizerParameterizer makeInstance() {
return new VisualizerParameterizer(samplesize, stylelib, factories);
}
}
}